mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 06:16:06 -10:00
Merge current master before PR validation
Co-Authored-By: Paperclip <[email protected]>
This commit is contained in:
commit
715cee88ce
200 files changed
+6595
-743
No files matched your search
@@ -446,6 +446,9 @@ jobs:
|
||||
- name: Typecheck workspaces whose build scripts skip TypeScript
|
||||
run: pnpm run typecheck:build-gaps
|
||||
|
||||
- name: Typecheck test files that no build reads
|
||||
run: node scripts/typecheck-test-programs.mjs --low-memory
|
||||
|
||||
- name: Typecheck the server in low-memory layers
|
||||
env:
|
||||
PAPERCLIP_TYPECHECK_LOW_MEMORY: "1"
|
||||
|
||||
@@ -1319,6 +1319,40 @@ that file, and it fails when a referenced config does not exist yet. The
|
||||
program. That program peaks at about 15 GiB at default settings and about 8 GiB on
|
||||
one thread.
|
||||
|
||||
#### Typecheck test files outside the build
|
||||
|
||||
Most package tsconfigs exclude tests, and Vitest strips types, so a type error in
|
||||
one of those tests fails nothing. `scripts/typecheck-test-programs.mjs` reads them
|
||||
through separate programs, usually a `tsconfig.test.json` beside the package's
|
||||
`tsconfig.json`:
|
||||
|
||||
```sh
|
||||
node scripts/typecheck-test-programs.mjs --low-memory
|
||||
node scripts/typecheck-test-programs.mjs packages/paperclip-runner/tsconfig.test.json
|
||||
```
|
||||
|
||||
The first form runs in the required `Typecheck + Release Registry` job. It checks
|
||||
one program at a time and the largest peaks below 2 GiB. It then fails if a tracked
|
||||
`*.test.ts(x)` or `*.spec.ts(x)` file is in no program. The compiler's file list
|
||||
decides that, because `include` silently drops `foo.test.tsx` when `foo.test.ts`
|
||||
exists.
|
||||
|
||||
To add a test directory, add it to the `include` of the package's
|
||||
`tsconfig.test.json`. To add a package, add a program to `PROGRAMS` in the script.
|
||||
|
||||
Sandbox providers are outside the root workspace, so the root install has none of
|
||||
their SDKs. `scripts/typecheck-sandbox-providers` is a small workspace that lists
|
||||
them and holds one lockfile. Its install replaces any standalone `node_modules` in
|
||||
those providers. After changing a provider's `package.json`, refresh that lockfile:
|
||||
|
||||
```sh
|
||||
pnpm -C scripts/typecheck-sandbox-providers install --ignore-scripts --lockfile-only
|
||||
```
|
||||
|
||||
Do not commit a lockfile inside a provider.
|
||||
`scripts/build-standalone-public-packages.mjs` uses a frozen install when one
|
||||
exists, which changes what a release resolves.
|
||||
|
||||
#### Reused Runner build in the server build
|
||||
|
||||
The server build vendors `packages/paperclip-runner/dist`, so its
|
||||
|
||||
@@ -2589,13 +2589,26 @@ Shutdown, process loss, and provider failure use the existing durable failure re
|
||||
If shutdown or a recorded unexpected system interruption stops Paperclip Runner
|
||||
preparation before native execution was selected, recovery verifies that no
|
||||
provider, process launch, native result or coordinator exists. The previous
|
||||
controller must have settled or lost its lease.
|
||||
controller must have settled or lost its lease. A failed `process_lost` run also
|
||||
qualifies when restart recovery committed its explicit bootstrap certificate
|
||||
that provider work did not start. The error label alone grants no replay.
|
||||
Required workspace recovery must have an exact saved-file receipt and stopped
|
||||
leases before the task can continue. Once these checks pass, the server retires
|
||||
its own unchanged bootstrap-failure block and schedules one bounded retry.
|
||||
Startup also discovers this obligation when file recovery completed before the
|
||||
restart. A pending Retry or saved message owns its continuation instead of
|
||||
starting a competing automatic attempt. Retry uses the same preparation proof
|
||||
starting a competing automatic attempt. If a replacement was already withheld
|
||||
by the workspace gate before execution, continuation retains that cancelled row
|
||||
and uses its existing predecessor claim and remaining failure budget.
|
||||
Recovery adopts that withheld row only when it is the source's sole retry child.
|
||||
Multiple-child history reuses the newest existing child, ordered by creation time
|
||||
and run ID, without scheduling another replacement.
|
||||
Clearing the file hold records a pending continuation before enqueue. Restart
|
||||
reconstructs that obligation until a successor is recorded. Known admission
|
||||
gates defer it without a polling deadline; eligibility changes wake the existing
|
||||
recovery worker. An unresolved enqueue or receipt write keeps its shared retry
|
||||
deadline. Delivery or exhaustion retires that obligation.
|
||||
Retry uses the same preparation proof
|
||||
and preserves the recovered workspace. Current task ownership, human blocks,
|
||||
pauses, approvals, budgets and the existing retry limit remain effective.
|
||||
Operator Stops retain their explicit continuation requirements. Other ambiguous
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
"smoke:notion-generic-live": "node scripts/smoke/notion-generic-live.mjs",
|
||||
"smoke:posthog-live": "node scripts/smoke/posthog-live.mjs",
|
||||
"smoke:terminal-bench-loop-skill": "node scripts/smoke/terminal-bench-loop-skill-smoke.mjs",
|
||||
"test:release-registry": "node scripts/ci-profile/node-test.mjs scripts/ci-profile/reporters.test.mjs scripts/__tests__/canary-startup-playwright.test.mjs scripts/__tests__/cursor-public-install-sandbox.test.mjs scripts/__tests__/grok-public-install-sandbox.test.mjs scripts/__tests__/public-install-closure.test.mjs scripts/__tests__/public-install-registry.test.mjs scripts/__tests__/prefetch-child.test.mjs scripts/__tests__/stage-bundled-packages.test.mjs scripts/__tests__/pack-staged-packages.test.mjs scripts/__tests__/retain-runner-qualification-packages.test.mjs scripts/__tests__/runner-release-crate-cache.test.mjs scripts/__tests__/runner-debug-crate-cache.test.mjs scripts/__tests__/vitest-worker-isolation.test.mjs scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs scripts/check-no-git-push.test.mjs scripts/release-lib.test.mjs scripts/release-registry-versions.test.mjs scripts/link-plugin-dev-sdk.test.js scripts/acpx-patch-packaging.test.mjs scripts/service-onboard-smoke.test.mjs scripts/docker-onboard-smoke.test.mjs scripts/preview-artifacts.test.mjs scripts/cloud-migrator-artifacts.test.mjs scripts/__tests__/local-verify.test.mjs",
|
||||
"test:release-registry": "node scripts/ci-profile/node-test.mjs scripts/ci-profile/reporters.test.mjs scripts/__tests__/canary-startup-playwright.test.mjs scripts/__tests__/cursor-public-install-sandbox.test.mjs scripts/__tests__/grok-public-install-sandbox.test.mjs scripts/__tests__/public-install-closure.test.mjs scripts/__tests__/public-install-registry.test.mjs scripts/__tests__/prefetch-child.test.mjs scripts/__tests__/stage-bundled-packages.test.mjs scripts/__tests__/pack-staged-packages.test.mjs scripts/__tests__/retain-runner-qualification-packages.test.mjs scripts/__tests__/runner-release-crate-cache.test.mjs scripts/__tests__/runner-debug-crate-cache.test.mjs scripts/__tests__/vitest-worker-isolation.test.mjs scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs scripts/check-no-git-push.test.mjs scripts/release-lib.test.mjs scripts/release-registry-versions.test.mjs scripts/link-plugin-dev-sdk.test.js scripts/acpx-patch-packaging.test.mjs scripts/service-onboard-smoke.test.mjs scripts/docker-onboard-smoke.test.mjs scripts/preview-artifacts.test.mjs scripts/cloud-migrator-artifacts.test.mjs scripts/typecheck-test-programs.test.mjs scripts/__tests__/local-verify.test.mjs",
|
||||
"storybook-visual:baseline": "node scripts/storybook-visual-baseline.mjs",
|
||||
"test:storybook-visual": "node scripts/storybook-visual-baseline.mjs download && node scripts/storybook-visual-baseline.mjs verify && pnpm build-storybook && npx playwright test --config tests/storybook-visual/playwright.config.ts",
|
||||
"test:storybook-visual:update": "node scripts/storybook-visual-baseline.mjs download && pnpm build-storybook && npx playwright test --config tests/storybook-visual/playwright.config.ts --update-snapshots && node scripts/storybook-visual-baseline.mjs pack",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["scripts/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": []
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import type { HarnessDriver, HarnessSession, PersistedHarnessSession } from "../contracts/harness-driver.js";
|
||||
import { PROVIDER_STATE_INCOMPATIBLE, runnerRecoveryFailureClass } from "../contracts/harness-driver.js";
|
||||
import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js";
|
||||
import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js";
|
||||
import { NativeSessionProtocolIntegrityError, type NativeSession } from "../contracts/native-session-backend.js";
|
||||
import { HarnessDriverBackend } from "./harness-driver-backend.js";
|
||||
|
||||
const result: PrpStructuredRunResult = {
|
||||
@@ -55,8 +55,13 @@ function prpEvent(sourceSeq: number, eventType: PrpEvent["eventType"], payload:
|
||||
|
||||
const runtimeResolutions: unknown[] = [];
|
||||
|
||||
function cancelSession(session: NativeSession, input: Parameters<NonNullable<NativeSession["cancel"]>>[0]) {
|
||||
if (!session.cancel) throw new Error("backend session does not support cancellation");
|
||||
return session.cancel(input);
|
||||
}
|
||||
|
||||
class FakeHarnessSession implements HarnessSession {
|
||||
ids() { return { driverSessionId: "driver-1", providerSessionId: "provider-1" }; }
|
||||
ids(): ReturnType<HarnessSession["ids"]> { return { driverSessionId: "driver-1", providerSessionId: "provider-1" }; }
|
||||
async *events() {
|
||||
yield prpEvent(1, "run.result.proposed", result);
|
||||
yield prpEvent(2, "turn.completed", { status: "completed" });
|
||||
@@ -942,7 +947,7 @@ describe("HarnessDriverBackend", () => {
|
||||
);
|
||||
class TerminalThenIntegrityFailure extends FakeHarnessSession {
|
||||
goal = vi.fn(async () => null);
|
||||
steer = vi.fn(async () => ({ correlationId: "blocked-steer" }));
|
||||
steer = vi.fn(async () => {});
|
||||
override async *events() {
|
||||
yield* super.events();
|
||||
throw fault;
|
||||
@@ -1068,8 +1073,8 @@ describe("HarnessDriverBackend", () => {
|
||||
const session = await backend.openSession({
|
||||
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" }, workingDirectory: "/workspace",
|
||||
});
|
||||
if (journalOnly) session.revokeTurnPublication();
|
||||
else await session.cancel({ reason: "durable governed wait", signal: new AbortController().signal }).cleanup;
|
||||
if (journalOnly) session.revokeTurnPublication!();
|
||||
else await cancelSession(session, { reason: "durable governed wait", signal: new AbortController().signal }).cleanup;
|
||||
const events = [];
|
||||
for await (const event of session.events()) events.push(event);
|
||||
expect(events.filter(event => event.payload.kind === "agentMessage")).toHaveLength(0);
|
||||
@@ -1085,7 +1090,7 @@ describe("HarnessDriverBackend", () => {
|
||||
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
await session.cancel({ reason: "user stop during startup", signal: new AbortController().signal }).cleanup;
|
||||
await cancelSession(session, { reason: "user stop during startup", signal: new AbortController().signal }).cleanup;
|
||||
await expect(session.startTurn({ message: { role: "user", text: "cancelled work" } }))
|
||||
.rejects.toThrow("native_session_cancelled");
|
||||
expect(start).not.toHaveBeenCalled();
|
||||
@@ -1108,7 +1113,7 @@ describe("HarnessDriverBackend", () => {
|
||||
});
|
||||
const iterator = session.events()[Symbol.asyncIterator]();
|
||||
await iterator.next();
|
||||
await session.cancel({ reason: "operator stop", signal: new AbortController().signal }).cleanup;
|
||||
await cancelSession(session, { reason: "operator stop", signal: new AbortController().signal }).cleanup;
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "runtime_request.cancelled", sourceSeq: 3 } });
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "turn.failed", sourceSeq: 4 } });
|
||||
await expect(iterator.next()).resolves.toMatchObject({ done: true });
|
||||
@@ -1133,7 +1138,7 @@ describe("HarnessDriverBackend", () => {
|
||||
});
|
||||
const iterator = session.events()[Symbol.asyncIterator]();
|
||||
await iterator.next();
|
||||
await expect(session.cancel({ reason: "operator stop", signal: new AbortController().signal }).cleanup).rejects.toThrow("no active provider turn");
|
||||
await expect(cancelSession(session, { reason: "operator stop", signal: new AbortController().signal }).cleanup).rejects.toThrow("no active provider turn");
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "turn.cancelled", sourceSeq: 2,
|
||||
payload: { status: "cancelled", providerTerminalState: "completed", reason: "cancelled_after_provider_completed" } } });
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "run.terminal", sourceSeq: 3,
|
||||
@@ -1166,7 +1171,7 @@ describe("HarnessDriverBackend", () => {
|
||||
});
|
||||
const iterator = session.events()[Symbol.asyncIterator]();
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "runtime_request.created" } });
|
||||
await session.cancel({
|
||||
await cancelSession(session, {
|
||||
reason: "operator cancelled the run",
|
||||
signal: new AbortController().signal,
|
||||
}).cleanup;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as codexSecurity from "../drivers/codex/codex-security-config.js";
|
||||
|
||||
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput, type NativeExecutionInputV1, type NativeExecutionInputV2 } from "../contracts/native-execution.js";
|
||||
import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions } from "../contracts/runtime-context.js";
|
||||
|
||||
const contextRoots: string[] = [];
|
||||
@@ -36,12 +36,12 @@ import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } fr
|
||||
import { createOpenCodeNativeSessionBackend } from "./opencode-native-backend.js";
|
||||
|
||||
function execution(
|
||||
provider: NativeExecutionInput["provider"] = {
|
||||
provider: NativeExecutionInputV1["provider"] = {
|
||||
kind: "codex",
|
||||
model: null,
|
||||
approvalPolicy: "never",
|
||||
},
|
||||
): NativeExecutionInput {
|
||||
): NativeExecutionInputV1 {
|
||||
return {
|
||||
schema: "paperclip.native-execution-input.v1",
|
||||
binding: {
|
||||
@@ -88,7 +88,7 @@ function execution(
|
||||
|
||||
function acpxExecution(
|
||||
agent: "codex" | "pi" | "claude" = "codex",
|
||||
): NativeExecutionInput {
|
||||
): NativeExecutionInputV1 {
|
||||
return {
|
||||
...execution(),
|
||||
session: {
|
||||
@@ -107,7 +107,7 @@ function acpxExecution(
|
||||
};
|
||||
}
|
||||
|
||||
function opencodeExecution(): NativeExecutionInput {
|
||||
function opencodeExecution(): NativeExecutionInputV1 {
|
||||
return {
|
||||
...execution(),
|
||||
session: {
|
||||
@@ -124,7 +124,7 @@ function opencodeExecution(): NativeExecutionInput {
|
||||
};
|
||||
}
|
||||
|
||||
function planningExecution(input: NativeExecutionInput): NativeExecutionInput {
|
||||
function planningExecution(input: NativeExecutionInputV1): NativeExecutionInputV2 {
|
||||
return {
|
||||
...input,
|
||||
schema: "paperclip.native-execution-input.v2",
|
||||
@@ -143,7 +143,7 @@ function planningExecution(input: NativeExecutionInput): NativeExecutionInput {
|
||||
|
||||
function managedExecution(
|
||||
kind: "claude_managed" | "aws_agentcore",
|
||||
): NativeExecutionInput {
|
||||
): NativeExecutionInputV1 {
|
||||
if (kind === "claude_managed") {
|
||||
return {
|
||||
...execution(),
|
||||
@@ -223,7 +223,7 @@ describe("native backend factory", () => {
|
||||
});
|
||||
try {
|
||||
const backend = createNativeSessionBackend(input, { environment: {} });
|
||||
await expect(backend.openSession({ identity: { runId: "run", sessionId: "session", companyId: "company", agentId: "agent" },
|
||||
await expect(backend.openSession({ identity: { runId: "run", sessionId: "session", companyId: "company", issueId: "issue", agentId: "agent" },
|
||||
workingDirectory: WORKSPACE })).rejects.toThrow("offline-stop-before-process-spawn");
|
||||
expect(generated).toBeDefined();
|
||||
if (webSearch) expect(generated?.slice(-3)).toEqual(["-c", 'web_search="disabled"', "app-server"]);
|
||||
@@ -404,7 +404,7 @@ describe("native backend factory", () => {
|
||||
expect(recovered).toMatchObject({ recovered: true });
|
||||
expect(factory).toHaveBeenCalledWith(expect.objectContaining({ baseInstructions: retainedSystemInstructions }));
|
||||
expect(transport.calls.some(call => call.method === "thread/start" || call.method === "turn/start")).toBe(false);
|
||||
if (recovered.recovered) await recovered.session.close({ reason: "test complete" });
|
||||
if (recovered.recovered) await recovered.session!.close({ reason: "test complete" });
|
||||
});
|
||||
|
||||
describe("runner-prepared launch working directory", () => {
|
||||
@@ -420,7 +420,7 @@ describe("native backend factory", () => {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
const identity = { runId: "run", sessionId: "session", companyId: "company", agentId: "agent" };
|
||||
const identity = { runId: "run", sessionId: "session", companyId: "company", issueId: "issue", agentId: "agent" };
|
||||
const providers = [
|
||||
["Codex", execution()],
|
||||
["OpenCode", opencodeExecution()],
|
||||
@@ -452,8 +452,8 @@ describe("native backend factory", () => {
|
||||
}).recoverSession!({ ...snapshot, activeTurnId: null }, { signal: new AbortController().signal });
|
||||
expect(recovered).toMatchObject({ recovered: true });
|
||||
if (recovered.recovered) {
|
||||
expect((await recovered.session.snapshot()).workingDirectory).toBe(cwd);
|
||||
await recovered.session.close({ reason: "test complete" });
|
||||
expect((await recovered.session!.snapshot()).workingDirectory).toBe(cwd);
|
||||
await recovered.session!.close({ reason: "test complete" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -769,6 +769,7 @@ describe("native backend factory", () => {
|
||||
await backend.openSession({
|
||||
identity: {
|
||||
companyId: "company",
|
||||
issueId: "issue",
|
||||
agentId: "agent",
|
||||
runId: "run",
|
||||
sessionId: "session",
|
||||
@@ -807,7 +808,7 @@ describe("native backend factory", () => {
|
||||
environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE },
|
||||
});
|
||||
const session = await backend.openSession({
|
||||
identity: { companyId: "company", agentId: "agent", runId: "run", sessionId: "session" },
|
||||
identity: { companyId: "company", issueId: "issue", agentId: "agent", runId: "run", sessionId: "session" },
|
||||
workingDirectory: WORKSPACE,
|
||||
});
|
||||
const first = JSON.stringify(buildNativeModelEnvelope(input));
|
||||
|
||||
@@ -21,7 +21,7 @@ import { inspectNativeCompletionSourceMetadata } from "../../../../tests/runner-
|
||||
// It measures complete Paperclip-supplied instruction/tool/message projections,
|
||||
// not provider-owned stock prompts, tokenization, billing or model behavior.
|
||||
const tools = PAPERCLIP_SEMANTIC_ACTION_CATALOG
|
||||
.filter(tool => tool.placement === "core")
|
||||
.filter(tool => tool.placement === "always")
|
||||
.map(tool => ({ name: tool.operationId, description: tool.description, inputSchema: tool.inputSchema }));
|
||||
const identity = { companyId: "company", agentId: "agent", issueId: "issue", runId: "run", sessionId: "session" };
|
||||
const { qualificationModel: _qualificationModel, reportedModelId: _reportedModelId, permissionPolicy: _permissionPolicy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-sonnet-5");
|
||||
@@ -182,7 +182,7 @@ describe("direct OpenCode HTTP instruction boundary", () => {
|
||||
try {
|
||||
const checkpoint = await session.snapshot();
|
||||
const send = async (current: typeof session, phase: string, value: NativeExecutionInput) => {
|
||||
const envelope = buildNativeModelEnvelope(value, { resumedSession: phase === "continuation" });
|
||||
const envelope = phase === "continuation" ? buildNativeModelEnvelope(value, { resumedSession: true }) : buildNativeModelEnvelope(value);
|
||||
if ("requestedSkills" in envelope && backend.preparedTaskConstraints) envelope.constraints = [...backend.preparedTaskConstraints];
|
||||
await current.startTurn({ message: { role: "user", text: JSON.stringify(envelope) } });
|
||||
for await (const event of current.events()) { if (event.eventType === "turn.completed") break; if (["turn.failed", "turn.cancelled"].includes(event.eventType)) throw new Error(`Fixture turn failed: ${event.eventType}`); }
|
||||
|
||||
@@ -54,16 +54,20 @@ describe("public runner browser client", () => {
|
||||
fetchImpl: vi.fn() as unknown as typeof fetch,
|
||||
eventSourceFactory: (nextUrl) => { url = nextUrl; return source; },
|
||||
});
|
||||
const event = {
|
||||
const event: PrpEvent = {
|
||||
schema: "paperclip.prp.event.v1",
|
||||
sourceEventId: "event-9",
|
||||
sourceKind: "harness",
|
||||
sourceKind: "runner",
|
||||
sourceInstanceId: "driver",
|
||||
sourceSeq: 9,
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session/id",
|
||||
schemaVersion: 1,
|
||||
priority: 2,
|
||||
emittedAt: "2026-08-08T00:00:00.000Z",
|
||||
eventType: "item.delta",
|
||||
payload: { text: "exact" },
|
||||
} as PrpEvent;
|
||||
};
|
||||
const onEvent = vi.fn();
|
||||
const onError = vi.fn();
|
||||
const handle = client.openEventStream("session/id", 8, { onEvent, onError });
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { LiveConsoleScriptedDriver } from "../mock-core/live-console-scripted-driver";
|
||||
import { LiveConsoleScriptedDriver } from "../mock-core/live-console-scripted-driver.js";
|
||||
import {
|
||||
parseHarnessRuntimeRequestResolution,
|
||||
type HarnessSession,
|
||||
} from "../contracts/harness-driver";
|
||||
import type { PrpEvent } from "../protocol/replay-contract";
|
||||
} from "../contracts/harness-driver.js";
|
||||
import type { PrpEvent } from "../protocol/replay-contract.js";
|
||||
import {
|
||||
applyPrpEvent,
|
||||
createSessionSnapshotFromMetadata,
|
||||
reduceSessionEvents,
|
||||
type SessionSnapshot,
|
||||
} from "../reducer/session-reducer";
|
||||
} from "../reducer/session-reducer.js";
|
||||
import type { RunnerCapabilities } from "./protocol.js";
|
||||
import {
|
||||
buildTranscript,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PAPERCLIP_PROTOCOL_ACTIONS } from "../protocol-actions/index.js";
|
||||
@@ -50,8 +50,9 @@ describe("canonical Paperclip protocol action contracts", () => {
|
||||
).toEqual([]);
|
||||
if (action.scenario.descriptor.idempotency === "required") {
|
||||
expect(action.examples).toHaveProperty("scenarioCall");
|
||||
expect(scenarioCall.idempotencyKey).toEqual(expect.any(String));
|
||||
expect(scenarioCall.idempotencyKey).not.toHaveLength(0);
|
||||
const { idempotencyKey } = scenarioCall as { idempotencyKey?: string };
|
||||
expect(idempotencyKey).toEqual(expect.any(String));
|
||||
expect(idempotencyKey).not.toHaveLength(0);
|
||||
} else {
|
||||
expect(scenarioCall).not.toHaveProperty("idempotencyKey");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createTaskAction } from "../protocol-actions/create-task.js";
|
||||
import { createProjectAction } from "../protocol-actions/create-project.js";
|
||||
|
||||
@@ -449,7 +449,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
// The receipt hashes the actual emitted input, not a second normalization.
|
||||
await loadActualToolCommands(tools).resolve({ callId: "3", turnId: "test-turn", result: { accepted: true }, error: null });
|
||||
}
|
||||
const body = await (await response).json();
|
||||
const body = await (await response).json() as ToolCallResponseBody;
|
||||
const receipt = readNativeSemanticReceipt({ contents: body.result.content });
|
||||
expect(receipt).toMatchObject({ schema: "paperclip.semantic_tool_receipt.v2", inputSha256: semanticInputSha256(raw),
|
||||
normalizedInputSha256: mode === "forwarded" ? semanticInputSha256(emitted[0].input) : null,
|
||||
@@ -490,7 +490,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
expect(validatePrpStructuredRunResult(emitted[1].input).ok).toBe(true);
|
||||
await commands.resolve({ callId: "3", turnId: "test-turn", result: { accepted: true }, error: null });
|
||||
}
|
||||
const body = await (await response).json();
|
||||
const body = await (await response).json() as ToolCallResponseBody;
|
||||
const receipt = readNativeSemanticReceipt({ contents: body.result.content });
|
||||
expect(receipt).toMatchObject({ inputSha256: semanticInputSha256(raw),
|
||||
normalizedInputSha256: eligible ? semanticInputSha256(emitted[1].input) : null, outcome: eligible ? "returned" : "error" });
|
||||
@@ -517,7 +517,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
await vi.waitFor(() => expect(emitted).toHaveLength(1));
|
||||
await expect(handler(call)).rejects.toThrow("duplicated");
|
||||
} finally { controller.abort(); }
|
||||
expect((await result).message).toContain("cancelled");
|
||||
expect((await result as Error).message).toContain("cancelled");
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
expect(tools.size).toBe(0);
|
||||
expect(emitted[0].operationId).toBe("get_task_context");
|
||||
@@ -547,7 +547,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
const bridge = await startRunnerToolBridge({ handler, captureSemanticReceipt: () => () => {} });
|
||||
try {
|
||||
const body = await (await fetch(bridge.url, { method: "POST", headers: { Authorization: `Bearer ${bridge.secret}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request) })).json();
|
||||
body: JSON.stringify(request) })).json() as ToolCallResponseBody;
|
||||
expect(body.result.isError).toBe(true);
|
||||
expect(body.result.content[0].text).toBe("ACPX semantic tool call exceeds the sidecar frame limit");
|
||||
expect(readNativeSemanticReceipt({ contents: body.result.content })).toMatchObject({ outcome: "error", normalizedInputSha256: null });
|
||||
@@ -561,7 +561,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
} finally { await bridge.close(); }
|
||||
});
|
||||
|
||||
it.each(["backpressure", "throws"])("uses actual writer acceptance, preserving %s semantics", async mode => {
|
||||
it.each(["backpressure", "throws"] as const)("uses actual writer acceptance, preserving %s semantics", async mode => {
|
||||
const wire = loadActualSidecarWriter(mode);
|
||||
const tools = new Map<string, any>(), emitted: any[] = [], commit = vi.fn(), capture = vi.fn((_input: unknown) => commit);
|
||||
const raw = { reportedWorkDisposition: "done", summary: "Complete", evidence: [], verification: [],
|
||||
@@ -727,7 +727,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
expect(Buffer.byteLength(JSON.stringify(JSON.parse(wire.writes[0]!).payload))).toBeGreaterThan(256 * 1024);
|
||||
expect(Buffer.byteLength(wire.writes[0]!)).toBeLessThan(ACPX_SIDECAR_MAX_FRAME_BYTES);
|
||||
}
|
||||
const body = await (await response).json();
|
||||
const body = await (await response).json() as ToolCallResponseBody;
|
||||
if (mode === "error") {
|
||||
expect(JSON.stringify(body)).not.toContain(credentialCanary);
|
||||
expect(JSON.stringify(body)).toContain("[REDACTED]");
|
||||
@@ -820,8 +820,9 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
const sanitize = new Function("boundedOptionalText", "stableProviderIdentity", `return event => {${source.slice(start, end)} return null;}`)(
|
||||
(value: string) => value, (value: string) => `stable-${value}`,
|
||||
);
|
||||
const projection = createPiMessageProjection<{ type: string; text: string; stream: string; messageId: string; meta: Record<string, string> }>();
|
||||
const event = (kind: string) => ({ type: "text_delta", text: "", stream: "output", messageId: `pi-message-${"a".repeat(64)}`,
|
||||
type PiTextFrame = { type: string; text: string; stream: string; messageId: string; meta: Record<string, string> };
|
||||
const projection = createPiMessageProjection<PiTextFrame>();
|
||||
const event = (kind: string): PiTextFrame => ({ type: "text_delta", text: "", stream: "output", messageId: `pi-message-${"a".repeat(64)}`,
|
||||
meta: { origin: "pi-native-assistant", source: "pi-rpc-message-v1", kind, secret: "DROP_ME" } });
|
||||
const startFrame = sanitize(projection.normalize(event("start")));
|
||||
const endFrame = sanitize(projection.normalize(event("end:toolUse")));
|
||||
@@ -830,7 +831,7 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
expect(startFrame.messageId).toBe(endFrame.messageId);
|
||||
expect(JSON.stringify([startFrame, endFrame])).not.toContain("DROP_ME");
|
||||
projection.settle();
|
||||
const loaded = createPiMessageProjection<ReturnType<typeof event>>();
|
||||
const loaded = createPiMessageProjection<PiTextFrame>();
|
||||
const history = sanitize(loaded.normalize({ ...event("history"), messageId: `pi-history-message-${"b".repeat(64)}`,
|
||||
text: "old reply", meta: { origin: "pi-history-assistant", source: "pi-session-history-v1", kind: "history" } }));
|
||||
expect(history).toMatchObject({ text: "old reply", piMessageHistory: true }); loaded.settle();
|
||||
@@ -1744,6 +1745,8 @@ class SidecarProcess {
|
||||
}
|
||||
}
|
||||
|
||||
type ToolCallResponseBody = { result: { content: Array<{ text: string }>; isError?: boolean } };
|
||||
|
||||
function loadWaitForTool(input: {
|
||||
tools: Map<string, unknown>;
|
||||
emitted: unknown[];
|
||||
|
||||
@@ -54,7 +54,9 @@ describe("ACPX sidecar input sequencing", () => {
|
||||
async () => {
|
||||
events.push(command);
|
||||
},
|
||||
() => events.push(`${command}:error`),
|
||||
() => {
|
||||
events.push(`${command}:error`);
|
||||
},
|
||||
);
|
||||
}
|
||||
const shutdown = pending.then(() => events.push("shutdown"));
|
||||
@@ -87,7 +89,9 @@ describe("ACPX sidecar input sequencing", () => {
|
||||
async () => {
|
||||
events.push("recovered");
|
||||
},
|
||||
() => events.push("unexpected"),
|
||||
() => {
|
||||
events.push("unexpected");
|
||||
},
|
||||
);
|
||||
|
||||
await recovered;
|
||||
@@ -113,7 +117,9 @@ describe("ACPX sidecar input sequencing", () => {
|
||||
async () => {
|
||||
events.push("recovered");
|
||||
},
|
||||
() => events.push("unexpected"),
|
||||
() => {
|
||||
events.push("unexpected");
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("eval-session request contract", () => {
|
||||
const value = request({ provider: "acpx", acpxAgent: agent, model: "explicit-provider-model" });
|
||||
expect(() => parseEvalSessionRequest(value)).toThrow("--candidate-profile");
|
||||
expect(parseEvalSessionRequest(value, { candidateProfile: agent })).toMatchObject({ acpxAgent: agent, model: "explicit-provider-model" });
|
||||
expect(() => parseEvalSessionRequest(value, { candidateProfile: agent === "pi" ? "cursor" : "pi" })).toThrow("must match");
|
||||
expect(() => parseEvalSessionRequest(value, { candidateProfile: "pi" })).toThrow("must match");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, model: "" }), { candidateProfile: agent })).toThrow("request.model");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: "codex", session: { acpxAgent: agent } }))).toThrow("session.acpxAgent must match");
|
||||
expect(() => parseEvalSessionRequest(request({ provider: "acpx", acpxAgent: agent, candidateProfile: agent }))).toThrow("--candidate-profile");
|
||||
@@ -525,10 +525,10 @@ describe("eval-session usage", () => {
|
||||
providerReported: { nanodollars: null, recordedAttempts: 0 },
|
||||
estimated: { nanodollars: 6_200_000, recordedAttempts: 1 },
|
||||
});
|
||||
expect(snapshot.usageLedger[0]?.costNanodollars).toBe(legacyCost);
|
||||
expect(snapshot.usageLedger![0]?.costNanodollars).toBe(legacyCost);
|
||||
const limitedRequest = parseEvalSessionRequest(request());
|
||||
limitedRequest.limits.maxEstimatedCostNanodollars = 10_000_000;
|
||||
const costlySnapshot = { ...snapshot, usageLedger: [{ ...snapshot.usageLedger[0]!, costNanodollars: 10_000_001 }] };
|
||||
const costlySnapshot = { ...snapshot, usageLedger: [{ ...snapshot.usageLedger![0]!, costNanodollars: 10_000_001 }] };
|
||||
expect(() => boundedEvalSessionUsage(limitedRequest, {
|
||||
turnId: "codex-turn", status: "completed", assistantText: "Completed", snapshot: costlySnapshot,
|
||||
})).toThrow("legacy ledger cost limit exceeded");
|
||||
@@ -609,7 +609,7 @@ describe("eval-session budget settlement", () => {
|
||||
});
|
||||
const input = request({ provider: "acpx", acpxAgent: agent, model,
|
||||
runnerd: { path: binary, sha256: createHash("sha256").update("unused fake runner").digest("hex") },
|
||||
session: { workingDirectory: workspace, ...(agent === "pi" ? { piThinkingLevel: "low" } : {}) }, limits: { turnTimeoutMs: 1000, maxAgentTurns: 2, maxEstimatedCostNanodollars: 100_000_000 },
|
||||
session: { workingDirectory: workspace }, limits: { turnTimeoutMs: 1000, maxAgentTurns: 2, maxEstimatedCostNanodollars: 100_000_000 },
|
||||
});
|
||||
const requestPath = join(workspace, "request.json");
|
||||
const outputPath = join(workspace, "output.json");
|
||||
|
||||
@@ -17,14 +17,18 @@ describe("OpenCode proxy input sequencing", () => {
|
||||
await firstBlocked;
|
||||
events.push("initialize:complete");
|
||||
},
|
||||
() => events.push("initialize:error"),
|
||||
() => {
|
||||
events.push("initialize:error");
|
||||
},
|
||||
);
|
||||
pending = enqueueOpenCodeProxyInput(
|
||||
pending,
|
||||
async () => {
|
||||
events.push("thread:start");
|
||||
},
|
||||
() => events.push("thread:error"),
|
||||
() => {
|
||||
events.push("thread:error");
|
||||
},
|
||||
);
|
||||
const shutdown = pending.then(() => {
|
||||
events.push("shutdown");
|
||||
@@ -50,7 +54,9 @@ describe("OpenCode proxy input sequencing", () => {
|
||||
async () => {
|
||||
throw new Error("initialize failed");
|
||||
},
|
||||
(error) => events.push((error as Error).message),
|
||||
(error) => {
|
||||
events.push((error as Error).message);
|
||||
},
|
||||
);
|
||||
|
||||
await pending;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import type { NativeExecutionInput, NativeExecutionInputV1 } from "../contracts/native-execution.js";
|
||||
import type {
|
||||
NativeRunIdentity,
|
||||
NativeSessionCapabilities,
|
||||
@@ -29,8 +29,8 @@ type ProfileKey = "codex" | "opencode" | "acpx-claude" | "acpx-codex";
|
||||
|
||||
interface LocalNativeProfile {
|
||||
key: ProfileKey;
|
||||
driverKind: NativeExecutionInput["session"]["driverKind"];
|
||||
provider: NativeExecutionInput["provider"];
|
||||
driverKind: NativeExecutionInputV1["session"]["driverKind"];
|
||||
provider: NativeExecutionInputV1["provider"];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeLegacyPrpStructuredRunResult } from "../protocol/result-normalization.js";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
PRP_BLOCK_RESULT_OUTPUT_SCHEMA,
|
||||
|
||||
@@ -76,5 +76,5 @@ describe("ProviderTraceFileSink", () => {
|
||||
});
|
||||
|
||||
function statMode(value: Awaited<ReturnType<typeof stat>>): number {
|
||||
return value.mode & 0o777;
|
||||
return Number(value.mode) & 0o777;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { questionSetSchema } from "../protocol/generated/schema-bundle.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ it("settles a create rejected during real-facade Stop before cold recovery and t
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000) };
|
||||
const options = { provider: "cursor_cloud" as const, cursorCloud, stateDirectory, runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand,
|
||||
cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" as const }, environment };
|
||||
lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null }, environment };
|
||||
const first = createRunnerdCodexTransport({ ...options, prpIdentity: identity }), bundles = [first];
|
||||
let stop: Promise<unknown> | undefined;
|
||||
try {
|
||||
@@ -169,7 +169,7 @@ it("opens and closes the real Cloud facade without a provider prompt", async ()
|
||||
runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand,
|
||||
cursorCloudProxyPath: proxyPath,
|
||||
cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" },
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
prpIdentity: { runnerInstanceId: "runner-cloud-launch", environmentLeaseId: "lease-cloud-launch",
|
||||
runId: "00000000-0000-4000-8000-000000000761", normalizedSessionId, turnId: "turn-cloud-launch", itemId: "item-cloud-launch" },
|
||||
environment: {
|
||||
@@ -249,7 +249,7 @@ it("preserves native Cloud tool identity and redacted output through the real Ru
|
||||
provider: "cursor_cloud", cursorCloud, stateDirectory: join(root, "runner-session"),
|
||||
runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath,
|
||||
cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" },
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
prpIdentity: { runnerInstanceId: "runner-cloud-tool", environmentLeaseId: "lease-cloud-tool",
|
||||
runId: "00000000-0000-4000-8000-000000000762", normalizedSessionId, turnId: "turn-cloud-tool", itemId: "item-cloud-tool" },
|
||||
environment: {
|
||||
@@ -343,7 +343,7 @@ async function cloudQuestionHandoff(terminalDelayMs: number, governed: boolean,
|
||||
provider: "cursor_cloud", cursorCloud, stateDirectory: join(root, "runner-session"),
|
||||
runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand, cursorCloudProxyPath: proxyPath,
|
||||
cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" },
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
prpIdentity: { runnerInstanceId: "runner-cloud-question", environmentLeaseId: "lease-cloud-question",
|
||||
runId: "00000000-0000-4000-8000-000000000763", normalizedSessionId, turnId: "turn-cloud-question", itemId: "item-cloud-question" },
|
||||
environment: {
|
||||
@@ -405,7 +405,7 @@ async function cloudQuestionHandoff(terminalDelayMs: number, governed: boolean,
|
||||
const execution = executeNativeSession({ input, backend, runnerInstanceId: "runner-cloud-question", controlPlaneInstanceId: "control-cloud-question", timeoutMs: 120_000,
|
||||
controlPlane: {
|
||||
async openRun() {}, async checkpointSession() {}, completeRun,
|
||||
async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(replay) { return { events: history.filter(event => event.sourceInstanceId === replay.sourceInstanceId && event.sourceSeq > replay.afterSourceSeq), highestContiguousSourceSeq: 0 }; },
|
||||
},
|
||||
resolveGovernedWait: ({ event }) => event.eventType === "item.completed" && event.payload.kind === "dynamicToolCall" ? yielded : null,
|
||||
@@ -520,7 +520,7 @@ async function cloudColdAuthorityRotation(instructions: string, validShutdown =
|
||||
PAPERCLIP_CURSOR_CLOUD_RELAY_URL: `https://relay.example.test/mcp/native-runs/${runId}`, PAPERCLIP_CURSOR_CLOUD_RELAY_EXPIRES_AT: String(Date.now() + 60_000) });
|
||||
const options = { provider: "cursor_cloud" as const, cursorCloud, stateDirectory, runnerBinary: defaultCapabilityRunnerdBinary(), providerNodeCommand,
|
||||
cursorCloudProxyPath: proxyPath, cursorCloudProxySha256: `sha256:${createHash("sha256").update(await readFile(proxyPath)).digest("hex")}`,
|
||||
lifecyclePolicy: { mode: "per_turn" as const } };
|
||||
lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null } };
|
||||
const mcp = async (token: string, body: Record<string, unknown>, sessionId?: string, priorScopeHeader?: string): Promise<{ status: number; sessionId?: string; value: any }> => {
|
||||
const scopeHeader = priorScopeHeader ?? await currentCloudScopeHeader(runtime, normalizedSessionId);
|
||||
return new Promise((accept, reject) => {
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
inspectRetainedRunInstructions,
|
||||
inspectStagedRunAttachmentHeader,
|
||||
type DurablePrpCheckpoint,
|
||||
type DurablePrpControlPlaneStore,
|
||||
type RunnerProcessHandle,
|
||||
durableRecoveryInternals,
|
||||
SemanticToolNotDispatchedError,
|
||||
inspectWarmRunTransition,
|
||||
@@ -48,6 +50,16 @@ import {
|
||||
} from "./durable-prp-control-plane.js";
|
||||
import type { DurableRecoveryIdentity } from "./prp-transport-types.js";
|
||||
|
||||
type WritableCoreStore = DurablePrpControlPlaneStore & {
|
||||
save(): void;
|
||||
commit(candidate: DurablePrpControlPlaneStore["state"]): void;
|
||||
};
|
||||
|
||||
function restartRunner(handle: RunnerProcessHandle, ticket: string): void {
|
||||
if (!handle.restart) throw new Error("Runner handle does not support restart");
|
||||
handle.restart(ticket);
|
||||
}
|
||||
|
||||
const identity: DurableRecoveryIdentity = {
|
||||
runnerInstanceId: "runner-test-1",
|
||||
environmentLeaseId: "environment-test-1",
|
||||
@@ -229,7 +241,7 @@ it("recovers only the matching run's admitted instructions from a valid checkpoi
|
||||
expect(inspectRetainedRunInstructions(core.store.state, { ...targetExpected, runtimeContextDigest: digest })).toBeNull();
|
||||
expect(inspectRetainedRunInstructions(core.store.state, expected)).toBe(template.provider.instructions);
|
||||
const corrupted = structuredClone(core.store.state);
|
||||
corrupted.stagedRunAttachment!.templateDigest = `sha256:${"f".repeat(64)}`;
|
||||
corrupted.stagedRunAttachment = { ...corrupted.stagedRunAttachment!, templateDigest: `sha256:${"f".repeat(64)}` };
|
||||
expect(inspectRetainedRunInstructions(corrupted, targetExpected)).toBeNull();
|
||||
} finally { await core.stop(); rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -545,7 +557,7 @@ it("excludes streamed output from checkpoints even when the live window is full"
|
||||
anchor.eventType = "turn.started";
|
||||
(anchor.envelope.payload as Record<string, unknown>).eventType = "turn.started";
|
||||
core.store.state.ackedSourceSeq = 4096;
|
||||
core.store.save();
|
||||
(core.store as WritableCoreStore).save();
|
||||
const snapshots: DurablePrpCheckpoint[] = [];
|
||||
core.setCheckpointPersistence(async saved => { snapshots.push(saved); });
|
||||
try {
|
||||
@@ -918,7 +930,7 @@ it.each([false, true])("dispatches a fresh semantic input once when recovery ove
|
||||
if (JSON.parse(saved.records.get("cursor")!).ackedSourceSeq > 0) { writeHeld = true; await writeBarrier; }
|
||||
});
|
||||
const pending: Promise<void>[] = [];
|
||||
let client: Awaited<ReturnType<typeof authenticate>>;
|
||||
let client: AuthenticatedClient | null = null;
|
||||
try {
|
||||
await core.start();
|
||||
client = (await authenticate(core, core.issueBootstrapTicket()))!;
|
||||
@@ -1149,7 +1161,7 @@ it.each(["event", "progress", "lifecycle", "lifecycle_priority_one", "usage", "c
|
||||
expect(() => core.queueCommand("session.snapshot")).toThrow("indeterminate");
|
||||
}
|
||||
if (mode === "authority_conflict") {
|
||||
core.store.commit(structuredClone(core.store.state));
|
||||
(core.store as WritableCoreStore).commit(structuredClone(core.store.state));
|
||||
core.queueCommand("session.snapshot", {}, "command_after_authority");
|
||||
expect(JSON.parse(readFileSync(core.store.path, "utf8")).ackedSourceSeq).toBe(0);
|
||||
expect(core.store.state.ackedSourceSeq).toBe(0);
|
||||
@@ -1817,7 +1829,7 @@ it("pins the ACPX launch profile in runner startup arguments and restarts", () =
|
||||
},
|
||||
});
|
||||
|
||||
handle.restart("replacement-ticket");
|
||||
restartRunner(handle, "replacement-ticket");
|
||||
expect(launches).toHaveLength(2);
|
||||
for (const launch of launches) {
|
||||
expect(launch.args).toContain("--acpx-launch-authority-digest");
|
||||
@@ -1869,7 +1881,7 @@ it("pins the OpenCode launch profile in runner startup arguments and restarts",
|
||||
},
|
||||
});
|
||||
|
||||
handle.restart("replacement-ticket");
|
||||
restartRunner(handle, "replacement-ticket");
|
||||
expect(launches).toHaveLength(2);
|
||||
for (const launch of launches) {
|
||||
expect(launch.args).toEqual(
|
||||
@@ -2057,7 +2069,7 @@ it("preserves only bounded GitHub credential projection at the runner spawn boun
|
||||
it("passes structured inputs to an external owner and preserves its replacement veto", () => {
|
||||
const process = { child: { exitCode: null, signalCode: null, kill: () => true },
|
||||
completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }), restart: null };
|
||||
const launcher = vi.fn(() => process);
|
||||
const launcher = vi.fn((_spec: RunnerProcessLaunchSpec) => process);
|
||||
const options = { connection: { mode: "external" as const }, stateDirectory: "/unused", identity,
|
||||
ticket: "synthetic-ticket", maxOutboxBytes: 262144, p0ReserveBytes: 65536,
|
||||
runnerVersion: expectedRunnerVersion, runnerDigest: expectedRunnerDigest,
|
||||
@@ -2065,7 +2077,7 @@ it("passes structured inputs to an external owner and preserves its replacement
|
||||
expect(() => spawnRunner(options)).toThrow("requires a process owner");
|
||||
expect(spawnRunner({ ...options, processLauncher: launcher })).toBe(process);
|
||||
expect(launcher).toHaveBeenCalledWith(expect.objectContaining({ nativeRunner: expect.objectContaining(options) }));
|
||||
expect(launcher.mock.calls[0][0].args).not.toContain("--connect-url");
|
||||
expect(launcher.mock.calls[0]![0].args).not.toContain("--connect-url");
|
||||
});
|
||||
|
||||
it("preserves the controller-selected ACPX provider package root", () => {
|
||||
@@ -2147,7 +2159,7 @@ it.each([
|
||||
completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }) };
|
||||
},
|
||||
});
|
||||
handle.restart("replacement-ticket");
|
||||
restartRunner(handle, "replacement-ticket");
|
||||
for (const launch of launches.splice(0)) {
|
||||
expect(launch.environment[key]).toBe(explicit ? "explicit-fixture-credential" : undefined);
|
||||
const receipt = launch.environment[ACPX_CREDENTIAL_BINDING_ENV];
|
||||
|
||||
@@ -19,37 +19,41 @@ function snapshot(): CapabilityLiveSessionSnapshot {
|
||||
current.lifecycle = "running";
|
||||
current.artifacts.push({
|
||||
id: "artifact-1",
|
||||
companyId: "company-1",
|
||||
taskId: "task-1",
|
||||
runId: "run-1",
|
||||
kind: "text",
|
||||
name: "result.txt",
|
||||
filename: "result.txt",
|
||||
contentType: "text/plain",
|
||||
byteSize: 6,
|
||||
sha256: "a".repeat(64),
|
||||
contentRef: "file:///private/result.txt",
|
||||
metadata: {},
|
||||
createdAt: "2026-08-09T00:00:01.000Z",
|
||||
});
|
||||
return {
|
||||
id: "session-1",
|
||||
schema: "paperclip.capability.live-session.v1",
|
||||
sessionId: "session-1",
|
||||
revision: 1,
|
||||
status: "running",
|
||||
activeTurnId: null,
|
||||
providerThreadId: "thread-1",
|
||||
providerSessionId: "provider-session-1",
|
||||
providerModel: "gpt-test",
|
||||
providerModel: { id: "gpt-test", provider: "openai" },
|
||||
config: {
|
||||
seedState: JSON.stringify(seed),
|
||||
workingDirectory: "/private/worktree",
|
||||
scenario: "hb-baseline",
|
||||
scenario: { id: "hb-baseline" },
|
||||
capabilities: ["task:read"],
|
||||
explicitClaims: [],
|
||||
turnTimeoutMs: 1_000,
|
||||
},
|
||||
authority: {
|
||||
active: true,
|
||||
companyId: "company-1",
|
||||
actorId: "actor-1",
|
||||
taskId: "task-1",
|
||||
runId: "run-1",
|
||||
sessionId: "session-1",
|
||||
scenarioId: "hb-baseline",
|
||||
capabilities: ["task:read"],
|
||||
explicitClaims: [],
|
||||
},
|
||||
transcript: [],
|
||||
evidence: [{
|
||||
@@ -64,8 +68,9 @@ function snapshot(): CapabilityLiveSessionSnapshot {
|
||||
},
|
||||
}],
|
||||
mockState: JSON.stringify(current),
|
||||
authorizationRecords: [],
|
||||
process: null,
|
||||
networkEvidence: [],
|
||||
networkEvidence: { realPaperclipRequests: 0, childPaperclipEnvironmentKeys: [] },
|
||||
createdAt: "2026-08-09T00:00:00.000Z",
|
||||
updatedAt: "2026-08-09T00:00:01.000Z",
|
||||
stateHistory: [{
|
||||
|
||||
@@ -168,5 +168,6 @@ it("rejects an unacknowledged thinking-level change after a native family model
|
||||
const f = await fixture("family-change"), owner = await f.open();
|
||||
const result = await f.turn(owner);
|
||||
expect(result.status).toBe("failed");
|
||||
if (result.status !== "failed") throw new Error("turn did not fail");
|
||||
expect(result.error?.message).toMatch(/admission/i);
|
||||
});
|
||||
@@ -22,7 +22,7 @@ function response(message: ReturnType<typeof request>, changes: Record<string, u
|
||||
sessionId: "native-session", models: { currentModelId: model }, modes: { currentModeId: "default" },
|
||||
_meta: { paperclipAntigravityAdmission: {
|
||||
...meta.antigravity.paperclipAdmission, sessionId: "native-session", connectionId: "native-connection", ...changes,
|
||||
} },
|
||||
} as Record<string, unknown> },
|
||||
} };
|
||||
}
|
||||
const prompt = () => ({ id: "turn", method: "session/prompt", params: { sessionId: "native-session", prompt: [{ type: "text", text: "Do the task" }] } });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { appendSemanticToolReceipt } from "../semantic-tool-receipt.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { cursorToolIdentity } from "./cursor-plan-tool-identity.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it, vi, type Mock } from "vitest";
|
||||
import { AcpxProviderProcessLostError } from "./codex-runtime-adapter.js";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "./qualified-profiles.js";
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
it("captures the direct driver's normalized input only when the exact proposal is retained", async () => {
|
||||
const fixture = driverFixture({ agent: "copilot", model: "explicit-test-model", providerPolicy: { readOnly: true } }, { runtimeEvents: [] });
|
||||
const session = await fixture.driver.openSession({ runId: "run-normalized-receipt", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
await session.startTurn({ message: { text: "Complete" } });
|
||||
await session.startTurn({ message: { role: "user", text: "Complete" } });
|
||||
const { schema: _, attentionRequests: __, artifacts: ___, ...raw } = completedResult();
|
||||
const commit = vi.fn(), capture = vi.fn((value: unknown) => { expect(commit).not.toHaveBeenCalled(); return commit; });
|
||||
const handler = fixture.hostOptions!.semanticTools!.handler;
|
||||
@@ -58,7 +58,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
runtimeEvents: Array.from({ length: 8 }, (_, n) => ({ type: "text_delta" as const, stream: "output" as const, text: `bounded-${n}` })),
|
||||
});
|
||||
const session = await fixture.driver.openSession({ runId: "run-normalized-pressure", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
await session.startTurn({ message: { text: "Complete" } });
|
||||
await session.startTurn({ message: { role: "user", text: "Complete" } });
|
||||
await vi.waitFor(async () => expect((await session.transcript!()).eventCount).toBeGreaterThanOrEqual(4));
|
||||
const commit = vi.fn(), capture = vi.fn(() => commit);
|
||||
await expect(fixture.hostOptions!.semanticTools!.handler({ tool: PRP_COMPLETION_TOOL_NAME, callId: "finish", arguments: completedResult(),
|
||||
@@ -107,23 +107,23 @@ describe("Codex ACPX harness driver", () => {
|
||||
},
|
||||
} };
|
||||
fixture.host.status.mockResolvedValueOnce(before as never).mockResolvedValue(after as never);
|
||||
await session.startTurn({ message: { text: "fixture" } });
|
||||
await session.startTurn({ message: { role: "user", text: "fixture" } });
|
||||
fixture.finishTurn({ status: "completed", stopReason: "end_turn" });
|
||||
const events = await collectUntil(session.events(), "turn.completed");
|
||||
const notices = events.filter(e => e.eventType === "provider.notice.recorded" && e.payload.category === "cursor_native_usage_observed");
|
||||
expect(notices).toHaveLength(agent === "cursor" ? 1 : 0);
|
||||
expect(events.filter(e => e.eventType === "usage.updated")).toEqual([]);
|
||||
expect(events.filter(e => e.eventType === "usage.reported")).toEqual([]);
|
||||
expect(events.at(-1)?.eventType).toBe("turn.completed");
|
||||
await session.close({ reason: "partial diagnostics verified" });
|
||||
});
|
||||
|
||||
it.each(["tool-1", "tool with spaces", "🧭/plan", "x".repeat(300)])("binds a native Cursor plan request to its actual projected tool identity: %s", async toolCallId => {
|
||||
const fixture = driverFixture({ agent: "cursor", model: "explicit-test-model", mode: "plan", providerPolicy: { readOnly: false } }, {
|
||||
runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "arbitrary tool display", kind: "execute", status: "pending" }],
|
||||
runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "arbitrary tool display", kind: "execute", status: "pending", text: "arbitrary tool display" }],
|
||||
});
|
||||
const session = await fixture.driver.openSession({ runId: "run-plan-identity", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const toolEvents = collectUntil(session.events(), "tool.execution.started");
|
||||
const { turnId } = await session.startTurn({ message: { text: "Plan" } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Plan" } });
|
||||
const tool = (await toolEvents).find(e => e.eventType === "tool.execution.started")!;
|
||||
const created = collectUntil(session.events(), "runtime_request.created");
|
||||
const callback = fixture.host.startTurn.mock.calls[0]![0].onExtensionRequest!;
|
||||
@@ -145,7 +145,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
];
|
||||
const fixture = driverFixture({ agent, model: "explicit-model", providerPolicy: { readOnly: true } }, { runtimeEvents });
|
||||
const session = await fixture.driver.openSession({ runId: "other-provider-identity", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const { turnId } = await session.startTurn({ message: { text: "fixture" } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "fixture" } });
|
||||
fixture.finishTurn({ status: "completed" });
|
||||
const events = await collectUntil(session.events(), "turn.completed");
|
||||
const messages = events.filter(event => event.payload.kind === "agentMessage");
|
||||
@@ -162,7 +162,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
runtimeEvents.push(chunk(undefined, "Info: native session activity"));
|
||||
const fixture = driverFixture({ agent: "copilot", model: "explicit-model", providerPolicy: { readOnly: true } }, { runtimeEvents });
|
||||
const session = await fixture.driver.openSession({ runId: "run-copilot-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
await session.startTurn({ message: { text: "Answer exactly" } });
|
||||
await session.startTurn({ message: { role: "user", text: "Answer exactly" } });
|
||||
fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" });
|
||||
const events = await collectUntil(session.events(), scenario === "cancelled" ? "turn.interrupted" : "turn.completed");
|
||||
const interim = events.find(event => event.eventType === "item.delta" && event.payload.text === "EXACT_MARKER");
|
||||
@@ -179,7 +179,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
it("keeps Pi retry-only activity as a rich notice without inventing a final answer", async () => {
|
||||
const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents: [] });
|
||||
const session = await fixture.driver.openSession({ runId: "run-pi-notice", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
await session.startTurn({ message: { text: "Work" } });
|
||||
await session.startTurn({ message: { role: "user", text: "Work" } });
|
||||
const input = fixture.host.startTurn.mock.calls[0]![0];
|
||||
await input.onExtensionNotification!("paperclip/pi_notice", { sessionId: "backend-1", category: "auto_retry_end", summary: "Retry did not recover.", severity: "warning", details: { attempt: 2, success: false } });
|
||||
fixture.finishTurn({ status: "completed" });
|
||||
@@ -201,7 +201,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
}
|
||||
const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents });
|
||||
const session = await fixture.driver.openSession({ runId: "run-pi-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
await session.startTurn({ message: { text: "Answer exactly" } });
|
||||
await session.startTurn({ message: { role: "user", text: "Answer exactly" } });
|
||||
fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" });
|
||||
const events = await collectUntil(session.events(), scenario === "missing-end" ? "turn.failed" : scenario === "cancelled" ? "turn.interrupted" : "turn.completed");
|
||||
expect(events.some(event => event.eventType === "item.delta" && event.payload.text === "Calling finish.")).toBe(true);
|
||||
@@ -233,7 +233,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
}
|
||||
const fixture = driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { runtimeEvents });
|
||||
const session = await fixture.driver.openSession({ runId: "run-kimi-boundaries", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const { turnId } = await session.startTurn({ message: { text: "Answer exactly" } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Answer exactly" } });
|
||||
fixture.finishTurn({ status: scenario === "cancelled" ? "cancelled" : "completed" });
|
||||
const events = await collectUntil(session.events(), scenario === "missing-end" ? "turn.failed" : scenario === "cancelled" ? "turn.interrupted" : "turn.completed");
|
||||
const progress = events.find(event => event.eventType === "item.delta" && event.payload.text === "Calling finish.")!;
|
||||
@@ -256,34 +256,34 @@ describe("Codex ACPX harness driver", () => {
|
||||
const session = await fixture.driver.openSession({ runId: "run-controls", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
expect(fixture.hostOptions?.providerPolicy).toEqual({ readOnly: true });
|
||||
expect(session.turnControlCapabilities?.()).toEqual({ steering: true, queuedFollowUp: true });
|
||||
const { turnId } = await session.startTurn({ message: { text: "Work" } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } });
|
||||
const turnInput = fixture.host.startTurn.mock.calls[0]![0];
|
||||
const context = { requestId: 0, signal: new AbortController().signal };
|
||||
await expect(turnInput.onExtensionRequest!("provider/request", { sessionId: "agent-1" }, context)).rejects.toThrow(/session mismatch/);
|
||||
await session.steer!({ turnId, correlationId: "control-1", message: { text: "Change focus" } });
|
||||
await session.steer!({ turnId, correlationId: "control-2", mode: "follow_up", message: { text: "Then validate" } });
|
||||
await session.steer!({ turnId, correlationId: "control-1", message: { role: "user", text: "Change focus" } });
|
||||
await session.steer!({ turnId, correlationId: "control-2", mode: "follow_up", message: { role: "user", text: "Then validate" } });
|
||||
expect(fixture.host.steerActiveTurn).toHaveBeenCalledExactlyOnceWith("Change focus", `run-controls:${turnId}`);
|
||||
expect(fixture.host.queueFollowUp).toHaveBeenCalledExactlyOnceWith("Then validate", `run-controls:${turnId}`);
|
||||
expect(fixture.host.interruptActiveTurn).not.toHaveBeenCalled();
|
||||
await expect(session.steer!({ turnId, correlationId: "control-2", message: { text: "Duplicate" } })).rejects.toThrow(/already attempted/);
|
||||
await expect(session.steer!({ turnId: "wrong-turn", message: { text: "Stale" } })).rejects.toThrow();
|
||||
await expect(session.steer!({ turnId, correlationId: "control-2", message: { role: "user", text: "Duplicate" } })).rejects.toThrow(/already attempted/);
|
||||
await expect(session.steer!({ turnId: "wrong-turn", message: { role: "user", text: "Stale" } })).rejects.toThrow();
|
||||
fixture.finishTurn({ status: "completed" });
|
||||
const events = await collectUntil(session.events(), "turn.completed");
|
||||
expect(events.filter(event => event.payload.kind === "steering_acknowledgement").map(event => event.payload.mode)).toEqual(["steer", "follow_up"]);
|
||||
for (const event of events) expect(validatePrpEvent(event).ok).toBe(true);
|
||||
await expect(session.steer!({ turnId, message: { text: "Already settled" } })).rejects.toThrow();
|
||||
await expect(session.steer!({ turnId, message: { role: "user", text: "Already settled" } })).rejects.toThrow();
|
||||
await session.close({ reason: "controls verified" });
|
||||
});
|
||||
|
||||
it("rejects unnegotiated controls and retains ambiguous delivery attempts", async () => {
|
||||
const fixture = driverFixture({ agent: "pi", model: "openrouter/deepseek/deepseek-v4-flash-0731", providerPolicy: { readOnly: true } }, { runtimeEvents: [] });
|
||||
const session = await fixture.driver.openSession({ runId: "run-controls", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const { turnId } = await session.startTurn({ message: { text: "Work" } });
|
||||
await expect(session.steer!({ turnId, message: { text: "No handshake" } })).rejects.toThrow(/did not negotiate/);
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Work" } });
|
||||
await expect(session.steer!({ turnId, message: { role: "user", text: "No handshake" } })).rejects.toThrow(/did not negotiate/);
|
||||
expect(fixture.host.steerActiveTurn).not.toHaveBeenCalled();
|
||||
fixture.host.steeringCapability.mockReturnValue({ steering: true, queuedFollowUp: false });
|
||||
fixture.host.steerActiveTurn.mockRejectedValueOnce(new Error("delivery acknowledgement lost"));
|
||||
const input = { turnId, correlationId: "ambiguous", message: { text: "Maybe delivered" } };
|
||||
const input = { turnId, correlationId: "ambiguous", message: { role: "user" as const, text: "Maybe delivered" } };
|
||||
await expect(session.steer!(input)).rejects.toThrow(/acknowledgement lost/);
|
||||
await expect(session.steer!(input)).rejects.toThrow(/already attempted/);
|
||||
expect(fixture.host.steerActiveTurn).toHaveBeenCalledOnce();
|
||||
@@ -453,7 +453,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
],
|
||||
});
|
||||
const session = await fixture.driver.openSession({ runId: "run-claude-banners", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
await session.startTurn({ message: { text: "Fix it" } });
|
||||
await session.startTurn({ message: { role: "user", text: "Fix it" } });
|
||||
fixture.finishTurn({ status: "completed" });
|
||||
const events = await collectUntil(session.events(), "turn.completed");
|
||||
for (const event of events) validatePrpEvent(event);
|
||||
@@ -713,11 +713,11 @@ describe("Codex ACPX harness driver", () => {
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).rejects.toThrow("does not match");
|
||||
await session.interrupt({ turnId, reason: "user cancelled" });
|
||||
await session.interrupt!({ turnId, reason: "user cancelled" });
|
||||
expect(fixture.host.interruptActiveTurn).toHaveBeenCalledWith(
|
||||
"user cancelled",
|
||||
);
|
||||
await expect(session.interrupt({ turnId: "stale-turn" })).rejects.toThrow(
|
||||
await expect(session.interrupt!({ turnId: "stale-turn" })).rejects.toThrow(
|
||||
"is not the active turn",
|
||||
);
|
||||
await session.close({ reason: "cancelled" });
|
||||
@@ -1291,13 +1291,13 @@ describe("Codex ACPX harness driver", () => {
|
||||
const fixture = driverFixture({}, { closeSettlementTimeoutMs: 1 });
|
||||
const hostA = {
|
||||
...fixture.host,
|
||||
close: vi.fn(async () => {
|
||||
close: vi.fn(async (_input: { reason: string }): Promise<void> => {
|
||||
throw new Error("host A cleanup failed");
|
||||
}),
|
||||
};
|
||||
const hostB = {
|
||||
...fixture.host,
|
||||
close: vi.fn(async () => {
|
||||
close: vi.fn(async (_input: { reason: string }): Promise<void> => {
|
||||
throw new Error("host B cleanup failed");
|
||||
}),
|
||||
};
|
||||
@@ -1606,7 +1606,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
).rejects.toThrow("awaiting canonical terminal-event retention");
|
||||
expect(dynamicToolHandler).not.toHaveBeenCalled();
|
||||
await expect(
|
||||
session.interrupt({ turnId, reason: "too late" }),
|
||||
session.interrupt!({ turnId, reason: "too late" }),
|
||||
).rejects.toThrow("awaiting canonical terminal-event retention");
|
||||
expect(fixture.host.interruptActiveTurn).not.toHaveBeenCalled();
|
||||
await expect(session.snapshot()).resolves.toMatchObject({
|
||||
@@ -1761,7 +1761,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
const fixture = driverFixture({ agent: "copilot", model: "gpt-5.6-luna", providerPolicy: { readOnly: false } }, { eventStreamFailure: failure.promise });
|
||||
const session = await fixture.driver.openSession({ runId: "run-provider-death", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const created = collectUntil(session.events(), "runtime_request.created");
|
||||
const { turnId } = await session.startTurn({ message: { text: "Edit the file" } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Edit the file" } });
|
||||
const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!;
|
||||
const controller = new AbortController();
|
||||
const response = callback({ inferredKind: "edit", raw: {
|
||||
@@ -1895,11 +1895,11 @@ describe("Codex ACPX harness driver", () => {
|
||||
it.each(["cursor-tool", "native\u0000tool", "native\u007ftool"].flatMap(toolCallId => ["written", "failed"].map(outcome => ({ toolCallId, outcome }))))("binds Cursor denial evidence to its original tool and response write: $outcome/$toolCallId", async ({ outcome, toolCallId }) => {
|
||||
const command = "printf 'sensitive-value' > /workspace/denied.txt";
|
||||
const fixture = driverFixture({ agent: "cursor", model: "explicit-test-model", providerPolicy: { readOnly: false } }, {
|
||||
runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "Run command", kind: "execute", status: "pending", rawInput: { command } }],
|
||||
runtimeEvents: [{ type: "tool_call", tag: "tool_call", toolCallId, title: "Run command", kind: "execute", status: "pending", text: "Run command", rawInput: { command } }],
|
||||
});
|
||||
const session = await fixture.driver.openSession({ runId: "run-cursor-receipt", normalizedSessionId: "session-1", workingDirectory: "/workspace" });
|
||||
const origin = collectUntil(session.events(), "tool.execution.started");
|
||||
const { turnId } = await session.startTurn({ message: { text: "Request the command." } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Request the command." } });
|
||||
const originEvents = await origin;
|
||||
const created = collectUntil(session.events(), "runtime_request.created");
|
||||
const callback = fixture.host.startTurn.mock.calls[0]![0].onPermissionRequest!;
|
||||
@@ -1942,7 +1942,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
const fixture = driverFixture({ agent, providerPolicy: { readOnly: false } }, { runtimeEvents });
|
||||
const session = await fixture.driver.openSession({ runId: "run-attached", normalizedSessionId: "session-1", workingDirectory: "/fixture/workspace" });
|
||||
const completed = collectUntil(session.events(), "turn.completed");
|
||||
const { turnId } = await session.startTurn({ message: { text: "Start attached work." } });
|
||||
const { turnId } = await session.startTurn({ message: { role: "user", text: "Start attached work." } });
|
||||
fixture.finishTurn({ status: "completed", stopReason: "end_turn" });
|
||||
const events = await completed;
|
||||
const evidence = events.filter(event => event.eventType === "provider.notice.recorded" && event.payload.category === "copilot_tool_evidence_v1");
|
||||
@@ -1976,6 +1976,7 @@ describe("Codex ACPX harness driver", () => {
|
||||
const providerResponse = onElicitation(
|
||||
{
|
||||
mode: "form",
|
||||
sessionId: "backend-1",
|
||||
message: "Choose deployment settings.",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
@@ -2065,6 +2066,8 @@ describe("Codex ACPX harness driver", () => {
|
||||
onElicitation(
|
||||
{
|
||||
mode: "form",
|
||||
sessionId: "backend-1",
|
||||
message: "Provide a value.",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
@@ -2101,6 +2104,8 @@ describe("Codex ACPX harness driver", () => {
|
||||
const providerResponse = onElicitation(
|
||||
{
|
||||
mode: "form",
|
||||
sessionId: "backend-1",
|
||||
message: "Provide a value.",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
@@ -2148,6 +2153,8 @@ describe("Codex ACPX harness driver", () => {
|
||||
const providerResponse = onElicitation(
|
||||
{
|
||||
mode: "form",
|
||||
sessionId: "backend-1",
|
||||
message: "Provide a value.",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
@@ -2194,6 +2201,8 @@ describe("Codex ACPX harness driver", () => {
|
||||
const providerResponse = onElicitation(
|
||||
{
|
||||
mode: "form",
|
||||
sessionId: "backend-1",
|
||||
message: "Provide a value.",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
@@ -2262,6 +2271,8 @@ describe("Codex ACPX harness driver", () => {
|
||||
const providerResponse = onElicitation(
|
||||
{
|
||||
mode: "form",
|
||||
sessionId: "backend-1",
|
||||
message: "Provide a value.",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
@@ -3177,12 +3188,12 @@ function driverFixture(
|
||||
function recoveryWorkspaceLease(
|
||||
path = "/workspace",
|
||||
): AcpxRecoveryWorkspaceLease & {
|
||||
assertHeld: ReturnType<typeof vi.fn>;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
assertHeld: Mock<() => void>;
|
||||
close: Mock<() => Promise<void>>;
|
||||
} {
|
||||
return {
|
||||
path,
|
||||
assertHeld: vi.fn(),
|
||||
assertHeld: vi.fn<() => void>(),
|
||||
close: vi.fn(async () => undefined),
|
||||
};
|
||||
}
|
||||
@@ -3207,6 +3218,7 @@ function fakeHost(createTurn: () => AcpxRuntimeTurn, onClose: () => void) {
|
||||
workspacePath: "/workspace",
|
||||
workspaceDigest: `sha256:${"b".repeat(64)}`,
|
||||
runtimeRoot: "/runtime/acpx/session-1",
|
||||
commandDigest: `sha256:${"c".repeat(64)}`,
|
||||
profileDigest: `sha256:${"a".repeat(64)}`,
|
||||
requestedModel: "gpt-5.6-sol",
|
||||
effectiveModel: "gpt-5.6-sol",
|
||||
@@ -3225,7 +3237,7 @@ function fakeHost(createTurn: () => AcpxRuntimeTurn, onClose: () => void) {
|
||||
steerActiveTurn: vi.fn(async (_text: string, _requestId?: string) => undefined),
|
||||
queueFollowUp: vi.fn(async (_text: string, _requestId?: string) => undefined),
|
||||
interruptActiveTurn: vi.fn(async () => undefined),
|
||||
close: vi.fn(async () => {
|
||||
close: vi.fn(async (_input: { reason: string }) => {
|
||||
onClose();
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type {
|
||||
AcpPermissionRequest,
|
||||
AcpAgentRegistry,
|
||||
AcpRuntime,
|
||||
AcpRuntimeHandle,
|
||||
@@ -114,7 +115,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
if (!restoring) spawnProvider();
|
||||
return HANDLE;
|
||||
});
|
||||
vi.mocked(runtime.setConfigOption).mockImplementation(async input => {
|
||||
vi.mocked(runtime.setConfigOption!).mockImplementation(async input => {
|
||||
if (restoring) spawnProvider();
|
||||
const guard = created.protocolGuardFactory!();
|
||||
guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } });
|
||||
@@ -546,7 +547,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
const options = openOptions(fakeCommand()); options.profile = { ...options.profile, agent: "cursor" };
|
||||
options.refreshConsumedCommand = vi.fn(async () => undefined);
|
||||
let created!: AcpRuntimeOptions;
|
||||
vi.mocked(runtime.setConfigOption).mockImplementation(async () => {
|
||||
vi.mocked(runtime.setConfigOption!).mockImplementation(async () => {
|
||||
const guard = created.protocolGuardFactory!();
|
||||
const binding = cursorInstructionBinding(options.systemInstructions);
|
||||
guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } });
|
||||
@@ -574,7 +575,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
let created!: AcpRuntimeOptions;
|
||||
const binding = cursorInstructionBinding(options.systemInstructions);
|
||||
const configs = (mode: string) => [{ id: "mode", currentValue: mode }];
|
||||
vi.mocked(runtime.setConfigOption).mockImplementation(async input => {
|
||||
vi.mocked(runtime.setConfigOption!).mockImplementation(async input => {
|
||||
const guard = created.protocolGuardFactory!();
|
||||
guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } });
|
||||
guard("inbound", { id: 0, result: { modes: { currentModeId: "agent" }, configOptions: configs("agent"), _meta: { paperclipCursorInstructions: {
|
||||
@@ -586,7 +587,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
const port = await openCodexAcpxRuntime(options, {
|
||||
createRegistry: () => registry(), createStore: () => store(), createRuntime: value => { created = value; return runtime; },
|
||||
});
|
||||
expect(vi.mocked(runtime.setConfigOption).mock.calls.map(([call]) => [call.key, call.value])).toEqual([["model", options.profile.reportedModelId], ["mode", selected]]);
|
||||
expect(vi.mocked(runtime.setConfigOption!).mock.calls.map(([call]) => [call.key, call.value])).toEqual([["model", options.profile.reportedModelId], ["mode", selected]]);
|
||||
expect(options.refreshConsumedCommand).toHaveBeenCalledTimes(2);
|
||||
expect(await port.identity()).toMatchObject({ mode: selected });
|
||||
expect(runtime.startTurn).not.toHaveBeenCalled();
|
||||
@@ -652,13 +653,13 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
|
||||
it("requires the Pi capability handshake and acknowledgments, preserves follow-up order, and never starts a second prompt", async () => {
|
||||
const pending = pendingExtensionTurn("turn-1");
|
||||
const runtime = Object.assign(fakeRuntime(), { requestExtension: vi.fn(async () => ({ accepted: true })) });
|
||||
const runtime = Object.assign(fakeRuntime(), { requestExtension: vi.fn(async (_input: unknown) => ({ accepted: true })) });
|
||||
vi.mocked(runtime.startTurn).mockReturnValue(pending.turn);
|
||||
let created!: AcpRuntimeOptions & { onAgentInitialize?: (result: unknown) => void };
|
||||
const options = openOptions(fakeCommand());
|
||||
options.profile = { ...options.profile, agent: "pi" };
|
||||
options.piThinkingLevel = "low";
|
||||
vi.mocked(runtime.setConfigOption).mockImplementation(async input => {
|
||||
vi.mocked(runtime.setConfigOption!).mockImplementation(async input => {
|
||||
const guard = created.protocolGuardFactory!();
|
||||
guard("outbound", { id: 0, method: "session/load", params: { sessionId: "backend-1" } });
|
||||
guard("inbound", { id: 0, result: { modes: { currentModeId: "high" }, configOptions: [{ id: "thought_level", currentValue: "high" }] } });
|
||||
@@ -734,9 +735,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
expect(runtimeOptions!.permissionPolicy).toMatchObject({ defaultAction: "escalate" });
|
||||
const turn = port.startTurn({ text: "Attempt a write.", requestId: "permission-test" });
|
||||
try {
|
||||
await runtimeOptions!.onPermissionRequest!({
|
||||
sessionId: "backend-1", inferredKind: "write", raw: {},
|
||||
}, { signal: new AbortController().signal });
|
||||
await runtimeOptions!.onPermissionRequest!(permissionRequest("backend-1", "edit"), { signal: new AbortController().signal });
|
||||
expect(signal?.aborted).toBe(true);
|
||||
await expect(turn.result).rejects.toThrow("Approval required");
|
||||
await expect((async () => { for await (const _event of turn.events) { /* drain */ } })())
|
||||
@@ -2361,21 +2360,13 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
]);
|
||||
await expect(
|
||||
runtimeOptions?.onPermissionRequest?.(
|
||||
{
|
||||
sessionId: "session-1",
|
||||
inferredKind: "execute",
|
||||
raw: { _meta: { is_mcp_tool_approval: true } },
|
||||
},
|
||||
permissionRequest("session-1", "execute", { _meta: { is_mcp_tool_approval: true } }),
|
||||
{ signal: new AbortController().signal },
|
||||
),
|
||||
).resolves.toEqual({ outcome: "reject_once" });
|
||||
await expect(
|
||||
runtimeOptions?.onPermissionRequest?.(
|
||||
{
|
||||
sessionId: "session-1",
|
||||
inferredKind: "execute",
|
||||
raw: {},
|
||||
},
|
||||
permissionRequest("session-1", "execute"),
|
||||
{ signal: new AbortController().signal },
|
||||
),
|
||||
).resolves.toEqual({ outcome: "reject_once" });
|
||||
@@ -2399,11 +2390,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
|
||||
await expect(
|
||||
runtimeOptions?.onPermissionRequest?.(
|
||||
{
|
||||
sessionId: "session-1",
|
||||
inferredKind: "write",
|
||||
raw: {},
|
||||
},
|
||||
permissionRequest("session-1", "edit"),
|
||||
{ signal: new AbortController().signal },
|
||||
),
|
||||
).resolves.toEqual({ outcome: "reject_once" });
|
||||
@@ -2438,11 +2425,7 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
expect(runtimeOptions?.nonInteractivePermissions).toBe("fail");
|
||||
await expect(
|
||||
runtimeOptions?.onPermissionRequest?.(
|
||||
{
|
||||
sessionId: "backend-1",
|
||||
inferredKind: "write",
|
||||
raw: {},
|
||||
},
|
||||
permissionRequest("backend-1", "edit"),
|
||||
{ signal: new AbortController().signal },
|
||||
),
|
||||
).resolves.toEqual(expected);
|
||||
@@ -3618,8 +3601,18 @@ function fakeCommand(): VerifiedAcpxCommandLease {
|
||||
return { spawn: vi.fn(), close: vi.fn() };
|
||||
}
|
||||
|
||||
function fakeChild(): ChildProcess {
|
||||
const child = new EventEmitter() as ChildProcess;
|
||||
type FakeChildProcess = ChildProcess & { exitCode: number | null; signalCode: NodeJS.Signals | null };
|
||||
|
||||
function permissionRequest(
|
||||
sessionId: string,
|
||||
inferredKind: AcpPermissionRequest["inferredKind"],
|
||||
raw: Record<string, unknown> = {},
|
||||
): AcpPermissionRequest {
|
||||
return { sessionId, inferredKind, raw: raw as unknown as AcpPermissionRequest["raw"] };
|
||||
}
|
||||
|
||||
function fakeChild(): FakeChildProcess {
|
||||
const child = new EventEmitter() as FakeChildProcess;
|
||||
Object.defineProperties(child, {
|
||||
exitCode: { value: null, writable: true },
|
||||
signalCode: { value: null, writable: true },
|
||||
@@ -3633,10 +3626,10 @@ function fakeChild(): ChildProcess {
|
||||
}
|
||||
|
||||
function failingSignalChild(): {
|
||||
child: ChildProcess;
|
||||
child: FakeChildProcess;
|
||||
errors: [Error, Error];
|
||||
} {
|
||||
const child = new EventEmitter() as ChildProcess;
|
||||
const child = new EventEmitter() as FakeChildProcess;
|
||||
const errors: [Error, Error] = [
|
||||
new Error("SIGTERM delivery failed"),
|
||||
new Error("SIGKILL delivery failed"),
|
||||
@@ -3654,8 +3647,8 @@ function failingSignalChild(): {
|
||||
return { child, errors };
|
||||
}
|
||||
|
||||
function childThatExitsAfterSignalCalls(exitAfter: number): ChildProcess {
|
||||
const child = new EventEmitter() as ChildProcess;
|
||||
function childThatExitsAfterSignalCalls(exitAfter: number): FakeChildProcess {
|
||||
const child = new EventEmitter() as FakeChildProcess;
|
||||
Object.defineProperties(child, {
|
||||
exitCode: { value: null, writable: true },
|
||||
signalCode: { value: null, writable: true },
|
||||
@@ -3676,8 +3669,8 @@ function childThatExitsAfterSignalCalls(exitAfter: number): ChildProcess {
|
||||
return child;
|
||||
}
|
||||
|
||||
function stubbornChild(): ChildProcess {
|
||||
const child = new EventEmitter() as ChildProcess;
|
||||
function stubbornChild(): FakeChildProcess {
|
||||
const child = new EventEmitter() as FakeChildProcess;
|
||||
Object.defineProperties(child, {
|
||||
pid: { value: 71_001 },
|
||||
exitCode: { value: null, writable: true },
|
||||
@@ -3691,8 +3684,8 @@ function stubbornChild(): ChildProcess {
|
||||
return child;
|
||||
}
|
||||
|
||||
function childThatExitsOnKill(): ChildProcess {
|
||||
const child = new EventEmitter() as ChildProcess;
|
||||
function childThatExitsOnKill(): FakeChildProcess {
|
||||
const child = new EventEmitter() as FakeChildProcess;
|
||||
Object.defineProperties(child, {
|
||||
pid: { value: 71_002 },
|
||||
exitCode: { value: null, writable: true },
|
||||
|
||||
@@ -20,7 +20,7 @@ it("reports missing Copilot authentication before spawn and releases admission o
|
||||
agentServerPackageJsonPath: join(root, "provider/package.json"), agentRuntimePackageJsonPath: null, openCommand }),
|
||||
openRuntime, reportRetainedCleanupFailure: () => {},
|
||||
};
|
||||
const options = { agent: "copilot" as const, model: "explicit-model", normalizedSessionId: "copilot-auth-test",
|
||||
const options = { agent: "copilot" as const, model: "explicit-model", permissionMode: "approve-reads" as const, normalizedSessionId: "copilot-auth-test",
|
||||
runtimeDirectory, workingDirectory, providerPolicy: { readOnly: false } };
|
||||
try {
|
||||
await expect(AcpxRuntimeHost.open({ ...options, environment: { GH_TOKEN: "ambient-forbidden", GITHUB_TOKEN: "ambient-forbidden" } }, dependencies))
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/// <reference lib="es2024.string" />
|
||||
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { validateAcpxRichEvent } from "./profile-extensions.js";
|
||||
import { PAPERCLIP_QUESTION_RESPONSE_SCHEMA } from "../../contracts/question-set.js";
|
||||
import { PAPERCLIP_QUESTION_RESPONSE_SCHEMA, type PaperclipQuestionAnswer } from "../../contracts/question-set.js";
|
||||
import {
|
||||
createCursorNotificationNormalizer, createCursorProfileExtensionAdapter, createCursorSubagentNormalizer, cursorWorkspaceArtifactReference,
|
||||
normalizeCursorPlanRequest, normalizeCursorQuestionRequest, normalizeCursorSubagentUpdate,
|
||||
} from "./cursor-extensions.js";
|
||||
|
||||
const response = (answers: Record<string, unknown>) => ({ schema: PAPERCLIP_QUESTION_RESPONSE_SCHEMA, answers });
|
||||
const response = (answers: Record<string, PaperclipQuestionAnswer>) => ({ schema: PAPERCLIP_QUESTION_RESPONSE_SCHEMA, answers });
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
|
||||
async function workspace() { const root = await mkdtemp(join(tmpdir(), "cursor-extension-")); roots.push(root); return root; }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference lib="es2024.promise" />
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -43,7 +44,7 @@ it.each(["tool-first", "permission-first"])("real ACPX streams Cursor tool origi
|
||||
const evidence = createCursorToolEvidence({ sessionId: "native", turnId: "turn", workingDirectory: cwd, active: () => true,
|
||||
emit: event => { validateAcpxRichEvent(event); fields.push(Object.fromEntries((event.payload.details as Array<{ name: string; value: string }>).map(field => [field.name, field.value]))); },
|
||||
});
|
||||
const runtime = createAcpRuntime({ cwd, agentRegistry: createAgentRegistry({ overrides: { cursor: "fixture" } }), sessionStore: createRuntimeStore({ stateDir: join(cwd, "state") }), permissionMode: "approve-reads", authPolicy: "skip",
|
||||
const runtime = createAcpRuntime({ cwd, agentRegistry: createAgentRegistry({ overrides: { cursor: "fixture" } }), sessionStore: createRuntimeStore({ stateDir: join(cwd, "state") }), permissionMode: "approve-reads",
|
||||
spawnAgent: () => { const child = spawn(process.execPath, ["-e", peer], { cwd, env: { ORDER: order }, stdio: ["pipe", "pipe", "pipe"] }); children.push(child); return child; },
|
||||
onAcpMessage: (direction, value) => {
|
||||
const message = value as { id?: number; result?: { outcome?: unknown } };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference lib="es2024.promise" />
|
||||
import { spawn } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
|
||||
@@ -34,7 +34,7 @@ it.each(["tool-first", "permission-first"])("correlates opaque C0/C1/DEL native
|
||||
expect(expectedIds.at(-1)).toBe("safe-tool-1");
|
||||
for (const [index, toolCallId] of rawIds.entries()) {
|
||||
const native = { sessionId: "session", inferredKind: "execute" as const, raw: {
|
||||
sessionId: "session", toolCall: { toolCallId, kind: "execute" },
|
||||
sessionId: "session", toolCall: { toolCallId, kind: "execute" as const },
|
||||
options: [{ kind: "reject_once" as const, optionId: `native-denial-${index}`, name: "Deny" }],
|
||||
} };
|
||||
const before = structuredClone(native);
|
||||
@@ -99,7 +99,7 @@ it("ignores inactive turns and never lets observation errors undo a delivered de
|
||||
});
|
||||
it("bounds retained tool origins", () => {
|
||||
const s = setup(); for (let i = 0; i < 257; i++) s.p.tool({ ...initial, toolCallId: `tool-${i}` });
|
||||
expect(s.fields().at(-1).stage).toBe("evidence_incomplete"); expect(s.events).toHaveLength(257);
|
||||
expect(s.fields().at(-1)!.stage).toBe("evidence_incomplete"); expect(s.events).toHaveLength(257);
|
||||
});
|
||||
|
||||
it("rejects commands missing from their original frame and bounded oversized input", () => {
|
||||
@@ -159,5 +159,5 @@ it("preserves execute denial evidence after valid edit and read permission input
|
||||
it.each(["edit", "read", "unknown-native-kind"])("still rejects a contradictory permission kind %s", kind => {
|
||||
const s = setup(); s.p.tool(initial);
|
||||
s.p.permission({ raw: { sessionId: "session", toolCall: { toolCallId: "tool", kind, rawInput: { path: "/fixture/file" } } } }, "request", ["decline"]);
|
||||
expect(s.fields().at(-1).stage).toBe("evidence_incomplete"); expect(s.unavailable()).toBe(true);
|
||||
expect(s.fields().at(-1)!.stage).toBe("evidence_incomplete"); expect(s.unavailable()).toBe(true);
|
||||
});
|
||||
@@ -7,6 +7,11 @@ import { expect, it } from "vitest";
|
||||
import { createGrokEffortAdmission } from "./grok-effort.js";
|
||||
import { acpxSidecarErrorCode } from "../../cli/acpx-sidecar-input.js";
|
||||
|
||||
// The patched acpx runtime implements this method without declaring it.
|
||||
interface ExtensionRuntime {
|
||||
requestExtension(input: { handle: AcpRuntimeHandle; method: string; params: Record<string, unknown>; sessionMode?: "persistent" | "oneshot" }): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
it("retains a native Grok acknowledgement failure through the real ACPX connection", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "grok-rejected-admission-"));
|
||||
const fixture = join(root, "grok.cjs");
|
||||
@@ -94,7 +99,7 @@ it.each(["low", "high"] as const)("transports and restores native Grok %s effort
|
||||
handle = await runtime.ensureSession(input);
|
||||
expect(handle.backendSessionId).toBe(originalSession);
|
||||
expect(admission.isReady()).toBe(false);
|
||||
await runtime.requestExtension({ handle, method: "_x.ai/session/info", params: { sessionId: handle.backendSessionId }, sessionMode: "persistent" });
|
||||
await (runtime as typeof runtime & ExtensionRuntime).requestExtension({ handle, method: "_x.ai/session/info", params: { sessionId: handle.backendSessionId }, sessionMode: "persistent" });
|
||||
admission.assertReady();
|
||||
const turn = runtime.startTurn({ handle, text: "Verify the restored fixture", mode: "prompt", requestId: "grok-cold-turn", timeoutMs: 5_000 });
|
||||
const output: string[] = [];
|
||||
|
||||
@@ -686,7 +686,7 @@ describe("native ACPX execution closure", () => {
|
||||
const fences = await Promise.all([listen(), listen()]);
|
||||
const fds = fences.map(server => (server as Server & { _handle?: { fd?: number } })._handle!.fd!);
|
||||
const installation = await verifyNativeAcpxInstallation(declaration);
|
||||
const owner = createAcpxCommandLeaseOwner(await installation.openCommand(), installation.openCommand);
|
||||
const owner = createAcpxCommandLeaseOwner(await installation.openCommand(), signal => installation.openCommand({ signal }));
|
||||
const pidFile = join(declaration.distributionRoot, "native-pid");
|
||||
let port: Awaited<ReturnType<typeof openCodexAcpxRuntime>> | undefined;
|
||||
let deadline: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdtemp, mkdir, realpath, rename, rm, symlink, writeFile } from "node:f
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import Ajv from "ajv";
|
||||
import { Ajv } from "ajv";
|
||||
import {
|
||||
checkPiNativeTool, installPiRuntimeExtension, piMcpRequest, readPiRuntimeConfiguration,
|
||||
PI_NATIVE_QUESTION_TOOL, PI_PERMISSION_TITLE_PREFIX, type PiExtensionApi, type PiRuntimeConfiguration, type PiToolDefinition,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => { for (const path of directories.splice(0)) await rm(path, { recursive: true, force: true }); });
|
||||
async function workspace() {
|
||||
async function workspace(): Promise<{ root: string; config: PiRuntimeConfiguration }> {
|
||||
const root = await mkdtemp(join(tmpdir(), "paperclip-pi-extension-")); directories.push(root);
|
||||
await mkdir(join(root, "workspace")); await mkdir(join(root, "skills")); await mkdir(join(root, "private"));
|
||||
return { root, config: {
|
||||
@@ -20,7 +20,7 @@ async function workspace() {
|
||||
workspace: join(root, "workspace"), readOnly: false,
|
||||
readRoots: [join(root, "skills")], protectedRoots: [join(root, "private")],
|
||||
instructions: "Use the assigned Paperclip tools", servers: [],
|
||||
} satisfies PiRuntimeConfiguration };
|
||||
} };
|
||||
}
|
||||
function harness() {
|
||||
const handlers = new Map<string, (...args: any[]) => any>();
|
||||
@@ -241,7 +241,7 @@ describe("owned Pi runtime extension", () => {
|
||||
it("revalidates paths after a human wait and never treats cancellation as approval", async () => {
|
||||
const { root, config } = await workspace(); const h = harness();
|
||||
await installPiRuntimeExtension(h.api, config); h.handlers.get("turn_start")!();
|
||||
const context = { cwd: config.workspace, ui: { select: vi.fn(async () => {
|
||||
const context = { cwd: config.workspace, ui: { select: vi.fn(async (_title: string) => {
|
||||
await symlink(join(root, "private"), join(config.workspace, "target"));
|
||||
return "Allow once";
|
||||
}) } };
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("historical ACPX profile decoding", () => {
|
||||
["copilot", "copilot-profile-v85-cursor-cloud-qualification-historical-identity.json", "977d68b327f3964e8337ef8ba00230eb1b1697017db21f8010f664fee3d3c70a", "private Cloud qualification"],
|
||||
["grok", "grok-profile-v14-identity.json", "08294ec6bdcbdbe46c8c8b711d8b368558a05f0d84d25c9f5fd0c3b67b79cd17", "published ed8"],
|
||||
["grok", "grok-profile-v14-cursor-cloud-qualification-historical-identity.json", "fa34ba395ff51affed7e7195dc44f4e33a3c6718ada1c80e8e36852cf7af40ac", "private Cloud qualification"],
|
||||
] as const)("keeps %s identity %s immutable and decode-only", (agent, filename, checksum) => {
|
||||
] as const)("keeps %s identity %s immutable and decode-only", (agent, filename, checksum, _source) => {
|
||||
const bytes = readFileSync(new URL(`../../../test/fixtures/${filename}`, import.meta.url));
|
||||
expect(createHash("sha256").update(bytes).digest("hex")).toBe(checksum);
|
||||
const identity = JSON.parse(bytes.toString("utf8"));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference lib="es2024.promise" />
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { deliverAcpxResponse, requireAcpxResponseDelivery } from "./response-delivery.js";
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("ACPX runtime host", () => {
|
||||
it("rejects preconfigured Codex before admission resources or runtime launch", async () => {
|
||||
const fixture = await hostFixture();
|
||||
const openRuntime = vi.fn(), prepareSandbox = vi.fn();
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "codex", permissionMode: "approve-all",
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "codex", model: "gpt-5.6-sol", permissionMode: "approve-all",
|
||||
environment: { HOME: fixture.root, PAPERCLIP_RUNNER_HOME_MODE: "preconfigured" },
|
||||
}, fixture.dependencies({ openRuntime, prepareSandbox }))).rejects.toThrow("cannot protect Runner state");
|
||||
expect(prepareSandbox).not.toHaveBeenCalled();
|
||||
@@ -282,7 +282,7 @@ describe("ACPX runtime host", () => {
|
||||
it.each(["claude", "codex"] as const)("rejects preconfigured read-only %s before provider admission", async agent => {
|
||||
const fixture = await hostFixture();
|
||||
const openRuntime = vi.fn();
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, agent, permissionMode: "approve-all",
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, agent, model: "explicit-test-model", permissionMode: "approve-all",
|
||||
providerPolicy: { readOnly: true }, environment: { HOME: fixture.root, PAPERCLIP_RUNNER_HOME_MODE: "preconfigured" },
|
||||
}, fixture.dependencies({ openRuntime }))).rejects.toThrow("preconfigured ACPX read-only execution is not supported");
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
@@ -291,7 +291,7 @@ describe("ACPX runtime host", () => {
|
||||
it.each(["approve-reads", "approve-paperclip", "deny-all"] as const)("rejects unqualified preconfigured permission mode %s before admission", async permissionMode => {
|
||||
const fixture = await hostFixture();
|
||||
const openRuntime = vi.fn();
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, permissionMode,
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5", permissionMode,
|
||||
environment: { HOME: fixture.root, PAPERCLIP_RUNNER_HOME_MODE: "preconfigured" },
|
||||
}, fixture.dependencies({ openRuntime }))).rejects.toThrow("requires ACPX approve-all");
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
@@ -379,7 +379,7 @@ describe("ACPX runtime host", () => {
|
||||
])("rejects invalid assigned connection bindings before runtime launch", async (environment) => {
|
||||
const fixture = await hostFixture();
|
||||
const openRuntime = vi.fn();
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, environment }, fixture.dependencies({ openRuntime }))).rejects.toThrow(/assigned native MCP/);
|
||||
await expect(AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5", permissionMode: "approve-reads", environment }, fixture.dependencies({ openRuntime }))).rejects.toThrow(/assigned native MCP/);
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -491,7 +491,8 @@ describe("ACPX runtime host", () => {
|
||||
const host = await AcpxRuntimeHost.open({
|
||||
...fixture.options,
|
||||
agent: "claude", model: "claude-sonnet-5",
|
||||
environment: { ...fixture.options.environment,
|
||||
permissionMode: "approve-reads",
|
||||
environment: {
|
||||
PAPERCLIP_NATIVE_MCP_NAME: "paperclip-assigned",
|
||||
PAPERCLIP_NATIVE_MCP_URL: "http://127.0.0.1:3211/mcp/gateway",
|
||||
PAPERCLIP_NATIVE_MCP_TOKEN: "fixture-gateway-token-".repeat(3),
|
||||
@@ -518,7 +519,7 @@ describe("ACPX runtime host", () => {
|
||||
let servers: AcpxRuntimePortOpenOptions["mcpServers"] = [];
|
||||
const host = await AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5",
|
||||
permissionMode: "approve-reads", providerPolicy: { readOnly },
|
||||
environment: { ...fixture.options.environment, PAPERCLIP_NATIVE_MCP_STDIO: JSON.stringify([binding]), PAPERCLIP_NATIVE_MCP_PROXY: "/verified/runnerd" },
|
||||
environment: { PAPERCLIP_NATIVE_MCP_STDIO: JSON.stringify([binding]), PAPERCLIP_NATIVE_MCP_PROXY: "/verified/runnerd" },
|
||||
}, fixture.dependencies({ openRuntime: async options => {
|
||||
servers = options.mcpServers;
|
||||
return runtimePort({ getStatus: async () => ({ models: { currentModelId: "claude-sonnet-5" } }) });
|
||||
@@ -1042,12 +1043,12 @@ describe("ACPX runtime host", () => {
|
||||
const events: CanonicalProviderEvent[] = [];
|
||||
const evidence = createCopilotToolEvidence({ sessionId: "backend-1", turnId: "turn-1", workingDirectory: fixture.options.workingDirectory,
|
||||
active: () => true, emit: event => { validateAcpxRichEvent(event); events.push(JSON.parse(JSON.stringify(event))); } });
|
||||
let bridge: AcpxRuntimePortOpenOptions["mcpServers"][number] | undefined;
|
||||
let bridge: Extract<AcpxRuntimePortOpenOptions["mcpServers"][number], { url: string }> | undefined;
|
||||
const host = await AcpxRuntimeHost.open({ ...fixture.options, agent: "copilot", model: "gpt-5.6-sol",
|
||||
permissionMode: "deny-all", environment: { COPILOT_GITHUB_TOKEN: "fixture-not-a-real-credential" },
|
||||
semanticTools: { tools: [{ name: "get_task_context", inputSchema: { type: "object", properties: {}, additionalProperties: false } }],
|
||||
handler: async () => ({ accepted: false }), captureSemanticReceipt: () => evidence.captureSemanticReceipt() },
|
||||
}, fixture.dependencies({ openRuntime: async options => { bridge = options.mcpServers[0]; return runtimePort(); } }));
|
||||
}, fixture.dependencies({ openRuntime: async options => { bridge = urlBridge(options.mcpServers[0]); return runtimePort(); } }));
|
||||
try {
|
||||
const response = await fetch(bridge!.url, { method: "POST", headers: { Authorization: `Bearer ${bridge!.bearerToken}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "get_task_context", arguments: {} } }) });
|
||||
@@ -1092,7 +1093,7 @@ describe("ACPX runtime host", () => {
|
||||
},
|
||||
fixture.dependencies({
|
||||
openRuntime: async (options) => {
|
||||
bridge = options.mcpServers[0];
|
||||
bridge = urlBridge(options.mcpServers[0]);
|
||||
return runtimePort();
|
||||
},
|
||||
}),
|
||||
@@ -1990,10 +1991,10 @@ describe("ACPX runtime host", () => {
|
||||
it("retains a settled turn when shutdown cleanup must be retried", async () => {
|
||||
const fixture = await hostFixture();
|
||||
const credential = credentialLeaseFixture(fixture.root);
|
||||
let settleTurn!: (value: { stopReason: string }) => void;
|
||||
let settleTurn!: (value: Awaited<AcpxRuntimeTurn["result"]>) => void;
|
||||
const turn = {
|
||||
...runtimeTurn(),
|
||||
result: new Promise<{ stopReason: string }>((resolve) => {
|
||||
result: new Promise<Awaited<AcpxRuntimeTurn["result"]>>((resolve) => {
|
||||
settleTurn = resolve;
|
||||
}),
|
||||
};
|
||||
@@ -2019,7 +2020,7 @@ describe("ACPX runtime host", () => {
|
||||
host.startTurn({ text: "Complete the task.", requestId: "turn-1" });
|
||||
|
||||
const firstClose = host.close({ reason: "first shutdown" });
|
||||
settleTurn({ stopReason: "end_turn" });
|
||||
settleTurn({ status: "completed", stopReason: "end_turn" });
|
||||
await expect(firstClose).rejects.toThrow(/cleanup failed/);
|
||||
expect(turn.cancel).toHaveBeenCalledOnce();
|
||||
expect(credential.close).not.toHaveBeenCalled();
|
||||
@@ -2332,6 +2333,11 @@ function credentialLeaseFixture(root: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function urlBridge(server: AcpxRuntimePortOpenOptions["mcpServers"][number] | undefined) {
|
||||
if (!server || !("url" in server)) throw new Error("missing tool bridge");
|
||||
return server;
|
||||
}
|
||||
|
||||
function runtimePort(
|
||||
input: {
|
||||
identity?: AcpxRuntimePort["identity"];
|
||||
@@ -2402,7 +2408,7 @@ async function hostFixture() {
|
||||
dependencies(
|
||||
input: Pick<AcpxRuntimeHostDependencies, "openRuntime"> &
|
||||
Partial<
|
||||
Pick<AcpxRuntimeHostDependencies, "reportRetainedCleanupFailure">
|
||||
Pick<AcpxRuntimeHostDependencies, "reportRetainedCleanupFailure" | "prepareSandbox" | "verifyInstallation">
|
||||
> & {
|
||||
// Opt-in only. When true, `prepareSandbox` runs the real
|
||||
// preparation once, then returns that same sandbox for every
|
||||
@@ -2413,19 +2419,19 @@ async function hostFixture() {
|
||||
): AcpxRuntimeHostDependencies {
|
||||
let reusedSandbox: AcpxRuntimeSandbox | null = null;
|
||||
return {
|
||||
verifyInstallation: async (profile) =>
|
||||
verifyInstallation: input.verifyInstallation ?? (async (profile) =>
|
||||
({
|
||||
commandDigest: profile.commandDigest,
|
||||
agentServerPackageJsonPath: join(root, "package.json"),
|
||||
agentRuntimePackageJsonPath: null,
|
||||
openCommand: async () => command,
|
||||
}) satisfies VerifiedAcpxInstallation,
|
||||
}) satisfies VerifiedAcpxInstallation),
|
||||
openRuntime: input.openRuntime,
|
||||
// Record the skills home the instant the sandbox exists, ahead of
|
||||
// the materialize call that seals it. A test that overrides
|
||||
// `prepareSandbox` for a non-Claude agent replaces this wrapper, but
|
||||
// those agents never materialize skills, so nothing is lost.
|
||||
prepareSandbox: async (sandboxInput) => {
|
||||
prepareSandbox: input.prepareSandbox ?? (async (sandboxInput) => {
|
||||
if (input.reuseSandbox && reusedSandbox) return reusedSandbox;
|
||||
const sandbox = await prepareAcpxRuntimeSandbox(sandboxInput);
|
||||
if (sandboxInput.agent === "claude") {
|
||||
@@ -2433,7 +2439,7 @@ async function hostFixture() {
|
||||
}
|
||||
if (input.reuseSandbox) reusedSandbox = sandbox;
|
||||
return sandbox;
|
||||
},
|
||||
}),
|
||||
retainAdmissionCleanup: trackAdmissionCleanup,
|
||||
reportRetainedCleanupFailure:
|
||||
input.reportRetainedCleanupFailure ?? vi.fn(),
|
||||
|
||||
@@ -128,10 +128,11 @@ describe("Codex app-server transport limits", () => {
|
||||
});
|
||||
|
||||
it("installs cleanup handlers before synchronously reporting process ownership", async () => {
|
||||
const originalSpawn = ChildProcess.prototype.spawn;
|
||||
const childProcessPrototype = ChildProcess.prototype as ChildProcess & { spawn(...args: unknown[]): unknown };
|
||||
const originalSpawn = childProcessPrototype.spawn;
|
||||
let spawnedProcess: ChildProcess | undefined;
|
||||
const spawnSpy = vi
|
||||
.spyOn(ChildProcess.prototype, "spawn")
|
||||
.spyOn(childProcessPrototype, "spawn")
|
||||
.mockImplementation(function (this: ChildProcess, ...args) {
|
||||
spawnedProcess = this;
|
||||
return originalSpawn.apply(this, args);
|
||||
|
||||
+1
-1
@@ -660,7 +660,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError);
|
||||
for (const action of ["set", "pause", "resume", "clear"] as const) {
|
||||
await expect(
|
||||
unsupported.goal?.({ action, ...(action === "set" ? { objective: "Must not reach the provider" } : {}) }),
|
||||
unsupported.goal?.(action === "set" ? { action, objective: "Must not reach the provider" } : { action }),
|
||||
).rejects.toBeInstanceOf(HarnessCapabilityUnavailableError);
|
||||
}
|
||||
expect(unsupportedTransport.calls.filter(({ method }) =>
|
||||
|
||||
@@ -401,7 +401,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
{ runDelta: first, runDeltaComplete: true },
|
||||
{ runDelta: { ...first, ...second }, runDeltaComplete: false },
|
||||
]);
|
||||
expect(await session.usage()).toMatchObject({ runDeltaComplete: false });
|
||||
expect(await session.usage!()).toMatchObject({ runDeltaComplete: false });
|
||||
});
|
||||
|
||||
it("keeps usage reported between runs in the next run's delta so totals stay exact", async () => {
|
||||
@@ -478,7 +478,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
expect(events.some((event) => event.eventType === "session.failed")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(await session.usage()).toMatchObject({
|
||||
expect(await session.usage!()).toMatchObject({
|
||||
total: { inputTokens: 10, outputTokens: 4 },
|
||||
});
|
||||
});
|
||||
|
||||
+4
-3
@@ -785,6 +785,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
runId: "run-1",
|
||||
environmentLeaseId: "lease-1",
|
||||
runnerInstanceId: "runner-codex",
|
||||
driverSessionId: "thread-1",
|
||||
normalizedSessionId: "normalized-1",
|
||||
},
|
||||
capabilities,
|
||||
@@ -819,7 +820,7 @@ it("sends explicit skill input on every requested turn", async () => {
|
||||
await collectUntilTerminal(session.events());
|
||||
transport.turnStartResponse = Promise.resolve({ turn: { id: "turn-2", status: "inProgress", items: [] } });
|
||||
}
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
});
|
||||
|
||||
|
||||
@@ -830,7 +831,7 @@ it("reapplies explicit skills after recovery without persisting them into ordina
|
||||
runId: "initial", normalizedSessionId: "skill-recovery", workingDirectory: WORKSPACE,
|
||||
});
|
||||
const snapshot = await original.snapshot();
|
||||
await original.close();
|
||||
await original.close({ reason: "fixture complete" });
|
||||
for (const selected of [skillInputs, []]) {
|
||||
const transport = new FakeCodexTransport();
|
||||
const recovery = await makeDriver([transport], { skillInputs: selected }).recoverSession(snapshot);
|
||||
@@ -840,6 +841,6 @@ it("reapplies explicit skills after recovery without persisting them into ordina
|
||||
await session.startTurn({ message: { role: "user", text: "Continue" } });
|
||||
const items = transport.calls.find((call) => call.method === "turn/start")?.params.input as Array<{type:string}>;
|
||||
expect(items.filter((item) => item.type === "skill")).toEqual(selected);
|
||||
} finally { await session.close(); }
|
||||
} finally { await session.close({ reason: "fixture complete" }); }
|
||||
}
|
||||
});
|
||||
+4
-4
@@ -76,7 +76,7 @@ it("preserves runnerd permission expiry before a failed provider terminal and re
|
||||
expect(resolve).not.toHaveBeenCalled();
|
||||
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } });
|
||||
await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed")).toBe(true));
|
||||
await session.close(); await consume;
|
||||
await session.close({ reason: "fixture complete" }); await consume;
|
||||
const settlements = events.filter(event => ["runtime_request.expired", "runtime_request.cancelled", "runtime_request.resolved"].includes(event.eventType));
|
||||
expect(settlements).toHaveLength(1);
|
||||
expect(settlements[0]).toMatchObject({ eventType: "runtime_request.expired", turnId, itemId: "native-edit", payload: { ...payload, turnId, itemId: "native-edit" } });
|
||||
@@ -105,7 +105,7 @@ it("preserves canonical question expiry after provider loss and refuses late ans
|
||||
await expect(session.resolveRuntimeRequest?.({ requestId: "native-question", turnId, resolution: { action: "accept" } })).rejects.toThrow();
|
||||
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turnId, status: "failed", error: { code: "ACPX_PROVIDER_PROCESS_LOST", message: "Provider died" } } });
|
||||
await vi.waitFor(() => expect(events.some(event => event.eventType === "turn.failed" || event.eventType === "session.failed")).toBe(true));
|
||||
await session.close(); await consume;
|
||||
await session.close({ reason: "fixture complete" }); await consume;
|
||||
expect(events.filter(event => event.eventType === "runtime_request.expired")).toHaveLength(1);
|
||||
expect(events.find(event => event.eventType === "runtime_request.expired")).toMatchObject({ turnId, itemId: "question-item", payload });
|
||||
expect(events.some(event => event.eventType === "session.failed" && event.payload.code === "turn_binding_mismatch")).toBe(false);
|
||||
@@ -127,7 +127,7 @@ it.each(["turn", "kind", "type", "replay"])("refuses canonical permission expiry
|
||||
requestType: kind === "type" ? "input" : "permission", replayAllowed: kind === "replay", reason: "provider_process_lost" };
|
||||
const notification = runnerdRuntimeRequestExpiryNotification("runtime_request.expired", payload, "thread-1", kind === "turn" ? "foreign-turn" : turnId)!;
|
||||
transport.push(notification.method, notification.params);
|
||||
await pending; await consume; await session.close();
|
||||
await pending; await consume; await session.close({ reason: "fixture complete" });
|
||||
expect(events.some(e => e.eventType === "runtime_request.expired")).toBe(false);
|
||||
expect(events.some(e => e.eventType === "session.failed" && e.payload.code === "turn_binding_mismatch")).toBe(true);
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
});
|
||||
expect(await response).toEqual({ action: "accept", content: null, _meta: null });
|
||||
expect(session.pendingRuntimeRequests?.()).toHaveLength(0);
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
});
|
||||
|
||||
it("validates runtime request resolutions against the kind of request they answer", async () => {
|
||||
|
||||
+4
-4
@@ -68,7 +68,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
threadId: "thread-1", turnId: "turn-1", arguments: {} });
|
||||
expect(feedback).toHaveBeenCalledExactlyOnceWith({ ...report, continuation });
|
||||
expect((await session.snapshot()).semanticResult?.result).toEqual({ ...report, continuation });
|
||||
} finally { await session.close(); }
|
||||
} finally { await session.close({ reason: "fixture complete" }); }
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -92,7 +92,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
expect(response).toMatchObject({ success: false });
|
||||
expect(feedback).not.toHaveBeenCalled();
|
||||
expect((await session.snapshot()).semanticResult).toBeNull();
|
||||
} finally { await session.close(); }
|
||||
} finally { await session.close({ reason: "fixture complete" }); }
|
||||
});
|
||||
|
||||
it("returns current approval feedback and permits correcting a rejected completion report", async () => {
|
||||
@@ -111,7 +111,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
expect(await call("corrected")).toMatchObject({ success: true,
|
||||
contentItems: [{ type: "inputText", text: expect.stringContaining("/approvals/approval-1") }] });
|
||||
expect((await session.snapshot()).semanticResult?.result).toEqual(result);
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
});
|
||||
|
||||
it("accepts an explicit response-wake yield through paperclip_finish", async () => {
|
||||
@@ -194,7 +194,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
},
|
||||
},
|
||||
})).toMatchObject({ success: false });
|
||||
await otherSession.close();
|
||||
await otherSession.close({ reason: "fixture complete" });
|
||||
});
|
||||
|
||||
it("makes duplicate semantic completion idempotent and rejects changed payloads", async () => {
|
||||
|
||||
@@ -16,8 +16,8 @@ import { NativeSessionProtocolIntegrityError } from "../../contracts/native-sess
|
||||
import {
|
||||
DurablePrpControlPlane,
|
||||
durableRecoveryInternals,
|
||||
type DurableRecoveryIdentity,
|
||||
} from "../../control-plane/durable-prp-control-plane.js";
|
||||
import type { DurableRecoveryIdentity } from "../../control-plane/prp-transport-types.js";
|
||||
import { createCapabilityRunnerdCodexTransport } from "../../live/runnerd-codex-transport.js";
|
||||
import { executeNativeSession } from "../../native-session-runtime.js";
|
||||
import { CodexAppServerDriver } from "./codex-app-server-driver.js";
|
||||
@@ -44,7 +44,7 @@ async function authenticatedRunner(
|
||||
runnerBinary: string,
|
||||
) {
|
||||
const framed = (domain: string, parts: Buffer[]) => {
|
||||
const values = [Buffer.from(domain), Buffer.from([0])];
|
||||
const values: Buffer[] = [Buffer.from(domain), Buffer.from([0])];
|
||||
for (const part of parts) {
|
||||
const length = Buffer.alloc(8);
|
||||
length.writeBigUInt64BE(BigInt(part.length));
|
||||
@@ -476,6 +476,7 @@ describe("Codex protocol integrity propagation", () => {
|
||||
normalizedSessionId: identity.normalizedSessionId,
|
||||
driverKind: "codex_app_server",
|
||||
protocolVersion: 1,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
},
|
||||
provider: { kind: "codex", model: null },
|
||||
completionContract: {
|
||||
|
||||
@@ -335,7 +335,7 @@ describe("Codex structured question adapter", () => {
|
||||
questions: [{ id: "form", question: "Choose one" }],
|
||||
});
|
||||
expect(safeForm?.description).toContain("Bearer [REDACTED]");
|
||||
expect(safeForm?.description.endsWith(preservedTail)).toBe(true);
|
||||
expect(safeForm?.description?.endsWith(preservedTail)).toBe(true);
|
||||
|
||||
expect(() => normalizeCodexQuestionSet("mcpServer/elicitation/request", {
|
||||
requestedSchema: {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
turnId: string,
|
||||
) => {
|
||||
const saved = JSON.parse(JSON.stringify(await session.snapshot!()));
|
||||
await session.close();
|
||||
await session.close({ reason: "test complete" });
|
||||
transport.readResponse = {
|
||||
thread: { id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE, turns: [] },
|
||||
};
|
||||
@@ -60,7 +60,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
session = await coldRestart(session, transports[2]!, "third", "turn-3");
|
||||
await expect(runTurn(session, transports[2]!, "turn-3", { inputTokens: 170, outputTokens: 20 }))
|
||||
.resolves.toEqual({ inputTokens: 30, outputTokens: 4 });
|
||||
await session.close();
|
||||
await session.close({ reason: "test complete" });
|
||||
});
|
||||
|
||||
it.each(["thread", "snapshot"] as const)(
|
||||
@@ -137,7 +137,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
expect(await session.usage!()).toMatchObject({
|
||||
runDelta: { inputTokens: 40, outputTokens: 6 },
|
||||
});
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
second.readResponse = readResponse(140, 16);
|
||||
const recovery = await driver.recoverSession(persisted);
|
||||
expect(recovery.recovered).toBe(true);
|
||||
@@ -187,7 +187,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
inputTokens: 140,
|
||||
outputTokens: 16,
|
||||
});
|
||||
await recoveredSession.close();
|
||||
await recoveredSession.close({ reason: "fixture complete" });
|
||||
third.readResponse = readResponse(150, 19);
|
||||
const secondRecovery = await driver.recoverSession(advanced);
|
||||
expect(secondRecovery.recovered).toBe(true);
|
||||
@@ -205,8 +205,8 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
),
|
||||
).toEqual([]);
|
||||
} finally {
|
||||
await recoveredSession?.close();
|
||||
await session.close();
|
||||
await recoveredSession?.close({ reason: "fixture complete" });
|
||||
await session.close({ reason: "fixture complete" });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -245,7 +245,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
baseline: { inputTokens: 100 },
|
||||
latest: { inputTokens: 140 },
|
||||
};
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
const tokenUsage = {
|
||||
total: { inputTokens: 150 },
|
||||
last: { inputTokens: 10 },
|
||||
@@ -270,8 +270,8 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
second.calls.filter((call) => call.method === "turn/start"),
|
||||
).toEqual([]);
|
||||
} finally {
|
||||
await recoveredSession?.close();
|
||||
await session.close();
|
||||
await recoveredSession?.close({ reason: "fixture complete" });
|
||||
await session.close({ reason: "fixture complete" });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -300,7 +300,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
});
|
||||
await collectUntilTerminal(session.events());
|
||||
const persisted = JSON.parse(JSON.stringify(await session.snapshot!()));
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
second.readResponse = {
|
||||
thread: {
|
||||
id: "thread-1",
|
||||
@@ -384,7 +384,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
.filter((call) => call.method === "thread/read")
|
||||
.every((call) => call.params.includeTurns === false),
|
||||
).toBe(true);
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
});
|
||||
|
||||
it("keeps usage reported between runs in the next run's delta after a restore", async () => {
|
||||
@@ -401,7 +401,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const persisted = JSON.parse(JSON.stringify(await session.snapshot!()));
|
||||
expect(persisted.codexUsageBaseline).toMatchObject({ latest: { inputTokens: 130 }, accounted: { inputTokens: 100 } });
|
||||
await session.close();
|
||||
await session.close({ reason: "test complete" });
|
||||
second.readResponse = { thread: { id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE,
|
||||
turns: [{ id: "turn-1", status: "completed", items: [] }], tokenUsage: { total: { inputTokens: 130, outputTokens: 10 } } } };
|
||||
const recovery = await driver.recoverSession(persisted);
|
||||
@@ -413,7 +413,7 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
second.push("thread/tokenUsage/updated", { threadId: "thread-1", turnId: "turn-2", tokenUsage: { total: { inputTokens: 150, outputTokens: 16 } } });
|
||||
for await (const event of recovered.events()) if (event.payload.kind === "usage") break;
|
||||
expect(await recovered.usage!()).toMatchObject({ runDelta: { inputTokens: 50, outputTokens: 6 } });
|
||||
await recovered.close();
|
||||
await recovered.close({ reason: "test complete" });
|
||||
});
|
||||
|
||||
it("keeps the startup external sandbox profile on subsequent turns", async () => {
|
||||
@@ -439,6 +439,6 @@ describe("Codex resume accounting through the production driver", () => {
|
||||
transport.calls.find((call) => call.method === "turn/start")?.params
|
||||
.permissions,
|
||||
).toBe(start.params.permissions);
|
||||
await session.close();
|
||||
await session.close({ reason: "fixture complete" });
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseCodexTurnDiff, summarizeCodexTurnDiff } from "./codex-turn-diff.js";
|
||||
import { parseCodexTurnDiff, summarizeCodexTurnDiff, type ParsedCodexTurnDiffFile } from "./codex-turn-diff.js";
|
||||
|
||||
describe("Codex turn diff parser", () => {
|
||||
it("summarizes parsed files for work-product metadata", () => {
|
||||
expect(summarizeCodexTurnDiff([
|
||||
const textFiles: ParsedCodexTurnDiffFile[] = [
|
||||
{ path: "a.ts", operation: "modify", previousPath: null, additions: 4, deletions: 2, binary: false, diff: "patch" },
|
||||
{ path: "b.ts", operation: "create", previousPath: null, additions: 3, deletions: 0, binary: false, diff: "patch" },
|
||||
])).toEqual({ files: 2, additions: 7, deletions: 2 });
|
||||
];
|
||||
expect(summarizeCodexTurnDiff(textFiles)).toEqual({ files: 2, additions: 7, deletions: 2 });
|
||||
|
||||
expect(summarizeCodexTurnDiff([
|
||||
const binaryFiles: ParsedCodexTurnDiffFile[] = [
|
||||
{ path: "logo.png", operation: "modify", previousPath: null, additions: null, deletions: null, binary: true, diff: null },
|
||||
])).toEqual({ files: 1, additions: null, deletions: null });
|
||||
];
|
||||
expect(summarizeCodexTurnDiff(binaryFiles)).toEqual({ files: 1, additions: null, deletions: null });
|
||||
});
|
||||
|
||||
it("parses a complete snapshot with bounded file statistics", () => {
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("prepared native context transport", () => {
|
||||
text: expect.stringContaining('"task"'),
|
||||
}),
|
||||
]);
|
||||
expect((input?.[0] as { text: string }).text).toContain(
|
||||
expect((input as Array<{ text: string }>)[0]!.text).toContain(
|
||||
'"message":"ordinary standalone task"',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("Cursor Cloud bounded stdio proxy", () => {
|
||||
const terminalGate = new Promise<void>(resolve => { releaseTerminalWrites = resolve; });
|
||||
let pausedTerminalWrites = 0;
|
||||
const write = CursorCloudCheckpointStore.prototype.write;
|
||||
vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(value) {
|
||||
vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(this: CursorCloudCheckpointStore, value) {
|
||||
if (value.phase === "terminal" && value.pendingTerminalDelivery) {
|
||||
pausedTerminalWrites += 1;
|
||||
await terminalGate;
|
||||
@@ -151,7 +151,7 @@ describe("Cursor Cloud bounded stdio proxy", () => {
|
||||
});
|
||||
let openedSession!: CursorCloudSessionHandle;
|
||||
const openSession = CursorCloudDriver.prototype.openSession;
|
||||
vi.spyOn(CursorCloudDriver.prototype, "openSession").mockImplementation(async function(input) {
|
||||
vi.spyOn(CursorCloudDriver.prototype, "openSession").mockImplementation(async function(this: CursorCloudDriver, input) {
|
||||
openedSession = await openSession.call(this, input);
|
||||
return openedSession;
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe("Cursor Cloud v1 HTTP client", () => {
|
||||
if (request.url?.includes("/artifacts/download?")) return json(response, { url: `https://${host}/proof?private-signature`, expiresAt: TIME });
|
||||
response.end(bytes);
|
||||
});
|
||||
const externalHeaders: HeadersInit[] = [];
|
||||
const externalHeaders: Array<NonNullable<RequestInit["headers"]>> = [];
|
||||
let unexpectedTargets = 0;
|
||||
const transport: typeof fetch = (input, init) => {
|
||||
const url = String(input);
|
||||
|
||||
@@ -71,7 +71,7 @@ function maintainedMutations(peer: Peer, maxResponseBytes?: number) {
|
||||
|
||||
async function fixture(peer = new Peer(), extra: Partial<CursorCloudDriverOptions> = {}) {
|
||||
const root = await mkdtemp("/tmp/cc-"); roots.push(root);
|
||||
const handler = vi.fn(async () => ({ success: true, contentItems: [{ type: "inputText", text: "accepted" }] }));
|
||||
const handler = vi.fn(async (_call: Parameters<CursorCloudDriverOptions["dynamicToolHandler"]>[0]) => ({ success: true, contentItems: [{ type: "inputText", text: "accepted" }] }));
|
||||
const options: CursorCloudDriverOptions = { model: "explicit-model", config: { schema: "paperclip.cursor-cloud.config.v1", repos: [{ url: "https://github.com/example/repo", startingRef: "main" }] },
|
||||
binding: { companyId: "company", agentId: "agent", issueId: "issue", normalizedSessionId: "session" }, runtimeDirectory: root,
|
||||
relay: { url: "https://relay.example.test/mcp", token: "private-relay-token", socketPath: join(root, "mcp.sock"), expiresAt: Date.now() + 600_000 }, instructions: "Follow these exact instructions.",
|
||||
@@ -111,7 +111,11 @@ async function nextAuthority(f: Awaited<ReturnType<typeof fixture>>) {
|
||||
return { next, checkpoint: await savedCheckpoint(f.root, f.session), eventCount: f.events.length };
|
||||
}
|
||||
|
||||
function relayHeaders(peer: Peer): Record<string, string> { return structuredClone(peer.request!.mcpServers[0]!.headers!); }
|
||||
function relayHeaders(peer: Peer): Record<string, string> {
|
||||
const server = peer.request!.mcpServers![0]!;
|
||||
if (!("headers" in server)) throw new Error("Expected an HTTP relay MCP server");
|
||||
return structuredClone(server.headers!);
|
||||
}
|
||||
|
||||
async function rpc(path: string, body: unknown, token: string | Record<string, string> = "private-relay-token", session?: string): Promise<{ status: number; value: any; session?: string }> {
|
||||
return new Promise((accept, reject) => {
|
||||
@@ -206,7 +210,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
await f.session.startTurn({ message: { role: "user", text: "Stop an observation in flight" } });
|
||||
await reading.promise;
|
||||
try {
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
await vi.waitFor(() => expect(readAborted).toBe(true), { timeout: 100 });
|
||||
expect((await f.session.read()).phase).toBe("terminal");
|
||||
} finally { blocked.resolve(await original()); }
|
||||
@@ -257,7 +261,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
await f.session.startTurn({ message: { role: "user", text: "Stop during a provider retry delay" } });
|
||||
await waitFor(() => f.events.some(event => (event.payload as { code?: string }).code === "cursor_cloud_observation_retry"));
|
||||
const signal = ((peer.getRun.mock.calls[0] as unknown[])[2] as { signal: AbortSignal }).signal;
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect((await f.session.read()).phase).toBe("terminal");
|
||||
expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.createRun).not.toHaveBeenCalled();
|
||||
@@ -354,7 +358,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
}
|
||||
}
|
||||
await reading.promise;
|
||||
await session.interrupt!();
|
||||
await session.interrupt!({});
|
||||
await acknowledgeTerminal(session);
|
||||
await session.attachRun!({ runId: "paperclip-followup" });
|
||||
const next = await session.startTurn({ message: { role: "user", text: "Second task" } });
|
||||
@@ -370,7 +374,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8"))).toEqual(before);
|
||||
expect(await session.read()).toMatchObject({ agentId: previous.agentId, runId: followupRunId, turnId: next.turnId, phase: "running", pendingTerminalDelivery: null });
|
||||
expect(events.slice(eventCount)).toEqual([]);
|
||||
await session.interrupt!();
|
||||
await session.interrupt!({});
|
||||
expect(await session.read()).toMatchObject({ runId: followupRunId, phase: "terminal", pendingTerminalDelivery: { providerRunId: followupRunId, turnId: next.turnId, status: "CANCELLED" } });
|
||||
expect(events.filter(event => event.turnId === next.turnId && event.eventType === "turn.interrupted")).toHaveLength(1);
|
||||
});
|
||||
@@ -393,14 +397,14 @@ describe("Cursor Cloud native driver", () => {
|
||||
};
|
||||
const f = await fixture(peer);
|
||||
await f.session.startTurn({ message: { role: "user", text: "First task" } }); await waiting.promise;
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
const next = await nextAuthority(f);
|
||||
release.resolve(); await new Promise<void>(accept => setImmediate(accept));
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(next.checkpoint);
|
||||
expect(f.events.slice(next.eventCount)).toEqual([]);
|
||||
expect(streams.map(stream => stream.runId)).toEqual([runId, peer.native!.id]);
|
||||
expect(streams[1]!.signal!.aborted).toBe(false);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect((await f.session.read()).pendingTerminalDelivery).toMatchObject({ providerRunId: peer.native!.id, turnId: next.next.turnId });
|
||||
});
|
||||
|
||||
@@ -421,7 +425,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
released.resolve(); await new Promise<void>(accept => setImmediate(accept));
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(next.checkpoint);
|
||||
expect(f.events.slice(next.eventCount)).toEqual([]);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: peer.native!.id, turnId: next.next.turnId } });
|
||||
});
|
||||
|
||||
@@ -441,7 +445,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
released.resolve(oldAgent); await oldReconcile;
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(next.checkpoint);
|
||||
expect(f.events.slice(next.eventCount)).toEqual([]);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: peer.native!.id, turnId: next.next.turnId } });
|
||||
});
|
||||
|
||||
@@ -453,19 +457,19 @@ describe("Cursor Cloud native driver", () => {
|
||||
const previous = { ...peer.native! };
|
||||
const oldRead = deferred<CursorCloudRun>(), reading = deferred();
|
||||
peer.getRun.mockImplementationOnce(async () => { reading.resolve(); return oldRead.promise; });
|
||||
const oldStop = f.session.interrupt!(); await reading.promise;
|
||||
const oldStop = f.session.interrupt!({}); await reading.promise;
|
||||
peer.getRun.mockResolvedValueOnce({ ...previous, status: "FINISHED" });
|
||||
await f.session.reconcile!();
|
||||
const next = await nextAuthority(f);
|
||||
const currentRead = deferred<CursorCloudRun>(), currentReading = deferred();
|
||||
peer.getRun.mockImplementationOnce(async () => { currentReading.resolve(); return currentRead.promise; });
|
||||
const currentStop = f.session.interrupt!(); await currentReading.promise;
|
||||
const currentStop = f.session.interrupt!({}); await currentReading.promise;
|
||||
const before = await savedCheckpoint(f.root, f.session);
|
||||
oldRead.resolve(previous); await oldStop;
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(before);
|
||||
expect(peer.cancelRun).not.toHaveBeenCalled();
|
||||
const readCount = peer.getRun.mock.calls.length;
|
||||
const duplicateStop = f.session.interrupt!();
|
||||
const duplicateStop = f.session.interrupt!({});
|
||||
await new Promise<void>(accept => setImmediate(accept));
|
||||
expect(peer.getRun).toHaveBeenCalledTimes(readCount);
|
||||
currentRead.resolve({ ...peer.native! }); await Promise.all([currentStop, duplicateStop]);
|
||||
@@ -480,7 +484,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
if (phase === "follow-up") {
|
||||
await f.session.startTurn({ message: { role: "user", text: "First task" } });
|
||||
await waitFor(() => f.events.some(event => event.eventType === "item.delta"));
|
||||
await f.session.interrupt!(); await acknowledgeTerminal(f.session);
|
||||
await f.session.interrupt!({}); await acknowledgeTerminal(f.session);
|
||||
await f.session.attachRun!({ runId: "paperclip-followup" });
|
||||
}
|
||||
const before = await savedCheckpoint(f.root, f.session);
|
||||
@@ -498,7 +502,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(before);
|
||||
expect(f.events.slice(eventCount)).toEqual([]);
|
||||
const reconciliations = peer.getAgent.mock.calls.length, cancellations = peer.cancelRun.mock.calls.length;
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(peer.getAgent).toHaveBeenCalledTimes(reconciliations); expect(peer.cancelRun).toHaveBeenCalledTimes(cancellations);
|
||||
const snapshot = await f.session.snapshot(); await f.session.close({ reason: "storage_recovered" }); await f.pump;
|
||||
expect(peer.getAgent).toHaveBeenCalledTimes(reconciliations); expect(peer.cancelRun).toHaveBeenCalledTimes(cancellations);
|
||||
@@ -507,7 +511,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(await savedCheckpoint(f.root, recovered)).toEqual({ ...before, relayTurnToken: before.relayTurnToken ?? null });
|
||||
await recovered.startTurn({ message: { role: "user", text: "Retry after storage recovery" } });
|
||||
await waitFor(() => peer.createAgent.mock.calls.length === 1 && peer.createRun.mock.calls.length === (phase === "fresh" ? 0 : 1));
|
||||
await recovered.interrupt!();
|
||||
await recovered.interrupt!({});
|
||||
});
|
||||
|
||||
it("retains accepted provider work when its confirmed-identity checkpoint write fails", async () => {
|
||||
@@ -531,7 +535,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
await rm(checkpointPath, { recursive: true }); await rename(savedPath, checkpointPath);
|
||||
}
|
||||
expect(await savedCheckpoint(f.root, f.session)).toMatchObject({ agentId: peer.native!.agentId, runId, phase: "running", pendingOperation: null });
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.cancelRun).toHaveBeenCalledTimes(1);
|
||||
expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { status: "CANCELLED", providerRunId: runId } });
|
||||
});
|
||||
@@ -543,7 +547,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const failure = new Error("controlled post-publication persistence failure");
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
let intercepted = false;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
const persisted = originalWrite.call(this, value);
|
||||
if (value.phase === "creating" && !intercepted) {
|
||||
intercepted = true;
|
||||
@@ -556,13 +560,13 @@ describe("Cursor Cloud native driver", () => {
|
||||
const started = f.session.startTurn({ message: { role: "user", text: "Stop before unsent launch" } });
|
||||
const rejected = expect(started).rejects.toBe(failure);
|
||||
await held.promise;
|
||||
const stopped = f.session.interrupt!(); await stoppingWritten.promise;
|
||||
const stopped = f.session.interrupt!({}); await stoppingWritten.promise;
|
||||
expect(f.peer.createAgent).not.toHaveBeenCalled();
|
||||
release.resolve(); await rejected; await stopped;
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(before);
|
||||
expect(f.peer.createAgent).not.toHaveBeenCalled(); expect(f.peer.getAgent).not.toHaveBeenCalled(); expect(f.peer.cancelRun).not.toHaveBeenCalled();
|
||||
await f.session.startTurn({ message: { role: "user", text: "Next admitted task" } }); await f.peer.created.promise;
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
} finally { release.resolve(); write.mockRestore(); }
|
||||
});
|
||||
|
||||
@@ -571,10 +575,10 @@ describe("Cursor Cloud native driver", () => {
|
||||
const f = await fixture(peer);
|
||||
await f.session.startTurn({ message: { role: "user", text: "First task" } }); await peer.created.promise;
|
||||
await waitFor(() => f.events.some(event => event.eventType === "item.delta"));
|
||||
await f.session.interrupt!(); await acknowledgeTerminal(f.session);
|
||||
await f.session.interrupt!({}); await acknowledgeTerminal(f.session);
|
||||
const held = deferred(), release = deferred(), stoppingWritten = deferred();
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
const persisted = originalWrite.call(this, value);
|
||||
if (value.phase === "creating") return persisted.then(async () => { held.resolve(); await release.promise; });
|
||||
if (value.phase === "stopping") void persisted.then(() => stoppingWritten.resolve());
|
||||
@@ -582,7 +586,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
});
|
||||
try {
|
||||
const started = f.session.startTurn({ message: { role: "user", text: "Second task" } }); await held.promise;
|
||||
const stopped = f.session.interrupt!(); await stoppingWritten.promise;
|
||||
const stopped = f.session.interrupt!({}); await stoppingWritten.promise;
|
||||
expect(peer.createRun).not.toHaveBeenCalled();
|
||||
release.resolve();
|
||||
const next = await started; await stopped;
|
||||
@@ -605,7 +609,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const after = await savedCheckpoint(f.root, f.session);
|
||||
expect(after).toEqual({ ...before, sourceSequence: before.sourceSequence + 1 });
|
||||
expect(f.events.some(event => event.eventType === "turn.failed")).toBe(false);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: runId } });
|
||||
});
|
||||
|
||||
@@ -617,7 +621,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
f.peer.getRun.mockResolvedValueOnce({ ...f.peer.native!, status: "FINISHED", ...(identity === "agent" ? { agentId: "bc-different" } : { id: "run-22222222-2222-4222-8222-222222222222" }) });
|
||||
await expect(f.session.reconcile!()).rejects.toThrow("confirmed Agent/Run identity");
|
||||
expect(await savedCheckpoint(f.root, f.session)).toEqual(before);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(await f.session.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { providerRunId: runId } });
|
||||
});
|
||||
|
||||
@@ -660,7 +664,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const activeSnapshot = await restored.snapshot(); await restored.detachControllerForRestart!();
|
||||
await expect(new CursorCloudDriver(f.options).recoverSession(activeSnapshot)).rejects.toThrow("instructions changed");
|
||||
const final = (await new CursorCloudDriver(revised).recoverSession(activeSnapshot)).session!; sessions.push(final);
|
||||
await final.interrupt!();
|
||||
await final.interrupt!({});
|
||||
await acknowledgeTerminal(final);
|
||||
await final.startTurn({ message: { role: "user", text: "One more task" } });
|
||||
await waitFor(() => f.peer.createRun.mock.calls.length === 2);
|
||||
@@ -702,7 +706,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(f.handler).toHaveBeenCalledWith(expect.objectContaining({ tool: "write_note", arguments: { note: "first" }, turnId: expect.stringMatching(/^turn-/) }));
|
||||
await rpc(path, { id: "missing", method: "tools/call", params: { name: "not_admitted", arguments: {} } }, relayHeaders(f.peer), initialized.session);
|
||||
expect(f.handler).toHaveBeenCalledTimes(1);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(f.peer.cancelRun).toHaveBeenCalledTimes(1);
|
||||
expect(f.peer.getRun.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
const late = await rpc(path, { id: "late", method: "tools/call", params: { name: "write_note", arguments: { note: "late" } } }, relayHeaders(f.peer), initialized.session);
|
||||
@@ -849,7 +853,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
await waitFor(() => peer.getAgent.mock.calls.length > 0);
|
||||
expect(peer.createAgent).toHaveBeenCalledTimes(1);
|
||||
expect((await restored.read?.())?.effectiveModelAcknowledged).toBe(false);
|
||||
await restored.interrupt!();
|
||||
await restored.interrupt!({});
|
||||
const restoredSnapshot = await restored.snapshot(); await restored.close({ reason: "done" });
|
||||
await expect(new CursorCloudDriver({ ...f.options, model: "different-model" }).recoverSession(restoredSnapshot)).rejects.toThrow("changed");
|
||||
});
|
||||
@@ -873,7 +877,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(after.requestedAgentId).toMatch(/^bc-/);
|
||||
await expect(restored.startTurn({ message: { role: "user", text: "Must not replace uncertain work" } })).rejects.toThrow("unresolved remote work");
|
||||
expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.createRun).not.toHaveBeenCalled();
|
||||
peer.getAgent.mockImplementation(getAgent); await restored.interrupt!();
|
||||
peer.getAgent.mockImplementation(getAgent); await restored.interrupt!({});
|
||||
});
|
||||
|
||||
it("allows a definitive direct mutation rejection to settle without pretending work was accepted", async () => {
|
||||
@@ -893,7 +897,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
if (phase === "follow-up") {
|
||||
await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } });
|
||||
await peer.created.promise;
|
||||
await f.session.interrupt!(); await acknowledgeTerminal(f.session);
|
||||
await f.session.interrupt!({}); await acknowledgeTerminal(f.session);
|
||||
}
|
||||
const before = { fetch: fetch.mock.calls.length, getAgent: peer.getAgent.mock.calls.length, getRun: peer.getRun.mock.calls.length, cancel: peer.cancelRun.mock.calls.length };
|
||||
const text = "\u0001".repeat(400_000);
|
||||
@@ -905,11 +909,11 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(fetch).toHaveBeenCalledTimes(before.fetch);
|
||||
expect(await f.session.read()).toMatchObject({ phase: "idle", providerStatus: "REJECTED" });
|
||||
expect(f.events.some(event => event.turnId === turn.turnId && event.eventType === "turn.failed" && event.payload.remoteWorkAccepted === false)).toBe(true);
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
expect(peer.getAgent).toHaveBeenCalledTimes(before.getAgent);
|
||||
expect(peer.getRun).toHaveBeenCalledTimes(before.getRun);
|
||||
expect(peer.cancelRun).toHaveBeenCalledTimes(before.cancel);
|
||||
const snapshot = await f.session.snapshot(); await f.session.detachControllerForRestart(); await f.pump;
|
||||
const snapshot = await f.session.snapshot(); await f.session.detachControllerForRestart!(); await f.pump;
|
||||
const recovered = await new CursorCloudDriver(f.options).recoverSession(snapshot);
|
||||
expect(recovered.recovered).toBe(true);
|
||||
const restored = recovered.session!; sessions.push(restored);
|
||||
@@ -919,7 +923,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const next = await restored.startTurn({ message: { role: "user", text: "A smaller admitted task" } });
|
||||
await vi.waitFor(async () => expect(await restored.read()).toMatchObject({ phase: "running", turnId: next.turnId }));
|
||||
expect(fetch).toHaveBeenCalledTimes(before.fetch + 1);
|
||||
await restored.interrupt!();
|
||||
await restored.interrupt!({});
|
||||
});
|
||||
|
||||
it.each(["fresh", "follow-up"] as const)("keeps a %s oversized response fenced after the mutation was sent", async phase => {
|
||||
@@ -928,7 +932,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const f = await fixture(peer, { client: mutations.client });
|
||||
if (phase === "follow-up") {
|
||||
await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } });
|
||||
await peer.created.promise; await f.session.interrupt!(); await acknowledgeTerminal(f.session);
|
||||
await peer.created.promise; await f.session.interrupt!({}); await acknowledgeTerminal(f.session);
|
||||
}
|
||||
const before = mutations.fetch.mock.calls.length;
|
||||
mutations.fetch.mockImplementationOnce(async (url, init) => {
|
||||
@@ -954,7 +958,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
if (phase === "follow-up") {
|
||||
await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } });
|
||||
await peer.created.promise;
|
||||
await f.session.interrupt!();
|
||||
await f.session.interrupt!({});
|
||||
await acknowledgeTerminal(f.session);
|
||||
}
|
||||
const entered = deferred(), rejectCreate = deferred(), stopHeld = deferred(), releaseStop = deferred();
|
||||
@@ -975,7 +979,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const before = { getAgent: peer.getAgent.mock.calls.length, getRun: peer.getRun.mock.calls.length, cancel: peer.cancelRun.mock.calls.length };
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
let pausedStop = false, pausedRejection = false;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
if (value.phase === "idle" && value.terminalStatus === "REJECTED" && !pausedRejection) {
|
||||
pausedRejection = true; rejectionHeld.resolve();
|
||||
return releaseRejection.promise.then(() => originalWrite.call(this, value));
|
||||
@@ -989,7 +993,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
});
|
||||
let stopDone = false;
|
||||
let stopped: Promise<{ error: unknown | null }> | undefined;
|
||||
const stop = () => stopped = f.session.interrupt!().then(() => ({ error: null }), error => ({ error }))
|
||||
const stop = () => stopped = f.session.interrupt!({}).then(() => ({ error: null }), error => ({ error }))
|
||||
.finally(() => { stopDone = true; });
|
||||
try {
|
||||
const turn = await f.session.startTurn({ message: { role: "user", text: rejection === "unsent_payload" ? "\u0001".repeat(400_000) : "Rejected task" } });
|
||||
@@ -1029,7 +1033,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
expect(peer.cancelRun).toHaveBeenCalledTimes(before.cancel);
|
||||
if (mutations) expect(mutations.fetch).toHaveBeenCalledTimes(fetchesBefore!);
|
||||
const snapshot = await f.session.snapshot();
|
||||
await f.session.detachControllerForRestart(); await f.pump;
|
||||
await f.session.detachControllerForRestart!(); await f.pump;
|
||||
const recovered = await new CursorCloudDriver(f.options).recoverSession(snapshot);
|
||||
expect(recovered.recovered).toBe(true);
|
||||
const restored = recovered.session!; sessions.push(restored);
|
||||
@@ -1037,7 +1041,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
distinctFollowup(peer); peer.release = deferred();
|
||||
const next = await restored.startTurn({ message: { role: "user", text: "Next admitted task" } });
|
||||
await vi.waitFor(async () => expect(await restored.read()).toMatchObject({ phase: "running", turnId: next.turnId }));
|
||||
await restored.interrupt!();
|
||||
await restored.interrupt!({});
|
||||
expect(await restored.read()).toMatchObject({ phase: "terminal", pendingTerminalDelivery: { turnId: next.turnId } });
|
||||
} finally {
|
||||
rejectCreate.resolve(); releaseStop.resolve(); releaseRejection.resolve();
|
||||
@@ -1051,7 +1055,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const f = await fixture(peer), held = deferred(), release = deferred();
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
let paused = false;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
if (value.phase === "idle" && value.terminalStatus === "REJECTED" && !paused) {
|
||||
paused = true; held.resolve(); return release.promise.then(() => originalWrite.call(this, value));
|
||||
}
|
||||
@@ -1082,7 +1086,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
await f.session.reconcile!(); await acknowledgeTerminal(f.session); await f.session.attachRun!({ runId: "current-owner" });
|
||||
peer.createRun.mockRejectedValue(new CursorCloudClientError("http_error", { status: 422 }));
|
||||
const held = deferred(), release = deferred(), originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
if (value.phase === "idle" && value.terminalStatus === "REJECTED") { held.resolve(); return release.promise.then(() => originalWrite.call(this, value)); }
|
||||
return originalWrite.call(this, value);
|
||||
});
|
||||
@@ -1105,7 +1109,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const f = await fixture(peer, mutations ? { client: mutations.client } : {});
|
||||
if (phase === "follow-up") {
|
||||
await f.session.startTurn({ message: { role: "user", text: "Previous accepted task" } }); await peer.created.promise;
|
||||
await f.session.interrupt!(); await acknowledgeTerminal(f.session);
|
||||
await f.session.interrupt!({}); await acknowledgeTerminal(f.session);
|
||||
}
|
||||
const entered = deferred(), rejected = deferred();
|
||||
if (mutations) {
|
||||
@@ -1119,14 +1123,14 @@ describe("Cursor Cloud native driver", () => {
|
||||
});
|
||||
peer.getAgent.mockRejectedValue(new CursorCloudClientError("http_error", { status: 404 }));
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
return value.phase === "idle" && value.terminalStatus === "REJECTED"
|
||||
? Promise.reject(new Error("injected rejection checkpoint failure")) : originalWrite.call(this, value);
|
||||
});
|
||||
try {
|
||||
const before = mutations?.fetch.mock.calls.length;
|
||||
const turn = await f.session.startTurn({ message: { role: "user", text: mutations ? "\u0001".repeat(400_000) : "Rejected task" } }); await entered.promise;
|
||||
const stopped = f.session.interrupt!().then(() => null, error => error);
|
||||
const stopped = f.session.interrupt!({}).then(() => null, error => error);
|
||||
rejected.resolve();
|
||||
expect(await stopped).toMatchObject({ message: expect.stringContaining("remains fenced") });
|
||||
if (mutations) expect(mutations.fetch).toHaveBeenCalledTimes(before!);
|
||||
@@ -1147,7 +1151,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
peer.getAgent.mockRejectedValue(new CursorCloudClientError("http_error", { status: 404 }));
|
||||
const f = await fixture(peer), turn = await f.session.startTurn({ message: { role: "user", text: "Unknown create" } });
|
||||
await entered.promise;
|
||||
const stopped = f.session.interrupt!().then(() => null, error => error); released.resolve();
|
||||
const stopped = f.session.interrupt!({}).then(() => null, error => error); released.resolve();
|
||||
expect(await stopped).toMatchObject({ message: expect.stringContaining("remains fenced") });
|
||||
expect(await savedCheckpoint(f.root, f.session)).toMatchObject({ phase: "stopping", pendingOperation: "create_agent", terminalStatus: null });
|
||||
expect(f.events.some(event => event.turnId === turn.turnId && event.payload.remoteWorkAccepted === false)).toBe(false);
|
||||
@@ -1159,7 +1163,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const f = await fixture(peer, { cancelTimeoutMs: 120 });
|
||||
await f.session.startTurn({ message: { role: "user", text: "Keep running" } }); await peer.created.promise;
|
||||
await waitFor(() => f.events.some(event => event.eventType === "item.delta"));
|
||||
await expect(f.session.interrupt!()).rejects.toThrow("did not confirm");
|
||||
await expect(f.session.interrupt!({})).rejects.toThrow("did not confirm");
|
||||
expect(f.events.some(event => event.eventType === "turn.interrupted" || event.eventType === "turn.completed")).toBe(false);
|
||||
expect((await f.session.read?.())?.phase).toBe("stopping");
|
||||
await expect(f.session.startTurn({ message: { role: "user", text: "New work" } })).rejects.toThrow("unresolved");
|
||||
@@ -1204,7 +1208,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
const attempts: Array<NonNullable<typeof before.pendingTerminalDelivery>> = [];
|
||||
let injected = false;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(async function(this: CursorCloudCheckpointStore, value) {
|
||||
if (this.path !== checkpointPath || value.phase !== "terminal") return originalWrite.call(this, value);
|
||||
attempts.push(structuredClone(value.pendingTerminalDelivery));
|
||||
if (injected) { await releaseRetry.promise; return originalWrite.call(this, value); }
|
||||
@@ -1248,7 +1252,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const held = deferred(), release = deferred();
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
let paused = false;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
if (this.path === checkpointPath && value.phase === "terminal" && !paused) {
|
||||
paused = true; held.resolve(); return release.promise.then(() => originalWrite.call(this, value));
|
||||
}
|
||||
@@ -1261,7 +1265,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const late = await rpc(f.options.relay.socketPath, { id: "late", method: "tools/call", params: { name: "write_note", arguments: { note: "No authority after provider termination" } } }, relayHeaders(f.peer), initialized.session);
|
||||
expect(late.status).toBe(403); expect(f.handler).not.toHaveBeenCalled();
|
||||
let stopped = false, reconciled = false, snapshotted = false;
|
||||
const stop = f.session.interrupt!().finally(() => { stopped = true; });
|
||||
const stop = f.session.interrupt!({}).finally(() => { stopped = true; });
|
||||
const reconcile = f.session.reconcile!().finally(() => { reconciled = true; });
|
||||
const snapshot = f.session.snapshot().finally(() => { snapshotted = true; });
|
||||
await new Promise<void>(accept => setImmediate(accept));
|
||||
@@ -1290,7 +1294,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
const originalWrite = CursorCloudCheckpointStore.prototype.write;
|
||||
const deliveries: unknown[] = [];
|
||||
let unavailable = true;
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(value) {
|
||||
const write = vi.spyOn(CursorCloudCheckpointStore.prototype, "write").mockImplementation(function(this: CursorCloudCheckpointStore, value) {
|
||||
if (this.path === checkpointPath && value.phase === "terminal") {
|
||||
deliveries.push(structuredClone(value.pendingTerminalDelivery));
|
||||
if (unavailable) return Promise.reject(Object.assign(new Error("controlled unavailable checkpoint storage"), { code: "ENOSPC" }));
|
||||
@@ -1363,7 +1367,7 @@ describe("Cursor Cloud native driver", () => {
|
||||
await waitFor(() => stream.mock.calls.length > 0);
|
||||
expect(peer.createAgent).toHaveBeenCalledTimes(1); expect(peer.createRun).not.toHaveBeenCalled();
|
||||
expect(JSON.parse(await readFile(checkpointPath, "utf8")).deadlineAt).toBe(deadlineAt);
|
||||
await restored.interrupt!();
|
||||
await restored.interrupt!({});
|
||||
});
|
||||
|
||||
it("enforces the saved deadline while the first recovery read remains pending", async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const result = { schema: "paperclip.run_result.v1", reportedWorkDisposition: "do
|
||||
async function fixture(overrides: Partial<DotDriverOptions> = {}) {
|
||||
const identity = { companyId: randomUUID(), agentId: randomUUID(), issueId: randomUUID(), runId: randomUUID(), sessionId: randomUUID() };
|
||||
const principal = { companyId: identity.companyId, grantId: randomUUID() };
|
||||
const publish = vi.fn(async () => {});
|
||||
const publish = vi.fn<DotDriverOptions["publish"]>(async () => {});
|
||||
const executeTool = vi.fn(async () => ({ saved: true }));
|
||||
const assertAuthority = vi.fn(async () => {});
|
||||
const driver = new DotHarnessDriver({ identity, principal, expiresAt: Date.now() + 60_000,
|
||||
|
||||
@@ -96,7 +96,7 @@ it.each([
|
||||
return handle;
|
||||
});
|
||||
const getCommand = controlPlane.DurablePrpControlPlane.prototype.getCommand;
|
||||
const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "getCommand").mockImplementation(function (id) {
|
||||
const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "getCommand").mockImplementation(function (this: controlPlane.DurablePrpControlPlane, id) {
|
||||
const command = getCommand.call(this, id);
|
||||
// Hold the SDK poll until the real process has exited. The authenticated
|
||||
// shutdown receipt and its ACK remain committed in the transport journal.
|
||||
@@ -112,7 +112,7 @@ it.each([
|
||||
});
|
||||
let session: Awaited<ReturnType<typeof driver.openSession>> | undefined;
|
||||
try {
|
||||
session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! });
|
||||
session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root });
|
||||
if (kind === "shutdown") {
|
||||
await expect(session.close({ reason: "Test complete" })).resolves.toBeUndefined();
|
||||
expect(exited).toBe(true);
|
||||
@@ -152,7 +152,7 @@ it.each(["local", "local pending finish", "custom local directory", "external la
|
||||
identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId,
|
||||
normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId },
|
||||
runnerBinary: resolve("runner/target/debug/paperclip-runnerd"),
|
||||
onSpawn: process => { spawned = process; },
|
||||
onSpawn: async process => { spawned = process; },
|
||||
controlPlaneRegistration: async authority => {
|
||||
if (emptyController) {
|
||||
authority.setCheckpointPersistence(async saved => { checkpoint = controlPlane.decodeDurablePrpCheckpoint(new Map(saved.records)); });
|
||||
@@ -190,7 +190,7 @@ it.each(["local", "local pending finish", "custom local directory", "external la
|
||||
};
|
||||
}
|
||||
const driver = new RunnerdDotDriver(options);
|
||||
let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! });
|
||||
let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root });
|
||||
const operation = (action: ExternalProviderOperation["action"], args: Record<string, unknown>): ExternalProviderOperation => ({
|
||||
requestId: randomUUID(), bindingId: input.provider.binding.bindingId, bindingGeneration: 1, runId: input.binding.runId,
|
||||
normalizedSessionId: input.session.normalizedSessionId!, turnId: options.identity.turnId, assignmentRevision: 1,
|
||||
|
||||
@@ -82,7 +82,7 @@ it("uses the native request, exact question command ID and durable answer receip
|
||||
execution: input, stateDirectory: join(root, "state"), runnerBinary: resolve("runner/target/debug/paperclip-runnerd"),
|
||||
identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId,
|
||||
normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId },
|
||||
onSpawn: process => { spawned = process; },
|
||||
onSpawn: async process => { spawned = process; },
|
||||
controlPlaneRegistration: async authority => { await authority.start(controllerPort); controllerPort = Number(new URL(authority.connectUrl).port);
|
||||
return { connectUrl: authority.connectUrl, release: () => authority.stop() }; },
|
||||
port: { dispatch: async () => {}, settle: async event => { settlements.set(String(event.payload.requestId), event.payload.outcome); },
|
||||
@@ -95,7 +95,7 @@ it("uses the native request, exact question command ID and durable answer receip
|
||||
});
|
||||
const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "queueCommand");
|
||||
let driver = new RunnerdMuseDriver(options);
|
||||
let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! });
|
||||
let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root });
|
||||
try {
|
||||
const prepared = commandSpy.mock.calls.find(call => call[0] === "run.prepare");
|
||||
expect(JSON.stringify(prepared?.[1])).not.toContain("connections_search");
|
||||
@@ -166,7 +166,7 @@ it("bounds default Muse question answer command IDs at the native queue boundary
|
||||
execution: input, stateDirectory: join(root, "state"), runnerBinary: resolve("runner/target/debug/paperclip-runnerd"),
|
||||
identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId,
|
||||
normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId },
|
||||
onSpawn: process => { spawned = process; },
|
||||
onSpawn: async process => { spawned = process; },
|
||||
controlPlaneRegistration: async authority => { await authority.start(controllerPort); controllerPort = Number(new URL(authority.connectUrl).port);
|
||||
return { connectUrl: authority.connectUrl, release: () => authority.stop() }; },
|
||||
port: { dispatch: async () => {}, settle: async event => { settlements.set(String(event.payload.requestId), event.payload.outcome); },
|
||||
@@ -182,7 +182,7 @@ it("bounds default Muse question answer command IDs at the native queue boundary
|
||||
const response = { schema: "paperclip.question_response.v1" as const, answers: { environment: { selectedOptionIds: ["test"] } } };
|
||||
const commandSpy = vi.spyOn(controlPlane.DurablePrpControlPlane.prototype, "queueCommand");
|
||||
let driver = new RunnerdMuseDriver(options);
|
||||
let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! });
|
||||
let session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root });
|
||||
try {
|
||||
await session.startTurn({ message: { role: "user", text: "Answer boundary questions." } });
|
||||
await send!(operation("accept", {}));
|
||||
@@ -253,7 +253,7 @@ it("rejects bridge-unsupported questions at native admission without a stored re
|
||||
execution: input, stateDirectory: join(root, "state"), runnerBinary: resolve("runner/target/debug/paperclip-runnerd"),
|
||||
identity: { runnerInstanceId: randomUUID(), environmentLeaseId: randomUUID(), runId: input.binding.runId,
|
||||
normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId },
|
||||
onSpawn: process => { spawned = process; },
|
||||
onSpawn: async process => { spawned = process; },
|
||||
port: { dispatch: async () => {}, settle: async () => {}, inputAvailable: async () => {},
|
||||
attach: async callback => { send = callback; return async () => { send = undefined; }; } },
|
||||
};
|
||||
@@ -263,7 +263,7 @@ it("rejects bridge-unsupported questions at native admission without a stored re
|
||||
action: "request_user_input", input: args, digest: externalOperationDigest("request_user_input", args),
|
||||
});
|
||||
const driver = new RunnerdMuseDriver(options);
|
||||
const session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId! });
|
||||
const session = await driver.openSession({ runId: input.binding.runId, normalizedSessionId: input.session.normalizedSessionId!, workingDirectory: root });
|
||||
const events: PrpEvent[] = [];
|
||||
let eventReadFailure: unknown;
|
||||
const reading = (async () => { for await (const event of session.events()) events.push(event); })()
|
||||
@@ -315,8 +315,8 @@ it("keeps the exact Muse question and assignment alive past the native live wind
|
||||
await writeFile(join(root, "AGENTS.md"), "Use only first-party tools.");
|
||||
const input = execution(root);
|
||||
const events: PrpEvent[] = [];
|
||||
const dispatches: PrpEvent[] = [];
|
||||
const deliveries: PrpEvent[] = [];
|
||||
const dispatches: Array<{ sourceEventId: string; payload: Record<string, unknown> }> = [];
|
||||
const deliveries: Array<{ sourceEventId: string; payload: Record<string, unknown> }> = [];
|
||||
const settlements = new Map<string, Record<string, unknown>>();
|
||||
let send: ((operation: ExternalProviderOperation) => Promise<void>) | undefined;
|
||||
let spawned: { pid: number; processGroupId: number | null; startedAt: string } | undefined;
|
||||
@@ -327,7 +327,7 @@ it("keeps the exact Muse question and assignment alive past the native live wind
|
||||
normalizedSessionId: input.session.normalizedSessionId!, turnId: "turn-" + input.binding.runId, itemId: "item-" + input.binding.runId };
|
||||
const driver = new RunnerdMuseDriver({
|
||||
execution: input, stateDirectory: join(root, "state"), identity,
|
||||
runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), onSpawn: process => { spawned = process; },
|
||||
runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), onSpawn: async process => { spawned = process; },
|
||||
port: { dispatch: async event => { dispatches.push(event); }, inputAvailable: async event => { deliveries.push(event); },
|
||||
settle: async event => { settlements.set(String(event.payload.requestId), event.payload.outcome as Record<string, unknown>); },
|
||||
attach: async callback => { send = callback; return async () => { send = undefined; }; } },
|
||||
|
||||
@@ -12,10 +12,14 @@ const report = {
|
||||
evidence: [], verification: [],
|
||||
};
|
||||
|
||||
type RpcResponse = {
|
||||
result: { isError?: boolean; tools: Array<{ name: string; inputSchema: unknown }> };
|
||||
};
|
||||
|
||||
async function rpc(bridge: OpenCodeMcpBridge, id: number, method: string, params?: unknown) {
|
||||
return (await fetch(bridge.url, { method: "POST", headers: {
|
||||
Authorization: `Bearer ${bridge.secret}`, "Content-Type": "application/json",
|
||||
}, body: JSON.stringify({ jsonrpc: "2.0", id, method, params }) })).json();
|
||||
}, body: JSON.stringify({ jsonrpc: "2.0", id, method, params }) })).json() as Promise<RpcResponse>;
|
||||
}
|
||||
|
||||
it("binds closed rejection shapes to the actual offered finish schema without recording values", async () => {
|
||||
@@ -23,7 +27,7 @@ it("binds closed rejection shapes to the actual offered finish schema without re
|
||||
const bridge = await startOpenCodeMcpBridge({ handler, onDiagnostic: message => messages.push(message) });
|
||||
bridges.push(bridge);
|
||||
const listed = await rpc(bridge, 1, "tools/list");
|
||||
const schema = listed.result.tools.find((tool: { name: string }) => tool.name === "paperclip_finish").inputSchema;
|
||||
const schema = listed.result.tools.find((tool: { name: string }) => tool.name === "paperclip_finish")!.inputSchema;
|
||||
const digest = createHash("sha256").update(JSON.stringify(schema)).digest("hex");
|
||||
for (const [index, input] of [report, { ...report, continuation: null }].entries()) {
|
||||
const result = await rpc(bridge, index + 2, "tools/call", { name: "paperclip_finish", arguments: input });
|
||||
|
||||
@@ -241,7 +241,7 @@ describe("OpenCodeServerDriver", () => {
|
||||
});
|
||||
const session = await driver.openSession({ runId: "empty", normalizedSessionId: "empty", workingDirectory: root });
|
||||
try {
|
||||
expect(await session.read()).toMatchObject({ messages: [] });
|
||||
expect(await session.read!()).toMatchObject({ messages: [] });
|
||||
} finally {
|
||||
await session.close({ reason: "test" });
|
||||
}
|
||||
@@ -338,7 +338,7 @@ describe("OpenCodeServerDriver", () => {
|
||||
});
|
||||
const descriptor = await driver.descriptor();
|
||||
expect(
|
||||
descriptor.capabilities.typedEventFamilies.find(
|
||||
descriptor.capabilities.typedEventFamilies?.find(
|
||||
(family) => family.family === "plan",
|
||||
),
|
||||
).toMatchObject({ availability: "unsupported" });
|
||||
@@ -751,7 +751,7 @@ describe("OpenCodeServerDriver", () => {
|
||||
// run.terminal is owned by the native-session layer above this driver.
|
||||
// Keep both absences explicit instead of synthesizing Codex-shaped input.
|
||||
expect(
|
||||
(await driver.descriptor()).capabilities.typedEventFamilies.find(
|
||||
(await driver.descriptor()).capabilities.typedEventFamilies?.find(
|
||||
(family) => family.family === "plan",
|
||||
),
|
||||
).toMatchObject({ availability: "unsupported" });
|
||||
@@ -1105,7 +1105,7 @@ describe("OpenCodeServerDriver", () => {
|
||||
expect(JSON.stringify(events)).not.toContain(
|
||||
"submitted prompt must not become assistant text",
|
||||
);
|
||||
expect(await session.usage()).toMatchObject({
|
||||
expect(await session.usage!()).toMatchObject({
|
||||
input: 3,
|
||||
output: 2,
|
||||
costUsd: 0.001,
|
||||
@@ -1772,7 +1772,7 @@ describe("OpenCodeServerDriver", () => {
|
||||
// A prompt tools map replaces OpenCode's session permissions. Question is
|
||||
// enabled in config without overwriting the allow/ask/deny or path policy.
|
||||
expect(submittedPrompt).not.toHaveProperty("tools");
|
||||
expect(JSON.stringify(submittedPrompt?.parts ?? null)).not.toContain(
|
||||
expect(JSON.stringify((submittedPrompt as Record<string, unknown> | null)?.parts ?? null)).not.toContain(
|
||||
PAPERCLIP_EXECUTION_PROMPT,
|
||||
);
|
||||
const sessionRoot = join(root, "context");
|
||||
@@ -2871,13 +2871,13 @@ describe("OpenCodeServerDriver", () => {
|
||||
await session.startTurn({ message: { role: "user", text: "finish" } });
|
||||
await started;
|
||||
let stopped = false;
|
||||
const stop = session[operation]({ reason: "fixture stop" }).then(() => { stopped = true; });
|
||||
const stop = session[operation]!({ reason: "fixture stop" }).then(() => { stopped = true; });
|
||||
await new Promise<void>(resolve => setImmediate(resolve));
|
||||
expect(stopped).toBe(false);
|
||||
releaseFeedback();
|
||||
await stop;
|
||||
expect((await session.snapshot()).semanticResult).not.toBeNull();
|
||||
const transcript = await session.transcript();
|
||||
const transcript = await session.transcript!();
|
||||
expect(transcript.events.filter(event => event.eventType === "run.result.proposed")).toHaveLength(1);
|
||||
expect(transcript.events.some(event => event.eventType === "item.completed" && (event.payload.item as { is_error?: boolean })?.is_error)).toBe(false);
|
||||
} finally {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("per-turn OpenCode reasoning", () => {
|
||||
});
|
||||
expect(config.provider.openrouter.models["deepseek/deepseek-v4-flash-0731"].options).toBeUndefined();
|
||||
} finally {
|
||||
try { await session?.close(); }
|
||||
try { await session?.close({ reason: "fixture complete" }); }
|
||||
finally { await rm(root, { recursive: true, force: true }); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ afterEach(async () => {
|
||||
await Promise.all(bridges.splice(0).map((bridge) => bridge.close()));
|
||||
});
|
||||
|
||||
type ToolCallBody = { result: { content: unknown } };
|
||||
|
||||
async function rpc(
|
||||
bridge: RunnerToolBridge,
|
||||
body: Record<string, unknown>,
|
||||
@@ -563,7 +565,7 @@ describe("Copilot semantic receipt opt-in", () => {
|
||||
} });
|
||||
bridges.push(bridge);
|
||||
const request = { id: 3, method: "tools/call", params: { name: "paperclip_finish", arguments: rawFinish } };
|
||||
const body = await (await rpc(bridge, request)).json();
|
||||
const body = await (await rpc(bridge, request)).json() as ToolCallBody;
|
||||
expect(receipts).toHaveLength(1);
|
||||
expect(readNativeSemanticReceipt({ contents: body.result.content })).toEqual(receipts[0]);
|
||||
expect(receipts[0]).toMatchObject({ schema: "paperclip.semantic_tool_receipt.v2", inputSha256: semanticInputSha256(rawFinish),
|
||||
@@ -587,11 +589,11 @@ describe("Copilot semantic receipt opt-in", () => {
|
||||
const normalizedFirst = validatePrpStructuredRunResult(rawFinish); const normalizedSecond = validatePrpStructuredRunResult(secondInput);
|
||||
if (!normalizedFirst.ok || !normalizedSecond.ok) throw new Error("Invalid fixture");
|
||||
pending.get("second")!.capture(normalizedSecond.result)(); pending.get("second")!.settle();
|
||||
const secondReceipt = readNativeSemanticReceipt({ contents: (await (await second).json()).result.content });
|
||||
const secondReceipt = readNativeSemanticReceipt({ contents: (await (await second).json() as ToolCallBody).result.content });
|
||||
expect(secondReceipt).toMatchObject({ inputSha256: semanticInputSha256(secondInput), normalizedInputSha256: semanticInputSha256(normalizedSecond.result) });
|
||||
pending.get("second")!.capture(normalizedFirst.result)();
|
||||
pending.get("first")!.capture(normalizedFirst.result)(); pending.get("first")!.settle();
|
||||
const firstReceipt = readNativeSemanticReceipt({ contents: (await (await first).json()).result.content });
|
||||
const firstReceipt = readNativeSemanticReceipt({ contents: (await (await first).json() as ToolCallBody).result.content });
|
||||
expect(firstReceipt).toMatchObject({ inputSha256: semanticInputSha256(rawFinish), normalizedInputSha256: semanticInputSha256(normalizedFirst.result) });
|
||||
expect(receipts).toEqual([secondReceipt, firstReceipt]);
|
||||
});
|
||||
@@ -600,10 +602,10 @@ describe("Copilot semantic receipt opt-in", () => {
|
||||
const withEvidence = await startRunnerToolBridge({ tools: [tool("documents.read")], captureSemanticReceipt: () => () => {}, handler: async call => {
|
||||
expect(call.captureNormalizedInput).toBeUndefined(); return modelMetadata;
|
||||
} }); bridges.push(withEvidence);
|
||||
const body = await (await rpc(withEvidence, { id: 1, method: "tools/call", params: { name: "documents.read", arguments: modelMetadata } })).json();
|
||||
const body = await (await rpc(withEvidence, { id: 1, method: "tools/call", params: { name: "documents.read", arguments: modelMetadata } })).json() as ToolCallBody;
|
||||
expect(readNativeSemanticReceipt({ contents: body.result.content })).toHaveProperty("normalizedInputSha256", null);
|
||||
const plain = await startRunnerToolBridge({ handler: async call => { expect(call.captureNormalizedInput).toBeUndefined(); return { accepted: true }; } }); bridges.push(plain);
|
||||
const plainBody = await (await rpc(plain, { id: 2, method: "tools/call", params: { name: "paperclip_finish", arguments: rawFinish } })).json();
|
||||
const plainBody = await (await rpc(plain, { id: 2, method: "tools/call", params: { name: "paperclip_finish", arguments: rawFinish } })).json() as ToolCallBody;
|
||||
expect(readNativeSemanticReceipt({ contents: plainBody.result.content })).toBeNull();
|
||||
});
|
||||
it.each(["small", "chunked", "tagged", "error"])("preserves %s result encoding and captures scope before dispatch", async encoding => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { prpSchemaBundle } from "../protocol/generated/schema-bundle.js";
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("live ACP request recovery", () => {
|
||||
recovered = await original.snapshot();
|
||||
expect(recovered.pendingRuntimeRequests).toHaveLength(2);
|
||||
} finally {
|
||||
await original.close();
|
||||
await original.close({ reason: "test complete" });
|
||||
}
|
||||
class AdoptedTransport extends FakeCodexTransport {
|
||||
takeRestoredRuntimeRequests(): CodexRpcServerRequest[] { return requests; }
|
||||
@@ -99,7 +99,7 @@ describe("live ACP request recovery", () => {
|
||||
await expect(session.resolveRuntimeRequest?.({ requestId, turnId: "turn-1", resolution })).rejects.toThrow("no longer pending");
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
await session.close(); await drained;
|
||||
await session.close({ reason: "test complete" }); await drained;
|
||||
expect(recoveredEvents.map(event => event.eventType)).not.toContain("runtime_request.created");
|
||||
expect(recoveredEvents.filter(event => event.eventType === "runtime_request.expired").map(event => event.payload.requestId)).not.toContain(requestId);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,9 @@ describe("Antigravity candidate trusted startup", () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "antigravity-candidate-continuation-"));
|
||||
const admission = candidate();
|
||||
const retained = { driverSessionId: "driver-retained", providerSessionId: "native-retained", providerIdentity: {
|
||||
kind: "acpx", normalizedSessionId: identity.normalizedSessionId,
|
||||
kind: "acpx" as const, normalizedSessionId: identity.normalizedSessionId,
|
||||
acpxRecordId: "acpx-retained", backendSessionId: "backend-retained", agentSessionId: "native-retained",
|
||||
workspaceDigest: `sha256:${"e".repeat(64)}`, providerLifetimeFenceCandidates: [1, 2, 3] as const,
|
||||
// The recovery binding digest is distinct from the release command digest.
|
||||
profileDigest: `sha256:${"a".repeat(64)}`, requestedModel: admission.model, effectiveModel: admission.model,
|
||||
} };
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
} from "../drivers/codex/app-server-transport.js";
|
||||
import type { CapabilityIssueThreadSnapshot, CapabilityThreadItem } from "../issue-thread/types.js";
|
||||
import type { CapabilityLiveTransportFactory } from "./live-session.js";
|
||||
import type { CapabilityRunnerdProcessEvidence } from "./runnerd-codex-transport.js";
|
||||
import {
|
||||
createCapabilityTurnStreamDecoder,
|
||||
readCapabilityTurnStream,
|
||||
@@ -343,10 +344,25 @@ class FakeCleanRoomTransport implements CodexAppServerTransport {
|
||||
|
||||
function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransportFactory {
|
||||
return (options) => {
|
||||
const evidence = {
|
||||
const evidence: CapabilityRunnerdProcessEvidence = {
|
||||
runnerPid: 7100,
|
||||
runnerProcessGroupId: 7100,
|
||||
providerPid: null,
|
||||
providerProcessStartedAt: null,
|
||||
codexPid: 7200,
|
||||
codexProcessStartedAt: null,
|
||||
sidecarPid: null,
|
||||
sidecarProcessStartedAt: null,
|
||||
agentPid: null,
|
||||
agentProcessStartedAt: null,
|
||||
providerDriver: null,
|
||||
providerVersion: null,
|
||||
acpxAgent: null,
|
||||
agentServerVersion: null,
|
||||
agentRuntimeVersion: null,
|
||||
acpProtocolVersion: null,
|
||||
providerExecutionKind: null,
|
||||
providerService: null,
|
||||
runnerExited: false,
|
||||
runnerExitCode: null,
|
||||
runnerSignal: null,
|
||||
@@ -359,7 +375,7 @@ function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransport
|
||||
options.onEvidence?.(evidence);
|
||||
});
|
||||
options.onEvidence?.(evidence);
|
||||
return { transport, evidence: () => ({ ...evidence }) };
|
||||
return { transport, evidence: () => ({ ...evidence }), detachControllerForRestart: async () => {} };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -369,6 +385,8 @@ interface CleanRoomPayload {
|
||||
identity: { token: string; companyId: string; actorId: string; taskId: string; identifier: string };
|
||||
limits: { maxTurns: number; maxMessageBytes: number };
|
||||
view: CapabilityIssueThreadSnapshot;
|
||||
toolResult?: unknown;
|
||||
toolTurnId?: string | null;
|
||||
configuration: {
|
||||
provider: "codex" | "opencode";
|
||||
model: string | null;
|
||||
@@ -801,9 +819,9 @@ describe("Capability clean-room chat server", () => {
|
||||
expect(after.toolTurnId).toMatch(/^turn-/);
|
||||
const turn = after.view.turns.find((candidate: { id: string }) => candidate.id === after.toolTurnId);
|
||||
expect(turn).toBeDefined();
|
||||
expect(turn.items.some((item: CapabilityThreadItem) => item.kind === "user_message")).toBe(true);
|
||||
expect(turn.items.some((item: CapabilityThreadItem) => item.kind === "tool_activity" && item.operationId === "report_progress")).toBe(true);
|
||||
expect(turn.items.some((item: CapabilityThreadItem) => item.kind === "durable_comment" && item.body === "Progress entered manually from DevTools.")).toBe(true);
|
||||
expect(turn!.items.some((item: CapabilityThreadItem) => item.kind === "user_message")).toBe(true);
|
||||
expect(turn!.items.some((item: CapabilityThreadItem) => item.kind === "tool_activity" && item.operationId === "report_progress")).toBe(true);
|
||||
expect(turn!.items.some((item: CapabilityThreadItem) => item.kind === "durable_comment" && item.body === "Progress entered manually from DevTools.")).toBe(true);
|
||||
expect(after.view.evidence.state.at(-1)?.toRevision).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
type CapabilityLiveTurnEvent,
|
||||
} from "./live-session.js";
|
||||
import { DurableCapabilityLiveSessionStore } from "./durable-live-session-store.js";
|
||||
import { defaultCapabilityRunnerdBinary } from "./runnerd-codex-transport.js";
|
||||
import { defaultCapabilityRunnerdBinary, type CapabilityRunnerdProcessEvidence } from "./runnerd-codex-transport.js";
|
||||
import { persistedCursorUsageNotice } from "../drivers/acpx/usage-accounting.js";
|
||||
import { captureTurnRejection } from "../../test/capture-turn-rejection.js";
|
||||
import * as workspaceDiff from "./workspace-diff.js";
|
||||
@@ -45,7 +45,7 @@ it("admits Pi live sessions without candidate opt-in while preserving the exact
|
||||
const session = await service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "openrouter/deepseek/deepseek-v4-flash-0731" });
|
||||
try {
|
||||
expect(session.snapshot().config.acpxProfile).toMatchObject({
|
||||
agent: "pi", ...resolveQualifiedAcpxProfile("pi", "openrouter/deepseek/deepseek-v4-flash-0731"),
|
||||
...resolveQualifiedAcpxProfile("pi", "openrouter/deepseek/deepseek-v4-flash-0731"), agent: "pi",
|
||||
});
|
||||
const custom = await service.create({ provider: "acpx", acpxAgent: "pi", piThinkingLevel: "low", requestedModel: "another-model" });
|
||||
expect(custom.snapshot().config.acpxProfile).toMatchObject({ qualificationModel: "another-model", reportedModelId: "another-model" });
|
||||
@@ -322,10 +322,25 @@ class FakeCapabilityCodexTransport implements CodexAppServerTransport {
|
||||
|
||||
function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransportFactory {
|
||||
return (options) => {
|
||||
const evidence = {
|
||||
const evidence: CapabilityRunnerdProcessEvidence = {
|
||||
runnerPid: 4100 + state.transports.length,
|
||||
runnerProcessGroupId: 4100 + state.transports.length,
|
||||
providerPid: null,
|
||||
providerProcessStartedAt: null,
|
||||
codexPid: 5100 + state.transports.length,
|
||||
codexProcessStartedAt: null,
|
||||
sidecarPid: null,
|
||||
sidecarProcessStartedAt: null,
|
||||
agentPid: null,
|
||||
agentProcessStartedAt: null,
|
||||
providerDriver: null,
|
||||
providerVersion: null,
|
||||
acpxAgent: null,
|
||||
agentServerVersion: null,
|
||||
agentRuntimeVersion: null,
|
||||
acpProtocolVersion: null,
|
||||
providerExecutionKind: null,
|
||||
providerService: null,
|
||||
runnerExited: false,
|
||||
runnerExitCode: null,
|
||||
runnerSignal: null,
|
||||
@@ -337,7 +352,7 @@ function fakeTransportFactory(state: FakeProviderState): CapabilityLiveTransport
|
||||
options.onEvidence?.(evidence);
|
||||
});
|
||||
options.onEvidence?.(evidence);
|
||||
return { transport, evidence: () => ({ ...evidence, runnerExited: transport.processInfo().exited }) };
|
||||
return { transport, evidence: () => ({ ...evidence, runnerExited: transport.processInfo().exited }), detachControllerForRestart: async () => {} };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -394,7 +409,7 @@ class TransientFailureLiveSessionStore implements CapabilityLiveSessionStore {
|
||||
throw new Error("transient live-session store failure");
|
||||
}
|
||||
if (
|
||||
snapshot.terminalTurns.length > 0 &&
|
||||
snapshot.terminalTurns!.length > 0 &&
|
||||
(this.#rejectTerminalSaves || this.#terminalSaveFailuresRemaining > 0)
|
||||
) {
|
||||
if (this.#terminalSaveFailuresRemaining > 0) {
|
||||
@@ -647,7 +662,7 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
outputTokens: 4,
|
||||
})).resolves.toBe("committed");
|
||||
|
||||
expect((await store.load(session.id))?.usageLedger.map((receipt) => receipt.receiptId)).toEqual([
|
||||
expect((await store.load(session.id))?.usageLedger!.map((receipt) => receipt.receiptId)).toEqual([
|
||||
"usage-before-recovery",
|
||||
"usage-after-recovery",
|
||||
]);
|
||||
@@ -1159,7 +1174,7 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
expect(first.snapshot.providerThreadId).toBe(second.snapshot.providerThreadId);
|
||||
expect(state.attachments).toHaveLength(1);
|
||||
expect(second.snapshot.providerRunBinding).toEqual(state.attachments[0]);
|
||||
expect(second.snapshot.usageLedger.slice(-2)).toMatchObject([
|
||||
expect(second.snapshot.usageLedger!.slice(-2)).toMatchObject([
|
||||
{ providerRequests: 1, inputTokens: 100, outputTokens: 10 },
|
||||
{ providerRequests: 1, inputTokens: 100, outputTokens: 10 },
|
||||
]);
|
||||
@@ -1497,7 +1512,7 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
const result = await session.sendMessage("report overlapping usage");
|
||||
|
||||
expect(result.snapshot.usageLedger).toHaveLength(1);
|
||||
expect(result.snapshot.usageLedger[0]).toMatchObject({
|
||||
expect(result.snapshot.usageLedger![0]).toMatchObject({
|
||||
providerRequests: 1,
|
||||
inputTokens: 100,
|
||||
outputTokens: 10,
|
||||
@@ -1596,7 +1611,7 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
const result = await session.sendMessage("report late usage");
|
||||
|
||||
expect(result.snapshot.usageLedger).toHaveLength(1);
|
||||
expect(result.snapshot.usageLedger[0]).toMatchObject({
|
||||
expect(result.snapshot.usageLedger![0]).toMatchObject({
|
||||
providerRequests: 2,
|
||||
inputTokens: 100,
|
||||
outputTokens: 10,
|
||||
|
||||
@@ -106,10 +106,10 @@ it.each([
|
||||
const recovered = await restarted.backend.recoverSession!(snapshot, { signal: new AbortController().signal });
|
||||
expect(recovered).toMatchObject({ recovered: true });
|
||||
if (!recovered.recovered) throw new Error(recovered.reason);
|
||||
expect((await recovered.session.snapshot()).workingDirectory).toBe(cwd);
|
||||
expect((await recovered.session!.snapshot()).workingDirectory).toBe(cwd);
|
||||
// Recovery reuses the saved preparation instead of resolving a new directory.
|
||||
expect(restarted.prepareLaunch.mock.calls[0]![1]).toMatchObject({ configurationId: "plugin-run" });
|
||||
await recovered.session.close({ reason: "test complete" });
|
||||
await recovered.session!.close({ reason: "test complete" });
|
||||
|
||||
const moved = await pluginBackend(root, join(root, "unused"), "runner_prepared_launch");
|
||||
await expect(moved.backend.recoverSession!({ ...snapshot, workingDirectory: join(root, "elsewhere") },
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("ACPX questions through the shared runner transport", () => {
|
||||
[timing!.id]: { selectedOptionIds: [timing!.options![1]!.id] },
|
||||
} };
|
||||
expect(runtimeRequestResponse({ requestId: "question-1", requestKind: "elicitation", method: origin.method,
|
||||
turnId: "turn", itemId: "item", status: "pending", prompt: "Interview", input: shown }, { action: "submit", response }, context)).toEqual({ action: "submit", response });
|
||||
turnId: "turn", itemId: "item", status: "pending", prompt: "Interview", details: {}, input: shown }, { action: "submit", response }, context)).toEqual({ action: "submit", response });
|
||||
expect(form.accept(response)).toEqual({ action: "accept", content: { organization: "Garden club", goal: "Welcome note", timing: "later" } });
|
||||
});
|
||||
it("does not admit an invalid canonical question set", () => {
|
||||
|
||||
@@ -47,7 +47,7 @@ async function fixture(agent: "codex" | "claude" = "codex", nativeSource = false
|
||||
binding: { companyId: nativeIdentity.companyId, agentId: nativeIdentity.agentId, issueId: nativeIdentity.issueId, runId: identity.runId, executionWorkspaceId: "workspace" },
|
||||
task: { identifier: "TASK-1", title: "Complete fake work", description: null, prompt: "Complete fake work", workMode: "standard" },
|
||||
workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null },
|
||||
session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "acpx_runtime", protocolVersion: 1 },
|
||||
session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "acpx_runtime", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
provider: { kind: "acpx", agent, model: "explicit-source-model", permissionPolicy: "interactive", profile: resolveQualifiedAcpxProfile(agent, "explicit-source-model") },
|
||||
completionContract: { id: "contract", sha256: sha("contract"), schemaVersion: "paperclip.completion-contract.v1",
|
||||
contract: { revision: "1", objective: "Complete fake work", criteria: [{ id: "objective", requirement: "Complete fake work" }] } }, interactionResponses: [], credentialBindings: [] };
|
||||
@@ -147,7 +147,7 @@ it.skipIf(process.platform === "win32")("recovers and adopts ACPX through the re
|
||||
environment: { PAPERCLIP_WORKSPACE_CWD: f.root }, dynamicTools: [] });
|
||||
let recovered = await backend().recoverSession!(f.nativeSnapshot!, { signal: new AbortController().signal });
|
||||
expect(recovered.recovered).toBe(true);
|
||||
const pid = source.evidence().runnerPid, startedAt = source.transport.processInfo().startedAt;
|
||||
const pid = source.evidence().runnerPid, startedAt = source.transport.processInfo!().startedAt;
|
||||
expect(await f.count("turn.start")).toBe(1); expect(await f.count("processStart")).toBe(2);
|
||||
await recovered.session!.detachControllerForRestart!();
|
||||
const launch = vi.fn(() => { throw new Error("duplicate source launch"); });
|
||||
@@ -197,7 +197,7 @@ it.skipIf(process.platform === "win32").each(["codex", "claude", "adopt", "pause
|
||||
await expect(source.transport.request("turn/start", {})).rejects.toThrow("source_attachment_pending");
|
||||
const pid = source.evidence().runnerPid;
|
||||
if (mode === "adopt") {
|
||||
const startedAt = source.transport.processInfo().startedAt;
|
||||
const startedAt = source.transport.processInfo!().startedAt;
|
||||
await source.detachControllerForRestart();
|
||||
const launch = vi.fn(() => { throw new Error("duplicate source launch"); });
|
||||
source = f.recover({ runnerProcessLauncher: launch, authorizeRunnerAdoption: async () => { throw new Error("old source configuration"); },
|
||||
|
||||
@@ -38,7 +38,7 @@ async function verifyLiveRunnerAdoption(
|
||||
evidence: [], verification: [], attentionRequests: [], artifacts: [],
|
||||
}));
|
||||
const server = createServer();
|
||||
let authority: DurablePrpControlPlane | null = null;
|
||||
let authority = null as DurablePrpControlPlane | null;
|
||||
const checkpoint = vi.fn(async () => undefined);
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
if (!authority) {
|
||||
@@ -136,7 +136,7 @@ async function verifyLiveRunnerAdoption(
|
||||
);
|
||||
}
|
||||
runnerPid = first.evidence().runnerPid;
|
||||
runnerStartedAt = first.transport.processInfo().startedAt;
|
||||
runnerStartedAt = first.transport.processInfo!().startedAt;
|
||||
expect(runnerPid).toEqual(expect.any(Number));
|
||||
|
||||
if (goalMidTurn) {
|
||||
@@ -404,7 +404,7 @@ async function verifyLiveRunnerAdoption(
|
||||
} else expect(redispatchedTool).not.toHaveBeenCalled();
|
||||
}
|
||||
expect(adopted.evidence().runnerPid).toBe(runnerPid);
|
||||
expect(adopted.transport.processInfo().startedAt).toBe(runnerStartedAt);
|
||||
expect(adopted.transport.processInfo!().startedAt).toBe(runnerStartedAt);
|
||||
if (startWithoutCheckpoint) {
|
||||
const retained = JSON.parse(await readFile(controlPlaneStatePath, "utf8"));
|
||||
for (const type of ["run.prepare", "session.open"]) {
|
||||
@@ -448,7 +448,7 @@ async function verifyLiveRunnerAdoption(
|
||||
expect(await Promise.race([notificationsClosed, new Promise<boolean>(resolve => { queueDeadline = setTimeout(() => resolve(false), 250); })])).toBe(true);
|
||||
} finally { clearTimeout(queueDeadline); }
|
||||
await expect(adopted.transport.close()).rejects.toBe(probeError);
|
||||
expect(adopted.transport.processInfo()).toMatchObject({ exited: false });
|
||||
expect(adopted.transport.processInfo!()).toMatchObject({ exited: false });
|
||||
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
|
||||
expect(signal).not.toHaveBeenCalled();
|
||||
expect(checkpoint).not.toHaveBeenCalled();
|
||||
|
||||
+14
-4
@@ -14,9 +14,18 @@ import { expect, it, vi } from "vitest";
|
||||
import { DurablePrpControlPlane } from "../control-plane/durable-prp-control-plane.js";
|
||||
import * as durableControlPlane from "../control-plane/durable-prp-control-plane.js";
|
||||
import { createCapabilityRunnerdCodexTransport, readRunnerdArtifactBinding } from "./runnerd-codex-transport.js";
|
||||
import { defaultCapabilityRunnerdBinary, fakeCodex, fakeCodexArgs } from "../../test/support/runnerd-transport.js";
|
||||
import { committableStore, defaultCapabilityRunnerdBinary, fakeCodex, fakeCodexArgs } from "../../test/support/runnerd-transport.js";
|
||||
|
||||
it.each([
|
||||
type WarmAttachRecoveryCase = {
|
||||
lossPoint: string;
|
||||
routed: boolean;
|
||||
recoveryFault: string;
|
||||
executionPrepared?: boolean;
|
||||
externalArtifact?: boolean;
|
||||
ordinaryFollowup?: boolean | string;
|
||||
};
|
||||
|
||||
it.each<WarmAttachRecoveryCase>([
|
||||
...[
|
||||
"before-result",
|
||||
"after-result",
|
||||
@@ -478,14 +487,15 @@ it.each([
|
||||
).toBe(false);
|
||||
return;
|
||||
}
|
||||
const commit = core.store.commit.bind(core.store);
|
||||
const store = committableStore(core);
|
||||
const commit = store.commit.bind(store);
|
||||
let lossObserved = false;
|
||||
let signalLoss!: () => void;
|
||||
const loss = new Promise<void>((resolveLoss) => {
|
||||
signalLoss = resolveLoss;
|
||||
});
|
||||
commitSpy = vi
|
||||
.spyOn(core.store, "commit")
|
||||
.spyOn(store, "commit")
|
||||
.mockImplementation((candidate) => {
|
||||
// The selected process-crash boundary stays unavailable until the
|
||||
// exact owned runner is joined. A later replay must not silently
|
||||
|
||||
@@ -67,7 +67,7 @@ it("keeps a quiet active Codex turn in the same process across connection lease
|
||||
expect(calls).not.toContain("turn/interrupt");
|
||||
expect(calls).not.toContain("thread/resume");
|
||||
const state = JSON.parse(await readFile(join(stateDirectory, "fake-codex-state.json"), "utf8"));
|
||||
expect(state.threadId).toBe(opened.thread.id);
|
||||
expect(state.threadId).toBe((opened.thread as { id: string }).id);
|
||||
expect(state.activeTurnId).toBe("provider-turn-1");
|
||||
} finally {
|
||||
await bundle.transport.close();
|
||||
|
||||
@@ -286,7 +286,7 @@ it.each([
|
||||
// output plus another full provider-owned prefix behind the old suspend.
|
||||
const runnerFile = join(original, "runner/runner-state.json");
|
||||
const runnerBefore = JSON.parse(await readFile(runnerFile, "utf8"));
|
||||
const template = builder.store.state.committedEvents[0]!.envelope.payload;
|
||||
const template = builder.store.state.committedEvents[0]!.envelope.payload as Record<string, unknown>;
|
||||
for (let index = 0; index < 90; index++) {
|
||||
const sourceSeq = runnerBefore.nextSourceSeq++;
|
||||
const event = {
|
||||
@@ -388,7 +388,7 @@ it.each([
|
||||
sessionId: identity.normalizedSessionId,
|
||||
},
|
||||
backend: {
|
||||
kind: "codex",
|
||||
kind: "runner" as const,
|
||||
name: maintenanceFixtureBackendName(fixtureId),
|
||||
},
|
||||
identity,
|
||||
@@ -406,7 +406,7 @@ it.each([
|
||||
recordEpoch,
|
||||
signal: maintenanceAbort.signal,
|
||||
};
|
||||
const close = vi.fn(async () => {
|
||||
const close = vi.fn(async (): Promise<void> => {
|
||||
throw new NativeSessionCloseUnrecoverableError();
|
||||
});
|
||||
const start = vi.fn(async () => {
|
||||
@@ -424,9 +424,10 @@ it.each([
|
||||
capabilities: async () => capabilities,
|
||||
events: async function* () {},
|
||||
startTurn: start,
|
||||
result: async () => { throw new Error("fixture never produces a result"); },
|
||||
close,
|
||||
snapshot: async () => ({
|
||||
backendKind: "codex",
|
||||
backendKind: "runner",
|
||||
sessionId: input.binding.sessionId,
|
||||
identity: input.binding,
|
||||
providerSessionId: thread.thread.id,
|
||||
@@ -471,6 +472,7 @@ it.each([
|
||||
normalizedSessionId: input.binding.sessionId,
|
||||
driverKind: "codex_app_server",
|
||||
protocolVersion: 1,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
},
|
||||
completionContract: {
|
||||
id: "contract-maintenance",
|
||||
|
||||
@@ -211,6 +211,7 @@ it("does not retry a real memoized transport close whose suspension proof is una
|
||||
normalizedSessionId: identity.sessionId,
|
||||
driverKind: "codex_app_server",
|
||||
protocolVersion: 1,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
},
|
||||
provider: { kind: "codex", model: null },
|
||||
completionContract: {
|
||||
@@ -499,7 +500,7 @@ it.each([
|
||||
message: { role: "user", text: "Keep working until interrupted." },
|
||||
});
|
||||
if (label === "interrupted") {
|
||||
await session.interrupt({
|
||||
await session.interrupt!({
|
||||
turnId: turn.turnId,
|
||||
reason: "Stopped by the user",
|
||||
});
|
||||
@@ -525,7 +526,7 @@ it.each([
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await session.close().catch(() => undefined);
|
||||
await session.close({ reason: "test complete" }).catch(() => undefined);
|
||||
await rm(stateDirectory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { codexSemanticToolSpecs } from "../drivers/codex/codex-app-server-driver
|
||||
import { releaseMaterializedNativeRuntimeSkills } from "../drivers/runtime-context-materializer.js";
|
||||
import { createCapabilityRunnerdCodexTransport, readRunnerdArtifactBinding } from "./runnerd-codex-transport.js";
|
||||
import {
|
||||
committableStore,
|
||||
defaultCapabilityRunnerdBinary,
|
||||
fakeCodex,
|
||||
fakeCodexArgs,
|
||||
@@ -163,8 +164,9 @@ it.each([
|
||||
providerPid = bundle.evidence().codexPid;
|
||||
const rotations: (typeof core.store.state)[] = [];
|
||||
const preparedStates: (typeof core.store.state)[] = [];
|
||||
const commit = core.store.commit.bind(core.store);
|
||||
vi.spyOn(core.store, "commit").mockImplementation((candidate) => {
|
||||
const store = committableStore(core);
|
||||
const commit = store.commit.bind(store);
|
||||
vi.spyOn(store, "commit").mockImplementation((candidate) => {
|
||||
commit(candidate);
|
||||
// Capture the durable old epoch at publication. The authenticated
|
||||
// successor may activate before attachRun observes the completed command.
|
||||
@@ -716,7 +718,7 @@ it("recovers an active second warm business run after deleting its controller co
|
||||
await writeFile(join(skillRoot, "SKILL.md"), "# Pinned skill\n");
|
||||
await writeFile(join(instructionRoot, "AGENTS.md"), "Pinned instructions\n");
|
||||
const server = createServer();
|
||||
let authority: DurablePrpControlPlane | null = null, generation = 0;
|
||||
let authority = null as DurablePrpControlPlane | null, generation = 0;
|
||||
let checkpoint: durableControlPlane.DurablePrpCheckpoint | undefined;
|
||||
const registered = new WeakSet<DurablePrpControlPlane>();
|
||||
server.on("upgrade", (request, socket, head) => authority
|
||||
@@ -760,7 +762,7 @@ it("recovers an active second warm business run after deleting its controller co
|
||||
providerPid = first.evidence().codexPid;
|
||||
expect(providerPid).toEqual(expect.any(Number));
|
||||
const providerStart = execFileSync("ps", ["-p", String(providerPid), "-o", "lstart="], { encoding: "utf8" }).trim();
|
||||
const startedAt = first.transport.processInfo().startedAt;
|
||||
const startedAt = first.transport.processInfo!().startedAt;
|
||||
const firstCompleted = waitComplete(first.transport);
|
||||
await within("first turn/start", first.transport.request("turn/start", { input: [{ type: "text", text: "first healthy business run" }] }));
|
||||
await within("healthy first turn completion", firstCompleted);
|
||||
@@ -805,7 +807,7 @@ it("recovers an active second warm business run after deleting its controller co
|
||||
expect(execFileSync("ps", ["-p", String(providerPid), "-o", "lstart="], { encoding: "utf8" }).trim()).toBe(providerStart);
|
||||
expect(calls).toBe(2);
|
||||
expect(authority!.store.state.commands.filter(command => command.type === "semantic_tool.result")).toHaveLength(0);
|
||||
expect(recovered.transport.processInfo().startedAt).toBe(startedAt);
|
||||
expect(recovered.transport.processInfo!().startedAt).toBe(startedAt);
|
||||
expect(authority!.store.state.identity).toEqual(savedIdentity);
|
||||
expect(authority!.store.state.commands.filter(command => command.type === "turn.start")).toHaveLength(1);
|
||||
expect(noSpawn).not.toHaveBeenCalled();
|
||||
|
||||
@@ -283,7 +283,7 @@ it.each(["resolved", "rejected"] as const)(
|
||||
expect(error).toMatchObject({ message: expect.stringContaining("runner_process_termination_unconfirmed") });
|
||||
if (outcome === "rejected") expect(error).toHaveProperty("cause", probeError);
|
||||
expect(children).toHaveLength(1);
|
||||
expect(bundle.transport.processInfo()).toMatchObject({ exited: false });
|
||||
expect(bundle.transport.processInfo!()).toMatchObject({ exited: false });
|
||||
expect(() => process.kill(children[0]!.pid!, 0)).not.toThrow();
|
||||
} finally {
|
||||
const providerPid = bundle.evidence().codexPid;
|
||||
@@ -358,7 +358,7 @@ it("preserves the cause when a replacement runner's health cannot be verified",
|
||||
if (child) return {
|
||||
child: { pid: 42, exitCode: null, kill: () => false },
|
||||
completion: Promise.reject(probeError),
|
||||
isExitConfirmed: () => false,
|
||||
isExitConfirmed: (): boolean => false,
|
||||
};
|
||||
child = spawn(spec.command, [...spec.args], { cwd: spec.cwd, env: spec.environment, stdio: "ignore" });
|
||||
const ownedChild = child;
|
||||
@@ -390,7 +390,7 @@ it("preserves the cause when a replacement runner's health cannot be verified",
|
||||
});
|
||||
expect(error).toHaveProperty("message", expect.stringContaining("probe_timeout"));
|
||||
expect(launch).toHaveBeenCalledTimes(2);
|
||||
expect(bundle.transport.processInfo()).toMatchObject({ exited: false });
|
||||
expect(bundle.transport.processInfo!()).toMatchObject({ exited: false });
|
||||
} finally {
|
||||
const providerPid = bundle.evidence().codexPid;
|
||||
if (providerPid) {
|
||||
@@ -415,7 +415,7 @@ it("retries a replacement whose staging failed before any process launched", asy
|
||||
if (launch.mock.calls.length === 2) return {
|
||||
child: { pid: undefined, exitCode: null, kill: () => false },
|
||||
completion: Promise.reject(stagingError),
|
||||
isExitConfirmed: () => true,
|
||||
isExitConfirmed: (): boolean => true,
|
||||
};
|
||||
const child = spawn(spec.command, [...spec.args], { cwd: spec.cwd, env: spec.environment, stdio: "ignore" });
|
||||
children.push(child);
|
||||
@@ -2659,7 +2659,7 @@ it.each(["reuse", "large-instructions", "rotated-recovery", "removed-context", "
|
||||
resolved.skills[0]!.bundle.rootPath = "/guest/.agents/skills/assigned";
|
||||
const materialize = vi.spyOn(runtimeMaterializer, "prepareIsolatedCodexHome").mockRejectedValue(new Error("unexpected Core materialization"));
|
||||
const discover = vi.spyOn(codexCommandRuntime, "resolveCodexCommand").mockImplementation(() => { throw new Error("unexpected Core discovery"); });
|
||||
let configuration: Record<string, unknown> | undefined;
|
||||
let configuration: Record<string, unknown> | null | undefined;
|
||||
let recovered = false;
|
||||
let requestedRun = "";
|
||||
let requestedInstructions = "";
|
||||
@@ -3574,7 +3574,7 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
let template: Record<string, unknown> | null = null;
|
||||
let registrationStarted!: () => void;
|
||||
const started = new Promise<void>(resolve => { registrationStarted = resolve; });
|
||||
let returnRegistration!: (registration: { connection: { mode: "listen"; listenAddress: string; listenPort: number; listenPath: string }; release: () => Promise<void> }) => void;
|
||||
let returnRegistration!: (registration: { connection: { mode: "listen"; listenAddress: "0.0.0.0"; listenPort: number; listenPath: string }; release: () => Promise<void> }) => void;
|
||||
const registration = new Promise<Parameters<typeof returnRegistration>[0]>(resolve => { returnRegistration = resolve; });
|
||||
let finishRelease!: () => void;
|
||||
const releasing = new Promise<void>(resolve => { finishRelease = resolve; });
|
||||
@@ -3585,7 +3585,7 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
if (releaseFails) throw releaseError;
|
||||
});
|
||||
const registered = {
|
||||
connection: { mode: "listen" as const, listenAddress: "127.0.0.1", listenPort: 43127, listenPath: "/setup-probe" },
|
||||
connection: { mode: "listen" as const, listenAddress: "0.0.0.0" as const, listenPort: 43127, listenPath: "/setup-probe" },
|
||||
release,
|
||||
};
|
||||
let registrationSignal: AbortSignal | undefined;
|
||||
@@ -4037,7 +4037,7 @@ it("runs the lab provider boundary through authenticated durable PRP", async ()
|
||||
}
|
||||
expect(methods).toContain("turn/completed");
|
||||
expect(terminalParams).toMatchObject({
|
||||
threadId: opened.thread.id,
|
||||
threadId: (opened.thread as { id: string }).id,
|
||||
turnId: "provider-turn-1",
|
||||
});
|
||||
expect(bundle.evidence().diagnostics).toContain(
|
||||
@@ -4081,7 +4081,7 @@ it("controls a Codex session goal end to end through durable PRP v2", async () =
|
||||
},
|
||||
],
|
||||
});
|
||||
const threadId = opened.thread.id;
|
||||
const threadId = (opened.thread as { id: string }).id;
|
||||
|
||||
await expect(
|
||||
bundle.transport.request("thread/goal/set", {
|
||||
@@ -4236,7 +4236,7 @@ it.each([false, true])("binds goal turns through the full Codex harness (autonom
|
||||
});
|
||||
expect(observed.some((event) => event.eventType === "session.failed")).toBe(false);
|
||||
} finally {
|
||||
await session?.close();
|
||||
await session?.close({ reason: "test complete" });
|
||||
await bundle.transport.close();
|
||||
await rm(stateDirectory, { recursive: true, force: true });
|
||||
}
|
||||
@@ -4882,7 +4882,7 @@ it("binds an immediately failed durable turn before exposing its terminal", asyn
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
await session.close({ reason: "test complete" });
|
||||
await rm(stateDirectory, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
@@ -5089,9 +5089,10 @@ it("captures exact provider frames and correlates Rust and TypeScript interpreta
|
||||
if (event.eventType === "turn.completed") break;
|
||||
}
|
||||
} finally {
|
||||
await session.close();
|
||||
await session.close({ reason: "test complete" });
|
||||
}
|
||||
const snapshot = await session.snapshot();
|
||||
if (snapshot.lastSourceSequence === undefined) throw new Error("Expected a source sequence in the session snapshot");
|
||||
for (
|
||||
let sourceSeq = 1;
|
||||
sourceSeq <= snapshot.lastSourceSequence;
|
||||
@@ -5791,7 +5792,7 @@ it.each([
|
||||
expect(outcomes[1].result).toMatchObject({ content: [{ text: expect.stringContaining(feedback) }] });
|
||||
if (adoptAfterTurn) {
|
||||
const snapshot = await session.snapshot();
|
||||
const runnerStartedAt = bundle.transport.processInfo().startedAt;
|
||||
const runnerStartedAt = bundle.transport.processInfo!().startedAt;
|
||||
survivingPid = bundle.evidence().runnerPid;
|
||||
expect(survivingPid).toEqual(expect.any(Number));
|
||||
const artifact = readRunnerdArtifactBinding(defaultCapabilityRunnerdBinary());
|
||||
@@ -5833,7 +5834,7 @@ it.each([
|
||||
expect(recovery.session).toBeDefined();
|
||||
session = recovery.session!;
|
||||
expect(adopted.evidence().runnerPid).toBe(survivingPid);
|
||||
expect(adopted.transport.processInfo().startedAt).toBe(runnerStartedAt);
|
||||
expect(adopted.transport.processInfo!().startedAt).toBe(runnerStartedAt);
|
||||
expect((await session.snapshot()).providerSessionId).toBe(snapshot.providerSessionId);
|
||||
expect(duplicateLauncher).not.toHaveBeenCalled();
|
||||
}
|
||||
@@ -5843,7 +5844,7 @@ it.each([
|
||||
expect(allRequests.map(request => request.variant)).toEqual(supportsReasoning
|
||||
? ["paperclip-no-reasoning", "paperclip-default"] : [undefined, undefined]);
|
||||
},
|
||||
closeSession: async () => { await session?.close(); },
|
||||
closeSession: async () => { await session?.close({ reason: "test complete" }); },
|
||||
closeTransport: async () => {
|
||||
try { await adopted?.transport.close(); }
|
||||
finally {
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { DurablePrpControlPlane } from "../control-plane/durable-prp-control-plane.js";
|
||||
import { DurablePrpControlPlane, type RunnerProcessResult } from "../control-plane/durable-prp-control-plane.js";
|
||||
import { createCapabilityRunnerdCodexTransport, defaultCapabilityRunnerdBinary, readRunnerdArtifactBinding, runnerdRecoveryInternals } from "./runnerd-codex-transport.js";
|
||||
|
||||
it.each(["resolved", "rejected"] as const)("keeps the checkpoint behind controller retirement (%s)", async outcome => {
|
||||
@@ -39,7 +39,7 @@ it.each(["drain", "timeout", "external_close"] as const)("joins an output flush
|
||||
const artifact = readRunnerdArtifactBinding(binary);
|
||||
let core!: DurablePrpControlPlane;
|
||||
const start = DurablePrpControlPlane.prototype.start;
|
||||
const startSpy = vi.spyOn(DurablePrpControlPlane.prototype, "start").mockImplementation(function (port) {
|
||||
const startSpy = vi.spyOn(DurablePrpControlPlane.prototype, "start").mockImplementation(function (this: DurablePrpControlPlane, port) {
|
||||
core = this;
|
||||
return start.call(this, port);
|
||||
});
|
||||
@@ -81,7 +81,7 @@ it.each(["drain", "timeout", "external_close"] as const)("joins an output flush
|
||||
let stderr = "";
|
||||
owned.stderr?.on("data", chunk => { stderr += String(chunk); });
|
||||
exit = new Promise(resolveExit => owned.once("exit", () => resolveExit()));
|
||||
const completion = new Promise<{ code: number | null; signal: string | null; stdout: string; stderr: string }>((resolveExit, reject) => {
|
||||
const completion = new Promise<RunnerProcessResult>((resolveExit, reject) => {
|
||||
owned.once("error", reject);
|
||||
owned.once("exit", (code, signal) => resolveExit({ code, signal, stdout: "", stderr }));
|
||||
});
|
||||
|
||||
@@ -76,14 +76,14 @@ it.skipIf(!compilerAvailable || !existsSync(runner)).each(["discovery", "complet
|
||||
expect(seen).toEqual(tool !== "completion" ? ["paperclip_search_assigned_tools", "paperclip_finish"] : ["paperclip_finish"]);
|
||||
expect(text.join("")).toBe("Streaming progress. ".repeat(800));
|
||||
if (tool === "durable") {
|
||||
const runnerPid = transport.processInfo().pid;
|
||||
const runnerPid = transport.processInfo!().pid;
|
||||
await transport.attachRun!({ runId: "next-settled-run", turnId: "next-turn", itemId: "next-item" });
|
||||
completed = (async () => {
|
||||
for await (const event of transport.notifications()) if (event.method === "turn/completed") return;
|
||||
})();
|
||||
await transport.request("turn/start", { input: [{ type: "text", text: "stream-backlog-discovery" }] });
|
||||
await completed;
|
||||
expect(transport.processInfo().pid).toBe(runnerPid);
|
||||
expect(transport.processInfo!().pid).toBe(runnerPid);
|
||||
expect(seen).toEqual(["paperclip_search_assigned_tools", "paperclip_finish", "paperclip_search_assigned_tools", "paperclip_finish"]);
|
||||
const provider = JSON.parse(await readFile(join(root, "state", "runner", "codex-provider-state.json"), "utf8"));
|
||||
expect(provider.toolBridge.pending).toEqual({});
|
||||
|
||||
@@ -15,7 +15,7 @@ it.each(["none", "before-start", "after-start"] as const)("completes after the r
|
||||
const execution: NativeExecutionInputV1 = {
|
||||
schema: "paperclip.native-execution-input.v1",
|
||||
binding: { runId: identity.runId, companyId: identity.companyId, issueId: identity.issueId, agentId: identity.agentId, executionWorkspaceId: "workspace-restart" },
|
||||
session: { normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1 },
|
||||
session: { normalizedSessionId: identity.sessionId, driverKind: "codex_app_server", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
task: { identifier: "TEST-1", title: "Finish checks", description: null, prompt: "Watch the already running checks and finish the request.", workMode: "standard" },
|
||||
workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null },
|
||||
provider: { kind: "codex", model: null },
|
||||
@@ -88,7 +88,7 @@ it.each(["none", "before-start", "after-start"] as const)("completes after the r
|
||||
throw interruptedController;
|
||||
}
|
||||
},
|
||||
async appendEvent(event) { events.push(event); durableEvents.push(event); return { cursor: durableEvents.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event) { if (!("sourceSeq" in event)) throw new Error("Expected a durable PRP event"); events.push(event); durableEvents.push(event); return { cursor: durableEvents.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(request) {
|
||||
const source = durableEvents.filter(event => event.sourceInstanceId === request.sourceInstanceId);
|
||||
return { events: source.filter(event => event.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: source.at(-1)?.sourceSeq ?? 0 };
|
||||
|
||||
@@ -63,7 +63,7 @@ it("delivers authenticated runtime activity through the real Runner bridge acros
|
||||
const firstContext = (await helper(["notification-context"])).stdout.trim();
|
||||
for (const runId of ["activity-first", "activity-next"]) {
|
||||
if (runId === "activity-next") {
|
||||
attaching = session.attachRun!({ runId });
|
||||
attaching = Promise.resolve(session.attachRun!({ runId }));
|
||||
await releasing;
|
||||
await vi.waitFor(async () => {
|
||||
expect((await helper(["notification-context"])).stdout.trim()).not.toBe(firstContext);
|
||||
@@ -93,7 +93,7 @@ it("delivers authenticated runtime activity through the real Runner bridge acros
|
||||
} finally {
|
||||
releasePrevious();
|
||||
await attaching?.catch(() => undefined);
|
||||
await session?.close();
|
||||
await session?.close({ reason: "test complete" });
|
||||
await bundle.transport.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ it.each(["omitted", "null", "resolved"] as const)("retains a server-owned worksp
|
||||
expect(terminals).toHaveLength(1);
|
||||
expect(terminals[0].envelope.payload).toMatchObject({ runId, eventType: "run.terminal", payload: { runTerminalState: "succeeded", reportedWorkDisposition: "yielded" } });
|
||||
} finally {
|
||||
await session?.close();
|
||||
await session?.close({ reason: "test complete" });
|
||||
await bundle.transport.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ async function sourceFixture(registration?: CapabilityRunnerdCodexTransportOptio
|
||||
runId: identity.runId, executionWorkspaceId: "workspace" },
|
||||
task: { identifier: "TASK-1", title: "Complete fake work", description: null, prompt: "Complete fake work", workMode: "standard" },
|
||||
workspace: { cwd: root, repoUrl: null, repoRef: null, branchName: null },
|
||||
session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1 },
|
||||
session: { normalizedSessionId: identity.normalizedSessionId, driverKind: "codex_app_server", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
provider: { kind: "codex", model: null, approvalPolicy: "never" },
|
||||
completionContract: { id: "contract", sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.completion-contract.v1",
|
||||
contract: { revision: "1", objective: "Complete fake work", criteria: [{ id: "objective", requirement: "Complete fake work" }] } }, interactionResponses: [], credentialBindings: [] };
|
||||
@@ -231,7 +231,7 @@ it.each([["launch", "default"], ["adopt", "default"], ["adopt-denied", "default"
|
||||
expect((await read(fixture.coreFile)).preparedLaunchConfigurations).toEqual(before.preparedLaunchConfigurations);
|
||||
await expect(readFile(join(fixture.root, "launch-preparation.json"))).rejects.toHaveProperty("code", "ENOENT");
|
||||
if (mode !== "launch") {
|
||||
const evidence = resumed.evidence(), startedAt = resumed.transport.processInfo().startedAt;
|
||||
const evidence = resumed.evidence(), startedAt = resumed.transport.processInfo!().startedAt;
|
||||
await resumed.detachControllerForRestart();
|
||||
prepare.mockClear();
|
||||
if (mode === "adopt-denied") prepare.mockRejectedValueOnce(new Error("source preparation authorization revoked"));
|
||||
|
||||
@@ -205,7 +205,7 @@ it.each([["connect", "owned"], ["listen", "owned"], ["external", "owned"], ["con
|
||||
try {
|
||||
const opened = await within(first.transport.request("thread/start", { cwd: root, dynamicTools: [] }), "source open");
|
||||
runnerPid = first.evidence().runnerPid; providerPid = first.evidence().providerPid;
|
||||
const startedAt = first.transport.processInfo().startedAt;
|
||||
const startedAt = first.transport.processInfo!().startedAt;
|
||||
const providerStart = execFileSync("ps", ["-p", String(providerPid), "-o", "lstart="], { encoding: "utf8" }).trim();
|
||||
expect(runnerPid).toBeGreaterThan(0); expect(providerPid).toBeGreaterThan(0);
|
||||
const attach = first.transport.attachRun!({ runId: target.runId, turnId: target.turnId, itemId: target.itemId });
|
||||
|
||||
@@ -9,6 +9,9 @@ vi.mock("node:fs", async (importOriginal) => {
|
||||
return { ...fs, readSync: vi.fn(fs.readSync) };
|
||||
});
|
||||
|
||||
type PositionalRead = [fd: number, buffer: NodeJS.ArrayBufferView, offset: number, length: number, position: number | null];
|
||||
const readPosition = (call: number) => (vi.mocked(readSync).mock.calls[call] as unknown as PositionalRead)[4];
|
||||
|
||||
const interpretation = (frameId: number, ...emittedEventIds: string[]) =>
|
||||
`${JSON.stringify({ kind: "interpretation", frameId, emittedEventIds })}\n`;
|
||||
const settled = `${JSON.stringify({ kind: "trace_status", debugChannel: "rust_native", status: "complete" })}\n`;
|
||||
@@ -41,7 +44,7 @@ describe("runnerd trace frame index", () => {
|
||||
expect(index.locate(path, "pending-1")).toEqual({ frameId: 2, nativeChannelSettled: true });
|
||||
expect(index.locate(path, "missing")).toEqual({ frameId: null, nativeChannelSettled: true });
|
||||
expect(readSync).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(readSync).mock.calls[1]![4]).toBe(Buffer.byteLength(first));
|
||||
expect(readPosition(1)).toBe(Buffer.byteLength(first));
|
||||
});
|
||||
|
||||
it("retains partial lines and split UTF-8 until the append is complete", () => {
|
||||
@@ -87,7 +90,7 @@ describe("runnerd trace frame index", () => {
|
||||
expect(index.locate(path, "last").frameId).toBeNull();
|
||||
expect(index.locate(path, "last").frameId).toBeNull();
|
||||
expect(index.locate(path, "last").frameId).toBe(4);
|
||||
for (const call of vi.mocked(readSync).mock.calls) expect(call[3]).toBeLessThanOrEqual(1024 * 1024);
|
||||
for (const call of vi.mocked(readSync).mock.calls as unknown as PositionalRead[]) expect(call[3]).toBeLessThanOrEqual(1024 * 1024);
|
||||
const calls = vi.mocked(readSync).mock.calls.length;
|
||||
for (let i = 0; i < 1000; i++) index.locate(path, "missing");
|
||||
expect(readSync).toHaveBeenCalledTimes(calls);
|
||||
@@ -124,8 +127,8 @@ describe("runnerd trace frame index", () => {
|
||||
}
|
||||
expect(readSync).toHaveBeenCalledTimes(72);
|
||||
for (let i = 0; i < 24; i++) {
|
||||
expect(vi.mocked(readSync).mock.calls[24 + i]![4]).toBe(1024 * 1024);
|
||||
expect(vi.mocked(readSync).mock.calls[48 + i]![4]).toBe(2 * 1024 * 1024);
|
||||
expect(readPosition(24 + i)).toBe(1024 * 1024);
|
||||
expect(readPosition(48 + i)).toBe(2 * 1024 * 1024);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ it("starts one provider after delayed bootstrap admission and retains it for a s
|
||||
await bundle.transport.request("turn/start", { input: [{ type: "text", text }] });
|
||||
for await (const event of bundle.transport.notifications()) {
|
||||
if (event.method === "turn/completed") {
|
||||
expect(event.params).toMatchObject({ threadId: opened.thread.id });
|
||||
expect(event.params).toMatchObject({ threadId: (opened.thread as { id: string }).id });
|
||||
terminalTurns.push(event.params);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("Scenario chat deployment boundary", () => {
|
||||
it("requires exact Origin and Fetch Metadata on every mutation", async () => {
|
||||
await withHttpRuntime(async ({ request }) => {
|
||||
const body = JSON.stringify({ scenarioId: SCENARIO_ID });
|
||||
const invalidHeaders = [
|
||||
const invalidHeaders: Record<string, string>[] = [
|
||||
{ origin: `${PUBLIC_ORIGIN}/` },
|
||||
{ "sec-fetch-site": "none" },
|
||||
{ "sec-fetch-site": "cross-site" },
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type CapabilityLiveTransportFactory,
|
||||
type CapabilityLiveTurnEvent,
|
||||
} from "./live-session.js";
|
||||
import type { CapabilityRunnerdProcessEvidence } from "./runnerd-codex-transport.js";
|
||||
import {
|
||||
createCapabilityTurnStreamDecoder,
|
||||
encodeCapabilityTurnStreamFrame,
|
||||
@@ -161,10 +162,25 @@ class GatedTransport implements CodexAppServerTransport {
|
||||
|
||||
function gatedTransportFactory(gate: Gate): CapabilityLiveTransportFactory {
|
||||
return (options) => {
|
||||
const evidence = {
|
||||
const evidence: CapabilityRunnerdProcessEvidence = {
|
||||
runnerPid: 7100,
|
||||
runnerProcessGroupId: 7100,
|
||||
providerPid: null,
|
||||
providerProcessStartedAt: null,
|
||||
codexPid: 7200,
|
||||
codexProcessStartedAt: null,
|
||||
sidecarPid: null,
|
||||
sidecarProcessStartedAt: null,
|
||||
agentPid: null,
|
||||
agentProcessStartedAt: null,
|
||||
providerDriver: null,
|
||||
providerVersion: null,
|
||||
acpxAgent: null,
|
||||
agentServerVersion: null,
|
||||
agentRuntimeVersion: null,
|
||||
acpProtocolVersion: null,
|
||||
providerExecutionKind: null,
|
||||
providerService: null,
|
||||
runnerExited: false,
|
||||
runnerExitCode: null,
|
||||
runnerSignal: null,
|
||||
@@ -172,7 +188,7 @@ function gatedTransportFactory(gate: Gate): CapabilityLiveTransportFactory {
|
||||
diagnostics: [],
|
||||
};
|
||||
options.onEvidence?.(evidence);
|
||||
return { transport: new GatedTransport(gate), evidence: () => ({ ...evidence }) };
|
||||
return { transport: new GatedTransport(gate), evidence: () => ({ ...evidence }), detachControllerForRestart: async () => {} };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -584,6 +584,7 @@ describe("CapabilityMockControlPlaneAdapter", () => {
|
||||
commandKind: "record_budget_usage",
|
||||
effect: "lost_ack",
|
||||
remaining: 1,
|
||||
code: "budget_ack_lost",
|
||||
}],
|
||||
});
|
||||
await adapter.start();
|
||||
|
||||
@@ -333,7 +333,7 @@ class StubDriver implements HarnessDriver {
|
||||
|
||||
const servers: LiveConsoleDemoServer[] = [];
|
||||
|
||||
function browserHeaders(url: string, headers: HeadersInit = {}): Headers {
|
||||
function browserHeaders(url: string, headers: ConstructorParameters<typeof Headers>[0] = {}): Headers {
|
||||
const result = new Headers(headers);
|
||||
result.set("origin", new URL(url).origin);
|
||||
result.set("sec-fetch-site", "same-origin");
|
||||
|
||||
@@ -82,6 +82,7 @@ function reduce(events: readonly PrpEvent[]): SessionSnapshot {
|
||||
environmentLeaseId: "package-local",
|
||||
runnerInstanceId: "live-console-demo-server",
|
||||
normalizedSessionId: events[0]?.normalizedSessionId ?? "session",
|
||||
driverSessionId: "live-console-demo",
|
||||
},
|
||||
capabilities: {
|
||||
schema: "paperclip.prp.capabilities.v1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { LocalRunnerRunMetadata, LocalRunnerRunTrace } from "../contracts/local-runner.js";
|
||||
import {
|
||||
applyLocalRunnerLiveEvent,
|
||||
createLocalRunnerLiveSnapshot,
|
||||
@@ -13,6 +14,16 @@ import {
|
||||
startLocalRunnerScenario,
|
||||
} from "./local-runner.js";
|
||||
|
||||
function traceMetadata(trace: LocalRunnerRunTrace): LocalRunnerRunMetadata {
|
||||
return {
|
||||
schema: "paperclip.runner.local-runner.metadata.v1",
|
||||
scenario: trace.scenario,
|
||||
fixtureName: trace.fixtureName,
|
||||
identity: trace.identity,
|
||||
capabilities: trace.capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Local runner and fake harness", () => {
|
||||
it("runs lifecycle, tool, file, structured-result, and process-exit events", async () => {
|
||||
const trace = await runLocalRunnerScenario({ scenario: "happy-path", delayMs: 1 });
|
||||
@@ -108,7 +119,7 @@ describe("Local runner and fake harness", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
replayLocalRunnerEvents(trace, trace.events).timeline.find(
|
||||
replayLocalRunnerEvents(traceMetadata(trace), trace.events).timeline.find(
|
||||
(event) => event.eventType === "harness.diagnostic",
|
||||
)?.summary,
|
||||
).toBe(
|
||||
@@ -160,7 +171,7 @@ describe("Local runner and fake harness", () => {
|
||||
|
||||
it("produces the same snapshot for validated live events and replay", async () => {
|
||||
const trace = await runLocalRunnerScenario({ scenario: "happy-path", delayMs: 1 });
|
||||
let live = createLocalRunnerLiveSnapshot(trace);
|
||||
let live = createLocalRunnerLiveSnapshot(traceMetadata(trace));
|
||||
for (const event of trace.events) {
|
||||
const next = applyLocalRunnerLiveEvent(live, event);
|
||||
expect(next.ok).toBe(true);
|
||||
@@ -168,7 +179,7 @@ describe("Local runner and fake harness", () => {
|
||||
live = next.snapshot;
|
||||
}
|
||||
}
|
||||
const replay = replayLocalRunnerEvents(trace, trace.events);
|
||||
const replay = replayLocalRunnerEvents(traceMetadata(trace), trace.events);
|
||||
|
||||
expect(localRunnerSnapshotsMatch(live, replay)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
PAPERCLIP_EXECUTION_PROMPT_REVISION,
|
||||
canonicalNativeRuntimeContextDigest,
|
||||
nativeRuntimePromptDigest,
|
||||
type NativeRuntimeContextSnapshot,
|
||||
} from "./contracts/runtime-context.js";
|
||||
import {
|
||||
executeNativeSession,
|
||||
@@ -129,6 +130,7 @@ const input: NativeExecutionInputV1 = {
|
||||
normalizedSessionId: identity.sessionId,
|
||||
driverKind: "codex_app_server",
|
||||
protocolVersion: 1,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
},
|
||||
provider: { kind: "codex", model: null },
|
||||
completionContract: {
|
||||
@@ -145,13 +147,15 @@ const input: NativeExecutionInputV1 = {
|
||||
credentialBindings: [],
|
||||
};
|
||||
|
||||
type RuntimeContextFields = Omit<NativeRuntimeContextSnapshot, "aggregateDigest">;
|
||||
|
||||
function preparedInput(): NativeExecutionInputV5 {
|
||||
const digest = "0".repeat(64);
|
||||
const context = {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
} as const;
|
||||
} satisfies RuntimeContextFields;
|
||||
const requirement = input.completionContract.contract.criteria[0]!.requirement;
|
||||
const prompt = `${input.task.prompt}\n\n${requirement}`;
|
||||
return {
|
||||
@@ -256,7 +260,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const completeRun = vi.fn(async () => undefined);
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, async checkpointSession() {},
|
||||
async appendEvent(event) { committed.push(structuredClone(event)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { committed.push(structuredClone(event)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(query) { const events = committed.filter(event => event.sourceInstanceId === query.sourceInstanceId); return { events: events.filter(event => event.sourceSeq > query.afterSourceSeq).slice(0, query.limit), highestContiguousSourceSeq: highestContiguous(events) }; },
|
||||
completeRun,
|
||||
};
|
||||
@@ -318,7 +322,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, async checkpointSession() {},
|
||||
async loadSessionCheckpoint() { return recovered ? structuredClone(snapshot) : null; },
|
||||
async appendEvent(event) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
||||
async completeRun() {},
|
||||
};
|
||||
@@ -390,7 +394,7 @@ describe("executeNativeSession recovery", () => {
|
||||
async openRun() {},
|
||||
async loadSessionCheckpoint() { return structuredClone(checkpoint); },
|
||||
async checkpointSession(value) { checkpoints.push(structuredClone(value)); },
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const existing = journal.find(value => value.sourceEventId === event.sourceEventId);
|
||||
if (existing) expect(event).toEqual(existing);
|
||||
else journal.push(structuredClone(event as PrpEvent));
|
||||
@@ -479,7 +483,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession(snapshot) { checkpoints.push(structuredClone(snapshot)); },
|
||||
async appendEvent(event) { events.push(event); return { cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { events.push(event); return { cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
||||
async completeRun() {},
|
||||
};
|
||||
@@ -533,7 +537,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
||||
async completeRun() {},
|
||||
};
|
||||
@@ -555,7 +559,7 @@ describe("executeNativeSession recovery", () => {
|
||||
resume: true, typedEvents: true, steering: false,
|
||||
interruption: true, structuredResult: true,
|
||||
};
|
||||
const cancel = vi.fn(async () => { finish(); });
|
||||
const cancel = vi.fn(() => { finish(); return { cleanup: Promise.resolve() }; });
|
||||
const close = vi.fn(async () => { finish(); });
|
||||
const session: NativeSession = {
|
||||
identity: () => identity,
|
||||
@@ -574,7 +578,7 @@ describe("executeNativeSession recovery", () => {
|
||||
},
|
||||
cancel, close,
|
||||
};
|
||||
const appendEvent = vi.fn<ControlPlanePort["appendEvent"]>(async event => ({
|
||||
const appendEvent = vi.fn<ControlPlanePort["appendEvent"]>(async (event: PrpEvent) => ({
|
||||
cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq,
|
||||
disposition: "committed",
|
||||
}));
|
||||
@@ -728,7 +732,7 @@ describe("executeNativeSession recovery", () => {
|
||||
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
} as const;
|
||||
} satisfies RuntimeContextFields;
|
||||
const completed = await executeNativeSession({
|
||||
input: snapshotBeforeUpdate ? {
|
||||
...input, schema: "paperclip.native-execution-input.v4", executionMode: "default", planningContext: null,
|
||||
@@ -748,7 +752,7 @@ describe("executeNativeSession recovery", () => {
|
||||
controlPlane: {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
appended.push(event);
|
||||
return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" };
|
||||
},
|
||||
@@ -885,7 +889,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
appended.push(event);
|
||||
return {
|
||||
cursor: event.sourceSeq,
|
||||
@@ -1034,7 +1038,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
appended.push(event);
|
||||
return {
|
||||
cursor: event.sourceSeq,
|
||||
@@ -1185,7 +1189,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
return {
|
||||
cursor: event.sourceSeq,
|
||||
highestContiguousSourceSeq: event.sourceSeq,
|
||||
@@ -1241,7 +1245,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" };
|
||||
},
|
||||
async replayEvents() {
|
||||
@@ -1574,7 +1578,7 @@ describe("executeNativeSession recovery", () => {
|
||||
},
|
||||
controlPlane: {
|
||||
async openRun() {}, async checkpointSession() {},
|
||||
async appendEvent(event) { appended.push(event); return {
|
||||
async appendEvent(event: PrpEvent) { appended.push(event); return {
|
||||
cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed",
|
||||
}; },
|
||||
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; },
|
||||
@@ -2568,12 +2572,12 @@ describe("executeNativeSession recovery", () => {
|
||||
},
|
||||
skills: [],
|
||||
mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
} as const;
|
||||
} satisfies RuntimeContextFields;
|
||||
const openSession = vi.fn();
|
||||
const backend: NativeSessionBackend = {
|
||||
async descriptor() {
|
||||
return {
|
||||
kind: "future-provider",
|
||||
kind: "mock",
|
||||
name: "future-provider",
|
||||
version: "1",
|
||||
capabilities: {
|
||||
@@ -2741,6 +2745,7 @@ describe("executeNativeSession recovery", () => {
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: true,
|
||||
structuredResult: true,
|
||||
};
|
||||
},
|
||||
async *events() {},
|
||||
@@ -2854,7 +2859,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const snapshot = vi.fn(async () => { throw snapshotFailure; });
|
||||
const session: NativeSession = {
|
||||
identity: () => identity,
|
||||
async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true }; },
|
||||
async capabilities() { return { resume: false, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
||||
async *events() {},
|
||||
async startTurn() { throw new Error("unexpected turn"); },
|
||||
async result() { return null; },
|
||||
@@ -2886,6 +2891,7 @@ describe("executeNativeSession recovery", () => {
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: true,
|
||||
structuredResult: true,
|
||||
};
|
||||
},
|
||||
async *events() {},
|
||||
@@ -3509,7 +3515,7 @@ describe("executeNativeSession recovery", () => {
|
||||
}
|
||||
},
|
||||
);
|
||||
const checkpointSession = vi.fn<ControlPlanePort["checkpointSession"]>(
|
||||
const checkpointSession = vi.fn<NonNullable<ControlPlanePort["checkpointSession"]>>(
|
||||
async () => undefined,
|
||||
);
|
||||
const port: ControlPlanePort = {
|
||||
@@ -3744,8 +3750,9 @@ describe("executeNativeSession recovery", () => {
|
||||
timeoutMs: 10,
|
||||
keepSessionOpen: true,
|
||||
onSession: (current) => retainedSessions.push(current),
|
||||
onPostCompletionEnrichmentFailure: ({ stage }) =>
|
||||
enrichmentFailures.push(stage),
|
||||
onPostCompletionEnrichmentFailure: ({ stage }) => {
|
||||
enrichmentFailures.push(stage);
|
||||
},
|
||||
});
|
||||
await enrichmentStalled;
|
||||
if (stalledSignal !== undefined)
|
||||
@@ -4650,12 +4657,12 @@ describe("executeNativeSession recovery", () => {
|
||||
async *events() { throw new Error("provider failed after editing instructions"); },
|
||||
startTurn: async () => ({ turnId: "instruction-turn" }),
|
||||
result: async () => null,
|
||||
snapshot: async () => ({ backendKind: "local", sessionId: "instruction-session", identity: scopedIdentity,
|
||||
snapshot: async () => ({ backendKind: "runner", sessionId: "instruction-session", identity: scopedIdentity,
|
||||
providerSessionId: "instruction-provider", cursor: null, activeTurnId: null, pendingRuntimeRequests: [], lineage: [] }),
|
||||
close: async () => { order.push("closing"); await closed; order.push("stopped"); },
|
||||
};
|
||||
const backend: NativeSessionBackend = {
|
||||
descriptor: async () => ({ kind: "local", name: "instruction-close-test", version: "1", capabilities }),
|
||||
descriptor: async () => ({ kind: "runner", name: "instruction-close-test", version: "1", capabilities }),
|
||||
openSession: async () => session,
|
||||
};
|
||||
const controlPlane: ControlPlanePort = {
|
||||
@@ -4747,7 +4754,7 @@ describe("executeNativeSession recovery", () => {
|
||||
close: vi.fn(async () => { throw failure; }),
|
||||
};
|
||||
const backend: NativeSessionBackend = {
|
||||
descriptor: async () => ({ kind: "local", name: "local-stop-test", version: "1", capabilities }),
|
||||
descriptor: async () => ({ kind: "runner", name: "local-stop-test", version: "1", capabilities }),
|
||||
openSession: vi.fn(async () => session),
|
||||
};
|
||||
const controlPlane: ControlPlanePort = {
|
||||
@@ -4982,7 +4989,7 @@ describe("executeNativeSession recovery", () => {
|
||||
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 5_000,
|
||||
controlPlane: {
|
||||
async openRun() {}, async checkpointSession() {}, completeRun,
|
||||
async appendEvent(event, options) {
|
||||
async appendEvent(event: PrpEvent, options) {
|
||||
if (options?.deferPostCommitObservers) deferredEventTypes.push(event.eventType);
|
||||
events.push(structuredClone(event)); return {
|
||||
cursor: events.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed",
|
||||
@@ -5080,7 +5087,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
events.push(structuredClone(event as PrpEvent));
|
||||
return {
|
||||
cursor: events.length,
|
||||
@@ -5199,7 +5206,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
events.push(structuredClone(event as PrpEvent));
|
||||
const sourceEvents = events.filter(
|
||||
(candidate) => candidate.sourceInstanceId === event.sourceInstanceId,
|
||||
@@ -5417,7 +5424,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
if (event.eventType === "item.completed") {
|
||||
persistingAnswer = true;
|
||||
await persistence;
|
||||
@@ -5925,7 +5932,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const openReplacementSession = vi.fn();
|
||||
const backend: NativeSessionBackend = {
|
||||
descriptor: async () => ({ kind: "mock", name: "incompatible", version: "1",
|
||||
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true } }),
|
||||
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true } }),
|
||||
openSession, openReplacementSession,
|
||||
recoverSession: async () => ({ recovered: false, reason: "saved launch profile changed",
|
||||
failureClass: PROVIDER_STATE_INCOMPATIBLE }),
|
||||
@@ -6093,6 +6100,7 @@ describe("executeNativeSession recovery", () => {
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: true,
|
||||
structuredResult: true,
|
||||
};
|
||||
},
|
||||
async *events() {},
|
||||
@@ -6136,6 +6144,7 @@ describe("executeNativeSession recovery", () => {
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: true,
|
||||
structuredResult: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -6224,6 +6233,7 @@ describe("executeNativeSession recovery", () => {
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: true,
|
||||
structuredResult: true,
|
||||
};
|
||||
},
|
||||
async *events() {},
|
||||
@@ -6268,6 +6278,7 @@ describe("executeNativeSession recovery", () => {
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: true,
|
||||
structuredResult: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -6375,7 +6386,7 @@ describe("executeNativeSession recovery", () => {
|
||||
},
|
||||
async replayEvents(replay) {
|
||||
return {
|
||||
events: replay.afterSourceSeq === 0 ? [runnerEvent(1)] : [],
|
||||
events: replay.afterSourceSeq === 0 ? [runnerEvent(1, "turn.started")] : [],
|
||||
highestContiguousSourceSeq: 1,
|
||||
};
|
||||
},
|
||||
@@ -6479,7 +6490,7 @@ describe("executeNativeSession recovery", () => {
|
||||
},
|
||||
async replayEvents(replay) {
|
||||
return {
|
||||
events: replay.afterSourceSeq === 0 ? [runnerEvent(1)] : [],
|
||||
events: replay.afterSourceSeq === 0 ? [runnerEvent(1, "turn.started")] : [],
|
||||
highestContiguousSourceSeq: 1,
|
||||
};
|
||||
},
|
||||
@@ -6530,7 +6541,7 @@ describe("executeNativeSession recovery", () => {
|
||||
sourceEventId: "provider-recovery:1",
|
||||
sourceSeq: 1,
|
||||
sourceInstanceId: "provider-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
runId: identity.runId,
|
||||
normalizedSessionId: identity.sessionId,
|
||||
turnId: "turn-recovery",
|
||||
@@ -6594,7 +6605,7 @@ describe("executeNativeSession recovery", () => {
|
||||
return structuredClone(checkpoint);
|
||||
},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const list = bySource.get(event.sourceInstanceId) ?? [];
|
||||
list.push(structuredClone(event));
|
||||
bySource.set(event.sourceInstanceId, list);
|
||||
@@ -6718,7 +6729,7 @@ describe("executeNativeSession recovery", () => {
|
||||
async checkpointSession(snapshot) {
|
||||
checkpoints.push(structuredClone(snapshot));
|
||||
},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const target =
|
||||
event.sourceInstanceId === "runner-recovery"
|
||||
? runnerEvents
|
||||
@@ -6852,7 +6863,7 @@ describe("executeNativeSession recovery", () => {
|
||||
return structuredClone(checkpoint);
|
||||
},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
events.push(structuredClone(event));
|
||||
return {
|
||||
cursor: events.length,
|
||||
@@ -6989,8 +7000,8 @@ describe("executeNativeSession recovery", () => {
|
||||
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest, manifestDigest: digest, rootPath: "/runtime/instructions", fileCount: 1, totalBytes: 1 } },
|
||||
skills: [skill],
|
||||
mcp: { assignmentSetId: "none", digest, bindingId: null },
|
||||
} as const;
|
||||
const legacyContext = { ...context, skills: [] as const };
|
||||
} satisfies RuntimeContextFields;
|
||||
const legacyContext = { ...context, skills: [] } satisfies RuntimeContextFields;
|
||||
const prepared = preparedInput();
|
||||
const executionInput = preparedMode
|
||||
? {
|
||||
@@ -7049,7 +7060,7 @@ describe("executeNativeSession recovery", () => {
|
||||
terminal: null,
|
||||
terminalTurns: [],
|
||||
};
|
||||
const startTurn = vi.fn(async () => ({ turnId: "turn-replacement" }));
|
||||
const startTurn = vi.fn<NativeSession["startTurn"]>(async () => ({ turnId: "turn-replacement" }));
|
||||
const replacementSession: NativeSession = {
|
||||
identity: () => identity,
|
||||
async capabilities() {
|
||||
@@ -7108,7 +7119,7 @@ describe("executeNativeSession recovery", () => {
|
||||
return structuredClone(checkpoint);
|
||||
},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
events.push(structuredClone(event));
|
||||
return {
|
||||
cursor: events.length,
|
||||
@@ -7194,7 +7205,7 @@ describe("executeNativeSession recovery", () => {
|
||||
sourceEventId: "provider-recovery:2",
|
||||
sourceSeq: 2,
|
||||
sourceInstanceId: "provider-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
runId: identity.runId,
|
||||
normalizedSessionId: identity.sessionId,
|
||||
turnId: "turn-continuation",
|
||||
@@ -7204,7 +7215,7 @@ describe("executeNativeSession recovery", () => {
|
||||
emittedAt: "2026-08-09T00:00:01.000Z",
|
||||
payload: {},
|
||||
};
|
||||
const startTurn = vi.fn(async () => ({ turnId: "turn-continuation" }));
|
||||
const startTurn = vi.fn<NativeSession["startTurn"]>(async () => ({ turnId: "turn-continuation" }));
|
||||
const session: NativeSession = {
|
||||
identity: () => identity,
|
||||
async capabilities() {
|
||||
@@ -7273,7 +7284,7 @@ describe("executeNativeSession recovery", () => {
|
||||
return structuredClone(checkpoint);
|
||||
},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const list = bySource.get(event.sourceInstanceId) ?? [];
|
||||
list.push(structuredClone(event));
|
||||
bySource.set(event.sourceInstanceId, list);
|
||||
@@ -7490,7 +7501,7 @@ describe("executeNativeSession recovery", () => {
|
||||
sourceEventId: "provider-recovery:2",
|
||||
sourceSeq: 2,
|
||||
sourceInstanceId: "provider-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
runId: identity.runId,
|
||||
normalizedSessionId: identity.sessionId,
|
||||
turnId: "turn-disposition",
|
||||
@@ -7571,7 +7582,7 @@ describe("executeNativeSession recovery", () => {
|
||||
prematureDispositionCheckpoint = true;
|
||||
durableCheckpoint = structuredClone(snapshot);
|
||||
},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
if (event.eventType === terminalType && failNextAppend) {
|
||||
failNextAppend = false;
|
||||
throw appendFailure;
|
||||
@@ -7674,7 +7685,7 @@ describe("executeNativeSession recovery", () => {
|
||||
sourceEventId: "runner-recovery:run-native:4",
|
||||
sourceSeq: 4,
|
||||
sourceInstanceId: "runner-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
runId: identity.runId,
|
||||
normalizedSessionId: identity.sessionId,
|
||||
turnId: "turn-disposition",
|
||||
@@ -7784,7 +7795,7 @@ describe("executeNativeSession recovery", () => {
|
||||
return structuredClone(checkpoint);
|
||||
},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const list = bySource.get(event.sourceInstanceId) ?? [];
|
||||
list.push(structuredClone(event));
|
||||
bySource.set(event.sourceInstanceId, list);
|
||||
@@ -7935,7 +7946,7 @@ describe("executeNativeSession recovery", () => {
|
||||
return structuredClone(checkpoint);
|
||||
},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const list = bySource.get(event.sourceInstanceId) ?? [];
|
||||
list.push(structuredClone(event));
|
||||
bySource.set(event.sourceInstanceId, list);
|
||||
@@ -8085,7 +8096,7 @@ describe("executeNativeSession recovery", () => {
|
||||
pendingRuntimeRequests: [],
|
||||
lineage: [],
|
||||
};
|
||||
const events = [
|
||||
const events: PrpEvent[] = [
|
||||
{
|
||||
...controlEvent(1, "run.result.accepted", { result }),
|
||||
turnId: "turn-with-result",
|
||||
@@ -8223,7 +8234,7 @@ describe("executeNativeSession recovery", () => {
|
||||
backend: { async descriptor() { return { kind: "mock", name: "missing-current-completion", version: "1", capabilities }; }, async openSession() { return session; } },
|
||||
controlPlane: {
|
||||
async openRun() {}, async checkpointSession() {},
|
||||
async appendEvent(event) { events.push(structuredClone(event as PrpEvent)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { events.push(structuredClone(event as PrpEvent)); return { cursor: event.sourceSeq, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents() { return { events: [], highestContiguousSourceSeq: 0 }; }, completeRun,
|
||||
}, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 1000,
|
||||
resolveMissingResult: async () => null,
|
||||
@@ -8239,7 +8250,7 @@ describe("executeNativeSession recovery", () => {
|
||||
sourceEventId: "provider-recovery:1",
|
||||
sourceSeq: 1,
|
||||
sourceInstanceId: "provider-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
runId: identity.runId,
|
||||
normalizedSessionId: identity.sessionId,
|
||||
turnId: "turn-waiting",
|
||||
@@ -8420,7 +8431,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, completeRun: complete,
|
||||
async checkpointSession(value) { checkpoint = structuredClone(value); },
|
||||
async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(request) { return { events: history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: 0 }; },
|
||||
};
|
||||
const outcome = expect(executeNativeSession({ input, backend, controlPlane: port, timeoutMs: 1_000,
|
||||
@@ -8475,7 +8486,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, completeRun: complete,
|
||||
async checkpointSession(value) { checkpoint = structuredClone(value); },
|
||||
async appendEvent(event) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async appendEvent(event: PrpEvent) { history.push(event); return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" }; },
|
||||
async replayEvents(request) { return { events: history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq), highestContiguousSourceSeq: 0 }; },
|
||||
};
|
||||
const execution = executeNativeSession({ input, backend, controlPlane: port, timeoutMs: operationMs ?? 120_000,
|
||||
@@ -8523,7 +8534,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, completeRun: complete,
|
||||
async checkpointSession(value) { checkpoint = structuredClone(value); },
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
const duplicate = history.some(row => row.sourceEventId === event.sourceEventId);
|
||||
if (!duplicate) history.push(event);
|
||||
return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: duplicate ? "duplicate" : "committed" };
|
||||
@@ -8585,7 +8596,7 @@ describe("executeNativeSession recovery", () => {
|
||||
throw new Error("controller lost after governed checkpoint");
|
||||
}
|
||||
},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
if (!history.some(row => row.sourceEventId === event.sourceEventId)) history.push(event);
|
||||
return { cursor: history.length, highestContiguousSourceSeq: event.sourceSeq, disposition: "committed" };
|
||||
},
|
||||
@@ -8673,14 +8684,14 @@ describe("executeNativeSession recovery", () => {
|
||||
}),
|
||||
sourceEventId: "provider-recovery:1",
|
||||
sourceInstanceId: "provider-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
turnId: "turn-waiting",
|
||||
};
|
||||
const turnInterrupted: PrpEvent = {
|
||||
...controlEvent(3, "turn.interrupted", { reason: "governed_wait" }),
|
||||
sourceEventId: "provider-recovery:3",
|
||||
sourceInstanceId: "provider-recovery",
|
||||
sourceKind: "provider",
|
||||
sourceKind: "runner",
|
||||
turnId: "turn-waiting",
|
||||
};
|
||||
let releaseCancelled!: () => void;
|
||||
@@ -8945,7 +8956,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
events.push(structuredClone(event as PrpEvent));
|
||||
if (event.eventType === "runtime_request.created") releaseCreated();
|
||||
const sourceEvents = events.filter(
|
||||
@@ -9491,7 +9502,7 @@ describe("executeNativeSession recovery", () => {
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {},
|
||||
async checkpointSession() {},
|
||||
async appendEvent(event) {
|
||||
async appendEvent(event: PrpEvent) {
|
||||
if (event.eventType === "runtime_request.resolved") {
|
||||
markSettlementAppendStarted();
|
||||
await settlementAppendReleased;
|
||||
@@ -9521,11 +9532,11 @@ describe("executeNativeSession recovery", () => {
|
||||
};
|
||||
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
let queuedHandoffCallback: (() => void) | null = null;
|
||||
let queuedHandoffCallback = null as (() => void) | null;
|
||||
const timeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation(((
|
||||
callback,
|
||||
delay,
|
||||
...args
|
||||
callback: (...args: unknown[]) => void,
|
||||
delay?: number,
|
||||
...args: unknown[]
|
||||
) => {
|
||||
if (delay === 123_456) {
|
||||
queuedHandoffCallback = () => callback(...args);
|
||||
@@ -9576,7 +9587,7 @@ it("fails preconfigured recovery without replacing a missing provider session",
|
||||
const backend: NativeSessionBackend = {
|
||||
descriptor: async () => ({ kind: "mock", name: "preconfigured", version: "1",
|
||||
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
||||
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true } }),
|
||||
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true } }),
|
||||
openSession, openReplacementSession,
|
||||
recoverSession: async () => ({ recovered: false, reason: "provider history is missing" }),
|
||||
};
|
||||
@@ -9586,7 +9597,7 @@ it("fails preconfigured recovery without replacing a missing provider session",
|
||||
};
|
||||
await expect(executeNativeSession({
|
||||
input: { ...preparedInput(), schema: "paperclip.native-execution-input.v7", homeMode: "preconfigured" },
|
||||
backend, controlPlane: port, runnerInstanceId: "runner-preconfigured", persistedSession: checkpoint,
|
||||
backend, controlPlane: port, runnerInstanceId: "runner-preconfigured", controlPlaneInstanceId: "control-preconfigured", persistedSession: checkpoint,
|
||||
})).rejects.toThrow("native_session_recovery_failed: provider history is missing");
|
||||
expect(openSession).not.toHaveBeenCalled();
|
||||
expect(openReplacementSession).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { paperclipSemanticAction } from "../catalog/semantic-action-catalog.js";
|
||||
import { paperclipSemanticActionNamespace } from "../semantic-tools/paperclip-discovery.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PAPERCLIP_COLLABORATION_PROTOCOL_ACTIONS } from "./collaboration.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PAPERCLIP_CORE_PROTOCOL_ACTIONS } from "./core.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { questionSetSchema, requestSchema } from "./generated/schema-bundle.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { providerDescriptorSchema } from "./generated/schema-bundle.js";
|
||||
@@ -19,7 +19,7 @@ describe("provider runtime descriptor", () => {
|
||||
}
|
||||
const event: PrpEvent = {
|
||||
schema: "paperclip.prp.event.v2", schemaVersion: 2, sourceEventId: "controls", sourceSeq: 1,
|
||||
sourceInstanceId: "runner", sourceKind: "runner", runId: "run", eventType: "session.capabilities.updated",
|
||||
sourceInstanceId: "runner", sourceKind: "runner", runId: "run", normalizedSessionId: "session", eventType: "session.capabilities.updated",
|
||||
priority: 0, emittedAt: "2026-09-28T12:00:00.000Z", payload: {
|
||||
sessionGoals: { availability: "unsupported", reason: "provider has no native goals", actions: [],
|
||||
autonomousUpdates: false, persistentAcrossResume: false, maxObjectiveChars: 4000,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import Ajv2020 from "ajv/dist/2020";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type Classification =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { usageSchema } from "./generated/schema-bundle.js";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("PRP v1 JSON Schema contract", () => {
|
||||
expect(result.fixture.futureFixtureHint).toEqual({
|
||||
producerVersion: "1.1-preview",
|
||||
});
|
||||
expect(result.fixture.events[0]?.futureEnvelopeField).toBe(42);
|
||||
expect((result.fixture.events[0] as unknown as Record<string, unknown>).futureEnvelopeField).toBe(42);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -269,7 +269,7 @@ describe("PRP v1 JSON Schema contract", () => {
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(
|
||||
result.fixture.events[0]?.payload.semantic_tool?.futureEnvelopeHint,
|
||||
(result.fixture.events[0]?.payload.semantic_tool as { futureEnvelopeHint?: unknown } | undefined)?.futureEnvelopeHint,
|
||||
).toEqual({ version: "1.1-preview" });
|
||||
|
||||
const withoutEnvelope = structuredClone(result.fixture);
|
||||
|
||||
@@ -10,6 +10,7 @@ function workspaceEvent(eventType: "workspace.change.updated" | "workspace.diff.
|
||||
sourceInstanceId: "runner-1",
|
||||
sourceKind: "runner",
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
eventType,
|
||||
schemaVersion: 1,
|
||||
@@ -44,7 +45,7 @@ describe("PRP workspace change events", () => {
|
||||
|
||||
it("rejects traversal paths and incomplete recorded diffs", () => {
|
||||
const traversal = workspaceEvent("workspace.diff.recorded");
|
||||
traversal.payload.files[0]!.path = "../secret";
|
||||
(traversal.payload.files as Array<{ path: string }>)[0]!.path = "../secret";
|
||||
expect(validatePrpEvent(traversal).ok).toBe(false);
|
||||
const incomplete = workspaceEvent("workspace.diff.recorded");
|
||||
incomplete.payload.complete = false;
|
||||
@@ -59,6 +60,7 @@ describe("PRP workspace change events", () => {
|
||||
sourceInstanceId: "runner-1",
|
||||
sourceKind: "runner",
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
eventType: "workspace.file.referenced",
|
||||
schemaVersion: 1,
|
||||
|
||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user