Files
PaperClipAI/server/src/__tests__/heartbeat-workspace-session.test.ts
T
Devin FoleyandPaperclip 3d0e74e743 Keep proven external connection failures out of Sentry (#15590)
Apply the reviewed change for Keep proven external connection failures out of Sentry.

Validation: local typecheck/build and focused regression tests, passing exact-head CI, Greptile 5/5, and independent source review. The PR records full-suite evidence and any local environment limitations.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-08 10:19:40 -07:00

3347 lines
115 KiB
TypeScript

import { execFile as execFileCallback } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { describe, expect, it, vi } from "vitest";
import type { agents } from "@paperclipai/db";
import { sessionCodec as codexSessionCodec } from "@paperclipai/adapter-codex-local/server";
import { resolveDefaultAgentWorkspaceDir } from "../home-paths.js";
import {
applyPersistedExecutionWorkspaceConfig,
assertGitSensitiveAdapterWorkspaceValid,
assertGitWorktreeBaseWorkspaceReady,
assertPushCapabilityCheckoutValid,
buildExplicitResumeSessionOverride,
buildEffectiveRunSessionConfigMetadata,
buildEffectiveRunWorkspaceConfigMetadata,
buildWorkspaceConfigFreshnessOperation,
deriveTaskKeyWithHeartbeatFallback,
formatRuntimeWorkspaceWarningLog,
mergeExecutionWorkspaceMetadataForPersistence,
mergeCoalescedContextSnapshot,
preflightLowTrustWorkspaceIsolation,
prioritizeProjectWorkspaceCandidatesForRun,
parseSessionCompactionPolicy,
provisionExecutionWorkspaceForFreshnessDecision,
reconcileReusedExecutionWorkspaceProjectWorkspaceId,
resolveNativeRecoveryExecutionWorkspaceBinding,
resolveExecutionWorkspaceBranchOwnership,
resolveExecutionWorkspaceConfigFreshness,
resolveExecutionWorkspaceReuseRequestForIssue,
resolveExecutionWorkspaceReuseProvisioningPolicy,
resolveNextSessionState,
resolveTaskSessionConfigFreshness,
isWorkspaceSyncConflictFailure,
requiresPushCapabilityPreflight,
resolveWorkspaceAfterLowTrustPreflight,
resolveRuntimeSessionParamsForWorkspace,
shouldDeferFollowupWakeForSameIssue,
stripHostWorkspaceProvisionForLowTrustSandbox,
stripWorkspaceRuntimeFromExecutionRunConfig,
shouldResetTaskSessionForModelChange,
stripConfiguredModelFromSessionParams,
stripPaperclipSessionMetadataFromSessionParams,
normalizeSessionParams,
isTaskSessionCredentialCompatible,
shouldResetTaskSessionForWake,
scrubGitCredentialText,
buildAnchorFallbackWorkspaceNotes,
type ResolvedWorkspaceForRun,
} from "../services/heartbeat.ts";
import type { TrustPresetResolution } from "../services/trust-preset-resolver.ts";
const execFile = promisify(execFileCallback);
function buildResolvedWorkspace(overrides: Partial<ResolvedWorkspaceForRun> = {}): ResolvedWorkspaceForRun {
return {
cwd: "/tmp/project",
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: null,
repoRef: null,
workspaceHints: [],
warnings: [],
baseCwdFallback: false,
materializationFailures: [],
...overrides,
};
}
type WorkspaceValidationInput = Parameters<typeof assertGitSensitiveAdapterWorkspaceValid>[0];
function buildWorkspaceValidationInput(
overrides: Partial<WorkspaceValidationInput> = {},
): WorkspaceValidationInput {
return {
adapterType: "codex_local",
agentId: "agent-1",
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
resolvedWorkspace: buildResolvedWorkspace(),
executionWorkspace: {
baseCwd: "/tmp/project",
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: null,
repoRef: null,
strategy: "project_primary",
cwd: "/tmp/project",
branchName: null,
worktreePath: null,
warnings: [],
created: false,
baseRefSha: null,
},
persistedExecutionWorkspace: {
id: "execution-workspace-1",
companyId: "company-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
sourceIssueId: "issue-1",
mode: "project_workspace",
strategyType: "project_primary",
name: "Primary workspace",
status: "active",
cwd: "/tmp/project",
repoUrl: null,
baseRef: null,
branchName: null,
providerType: "local_path",
providerRef: null,
derivedFromExecutionWorkspaceId: null,
lastUsedAt: new Date("2026-06-06T00:00:00.000Z"),
openedAt: new Date("2026-06-06T00:00:00.000Z"),
closedAt: null,
cleanupEligibleAt: null,
cleanupReason: null,
config: null,
metadata: null,
createdAt: new Date("2026-06-06T00:00:00.000Z"),
updatedAt: new Date("2026-06-06T00:00:00.000Z"),
},
executionTarget: { kind: "local" },
...overrides,
};
}
async function runGit(cwd: string, args: string[]) {
await execFile("git", args, { cwd });
}
async function createGitCheckout(options: { withRemote: boolean }) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-push-preflight-"));
await runGit(root, ["init"]);
if (options.withRemote) {
await runGit(root, ["remote", "add", "origin", "https://github.com/example/repo.git"]);
}
return root;
}
async function expectWorkspaceValidationFailure(
input: WorkspaceValidationInput,
reason: string,
message: string,
) {
await expect(assertGitSensitiveAdapterWorkspaceValid(input)).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining(message),
resultJson: {
workspaceValidation: expect.objectContaining({
reason,
adapterType: input.adapterType,
issueId: input.issue?.id,
}),
},
});
}
function buildAgent(adapterType: string, runtimeConfig: Record<string, unknown> = {}) {
return {
id: "agent-1",
companyId: "company-1",
projectId: null,
goalId: null,
name: "Agent",
role: "engineer",
title: null,
icon: null,
status: "running",
reportsTo: null,
capabilities: null,
adapterType,
adapterConfig: {},
runtimeConfig,
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
permissions: {},
lastHeartbeatAt: null,
metadata: null,
createdAt: new Date(),
updatedAt: new Date(),
} as unknown as typeof agents.$inferSelect;
}
const hermesSessionCodec = {
deserialize(raw: unknown) {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
const record = raw as Record<string, unknown>;
const sessionId = typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId.trim() : null;
return sessionId ? { sessionId } : null;
},
serialize(params: Record<string, unknown> | null) {
if (!params) return null;
const sessionId = typeof params.sessionId === "string" && params.sessionId.trim() ? params.sessionId.trim() : null;
return sessionId ? { sessionId } : null;
},
getDisplayId(params: Record<string, unknown> | null) {
return typeof params?.sessionId === "string" && params.sessionId.trim() ? params.sessionId.trim() : null;
},
};
const truncatingHermesSessionCodec = {
...hermesSessionCodec,
getDisplayId(params: Record<string, unknown> | null) {
const sessionId = hermesSessionCodec.getDisplayId(params);
return sessionId ? sessionId.slice(0, 16) : null;
},
};
function lowTrustResolution(): TrustPresetResolution {
return {
kind: "low_trust_review",
preset: "low_trust_review",
boundary: {
mode: "low_trust_review",
companyId: "company-1",
rootIssueId: "issue-1",
},
sourcePresets: { agent: "low_trust_review" },
};
}
function standardTrustResolution(): TrustPresetResolution {
return {
kind: "standard",
preset: "standard",
boundary: null,
sourcePresets: {},
};
}
function buildIssueAncestryDb(rows: Array<{ id: string; companyId: string; parentId: string | null }>) {
const queue = [...rows];
return {
select: () => ({
from: () => ({
where: () => {
const row = queue.shift();
return Promise.resolve(row ? [row] : []);
},
}),
}),
};
}
describe("assertGitSensitiveAdapterWorkspaceValid", () => {
it("rejects a project-workspace-linked issue that is missing its project id before adapter launch", async () => {
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: null,
projectWorkspaceId: "workspace-1",
},
}),
"missing_project_id",
"linked to a project workspace but has no project id",
);
});
it("rejects a git-sensitive local adapter when effective cwd differs from the persisted workspace cwd", async () => {
const input = buildWorkspaceValidationInput();
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
executionWorkspace: {
...input.executionWorkspace,
cwd: "/tmp/agent-fallback",
},
}),
"persisted_cwd_mismatch",
'resolved adapter cwd "/tmp/agent-fallback"',
);
});
it("rejects a workspace-linked issue when no execution workspace was persisted", async () => {
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
persistedExecutionWorkspace: null,
}),
"missing_persisted_execution_workspace",
"requires a project execution workspace",
);
});
it("rejects a workspace-linked issue when no effective adapter cwd was resolved", async () => {
const input = buildWorkspaceValidationInput();
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
executionWorkspace: {
...input.executionWorkspace,
cwd: null,
},
}),
"missing_effective_cwd",
"no adapter cwd was resolved",
);
});
it("rejects a persisted execution workspace linked to a different project workspace", async () => {
const input = buildWorkspaceValidationInput();
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
persistedExecutionWorkspace: {
...input.persistedExecutionWorkspace!,
projectWorkspaceId: "workspace-other",
},
}),
"project_workspace_mismatch",
'expected project workspace "workspace-1"',
);
});
it("rejects a persisted execution workspace missing its project workspace id", async () => {
const input = buildWorkspaceValidationInput();
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
persistedExecutionWorkspace: {
...input.persistedExecutionWorkspace!,
projectWorkspaceId: null,
},
}),
"persisted_workspace_missing_project_workspace_id",
"has no project workspace id",
);
});
it("rejects a workspace-linked issue that would launch from the agent fallback cwd", async () => {
const input = buildWorkspaceValidationInput();
const fallbackCwd = resolveDefaultAgentWorkspaceDir("agent-1");
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
executionWorkspace: {
...input.executionWorkspace,
cwd: fallbackCwd,
},
persistedExecutionWorkspace: {
...input.persistedExecutionWorkspace!,
cwd: fallbackCwd,
},
}),
"fallback_agent_home_cwd",
"would launch from agent fallback cwd",
);
});
it("rejects a git worktree persisted workspace when cwd differs from providerRef", async () => {
const input = buildWorkspaceValidationInput();
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
executionWorkspace: {
...input.executionWorkspace,
strategy: "git_worktree",
cwd: "/tmp/worktree-current",
},
persistedExecutionWorkspace: {
...input.persistedExecutionWorkspace!,
strategyType: "git_worktree",
cwd: "/tmp/worktree-current",
providerRef: "/tmp/worktree-expected",
},
}),
"git_worktree_provider_ref_mismatch",
'expected git worktree "/tmp/worktree-expected"',
);
});
it("rejects a git worktree persisted workspace when the checked-out branch differs from the recorded branch", async () => {
const repoRoot = await createGitCheckout({ withRemote: false });
const worktreeParent = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-branch-worktree-"));
const worktreePath = path.join(worktreeParent, "workspace");
const recordedBranch = "PAP-1-recorded-branch";
const actualBranch = "PAP-1-push-pr-head";
try {
await runGit(repoRoot, ["config", "user.email", "test@example.com"]);
await runGit(repoRoot, ["config", "user.name", "Paperclip Test"]);
await fs.writeFile(path.join(repoRoot, "README.md"), "initial\n", "utf8");
await runGit(repoRoot, ["add", "README.md"]);
await runGit(repoRoot, ["commit", "-m", "Initial commit"]);
await runGit(repoRoot, ["worktree", "add", "-b", recordedBranch, worktreePath, "HEAD"]);
await runGit(worktreePath, ["checkout", "-b", actualBranch]);
const input = buildWorkspaceValidationInput();
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
resolvedWorkspace: buildResolvedWorkspace({ cwd: worktreePath }),
executionWorkspace: {
...input.executionWorkspace,
strategy: "git_worktree",
baseCwd: repoRoot,
cwd: worktreePath,
branchName: recordedBranch,
worktreePath,
},
persistedExecutionWorkspace: {
...input.persistedExecutionWorkspace!,
strategyType: "git_worktree",
cwd: worktreePath,
providerType: "git_worktree",
providerRef: worktreePath,
branchName: recordedBranch,
},
}),
"git_worktree_branch_mismatch",
`expected git worktree branch "${recordedBranch}"`,
);
} finally {
await fs.rm(repoRoot, { recursive: true, force: true });
await fs.rm(worktreeParent, { recursive: true, force: true });
}
});
it("rejects a workspace-linked issue when adapter cwd has no git metadata", async () => {
const input = buildWorkspaceValidationInput();
const cwd = "/tmp/paperclip-workspace-without-git-metadata";
await expectWorkspaceValidationFailure(
buildWorkspaceValidationInput({
resolvedWorkspace: buildResolvedWorkspace({ cwd }),
executionWorkspace: {
...input.executionWorkspace,
baseCwd: cwd,
cwd,
},
persistedExecutionWorkspace: {
...input.persistedExecutionWorkspace!,
cwd,
},
}),
"missing_git_metadata",
"has no .git metadata",
);
});
it("does not apply the git-sensitive workspace guard to non-local execution targets", async () => {
const input = buildWorkspaceValidationInput();
await expect(
assertGitSensitiveAdapterWorkspaceValid(
buildWorkspaceValidationInput({
executionTarget: { kind: "cloud" },
executionWorkspace: {
...input.executionWorkspace,
cwd: "/tmp/agent-fallback",
},
}),
),
).resolves.toBeUndefined();
});
});
describe("assertGitWorktreeBaseWorkspaceReady", () => {
it.each(["all_external", "mixed", "malformed"])("keeps materialization aggregation fail-closed: %s", async (kind) => {
const first = { schemaVersion: 1, provider: "git", operation: "clone", reason: "authentication_failed" } as const;
const second = kind === "all_external" ? { ...first, reason: "dns_failure" as const } :
kind === "malformed" ? { ...first, reason: "unknown" } : undefined;
const failures = [first, second].map((connectionFailure, index) => ({
projectWorkspaceId: `workspace-${index}`, repoUrl: "https://example.test/team/repo.git",
error: "Managed checkout failed", ...(connectionFailure ? { connectionFailure } : {}),
}));
const error = await assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: { id: "issue-1", identifier: "TEST-1", projectId: "project-1", projectWorkspaceId: "workspace-0" },
base: { baseCwd: "/tmp/unused-fallback", source: "project_primary", projectId: "project-1", workspaceId: "workspace-0", repoUrl: "https://example.test/team/repo.git", repoRef: null },
anchor: { baseCwdFallback: true, materializationFailures: failures as never },
}).catch((error: unknown) => error);
expect(error).toMatchObject({
code: "workspace_validation_failed",
resultJson: { workspaceValidation: { reason: "git_worktree_base_materialization_failed", materializationFailures: failures } },
});
expect((error as { resultJson: Record<string, unknown> }).resultJson.connectionFailure)
.toEqual(kind === "all_external" ? first : undefined);
});
it("rejects projectless isolated git worktrees that resolved to agent_home", async () => {
const fallbackCwd = resolveDefaultAgentWorkspaceDir("agent-1");
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: null,
projectWorkspaceId: null,
executionWorkspaceId: null,
executionWorkspacePreference: "isolated_workspace",
},
base: {
baseCwd: fallbackCwd,
source: "agent_home",
projectId: null,
workspaceId: null,
repoUrl: null,
repoRef: null,
},
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("needs a project / project workspace or a reusable execution workspace"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "git_worktree_base_agent_home",
issueId: "issue-1",
resolvedWorkspaceSource: "agent_home",
requestedExecutionWorkspaceMode: "isolated_workspace",
workspaceStrategyType: "git_worktree",
}),
},
});
});
it("rejects operator-branch git worktrees that resolved to agent_home", async () => {
const fallbackCwd = resolveDefaultAgentWorkspaceDir("agent-1");
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "operator_branch",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: null,
projectWorkspaceId: null,
},
base: {
baseCwd: fallbackCwd,
source: "agent_home",
projectId: null,
workspaceId: null,
repoUrl: null,
repoRef: null,
},
})).rejects.toMatchObject({
code: "workspace_validation_failed",
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "git_worktree_base_agent_home",
requestedExecutionWorkspaceMode: "operator_branch",
}),
},
});
});
it("rejects isolated git worktrees when the resolved base is not a git checkout", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-non-git-workspace-"));
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: "https://github.com/example/repo.git",
repoRef: "origin/master",
},
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("is not a git checkout"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "git_worktree_base_not_git_checkout",
issueId: "issue-1",
resolvedWorkspaceSource: "project_primary",
resolvedWorkspaceCwd: cwd,
}),
},
});
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
function configuredLocalPathInput(cwd: string): Parameters<typeof assertGitWorktreeBaseWorkspaceReady>[0] {
return {
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: { id: "issue-1", identifier: null, projectId: "project-1", projectWorkspaceId: "workspace-1" },
base: { baseCwd: cwd, source: "project_primary", projectId: "project-1", workspaceId: "workspace-1", repoUrl: null, repoRef: null },
anchor: { localPathOnlyWorkspace: true, baseCwdFallback: false, materializationFailures: [] },
};
}
it("marks a proven non-Git local path as an owner configuration conflict without changing the failure", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-local-path-policy-"));
try {
await expect(assertGitWorktreeBaseWorkspaceReady(configuredLocalPathInput(cwd))).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("is not a git checkout"),
resultJson: { workspaceValidation: {
reason: "git_worktree_base_not_git_checkout",
configurationReason: "local_path_requires_git_checkout",
} },
});
} finally { await fs.rm(cwd, { recursive: true, force: true }); }
});
it.each(["legacy", "repository", "fallback", "materialization", "session", "unbound"])(
"does not mark ambiguous non-Git workspace evidence as a configuration-only failure: %s", async (kind) => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-ambiguous-workspace-"));
try {
const input = configuredLocalPathInput(cwd);
if (kind === "legacy") input.anchor!.localPathOnlyWorkspace = undefined;
if (kind === "repository") input.base.repoUrl = "https://example.com/repository.git";
if (kind === "fallback") input.anchor!.baseCwdFallback = true;
if (kind === "materialization") input.anchor!.materializationFailures = [{ projectWorkspaceId: "another-workspace", repoUrl: null, error: "checkout failed" }];
if (kind === "session") input.base.source = "task_session";
if (kind === "unbound") input.base.workspaceId = null;
const error = await assertGitWorktreeBaseWorkspaceReady(input).catch((error) => error);
expect(error.code).toBe("workspace_validation_failed");
expect(error.resultJson.workspaceValidation.configurationReason).toBeUndefined();
} finally { await fs.rm(cwd, { recursive: true, force: true }); }
},
);
it.each(["missing_git", "permission", "git_error", "killed", "missing_directory"])(
"does not classify a Git or filesystem failure as an owner-only configuration mismatch: %s", async (kind) => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-git-probe-failure-"));
const bin = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-git-probe-bin-"));
try {
if (kind !== "missing_git" && kind !== "missing_directory") {
const command = kind === "killed"
? "kill -TERM $$"
: kind === "permission"
? "printf '%s\\n' 'fatal: cannot open .git: Permission denied' >&2; exit 128"
: "printf '%s\\n' 'fatal: bad object HEAD' >&2; exit 128";
await fs.writeFile(path.join(bin, "git"), `#!/bin/sh\n${command}\n`, { mode: 0o755 });
}
if (kind !== "missing_directory") vi.stubEnv("PATH", bin);
const input = configuredLocalPathInput(kind === "missing_directory" ? path.join(cwd, "absent") : cwd);
const error = await assertGitWorktreeBaseWorkspaceReady(input).catch((error) => error);
expect(error.code).toBe("workspace_validation_failed");
expect(error.resultJson.workspaceValidation.configurationReason).toBeUndefined();
} finally {
vi.unstubAllEnvs();
await fs.rm(cwd, { recursive: true, force: true });
await fs.rm(bin, { recursive: true, force: true });
}
},
);
it("still accepts a configured local path that is a Git checkout", async () => {
const cwd = await createGitCheckout({ withRemote: false });
try { await expect(assertGitWorktreeBaseWorkspaceReady(configuredLocalPathInput(cwd))).resolves.toBeUndefined(); }
finally { await fs.rm(cwd, { recursive: true, force: true }); }
});
it.each(["empty_metadata", "missing_head", "damaged_head", "dangling_metadata", "ancestor_metadata", "bare_metadata"])(
"keeps real broken repository metadata reportable even when Git says not a repository: %s", async (kind) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-broken-local-repo-"));
try {
let cwd = root;
if (kind === "missing_head" || kind === "damaged_head") {
await execFile("git", ["init", root]);
if (kind === "missing_head") await fs.rm(path.join(root, ".git", "HEAD"));
else await fs.writeFile(path.join(root, ".git", "HEAD"), "damaged HEAD\n");
} else if (kind === "dangling_metadata") {
await fs.symlink(path.join(root, "missing-git-dir"), path.join(root, ".git"));
} else if (kind === "bare_metadata") {
await fs.mkdir(path.join(root, "objects"));
} else {
await fs.mkdir(path.join(root, ".git"));
if (kind === "ancestor_metadata") {
cwd = path.join(root, "documents");
await fs.mkdir(cwd);
}
}
const gitFailure = await execFile("git", ["rev-parse", "--show-toplevel"], {
cwd, env: { ...process.env, LC_ALL: "C" },
}).catch((error) => error);
expect(gitFailure.code).toBe(128);
expect(gitFailure.stderr).toMatch(/^fatal: not a git repository/);
const failure = await assertGitWorktreeBaseWorkspaceReady(configuredLocalPathInput(cwd)).catch((error) => error);
expect(failure.code).toBe("workspace_validation_failed");
expect(failure.resultJson.workspaceValidation.configurationReason).toBeUndefined();
} finally { await fs.rm(root, { recursive: true, force: true }); }
},
);
it("rejects isolated git worktrees when the project workspace could not be materialized, even if the fallback cwd is a git checkout", async () => {
// The fallback agent-home dir being a git repo must not let the run proceed: it would be an
// unrelated repository, and the not-a-git-checkout probe would mask the real clone failure.
const cwd = await createGitCheckout({ withRemote: false });
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: "https://github.com/example/private-repo.git",
repoRef: "origin/master",
},
anchor: {
baseCwdFallback: true,
materializationFailures: [{
projectWorkspaceId: "workspace-1",
repoUrl: "https://github.com/example/private-repo.git",
error: 'Failed to prepare managed checkout for "https://github.com/example/private-repo.git": fatal: could not read Username',
}],
},
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("could not be prepared: Failed to prepare managed checkout"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "git_worktree_base_materialization_failed",
issueId: "issue-1",
baseCwdFallback: true,
materializationFailures: [expect.objectContaining({
projectWorkspaceId: "workspace-1",
error: expect.stringContaining("could not read Username"),
})],
}),
},
});
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("keeps the not-a-git-checkout reason for a fallback with no failed materialization attempt", async () => {
// A configured path that is simply unavailable is not a clone failure; the message must
// not steer the operator toward repairing clone access.
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-unavailable-path-fallback-"));
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: null,
repoRef: null,
},
anchor: { baseCwdFallback: true },
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("is not a git checkout"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "git_worktree_base_not_git_checkout",
}),
},
});
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("refuses a git-checkout fallback cwd that is not the project workspace, without claiming a clone failed", async () => {
const cwd = await createGitCheckout({ withRemote: false });
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: null,
repoRef: null,
},
anchor: { baseCwdFallback: true, materializationFailures: [] },
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("configured project workspace path is not available"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "git_worktree_base_fallback_not_project_workspace",
baseCwdFallback: true,
materializationFailures: [],
}),
},
});
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("allows isolated git worktrees when the anchor reports no fallback", async () => {
const cwd = await createGitCheckout({ withRemote: false });
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: "https://github.com/example/repo.git",
repoRef: "origin/master",
},
anchor: { baseCwdFallback: false, materializationFailures: [] },
})).resolves.toBeUndefined();
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("allows isolated git worktrees when the resolved base is a git checkout", async () => {
const cwd = await createGitCheckout({ withRemote: false });
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: "https://github.com/example/repo.git",
repoRef: "origin/master",
},
})).resolves.toBeUndefined();
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("does not require git for shared project-primary workspaces", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-shared-workspace-"));
try {
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "shared_workspace",
config: { workspaceStrategy: { type: "git_worktree" } },
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
},
base: {
baseCwd: cwd,
source: "project_primary",
projectId: "project-1",
workspaceId: "workspace-1",
repoUrl: null,
repoRef: null,
},
})).resolves.toBeUndefined();
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("allows isolated workspace with no explicit strategy type even when base is agent_home", async () => {
// No workspaceStrategy.type → realizeExecutionWorkspace defaults to project_primary (not git_worktree),
// so the guard must not fire. This prevents false workspace_validation_failed for configs that omit type.
const fallbackCwd = resolveDefaultAgentWorkspaceDir("agent-1");
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "isolated_workspace",
config: {},
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: null,
projectWorkspaceId: null,
},
base: {
baseCwd: fallbackCwd,
source: "agent_home",
projectId: null,
workspaceId: null,
repoUrl: null,
repoRef: null,
},
})).resolves.toBeUndefined();
});
it("allows operator-branch workspace with no explicit strategy type even when base is agent_home", async () => {
const fallbackCwd = resolveDefaultAgentWorkspaceDir("agent-1");
await expect(assertGitWorktreeBaseWorkspaceReady({
requestedExecutionWorkspaceMode: "operator_branch",
config: {},
issue: {
id: "issue-1",
identifier: "PAP-1",
projectId: null,
projectWorkspaceId: null,
},
base: {
baseCwd: fallbackCwd,
source: "agent_home",
projectId: null,
workspaceId: null,
repoUrl: null,
repoRef: null,
},
})).resolves.toBeUndefined();
});
});
describe("scrubGitCredentialText", () => {
it("masks URL userinfo including tokens", () => {
expect(scrubGitCredentialText(
'fatal: unable to access "https://x-access-token:ghp_abc123@github.com/example/repo.git"',
)).toBe('fatal: unable to access "https://***@github.com/example/repo.git"');
});
it("masks bare-username userinfo and multiple occurrences", () => {
expect(scrubGitCredentialText(
"clone https://alice@github.com/a.git then http://token@internal.example/b.git",
)).toBe("clone https://***@github.com/a.git then http://***@internal.example/b.git");
});
it("masks userinfo on non-HTTP schemes, leaving scp-style remotes alone", () => {
expect(scrubGitCredentialText(
"fatal: cannot clone ssh://deploy:hunter2@internal.example/repo.git or git+ssh://bob@host/x.git",
)).toBe("fatal: cannot clone ssh://***@internal.example/repo.git or git+ssh://***@host/x.git");
expect(scrubGitCredentialText("fetch from git@github.com:example/repo.git failed")).toBe(
"fetch from git@github.com:example/repo.git failed",
);
});
it("masks entire URL query strings regardless of parameter names", () => {
expect(scrubGitCredentialText(
"fatal: unable to access 'https://github.com/example/repo.git?access_token=ghs_secret&ref=main'",
)).toBe("fatal: unable to access 'https://github.com/example/repo.git?***'");
expect(scrubGitCredentialText(
"clone https://gitlab.example/repo.git?anything=glpat-123 failed",
)).toBe("clone https://gitlab.example/repo.git?*** failed");
});
it("leaves credential-free text unchanged", () => {
const text = 'Failed to clone "https://github.com/example/repo.git": exit code 128';
expect(scrubGitCredentialText(text)).toBe(text);
const plain = "git clone failed with exit code=128 at key step ref=main";
expect(scrubGitCredentialText(plain)).toBe(plain);
});
});
describe("buildAnchorFallbackWorkspaceNotes", () => {
it("reports materialization failures ahead of the generic no-cwd note", () => {
expect(buildAnchorFallbackWorkspaceNotes({
fallbackCwd: "/paperclip/workspaces/agent-1",
preferredWorkspaceWarning: null,
materializationFailures: [{
projectWorkspaceId: "workspace-1",
repoUrl: "https://github.com/example/private.git",
error: "fatal: could not read Username",
}],
missingProjectCwds: [],
hasConfiguredProjectCwd: false,
})).toEqual([
'Failed to prepare the project workspace checkout: fatal: could not read Username. Using fallback workspace "/paperclip/workspaces/agent-1" for this run.',
]);
});
it("counts additional failed candidates", () => {
const failure = {
projectWorkspaceId: "workspace-1",
repoUrl: null,
error: "clone timed out",
};
expect(buildAnchorFallbackWorkspaceNotes({
fallbackCwd: "/fallback",
preferredWorkspaceWarning: null,
materializationFailures: [failure, { ...failure, projectWorkspaceId: "workspace-2" }],
missingProjectCwds: [],
hasConfiguredProjectCwd: false,
})).toEqual([
'Failed to prepare the project workspace checkout (clone timed out), and 1 other candidate workspace(s) also failed. Using fallback workspace "/fallback" for this run.',
]);
});
it("preserves the existing missing-path and no-cwd notes when nothing failed to materialize", () => {
expect(buildAnchorFallbackWorkspaceNotes({
fallbackCwd: "/fallback",
preferredWorkspaceWarning: "Selected project workspace \"workspace-9\" is not available on this project.",
materializationFailures: [],
missingProjectCwds: ["/missing/path"],
hasConfiguredProjectCwd: true,
})).toEqual([
'Selected project workspace "workspace-9" is not available on this project.',
'Project workspace path "/missing/path" is not available yet. Using fallback workspace "/fallback" for this run.',
]);
expect(buildAnchorFallbackWorkspaceNotes({
fallbackCwd: "/fallback",
preferredWorkspaceWarning: null,
materializationFailures: [],
missingProjectCwds: [],
hasConfiguredProjectCwd: false,
})).toEqual([
'Project workspace has no local cwd configured. Using fallback workspace "/fallback" for this run.',
]);
});
it("emits both failure and missing-path notes when a project has both kinds of candidates", () => {
expect(buildAnchorFallbackWorkspaceNotes({
fallbackCwd: "/fallback",
preferredWorkspaceWarning: null,
materializationFailures: [{
projectWorkspaceId: "workspace-1",
repoUrl: "https://github.com/example/private.git",
error: "authentication failed",
}],
missingProjectCwds: ["/missing/path"],
hasConfiguredProjectCwd: true,
})).toEqual([
'Failed to prepare the project workspace checkout: authentication failed. Using fallback workspace "/fallback" for this run.',
'Project workspace path "/missing/path" is not available yet. Using fallback workspace "/fallback" for this run.',
]);
});
});
describe("assertPushCapabilityCheckoutValid", () => {
it("rejects a GitHub PR workflow checkout without a configured push remote", async () => {
const cwd = await createGitCheckout({ withRemote: false });
try {
await expect(assertPushCapabilityCheckoutValid({
enabled: true,
issue: {
id: "issue-1",
identifier: "PAP-1",
},
cwd,
})).rejects.toMatchObject({
code: "workspace_validation_failed",
message: expect.stringContaining("has no configured push remote"),
resultJson: {
workspaceValidation: expect.objectContaining({
reason: "missing_git_push_remote",
issueId: "issue-1",
executionWorkspaceCwd: cwd,
}),
},
});
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
it("allows a GitHub PR workflow checkout when a push remote is configured", async () => {
const cwd = await createGitCheckout({ withRemote: true });
try {
await expect(assertPushCapabilityCheckoutValid({
enabled: true,
issue: {
id: "issue-1",
identifier: "PAP-1",
},
cwd,
})).resolves.toBeUndefined();
} finally {
await fs.rm(cwd, { recursive: true, force: true });
}
});
});
describe("requiresPushCapabilityPreflight", () => {
it("only enables the guard when the issue explicitly mentions the GitHub PR workflow skill", () => {
expect(requiresPushCapabilityPreflight({
adapterType: "codex_local",
issueId: "issue-1",
explicitRunScopedSkillKeys: ["paperclipai/bundled/software-development/github-pr-workflow"],
})).toBe(true);
expect(requiresPushCapabilityPreflight({
adapterType: "codex_local",
issueId: "issue-1",
explicitRunScopedSkillKeys: [],
})).toBe(false);
expect(requiresPushCapabilityPreflight({
adapterType: "cursor-cloud",
issueId: "issue-1",
explicitRunScopedSkillKeys: ["paperclipai/bundled/software-development/github-pr-workflow"],
})).toBe(false);
});
});
describe("stripHostWorkspaceProvisionForLowTrustSandbox", () => {
it("removes only the host-side provision command for sandbox-backed low-trust runs", () => {
const config = {
workspaceStrategy: {
type: "git_worktree",
branchTemplate: "{{issue.identifier}}-{{slug}}",
provisionCommand: "bash ./scripts/provision-worktree.sh",
teardownCommand: "bash ./scripts/teardown-worktree.sh",
},
workspaceRuntime: {
services: [{ name: "web" }],
},
};
const result = stripHostWorkspaceProvisionForLowTrustSandbox({
config,
trustPreset: lowTrustResolution(),
selectedEnvironmentDriver: "sandbox",
});
expect(result).not.toBe(config);
expect(result.workspaceStrategy).toEqual({
type: "git_worktree",
branchTemplate: "{{issue.identifier}}-{{slug}}",
teardownCommand: "bash ./scripts/teardown-worktree.sh",
});
expect(result.workspaceRuntime).toBe(config.workspaceRuntime);
expect(config.workspaceStrategy.provisionCommand).toBe("bash ./scripts/provision-worktree.sh");
});
it("preserves provision commands for standard-trust runs", () => {
const config = {
workspaceStrategy: {
type: "git_worktree",
provisionCommand: "bash ./scripts/provision-worktree.sh",
},
};
expect(stripHostWorkspaceProvisionForLowTrustSandbox({
config,
trustPreset: standardTrustResolution(),
selectedEnvironmentDriver: "sandbox",
})).toBe(config);
});
it("preserves provision commands when a low-trust run is not sandbox-backed", () => {
const config = {
workspaceStrategy: {
type: "git_worktree",
provisionCommand: "bash ./scripts/provision-worktree.sh",
},
};
expect(stripHostWorkspaceProvisionForLowTrustSandbox({
config,
trustPreset: lowTrustResolution(),
selectedEnvironmentDriver: "local",
})).toBe(config);
});
});
describe("preflightLowTrustWorkspaceIsolation", () => {
it("fails non-sandbox low-trust runs before the caller reaches host workspace side effects", async () => {
let hostWorkspaceSideEffectReached = false;
await expect((async () => {
await preflightLowTrustWorkspaceIsolation({
trustPreset: lowTrustResolution(),
isolatedWorkspacesEnabled: true,
effectiveExecutionWorkspaceMode: "isolated_workspace",
issue: {
companyId: "company-1",
id: "issue-1",
projectId: "project-1",
},
resolveSelectedEnvironmentDriver: async () => "local",
});
hostWorkspaceSideEffectReached = true;
})()).rejects.toMatchObject({
status: 422,
details: expect.objectContaining({
code: "low_trust_requires_sandbox_environment",
}),
});
expect(hostWorkspaceSideEffectReached).toBe(false);
});
it("returns the sandbox driver for sandbox-backed low-trust runs", async () => {
await expect(preflightLowTrustWorkspaceIsolation({
trustPreset: lowTrustResolution(),
isolatedWorkspacesEnabled: true,
effectiveExecutionWorkspaceMode: "isolated_workspace",
issue: {
companyId: "company-1",
id: "issue-1",
projectId: "project-1",
},
resolveSelectedEnvironmentDriver: async () => "sandbox",
})).resolves.toBe("sandbox");
});
it("allows child issues inside a rootIssueId low-trust boundary during workspace preflight", async () => {
await expect(preflightLowTrustWorkspaceIsolation({
db: buildIssueAncestryDb([
{ id: "issue-child", companyId: "company-1", parentId: "issue-1" },
{ id: "issue-1", companyId: "company-1", parentId: null },
]) as any,
trustPreset: lowTrustResolution(),
isolatedWorkspacesEnabled: true,
effectiveExecutionWorkspaceMode: "isolated_workspace",
issue: {
companyId: "company-1",
id: "issue-child",
projectId: null,
},
resolveSelectedEnvironmentDriver: async () => "sandbox",
})).resolves.toBe("sandbox");
});
});
describe("resolveWorkspaceAfterLowTrustPreflight", () => {
it("fails non-sandbox low-trust runs before resolving workspaces", async () => {
let workspaceResolverReached = false;
await expect(resolveWorkspaceAfterLowTrustPreflight({
trustPreset: lowTrustResolution(),
isolatedWorkspacesEnabled: true,
effectiveExecutionWorkspaceMode: "isolated_workspace",
issue: {
companyId: "company-1",
id: "issue-1",
projectId: "project-1",
},
resolveSelectedEnvironmentDriver: async () => "local",
resolveWorkspace: async () => {
workspaceResolverReached = true;
return buildResolvedWorkspace();
},
})).rejects.toMatchObject({
status: 422,
details: expect.objectContaining({
code: "low_trust_requires_sandbox_environment",
}),
});
expect(workspaceResolverReached).toBe(false);
});
it("preserves standard-trust workspace resolution", async () => {
const workspace = buildResolvedWorkspace({ cwd: "/tmp/standard-workspace" });
await expect(resolveWorkspaceAfterLowTrustPreflight({
trustPreset: standardTrustResolution(),
isolatedWorkspacesEnabled: false,
effectiveExecutionWorkspaceMode: "shared_workspace",
issue: {
companyId: "company-1",
id: "issue-1",
projectId: "project-1",
},
resolveSelectedEnvironmentDriver: async () => {
throw new Error("standard trust should not inspect the environment driver");
},
resolveWorkspace: async () => workspace,
})).resolves.toEqual({
selectedEnvironmentDriver: null,
workspace,
});
});
});
describe("resolveRuntimeSessionParamsForWorkspace", () => {
it("keeps a legacy projectless Codex session in the default agent workspace", () => {
const agentId = "agent-projectless-legacy";
const fallbackCwd = resolveDefaultAgentWorkspaceDir(agentId);
const previousSessionParams = {
sessionId: "legacy-session-1",
cwd: fallbackCwd,
};
const result = resolveRuntimeSessionParamsForWorkspace({
agentId,
previousSessionParams,
resolvedWorkspace: buildResolvedWorkspace({
cwd: fallbackCwd,
source: "agent_home",
projectId: null,
workspaceId: null,
}),
});
expect(result).toEqual({
sessionParams: previousSessionParams,
warning: null,
});
});
it("migrates fallback workspace sessions to project workspace when project cwd becomes available", () => {
const agentId = "agent-123";
const fallbackCwd = resolveDefaultAgentWorkspaceDir(agentId);
const result = resolveRuntimeSessionParamsForWorkspace({
agentId,
previousSessionParams: {
sessionId: "session-1",
cwd: fallbackCwd,
workspaceId: "workspace-1",
},
resolvedWorkspace: buildResolvedWorkspace({ cwd: "/tmp/new-project-cwd" }),
});
expect(result.sessionParams).toMatchObject({
sessionId: "session-1",
cwd: "/tmp/new-project-cwd",
workspaceId: "workspace-1",
});
expect(result.warning).toContain("Attempting to resume session");
});
it("does not migrate when previous session cwd is not the fallback workspace", () => {
const result = resolveRuntimeSessionParamsForWorkspace({
agentId: "agent-123",
previousSessionParams: {
sessionId: "session-1",
cwd: "/tmp/some-other-cwd",
workspaceId: "workspace-1",
},
resolvedWorkspace: buildResolvedWorkspace({ cwd: "/tmp/new-project-cwd" }),
});
expect(result.sessionParams).toEqual({
sessionId: "session-1",
cwd: "/tmp/some-other-cwd",
workspaceId: "workspace-1",
});
expect(result.warning).toBeNull();
});
it("does not migrate when resolved workspace id differs from previous session workspace id", () => {
const agentId = "agent-123";
const fallbackCwd = resolveDefaultAgentWorkspaceDir(agentId);
const result = resolveRuntimeSessionParamsForWorkspace({
agentId,
previousSessionParams: {
sessionId: "session-1",
cwd: fallbackCwd,
workspaceId: "workspace-1",
},
resolvedWorkspace: buildResolvedWorkspace({
cwd: "/tmp/new-project-cwd",
workspaceId: "workspace-2",
}),
});
expect(result.sessionParams).toEqual({
sessionId: "session-1",
cwd: fallbackCwd,
workspaceId: "workspace-1",
});
expect(result.warning).toBeNull();
});
});
describe("applyPersistedExecutionWorkspaceConfig", () => {
it("does not add workspace runtime when only the project workspace had manual runtime config", () => {
const result = applyPersistedExecutionWorkspaceConfig({
config: {},
workspaceConfig: null,
mode: "isolated_workspace",
});
expect("workspaceRuntime" in result).toBe(false);
});
it("applies explicit persisted execution workspace runtime config when present", () => {
const result = applyPersistedExecutionWorkspaceConfig({
config: {},
workspaceConfig: {
provisionCommand: null,
teardownCommand: null,
cleanupCommand: null,
desiredState: null,
workspaceRuntime: {
services: [{ name: "workspace-web" }],
},
},
mode: "isolated_workspace",
});
expect(result.workspaceRuntime).toEqual({
services: [{ name: "workspace-web" }],
});
});
});
describe("mergeExecutionWorkspaceMetadataForPersistence", () => {
it("persists branch ownership independently of fresh worktree creation", () => {
const executionWorkspace = {
created: true,
branchCreatedByRuntime: false,
};
const metadata = mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: null,
source: "task_session",
createdByRuntime: resolveExecutionWorkspaceBranchOwnership(executionWorkspace),
strategyType: "git_worktree",
configSnapshot: null,
shouldReuseExisting: false,
baseRef: "origin/main",
baseRefSha: null,
});
expect(metadata.createdByRuntime).toBe(false);
expect(metadata.gitBranchOwnershipVersion).toBe(1);
});
it("does not downgrade recorded runtime ownership after worktree reuse", () => {
const executionWorkspace = {
created: false,
branchCreatedByRuntime: true,
};
const metadata = mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: { createdByRuntime: true },
source: "task_session",
createdByRuntime: resolveExecutionWorkspaceBranchOwnership(executionWorkspace),
strategyType: "git_worktree",
configSnapshot: null,
shouldReuseExisting: false,
baseRef: "origin/main",
baseRefSha: null,
});
expect(metadata.createdByRuntime).toBe(true);
expect(metadata.gitBranchOwnershipVersion).toBe(1);
});
it("merges config snapshot for newly realized workspaces", () => {
expect(mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: null,
source: "task_session",
createdByRuntime: true,
strategyType: "project_primary",
configSnapshot: {
environmentId: "env-new",
provisionCommand: "bash ./scripts/provision.sh",
},
shouldReuseExisting: false,
baseRef: null,
baseRefSha: null,
})).toEqual({
source: "task_session",
createdByRuntime: true,
config: {
environmentId: "env-new",
provisionCommand: "bash ./scripts/provision.sh",
runtimeProvisionCommand: null,
teardownCommand: null,
cleanupCommand: null,
desiredState: null,
serviceStates: null,
workspaceRuntime: null,
},
});
});
it("preserves persisted config snapshot when reusing an existing workspace", () => {
expect(mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: {
config: {
environmentId: "env-old",
provisionCommand: "bash ./scripts/existing-provision.sh",
},
},
source: "task_session",
createdByRuntime: false,
strategyType: "project_primary",
configSnapshot: {
environmentId: "env-new",
provisionCommand: "bash ./scripts/new-provision.sh",
},
shouldReuseExisting: true,
baseRef: null,
baseRefSha: null,
})).toEqual({
config: {
environmentId: "env-old",
provisionCommand: "bash ./scripts/existing-provision.sh",
},
source: "task_session",
createdByRuntime: false,
});
});
it("records the resolved base ref SHA for newly realized workspaces", () => {
expect(mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: null,
source: "task_session",
createdByRuntime: true,
strategyType: "project_primary",
configSnapshot: null,
shouldReuseExisting: false,
baseRef: "origin/main",
baseRefSha: "abc1234567890",
})).toEqual({
source: "task_session",
createdByRuntime: true,
baseRefSnapshot: {
baseRef: "origin/main",
resolvedSha: "abc1234567890",
},
});
});
});
type WorkspaceConfigMetadata = ReturnType<typeof buildEffectiveRunWorkspaceConfigMetadata>;
function buildWorkspaceConfigMetadata(
overrides: Partial<Parameters<typeof buildEffectiveRunWorkspaceConfigMetadata>[0]> = {},
) {
return buildEffectiveRunWorkspaceConfigMetadata({
mode: "isolated_workspace",
projectId: "project-1",
projectWorkspaceId: "workspace-1",
strategyType: "git_worktree",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/main",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
},
repoUrl: "https://github.com/example/repo.git",
repoRef: "origin/main",
configSnapshot: {
provisionCommand: "pnpm install",
teardownCommand: "pnpm stop",
cleanupCommand: "pnpm clean",
desiredState: "running",
serviceStates: { "0": "running" },
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev", port: 3100 }],
},
},
environment: {
selectedEnvironmentId: "environment-1",
driver: "local",
config: { provider: "local" },
},
realization: {
environmentDriver: "local",
environmentProvider: "local",
},
evaluatedAt: "2026-06-26T00:00:00.000Z",
...overrides,
});
}
function persistedWorkspaceConfigFingerprint(metadata: WorkspaceConfigMetadata) {
return {
configFingerprint: {
version: metadata.version,
workspaceHash: metadata.fingerprint,
categories: metadata.categories,
categoryFingerprints: metadata.categoryFingerprints,
lastEvaluatedAt: metadata.evaluatedAt,
},
};
}
describe("effective run execution workspace config freshness", () => {
it("reuses an existing workspace when the stored workspace fingerprint is unchanged", () => {
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(metadata),
nextMetadata: metadata,
});
expect(decision).toMatchObject({
action: "reuse",
shouldReuseExisting: true,
shouldRefreshConfigSnapshot: false,
changedCategories: [],
storedFingerprintPresent: true,
});
});
it("refreshes metadata and config for runtime-service-only drift without replacing the workspace", () => {
const base = buildWorkspaceConfigMetadata();
const next = buildWorkspaceConfigMetadata({
configSnapshot: {
provisionCommand: "pnpm install",
teardownCommand: "pnpm stop",
cleanupCommand: "pnpm clean",
desiredState: "running",
serviceStates: { "0": "running" },
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
},
});
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(base),
nextMetadata: next,
});
expect(decision).toMatchObject({
action: "refresh",
shouldReuseExisting: true,
shouldRefreshConfigSnapshot: true,
changedCategories: ["runtimeServices"],
});
const metadata = mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: {
config: {
workspaceRuntime: { services: [{ name: "web", command: "pnpm dev", port: 3100 }] },
},
...persistedWorkspaceConfigFingerprint(base),
},
source: "task_session",
createdByRuntime: false,
strategyType: "project_primary",
configSnapshot: {
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
desiredState: "running",
serviceStates: { "0": "running" },
},
shouldReuseExisting: true,
shouldRefreshConfigSnapshot: true,
workspaceConfigMetadata: next,
baseRef: "origin/main",
baseRefSha: "abc123",
});
expect(metadata?.config).toMatchObject({
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
});
expect(metadata?.configFingerprint).toMatchObject({
workspaceHash: next.fingerprint,
categories: next.categories,
});
});
it.each([
{
name: "mode",
category: "mode",
next: buildWorkspaceConfigMetadata({ mode: "shared_workspace" }),
},
{
name: "workspace strategy",
category: "strategy",
next: buildWorkspaceConfigMetadata({
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/main",
branchTemplate: "custom-{{issue.identifier}}",
worktreeParentDir: ".paperclip/worktrees",
},
}),
},
{
name: "project workspace",
category: "projectWorkspace",
next: buildWorkspaceConfigMetadata({ projectWorkspaceId: "workspace-2" }),
},
{
name: "base ref",
category: "repo",
next: buildWorkspaceConfigMetadata({
repoRef: "origin/release",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/release",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
},
}),
},
{
name: "environment realization",
category: "environment",
next: buildWorkspaceConfigMetadata({
environment: {
selectedEnvironmentId: "environment-2",
driver: "sandbox",
config: { provider: "daytona" },
},
realization: {
environmentDriver: "sandbox",
environmentProvider: "daytona",
},
}),
},
] as const)("replaces the workspace when $name changes", ({ category, next }) => {
const base = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(base),
nextMetadata: next,
});
expect(decision.action).toBe("replace");
expect(decision.shouldReuseExisting).toBe(false);
expect(decision.changedCategories).toContain(category);
});
it("keeps replacement-class drift visible when explicit reuse restores the old workspace", () => {
const base = buildWorkspaceConfigMetadata();
const next = buildWorkspaceConfigMetadata({
repoRef: "origin/release",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/release",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
},
configSnapshot: {
provisionCommand: "pnpm install --frozen-lockfile",
},
});
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(base),
nextMetadata: next,
});
const policy = resolveExecutionWorkspaceReuseProvisioningPolicy({
requestedShouldReuseExisting: true,
workspaceConfigFreshness: decision,
});
expect(decision.action).toBe("replace");
expect(policy).toEqual({
shouldRestoreExistingWorkspace: true,
shouldRefreshWorkspaceConfigSnapshot: false,
shouldPersistLatestWorkspaceConfigMetadata: false,
});
const metadata = mergeExecutionWorkspaceMetadataForPersistence({
existingMetadata: {
config: {
provisionCommand: "pnpm install",
},
...persistedWorkspaceConfigFingerprint(base),
},
source: "task_session",
createdByRuntime: false,
strategyType: "project_primary",
configSnapshot: {
provisionCommand: "pnpm install --frozen-lockfile",
},
shouldReuseExisting: policy.shouldRestoreExistingWorkspace,
shouldRefreshConfigSnapshot: policy.shouldRefreshWorkspaceConfigSnapshot,
workspaceConfigMetadata: policy.shouldPersistLatestWorkspaceConfigMetadata ? next : null,
baseRef: "origin/release",
baseRefSha: "release-sha",
});
expect(metadata?.config).toEqual({
provisionCommand: "pnpm install",
});
expect(metadata?.configFingerprint).toMatchObject({
workspaceHash: base.fingerprint,
categories: base.categories,
});
expect(metadata?.configFingerprint).not.toMatchObject({
workspaceHash: next.fingerprint,
});
});
it("fails loudly when explicit reuse restore errors", async () => {
const base = buildWorkspaceConfigMetadata();
const next = buildWorkspaceConfigMetadata({
repoRef: "origin/release",
workspaceStrategy: {
type: "git_worktree",
baseRef: "origin/release",
branchTemplate: "{{issue.identifier}}-{{slug}}",
worktreeParentDir: ".paperclip/worktrees",
},
});
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(base),
nextMetadata: next,
});
const realizeWorkspace = vi.fn(async () => ({ id: "fallback-workspace", warnings: [] }));
await expect(provisionExecutionWorkspaceForFreshnessDecision({
requestedShouldReuseExisting: true,
existingExecutionWorkspaceId: "workspace-old",
issueRef: { id: "issue-1", identifier: "PAP-42" },
runId: "run-1",
workspaceConfigFreshness: decision,
restoreExistingWorkspace: async () => {
throw new Error("restore command failed");
},
realizeWorkspace,
})).rejects.toThrow(/restore command failed/);
expect(realizeWorkspace).not.toHaveBeenCalled();
});
it.each([
{ name: "missing", status: null },
{ name: "archived", status: "archived" },
])("fails loudly when the inherited workspace row is $name", async ({ status }) => {
const reuseRequest = resolveExecutionWorkspaceReuseRequestForIssue({
issueExecutionWorkspaceId: "workspace-old",
issueExecutionWorkspacePreference: "reuse_existing",
existingExecutionWorkspaceStatus: status,
});
expect(reuseRequest).toEqual({
requestedExecutionWorkspaceId: "workspace-old",
requestedShouldReuseExisting: true,
existingExecutionWorkspaceAvailable: false,
});
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: reuseRequest.requestedShouldReuseExisting &&
reuseRequest.existingExecutionWorkspaceAvailable,
existingWorkspaceMetadata: null,
nextMetadata: metadata,
});
const realizeWorkspace = vi.fn(async () => ({ id: "fallback-workspace", warnings: [] }));
await expect(provisionExecutionWorkspaceForFreshnessDecision({
requestedShouldReuseExisting: reuseRequest.requestedShouldReuseExisting,
existingExecutionWorkspaceId: reuseRequest.requestedExecutionWorkspaceId,
issueRef: { id: "issue-1", identifier: "PAP-42" },
runId: "run-1",
workspaceConfigFreshness: decision,
restoreExistingWorkspace: reuseRequest.existingExecutionWorkspaceAvailable
? async () => ({ id: "workspace-old", warnings: [] })
: null,
realizeWorkspace,
})).rejects.toThrow(/could not be restored/);
expect(realizeWorkspace).not.toHaveBeenCalled();
});
it("does not mistake a projectless native run-id binding for a missing persisted workspace", () => {
expect(resolveNativeRecoveryExecutionWorkspaceBinding({
bindingId: "run-projectless",
persistedWorkspaceFound: false,
})).toBeNull();
expect(resolveNativeRecoveryExecutionWorkspaceBinding({
bindingId: "workspace-persisted",
persistedWorkspaceFound: true,
})).toBe("workspace-persisted");
});
it.each([
{ name: "a different branch", branchName: "PAP-9001-derived-child-branch" },
{ name: "no recorded branch", branchName: null },
])(
"realizes the pinned existing branch instead of reusing an inherited workspace on $name",
async ({ branchName }) => {
const reuseRequest = resolveExecutionWorkspaceReuseRequestForIssue({
issueExecutionWorkspaceId: "workspace-old",
issueExecutionWorkspacePreference: "reuse_existing",
existingExecutionWorkspaceStatus: "active",
requestedExistingBranch: "PAP-14380-salvage-pap-9514",
existingExecutionWorkspaceBranchName: branchName,
});
expect(reuseRequest).toEqual({
requestedExecutionWorkspaceId: "workspace-old",
requestedShouldReuseExisting: false,
existingExecutionWorkspaceAvailable: false,
});
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: false,
existingWorkspaceMetadata: null,
nextMetadata: metadata,
});
const realizeWorkspace = vi.fn(async () => ({ id: "pinned-branch-workspace", warnings: [] }));
const restoreExistingWorkspace = vi.fn(async () => ({ id: "workspace-old", warnings: [] }));
const result = await provisionExecutionWorkspaceForFreshnessDecision({
requestedShouldReuseExisting: reuseRequest.requestedShouldReuseExisting,
existingExecutionWorkspaceId: reuseRequest.requestedExecutionWorkspaceId,
issueRef: { id: "issue-1", identifier: "PAP-42" },
runId: "run-1",
workspaceConfigFreshness: decision,
restoreExistingWorkspace,
realizeWorkspace,
});
expect(result.executionWorkspace).toEqual({ id: "pinned-branch-workspace", warnings: [] });
expect(result.reusedExecutionWorkspace).toBeNull();
expect(restoreExistingWorkspace).not.toHaveBeenCalled();
},
);
it("keeps reusing an inherited workspace whose branch matches the pinned existing branch", () => {
expect(resolveExecutionWorkspaceReuseRequestForIssue({
issueExecutionWorkspaceId: "workspace-old",
issueExecutionWorkspacePreference: "reuse_existing",
existingExecutionWorkspaceStatus: "active",
requestedExistingBranch: "PAP-14380-salvage-pap-9514",
existingExecutionWorkspaceBranchName: "PAP-14380-salvage-pap-9514",
})).toEqual({
requestedExecutionWorkspaceId: "workspace-old",
requestedShouldReuseExisting: true,
existingExecutionWorkspaceAvailable: true,
});
});
it("fails loudly when explicit reuse restore returns no workspace", async () => {
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(metadata),
nextMetadata: metadata,
});
const realizeWorkspace = vi.fn(async () => ({ id: "fallback-workspace", warnings: [] }));
await expect(provisionExecutionWorkspaceForFreshnessDecision({
requestedShouldReuseExisting: true,
existingExecutionWorkspaceId: "workspace-old",
issueRef: { id: "issue-1", identifier: "PAP-42" },
runId: "run-1",
workspaceConfigFreshness: decision,
restoreExistingWorkspace: async () => null,
realizeWorkspace,
})).rejects.toThrow(/could not be restored/);
expect(realizeWorkspace).not.toHaveBeenCalled();
});
it("formats a safe workspace operation payload for config drift decisions", () => {
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(buildWorkspaceConfigMetadata()),
nextMetadata: buildWorkspaceConfigMetadata({
configSnapshot: {
workspaceRuntime: {
services: [{ name: "web", command: "pnpm dev -- --host 0.0.0.0", port: 3200 }],
},
},
}),
});
const operation = buildWorkspaceConfigFreshnessOperation({
decision,
hasExistingWorkspace: true,
reuseRequested: true,
workspaceReused: true,
configSnapshotRefreshed: true,
previousWorkspaceId: "workspace-old",
activeWorkspaceId: "workspace-old",
});
expect(operation).toMatchObject({
metadata: {
kind: "config_freshness",
action: "refresh",
changedCategories: ["lifecycleCommands", "runtimeServices"],
changedCategoryLabels: ["workspace lifecycle commands", "runtime services"],
reuseRequested: true,
workspaceReused: true,
configSnapshotRefreshed: true,
previousWorkspaceId: "workspace-old",
activeWorkspaceId: "workspace-old",
},
system: expect.stringContaining("refreshed execution workspace config"),
});
const serialized = JSON.stringify(operation);
expect(serialized).toContain("runtime services");
expect(serialized).not.toContain("pnpm dev");
expect(serialized).not.toContain("0.0.0.0");
});
it("does not record a freshness operation when an unchanged workspace is simply reused", () => {
const metadata = buildWorkspaceConfigMetadata();
const decision = resolveExecutionWorkspaceConfigFreshness({
hasExistingWorkspace: true,
existingWorkspaceMetadata: persistedWorkspaceConfigFingerprint(metadata),
nextMetadata: metadata,
});
expect(buildWorkspaceConfigFreshnessOperation({
decision,
hasExistingWorkspace: true,
reuseRequested: true,
workspaceReused: true,
configSnapshotRefreshed: false,
previousWorkspaceId: "workspace-1",
activeWorkspaceId: "workspace-1",
})).toBeNull();
});
});
describe("stripWorkspaceRuntimeFromExecutionRunConfig", () => {
it("removes workspace runtime before heartbeat execution", () => {
const input = {
cwd: "/tmp/project",
workspaceStrategy: {
type: "git_worktree",
},
workspaceRuntime: {
services: [{ name: "web" }],
},
};
const result = stripWorkspaceRuntimeFromExecutionRunConfig(input);
expect(result).toEqual({
cwd: "/tmp/project",
workspaceStrategy: {
type: "git_worktree",
},
});
expect(input.workspaceRuntime).toEqual({
services: [{ name: "web" }],
});
});
});
describe("shouldResetTaskSessionForWake", () => {
it("resets session context on assignment wake", () => {
expect(shouldResetTaskSessionForWake({ wakeReason: "issue_assigned" })).toBe(true);
});
it("preserves session context on execution review handoff wakes", () => {
expect(shouldResetTaskSessionForWake({ wakeReason: "execution_review_requested" })).toBe(false);
});
it("resets session context on execution approval wakes", () => {
expect(shouldResetTaskSessionForWake({ wakeReason: "execution_approval_requested" })).toBe(true);
});
it("preserves session context on execution changes-requested handoff wakes", () => {
expect(shouldResetTaskSessionForWake({ wakeReason: "execution_changes_requested" })).toBe(false);
});
it("preserves session context on timer heartbeats", () => {
expect(shouldResetTaskSessionForWake({ wakeSource: "timer" })).toBe(false);
});
it("preserves session context on manual on-demand invokes by default", () => {
expect(
shouldResetTaskSessionForWake({
wakeSource: "on_demand",
wakeTriggerDetail: "manual",
}),
).toBe(false);
});
it("resets session context when a fresh session is explicitly requested", () => {
expect(
shouldResetTaskSessionForWake({
wakeSource: "on_demand",
wakeTriggerDetail: "manual",
forceFreshSession: true,
}),
).toBe(true);
});
it("resets session context for accepted planning confirmations that refresh workspace selection", () => {
expect(
shouldResetTaskSessionForWake({
wakeReason: "issue_commented",
interactionKind: "request_confirmation",
interactionStatus: "accepted",
forceFreshSession: true,
workspaceRefreshReason: "accepted_plan_confirmation",
}),
).toBe(true);
});
it("does not reset session context on mention wake comment", () => {
expect(
shouldResetTaskSessionForWake({
wakeReason: "issue_comment_mentioned",
wakeCommentId: "comment-1",
}),
).toBe(false);
});
it("does not reset session context when commentId is present", () => {
expect(
shouldResetTaskSessionForWake({
wakeReason: "issue_commented",
commentId: "comment-2",
}),
).toBe(false);
});
it("does not reset for comment wakes", () => {
expect(shouldResetTaskSessionForWake({ wakeReason: "issue_commented" })).toBe(false);
});
it("does not reset when wake reason is missing", () => {
expect(shouldResetTaskSessionForWake({})).toBe(false);
});
it("does not reset session context on callback on-demand invokes", () => {
expect(
shouldResetTaskSessionForWake({
wakeSource: "on_demand",
wakeTriggerDetail: "callback",
}),
).toBe(false);
});
});
describe("shouldDeferFollowupWakeForSameIssue", () => {
it("defers a same-agent follow-up for mention-style comment wakes while a run is active", () => {
expect(
shouldDeferFollowupWakeForSameIssue({
activeRunStatus: "running",
isSameExecutionAgent: true,
wakeCommentId: "comment-1",
forceFreshSession: false,
}),
).toBe(true);
});
it("defers a same-agent follow-up when a fresh session is explicitly requested", () => {
expect(
shouldDeferFollowupWakeForSameIssue({
activeRunStatus: "running",
isSameExecutionAgent: true,
wakeCommentId: null,
forceFreshSession: true,
}),
).toBe(true);
});
it("does not defer when the existing run is only queued", () => {
expect(
shouldDeferFollowupWakeForSameIssue({
activeRunStatus: "queued",
isSameExecutionAgent: true,
wakeCommentId: null,
forceFreshSession: true,
}),
).toBe(false);
});
it("does not defer normal same-agent wakes without a comment or fresh-session request", () => {
expect(
shouldDeferFollowupWakeForSameIssue({
activeRunStatus: "running",
isSameExecutionAgent: true,
wakeCommentId: null,
forceFreshSession: false,
}),
).toBe(false);
});
});
describe("shouldResetTaskSessionForModelChange", () => {
it("resets when configured model differs from persisted session model", () => {
expect(
shouldResetTaskSessionForModelChange({
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "opencode/mimo-v2-pro-free",
},
}),
).toBe(true);
});
it("does not reset when models match", () => {
expect(
shouldResetTaskSessionForModelChange({
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
},
}),
).toBe(false);
});
it("does not reset when persisted session model is missing", () => {
expect(
shouldResetTaskSessionForModelChange({
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
},
}),
).toBe(false);
});
it("does not reset when configured model is missing", () => {
expect(
shouldResetTaskSessionForModelChange({
configuredModel: null,
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
},
}),
).toBe(false);
});
it("does not reset when task session params are missing", () => {
expect(
shouldResetTaskSessionForModelChange({
configuredModel: "gpt-5.4-mini",
taskSessionParams: null,
}),
).toBe(false);
});
});
type SessionConfigMetadata = Awaited<ReturnType<typeof buildEffectiveRunSessionConfigMetadata>>;
async function buildSessionConfigMetadata(
overrides: Partial<Parameters<typeof buildEffectiveRunSessionConfigMetadata>[0]> = {},
) {
return buildEffectiveRunSessionConfigMetadata({
adapterType: "codex_local",
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
env: {
OPENAI_API_KEY: "resolved-secret-value",
PLAIN_FLAG: "plain-value",
},
},
agentRuntimeConfig: {
heartbeat: {
maxConcurrentRuns: 1,
},
},
issueOverrides: null,
workspaceConfig: {
requestedMode: "agent_default",
effectiveMode: "agent_default",
projectConfigRevisionAt: "2026-06-01T00:00:00.000Z",
},
environment: {
selectionSource: "default",
selectedEnvironmentId: "environment-1",
selectedEnvironment: {
id: "environment-1",
driver: "local",
configRevisionAt: "2026-06-01T00:00:00.000Z",
},
},
environmentEnv: {
ENVIRONMENT_FLAG: "enabled",
},
projectEnv: {
PROJECT_FLAG: "enabled",
},
routineEnv: null,
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
secretKey: "openai-api-key",
version: 7,
provider: "local_encrypted",
outcome: "success",
},
],
runtimeSkills: [
{
key: "paperclip",
runtimeName: "paperclip",
source: "/tmp/paperclip/runtime-skills/paperclip",
versionId: null,
currentVersionId: "skill-version-1",
sourceStatus: "available",
missingDetail: null,
},
],
agentConfigRevision: {
id: "agent-config-revision-1",
changedKeys: ["adapterConfig"],
configRevisionAt: "2026-06-01T00:00:00.000Z",
},
...overrides,
});
}
function sessionParamsWithConfigMetadata(
metadata: SessionConfigMetadata,
configuredModel = "gpt-5.4-mini",
) {
return {
sessionId: "thread-1",
__paperclipConfiguredModel: configuredModel,
__paperclipConfigFingerprint: metadata.fingerprint,
__paperclipConfigFingerprintVersion: metadata.version,
__paperclipConfigCategories: metadata.categories,
__paperclipConfigCategoryFingerprints: metadata.categoryFingerprints,
};
}
describe("effective run session config freshness", () => {
it("resets legacy sessions after connection instructions change or disappear", async () => {
const first = await buildSessionConfigMetadata({ effectiveAdapterConfig: { paperclipConnectionInstructions: { text: "Use the handbook.", digest: "before" } } });
for (const instructions of [null, { text: "Use the updated handbook.", digest: "after" }]) {
const next = await buildSessionConfigMetadata({ effectiveAdapterConfig: { paperclipConnectionInstructions: instructions } });
expect(resolveTaskSessionConfigFreshness({ hasTaskSession: true, configuredModel: "gpt-5.4-mini", taskSessionParams: sessionParamsWithConfigMetadata(first), configMetadata: next }).reset).toBe(true);
}
});
it("reuses managed AI sessions across temporary credential homes while preserving configuration boundaries", async () => {
const config = (home: string) => ({
model: "gpt-5.4-mini",
approvalPolicy: "never",
managedAiConnection: { identity: "account-1:credential-generation-1" },
env: {
HOME: home,
XDG_CONFIG_HOME: path.join(home, "config"),
XDG_DATA_HOME: path.join(home, "data"),
CODEX_HOME: path.join(home, "provider"),
GROK_HOME: path.join(home, "provider"),
CLAUDE_CONFIG_DIR: path.join(home, "provider"),
CUSTOM_SETTING: "original",
},
});
const first = await buildSessionConfigMetadata({ effectiveAdapterConfig: config("/tmp/ai-first"), managedAiHome: "/tmp/ai-first" });
const nextConfig = config("/tmp/ai-next");
const next = await buildSessionConfigMetadata({ effectiveAdapterConfig: nextConfig, managedAiHome: "/tmp/ai-next" });
expect(next.fingerprint).toBe(first.fingerprint);
expect(nextConfig.env.HOME).toBe("/tmp/ai-next");
expect(resolveTaskSessionConfigFreshness({
hasTaskSession: true, configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(first), configMetadata: next,
}).reset).toBe(false);
for (const changed of [
{ ...nextConfig, model: "different-model" },
{ ...nextConfig, approvalPolicy: "on-request" },
{ ...nextConfig, managedAiConnection: { identity: "account-2:credential-generation-1" } },
{ ...nextConfig, managedAiConnection: { identity: "account-1:credential-generation-2" } },
{ ...nextConfig, env: { ...nextConfig.env, CUSTOM_SETTING: "changed" } },
{ ...nextConfig, env: { ...nextConfig.env, CODEX_HOME: "/custom/provider" } },
]) {
const metadata = await buildSessionConfigMetadata({ effectiveAdapterConfig: changed, managedAiHome: "/tmp/ai-next" });
expect(metadata.fingerprint).not.toBe(first.fingerprint);
}
const unmanagedFirst = await buildSessionConfigMetadata({ effectiveAdapterConfig: config("/custom/first") });
const unmanagedNext = await buildSessionConfigMetadata({ effectiveAdapterConfig: config("/custom/next") });
expect(unmanagedFirst.fingerprint).not.toBe(unmanagedNext.fingerprint);
});
it("resets when effective adapter config changes after model/profile/env resolution", async () => {
const base = await buildSessionConfigMetadata();
const next = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
approvalPolicy: "never",
},
});
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: next,
});
expect(decision).toMatchObject({
reset: true,
changedCategories: ["adapterConfig"],
});
expect(decision.reasons.join("\n")).toContain("adapter config");
});
it("does not reset for issue comment timestamps but still resets for workspace settings", async () => {
const base = await buildSessionConfigMetadata({
workspaceConfig: {
requestedMode: "agent_default",
effectiveMode: "agent_default",
issueConfigRevisionAt: "2026-06-01T00:00:00.000Z",
issueSettings: null,
},
});
const commentOnly = await buildSessionConfigMetadata({
workspaceConfig: {
requestedMode: "agent_default",
effectiveMode: "agent_default",
issueConfigRevisionAt: "2026-06-01T00:05:00.000Z",
issueSettings: null,
},
});
const workspaceChanged = await buildSessionConfigMetadata({
workspaceConfig: {
requestedMode: "isolated_workspace",
effectiveMode: "isolated_workspace",
issueConfigRevisionAt: "2026-06-01T00:05:00.000Z",
issueSettings: { mode: "isolated_workspace" },
},
});
expect(
resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: commentOnly,
}),
).toMatchObject({
reset: false,
changedCategories: [],
reasons: [],
});
expect(
resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: workspaceChanged,
}),
).toMatchObject({
reset: true,
changedCategories: ["workspaceConfig"],
});
});
it("does not reset when a reusable execution workspace becomes realized", async () => {
const base = await buildSessionConfigMetadata({
workspaceConfig: {
requestedMode: "shared_workspace",
effectiveMode: "shared_workspace",
reusableExecutionWorkspaceConfig: null,
existingExecutionWorkspace: null,
},
});
const realized = await buildSessionConfigMetadata({
workspaceConfig: {
requestedMode: "shared_workspace",
effectiveMode: "shared_workspace",
reusableExecutionWorkspaceConfig: {
strategyType: "project_primary",
workspaceGeneration: 1,
},
existingExecutionWorkspace: {
id: "workspace-realized-after-first-turn",
mode: "shared_workspace",
strategyType: "project_primary",
},
},
});
expect(
resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: realized,
}),
).toMatchObject({
reset: false,
changedCategories: [],
reasons: [],
});
});
it("keeps model-only compatibility as an additional reset reason", async () => {
const base = await buildSessionConfigMetadata();
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base, "opencode/mimo-v2-pro-free"),
configMetadata: base,
});
expect(decision.reset).toBe(true);
expect(decision.reasons).toEqual([
'configured model changed from "opencode/mimo-v2-pro-free" to "gpt-5.4-mini"',
]);
});
it("freshens legacy task sessions that lack versioned config metadata", async () => {
const metadata = await buildSessionConfigMetadata();
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
},
configMetadata: metadata,
});
expect(decision.reset).toBe(true);
expect(decision.changedCategories).toEqual(metadata.categories);
expect(decision.reasons).toEqual(["effective run configuration fingerprint metadata is missing"]);
});
it("uses persisted fingerprint metadata even when an adapter codec omits it from resume params", async () => {
const metadata = await buildSessionConfigMetadata();
const persistedParams = sessionParamsWithConfigMetadata(metadata);
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: persistedParams,
configMetadata: metadata,
});
expect(decision.reset).toBe(false);
expect(decision.reasons).toEqual([]);
});
it("preserves legacy metadata gaps only for active accepted-plan continuation sessions", async () => {
const metadata = await buildSessionConfigMetadata();
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: {
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
},
configMetadata: metadata,
preserveLegacySessionWithoutConfigMetadata: true,
});
expect(decision.reset).toBe(false);
expect(decision.changedCategories).toEqual([]);
expect(decision.reasons).toEqual([]);
});
it("names safe categories for issue override, env, secret, and runtime skill drift", async () => {
const base = await buildSessionConfigMetadata();
const cases: Array<{
name: string;
category: string;
metadata: SessionConfigMetadata;
}> = [
{
name: "issue overrides",
category: "issueOverrides",
metadata: await buildSessionConfigMetadata({
issueOverrides: {
adapterConfig: {
reasoningEffort: "high",
},
},
}),
},
{
name: "project env bindings",
category: "envBindings",
metadata: await buildSessionConfigMetadata({
projectEnv: {
PROJECT_FLAG: "enabled",
NEW_PROJECT_FLAG: "present",
},
}),
},
{
name: "secret version",
category: "secrets",
metadata: await buildSessionConfigMetadata({
secretManifest: [
{
configPath: "env.OPENAI_API_KEY",
envKey: "OPENAI_API_KEY",
secretId: "secret-1",
bindingId: "binding-1",
secretKey: "openai-api-key",
version: 8,
provider: "local_encrypted",
outcome: "success",
},
],
}),
},
{
name: "runtime skills",
category: "runtimeSkills",
metadata: await buildSessionConfigMetadata({
runtimeSkills: [
{
key: "paperclip",
runtimeName: "paperclip",
source: "/tmp/paperclip/runtime-skills/paperclip",
versionId: null,
currentVersionId: "skill-version-2",
sourceStatus: "available",
missingDetail: null,
},
],
}),
},
];
for (const testCase of cases) {
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: testCase.metadata,
});
expect(decision.reset, testCase.name).toBe(true);
expect(decision.changedCategories, testCase.name).toContain(testCase.category);
}
});
it("detects instructions content drift without storing the contents", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-session-fingerprint-"));
const instructionsPath = path.join(root, "AGENTS.md");
await fs.writeFile(instructionsPath, "Version one instructions.\n", "utf8");
const base = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
instructionsBundleMode: "managed",
instructionsRootPath: root,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: instructionsPath,
},
});
await fs.writeFile(instructionsPath, "Version two instructions.\n", "utf8");
const next = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
instructionsBundleMode: "managed",
instructionsRootPath: root,
instructionsEntryFile: "AGENTS.md",
instructionsFilePath: instructionsPath,
},
});
const decision = resolveTaskSessionConfigFreshness({
hasTaskSession: true,
configuredModel: "gpt-5.4-mini",
taskSessionParams: sessionParamsWithConfigMetadata(base),
configMetadata: next,
});
expect(decision.reset).toBe(true);
expect(decision.changedCategories).toContain("instructions");
expect(next.fingerprints.sessionFingerprint.canonicalJson).not.toContain("Version two instructions");
});
it("does not read unbounded legacy instructions paths for config fingerprints", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-session-fingerprint-"));
const instructionsPath = path.join(root, "AGENTS.md");
await fs.writeFile(instructionsPath, "Legacy direct-path instructions.\n", "utf8");
const metadata = await buildSessionConfigMetadata({
effectiveAdapterConfig: {
command: "codex",
model: "gpt-5.4-mini",
instructionsFilePath: instructionsPath,
},
});
const canonical = metadata.fingerprints.sessionFingerprint.canonicalJson;
expect(canonical).toContain("missing_absolute_root");
expect(canonical).not.toContain("Legacy direct-path instructions");
expect(canonical).not.toContain("contentHash");
});
it("does not include raw secret or plain env values in canonical session metadata", async () => {
const metadata = await buildSessionConfigMetadata();
const canonical = metadata.fingerprints.sessionFingerprint.canonicalJson;
expect(canonical).toContain("secret-1");
expect(canonical).toContain('"version":7');
expect(canonical).not.toContain("resolved-secret-value");
expect(canonical).not.toContain("plain-value");
expect(canonical).not.toContain("enabled");
expect(canonical).not.toContain("openai-api-key");
});
});
describe("stripConfiguredModelFromSessionParams", () => {
it("removes the internal model key from persisted session params", () => {
expect(
stripConfiguredModelFromSessionParams({
sessionId: "thread-1",
__paperclipConfiguredModel: "gpt-5.4-mini",
}),
).toEqual({ sessionId: "thread-1" });
});
it("returns null when session params are missing", () => {
expect(stripConfiguredModelFromSessionParams(null)).toBeNull();
expect(stripConfiguredModelFromSessionParams(undefined)).toBeNull();
});
it("returns a copy without mutating the input", () => {
const input = { sessionId: "thread-1", __paperclipConfiguredModel: "gpt-5.4-mini" };
const result = stripConfiguredModelFromSessionParams(input);
expect(result).not.toBe(input);
expect(input.__paperclipConfiguredModel).toBe("gpt-5.4-mini");
});
it("returns an empty object when only the internal model key is present (caller must normalize)", () => {
const stripped = stripConfiguredModelFromSessionParams({
__paperclipConfiguredModel: "gpt-5.4-mini",
});
expect(stripped).toEqual({});
// Callers that forward params to adapters must normalize {} back to null so
// the pre-PR null contract is preserved (adapters distinguishing {} from null).
expect(normalizeSessionParams(stripped)).toBeNull();
});
});
describe("stripPaperclipSessionMetadataFromSessionParams", () => {
it("removes all internal Paperclip session metadata before adapter invocation", () => {
expect(
stripPaperclipSessionMetadataFromSessionParams({
sessionId: "thread-1",
cwd: "/tmp/project",
paperclipAiCredentialIdentity: "grant:user:generation",
__paperclipConfiguredModel: "gpt-5.4-mini",
__paperclipConfigFingerprint: "v1:sha256:abc",
__paperclipConfigFingerprintVersion: 1,
__paperclipConfigCategories: ["adapterConfig"],
__paperclipConfigCategoryFingerprints: { adapterConfig: "v1:sha256:def" },
}),
).toEqual({
sessionId: "thread-1",
cwd: "/tmp/project",
});
});
});
describe("isTaskSessionCredentialCompatible", () => {
it("retains the server-owned identity even when the Codex codec drops it", () => {
const saved = { sessionId: "thread-1", paperclipAiCredentialIdentity: "grant:user:generation" };
const decoded = codexSessionCodec.deserialize(saved);
expect(decoded).toEqual({ sessionId: "thread-1" });
expect(isTaskSessionCredentialCompatible(saved, "grant:user:generation")).toBe(true);
});
it.each([
undefined,
null,
{},
{ paperclipAiCredentialIdentity: "other-grant:user:generation" },
{ paperclipAiCredentialIdentity: "grant:other-user:generation" },
{ paperclipAiCredentialIdentity: "grant:user:new-generation" },
])("requires the same saved grant, responsible user, and credential generation: %j", (saved) => {
expect(isTaskSessionCredentialCompatible(saved, "grant:user:generation")).toBe(false);
});
it("preserves unmanaged session behavior", () => {
expect(isTaskSessionCredentialCompatible({ sessionId: "thread-1" }, undefined)).toBe(true);
});
});
describe("normalizeSessionParams", () => {
it("collapses an empty object to null", () => {
expect(normalizeSessionParams({})).toBeNull();
});
it("returns null for null or undefined inputs", () => {
expect(normalizeSessionParams(null)).toBeNull();
expect(normalizeSessionParams(undefined)).toBeNull();
});
it("preserves a non-empty object", () => {
const params = { sessionId: "thread-1" };
expect(normalizeSessionParams(params)).toBe(params);
});
});
describe("deriveTaskKeyWithHeartbeatFallback", () => {
it("returns explicit taskKey when present", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ taskKey: "issue-123" }, null)).toBe("issue-123");
});
it("returns explicit issueId when no taskKey", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ issueId: "issue-456" }, null)).toBe("issue-456");
});
it("returns __heartbeat__ for timer wakes with no explicit key", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ wakeSource: "timer" }, null)).toBe("__heartbeat__");
});
it("prefers explicit key over heartbeat fallback even on timer wakes", () => {
expect(
deriveTaskKeyWithHeartbeatFallback({ wakeSource: "timer", taskKey: "issue-789" }, null),
).toBe("issue-789");
});
it("returns null for non-timer wakes with no explicit key", () => {
expect(deriveTaskKeyWithHeartbeatFallback({ wakeSource: "on_demand" }, null)).toBeNull();
});
it("returns null for empty context", () => {
expect(deriveTaskKeyWithHeartbeatFallback({}, null)).toBeNull();
});
});
describe("comment wake batching", () => {
it("updates the latest comment when coalescing queued follow-up wakes", () => {
const merged = mergeCoalescedContextSnapshot(
{
issueId: "issue-1",
wakeReason: "issue_commented",
wakeCommentId: "comment-1",
wakeCommentIds: ["comment-1"],
paperclipWake: {
latestCommentId: "comment-1",
},
},
{
issueId: "issue-1",
wakeReason: "issue_commented",
wakeCommentId: "comment-2",
},
);
expect(merged.wakeCommentIds).toEqual(["comment-1", "comment-2"]);
expect(merged.commentId).toBe("comment-2");
expect(merged.wakeCommentId).toBe("comment-2");
expect(merged.paperclipWake).toBeUndefined();
});
it("keeps forceFreshSession sticky once any coalesced wake requests it", () => {
const merged = mergeCoalescedContextSnapshot(
{
issueId: "issue-1",
forceFreshSession: true,
},
{
issueId: "issue-1",
forceFreshSession: false,
},
);
expect(merged.forceFreshSession).toBe(true);
});
it("keeps connection tool refresh intent while allowing harness session recovery", () => {
const merged = mergeCoalescedContextSnapshot(
{ issueId: "issue-1", wakeReason: "issue_commented", refreshTools: true },
{ issueId: "issue-1", wakeReason: "issue_commented", refreshTools: false },
);
expect(merged.refreshTools).toBe(true);
expect(shouldResetTaskSessionForWake(merged)).toBe(false);
});
});
describe("buildExplicitResumeSessionOverride", () => {
it("reuses saved task session params when they belong to the selected failed run", () => {
const result = buildExplicitResumeSessionOverride({
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: "session-before",
resumeRunSessionIdAfter: "session-after",
taskSession: {
sessionParamsJson: {
sessionId: "session-after",
cwd: "/tmp/project",
},
sessionDisplayId: "session-after",
lastRunId: "run-1",
},
sessionCodec: codexSessionCodec,
});
expect(result).toEqual({
sessionDisplayId: "session-after",
sessionParams: {
sessionId: "session-after",
cwd: "/tmp/project",
},
});
});
it("falls back to the selected run session id when no matching task session params are available", () => {
const result = buildExplicitResumeSessionOverride({
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: "session-before",
resumeRunSessionIdAfter: "session-after",
taskSession: {
sessionParamsJson: {
sessionId: "other-session",
cwd: "/tmp/project",
},
sessionDisplayId: "other-session",
lastRunId: "run-2",
},
sessionCodec: codexSessionCodec,
});
expect(result).toEqual({
sessionDisplayId: "session-after",
sessionParams: {
sessionId: "session-after",
},
});
});
it("does not synthesize Hermes resume params from a truncated display id", () => {
const result = buildExplicitResumeSessionOverride({
adapterType: "hermes_local",
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: null,
resumeRunSessionIdAfter: "20260601_141558_",
taskSession: {
sessionParamsJson: {
sessionId: "20260601_141000_c861e4",
},
sessionDisplayId: "20260601_141000_",
lastRunId: "run-2",
},
sessionCodec: truncatingHermesSessionCodec,
});
expect(result).toBeNull();
});
it("uses validated Hermes run result params before truncated display ids", () => {
const result = buildExplicitResumeSessionOverride({
adapterType: "hermes_local",
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: null,
resumeRunSessionIdAfter: "20260601_141558_",
resumeRunSessionParams: {
sessionId: "20260601_141558_c861e4",
},
taskSession: null,
sessionCodec: truncatingHermesSessionCodec,
});
expect(result).toEqual({
sessionDisplayId: "20260601_141558_c861e4",
sessionParams: {
sessionId: "20260601_141558_c861e4",
},
});
});
it("keeps Hermes run result params and display id together when falling back from a prior session", () => {
const result = buildExplicitResumeSessionOverride({
adapterType: "hermes_local",
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: "20260601_140000_old123",
resumeRunSessionIdAfter: "20260601_141558_",
resumeRunSessionParams: {
sessionId: "20260601_141558_c861e4",
},
taskSession: null,
sessionCodec: truncatingHermesSessionCodec,
});
expect(result).toEqual({
sessionDisplayId: "20260601_141558_c861e4",
sessionParams: {
sessionId: "20260601_141558_c861e4",
},
});
});
it("ignores invalid Hermes run result params", () => {
const result = buildExplicitResumeSessionOverride({
adapterType: "hermes_local",
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: null,
resumeRunSessionIdAfter: "20260601_141558_",
resumeRunSessionParams: {
sessionId: "from",
},
taskSession: null,
sessionCodec: truncatingHermesSessionCodec,
});
expect(result).toBeNull();
});
it("keeps full Hermes task-session params even when the saved display id is truncated", () => {
const result = buildExplicitResumeSessionOverride({
adapterType: "hermes_local",
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: null,
resumeRunSessionIdAfter: "20260601_141558_",
taskSession: {
sessionParamsJson: {
sessionId: "20260601_141558_c861e4",
},
sessionDisplayId: "20260601_141558_",
lastRunId: "run-1",
},
sessionCodec: truncatingHermesSessionCodec,
});
expect(result).toEqual({
sessionDisplayId: "20260601_141558_c861e4",
sessionParams: {
sessionId: "20260601_141558_c861e4",
},
});
});
it("falls back from a poisoned Hermes session-after value to a valid session-before value", () => {
const result = buildExplicitResumeSessionOverride({
adapterType: "hermes_local",
resumeFromRunId: "run-1",
resumeRunSessionIdBefore: "20260601_141558_c861e4",
resumeRunSessionIdAfter: "from",
taskSession: null,
sessionCodec: hermesSessionCodec,
});
expect(result).toEqual({
sessionDisplayId: "20260601_141558_c861e4",
sessionParams: {
sessionId: "20260601_141558_c861e4",
},
});
});
});
describe("resolveNextSessionState", () => {
it("preserves previous valid Hermes session state when failed adapter output reports prose tokens", () => {
const result = resolveNextSessionState({
adapterType: "hermes_local",
codec: truncatingHermesSessionCodec,
adapterResult: {
exitCode: 1,
signal: null,
timedOut: false,
sessionParams: {
sessionId: "from",
},
sessionId: "from",
sessionDisplayId: "from",
errorMessage: "Session not found: 20260601_141558_",
},
outcome: "failed",
previousParams: {
sessionId: "20260601_141558_c861e4",
},
previousDisplayId: "20260601_141558_c861e4",
previousLegacySessionId: "20260601_141558_c861e4",
});
expect(result).toEqual({
params: {
sessionId: "20260601_141558_c861e4",
},
displayId: "20260601_141558_c861e4",
legacySessionId: "20260601_141558_c861e4",
});
});
it("drops poisoned previous Hermes session state instead of passing it to the next run", () => {
const result = resolveNextSessionState({
adapterType: "hermes_local",
codec: truncatingHermesSessionCodec,
adapterResult: {
exitCode: 1,
signal: null,
timedOut: false,
sessionId: "from",
sessionDisplayId: "from",
errorMessage: "Session not found: from",
},
outcome: "failed",
previousParams: {
sessionId: "from",
},
previousDisplayId: "from",
previousLegacySessionId: "from",
});
expect(result).toEqual({
params: null,
displayId: null,
legacySessionId: null,
});
});
it("derives Hermes display state from canonical params instead of adapter-truncated display ids", () => {
const result = resolveNextSessionState({
adapterType: "hermes_local",
codec: truncatingHermesSessionCodec,
adapterResult: {
exitCode: 0,
signal: null,
timedOut: false,
sessionParams: {
sessionId: "20260601_141558_c861e4",
},
sessionDisplayId: "20260601_141558_",
},
outcome: "succeeded",
previousParams: null,
previousDisplayId: null,
previousLegacySessionId: null,
});
expect(result).toEqual({
params: {
sessionId: "20260601_141558_c861e4",
},
displayId: "20260601_141558_c861e4",
legacySessionId: "20260601_141558_c861e4",
});
});
it("uses one canonical Hermes explicit session candidate instead of mixing valid and invalid fields", () => {
const result = resolveNextSessionState({
adapterType: "hermes_local",
codec: truncatingHermesSessionCodec,
adapterResult: {
exitCode: 0,
signal: null,
timedOut: false,
sessionParams: {
sessionId: "from",
},
sessionId: "20260601_141558_c861e4",
sessionDisplayId: "20260601_141558_",
},
outcome: "succeeded",
previousParams: {
sessionId: "20260601_140000_previous",
},
previousDisplayId: "20260601_140000_previous",
previousLegacySessionId: "20260601_140000_previous",
});
expect(result).toEqual({
params: {
sessionId: "20260601_141558_c861e4",
},
displayId: "20260601_141558_c861e4",
legacySessionId: "20260601_141558_c861e4",
});
});
it("keeps non-Hermes arbitrary session ids unchanged", () => {
const result = resolveNextSessionState({
adapterType: "codex_local",
codec: codexSessionCodec,
adapterResult: {
exitCode: 1,
signal: null,
timedOut: false,
sessionId: "from",
},
outcome: "failed",
previousParams: null,
previousDisplayId: null,
previousLegacySessionId: null,
});
expect(result.legacySessionId).toBe("from");
});
});
describe("formatRuntimeWorkspaceWarningLog", () => {
it("emits informational workspace warnings on stdout", () => {
expect(formatRuntimeWorkspaceWarningLog("Using fallback workspace")).toEqual({
stream: "stdout",
chunk: "[paperclip] Using fallback workspace\n",
});
});
});
describe("prioritizeProjectWorkspaceCandidatesForRun", () => {
it("moves the explicitly selected workspace to the front", () => {
const rows = [
{ id: "workspace-1", cwd: "/tmp/one" },
{ id: "workspace-2", cwd: "/tmp/two" },
{ id: "workspace-3", cwd: "/tmp/three" },
];
expect(
prioritizeProjectWorkspaceCandidatesForRun(rows, "workspace-2").map((row) => row.id),
).toEqual(["workspace-2", "workspace-1", "workspace-3"]);
});
it("keeps the original order when no preferred workspace is selected", () => {
const rows = [
{ id: "workspace-1" },
{ id: "workspace-2" },
];
expect(
prioritizeProjectWorkspaceCandidatesForRun(rows, null).map((row) => row.id),
).toEqual(["workspace-1", "workspace-2"]);
});
it("keeps the original order when the selected workspace is missing", () => {
const rows = [
{ id: "workspace-1" },
{ id: "workspace-2" },
];
expect(
prioritizeProjectWorkspaceCandidatesForRun(rows, "workspace-9").map((row) => row.id),
).toEqual(["workspace-1", "workspace-2"]);
});
});
describe("parseSessionCompactionPolicy", () => {
it("disables Paperclip-managed rotation by default for codex and claude local", () => {
expect(parseSessionCompactionPolicy(buildAgent("codex_local"))).toEqual({
enabled: true,
maxSessionRuns: 0,
maxRawInputTokens: 0,
maxSessionAgeHours: 0,
});
expect(parseSessionCompactionPolicy(buildAgent("claude_local"))).toEqual({
enabled: true,
maxSessionRuns: 0,
maxRawInputTokens: 0,
maxSessionAgeHours: 0,
});
});
it("keeps conservative defaults for adapters without confirmed native compaction", () => {
expect(parseSessionCompactionPolicy(buildAgent("cursor"))).toEqual({
enabled: true,
maxSessionRuns: 200,
maxRawInputTokens: 2_000_000,
maxSessionAgeHours: 72,
});
expect(parseSessionCompactionPolicy(buildAgent("opencode_local"))).toEqual({
enabled: true,
maxSessionRuns: 200,
maxRawInputTokens: 2_000_000,
maxSessionAgeHours: 72,
});
});
it("lets explicit agent overrides win over adapter defaults", () => {
expect(
parseSessionCompactionPolicy(
buildAgent("codex_local", {
heartbeat: {
sessionCompaction: {
maxSessionRuns: 25,
maxRawInputTokens: 500_000,
},
},
}),
),
).toEqual({
enabled: true,
maxSessionRuns: 25,
maxRawInputTokens: 500_000,
maxSessionAgeHours: 0,
});
});
});
describe("isWorkspaceSyncConflictFailure", () => {
it("matches the git workspace reconciliation failure signatures", () => {
expect(isWorkspaceSyncConflictFailure(
"Failed to merge concurrent remote git histories for a5d46a8005b3 and c1042c11774a: Command failed: git merge-tree --write-tree",
)).toBe(true);
expect(isWorkspaceSyncConflictFailure(
"Failed to integrate concurrent remote git history for a5d46a8005b3 after multiple retries.",
)).toBe(true);
expect(isWorkspaceSyncConflictFailure(
"error: /tmp/restore/git-delta.bundle did not send all necessary objects",
)).toBe(true);
expect(isWorkspaceSyncConflictFailure(
"error: Repository lacks these prerequisite commits: 4c631700",
)).toBe(true);
});
it("ignores unrelated adapter failures", () => {
expect(isWorkspaceSyncConflictFailure("Codex exited with code 2")).toBe(false);
expect(isWorkspaceSyncConflictFailure("no Codex credentials provisioned for managed home")).toBe(false);
expect(isWorkspaceSyncConflictFailure(null)).toBe(false);
expect(isWorkspaceSyncConflictFailure("")).toBe(false);
});
});
describe("reconcileReusedExecutionWorkspaceProjectWorkspaceId", () => {
it("backfills a null existing binding from the resolved value", () => {
expect(
reconcileReusedExecutionWorkspaceProjectWorkspaceId(null, "resolved-workspace"),
).toBe("resolved-workspace");
});
it("never overwrites an existing binding, even when a resolved value is present", () => {
expect(
reconcileReusedExecutionWorkspaceProjectWorkspaceId("existing-workspace", "resolved-workspace"),
).toBe("existing-workspace");
});
it("returns null when both existing and resolved are absent", () => {
expect(reconcileReusedExecutionWorkspaceProjectWorkspaceId(null, null)).toBeNull();
expect(reconcileReusedExecutionWorkspaceProjectWorkspaceId(undefined, undefined)).toBeNull();
expect(reconcileReusedExecutionWorkspaceProjectWorkspaceId(null, undefined)).toBeNull();
});
it("backfills when existing is undefined and resolved is present", () => {
expect(
reconcileReusedExecutionWorkspaceProjectWorkspaceId(undefined, "resolved-workspace"),
).toBe("resolved-workspace");
});
});