mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 23:20:53 +01:00
fix(evals): consume search probe response bodies once
This commit is contained in:
1 parent
8565913082
commit
643447b54d
4 files changed
+36
-2
No files matched your search
@@ -7,6 +7,7 @@ import type { CompanyPortabilityFileEntry } from "../../../packages/shared/src/t
|
||||
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
|
||||
import { makeDefinition } from "./internal-definition.js";
|
||||
import { mainPerson } from "./internal-privacy.js";
|
||||
import { readSearchProbeResponse } from "./internal-search-response.js";
|
||||
import { checks, entity, hasLink, observation, observeInternal, project, sameTask, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
const sha = (body: string | Buffer) => createHash("sha256").update(body).digest("hex");
|
||||
interface HistoricalTask extends Entity { completedAt?: string | null }
|
||||
@@ -118,7 +119,7 @@ function findArtifact(): ChatCatalogCase {
|
||||
async observe(ctx, state): Promise<SearchObservation> {
|
||||
if (!key) throw new Error("Historical search probe was not created during setup"); const o = await observeInternal(ctx, state); const file = state.finalAttachment as IssueAttachment;
|
||||
const read = async (path: string) => { const url = new URL(path, ctx.input.api.baseURL); if (url.origin !== ctx.input.api.baseURL || !url.pathname.startsWith("/api/")) throw new Error("Historical probe escaped fixture API");
|
||||
const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(15000), headers: { Authorization: `Bearer ${key!.token}` } }); const body: unknown = response.ok ? await response.json() : undefined; await response.body?.cancel(); return { status: response.status, body }; };
|
||||
const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(15000), headers: { Authorization: `Bearer ${key!.token}` } }); return readSearchProbeResponse(response); };
|
||||
const [currentSource, attachments, deliveryComments, bytes, search] = await Promise.all([
|
||||
ctx.input.api.get<HistoricalTask>(`/api/issues/${entity(state, "task").id}`), ctx.input.api.get<IssueAttachment[]>(`/api/issues/${entity(state, "task").id}/attachments`),
|
||||
ctx.input.api.get<Entity[]>(`/api/issues/${entity(state, "task").id}/comments?order=asc`), ctx.input.api.getAttachmentContent(file.id), read(`/api/companies/${o.companyId}/search?q=website`),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChatCatalogCase } from "../chat-catalog-contract.js";
|
||||
import { makeDefinition } from "./internal-definition.js";
|
||||
import { mainPerson } from "./internal-privacy.js";
|
||||
import { readSearchProbeResponse } from "./internal-search-response.js";
|
||||
import { checks, entity, hasLink, observation, observeInternal, project, putDocument, sameTask, task, type Entity, type InternalObservation } from "./internal-support.js";
|
||||
|
||||
interface DesignObservation extends InternalObservation {
|
||||
@@ -66,7 +67,7 @@ function findDesign(): ChatCatalogCase {
|
||||
const read = async (path: string) => {
|
||||
const url = new URL(path, ctx.input.api.baseURL); if (url.origin !== ctx.input.api.baseURL || !url.pathname.startsWith("/api/")) throw new Error("Design probe escaped fixture API");
|
||||
const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(15_000), headers: { Authorization: `Bearer ${key!.token}` } });
|
||||
const body: unknown = response.ok ? await response.json() : null; await response.body?.cancel(); return { status: response.status, body };
|
||||
return readSearchProbeResponse(response);
|
||||
};
|
||||
const o = await observeInternal(ctx, state); const hidden = entity(state, "privateTask"); const target = entity(state, "task");
|
||||
const [privateRead, search] = await Promise.all([read(`/api/issues/${hidden.id}`), read(`/api/companies/${o.companyId}/search?q=launch%20website%20design`)]);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readSearchProbeResponse } from "./internal-search-response.js";
|
||||
describe("actual search response body ownership", () => {
|
||||
it("reproduces the retained locked-stream observer failure with a real Response", async () => {
|
||||
const response = new Response(JSON.stringify({ results: [{ id: "source" }] }), { status: 200 });
|
||||
await response.json(); expect(response.bodyUsed).toBe(true); expect(response.body?.locked).toBe(true);
|
||||
await expect(response.body!.cancel()).rejects.toThrow("ReadableStream is locked");
|
||||
});
|
||||
it("returns actual parsed search bytes without cancelling an already consumed successful body", async () => {
|
||||
const response = new Response(JSON.stringify({ results: [{ id: "source" }] }), { status: 200 });
|
||||
const cancel = vi.spyOn(response.body!, "cancel");
|
||||
expect(await readSearchProbeResponse(response)).toEqual({ status: 200, body: { results: [{ id: "source" }] } });
|
||||
expect(cancel).not.toHaveBeenCalled(); expect(response.bodyUsed).toBe(true);
|
||||
});
|
||||
it.each([403, 404])("cancels an unread denied body, retaining actual %s and exposing no restricted bytes", async status => {
|
||||
const response = new Response("Restricted source body", { status });
|
||||
const cancel = vi.spyOn(response.body!, "cancel");
|
||||
expect(await readSearchProbeResponse(response)).toEqual({ status, body: null });
|
||||
expect(cancel).toHaveBeenCalledOnce(); expect(response.bodyUsed).toBe(true);
|
||||
});
|
||||
it("propagates invalid successful JSON instead of inventing empty search results", async () => {
|
||||
await expect(readSearchProbeResponse(new Response("not JSON", { status: 200 }))).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
/** These two independently authenticated search probes consume a successful
|
||||
* JSON body once. json() owns that stream reader; cancel() afterward throws
|
||||
* on its locked stream. An unread denied body still needs cancellation. */
|
||||
export async function readSearchProbeResponse(response: Response): Promise<{ status: number; body: unknown }> {
|
||||
if (response.ok) return { status: response.status, body: await response.json() };
|
||||
await response.body?.cancel();
|
||||
return { status: response.status, body: null };
|
||||
}
|
||||
Reference in new issue
Block a user