Skip to content

Commit 6f45564

Browse files
committed
refactor
1 parent 00f11ab commit 6f45564

File tree

8 files changed

+549
-111
lines changed

8 files changed

+549
-111
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { test, afterEach, expect, describe, setDefaultTimeout } from "bun:test";
2+
import path from "path";
3+
import {
4+
execContainer,
5+
findResourceInstance,
6+
removeContainer,
7+
runContainer,
8+
runTerraformApply,
9+
writeCoder,
10+
writeFileContainer,
11+
} from "~test";
12+
13+
let cleanupFunctions: (() => Promise<void>)[] = [];
14+
15+
const registerCleanup = (cleanup: () => Promise<void>) => {
16+
cleanupFunctions.push(cleanup);
17+
};
18+
19+
// Cleanup logic depends on the fact that bun's built-in test runner
20+
// runs tests sequentially.
21+
// https://bun.sh/docs/test/discovery#execution-order
22+
// Weird things would happen if tried to run tests in parallel.
23+
// One test could clean up resources that another test was still using.
24+
afterEach(async () => {
25+
// reverse the cleanup functions so that they are run in the correct order
26+
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
27+
cleanupFunctions = [];
28+
for (const cleanup of cleanupFnsCopy) {
29+
try {
30+
await cleanup();
31+
} catch (error) {
32+
console.error("Error during cleanup:", error);
33+
}
34+
}
35+
});
36+
37+
const setupContainer = async ({
38+
image,
39+
vars,
40+
}: {
41+
image?: string;
42+
vars?: Record<string, string>;
43+
} = {}) => {
44+
const state = await runTerraformApply(import.meta.dir, {
45+
agent_id: "foo",
46+
...vars,
47+
});
48+
const coderScript = findResourceInstance(state, "coder_script");
49+
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
50+
registerCleanup(() => removeContainer(id));
51+
return { id, coderScript };
52+
};
53+
54+
const loadTestFile = async (...relativePath: string[]) => {
55+
return await Bun.file(
56+
path.join(import.meta.dir, "testdata", ...relativePath),
57+
).text();
58+
};
59+
60+
const writeExecutable = async ({
61+
containerId,
62+
filePath,
63+
content,
64+
}: {
65+
containerId: string;
66+
filePath: string;
67+
content: string;
68+
}) => {
69+
await writeFileContainer(containerId, filePath, content, {
70+
user: "root",
71+
});
72+
await execContainer(
73+
containerId,
74+
["bash", "-c", `chmod 755 ${filePath}`],
75+
["--user", "root"],
76+
);
77+
};
78+
79+
const writeAgentAPIMockControl = async ({
80+
containerId,
81+
content,
82+
}: {
83+
containerId: string;
84+
content: string;
85+
}) => {
86+
await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, {
87+
user: "coder",
88+
});
89+
};
90+
91+
interface SetupProps {
92+
skipAgentAPIMock?: boolean;
93+
skipClaudeMock?: boolean;
94+
}
95+
96+
const projectDir = "/home/coder/project";
97+
98+
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
99+
const { id, coderScript } = await setupContainer({
100+
vars: {
101+
experiment_report_tasks: "true",
102+
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
103+
install_claude_code: "false",
104+
agentapi_version: "preview",
105+
folder: projectDir,
106+
},
107+
});
108+
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
109+
// the module script assumes that there is a coder executable in the PATH
110+
await writeCoder(id, "#!/bin/bash\necho ok");
111+
if (!props?.skipAgentAPIMock) {
112+
await writeExecutable({
113+
containerId: id,
114+
filePath: "/usr/bin/agentapi",
115+
content: await loadTestFile("agentapi-mock.js"),
116+
});
117+
}
118+
if (!props?.skipClaudeMock) {
119+
await writeExecutable({
120+
containerId: id,
121+
filePath: "/usr/bin/claude",
122+
content: await loadTestFile("claude-mock.js"),
123+
});
124+
}
125+
await writeExecutable({
126+
containerId: id,
127+
filePath: "/home/coder/script.sh",
128+
content: coderScript.script,
129+
});
130+
return { id };
131+
};
132+
133+
const expectAgentAPIStarted = async (id: string) => {
134+
const resp = await execContainer(id, [
135+
"bash",
136+
"-c",
137+
"set -e; set -o pipefail; curl http://localhost:3284/status | grep -q stable",
138+
]);
139+
if (resp.exitCode !== 0) {
140+
console.log(resp.stdout);
141+
console.log(resp.stderr);
142+
}
143+
expect(resp.exitCode).toBe(0);
144+
};
145+
146+
const execModuleScript = async (id: string) => {
147+
const resp = await execContainer(id, [
148+
"bash",
149+
"-c",
150+
`cd /home/coder && ./script.sh`,
151+
]);
152+
if (resp.exitCode !== 0) {
153+
console.log(resp.stdout);
154+
console.log(resp.stderr);
155+
}
156+
return resp;
157+
};
158+
159+
// increase the default timeout to 60 seconds
160+
setDefaultTimeout(60 * 1000);
161+
162+
// we don't run these tests in CI because they take too long and make network
163+
// calls. they are dedicated for local development.
164+
describe.skipIf(process.env.CI !== undefined)("claude-code", () => {
165+
// test that the script runs successfully if claude starts without any errors
166+
test("happy-path", async () => {
167+
const { id } = await setup();
168+
169+
const resp = await execContainer(id, [
170+
"bash",
171+
"-c",
172+
"sudo /home/coder/script.sh",
173+
]);
174+
expect(resp.exitCode).toBe(0);
175+
176+
await expectAgentAPIStarted(id);
177+
});
178+
179+
// test that the script removes lastSessionId from the .claude.json file
180+
test("last-session-id-removed", async () => {
181+
const { id } = await setup();
182+
183+
await writeFileContainer(
184+
id,
185+
"/home/coder/.claude.json",
186+
JSON.stringify({
187+
projects: {
188+
[projectDir]: {
189+
lastSessionId: "123",
190+
},
191+
},
192+
}),
193+
);
194+
195+
const catResp = await execContainer(id, [
196+
"bash",
197+
"-c",
198+
"cat /home/coder/.claude.json",
199+
]);
200+
expect(catResp.exitCode).toBe(0);
201+
expect(catResp.stdout).toContain("lastSessionId");
202+
203+
const respModuleScript = await execModuleScript(id);
204+
expect(respModuleScript.exitCode).toBe(0);
205+
206+
await expectAgentAPIStarted(id);
207+
208+
const catResp2 = await execContainer(id, [
209+
"bash",
210+
"-c",
211+
"cat /home/coder/.claude.json",
212+
]);
213+
expect(catResp2.exitCode).toBe(0);
214+
expect(catResp2.stdout).not.toContain("lastSessionId");
215+
});
216+
217+
// test that the script handles a .claude.json file that doesn't contain
218+
// a lastSessionId field
219+
test("last-session-id-not-found", async () => {
220+
const { id } = await setup();
221+
222+
await writeFileContainer(
223+
id,
224+
"/home/coder/.claude.json",
225+
JSON.stringify({
226+
projects: {
227+
"/home/coder": {},
228+
},
229+
}),
230+
);
231+
232+
const respModuleScript = await execModuleScript(id);
233+
expect(respModuleScript.exitCode).toBe(0);
234+
235+
await expectAgentAPIStarted(id);
236+
237+
const catResp = await execContainer(id, [
238+
"bash",
239+
"-c",
240+
"cat /home/coder/.claude-module/agentapi-start.log",
241+
]);
242+
expect(catResp.exitCode).toBe(0);
243+
expect(catResp.stdout).toContain(
244+
"No lastSessionId found in .claude.json - nothing to do",
245+
);
246+
});
247+
248+
// test that if claude fails to run with the --continue flag and returns a
249+
// no conversation found error, then the module script retries without the flag
250+
test("no-conversation-found", async () => {
251+
const { id } = await setup();
252+
await writeAgentAPIMockControl({
253+
containerId: id,
254+
content: "no-conversation-found",
255+
});
256+
// check that mocking works
257+
const respAgentAPI = await execContainer(id, [
258+
"bash",
259+
"-c",
260+
"agentapi --continue",
261+
]);
262+
expect(respAgentAPI.exitCode).toBe(1);
263+
expect(respAgentAPI.stderr).toContain("No conversation found to continue");
264+
265+
const respModuleScript = await execModuleScript(id);
266+
expect(respModuleScript.exitCode).toBe(0);
267+
268+
await expectAgentAPIStarted(id);
269+
});
270+
271+
test("install-agentapi", async () => {
272+
const { id } = await setup({ skipAgentAPIMock: true });
273+
274+
const respModuleScript = await execModuleScript(id);
275+
expect(respModuleScript.exitCode).toBe(0);
276+
277+
await expectAgentAPIStarted(id);
278+
const respAgentAPI = await execContainer(id, [
279+
"bash",
280+
"-c",
281+
"agentapi --version",
282+
]);
283+
expect(respAgentAPI.exitCode).toBe(0);
284+
});
285+
});

0 commit comments

Comments
 (0)