388 lines
14 KiB
TypeScript
388 lines
14 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { canonicalNativeRuntimeContextDigest } from "../../vendor/paperclip-runner/index.js";
|
|
import { buildNativeExecutionInput } from "./native-execution-input.js";
|
|
import { rebindNativeSessionCheckpoint } from "./native-session-resume.js";
|
|
import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js";
|
|
|
|
const companyId = "10000000-0000-4000-8000-000000000001";
|
|
const issueId = "20000000-0000-4000-8000-000000000002";
|
|
const agentId = "30000000-0000-4000-8000-000000000003";
|
|
const previousRunId = "40000000-0000-4000-8000-000000000004";
|
|
const currentRunId = "50000000-0000-4000-8000-000000000005";
|
|
const normalizedSessionId = "60000000-0000-4000-8000-000000000006";
|
|
|
|
function execution(runId: string, cwd = "/workspace") {
|
|
return buildNativeExecutionInput({
|
|
companyId,
|
|
runId,
|
|
issue: { id: issueId, identifier: "DOT-2", title: "Test", description: null, workMode: "standard" },
|
|
taskPrompt: "Only the current turn",
|
|
agentId,
|
|
workspace: { id: runId, cwd, repoUrl: null, repoRef: null, branchName: null },
|
|
normalizedSessionId,
|
|
provider: "codex",
|
|
completionContract: {
|
|
id: "70000000-0000-4000-8000-000000000007",
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
schemaVersion: "paperclip.run-result.v1",
|
|
contract: {
|
|
revision: "1",
|
|
objective: "Test session resumption",
|
|
criteria: [{ id: "objective", requirement: "Answer the current turn" }],
|
|
},
|
|
},
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
}
|
|
|
|
function planningExecution(runId: string, revisionId: string) {
|
|
return buildNativeExecutionInput({
|
|
companyId,
|
|
runId,
|
|
issue: { id: issueId, identifier: "DOT-2", title: "Test", description: null, workMode: "planning" },
|
|
taskPrompt: "Revise the plan",
|
|
agentId,
|
|
workspace: { id: runId, cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null },
|
|
normalizedSessionId,
|
|
provider: "codex",
|
|
executionMode: "plan",
|
|
planningContext: {
|
|
documentId: "80000000-0000-4000-8000-000000000008",
|
|
baseRevisionId: revisionId,
|
|
baseRevisionNumber: revisionId.endsWith("9") ? 9 : 8,
|
|
markdown: `Plan at ${revisionId}`,
|
|
sha256: `sha256:${"b".repeat(64)}`,
|
|
reviewContext: {},
|
|
},
|
|
completionContract: execution(runId).completionContract,
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
}
|
|
|
|
function previousRun(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: previousRunId,
|
|
companyId,
|
|
agentId,
|
|
nativeSessionId: normalizedSessionId,
|
|
runnerProfileJson: {
|
|
nativeExecutionInput: execution(previousRunId),
|
|
sessionCheckpoint: {
|
|
backendKind: "runner",
|
|
driverKind: "codex_app_server",
|
|
sessionId: "provider-thread-123",
|
|
providerSessionId: "provider-thread-123",
|
|
cursor: "42",
|
|
identity: { runId: previousRunId, sessionId: normalizedSessionId, companyId, issueId, agentId },
|
|
semanticResult: { schema: "paperclip.run-result.v1", reportedWorkDisposition: "done", summary: "old" },
|
|
terminal: { schema: "paperclip.prp.terminal.v1", turnTerminalState: "completed", runTerminalState: "succeeded", reportedWorkDisposition: "done" },
|
|
activeTurnId: "old-turn",
|
|
terminalTurns: [{ turnId: "old-turn", state: "completed" }],
|
|
pendingRuntimeRequests: [{ requestId: "old-request" }],
|
|
lineage: [{ threadId: "provider-thread-123" }],
|
|
},
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("rebindNativeSessionCheckpoint", () => {
|
|
it("retains provider identity but clears prior turn and event state", () => {
|
|
const rebound = rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: execution(currentRunId),
|
|
});
|
|
expect(rebound).toMatchObject({
|
|
sessionId: "provider-thread-123",
|
|
providerSessionId: "provider-thread-123",
|
|
driverKind: "codex_app_server",
|
|
cursor: null,
|
|
semanticResult: null,
|
|
terminal: null,
|
|
activeTurnId: null,
|
|
terminalTurns: [],
|
|
pendingRuntimeRequests: [],
|
|
providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
identity: { runId: currentRunId, sessionId: normalizedSessionId, companyId, issueId, agentId },
|
|
});
|
|
});
|
|
|
|
it("allows a provider replacement only after a durable response wake", () => {
|
|
const source = previousRun();
|
|
const profile = source.runnerProfileJson as Record<string, unknown>;
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.semanticResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "yielded",
|
|
summary: "Waiting for a response.",
|
|
continuation: {
|
|
kind: "response_wake",
|
|
idempotencyKey: "interaction-response:one",
|
|
},
|
|
};
|
|
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toMatchObject({
|
|
providerRecoveryPolicy: "allow_replacement_after_governed_wait",
|
|
semanticResult: null,
|
|
activeTurnId: null,
|
|
});
|
|
});
|
|
|
|
it("refuses to resume when the workspace changes", () => {
|
|
expect(rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: execution(currentRunId, "/different-workspace"),
|
|
})).toBeNull();
|
|
});
|
|
|
|
it("rotates when assigned context changes but permits a fresh run-scoped MCP binding", () => {
|
|
const reboundCredential = execution(currentRunId);
|
|
reboundCredential.runtimeContext.mcp.bindingId = "native-mcp:fresh-run";
|
|
expect(rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: reboundCredential,
|
|
})).not.toBeNull();
|
|
|
|
const changedAssignment = execution(currentRunId);
|
|
const withoutAggregate = {
|
|
prompt: changedAssignment.runtimeContext.prompt,
|
|
instructions: changedAssignment.runtimeContext.instructions,
|
|
skills: changedAssignment.runtimeContext.skills,
|
|
mcp: {
|
|
assignmentSetId: `sha256:${"1".repeat(64)}`,
|
|
digest: "1".repeat(64),
|
|
bindingId: "native-mcp:fresh-run",
|
|
},
|
|
};
|
|
changedAssignment.runtimeContext = {
|
|
...withoutAggregate,
|
|
aggregateDigest: canonicalNativeRuntimeContextDigest(withoutAggregate),
|
|
};
|
|
expect(rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: changedAssignment,
|
|
})).toBeNull();
|
|
});
|
|
|
|
it("refuses a checkpoint whose prior-run binding was rewritten", () => {
|
|
const source = previousRun();
|
|
const profile = source.runnerProfileJson as Record<string, unknown>;
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.identity = { ...(checkpoint.identity as Record<string, unknown>), runId: currentRunId };
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: source, currentExecution: execution(currentRunId) })).toBeNull();
|
|
});
|
|
|
|
it("reuses plan mode across canonical revisions but never across a mode change", () => {
|
|
const source = previousRun({ nativeExecutionInput: planningExecution(previousRunId, "revision-8") });
|
|
expect(rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: planningExecution(currentRunId, "revision-9"),
|
|
})).not.toBeNull();
|
|
expect(rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId),
|
|
})).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("buildNativeExecutionInput wake projection", () => {
|
|
it("writes native v4 and pins every provider's complete effective configuration", () => {
|
|
const common = {
|
|
companyId,
|
|
runId: currentRunId,
|
|
issue: { id: issueId, identifier: "DOT-4", title: "Permissions", description: null, workMode: "standard" },
|
|
taskPrompt: "Verify permissions",
|
|
agentId,
|
|
workspace: { id: currentRunId, cwd: "/workspace", repoUrl: null, repoRef: null, branchName: null },
|
|
normalizedSessionId,
|
|
completionContract: execution(currentRunId).completionContract,
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
} as const;
|
|
const codex = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "codex",
|
|
codexApprovalPolicy: "on-request",
|
|
});
|
|
const opencode = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "opencode",
|
|
model: "openrouter/z-ai/glm-5.2",
|
|
opencodePermissionMode: "ask",
|
|
});
|
|
const acpx = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "acpx",
|
|
acpxAgent: "claude",
|
|
model: "claude-sonnet-5",
|
|
acpxPermissionMode: "deny-all",
|
|
});
|
|
const claudeManaged = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "claude_managed",
|
|
model: "claude-sonnet-5",
|
|
managedProfile: {
|
|
profileId: "managed-profile",
|
|
anthropicAgentId: "agent-remote",
|
|
agentVersion: "7",
|
|
environmentId: "environment-remote",
|
|
betaVersion: "managed-agents-2026-04-01",
|
|
},
|
|
maxSessionListCostUsd: 0.75,
|
|
});
|
|
const agentCore = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "aws_agentcore",
|
|
model: "global.anthropic.claude-sonnet-4-6",
|
|
agentCoreProfile: {
|
|
profileId: "agentcore-profile",
|
|
region: "us-east-1",
|
|
accountId: "123456789012",
|
|
harnessArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/h-1",
|
|
harnessVersion: "3",
|
|
endpointArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:endpoint/e-1",
|
|
endpointQualifier: "prod",
|
|
agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/r-1",
|
|
memoryArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/m-1",
|
|
memoryId: "m-1",
|
|
invocationRoleArn: "arn:aws:iam::123456789012:role/invoke",
|
|
contextBucket: "paperclip-context",
|
|
contextPrefix: "runner/",
|
|
contextKmsKeyArn: "arn:aws:kms:us-east-1:123456789012:key/key-1",
|
|
qualificationRevision: "aws-agentcore-harness-v1",
|
|
eventExpiryDays: 90,
|
|
},
|
|
maxEstimatedSessionCostUsd: 1.25,
|
|
invocationLimits: {
|
|
maxIterations: 8,
|
|
maxOutputTokens: 4_096,
|
|
timeoutSeconds: 300,
|
|
},
|
|
});
|
|
const defaultOpenCode = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "opencode",
|
|
model: "openrouter/z-ai/glm-5.2",
|
|
});
|
|
const defaultAcpx = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "acpx",
|
|
model: "gpt-5.6-sol",
|
|
});
|
|
|
|
expect(codex).toMatchObject({
|
|
schema: "paperclip.native-execution-input.v4",
|
|
provider: { kind: "codex", approvalPolicy: "on-request" },
|
|
});
|
|
expect(opencode).toMatchObject({
|
|
schema: "paperclip.native-execution-input.v4",
|
|
provider: { kind: "opencode", permissionMode: "ask" },
|
|
});
|
|
expect(acpx).toMatchObject({
|
|
schema: "paperclip.native-execution-input.v4",
|
|
provider: { kind: "acpx", permissionMode: "deny-all" },
|
|
});
|
|
expect(claudeManaged).toMatchObject({
|
|
session: { driverKind: "claude_managed_agents_api" },
|
|
provider: {
|
|
kind: "claude_managed",
|
|
managedProfile: { profileId: "managed-profile" },
|
|
maxSessionListCostUsd: 0.75,
|
|
},
|
|
});
|
|
expect(agentCore).toMatchObject({
|
|
session: { driverKind: "aws_agentcore_harness_api" },
|
|
provider: {
|
|
kind: "aws_agentcore",
|
|
agentCoreProfile: {
|
|
profileId: "agentcore-profile",
|
|
eventExpiryDays: 90,
|
|
},
|
|
maxEstimatedSessionCostUsd: 1.25,
|
|
},
|
|
});
|
|
expect(defaultOpenCode).toMatchObject({
|
|
provider: { kind: "opencode", permissionMode: "ask" },
|
|
});
|
|
expect(defaultAcpx).toMatchObject({
|
|
provider: {
|
|
kind: "acpx",
|
|
agent: "codex",
|
|
permissionMode: "approve-reads",
|
|
},
|
|
});
|
|
expect(JSON.stringify([codex, opencode, claudeManaged, agentCore, acpx]))
|
|
.not.toMatch(/OPENAI_API_KEY|ANTHROPIC_API_KEY|AWS_SECRET_ACCESS_KEY|PAPERCLIP_API_KEY/);
|
|
});
|
|
|
|
it("places child completion summaries in the closed provider prompt", () => {
|
|
const input = buildNativeExecutionInput({
|
|
companyId,
|
|
runId: currentRunId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-146",
|
|
title: "Finish after child handoff",
|
|
description: "Use the child result.",
|
|
workMode: "standard",
|
|
},
|
|
taskPrompt: "Paperclip task context:\n- Issue: DOT-146",
|
|
wakePayload: {
|
|
reason: "issue_children_completed",
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-146",
|
|
title: "Finish after child handoff",
|
|
description: "Use the child result.",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
workMode: "standard",
|
|
},
|
|
childIssueSummaries: [{
|
|
id: "child-147",
|
|
identifier: "DOT-147",
|
|
title: "Build utility",
|
|
status: "done",
|
|
summary: "Created three files and passed 7/7 tests.",
|
|
}],
|
|
childIssueSummaryTruncated: false,
|
|
checkedOutByHarness: true,
|
|
},
|
|
resumedSession: true,
|
|
agentId,
|
|
workspace: {
|
|
id: currentRunId,
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
normalizedSessionId,
|
|
provider: "opencode",
|
|
model: "openrouter/z-ai/glm-5.2",
|
|
completionContract: {
|
|
id: "70000000-0000-4000-8000-000000000007",
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
schemaVersion: "paperclip.run-result.v1",
|
|
contract: {
|
|
revision: "1",
|
|
objective: "Finish after the child",
|
|
criteria: [{ id: "objective", requirement: "Report the child result" }],
|
|
},
|
|
},
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
|
|
expect(input.task.prompt).toContain("## Paperclip Resume Delta");
|
|
expect(input.task.prompt).toContain("reason: issue_children_completed");
|
|
expect(input.task.prompt).toContain("DOT-147 Build utility (done)");
|
|
expect(input.task.prompt).toContain("Created three files and passed 7/7 tests.");
|
|
expect(input.task.prompt).toContain("Paperclip task context:\n- Issue: DOT-146");
|
|
expect(input.task.prompt).not.toContain("Use the child result.");
|
|
});
|
|
});
|