test: run the posix-only ai agent tests on linux only

This commit is contained in:
LukeGus committed 2026-10-03 16:12:38 -05:00
1 parent bf817d8bd1
commit 7305a44b19
3 files changed
+170 -143

No files matched your search

+76 -69
View File
@@ -12,6 +12,9 @@ import {
import { providerPath } from "../../../src/backend/agents/routes.js"; import { providerPath } from "../../../src/backend/agents/routes.js";
import { REMOTE_RUNNER } from "../../../src/backend/agents/remote-runner.js"; import { REMOTE_RUNNER } from "../../../src/backend/agents/remote-runner.js";
// These run sh, git and agent binaries the way a remote Linux host does.
const posix = it.skipIf(process.platform === "win32");
describe("remote agent boundaries", () => { describe("remote agent boundaries", () => {
it("rejects shell syntax and invalid targets", () => { it("rejects shell syntax and invalid targets", () => {
const start = { const start = {
@@ -78,74 +81,78 @@ if(args[0]==='serve') {
}); });
`; `;
for (const agent of ["pi", "claude", "codex", "opencode"] as const) { for (const agent of ["pi", "claude", "codex", "opencode"] as const) {
it(`${agent}: native protocol streams a turn and returns to ready`, async () => { posix(
const dir = await mkdtemp(join(tmpdir(), "termix-agent-test-")); `${agent}: native protocol streams a turn and returns to ready`,
const executable = join(dir, "fake-agent"); async () => {
const image = join(dir, "image.png"); const dir = await mkdtemp(join(tmpdir(), "termix-agent-test-"));
await writeFile(image, "image"); const executable = join(dir, "fake-agent");
await writeFile(executable, fake, { mode: 0o700 }); const image = join(dir, "image.png");
const child = spawn(process.execPath, ["-e", REMOTE_RUNNER], { await writeFile(image, "image");
env: { ...process.env, HOME: dir }, await writeFile(executable, fake, { mode: 0o700 });
stdio: ["pipe", "pipe", "pipe"], const child = spawn(process.execPath, ["-e", REMOTE_RUNNER], {
}); env: { ...process.env, HOME: dir },
const events: Record<string, unknown>[] = []; stdio: ["pipe", "pipe", "pipe"],
let stderr = "";
child.stderr.on("data", (d) => (stderr += d));
const send = (m: unknown) => child.stdin.write(JSON.stringify(m) + "\n");
try {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(
() => reject(Error("timeout " + stderr + JSON.stringify(events))),
12000,
);
let prompted = false;
createInterface({ input: child.stdout }).on("line", (line) => {
const m = JSON.parse(line);
events.push(m);
if (m.kind === "error") {
clearTimeout(timer);
reject(Error(m.text));
}
if (m.kind === "permission")
send({ type: "answer", requestId: m.requestId, allow: true });
if (m.kind === "status" && m.text === "ready") {
if (!prompted) {
prompted = true;
send({
type: "prompt",
text: "hello",
attachments: [{ path: image, mime: "image/png" }],
});
} else {
clearTimeout(timer);
resolve();
}
}
});
send({
type: "start",
config: {
agent,
id: "test-session",
cwd: dir,
executable,
model: "test-model",
proxyUrl: "http://127.0.0.1:1",
token: "temporary-token",
providerType:
agent === "claude" ? "anthropic" : "openai_compatible",
},
});
}); });
expect( const events: Record<string, unknown>[] = [];
events.some((e) => e.kind === "text" && e.text === "verified"), let stderr = "";
).toBe(true); child.stderr.on("data", (d) => (stderr += d));
if (agent === "claude") const send = (m: unknown) => child.stdin.write(JSON.stringify(m) + "\n");
expect(events.some((e) => e.kind === "permission")).toBe(true); try {
} finally { await new Promise<void>((resolve, reject) => {
send({ type: "stop" }); const timer = setTimeout(
await new Promise((resolve) => child.once("exit", resolve)); () => reject(Error("timeout " + stderr + JSON.stringify(events))),
await rm(dir, { recursive: true, force: true }); 12000,
} );
}, 15000); let prompted = false;
createInterface({ input: child.stdout }).on("line", (line) => {
const m = JSON.parse(line);
events.push(m);
if (m.kind === "error") {
clearTimeout(timer);
reject(Error(m.text));
}
if (m.kind === "permission")
send({ type: "answer", requestId: m.requestId, allow: true });
if (m.kind === "status" && m.text === "ready") {
if (!prompted) {
prompted = true;
send({
type: "prompt",
text: "hello",
attachments: [{ path: image, mime: "image/png" }],
});
} else {
clearTimeout(timer);
resolve();
}
}
});
send({
type: "start",
config: {
agent,
id: "test-session",
cwd: dir,
executable,
model: "test-model",
proxyUrl: "http://127.0.0.1:1",
token: "temporary-token",
providerType:
agent === "claude" ? "anthropic" : "openai_compatible",
},
});
});
expect(
events.some((e) => e.kind === "text" && e.text === "verified"),
).toBe(true);
if (agent === "claude")
expect(events.some((e) => e.kind === "permission")).toBe(true);
} finally {
send({ type: "stop" });
await new Promise((resolve) => child.once("exit", resolve));
await rm(dir, { recursive: true, force: true });
}
},
15000,
);
} }
@@ -8,6 +8,9 @@ import {
forwardingScript, forwardingScript,
} from "../../../src/backend/agents/forwarding.js"; } from "../../../src/backend/agents/forwarding.js";
// These run sh, git and agent binaries the way a remote Linux host does.
const posix = it.skipIf(process.platform === "win32");
async function fixture(mode = "no") { async function fixture(mode = "no") {
const dir = await mkdtemp(join(tmpdir(), "termix-forwarding-")); const dir = await mkdtemp(join(tmpdir(), "termix-forwarding-"));
const bin = join(dir, "bin"); const bin = join(dir, "bin");
@@ -53,22 +56,25 @@ exit 0
}; };
} }
it("adds a scoped rule before existing Match blocks and is idempotent", async () => { posix(
const f = await fixture(); "adds a scoped rule before existing Match blocks and is idempotent",
try { async () => {
expect(f.run()).toContain("Enabled loopback remote forwarding"); const f = await fixture();
const config = await f.config(); try {
expect(config).toContain( expect(f.run()).toContain("Enabled loopback remote forwarding");
"AllowTcpForwarding no\n# BEGIN Termix Agent root 10.1.1.35\nMatch User root Address 10.1.1.35\n AllowTcpForwarding remote\n PermitListen 127.0.0.1:*\n GatewayPorts no", const config = await f.config();
); expect(config).toContain(
expect(config).toContain("Match Address 10.1.1.3\n DenyUsers root"); "AllowTcpForwarding no\n# BEGIN Termix Agent root 10.1.1.35\nMatch User root Address 10.1.1.35\n AllowTcpForwarding remote\n PermitListen 127.0.0.1:*\n GatewayPorts no",
expect(f.run()).toContain("already allowed"); );
expect(await f.config()).toBe(config); expect(config).toContain("Match Address 10.1.1.3\n DenyUsers root");
} finally { expect(f.run()).toContain("already allowed");
await f.close(); expect(await f.config()).toBe(config);
} } finally {
}); await f.close();
it("preserves existing local forwarding", async () => { }
},
);
posix("preserves existing local forwarding", async () => {
const f = await fixture("local"); const f = await fixture("local");
try { try {
f.run(); f.run();
@@ -11,6 +11,9 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { spawnSync, execFileSync } from "node:child_process"; import { spawnSync, execFileSync } from "node:child_process";
import { REMOTE_WORKSPACE } from "../../../src/backend/agents/workspace.js"; import { REMOTE_WORKSPACE } from "../../../src/backend/agents/workspace.js";
// These run sh, git and agent binaries the way a remote Linux host does.
const posix = it.skipIf(process.platform === "win32");
let home: string, cwd: string; let home: string, cwd: string;
beforeEach(() => { beforeEach(() => {
home = mkdtempSync(join(tmpdir(), "termix-workspace-")); home = mkdtempSync(join(tmpdir(), "termix-workspace-"));
@@ -34,61 +37,72 @@ function git(...args: string[]) {
stdio: ["pipe", "pipe", "pipe"], stdio: ["pipe", "pipe", "pipe"],
}); });
} }
it("uploads an image without trusting the caller's MIME type and preserves exact bytes", () => { posix(
const data = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 1, 2]); "uploads an image without trusting the caller's MIME type and preserves exact bytes",
const a = run({ () => {
operation: "upload", const data = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 1, 2]);
name: "image.png", const a = run({
data: data.toString("base64"), operation: "upload",
}); name: "image.png",
expect(a.mime).toBe("image/png"); data: data.toString("base64"),
expect(readFileSync(a.path)).toEqual(data); });
expect( expect(a.mime).toBe("image/png");
a.path.startsWith( expect(readFileSync(a.path)).toEqual(data);
join(home, ".local/state/termix-agents/test/attachments"), expect(
), a.path.startsWith(
).toBe(true); join(home, ".local/state/termix-agents/test/attachments"),
}); ),
it("rejects oversized files, directories and references outside the workspace", () => { ).toBe(true);
writeFileSync(join(home, "outside"), "secret"); },
symlinkSync(join(home, "outside"), join(cwd, "escape")); );
writeFileSync(join(cwd, "large"), Buffer.alloc(1024 * 1024 + 1)); posix(
for (const path of ["../outside", "escape", "large", "."]) "rejects oversized files, directories and references outside the workspace",
expect(() => run({ operation: "reference", path })).toThrow(); () => {
}); writeFileSync(join(home, "outside"), "secret");
it("shows staged, unstaged and untracked changes and creates an isolated branch", () => { symlinkSync(join(home, "outside"), join(cwd, "escape"));
git("init"); writeFileSync(join(cwd, "large"), Buffer.alloc(1024 * 1024 + 1));
git("config", "user.name", "Test"); for (const path of ["../outside", "escape", "large", "."])
git("config", "user.email", "test@example.com"); expect(() => run({ operation: "reference", path })).toThrow();
mkdirSync(join(cwd, "nested")); },
writeFileSync(join(cwd, "nested", "deleted.txt"), "nested base\n"); );
writeFileSync(join(cwd, "file.txt"), "base\n"); posix(
git("add", "."); "shows staged, unstaged and untracked changes and creates an isolated branch",
git("commit", "-m", "initial"); () => {
writeFileSync(join(cwd, "file.txt"), "staged\n"); git("init");
git("add", "."); git("config", "user.name", "Test");
writeFileSync(join(cwd, "file.txt"), "working\n"); git("config", "user.email", "test@example.com");
writeFileSync(join(cwd, "new.txt"), "untracked"); mkdirSync(join(cwd, "nested"));
expect(run({ operation: "status" }).files).toContainEqual({ writeFileSync(join(cwd, "nested", "deleted.txt"), "nested base\n");
status: "??", writeFileSync(join(cwd, "file.txt"), "base\n");
path: "new.txt", git("add", ".");
}); git("commit", "-m", "initial");
const diff = run({ operation: "diff", path: "file.txt" }).diff; writeFileSync(join(cwd, "file.txt"), "staged\n");
expect(diff).toContain("+working"); git("add", ".");
expect(diff).toContain("+staged"); writeFileSync(join(cwd, "file.txt"), "working\n");
expect(run({ operation: "diff", path: "new.txt" }).diff).toContain( writeFileSync(join(cwd, "new.txt"), "untracked");
"+untracked", expect(run({ operation: "status" }).files).toContainEqual({
); status: "??",
const wt = run({ operation: "worktree", branch: "termix/test" }); path: "new.txt",
expect(readFileSync(join(wt.path, "file.txt"), "utf8")).toBe("base\n"); });
expect(readFileSync(join(cwd, "file.txt"), "utf8")).toBe("working\n"); const diff = run({ operation: "diff", path: "file.txt" }).diff;
expect(() => run({ operation: "worktree", branch: "termix/test" })).toThrow(); expect(diff).toContain("+working");
rmSync(join(cwd, "nested"), { recursive: true }); expect(diff).toContain("+staged");
expect(run({ operation: "diff", path: "nested/deleted.txt" }).diff).toContain( expect(run({ operation: "diff", path: "new.txt" }).diff).toContain(
"-nested base", "+untracked",
); );
rmSync(join(cwd, "file.txt")); const wt = run({ operation: "worktree", branch: "termix/test" });
expect(run({ operation: "diff", path: "file.txt" }).diff).toContain( expect(readFileSync(join(wt.path, "file.txt"), "utf8")).toBe("base\n");
"-staged", expect(readFileSync(join(cwd, "file.txt"), "utf8")).toBe("working\n");
); expect(() =>
}); run({ operation: "worktree", branch: "termix/test" }),
).toThrow();
rmSync(join(cwd, "nested"), { recursive: true });
expect(
run({ operation: "diff", path: "nested/deleted.txt" }).diff,
).toContain("-nested base");
rmSync(join(cwd, "file.txt"));
expect(run({ operation: "diff", path: "file.txt" }).diff).toContain(
"-staged",
);
},
);