-
Notifications
You must be signed in to change notification settings - Fork 916
Fail CI if snapshots aren't present #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
7a9112c
a0b15a0
25efe9d
7fc8acb
7ec2598
93c29af
812ffd2
fe5e3e5
ae47a94
353ebbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,12 @@ const normalizedToolNames = { | |
| [shellConfig.writeShellToolName]: "${write_shell}", | ||
| }; | ||
|
|
||
| /** | ||
| * Default model to use when no stored data is available for a given test. | ||
| * This enables responding to /models without needing to have a capture file. | ||
| */ | ||
| const defaultModel = "claude-sonnet-4.5"; | ||
|
|
||
| /** | ||
| * An HTTP proxy that not only captures HTTP exchanges, but also stores them in a file on disk and | ||
| * replays the stored responses on subsequent runs. | ||
|
|
@@ -149,7 +155,9 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { | |
| options.requestOptions.path?.startsWith("/stop") && | ||
| options.requestOptions.method === "POST" | ||
| ) { | ||
| const skipWritingCache = options.requestOptions.path.includes("skipWritingCache=true"); | ||
| const skipWritingCache = options.requestOptions.path.includes( | ||
| "skipWritingCache=true", | ||
| ); | ||
| options.onResponseStart(200, {}); | ||
| options.onResponseEnd(); | ||
| await this.stop(skipWritingCache); | ||
|
|
@@ -184,13 +192,13 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { | |
| } | ||
|
|
||
| // Handle /models endpoint | ||
| if ( | ||
| options.requestOptions.path === "/models" && | ||
| state.storedData?.models.length | ||
| ) { | ||
| const modelsResponse = createGetModelsResponse( | ||
| state.storedData.models, | ||
| ); | ||
| // Use stored models if available, otherwise use default model | ||
| if (options.requestOptions.path === "/models") { | ||
| const models = | ||
| state.storedData?.models && state.storedData.models.length > 0 | ||
| ? state.storedData.models | ||
| : [defaultModel]; | ||
| const modelsResponse = createGetModelsResponse(models); | ||
| const body = JSON.stringify(modelsResponse); | ||
| const headers = { | ||
| "content-type": "application/json", | ||
|
|
@@ -202,6 +210,27 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { | |
| return; | ||
| } | ||
|
|
||
| // Handle memory endpoints - return stub responses in tests | ||
| // Matches: /agents/*/memory/*/enabled, /agents/*/memory/*/recent, etc. | ||
| if (options.requestOptions.path?.match(/\/agents\/.*\/memory\//)) { | ||
| let body: string; | ||
| if (options.requestOptions.path.includes("/enabled")) { | ||
| body = JSON.stringify({ enabled: false }); | ||
| } else if (options.requestOptions.path.includes("/recent")) { | ||
| body = JSON.stringify({ memories: [] }); | ||
| } else { | ||
| body = JSON.stringify({}); | ||
| } | ||
| const headers = { | ||
| "content-type": "application/json", | ||
| ...commonResponseHeaders, | ||
| }; | ||
| options.onResponseStart(200, headers); | ||
| options.onData(Buffer.from(body)); | ||
| options.onResponseEnd(); | ||
| return; | ||
| } | ||
|
Comment on lines
+213
to
+232
|
||
|
|
||
| // Handle /chat/completions endpoint | ||
| if ( | ||
| state.storedData && | ||
|
|
@@ -257,7 +286,7 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { | |
| // Fallback to normal proxying if no cached response found | ||
| // This implicitly captures the new exchange too | ||
| if (process.env.CI === "true") { | ||
| await emitNoMatchingRequestWarning( | ||
| await exitWithNoMatchingRequestError( | ||
| options, | ||
| state.testInfo, | ||
| state.workDir, | ||
|
|
@@ -295,7 +324,7 @@ async function writeCapturesToDisk( | |
| } | ||
| } | ||
|
|
||
| async function emitNoMatchingRequestWarning( | ||
| async function exitWithNoMatchingRequestError( | ||
| options: PerformRequestOptions, | ||
| testInfo: { file: string; line?: number } | undefined, | ||
| workDir: string, | ||
|
|
@@ -305,18 +334,27 @@ async function emitNoMatchingRequestWarning( | |
| if (testInfo?.file) parts.push(`file=${testInfo.file}`); | ||
| if (typeof testInfo?.line === "number") parts.push(`line=${testInfo.line}`); | ||
| const header = parts.length ? ` ${parts.join(",")}` : ""; | ||
| const normalized = await parseAndNormalizeRequest( | ||
| options.body, | ||
| workDir, | ||
| toolResultNormalizers, | ||
| ); | ||
| const normalizedMessages = normalized.conversations[0]?.messages ?? []; | ||
| const warningMessage = | ||
| `No cached response found for ${options.requestOptions.method} ${options.requestOptions.path}. ` + | ||
| `Final message: ${JSON.stringify( | ||
|
|
||
| let finalMessageInfo: string; | ||
| try { | ||
| const normalized = await parseAndNormalizeRequest( | ||
| options.body, | ||
| workDir, | ||
| toolResultNormalizers, | ||
| ); | ||
| const normalizedMessages = normalized.conversations[0]?.messages ?? []; | ||
| finalMessageInfo = JSON.stringify( | ||
| normalizedMessages[normalizedMessages.length - 1], | ||
| )}`; | ||
| process.stderr.write(`::warning${header}::${warningMessage}\n`); | ||
| ); | ||
| } catch { | ||
| finalMessageInfo = `(unable to parse request body: ${options.body?.slice(0, 200) ?? "empty"})`; | ||
| } | ||
|
|
||
| const errorMessage = | ||
| `No cached response found for ${options.requestOptions.method} ${options.requestOptions.path}. ` + | ||
| `Final message: ${finalMessageInfo}`; | ||
| process.stderr.write(`::error${header}::${errorMessage}\n`); | ||
| options.onError(new Error(errorMessage)); | ||
| } | ||
|
|
||
| async function findSavedChatCompletionResponse( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| models: | ||
| - claude-sonnet-4.5 | ||
| conversations: | ||
| - messages: | ||
| - role: system | ||
| content: ${system} | ||
| - role: user | ||
| content: Run 'sleep 2 && echo done' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow - it should get back a response instructing it to do the shell tool call, so that will be snapshotted won't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't get snapshotted, because we timeout on purpose before the response gets here
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I see. I thought it was timing out while executing the shell call (because of the
sleep 2part of it) but I see it actually times out before receiving the initial response, so this makes sense.