Files
PaperClipAI/tests/runner-e2e/api.test.ts
T
DottaandPaperclip 812b9e1a65 fix(e2e): normalize remote admission failures without private diagnostics
Preserve typed HTTP and transport timeout classification while removing raw response bodies and causes from admission reports. Reject unknown failures and malformed JSON without weakening ownership or deadline checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-30 08:48:21 -05:00

54 lines
3.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import type { APIRequestContext, APIResponse } from "@playwright/test";
import { RunnerApi, RunnerApiHttpError } from "./api.js";
const response = (status: number, data: unknown = {}): APIResponse => ({
ok: () => status >= 200 && status < 300, status: () => status,
json: async () => data, text: async () => JSON.stringify(data), url: () => "http://fixture.invalid/instructions",
}) as APIResponse;
afterEach(() => vi.unstubAllEnvs());
describe("fixture instruction revision fence", () => {
function api(current: APIResponse, saved = response(200)) {
vi.stubEnv("PAPERCLIP_RUNNER_E2E_PORT", "3100");
const request = { get: vi.fn(async () => current), put: vi.fn(async () => saved) };
return { request, api: new RunnerApi(request as unknown as APIRequestContext) };
}
it.each([[404, {}, null], [200, { contentHash: "current-hash" }, "current-hash"]])("saves from a %s read with its exact base", async (status, detail, baseHash) => {
const fixture = api(response(status as number, detail));
await fixture.api.saveAgentInstructions("agent", "Fixture instructions");
expect(fixture.request.get).toHaveBeenCalledWith("/api/agents/agent/instructions-bundle/file?path=AGENTS.md");
expect(fixture.request.put).toHaveBeenCalledExactlyOnceWith("/api/agents/agent/instructions-bundle/file", {
data: { path: "AGENTS.md", content: "Fixture instructions", baseHash },
});
});
it.each([response(403), response(500), response(200, {})])("never treats a failed or incomplete read as a new file", async current => {
const fixture = api(current);
await expect(fixture.api.saveAgentInstructions("agent", "Fixture instructions")).rejects.toThrow();
expect(fixture.request.put).not.toHaveBeenCalled();
});
it("surfaces a concurrent edit without retrying or overwriting it", async () => {
const fixture = api(response(200, { contentHash: "old" }), response(409, { error: "Revision conflict" }));
await expect(fixture.api.saveAgentInstructions("agent", "Fixture instructions")).rejects.toThrow("409");
expect(fixture.request.get).toHaveBeenCalledTimes(1);
expect(fixture.request.put).toHaveBeenCalledTimes(1);
});
});
it("passes a bounded GET timeout to the request transport without changing its response", async () => {
vi.stubEnv("PAPERCLIP_RUNNER_E2E_PORT", "3100");
const get = vi.fn(async () => response(200, { status: "running" }));
const api = new RunnerApi({ get } as unknown as APIRequestContext);
await expect(api.get("/api/heartbeat-runs/run", { timeout: 1234 })).resolves.toEqual({ status: "running" });
expect(get).toHaveBeenCalledExactlyOnceWith("/api/heartbeat-runs/run", { timeout: 1234 });
});
it("preserves GET HTTP status separately from diagnostic text", async () => {
vi.stubEnv("PAPERCLIP_RUNNER_E2E_PORT", "3100");
const api = new RunnerApi({ get: vi.fn(async () => response(503, { error: "PRIVATE" })) } as unknown as APIRequestContext);
const error = await api.get("/api/environments/env/leases").catch(error => error);
expect(error).toBeInstanceOf(RunnerApiHttpError);
if (!(error instanceof RunnerApiHttpError)) throw new Error("Expected typed HTTP error");
expect(error.status).toBe(503);
});