test(recovery): align retained allocation fixture with readiness (#18984)

## Thinking Path

> - Paperclip manages AI agents and their work.
> - Restart recovery must preserve the original remote allocation and
its files.
> - #18934 readies that same retained allocation before preparation.
> - The older #18936 failure fixture rejects every readiness call.
> - It stops before the intended preparation-failure boundary.
> - This change updates only that fixture and checks the original
allocation identity.

## Linked Issues or Issue Description

**What happened?**

The three remote preparation retention cases fail after #18934. Their
driver stub throws `Reattachment must not resume the allocation`. The
production code now calls readiness on the same validated lease before
preparation, so the test never reaches its intended execution failure.

**Expected behavior**

The fixture should allow readiness of the original allocation. It must
reject replacement acquisition and retain its preparation-error,
ownership, saved-file and retention checks.

**Steps to reproduce**

Run `src/__tests__/heartbeat-native-remote-failure-retention.test.ts` on
master `86814c6550`. All three cases stop at the stale readiness stub.

Refs #18934 and #18936. This one-file baseline correction is split from
#18970 so it can unblock the shared failure also observed on #18977. It
carries no core API change or evaluation source pin.

## What Changed

- Return the original lease from the fixture readiness stub.
- Assert exactly one readiness call for the original company,
environment, run, workspace, provider allocation and remote path.
- Assert that no new allocation is acquired. Keep all existing
preparation failure, ownership and retained-work assertions.

## Verification

- Before #18934, the three existing cases passed locally. On the actual
merged #18934 contract, they reproduced the three CI failures in 18.06
seconds.
- With this fixture correction, all three passed in 11.83 seconds.
Production runtime code and the original 60-second case deadline are
unchanged.
- The corrected test blob and its relevant production/helper blobs match
the already-tested merged-source proof. Current master still has the
same stale test blob; this isolated PR applies only the identical
fixture correction.
- Command from `server/`: `node ../node_modules/vitest/vitest.mjs run
--config vitest.config.ts
src/__tests__/heartbeat-native-remote-failure-retention.test.ts`.
- `git diff --check` passes. Full repository typecheck/build were not
rerun for this one-file fixture split; fresh normal CI and review are
pending.
- [Original failing CI
job](https://github.com/paperclipai/paperclip/actions/runs/41855440307/job/126665077718).
It remains failed; no remote retry was used as proof.

## Risks

- Low scope: test fixture only. A readiness call for a replacement or
wrong allocation fails the identity assertions.
- This does not alter production recovery, permissions, cleanup policy,
evaluation deadlines or grading.

## Model Used

OpenAI `gpt-6.1-sol`, with reasoning, repository inspection, code
editing and tool execution. The host did not expose the model
context-window limit.

## Checklist


- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <[email protected]>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-04 10:15:31 -06:00
1 parent 86814c6550
commit 04a24f4e06
1 file changed
+13 -3
@@ -11,7 +11,7 @@ import { appendHeartbeatRunEvent } from "../services/heartbeat-run-events.js";
import { issueRecoveryActionService } from "../services/issue-recovery-actions.js";
import { pinFailedObservedWorkspace } from "../services/native-runtime/native-observed-workspace-preservation.js";
import { heartbeatService } from "../services/heartbeat.js";
import { environmentRuntimeService, classifyEnvironmentCapabilities } from "../services/environment-runtime.js";
import { environmentRuntimeService, classifyEnvironmentCapabilities, type EnvironmentDriverResumeInput } from "../services/environment-runtime.js";
import { instanceSettingsService } from "../services/instance-settings.js";
import { buildNativeExecutionInput } from "../services/native-runtime/native-execution-input.js";
import { nativeRuntimeContextFixture } from "../services/native-runtime/runtime-context.test-fixture.js";
@@ -107,9 +107,11 @@ describeEmbeddedPostgres("heartbeat native remote preparation failure retention"
expect(await pinFailedObservedWorkspace(db, companyId, f.runId)).toBe(false);
throw preparationFailure;
});
const acquireAllocation = vi.fn(async () => { throw new Error("Unexpected new allocation"); });
const readyAllocation = vi.fn(async ({ lease }: EnvironmentDriverResumeInput) => lease);
const runtime = environmentRuntimeService(db, { drivers: [{ driver: "sandbox",
acquireRunLease: async () => { throw new Error("Unexpected new allocation"); },
resumeRunLease: async () => { throw new Error("Reattachment must not resume the allocation"); },
acquireRunLease: acquireAllocation,
resumeRunLease: readyAllocation,
releaseRunLease: async () => { throw new Error("Unexpected destructive cleanup"); },
retryPendingSandboxTeardown: async () => { throw new Error("Stop must remain separately verified"); },
realizeWorkspace: async () => { throw new Error("Unexpected workspace realization"); },
@@ -124,6 +126,14 @@ describeEmbeddedPostgres("heartbeat native remote preparation failure retention"
nativeSessionBackendFactory: () => { throw new Error("No provider backend may be created"); } });
await heartbeat.recoverNativeRunsAfterRestart(new Date(), [f.runId]);
await heartbeat.drainActiveRunExecutions();
expect(readyAllocation).toHaveBeenCalledExactlyOnceWith({
environment: expect.objectContaining({ id: environmentId }),
lease: expect.objectContaining({ id: leaseId, companyId, environmentId,
executionWorkspaceId: workspaceId, heartbeatRunId: f.runId, providerLeaseId,
status: "active", leasePolicy: "ephemeral" }),
requestedCwd: remoteCwd,
});
expect(acquireAllocation).not.toHaveBeenCalled();
expect(resumed).toHaveBeenCalledOnce();
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
expect(run.error).toBe(preparationFailure.message);