mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
fix(build): typecheck package, Playwright, and e2e test files in CI (#18839)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Runner, plugin, and Playwright tests verify how those agents run, connect, and appear in the board > - Most package tsconfigs exclude tests, and Vitest strips types, so a type error in those tests fails nothing > - The tests drifted to 803 real type errors, and some of those errors hid assertions that checked nothing > - paperclipai/paperclip#18739 and paperclipai/paperclip#18738 check the server test tree and four test projects, but not these files > - This pull request adds a required check for these test files, fixes every error, and fails CI if a test file is in no program > - The benefit is that a type error in any test file outside `server/src` now fails CI ## Linked Issues or Issue Description Refs #18739 Refs #18738 **What happened?** 517 test files were in no program that required CI checks. A type error in those files did not fail CI. **Expected behavior** CI fails on a type error in any test file. **Steps to reproduce** Add `export const value: number = "text";` to a test file under `packages/paperclip-runner/src` on master. CI stays green, because `tsconfig.json` excludes test files and Vitest strips types. **Measured gap** Each row is a program with its tests included, on one thread. File and error counts are for head `7d4b5e907a`. Memory peaks were measured before the two latest merges of master. | Group | Test files | Real errors | Peak | | --- | ---: | ---: | ---: | | `paperclip-runner` Node source | 290 | 644 | 1.09 GiB | | `paperclip-runner` surfaces | 9 | 12 | 0.42 GiB | | `paperclip-runner` devtools and examples | 14 | 0 | 0.68 GiB | | `sandbox-providers/kubernetes` | 21 | 65 | 0.44 GiB | | 8 other sandbox providers and `bridge-template` | 32 | 4 | 0.34 GiB | | `plugins/sdk` | 22 | 15 | 0.41 GiB | | `plugin-workspace-diff` | 5 | 0 | 0.37 GiB | | `plugin-orchestration-smoke-example` | 1 | 1 | 0.15 GiB | | `kv-demo-mcp-server` | 3 | 0 | 0.13 GiB | | `packages/db` scripts | 1 | 0 | 1.01 GiB | | 8 Playwright suites under `tests/` | 52 | 25 | 1.72 GiB | | `tests/runner-acceptance` | 3 | 0 | 0.58 GiB | | `tests/e2e`, through layer declarations | 63 | 37 | at most 1.59 GiB | | `ui` `AttentionInteractionResolver.test.tsx` | 1 | 0 | n/a | | **Total** | **517** | **803** | | 27 of the 803 errors came from master while this pull request was open. Each merge of master brought new test code that no check had read. Errors that were config errors, not real errors: - One TS2688 for each provider, because the root install does not install provider dependencies. - 9 in the Runner Node program (TS7016 and TS6059). - 2 in `tsconfig.browser.json`. TypeScript 7 removed `baseUrl`, so that config was invalid. - 213 in the Playwright suites (`@/*` aliases and Vite types). - About 60 in `tests/e2e` (import extensions and aliases). `packages/plugins/create-paperclip-plugin` has no gap. Its tsconfig excludes its test, but `cli/tsconfig.json` reaches that test through an import. `server/test-baselines` also has no gap, because `tests/lifecycle-baseline` reads it. ## What Changed - Added `scripts/typecheck-test-programs.mjs` and its tests. The `Typecheck + Release Registry` job runs it. It checks each program in `PROGRAMS` on one thread. - The script fails if a tracked `*.test.ts(x)` or `*.spec.ts(x)` file outside `server/src` is in no program. The compiler file list decides this, because `include` drops `foo.test.tsx` when `foo.test.ts` exists. - Added a `tsconfig.test.json` to each uncovered package, and `tests/tsconfig.playwright.json`. - Added `tests/e2e` to `PROJECTS` in `scripts/typecheck-test-projects.mjs`, with a `tsconfig.json` and a `tsconfig.layers.json`. - Added `scripts/typecheck-sandbox-providers`. It is a small workspace that installs provider SDKs from one lockfile. I did not add a lockfile inside each provider, because `scripts/build-standalone-public-packages.mjs` uses a frozen install when one exists, and that changes what a release resolves. - Corrected `packages/paperclip-runner/tsconfig.browser.json` for TypeScript 7. - Set `rootDir` to `..` in `packages/paperclip-runner/tsconfig.test.json`, because one Runner test imports from `packages/adapter-utils/src`. The program does not emit files, so this changes no build output. - Moved the Kubernetes provider from `vitest ^3.2.4` to `^4.1.8`, the version the other providers use. Vitest 3 requires `tinypool ^1.1.1`, which has two critical advisories (GHSA-85c8-ppgw-ccpr and GHSA-5gmw-xhrv-c9v3). The first patched version is 2.1.2. Its tests have the same result on both versions: 189 passed, and the same 12 `file-sync` tests failed. - Moved the Kubernetes provider from `zod ^3.24.2` to `^4.6.6`, and gave its two `z.record` calls a key schema. This is the only production change, and it stays in this pull request. See "Production change for review". - Renamed `AttentionInteractionResolver.test.ts` to `AttentionInteractionResolver.replace.test.ts`, so TypeScript reads the `.test.tsx` beside it. - Fixed all 803 errors, with changes to test files and test support only. The fix for `contracts/native-execution.test.ts` is the same as the fix in paperclipai/paperclip#18739, so those two changes merge cleanly. - Updated `doc/DEVELOPING.md`. This pull request does not edit `server/scripts/typecheck-layers.mjs`. paperclipai/paperclip#18739 and paperclipai/paperclip#18738 have both merged, and this branch includes them. ### Production change for review The new lockfile made dependency review flag `zod@3.25.76` as a high-severity advisory (GHSA-mrhx-c7xc-m3w5). The first patched version is 4.6.6, so 3.x has no fix. The published Kubernetes plugin uses zod to validate operator config. The zod 3 source does not compile against zod 4, so a typecheck-only override cannot fix this. The existing tests do not prove this change, because they also passed with the invalid one-argument `z.record`. So I parsed 45 inputs with the same schema on zod 3.25.76 and zod 4.6.7: | Result | Inputs | Detail | | --- | ---: | --- | | Identical | 23 | Includes defaults, valid config, and a `__proto__` key | | Same outcome, new error wording | 21 | For example, `Invalid url` is now `Invalid URL`. `validateConfig` shows this text to operators. | | Different outcome | 1 | `jobTtlSecondsAfterFinished: 2 ** 60` was valid and is now rejected. The new limit is `Number.MAX_SAFE_INTEGER`. | I did not test this against a real cluster. The decision is to keep this change in this pull request. Since then, paperclipai/paperclip#17364 has moved the rest of the repository to zod 4.6.x, so the Kubernetes plugin now matches it. ### Behavior change for review `reducedMotion` under `use` is not a Playwright option. It had no effect in 11 configs and 2 `test.use` calls, so those suites ran with `no-preference`. The options are now under `contextOptions`, so reduced motion now takes effect. `app-shell-design.spec.ts` asserts a real transition, so it now asks for `no-preference` itself. The baseline manifest has `snapshotCount: 0`, so this invalidates no published baseline. A frozen trial reference PNG can differ. ### Findings These are product questions. I did not change them, and I did not hide them with casts: 1. **`PrpEvent` and its schema disagree.** The TypeScript type in `src/protocol/replay-contract.ts` requires `normalizedSessionId`. The `required` list in `protocol/schemas/event.schema.json` does not include it. `validatePrpEvent` accepts events that the type forbids. 2. **`requestCheckboxConfirmation` is stricter than its runtime.** It uses the output type, so plugin authors must pass `continuationPolicy` and `allowDeclineReason`, although the schema gives them defaults. `z.input` would fix this. 3. **`RetainedRunnerdMaintenanceInput.backend.kind` is a free string.** The quarantine domain comes from `descriptor.kind`, but the release uses `proof.backend.kind`. If they differ, `completeRetainedNativeSessionCleanup` returns 0 and the quarantine stays. The production caller passes `"runner"` today, so this is not a live bug. 4. **`CodexHarnessSession.close(input?)` is optional, but the contract requires `{ reason }`.** These are tests that verified nothing. I corrected them, and they pass: - `backends/native-instruction-measurement.test.ts` filtered on `placement === "core"`, which is not a valid placement. It measured an empty tool list. It now measures 14 tools. - `drivers/acpx/codex-acpx-driver.test.ts` checked for `usage.updated`, which is not an event type. - The `hostFixture()` helper in `drivers/acpx/runtime-host.test.ts` ignored two overrides, so `expect(prepareSandbox).not.toHaveBeenCalled()` could not fail. - `live/live-session.test.ts` set `agent: "pi"` before a spread that overwrote it. - `native-distribution-integrity.test.ts` passed an AbortSignal where `openCommand` expects `{ signal }`. These are gaps that this pull request does not close: - **The root lockfile still has some old zod versions.** paperclipai/paperclip#17364 moved the direct zod dependencies to 4.6.x. `pnpm-lock.yaml` still resolves `zod@3.25.76`, `4.1.11`, and `4.4.3`, which are below the patched 4.6.6. `packages/paperclip-runner/scripts/pi-distribution/package.json` pins `zod` to `3.25.76`, and some agent SDKs resolve `4.4.3`. Dependency review does not flag them, because this pull request does not add them. - **Dead GitHub coverage.** `chat-adapters-ui-providers.spec.ts` returned early for GitHub, so four later `provider === "github"` branches never ran. I removed them. They included the private key file and paste match, and the reconnect flow. Nothing asserts those behaviors in that spec now. - **Provider tests do not run in CI.** The root `vitest.config.ts` lists only Daytona. The other 8 providers, `bridge-template` included, are typechecked now, but CI does not run their tests. - **One `packages/db` test never runs.** `scripts/clean-poisoned-claude-sessions.test.ts` is outside the `include` of `packages/db/vitest.config.ts`. - **`plugin-orchestration-smoke-example` does not collect.** `@paperclipai/shared` is not linked in its `node_modules`. It passes 3 of 3 with an alias. ## Verification Run these commands: ```sh node scripts/typecheck-test-programs.mjs --low-memory pnpm --filter @paperclipai/server typecheck node scripts/typecheck-test-projects.mjs --low-memory --reuse-declarations node --test scripts/typecheck-test-programs.test.mjs ``` Results on macOS (arm64), Node 26.12. CI uses Node 24, and its `Typecheck + Release Registry` job also passes. At head `7d4b5e907a`, the test programs, `tests/e2e`, and the script tests pass again with 0 errors. The times and peaks below come from the run before the two latest merges of master: | Check | Result | | --- | --- | | Test programs, `--low-memory` | Exit 0, 0 errors, 42 seconds, peak 1.78 GiB | | Server layers, low memory | Exit 0, 0 errors, 38 seconds, peak 3.41 GiB | | Test projects, including `tests/e2e` | Exit 0, 0 errors, 13 seconds, peak 1.59 GiB | | Script tests | 12 passed | | Injected type error in each of 23 places | Each one failed its program with TS2322 | | Coverage check | 0 unchecked files, 10 seconds | I did not run this in a 4 GiB container. The peaks above are `maximum resident set size` for the largest single process. **Runtime check of the fixes.** Types do not prove that a test still works, so I ran the suites before the two latest merges of master: | Suite | Result | | --- | --- | | Runner, full suite | 4,885 passed, 3 failed, 13 skipped | | Sandbox providers, 10 packages | 939 passed, 12 failed, 36 skipped | | Plugin SDK | 200 passed | | `kv-demo-mcp-server` | 12 passed | | Renamed UI test and its sibling | 4 passed | All 15 failures exist without these fixes or come from the local machine: - **3 Cursor Cloud timeouts.** `artifact-delivery.test.ts` has 1 and `cursor-cloud-driver.test.ts` has 2. They time out in a full local run, and all 133 tests in those files with `codex-security-config.test.ts` pass when run alone. They fail in the same way with those two files at their unedited versions. - **12 Kubernetes `file-sync.test.ts` failures.** They fail in the same way before and after the fixes. The first failure is that `/proc/self/fd` does not exist on macOS. After each of the two latest merges of master, I ran only the Runner test files that I changed: | Merge | Result | | --- | --- | | `885c249cb9`, 8 files | 534 passed | | `7d4b5e907a`, 7 files | 672 passed, 16 failed | The 16 failures are in the two `source-reconciliation` test files. They fail in the same way with master's unedited versions of those files. 15 of them fail with `ACPX sidecar exited at initialize`, so I think my local build of the Rust and sidecar binaries is out of date. I did not rebuild them to check. CI builds them from scratch, and all Runner jobs pass in CI at `7d4b5e907a`. **Not run:** - `tests/e2e` and the 8 Playwright suites need a server, so most of them have only compile checks. `playwright test --list` loads 308 tests from the main e2e config. - `packages/plugins/sandbox-providers/kubernetes/test/integration` needs a cluster, so it has only compile checks. - I did not run the full `pnpm test:run` or root `pnpm build`. Required CI runs them. **Reduced motion.** I ran 6 motion-sensitive Storybook specs in system Chrome with a temporary config, once with `reduce` and once with `no-preference`. Both runs had 45 passed and the same 5 failed. So reduced motion did not change those results. The 5 failures do not depend on motion. At least one comes from the browser, because system Chrome prints `oklch(55.6% 0 0)` where the test expects `oklch(0.556 0 0)`. I did not run the other Storybook specs. The `storybook-visual` label runs that workflow. ## Risks - **A new lockfile to maintain.** A change to a provider `package.json` now fails the typecheck job until someone refreshes `scripts/typecheck-sandbox-providers/pnpm-lock.yaml`. The failure message gives the command. - **The job has a new install.** The step downloads provider SDKs, so it depends on the npm registry. It uses `--frozen-lockfile` and `--ignore-scripts`. - **The local install replaces provider `node_modules`.** A contributor with a standalone provider install must reinstall that provider after running the script. - **Reduced motion can change Storybook results.** See "Behavior change for review". That workflow is not part of required CI. - **Kubernetes plugin validation changed.** A release of this plugin ships zod 4. Error wording changes, and very large integers are now rejected. See "Production change for review". - **Master moves fast.** Each merge of master brought new test code with type errors, 27 so far. After this merges, the check reports those errors in the pull request that adds them. ## Model Used - Provider: Anthropic Claude, through Claude Code - Model: Claude Opus 5.5 (`claude-opus-5-5`) - Capabilities: tool use, code execution, and subagents for the type fixes in separate files ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details. The app generated the branch name. - [ ] I have run tests locally and they pass. See the failures explained in Verification. - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b2004bf50
commit
83c6832ccb
176 files changed
+5771
-680
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
|
||||
|
||||
+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,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { SessionItemSnapshot } from "../reducer/session-reducer.js";
|
||||
import type { TranscriptEntry } from "../browser/transcript-model.js";
|
||||
import type { TranscriptEntry, TranscriptItemEntry } from "../browser/transcript-model.js";
|
||||
import { Composer } from "./components/composer.js";
|
||||
import { Conversation } from "./components/conversation.js";
|
||||
import { parseStructuredResponse } from "./components/message.js";
|
||||
@@ -17,7 +17,7 @@ const item: SessionItemSnapshot = {
|
||||
text: "Canonical answer",
|
||||
};
|
||||
|
||||
const entries: TranscriptEntry[] = [
|
||||
const entries: TranscriptItemEntry[] = [
|
||||
{
|
||||
kind: "item",
|
||||
key: "item:assistant-1",
|
||||
|
||||
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user