mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
## Thinking Path > - Paperclip manages agent work through tasks, projects, and routines. > - Daily QA must exercise the staging app through normal browser and API surfaces. > - A schedule must create real work and retain its result and history. > - Pausing that schedule must prevent the next invocation. > - The routine journey in #18652 passed in QA, while its separate completion journey has not passed. > - This PR delivers the routine test independently and preserves its measured flow and assertions. ## Linked Issues or Issue Description Refs: #18652. This is the routine-only delivery from that PR. The original completion implementation and its failed attempts remain on #18652. ## What Changed - Add the target-independent `routine-scheduled-run` journey with one native run and seven required checks. - Start from the measured routine source `ac4efb7c6f4782ea806661683c656fd2b4cc3189`. Keep its request, seven success assertions, one native-run limit, and 65-second pause check. - Bound each observed GET by the original remaining journey deadline. Preserve the normal API timeout for other callers. Keep the original routine error if final trigger disabling also fails, save a closed secondary receipt, and fail an otherwise successful journey when disabling fails. - Narrow the shared output-reader type to its one-argument GET capability without changing its runtime code. - Register only this case in the catalog, types, feature associations, dispatcher, and definition hash. Keep the strict catalog expectation in sync. - Retain the real Routines component controls and the existing project-flow coverage for the shared browser helper. - Document resource cleanup and the 65-second pause observation. Preserve the current routine map content. ## Verification - The original source passed [QA staging run 41567574185](https://pages.paperclip.ing/evals-qa-smoke-41567574185-1/). TestOps launch 1657 / result 3431 maps to case 718. All seven checks and five phases passed. One scheduler-bound native run, saved output attribution, the full pause window, and cleanup were verified. - Current source `8f62137f130f59fd3e39276e3d60185ddff24517` passed [fresh QA staging run 41582706162](https://pages.paperclip.ing/evals-qa-smoke-41582706162-1/) against Core `09162fe893b464dd2ccdafd14bd3ef1611202a86`. TestOps launch 1855. All seven checks, five phases, and all three workflow jobs passed. The run used one scheduler-bound native turn and observed 65.352 seconds after pause and completion. Canonical evidence collection and normal cleanup completed, with no active operations and the owned agent terminal. - This fresh pass covers the reviewed deadline and failure-reporting changes in the current source. The earlier ac4 pass and the byte-preserving b940 extraction remain historical records under their original conditions; neither failed attempts nor prior source conditions are regraded. - Causal controls: six assertions failed on the old behavior, then all 62 focused controls passed after the repair. Local: 834 related support tests, 41 shared chat/output-reader tests, and 26 unchanged real component controls passed. Runner E2E typecheck, recursive typecheck, build, maps, token gates, and `git diff --check` passed. The temporary output-reader type mismatch exposed by typecheck was corrected by narrowing the capability type. - The first restricted build/typecheck invocations failed because local tsx IPC was denied. A restricted support run also lacked the report CLI output. The same source passed with normal owned local IPC allowed; the original logs remain retained. - The original head's hosted e2e shard 2/8 failed at the unchanged mobile preference frame-count test: expected more than five frames, observed five. The cause is unknown. Its original log remains retained; it is not labeled a proven flake. No CI rerun was requested. - Current-head full CI run 41580423918, attempt 1, passed. The fresh review is 5/5 with zero unresolved threads. The broad local suite was not repeated. The earlier full local run recorded in #18643 had four failures and did not reach 164 serialized groups; this PR does not claim an all-local pass. - Prior failed routine attempts remain recorded. They exposed composer focus, collapsed schedule controls, pending browser events, and Markdown serialization problems. The measured later pass does not regrade those attempts. ## Risks - The shared browser-event helper also serves task creation and project creation. Its regression tests and existing consumers remain included. - The staging pass qualifies the captured source and configuration. The coordinator retains source merging, daily pinning, and plan selection. - The repository test and the unpassed completion test are not registered by this PR. ## Model Used OpenAI GPT-6 through Codex, with tool use and code execution. The exact deployment ID and context window are not exposed by this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub issue/PR references) --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
303 lines
24 KiB
TypeScript
303 lines
24 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { chmod, mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { AGENT_ADAPTER_TYPES } from "../../packages/shared/src/constants.js";
|
|
import { runnerMatrix } from "./catalog.js";
|
|
import { parseRunnerSelectors, selectRunnerExecutions, buildMatrixJobs } from "./selectors.js";
|
|
import { connectionHarnesses, connectionScopeGaps, connectionCell, supportsConnection } from "./connection-cases.js";
|
|
import { parseConnectionConfig, targetOrigin, selectedSecret, resolveConnectionSecret, resolveConnectionSettings } from "./connection-config.js";
|
|
import { connectionCheckpoints, connectionEvidencePasses, connectionRunDiagnostic, createConnectionProof, verifyConnectionArtifact, verifyConnectionRun, completedConnectionArtifactRun, connectionProbeChecks, type ConnectionEvidence } from "./connection-evidence.js";
|
|
import { validateRetainedRunnerResult } from "./result-validation.js";
|
|
import { ConnectionApi, verifyConnectionTarget } from "./connection-target.js";
|
|
import { cleanupConnectionCompany, connectionCreationReceipt, runConnectionCampaign, safeConnectionFailure, withConnectionCampaignCancellation } from "./connection-launch.js";
|
|
import * as connectionConfig from "./connection-config.js";
|
|
import * as connectionTarget from "./connection-target.js";
|
|
import * as reportAssets from "./report-assets.js";
|
|
import { buildRunnerCampaign } from "./history.js";
|
|
import { renderCaseOutcome } from "./case-outcome.js";
|
|
import type { RunnerE2EResult } from "./types.js";
|
|
|
|
const cells = runnerMatrix.filter(e => e.suite.id === "provider-connections");
|
|
const cell = (method: string) => cells.find(e => e.id === `provider-connections.connection-codex-native.local.agent-${method}`)!;
|
|
|
|
describe("connection fixture cleanup evidence", () => {
|
|
it.each([true, false])("captures stopped runs and logs before destructive cleanup (attached=%s)", async (attachedCompany) => {
|
|
const events: string[] = [];
|
|
let status = "running", deleted = false, revoked = false;
|
|
const evidence: Partial<ConnectionEvidence> = {};
|
|
const api = {
|
|
get: vi.fn(async (route: string) => {
|
|
if (route.endsWith("/agents")) return [{ id: "qa-agent", name: "Fixture" }, { id: "foreign", name: "Other" }];
|
|
if (route.includes("heartbeat-runs?")) return [{ id: "run", agentId: "qa-agent", status }];
|
|
if (route.endsWith("/ai-connections")) return { connections: [{ id: "account", status: revoked ? "revoked" : "active" }, { id: "foreign-account", status: "active" }] };
|
|
if (deleted) throw new Error("Run was deleted");
|
|
if (route.includes("/log?")) { events.push("log"); return { content: JSON.stringify({ chunk: JSON.stringify({ type: "acpx.error", childStderrTail: "HTTP 503 model overloaded" }) + "\n" }) }; }
|
|
events.push("run"); return { id: "run", status };
|
|
}),
|
|
patch: vi.fn(async () => { events.push("pause"); }),
|
|
post: vi.fn(async (route: string) => { if (route.endsWith("/cancel")) { events.push("cancel"); status = "cancelled"; } else events.push("archive"); }),
|
|
delete: vi.fn(async (route: string) => { if (route.endsWith("/foreign-account")) throw new Error("Revoked a concurrent campaign"); if (route.includes("tool-connections")) { events.push("revoke"); revoked = true; } else { events.push("delete-agent"); deleted = true; } }),
|
|
};
|
|
await cleanupConnectionCompany({ api: api as unknown as ConnectionApi, companyId: "company", agentName: "Fixture", attachedCompany,
|
|
retainCompany: false, createdConnectionIds: new Set(["account"]), evidence, collectDiagnostics: async () => {
|
|
const run = await api.get("/api/heartbeat-runs/run");
|
|
const log = await api.get("/api/heartbeat-runs/run/log?limitBytes=2000000");
|
|
evidence.runDiagnostics = [connectionRunDiagnostic(run, log)];
|
|
} });
|
|
expect(events).toEqual(["pause", "cancel", "run", "log", "revoke", attachedCompany ? "delete-agent" : "archive"]);
|
|
expect(evidence.runDiagnostics).toEqual([{ runId: "run", status: "cancelled", signals: ["provider_overloaded"], logAvailable: true }]);
|
|
expect(api.delete).not.toHaveBeenCalledWith("/api/agents/foreign");
|
|
expect(api.delete).not.toHaveBeenCalledWith("/api/tool-connections/foreign-account");
|
|
});
|
|
it("preserves diagnostics and avoids deletion when cancellation fails", async () => {
|
|
const collectDiagnostics = vi.fn(async () => {});
|
|
const api = {
|
|
get: vi.fn(async (route: string) => route.endsWith("/agents") ? [{ id: "qa-agent", name: "Fixture" }] : [{ id: "run", agentId: "qa-agent", status: "running" }]),
|
|
patch: vi.fn(async () => {}), post: vi.fn(async () => { throw new Error("cancel failed"); }), delete: vi.fn(),
|
|
};
|
|
await expect(cleanupConnectionCompany({ api: api as unknown as ConnectionApi, companyId: "company", agentName: "Fixture", attachedCompany: true,
|
|
retainCompany: false, createdConnectionIds: new Set(), evidence: {}, collectDiagnostics })).rejects.toThrow("cancel failed");
|
|
expect(collectDiagnostics).toHaveBeenCalledOnce();
|
|
expect(api.delete).not.toHaveBeenCalled();
|
|
});
|
|
it("accepts only creation receipts from this browser and its owned login sessions", () => {
|
|
const body = { connectionId: "00000000-0000-4000-8000-000000000021" };
|
|
const root = "/api/companies/company/ai-connections";
|
|
const logins = new Set(["owned-session"]);
|
|
expect(connectionCreationReceipt(root, "POST", "company", logins, body)).toBe(body.connectionId);
|
|
expect(connectionCreationReceipt(`${root}/local`, "POST", "company", logins, body)).toBe(body.connectionId);
|
|
expect(connectionCreationReceipt(`${root}/login/owned-session`, "GET", "company", logins, body)).toBe(body.connectionId);
|
|
expect(connectionCreationReceipt(`${root}/login/foreign-session`, "GET", "company", logins, body)).toBeUndefined();
|
|
expect(connectionCreationReceipt(root, "GET", "company", logins, body)).toBeUndefined();
|
|
expect(connectionCreationReceipt(root, "POST", "different-company", logins, body)).toBeUndefined();
|
|
expect(connectionCreationReceipt(root, "POST", "company", logins, { connectionId: "not-a-receipt" })).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("campaign interruption", () => {
|
|
it.each(["SIGINT", "SIGTERM", "SIGHUP"] as const)("keeps %s cancellation active until teardown finishes", async (signal) => {
|
|
const before = process.listenerCount(signal);
|
|
await withConnectionCampaignCancellation(async (state) => {
|
|
expect(process.listenerCount(signal)).toBe(before + 1);
|
|
process.emit(signal);
|
|
expect(state.aborted).toBe(true);
|
|
await Promise.resolve();
|
|
process.emit(signal);
|
|
expect(state.aborted).toBe(true);
|
|
});
|
|
expect(process.listenerCount(signal)).toBe(before);
|
|
});
|
|
it.each(["SIGINT", "SIGTERM", "SIGHUP"] as const)("stops owned targets and admits no next cell after %s during startup or reporting", async (signal) => {
|
|
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
|
|
const exitCode = process.exitCode;
|
|
for (const phase of ["startup", "report"] as const) {
|
|
const stop = vi.fn(async () => {});
|
|
const before = process.listenerCount(signal);
|
|
const config = parseConnectionConfig({});
|
|
const load = vi.spyOn(connectionConfig, "loadConnectionConfig").mockResolvedValue(config);
|
|
const credential = vi.spyOn(connectionConfig, "resolveConnectionSecret").mockRejectedValue(new connectionConfig.ConnectionBlock("missing_credential", "synthetic_missing_credential"));
|
|
const target = vi.spyOn(connectionTarget, "startConnectionTarget").mockImplementation(async (_config, _executions, _root, state) => {
|
|
expect(state?.aborted).toBe(false);
|
|
if (phase === "startup") process.emit(signal);
|
|
return { origin: "http://127.0.0.1:1", stop };
|
|
});
|
|
const verify = vi.spyOn(connectionTarget, "verifyConnectionTarget").mockResolvedValue({ commit: "a".repeat(40), deploymentMode: "local_trusted" });
|
|
const report = vi.spyOn(reportAssets, "stageDashboardBrandAssets").mockImplementation(async () => { if (phase === "report") process.emit(signal); });
|
|
let summary: string | undefined;
|
|
try {
|
|
summary = await runConnectionCampaign({ executions: [cell("api-key"), cell("subscription")], catalog: cells, repositoryRoot });
|
|
expect(credential).toHaveBeenCalledTimes(phase === "startup" ? 0 : 1);
|
|
expect(verify).toHaveBeenCalledTimes(phase === "startup" ? 0 : 1);
|
|
expect(stop).toHaveBeenCalledTimes(1);
|
|
expect(process.exitCode).toBe(1);
|
|
expect(process.listenerCount(signal)).toBe(before);
|
|
} finally {
|
|
load.mockRestore(); credential.mockRestore(); target.mockRestore(); verify.mockRestore(); report.mockRestore();
|
|
process.exitCode = exitCode;
|
|
if (summary) await rm(summary, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("live connection coverage", () => {
|
|
it("inventories every built-in adapter and keeps remote exclusions explicit", () => {
|
|
const inventory = new Set<string>(["paperclip_runner", ...connectionHarnesses.map(h => h.adapter), ...connectionScopeGaps.map(h => h.adapter)]);
|
|
expect(AGENT_ADAPTER_TYPES.filter(type => !inventory.has(type))).toEqual([]);
|
|
expect(connectionHarnesses.some(h => h.adapter === "hermes_local")).toBe(true);
|
|
expect(connectionScopeGaps.find(h => h.adapter === "hermes_gateway")?.status).toBe("excluded");
|
|
});
|
|
it("includes both UI entry points and actual runner variants without invented auth capabilities", () => {
|
|
expect(cells.length).toBe(116);
|
|
expect(new Set(cells.map(e => connectionCell(e).entry))).toEqual(new Set(["agent", "apps"]));
|
|
expect(cells.filter(e => connectionCell(e).harness.id === "grok").every(e => ["subscription", "api-key"].includes(connectionCell(e).mode))).toBe(true);
|
|
expect(supportsConnection(connectionHarnesses.find(h => h.id === "gemini")!, "subscription")).toBe(false);
|
|
});
|
|
it("never runs on --all or advertises a preinstalled subscription token as a credential", () => {
|
|
expect(selectRunnerExecutions(parseRunnerSelectors(["--all"])).some(e => e.suite.id === "provider-connections")).toBe(false);
|
|
expect(cell("subscription").requiredCredentials).toEqual([]);
|
|
expect(() => buildMatrixJobs(cells)).toThrow(/not admitted/);
|
|
expect(parseRunnerSelectors(["--suite", "provider-connections", "--connection-config", "/private/qa.json"]).connectionConfig).toBe("/private/qa.json");
|
|
});
|
|
});
|
|
|
|
describe("target and selected credential boundaries", () => {
|
|
it("passes a bounded GET timeout and preserves the default for other readers", async () => {
|
|
const fetch = vi.fn(async () => ({ ok: () => true, json: async () => ({ passed: true }) }));
|
|
const api = new ConnectionApi({ fetch } as any, "https://qa.test.paperclip.app");
|
|
await api.get("/api/health", { timeoutMs: 17 });
|
|
expect(fetch).toHaveBeenLastCalledWith("https://qa.test.paperclip.app/api/health", expect.objectContaining({ timeout: 17 }));
|
|
await api.get("/api/health");
|
|
expect(fetch).toHaveBeenLastCalledWith("https://qa.test.paperclip.app/api/health", expect.objectContaining({ timeout: 30_000 }));
|
|
await expect(api.get("/api/health", { timeoutMs: 0 })).rejects.toThrow("invalid_api_timeout");
|
|
expect(fetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
it("keeps scoped requests on tenant APIs and bounds extended probe timeouts", async () => {
|
|
const fetch = vi.fn(async (_url: string, _options: unknown) => ({ ok: () => true, json: async () => ({ passed: true }) }));
|
|
const api = new ConnectionApi({ fetch } as any, "https://qa.test.paperclip.app");
|
|
await api.post("/api/companies/qa/adapters/paperclip_runner/test-environment", {}, { timeoutMs: 120_000 });
|
|
expect(fetch.mock.calls[0]?.[1]).toMatchObject({ headers: { Origin: "https://qa.test.paperclip.app" }, timeout: 120_000, maxRedirects: 0 });
|
|
fetch.mockClear();
|
|
for (const route of ["https://other.test/api/health", "/api/../v1/admin", "/api/%2e%2e/v1/admin"]) await expect(api.get(route)).rejects.toThrow("invalid_api_route");
|
|
await expect(api.post("/api/health", {}, { timeoutMs: 120_001 })).rejects.toThrow("invalid_api_timeout");
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
it("accepts local/staging origins and rejects credential-bearing or redirectable target strings", () => {
|
|
expect(targetOrigin("http://127.0.0.1:3109/")).toBe("http://127.0.0.1:3109");
|
|
expect(targetOrigin("https://staging.example.test")).toBe("https://staging.example.test");
|
|
for (const url of ["http://staging.example.test", "https://user:password@example.test", "https://example.test/path", "https://example.test/?token=secret", "https://example.test/#secret", "file:///tmp/qa"]) expect(() => targetOrigin(url)).toThrow();
|
|
});
|
|
it("requires an exact staging revision and rejects unknown configuration instead of ignoring typos", () => {
|
|
expect(() => parseConnectionConfig({ target: { mode: "attach", baseURL: "https://example.test" } })).toThrow();
|
|
expect(() => parseConnectionConfig({ target: { mode: "managed-local", baseURL: "https://example.test" } })).toThrow();
|
|
expect(() => parseConnectionConfig({ broswer: {} })).toThrow();
|
|
expect(() => parseConnectionConfig({ browser: { loginTimeoutMs: Infinity } })).toThrow();
|
|
expect(parseConnectionConfig({}).browser.headed).toBe(true);
|
|
});
|
|
it("pins the target revision before permitting credential selection", async () => {
|
|
const config = parseConnectionConfig({ target: { mode: "attach", baseURL: "https://staging.example.test", expectedCommit: "a".repeat(40), deploymentMode: "authenticated" } });
|
|
const fetch = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify({ commit: "b".repeat(40), deploymentMode: "authenticated" })));
|
|
try {
|
|
await expect(verifyConnectionTarget("https://staging.example.test", config)).rejects.toThrow("target_identity_mismatch");
|
|
expect(fetch.mock.calls[0]?.[1]).toMatchObject({ redirect: "error" });
|
|
fetch.mockResolvedValue(new Response(JSON.stringify({ commit: "a".repeat(40), deploymentMode: "authenticated" })));
|
|
await expect(verifyConnectionTarget("https://staging.example.test", config)).resolves.toMatchObject({ commit: "a".repeat(40) });
|
|
} finally { fetch.mockRestore(); }
|
|
});
|
|
it("reads literal selected keys without sourcing shell commands or returning unrelated secrets", async () => {
|
|
expect(selectedSecret("UNRELATED=do-not-read\nexport OPENAI_API_KEY='chosen-value'\n", "OPENAI_API_KEY")).toBe("chosen-value");
|
|
expect(selectedSecret("OPENAI_API_KEY=one\nOPENAI_API_KEY=two", "OPENAI_API_KEY")).toBe("two");
|
|
expect(() => selectedSecret("OPENAI_API_KEY=$(touch /tmp/should-not-exist)", "OPENAI_API_KEY")).toThrow();
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "connection-secret-test-"));
|
|
try {
|
|
const file = path.join(root, "keys");
|
|
await writeFile(file, "OPENAI_API_KEY=selected-only\nUNRELATED=private\n", { mode: 0o600 });
|
|
const config = parseConnectionConfig({ secretFile: file });
|
|
expect(await resolveConnectionSecret(config, "OPENAI_API_KEY", {})).toBe("selected-only");
|
|
expect(await resolveConnectionSecret(config, undefined, {})).toBeUndefined();
|
|
await expect(resolveConnectionSecret(config, "XAI_API_KEY", {})).rejects.toThrow("missing_XAI_API_KEY");
|
|
await writeFile(path.join(root, "public"), "OPENAI_API_KEY=private", { mode: 0o644 });
|
|
await chmod(path.join(root, "public"), 0o644);
|
|
await expect(resolveConnectionSecret(parseConnectionConfig({ secretFile: path.join(root, "public") }), "OPENAI_API_KEY", {})).rejects.toThrow(/owner_only/);
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
});
|
|
it("never resolves an API key for a subscription or defaults a gateway to the direct provider's credential", () => {
|
|
expect(resolveConnectionSettings(cell("subscription"), parseConnectionConfig({})).credentialEnv).toBeUndefined();
|
|
expect(resolveConnectionSettings(cell("api-key"), parseConnectionConfig({})).credentialEnv).toBe("OPENAI_API_KEY");
|
|
expect(() => resolveConnectionSettings(cell("responses"), parseConnectionConfig({ routes: { responses: { model: "qa-alias", baseURL: "https://gateway.example.test/v1" } } }))).toThrow(/credential_reference/);
|
|
const config = parseConnectionConfig({ routes: { responses: { model: "qa-alias", baseURL: "https://gateway.example.test/v1", credentialEnv: "GATEWAY_QA_KEY" } } });
|
|
expect(resolveConnectionSettings(cell("responses"), config).credentialEnv).toBe("GATEWAY_QA_KEY");
|
|
});
|
|
});
|
|
|
|
function passingEvidence(): ConnectionEvidence {
|
|
return { version: 1, outcome: "passed", phase: "finished", assisted: true, authFreshness: "signed-in", target: { mode: "attach", origin: "https://staging.example.test", commit: "a".repeat(40), deploymentMode: "authenticated" }, entry: "agent", method: "subscription", checkpoints: Object.fromEntries(connectionCheckpoints.map(key => [key, true])), waits: [] };
|
|
}
|
|
|
|
describe("independent qualification", () => {
|
|
it("retains closed failure signals across split log chunks without messages or reasoning", () => {
|
|
const sentinel = "SENTINEL_CREDENTIAL_DO_NOT_RETAIN";
|
|
const rows = [
|
|
{ type: "acpx.text_delta", channel: "thought", text: 'HTTP 429 '+sentinel },
|
|
{ type: "acpx.error", childStderrTail: 'ApiError: {"error":{"code":503,"message":"model overloaded '+sentinel+'"}}' },
|
|
{ type: "acpx.tool_call", status: "failed", text: "Error checking existing file: Resource not found: "+sentinel },
|
|
].map(row => JSON.stringify(row)).join("\n")+"\n";
|
|
const split = Math.floor(rows.length / 2);
|
|
const log = { content: [rows.slice(0, split), rows.slice(split)].map(chunk => JSON.stringify({ chunk })).join("\n") };
|
|
const diagnostic = connectionRunDiagnostic({ id: "run-1", status: "failed" }, log);
|
|
expect(diagnostic).toEqual({ runId: "run-1", status: "failed", signals: ["provider_overloaded", "missing_file_conversion"], logAvailable: true });
|
|
expect(JSON.stringify(diagnostic)).not.toContain(sentinel);
|
|
expect(connectionRunDiagnostic({ id: "run-2", status: "running" })).toEqual({ runId: "run-2", status: "unknown", signals: [], logAvailable: false });
|
|
});
|
|
it("distinguishes terminal quota and unsupported configuration from generic failures", () => {
|
|
expect(connectionRunDiagnostic({ id: "quota", status: "failed", error: 'HTTP 429 RESOURCE_EXHAUSTED' }).signals).toEqual(["provider_quota"]);
|
|
expect(connectionRunDiagnostic({ id: "config", status: "failed", error: 'session/set_config_option: Method not found (-32601)' }).signals).toEqual(["unsupported_session_configuration"]);
|
|
expect(connectionRunDiagnostic({ id: "generic", status: "failed", error: 'unknown provider error' }).signals).toEqual([]);
|
|
expect(connectionRunDiagnostic({ id: "unattributed-503", status: "failed", error: 'HTTP 503' }).signals).toEqual([]);
|
|
});
|
|
it("waits for disposition repair and requires the successful artifact-producing run", () => {
|
|
const expected = { agentId: "a", connectionId: "c", method: "api_key", runtimeMode: "legacy", environmentId: "env" };
|
|
const first = { id: "first", agentId: "a", status: "succeeded", runtimeMode: "legacy", contextSnapshot: { paperclipEnvironment: { id: "env" }, aiConnection: { connectionId: "c", method: "api_key" } } };
|
|
const repair = { ...first, id: "repair" };
|
|
const file = { originalFilename: "proof.json", createdByAgentId: "a", originatingRunId: "repair" };
|
|
expect(completedConnectionArtifactRun([first, { ...repair, status: "running" }], [file], "proof.json", expected)).toBeUndefined();
|
|
expect(completedConnectionArtifactRun([first, repair], [file], "proof.json", expected)).toBe(repair);
|
|
expect(completedConnectionArtifactRun([first, { ...repair, status: "cancelled" }], [file], "proof.json", expected)).toBeUndefined();
|
|
expect(completedConnectionArtifactRun([first, repair], [{ ...file, createdByAgentId: "someone-else" }], "proof.json", expected)).toBeUndefined();
|
|
expect(completedConnectionArtifactRun([first, repair], [file], "followup.json", expected)).toBeUndefined();
|
|
expect(completedConnectionArtifactRun([first, repair], [file], "proof.json", { ...expected, connectionId: "another" })).toBeUndefined();
|
|
});
|
|
it("retains probe codes without provider text, URLs, or credential-bearing fields", () => {
|
|
const sentinel = "SENTINEL_CREDENTIAL_DO_NOT_RETAIN";
|
|
const checks = connectionProbeChecks({ checks: [
|
|
{ code: "hermes_cli_not_found", level: "error", message: sentinel, detail: `https://provider.test/?code=${sentinel}` },
|
|
{ code: sentinel, level: "error" }, { code: "safe_code", level: sentinel }, null,
|
|
] });
|
|
expect(checks).toEqual([{ code: "hermes_cli_not_found", level: "error" }]);
|
|
expect(JSON.stringify(checks)).not.toContain(sentinel);
|
|
});
|
|
it("rejects plausible prose, wrong bytes and wrong run attribution", () => {
|
|
const proof = createConnectionProof();
|
|
expect(verifyConnectionArtifact(Buffer.from(JSON.stringify(proof.expected)), proof.expected)).toBe(true);
|
|
expect(verifyConnectionArtifact(Buffer.from("Provider routing works"), proof.expected)).toBe(false);
|
|
expect(verifyConnectionArtifact(Buffer.from(JSON.stringify({ ...proof.expected, total: proof.expected.total + 1 })), proof.expected)).toBe(false);
|
|
const expected = { agentId: "a", connectionId: "c", method: "subscription", runtimeMode: "native", environmentId: "env", model: "m" };
|
|
const run = { agentId: "a", status: "succeeded", runtimeMode: "native", contextSnapshot: { paperclipEnvironment: { id: "env" }, aiConnection: { connectionId: "c", method: "subscription" } } };
|
|
expect(verifyConnectionRun(run, expected)).toBe(true);
|
|
expect(verifyConnectionRun(run, { ...expected, connectionId: "ambient" })).toBe(false);
|
|
expect(verifyConnectionRun(run, { ...expected, method: "api_key" })).toBe(false);
|
|
expect(verifyConnectionRun(run, { ...expected, environmentId: "another-env" })).toBe(false);
|
|
expect(verifyConnectionRun({ ...run, contextSnapshot: { aiConnection: run.contextSnapshot.aiConnection } }, expected)).toBe(false);
|
|
});
|
|
it("requires every checkpoint and never passes absent evidence or a human deadline", () => {
|
|
expect(connectionEvidencePasses(passingEvidence())).toBe(true);
|
|
for (const key of connectionCheckpoints) {
|
|
const evidence = passingEvidence(); delete evidence.checkpoints[key];
|
|
expect(connectionEvidencePasses(evidence)).toBe(false);
|
|
}
|
|
expect(connectionEvidencePasses({ ...passingEvidence(), outcome: "awaiting_user" })).toBe(false);
|
|
expect(connectionEvidencePasses({ ...passingEvidence(), target: { ...passingEvidence().target, commit: null } })).toBe(false);
|
|
});
|
|
it("withholds raw Playwright call logs including filled secrets, codes, and callback URLs", () => {
|
|
const sentinel = "SENTINEL_CREDENTIAL_DO_NOT_RETAIN";
|
|
for (const message of [`locator.fill: value ${sentinel}`, `https://provider.test/callback?code=${sentinel}`, `password=${sentinel}`, "lowercasesecret"]) {
|
|
const safe = safeConnectionFailure(new Error(message), "connection_creation");
|
|
expect(safe).toBe("connection_creation_failed"); expect(safe).not.toContain(sentinel);
|
|
}
|
|
});
|
|
it("keeps blocked authentication incomplete in the existing campaign/report", () => {
|
|
const execution = cell("subscription");
|
|
const result: RunnerE2EResult = { schema: "paperclip.runner-e2e.result/v2", executionId: execution.id, suiteId: execution.suite.id, suiteDefinitionHash: execution.suiteDefinitionHash,
|
|
attempt: 1, status: "failed", failureClass: "permanent_infrastructure", error: "provider_login_deadline_reached", profileId: execution.profile.id, environmentId: "local", caseId: execution.task.id, provider: "codex", model: "model", runtimeMode: "native",
|
|
startedAt: "2026-10-03T00:00:00Z", finishedAt: "2026-10-03T00:01:00Z", durationMs: 60_000, cleanup: "passed", providerConnection: { ...passingEvidence(), outcome: "awaiting_user", checkpoints: { target: true } } };
|
|
const campaign = buildRunnerCampaign({ campaignId: "test", generatedAt: result.finishedAt, expected: [execution.id], results: [result] });
|
|
expect(() => validateRetainedRunnerResult({ ...result, status: "passed" })).toThrow(/providerConnection/);
|
|
expect(() => validateRetainedRunnerResult({ ...result, status: "passed", providerConnection: undefined })).toThrow(/providerConnection/);
|
|
expect(campaign.passed).toBe(0); expect(campaign.incomplete).toBe(1); expect(campaign.complete).toBe(false);
|
|
expect(renderCaseOutcome(result, false, [])).toContain("Blocked: awaiting user");
|
|
const cleanupFailed = buildRunnerCampaign({ campaignId: "failed-cleanup", generatedAt: result.finishedAt, expected: [execution.id], results: [{ ...result, cleanup: "failed", failureClass: "cleanup_failure" }] });
|
|
expect(cleanupFailed.failed).toBe(1); expect(cleanupFailed.incomplete).toBe(0);
|
|
});
|
|
});
|