mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Tests protect sandbox execution, run recovery, credentials and the board UI. > - Several owner suites repeat the same setup and prove the same result more than once. > - These copies add maintenance work and can hide weak assertions. > - Each cut needs a retained test that proves every distinct contract. > - This pull request consolidates repeated setup and removes twelve redundant executions. > - It also replaces two weak assertions with checks of real outputs. > - The benefit is 2,831 fewer test lines with clearer regression proof. ## Linked Issues or Issue Description **What existing behavior does this improve?** The maintenance and regression strength of the existing Vitest owner suites. **Subsystem affected** Sandbox adapter utilities, server heartbeat recovery and environment capabilities, and the board UI. **Current behavior** Tests copy database rows, query builders, render calls and runtime inputs. Some cases repeat an existing contract. One token test omits a real bridge credential. A PostHog host comparison can compare absent controls. **Proposed behavior** Use small local helpers with fresh values and the same action order. Keep each distinct contract in an owner test. Check real bridge credentials and fixed PostHog method states. Carry the exact persisted first-run success assertion into the stronger cancellation keeper. **Reason and benefit** The ten test files shrink by 2,831 net lines. Twelve redundant executions retire. All other case tables, inputs, security boundaries, timeouts and cleanup rules remain. **Breaking changes** None. This round changes tests and audit evidence only. **Additional context** Refs: #15978, the merged first cleanup. Refs: #16022, the merged prior cleanup. Its squash merge is `dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`. Related open work: #14634 repairs feedback fixture completion; #14477 supplies explicit sandbox fixture tools; #14198 changes cleanup retry behavior in another heartbeat file. This audit preserves those separate action and cleanup contracts. It does not apply their changes. This PR was first stacked on `codex/test-audit-followup`. After #16022 was squash merged, only the two Round 6 commits were replayed onto merged master `dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`. The original stack head is preserved in a local snapshot branch. The current diff contains only ten test files and two evidence files. No merge is requested. ## What Changed - Consolidate repeated setup in three sandbox adapter suites. Remove seven redundant executions and retain the real protocol, cleanup and scratch-file checks. Save 875 lines. - Consolidate heartbeat database fixtures and read queries. Retain exact wake cancellation, total run count and the actual first-run success receipt. Remove one duplicate execution. Save 1,372 lines. - Remove two repeated rejected-capability cases. Carry the absent snapshot assertion into the retained runner case. Save 22 lines. - Consolidate UI fixtures and render calls. Remove two repeated connection cases. Replace the weak PostHog comparison with fixed method and required-key checks in both real hosts. Save 562 lines. - Record audit scope, decision and expansion counts, all twelve keeper mappings and deliberate mutation results in the [audit report](https://github.com/paperclipai/paperclip/blob/codex/test-audit-round-six-delivery/doc/plans/2026-10-12-test-audit-round6.md) and [controls evidence](https://github.com/paperclipai/paperclip/blob/codex/test-audit-round-six-delivery/doc/plans/2026-10-12-test-audit-round6-controls.json). ## Verification Source baseline: `a090035cafb29c06f6fca2dbe43561ad866b7c1b`. All ten original test programs are byte-identical at the stacked base `58c5c4aa241c85f4665a3159c192888bbb2a5cfa`. - Baseline: 1,841/1,841 cases passed in the main thirty-file owner/sibling run. Another 219/219 cases passed in nine extra files. - Preservation: all four proposal packages passed independent review. Whole-program helper expansion retains ordered inputs, callbacks, awaits, queries, fresh allocations and hooks outside the declared assertion migrations and repairs. - Candidate: all 2,048 cases passed across 39 complete owner/sibling files. The main run passed 1,831 cases. The extra run passed 217 cases. Both runs had zero failures and zero skips. - PostHog control: the repaired positive case passed. The original weak case passed with authentication choices removed. The repaired case failed at its fixed OAuth aria-checked assertion under the same mutation. - Eight accepted mutations failed at their intended assertions. They cover bridge-token exclusion, fallback cleanup, READY bytes, real scratch-file preservation, exact wake cancellation, the persisted first-run success receipt, rejected capability snapshots and reachable PostHog method controls. Two original weak cases passed with the same defects. All production source bytes were restored. - Rejected control: changing run status before legacy release timed out before wake cancellation. This result is not counted as assertion proof. Its replacement changed only the persisted receipt after real drain in the existing transaction. It failed at the direct fixed succeeded-row assertion. - No new code comments or 1,000-line threshold crossings were introduced. No production, schema, lockfile or workflow changes are included. On the exact final test sources, later committed as `d8de9422f0703d4508a9dc880b67a07f13e0f110`, `pnpm -r typecheck`, `pnpm check:token-gates` and `pnpm build` passed. The required full `pnpm test:run` completed with **exit 1** after **52.14 minutes** on clean, immutable `d8de9422f0703d4508a9dc880b67a07f13e0f110`. HEAD and tracked sources remained unchanged. The first general-server group ran 889 files: **882 passed, one failed and six skipped**. Its 18,032 cases were **17,925 passed, two failed and 105 skipped**. The only failing file was `server/src/__tests__/remove-test-home.test.ts`. On macOS, the read-only runtime bundle and symlink cleanup cases returned `ENOTEMPTY`. The permission-error case expects `EACCES` or `EPERM`. Four reported failure entries cover these two cases, including their cleanup errors. The test and helper are byte-identical to previous master `4b766b872993045a2e787404f0475503683e5ded` and audit master `e364b92c82`. The default wrapper stopped after that group failed. **Later local groups did not run.** The earlier public-MCP, HTTP log redaction and workspace snapshot failures did not recur. The 39 focused owner/sibling files and all 48 remote CI jobs completed independently. This result is an unresolved macOS cleanup limitation, not full local success. No test, skip, retry, timeout or cleanup rule was changed to hide it. The three adapter files received a whitespace tidy after focused execution. Full runtime AST and string/template/regex byte equality link those tested sources to the final sources. All 134 adapter helper calls were checked again. Replayed code commit `3d3defd2a60595a623045d9e2c6c586f4acb3452` has the exact final test bytes. Evidence commit `e868e6645ce0d598d756d3458f0b3d71afd6db4a` adds the report and controls JSON, with one historical ancestry clarification. All ten accepted test hashes and all seven production-owner source guards match. No package, lockfile, workspace, TypeScript, Rust or Vitest dependency manifest changed since the audit snapshot. The [original stack CI workflow](https://github.com/paperclipai/paperclip/actions/runs/38312876910) passed all 48 jobs on head `c171d6c0d603d22bb997c1f9a9bdcd5bcc5d22aa`. Those results are historical stack proof. The [fresh master-context CI workflow](https://github.com/paperclipai/paperclip/actions/runs/38318223856) completed successfully with **all 48 jobs passed** on current head `e868e6645ce0d598d756d3458f0b3d71afd6db4a`. Its tested merge commit was `41292af60c6eb60d477637d23822bb17b08dc07e`, combining workflow base `88bf4c6c1d222f818faa56257efe79207c892a6c` with that head. The delivery commits remain based on pinned master `dabb8c5daf430a1987af611e2e7ca4c7b23ef7d4`. Both required master checks, `ci / verify` and `ci / e2e`, passed. Two separate optional Storybook checks were skipped by their scope gate. [Fresh Greptile review](https://github.com/paperclipai/paperclip/pull/16047#issuecomment-6133323165) gives the current head **5/5**, with no findings, inline comments or unresolved review threads. The [current-head commitperclip review](https://github.com/paperclipai/paperclip/actions/runs/38318223666) also completed successfully. This PR remains open and draft; no merge is requested. The local macOS failure above remains unresolved. ## Risks - A helper could share a value or capture the wrong binding. Independent expansion and lexical binding checks guard the actual helper calls. - A retired case could contain a distinct assertion. Each cut has an exact keeper map and carried observations. - A mutation could fail before the intended assertion. Controls reject timeouts and earlier failures. The rejected receipt mutation remains in the evidence. - The delivery ancestry changed after the parent squash merge. Exact source hashes preserve the audited tests and producer receipts. Fresh CI and review passed in the current master context; the broad local run remains evidence from its immutable audit snapshot. - The complete local default failed in two unchanged macOS cleanup cases. Later local groups were not run. This limitation remains visible even though focused checks and remote CI passed. ## Model Used OpenAI Codex, based on GPT-6, assisted with this work. The exact served model ID and context window are not exposed in this session. It used repository tools, code execution, AST analysis and parallel audit agents. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
428 lines
17 KiB
TypeScript
428 lines
17 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { mockResolveEnvironmentDriverConfigForRuntime } = vi.hoisted(() => ({
|
|
mockResolveEnvironmentDriverConfigForRuntime: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../services/environment-config.js", () => ({
|
|
resolveEnvironmentDriverConfigForRuntime: mockResolveEnvironmentDriverConfigForRuntime,
|
|
}));
|
|
|
|
import type { EffectiveExecutionCapabilities } from "@paperclipai/adapter-utils/execution-target";
|
|
import { resolveEnvironmentExecutionTarget } from "../services/environment-execution-target.js";
|
|
import type { EnvironmentRuntimeService } from "../services/environment-runtime.js";
|
|
|
|
const SNAPSHOT: EffectiveExecutionCapabilities = {
|
|
reusableLeases: true,
|
|
nativeSyncIn: true,
|
|
nativeSyncOut: false,
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: false,
|
|
incrementalSessionOutput: false,
|
|
// Concurrent sync operations need BOTH sync verbs; this snapshot verified only
|
|
// inbound sync, so the opt-in stays off.
|
|
concurrentSyncOperations: false,
|
|
duplexCommandStream: false,
|
|
runnerWebSocketIngress: false,
|
|
};
|
|
|
|
// A snapshot that grants every capability. A test overrides one flag to prove
|
|
// that the removed capability alone changes the runtime decision.
|
|
const FULL_GRANT: EffectiveExecutionCapabilities = {
|
|
reusableLeases: true,
|
|
nativeSyncIn: true,
|
|
nativeSyncOut: true,
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: true,
|
|
incrementalSessionOutput: true,
|
|
concurrentSyncOperations: true,
|
|
duplexCommandStream: true,
|
|
runnerWebSocketIngress: true,
|
|
};
|
|
|
|
// Build a sandbox execution target with a fixed snapshot and a fixed
|
|
// `supportsSync` result. The helper returns the sandbox target so a test reads
|
|
// the runner and the streaming flag the snapshot gates.
|
|
async function buildSandboxTarget(input: {
|
|
snapshot: EffectiveExecutionCapabilities | null;
|
|
supportsSync: boolean;
|
|
config?: Record<string, unknown>;
|
|
// Reject the capability resolution to exercise the fail-closed error path.
|
|
rejectResolution?: boolean;
|
|
}) {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: { provider: "daytona", timeoutMs: 30_000, ...(input.config ?? {}) },
|
|
});
|
|
|
|
const execute = vi.fn().mockResolvedValue({
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
stdout: "ok",
|
|
stderr: "",
|
|
metadata: { durationMs: 600, getDurationMs: 15 },
|
|
});
|
|
const environmentRuntime = {
|
|
execute,
|
|
supportsSync: () => input.supportsSync,
|
|
syncIn: vi.fn(),
|
|
syncOut: vi.fn(),
|
|
resolveCapabilities: vi.fn(async () => {
|
|
if (input.rejectResolution) {
|
|
throw new Error("capability resolution failed");
|
|
}
|
|
return input.snapshot ? Object.freeze({ ...input.snapshot }) : null;
|
|
}),
|
|
} as unknown as EnvironmentRuntimeService;
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: "daytona" } },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: { remoteCwd: "/work" },
|
|
lease: { id: "lease-1", leasePolicy: "reuse_by_environment" } as never,
|
|
environmentRuntime,
|
|
});
|
|
|
|
if (target?.kind !== "remote" || target.transport !== "sandbox") {
|
|
throw new Error("expected a sandbox target");
|
|
}
|
|
return { target, execute };
|
|
}
|
|
|
|
describe("resolveEnvironmentExecutionTarget effective capability snapshot", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("test_execution_target_carries_read_only_effective_snapshot", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: { provider: "daytona", reuseLease: true, timeoutMs: 30_000 },
|
|
});
|
|
|
|
const resolveCapabilities = vi.fn(async () => Object.freeze({ ...SNAPSHOT }));
|
|
const environmentRuntime = {
|
|
supportsSync: () => false,
|
|
resolveCapabilities,
|
|
} as unknown as EnvironmentRuntimeService;
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: "daytona" } },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: { remoteCwd: "/work" },
|
|
lease: { id: "lease-1", leasePolicy: "reuse_by_environment" } as never,
|
|
environmentRuntime,
|
|
});
|
|
|
|
expect(target?.kind).toBe("remote");
|
|
if (target?.kind !== "remote" || target.transport !== "sandbox") {
|
|
throw new Error("expected a sandbox target");
|
|
}
|
|
expect(resolveCapabilities).toHaveBeenCalledTimes(1);
|
|
expect(target.effectiveCapabilities).toEqual(SNAPSHOT);
|
|
|
|
// The snapshot is read-only: it is frozen, so a write does not change it.
|
|
expect(Object.isFrozen(target.effectiveCapabilities)).toBe(true);
|
|
const snapshot = target.effectiveCapabilities as EffectiveExecutionCapabilities;
|
|
try {
|
|
(snapshot as { reusableLeases: boolean }).reusableLeases = false;
|
|
} catch {
|
|
// A strict-mode assignment throws; a non-strict one is a silent no-op.
|
|
}
|
|
expect(snapshot.reusableLeases).toBe(true);
|
|
});
|
|
|
|
it("omits the snapshot when no environment runtime resolves it", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: { provider: "daytona", reuseLease: false, timeoutMs: 30_000 },
|
|
});
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: "daytona" } },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {},
|
|
lease: null,
|
|
environmentRuntime: null,
|
|
});
|
|
|
|
expect(target?.kind).toBe("remote");
|
|
if (target?.kind !== "remote" || target.transport !== "sandbox") {
|
|
throw new Error("expected a sandbox target");
|
|
}
|
|
expect(target.effectiveCapabilities).toBeUndefined();
|
|
});
|
|
|
|
// The static `hasLeaseCapabilityModel` trait is `true` only for the
|
|
// `sandbox` driver in this phase (see `environment-driver-traits.ts`). An
|
|
// `ssh` lease must never reach the general resolver: the new
|
|
// `resolveCapabilities` method never returns `null` for a registered
|
|
// driver, so an ungated call would turn "no snapshot" into "every
|
|
// capability denied" for a driver this file does not otherwise gate on.
|
|
it("never calls the general capability resolver for an ssh lease", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "ssh",
|
|
config: {
|
|
host: "example.test",
|
|
port: 22,
|
|
username: "agent",
|
|
remoteWorkspacePath: "/work",
|
|
},
|
|
});
|
|
|
|
const resolveCapabilities = vi.fn(async () => ({ ...SNAPSHOT }));
|
|
const environmentRuntime = {
|
|
resolveCapabilities,
|
|
} as unknown as EnvironmentRuntimeService;
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "ssh", config: {} },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: {},
|
|
lease: { id: "lease-1", leasePolicy: "reuse_by_environment" } as never,
|
|
environmentRuntime,
|
|
});
|
|
|
|
expect(target?.kind).toBe("remote");
|
|
expect(resolveCapabilities).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("carries the environment-owned warm runner lifecycle and reuse requirement", async () => {
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: FULL_GRANT,
|
|
supportsSync: false,
|
|
config: {
|
|
reuseLease: true,
|
|
runnerLifecycleMode: "warm",
|
|
runnerIdleTimeoutMs: 45_000,
|
|
},
|
|
});
|
|
|
|
expect(target.runnerLifecyclePolicy).toEqual({
|
|
mode: "warm",
|
|
idleTimeoutMs: 45_000,
|
|
});
|
|
expect(target.reusableLeaseConfigured).toBe(true);
|
|
});
|
|
|
|
it("carries host-owned sandbox acquisition provenance without persisting provider ids in metadata", async () => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: { provider: "daytona", reuseLease: true, timeoutMs: 30_000 },
|
|
});
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
// This substrate PR does not advertise remote paperclip_runner support
|
|
// until the Rust WSS transport lands. A supported direct adapter exercises
|
|
// the same host-owned acquisition contract without widening rollout here.
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: "daytona" } },
|
|
leaseId: "lease-row-1",
|
|
leaseMetadata: {
|
|
remoteCwd: "/work",
|
|
sandboxLeaseAcquisition: { outcome: "resumed" },
|
|
},
|
|
lease: {
|
|
id: "lease-row-1",
|
|
providerLeaseId: "daytona-sandbox-1",
|
|
leasePolicy: "reuse_by_environment",
|
|
metadata: { sandboxLeaseAcquisition: { outcome: "resumed" } },
|
|
} as never,
|
|
environmentRuntime: {
|
|
supportsSync: () => false,
|
|
resolveCapabilities: vi.fn(async () => ({ ...FULL_GRANT })),
|
|
} as unknown as EnvironmentRuntimeService,
|
|
});
|
|
if (target?.kind !== "remote" || target.transport !== "sandbox") {
|
|
throw new Error("expected a sandbox target");
|
|
}
|
|
expect(target.sandboxLeaseAcquisition).toEqual({
|
|
outcome: "resumed",
|
|
providerLeaseId: "daytona-sandbox-1",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("effective snapshot gates the sync decision", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("exposes the native sync hooks when the snapshot grants both sync verbs", async () => {
|
|
const { target } = await buildSandboxTarget({ snapshot: FULL_GRANT, supportsSync: true });
|
|
expect(target.runner?.syncIn).toBeTypeOf("function");
|
|
expect(target.runner?.syncOut).toBeTypeOf("function");
|
|
});
|
|
|
|
it("omits the native sync hooks when the snapshot removes a sync verb", async () => {
|
|
// The snapshot verified inbound sync but not outbound sync. The runner
|
|
// exposes the sync hooks both-or-neither, so it keeps the base64 fallback.
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: { ...FULL_GRANT, nativeSyncOut: false },
|
|
supportsSync: true,
|
|
});
|
|
expect(target.runner?.syncIn).toBeUndefined();
|
|
expect(target.runner?.syncOut).toBeUndefined();
|
|
});
|
|
|
|
it("still requires supportsSync even when the snapshot grants native sync", async () => {
|
|
const { target } = await buildSandboxTarget({ snapshot: FULL_GRANT, supportsSync: false });
|
|
expect(target.runner?.syncIn).toBeUndefined();
|
|
expect(target.runner?.syncOut).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// The execution target carries the concurrent-sync opt-in on both the effective
|
|
// snapshot and the runner. The runner Boolean feeds the sync client, which then
|
|
// tells the orchestrator whether it may run sync operations concurrently.
|
|
describe("effective snapshot carries the concurrent-sync capability", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("carries the concurrent-sync opt-in on the snapshot and the runner", async () => {
|
|
const { target } = await buildSandboxTarget({ snapshot: FULL_GRANT, supportsSync: true });
|
|
expect(target.effectiveCapabilities?.concurrentSyncOperations).toBe(true);
|
|
expect(target.runner?.allowConcurrentSyncOperations).toBe(true);
|
|
});
|
|
|
|
it("keeps the concurrent-sync opt-in off when the snapshot removes it", async () => {
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: { ...FULL_GRANT, concurrentSyncOperations: false },
|
|
supportsSync: true,
|
|
});
|
|
expect(target.effectiveCapabilities?.concurrentSyncOperations).toBe(false);
|
|
expect(target.runner?.allowConcurrentSyncOperations).toBe(false);
|
|
});
|
|
|
|
it("keeps the concurrent-sync opt-in off when the resolution rejects", async () => {
|
|
// A rejected resolution carries no snapshot; it must not read as an open
|
|
// grant. The runner keeps the opt-in off.
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: null,
|
|
supportsSync: true,
|
|
rejectResolution: true,
|
|
});
|
|
expect(target.effectiveCapabilities).toBeUndefined();
|
|
expect(target.runner?.allowConcurrentSyncOperations).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Session-output streaming is decided downstream from the carried snapshot
|
|
// alone (see `streamAgentSessionOutput` in `acpx-engine/execute.ts`): the bridge
|
|
// streams only when the snapshot grants `incrementalSessionOutput`. That key is
|
|
// opt-in, so a generic one-shot provider that keeps persistent process sessions
|
|
// and runs independent control commands, yet never declares incremental session
|
|
// output, keeps the poll path. These tests prove the target carries the exact
|
|
// capability that drives that decision.
|
|
describe("effective snapshot carries the session-output-streaming capability", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("carries incremental session output when the snapshot grants it (stream on)", async () => {
|
|
const { target } = await buildSandboxTarget({ snapshot: FULL_GRANT, supportsSync: false });
|
|
expect(target.effectiveCapabilities?.incrementalSessionOutput).toBe(true);
|
|
});
|
|
|
|
it("drops incremental session output when the snapshot removes it (poll)", async () => {
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: { ...FULL_GRANT, incrementalSessionOutput: false },
|
|
supportsSync: false,
|
|
});
|
|
expect(target.effectiveCapabilities?.incrementalSessionOutput).toBe(false);
|
|
});
|
|
|
|
it("keeps the poll path for a generic one-shot provider with broad caps but no streaming opt-in", async () => {
|
|
// The regression case: a generic one-shot provider (for example Modal)
|
|
// exposes the two broad session capabilities but never emits incremental
|
|
// session output. The streaming gate reads `incrementalSessionOutput`, so
|
|
// this provider keeps the poll path.
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: {
|
|
...FULL_GRANT,
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: true,
|
|
incrementalSessionOutput: false,
|
|
},
|
|
supportsSync: false,
|
|
});
|
|
expect(target.effectiveCapabilities?.persistentProcessSessions).toBe(true);
|
|
expect(target.effectiveCapabilities?.independentControlCommands).toBe(true);
|
|
expect(target.effectiveCapabilities?.incrementalSessionOutput).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("effective snapshot gates the persistent-session execution decision", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("forces the persistent session when the snapshot grants persistent process sessions", async () => {
|
|
const { target, execute } = await buildSandboxTarget({ snapshot: FULL_GRANT, supportsSync: false });
|
|
await target.runner?.execute({ command: "node", args: ["agent.js"], useSession: true, commandTimeoutMs: 12000 });
|
|
const call = execute.mock.calls[0]![0] as { forceSession?: boolean };
|
|
expect(call.forceSession).toBe(true);
|
|
expect(call).toHaveProperty("commandTimeoutMs", 12000);
|
|
});
|
|
|
|
it("never forces the persistent session when the snapshot removes persistent process sessions", async () => {
|
|
// The bridge still asks for the session with `useSession`, but the provider
|
|
// cannot keep one, so the seam runs the command one-shot instead.
|
|
const { target, execute } = await buildSandboxTarget({
|
|
snapshot: { ...FULL_GRANT, persistentProcessSessions: false },
|
|
supportsSync: false,
|
|
});
|
|
await target.runner?.execute({ command: "node", args: ["agent.js"], useSession: true });
|
|
const call = execute.mock.calls[0]![0] as { forceSession?: boolean };
|
|
expect(call.forceSession).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("a rejected capability resolution fails closed for persistent-session behavior", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("never forces the persistent session when the resolution rejects", async () => {
|
|
// The bridge asks for the session with `useSession`, but a rejected
|
|
// resolution fails closed, so the seam runs the command one-shot instead.
|
|
const { target, execute } = await buildSandboxTarget({
|
|
snapshot: null,
|
|
supportsSync: false,
|
|
rejectResolution: true,
|
|
});
|
|
expect(target.effectiveCapabilities).toBeUndefined();
|
|
await target.runner?.execute({ command: "node", args: ["agent.js"], useSession: true });
|
|
const call = execute.mock.calls[0]![0] as { forceSession?: boolean };
|
|
expect(call.forceSession).toBe(false);
|
|
});
|
|
|
|
it("omits the native sync hooks when the resolution rejects", async () => {
|
|
// The worker advertises both sync verbs, but a rejected resolution must not
|
|
// read as an open grant. Fail closed and keep the base64 fallback, so an
|
|
// unverified provider never gets the native sync path.
|
|
const { target } = await buildSandboxTarget({
|
|
snapshot: null,
|
|
supportsSync: true,
|
|
rejectResolution: true,
|
|
});
|
|
expect(target.runner?.syncIn).toBeUndefined();
|
|
expect(target.runner?.syncOut).toBeUndefined();
|
|
});
|
|
});
|