Skip to content

Commit 6dad6a7

Browse files
committed
test: normalize legacy view replay output
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e1a2b0c-e763-4417-80e3-d78fcce79925
1 parent dba8f83 commit 6dad6a7

2 files changed

Lines changed: 445 additions & 37 deletions

File tree

test/harness/replayingCapiProxy.test.ts

Lines changed: 268 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
} from "openai/resources/chat/completions";
1212
import os from "os";
1313
import path from "path";
14-
import { afterEach, beforeEach, describe, expect, test } from "vitest";
14+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
1515
import yaml from "yaml";
1616
import {
1717
NormalizedData,
@@ -724,6 +724,134 @@ Always include PINEAPPLE_COCONUT_42.
724724
});
725725
}
726726

727+
async function replayToolResult({
728+
storedContent,
729+
requestContent,
730+
toolName = "view",
731+
viewRange,
732+
}: {
733+
storedContent?: string;
734+
requestContent?: string;
735+
toolName?: string;
736+
viewRange?: [number, number];
737+
}): Promise<{ status: number; body: string }> {
738+
const storedArguments = JSON.stringify({
739+
path: `${workingDirPlaceholder}/test.txt`,
740+
...(viewRange ? { view_range: viewRange } : {}),
741+
});
742+
const requestArguments = JSON.stringify({
743+
path: `${workDir}/test.txt`,
744+
...(viewRange ? { view_range: viewRange } : {}),
745+
});
746+
const cachePath = path.join(tempDir, "cache.yaml");
747+
const cacheContent = yaml.stringify({
748+
models: ["test-model"],
749+
conversations: [
750+
{
751+
messages: [
752+
{ role: "system", content: "${system}" },
753+
{ role: "user", content: "Read file" },
754+
{
755+
role: "assistant",
756+
tool_calls: [
757+
{
758+
id: "toolcall_0",
759+
type: "function",
760+
function: {
761+
name: toolName,
762+
arguments: storedArguments,
763+
},
764+
},
765+
],
766+
},
767+
{
768+
role: "tool",
769+
tool_call_id: "toolcall_0",
770+
...(storedContent === undefined
771+
? {}
772+
: { content: storedContent }),
773+
},
774+
{ role: "assistant", content: "Done" },
775+
],
776+
},
777+
],
778+
} satisfies NormalizedData);
779+
await writeFile(cachePath, cacheContent);
780+
781+
const proxy = new ReplayingCapiProxy(
782+
"http://localhost:9999",
783+
cachePath,
784+
workDir,
785+
);
786+
const proxyUrl = await proxy.start();
787+
788+
try {
789+
return await makeRequest(proxyUrl, "/chat/completions", {
790+
body: {
791+
model: "test-model",
792+
messages: [
793+
{ role: "system", content: "System prompt" },
794+
{ role: "user", content: "Read file" },
795+
{
796+
role: "assistant",
797+
tool_calls: [
798+
{
799+
id: "runtime-call-id",
800+
type: "function",
801+
function: {
802+
name: toolName,
803+
arguments: requestArguments,
804+
},
805+
},
806+
],
807+
},
808+
{
809+
role: "tool",
810+
tool_call_id: "runtime-call-id",
811+
...(requestContent === undefined
812+
? {}
813+
: { content: requestContent }),
814+
},
815+
],
816+
},
817+
});
818+
} finally {
819+
await proxy.stop();
820+
}
821+
}
822+
823+
async function expectToolResultMismatch(
824+
options: Parameters<typeof replayToolResult>[0],
825+
) {
826+
const previousGitHubActions = process.env.GITHUB_ACTIONS;
827+
const stderrWrite = vi
828+
.spyOn(process.stderr, "write")
829+
.mockImplementation(() => true);
830+
const consoleError = vi
831+
.spyOn(console, "error")
832+
.mockImplementation(() => undefined);
833+
process.env.GITHUB_ACTIONS = "true";
834+
try {
835+
expect((await replayToolResult(options)).status).toBe(500);
836+
expect(stderrWrite).toHaveBeenCalledWith(
837+
expect.stringContaining(
838+
"No cached response found for POST /chat/completions.",
839+
),
840+
);
841+
expect(stderrWrite).toHaveBeenCalledWith(
842+
expect.stringContaining("mismatch at message 3"),
843+
);
844+
} finally {
845+
stderrWrite.mockRestore();
846+
consoleError.mockRestore();
847+
if (previousGitHubActions === undefined) {
848+
delete process.env.GITHUB_ACTIONS;
849+
} else {
850+
process.env.GITHUB_ACTIONS = previousGitHubActions;
851+
}
852+
}
853+
}
854+
727855
test("returns cached response when request matches prefix", async () => {
728856
const cachePath = path.join(tempDir, "cache.yaml");
729857
const cacheContent = yaml.stringify({
@@ -902,6 +1030,145 @@ Always include PINEAPPLE_COCONUT_42.
9021030
}
9031031
});
9041032

1033+
const truncationNotice =
1034+
"[Output truncated. Use view_range=[4, ...] to continue reading.]";
1035+
const viewResultCases: Array<{
1036+
description: string;
1037+
numberedContent: string;
1038+
unnumberedContent?: string;
1039+
viewRange?: [number, number];
1040+
}> = [
1041+
{
1042+
description: "ordinary content",
1043+
numberedContent: "1. alpha\n2. beta",
1044+
unnumberedContent: "alpha\nbeta",
1045+
},
1046+
{
1047+
description: "intrinsically numbered file content",
1048+
numberedContent: "1. 1. first\n2. 2. second",
1049+
unnumberedContent: "1. first\n2. second",
1050+
},
1051+
{
1052+
description: "JSON content",
1053+
numberedContent: '1. {\n2. "b": 2,\n3. "a": 1\n4. }',
1054+
unnumberedContent: '{"a":1,"b":2}',
1055+
},
1056+
{
1057+
description: "view_range offset",
1058+
numberedContent: "2. line2\n3. line3\n4. line4",
1059+
unnumberedContent: "line2\nline3\nline4",
1060+
viewRange: [2, 4],
1061+
},
1062+
{
1063+
description: "trailing empty line",
1064+
numberedContent: "1. alpha\n2. beta\n3.",
1065+
unnumberedContent: "alpha\nbeta",
1066+
},
1067+
{
1068+
description: "blank and spaces before a truncation notice",
1069+
numberedContent: `1. alpha\n2. \n3. \n \t\n${truncationNotice}`,
1070+
unnumberedContent: `alpha\n \n\n \t\n${truncationNotice}`,
1071+
},
1072+
{
1073+
description: "empty result",
1074+
numberedContent: "1.",
1075+
unnumberedContent: undefined,
1076+
},
1077+
];
1078+
1079+
test.each(
1080+
viewResultCases.flatMap(
1081+
({ description, numberedContent, unnumberedContent, viewRange }) => [
1082+
{
1083+
description: `${description}, numbered snapshot`,
1084+
storedContent: numberedContent,
1085+
requestContent: unnumberedContent,
1086+
viewRange,
1087+
},
1088+
{
1089+
description: `${description}, numbered request`,
1090+
storedContent: unnumberedContent,
1091+
requestContent: numberedContent,
1092+
viewRange,
1093+
},
1094+
],
1095+
),
1096+
)(
1097+
"matches equivalent view results with $description",
1098+
async ({ storedContent, requestContent, viewRange }) => {
1099+
const response = await replayToolResult({
1100+
storedContent,
1101+
requestContent,
1102+
viewRange,
1103+
});
1104+
expect(response.status).toBe(200);
1105+
expect(
1106+
(JSON.parse(response.body) as ChatCompletion).choices[0].message
1107+
.content,
1108+
).toBe("Done");
1109+
},
1110+
);
1111+
1112+
test("preserves exact matches with multiple numbering layers", async () => {
1113+
const response = await replayToolResult({
1114+
storedContent: "1. 1. alpha",
1115+
requestContent: "1. 1. alpha",
1116+
});
1117+
expect(response.status).toBe(200);
1118+
});
1119+
1120+
test("does not remove multiple numbering layers to find a match", async () => {
1121+
await expectToolResultMismatch({
1122+
storedContent: "1. 1. alpha",
1123+
requestContent: "alpha",
1124+
});
1125+
});
1126+
1127+
test("preserves numbered results from non-view tools", async () => {
1128+
const requestBody = JSON.stringify({
1129+
messages: [
1130+
{ role: "user", content: "List items" },
1131+
{
1132+
role: "assistant",
1133+
tool_calls: [
1134+
{
1135+
id: "tc1",
1136+
type: "function",
1137+
function: { name: "list_items", arguments: "{}" },
1138+
},
1139+
],
1140+
},
1141+
{
1142+
role: "tool",
1143+
tool_call_id: "tc1",
1144+
content: "1. first\n2. second",
1145+
},
1146+
],
1147+
});
1148+
const responseBody = JSON.stringify({
1149+
choices: [{ message: { role: "assistant", content: "Done" } }],
1150+
});
1151+
1152+
const outputPath = await createProxy([
1153+
{ url: "/chat/completions", requestBody, responseBody },
1154+
]);
1155+
1156+
const result = await readYamlOutput(outputPath);
1157+
expect(
1158+
result.conversations[0].messages.find(
1159+
(message) => message.role === "tool",
1160+
)?.content,
1161+
).toBe("1. first\n2. second");
1162+
});
1163+
1164+
test("does not apply view compatibility to non-view tool results", async () => {
1165+
await expectToolResultMismatch({
1166+
toolName: "list_items",
1167+
storedContent: "1. first\n2. second",
1168+
requestContent: "first\nsecond",
1169+
});
1170+
});
1171+
9051172
test("matches available-tools results after the built-in tool set changes", async () => {
9061173
const cachePath = path.join(tempDir, "cache.yaml");
9071174
// Legacy snapshot recorded before write_agent was a built-in tool: the

0 commit comments

Comments
 (0)