test(recovery): fix race condition in workspace validation comment check (#8294)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The heartbeat service manages agent run lifecycles, including
workspace validation before launching git-sensitive local adapters
> - When workspace validation fails, `escalateStrandedAssignedIssue` in
the recovery service sets the issue to `"blocked"` and then
asynchronously posts a system comment
> - A test added in PR #7644 verifies the comment appears, but queried
the database immediately after `waitForValue` resolved the issue status
— creating a race between the status write and the comment write
> - The test was passing before PR #8284 (pnpm lockfile refresh) but
began failing after, suggesting the lockfile update shifted async
scheduling enough to expose the window
> - This pull request wraps the comment check in `waitForValue`,
consistent with two identical polling patterns already used in the same
test file (lines 1159-1162 and 1232-1235)
> - The benefit is a consistently-passing test that unblocks dependabot
PR #8155 from auto-merging via CI

## Linked Issues or Issue Description

Refs PAP-72. Unblocks PAP-68 (dependabot PR #8155 which has auto-merge
enabled and was blocked by this test failure).

## What Changed

- `server/src/__tests__/heartbeat-process-recovery.test.ts`: replaced
direct `db.select()` + `expect(comments.some(...))` with a
`waitForValue` polling loop that waits until a comment containing
`"workspace failed validation"` appears, then asserts it is truthy
- No production code changed

## Verification

The specific test `blocks a git-sensitive local adapter before launch
when a project-workspace-linked issue is missing its project id`
previously failed with:

```
AssertionError: expected false to be true
src/__tests__/heartbeat-process-recovery.test.ts:1429:94
```

After this fix: the test polls with a 3-second timeout and returns once
the comment row exists. The comment IS written by
`escalateStrandedAssignedIssue` — this was purely a timing issue in the
test assertion. CI on this PR serves as the confirmation run.

## Risks

Low risk — test-only change. No production code is modified. The
`waitForValue` helper is already used 10+ times in the same test file
with the same 3-second timeout; this use follows the identical pattern.

## Model Used

- Provider: Anthropic
- Model: Claude Sonnet 4.6 (`claude-sonnet-4-6`)
- Context window: 200k tokens
- Tool use: yes (file read, bash, edit tools)
- Reasoning mode: extended thinking

## 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
- [ ] I have run tests locally and they pass (CI will verify)
- [x] I have added or updated tests where applicable (this IS the test
fix)
- [x] If this change affects the UI, I have included before/after
screenshots (N/A)
- [x] I have updated relevant documentation to reflect my changes (N/A)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI run)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(in progress)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky Leach
2026-06-18 14:52:56 -07:00
committed by GitHub
parent 8a062be94d
commit 6cfc6731d9
@@ -1425,8 +1425,11 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
});
expect(recoveryAction?.nextAction).toContain("Repair the source issue workspace link");
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId));
expect(comments.some((comment) => comment.body.includes("workspace failed validation"))).toBe(true);
const validationComment = await waitForValue(async () => {
const rows = await db.select().from(issueComments).where(eq(issueComments.issueId, issueId));
return rows.find((comment) => comment.body.includes("workspace failed validation")) ?? null;
});
expect(validationComment).toBeTruthy();
});
it("queues one finish-handoff wake when a successful run leaves in-progress work without a next action", async () => {