Skip to content

Commit 400dae4

Browse files
refactor: clean up proxy match-failure diagnostics for long-term use
Extract diagnoseMatchFailure() as a pure function that mirrors the matching logic in findAssistantIndexAfterPrefix, producing clear per-conversation explanations of why matching failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 924ebbd commit 400dae4

1 file changed

Lines changed: 77 additions & 45 deletions

File tree

test/harness/replayingCapiProxy.ts

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -376,68 +376,100 @@ async function writeCapturesToDisk(
376376
}
377377
}
378378

379+
/**
380+
* Produces a human-readable explanation of why no stored conversation matched
381+
* a given request. For each stored conversation it reports the first reason
382+
* matching failed, mirroring the logic in {@link findAssistantIndexAfterPrefix}.
383+
*/
384+
function diagnoseMatchFailure(
385+
requestMessages: NormalizedMessage[],
386+
rawMessages: unknown[],
387+
storedData: NormalizedData | undefined,
388+
): string {
389+
const lines: string[] = [];
390+
lines.push(`Request has ${requestMessages.length} normalized messages (${rawMessages.length} raw).`);
391+
392+
if (!storedData || storedData.conversations.length === 0) {
393+
lines.push("No stored conversations to match against.");
394+
return lines.join("\n");
395+
}
396+
397+
for (let c = 0; c < storedData.conversations.length; c++) {
398+
const saved = storedData.conversations[c].messages;
399+
400+
// Same check as findAssistantIndexAfterPrefix: request must be a strict prefix
401+
if (requestMessages.length >= saved.length) {
402+
lines.push(
403+
`Conversation ${c} (${saved.length} messages): ` +
404+
`skipped — request has ${requestMessages.length} messages, need fewer than ${saved.length}.`,
405+
);
406+
continue;
407+
}
408+
409+
// Find the first message that doesn't match
410+
let mismatchIndex = -1;
411+
for (let i = 0; i < requestMessages.length; i++) {
412+
if (JSON.stringify(requestMessages[i]) !== JSON.stringify(saved[i])) {
413+
mismatchIndex = i;
414+
break;
415+
}
416+
}
417+
418+
if (mismatchIndex >= 0) {
419+
const raw = mismatchIndex < rawMessages.length
420+
? JSON.stringify(rawMessages[mismatchIndex]).slice(0, 300)
421+
: "(no raw message)";
422+
lines.push(
423+
`Conversation ${c} (${saved.length} messages): mismatch at message ${mismatchIndex}:`,
424+
` request: ${JSON.stringify(requestMessages[mismatchIndex]).slice(0, 200)}`,
425+
` saved: ${JSON.stringify(saved[mismatchIndex]).slice(0, 200)}`,
426+
` raw (pre-normalization): ${raw}`,
427+
);
428+
} else {
429+
// Prefix matched, but the next saved message isn't an assistant turn
430+
const nextRole = saved[requestMessages.length]?.role ?? "(end of conversation)";
431+
lines.push(
432+
`Conversation ${c} (${saved.length} messages): ` +
433+
`prefix matched, but next saved message is "${nextRole}" (need "assistant").`,
434+
);
435+
}
436+
}
437+
438+
return lines.join("\n");
439+
}
440+
379441
async function exitWithNoMatchingRequestError(
380442
options: PerformRequestOptions,
381443
testInfo: { file: string; line?: number } | undefined,
382444
workDir: string,
383445
toolResultNormalizers: ToolResultNormalizer[],
384446
storedData?: NormalizedData,
385447
) {
386-
const parts: string[] = [];
387-
if (testInfo?.file) parts.push(`file=${testInfo.file}`);
388-
if (typeof testInfo?.line === "number") parts.push(`line=${testInfo.line}`);
389-
const header = parts.length ? ` ${parts.join(",")}` : "";
390-
391-
let diagnostics = "";
448+
let diagnostics: string;
392449
try {
393-
const normalized = await parseAndNormalizeRequest(
394-
options.body,
395-
workDir,
396-
toolResultNormalizers,
397-
);
450+
const normalized = await parseAndNormalizeRequest(options.body, workDir, toolResultNormalizers);
398451
const requestMessages = normalized.conversations[0]?.messages ?? [];
399452

400-
// Also parse raw messages to see what normalization drops
401453
let rawMessages: unknown[] = [];
402454
try {
403-
const parsed = JSON.parse(options.body ?? "{}") as { messages?: unknown[] };
404-
rawMessages = parsed.messages ?? [];
405-
} catch { /* ignore */ }
406-
407-
diagnostics += `Request has ${requestMessages.length} normalized messages (${rawMessages.length} raw).\n`;
408-
409-
if (storedData) {
410-
for (let c = 0; c < storedData.conversations.length; c++) {
411-
const saved = storedData.conversations[c].messages;
412-
diagnostics += `Conversation ${c} has ${saved.length} messages. `;
413-
if (requestMessages.length >= saved.length) {
414-
diagnostics += `Skipped: request (${requestMessages.length}) >= saved (${saved.length}).\n`;
415-
continue;
416-
}
417-
let mismatchAt = -1;
418-
for (let i = 0; i < requestMessages.length; i++) {
419-
const reqMsg = JSON.stringify(requestMessages[i]);
420-
const savedMsg = JSON.stringify(saved[i]);
421-
if (reqMsg !== savedMsg) {
422-
mismatchAt = i;
423-
const rawMsg = i < rawMessages.length ? JSON.stringify(rawMessages[i]).slice(0, 300) : "(no raw)";
424-
diagnostics += `Mismatch at message ${i}:\n normalized: ${reqMsg.slice(0, 200)}\n saved: ${savedMsg.slice(0, 200)}\n raw: ${rawMsg}\n`;
425-
break;
426-
}
427-
}
428-
if (mismatchAt === -1) {
429-
const nextRole = saved[requestMessages.length]?.role;
430-
diagnostics += `Prefix matched but next message role is "${nextRole}" (need "assistant").\n`;
431-
}
432-
}
433-
}
455+
rawMessages = (JSON.parse(options.body ?? "{}") as { messages?: unknown[] }).messages ?? [];
456+
} catch { /* non-JSON body */ }
457+
458+
diagnostics = diagnoseMatchFailure(requestMessages, rawMessages, storedData);
434459
} catch (e) {
435-
diagnostics = `(unable to parse request: ${e})`;
460+
diagnostics = `(unable to parse request for diagnostics: ${e})`;
436461
}
437462

438463
const errorMessage =
439464
`No cached response found for ${options.requestOptions.method} ${options.requestOptions.path}.\n${diagnostics}`;
440-
process.stderr.write(`::error${header}::${errorMessage}\n`);
465+
466+
// Format as GitHub Actions annotation when test location is available
467+
const annotation = [
468+
testInfo?.file ? `file=${testInfo.file}` : "",
469+
typeof testInfo?.line === "number" ? `line=${testInfo.line}` : "",
470+
].filter(Boolean).join(",");
471+
process.stderr.write(`::error${annotation ? ` ${annotation}` : ""}::${errorMessage}\n`);
472+
441473
options.onError(new Error(errorMessage));
442474
}
443475

0 commit comments

Comments
 (0)