Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions test/harness/replayingCapiProxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,88 @@ Always include PINEAPPLE_COCONUT_42.
}
});

test("matches shell tool results with shell ID completion markers", async () => {
const originalShellConfig =
process.platform === "win32" ? ShellConfig.powerShell : ShellConfig.bash;
const cachePath = path.join(tempDir, "cache.yaml");
const cacheContent = yaml.stringify({
models: ["test-model"],
conversations: [
{
messages: [
{ role: "system", content: "${system}" },
{ role: "user", content: "Run command" },
{
role: "assistant",
tool_calls: [
{
id: "toolcall_0",
type: "function",
function: {
name: "${shell}",
arguments: '{"command":"echo ok"}',
},
},
],
},
{
role: "tool",
tool_call_id: "toolcall_0",
content: "ok\n<exited with exit code 0>",
},
{ role: "assistant", content: "Done" },
],
},
],
} satisfies NormalizedData);
await writeFile(cachePath, cacheContent);

const proxy = new ReplayingCapiProxy(
"http://localhost:9999",
cachePath,
workDir,
);
const proxyUrl = await proxy.start();

try {
const response = await makeRequest(proxyUrl, "/chat/completions", {
body: {
model: "test-model",
messages: [
{ role: "system", content: "System prompt" },
{ role: "user", content: "Run command" },
{
role: "assistant",
tool_calls: [
{
id: "runtime-call-id",
type: "function",
function: {
name: originalShellConfig.shellToolName,
arguments: '{"command":"echo ok"}',
},
},
],
},
{
role: "tool",
tool_call_id: "runtime-call-id",
content: "ok\n<shellId: 42 completed with exit code 0>",
},
],
},
});

expect(response.status).toBe(200);
expect(
(JSON.parse(response.body) as ChatCompletion).choices[0].message
.content,
).toBe("Done");
} finally {
await proxy.stop();
}
});

test("expands workdir placeholder in cached response", async () => {
const cachePath = path.join(tempDir, "cache.yaml");
const cacheContent = yaml.stringify({
Expand Down
8 changes: 8 additions & 0 deletions test/harness/replayingCapiProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy {
private startPromise: Promise<string> | null = null;
private defaultToolResultNormalizers: ToolResultNormalizer[] = [
{ toolName: "*", normalizer: normalizeLargeOutputFilepaths },
{ toolName: "${shell}", normalizer: normalizeShellExitMarkers },
{ toolName: "*", normalizer: normalizeGhAuthMessages },
{ toolName: "read_agent", normalizer: normalizeReadAgentTimings },
];
Expand Down Expand Up @@ -1087,6 +1088,13 @@ function normalizeLargeOutputFilepaths(result: string): string {
);
}

function normalizeShellExitMarkers(result: string): string {
return result.replace(
/<shellId:\s*[^>\r\n]+?\s+completed with exit code (-?\d+)>/g,
"<exited with exit code $1>",
);
}

// The `gh` CLI emits different "not authenticated" help text depending on the
// environment (local dev vs. inside GitHub Actions). Normalize both forms to a
// stable placeholder so snapshots don't drift between environments.
Expand Down
Loading