mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
## Thinking Path > - Paperclip manages agents and their work. > - Product E2E tests check complete user journeys. > - The current harness prepares self-hosted instances. > - Hosted workspaces need separate authentication, setup, and cleanup. > - The same in-app story should run on either deployment. > - This pull request adds deployment targets and explicit prerequisites. > - The private Cloud adapter owns fleet access and lifecycle operations. ## Linked Issues or Issue Description **Subsystem affected** Product E2E harness, fixture registry, and reports. **Problem or motivation** Self-hosted test setup cannot qualify a deployed Cloud workspace. Copying each story into a second harness would make its assertions diverge. **Proposed solution** Add explicit deployment targets, setup recipes, and ordered campaigns. Reuse a no-model task-properties story on both deployments. Let fresh Cloud onboarding provide verified context for that same story. Keep Cloud lifecycle and credentials behind a private target provider. **Roadmap alignment** This supports Cloud deployment qualification and the existing agent eval system. It adds no application endpoint or scheduler. Related work: #17112. The separate onboarding gateway UI prerequisite is #17253. ## What Changed - Add explicit target configuration and a credential-free preparation preview. Keep historical selectors and the current `--all` matrix unchanged. - Resolve requested fixture prerequisites, including a run-owned backlog task prepared before measurement. Reuse verified onboarding context without counting recipe setup as onboarding coverage. - Add task assignment persistence and the actual onboarding parent/child first-task journey, with an independent artifact oracle and native trace requirement. - Preserve failed boundaries and blocked dependents, deployment identity, costs, phase outcomes, and private evidence in the existing dashboard. - Validate success assertions and accounting before successful cleanup. Bound collection by the campaign deadline and cleanup/evidence hooks by a separate ten-minute grace, with cancellation and stop-only fallback. - Stage dashboard rendering separately so a timed-out renderer cannot overwrite retained failure results. Add evidence finalization hooks so the private provider can save evidence before target deletion and lease release. - Reject private journey bundles in the public history publisher. Browser-only results have no model or provider. ## Verification - The unchanged task-properties story passed on managed self-hosted and a registered testing Cloud workspace. Authentication, setup, journey, collection, and cleanup passed. Model calls: zero. Earlier attempts remain private and retained. - Final journey and report tests: 96 passed in eight files. Catalog compatibility: 39 passed. The wider focused run exposed three stale master catalog expectations; these now match the already shipped case-handoff v4 metadata and four-cell matrix. The default 385-cell selection is unchanged. - Review regressions reproduced premature success for a zero-request receipt and a missing required assertion. Both now fail before success cleanup while retaining cost receipts. The initial lifecycle fixes passed 33 checks. A subsequent review exposed a late renderer write that could erase saved failures; rendering now writes into isolated staging and publishes HTML only after it settles. Combined campaign, fixture-contract, and retained-report checks: 67 passed, including late writes, delayed timer callbacks, stalled collection, release, stop, evidence hooks, and browser close. Harness typecheck passed after the fixes. - A broader harness unit attempt completed 3,601 passing cases, two failures, and two skips. The failures exposed a stale Pi count and an omitted active Copilot fixture in the Daytona image input closure. The focused compatibility fixes passed 83 tests. After all source fixes, the full harness support suite passed: 160 files, 3,621 tests, two skips, plus 128 native-source checks. The earlier failures remain recorded. - Harness typecheck, repository typecheck, repository build, map checks, and Storybook catalog checks passed on the rebased branch. - The required full `pnpm test:run` attempt timed out after 35 minutes. It had 1,195 completed passing cases and one chat integration failure before its first suite completed. That case and nine adjacent cases pass in isolation. The original failure remains unresolved and retained; this is not a full-suite pass. - Current-head remote checks (`39b2a1ed8c49222fabd1ba8e225cac81bff5c6f7`): 53 passed, two skipped, none pending or failing. Greptile completed successfully at 5/5 with no unresolved review threads. - Fresh paid Cloud onboarding, dependent reuse, and disposable cleanup have not been qualified live. They require deployment of the separate UI prerequisite and actual Cloud gateway budget admission. The trusted Cloud workflow remains disabled. - Final live verification at `39b2a1ed8c`: the unchanged shared story passed on managed self-hosted and the registered testing Cloud stack using the prepared backlog-task recipe. Both cleaned up successfully with zero model calls. Independent private S3 checks confirmed final evidence and released QA identity/stack leases. Cloud evidence remains private. ## Risks - A deployed UI or Cloud control-plane change can invalidate selectors or provider contracts. Version drift fails the attempt. - Cloud evidence and lifecycle recovery are private-provider responsibilities. A pending cleanup or evidence upload must keep the target unavailable for reuse. - No release is deployed or pinned by this change. No automatic story retry is added. The initial pilot retains its explicit $10, one disposable workspace, and 30-minute bounds. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, code execution, and delegated agents. The runtime does not expose a more specific deployment model identifier or context-window size. ## 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 `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
293 lines
24 KiB
TypeScript
293 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("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);
|
|
});
|
|
});
|