Skip to content

Commit 6af5d31

Browse files
stephentoubCopilot
andcommitted
Normalize shell completion markers in replay proxy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b6c8b8f commit 6af5d31

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

test/harness/replayingCapiProxy.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,88 @@ Always include PINEAPPLE_COCONUT_42.
745745
}
746746
});
747747

748+
test("matches shell tool results with shell ID completion markers", async () => {
749+
const originalShellConfig =
750+
process.platform === "win32" ? ShellConfig.powerShell : ShellConfig.bash;
751+
const cachePath = path.join(tempDir, "cache.yaml");
752+
const cacheContent = yaml.stringify({
753+
models: ["test-model"],
754+
conversations: [
755+
{
756+
messages: [
757+
{ role: "system", content: "${system}" },
758+
{ role: "user", content: "Run command" },
759+
{
760+
role: "assistant",
761+
tool_calls: [
762+
{
763+
id: "toolcall_0",
764+
type: "function",
765+
function: {
766+
name: "${shell}",
767+
arguments: '{"command":"echo ok"}',
768+
},
769+
},
770+
],
771+
},
772+
{
773+
role: "tool",
774+
tool_call_id: "toolcall_0",
775+
content: "ok\n<exited with exit code 0>",
776+
},
777+
{ role: "assistant", content: "Done" },
778+
],
779+
},
780+
],
781+
} satisfies NormalizedData);
782+
await writeFile(cachePath, cacheContent);
783+
784+
const proxy = new ReplayingCapiProxy(
785+
"http://localhost:9999",
786+
cachePath,
787+
workDir,
788+
);
789+
const proxyUrl = await proxy.start();
790+
791+
try {
792+
const response = await makeRequest(proxyUrl, "/chat/completions", {
793+
body: {
794+
model: "test-model",
795+
messages: [
796+
{ role: "system", content: "System prompt" },
797+
{ role: "user", content: "Run command" },
798+
{
799+
role: "assistant",
800+
tool_calls: [
801+
{
802+
id: "runtime-call-id",
803+
type: "function",
804+
function: {
805+
name: originalShellConfig.shellToolName,
806+
arguments: '{"command":"echo ok"}',
807+
},
808+
},
809+
],
810+
},
811+
{
812+
role: "tool",
813+
tool_call_id: "runtime-call-id",
814+
content: "ok\n<shellId: 42 completed with exit code 0>",
815+
},
816+
],
817+
},
818+
});
819+
820+
expect(response.status).toBe(200);
821+
expect(
822+
(JSON.parse(response.body) as ChatCompletion).choices[0].message
823+
.content,
824+
).toBe("Done");
825+
} finally {
826+
await proxy.stop();
827+
}
828+
});
829+
748830
test("expands workdir placeholder in cached response", async () => {
749831
const cachePath = path.join(tempDir, "cache.yaml");
750832
const cacheContent = yaml.stringify({

test/harness/replayingCapiProxy.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy {
5454
private startPromise: Promise<string> | null = null;
5555
private defaultToolResultNormalizers: ToolResultNormalizer[] = [
5656
{ toolName: "*", normalizer: normalizeLargeOutputFilepaths },
57+
{ toolName: "${shell}", normalizer: normalizeShellExitMarkers },
5758
{ toolName: "*", normalizer: normalizeGhAuthMessages },
5859
{ toolName: "read_agent", normalizer: normalizeReadAgentTimings },
5960
];
@@ -1087,6 +1088,13 @@ function normalizeLargeOutputFilepaths(result: string): string {
10871088
);
10881089
}
10891090

1091+
function normalizeShellExitMarkers(result: string): string {
1092+
return result.replace(
1093+
/<shellId:\s*[^>\r\n]+?\s+completed with exit code (-?\d+)>/g,
1094+
"<exited with exit code $1>",
1095+
);
1096+
}
1097+
10901098
// The `gh` CLI emits different "not authenticated" help text depending on the
10911099
// environment (local dev vs. inside GitHub Actions). Normalize both forms to a
10921100
// stable placeholder so snapshots don't drift between environments.

0 commit comments

Comments
 (0)