From dd7ff9530a01173d1170227fceb24d7224ae932d Mon Sep 17 00:00:00 2001 From: Greg Jackson Date: Fri, 13 Mar 2026 20:17:28 +0000 Subject: [PATCH] fix: improve error messages in claimWork and heartbeat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claimWork now includes who claimed the intent when rejecting a second claim ("already claimed by pawel" instead of "not open"). heartbeat now distinguishes non-existent claims from non-active ones, surfacing the actual status. Also tightened two existing completeClaim test assertions to match on specific status (completed/abandoned) rather than the generic "not active" — no code change to completeClaim itself. All 68 tests pass against real PostgreSQL. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/db/queries.ts | 18 ++++++++++++++++-- tests/edge-cases.test.ts | 12 ++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/db/queries.ts b/src/db/queries.ts index ce5da8e..a1aaa80 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -257,7 +257,17 @@ export async function claimWork(params: ClaimWorkParams): Promise<{ claim: Claim const intentRes = await query(`SELECT * FROM intents WHERE id = $1`, [params.intent_id]); const intent = intentRes.rows[0]; if (!intent) throw new Error(`Intent ${params.intent_id} not found`); - if (intent.status !== 'open') throw new Error(`Intent ${params.intent_id} is not open (current: ${intent.status})`); + if (intent.status !== 'open') { + if (intent.status === 'claimed') { + const activeClaim = await query( + `SELECT claimed_by FROM claims WHERE intent_id = $1 AND status = 'active' LIMIT 1`, + [params.intent_id] + ); + const claimedBy = activeClaim.rows[0]?.claimed_by; + throw new Error(`Intent ${params.intent_id} is already claimed by ${claimedBy ?? 'unknown'}`); + } + throw new Error(`Intent ${params.intent_id} is not open (current: ${intent.status})`); + } // Create the claim const claimRes = await query( @@ -299,7 +309,11 @@ export async function heartbeat(claimId: string, filesTouching?: string[]): Prom `UPDATE claims SET ${sets.join(', ')} WHERE id = $${paramIdx} AND status = 'active' RETURNING *`, [...values, claimId] ); - if (res.rows.length === 0) throw new Error(`Active claim ${claimId} not found`); + if (res.rows.length === 0) { + const existing = await query(`SELECT status FROM claims WHERE id = $1`, [claimId]); + if (existing.rows.length === 0) throw new Error(`Claim ${claimId} not found`); + throw new Error(`Claim ${claimId} is not active (current: ${existing.rows[0].status})`); + } return res.rows[0]; } diff --git a/tests/edge-cases.test.ts b/tests/edge-cases.test.ts index 9e30b95..52422e4 100644 --- a/tests/edge-cases.test.ts +++ b/tests/edge-cases.test.ts @@ -62,13 +62,13 @@ describe('Edge Cases & Error Paths', () => { ).rejects.toThrow('not found'); }); - it('claim rejects already-claimed intent', async () => { + it('claim rejects already-claimed intent with claimer info', async () => { const intent = await seedOpenIntent(); await db.claimWork({ intent_id: intent.id as string, claimed_by: 'pawel' }); await expect( db.claimWork({ intent_id: intent.id as string, claimed_by: 'alice' }) - ).rejects.toThrow('not open'); + ).rejects.toThrow('claimed by pawel'); }); it('claim rejects done intent', async () => { @@ -90,7 +90,7 @@ describe('Edge Cases & Error Paths', () => { const { claim } = await db.claimWork({ intent_id: intent.id as string, claimed_by: 'pawel' }); await db.completeClaim(claim.id); - await expect(db.completeClaim(claim.id)).rejects.toThrow('not active'); + await expect(db.completeClaim(claim.id)).rejects.toThrow('completed'); }); it('complete rejects abandoned claim', async () => { @@ -98,7 +98,7 @@ describe('Edge Cases & Error Paths', () => { const { claim } = await db.claimWork({ intent_id: intent.id as string, claimed_by: 'pawel' }); await db.releaseClaim(claim.id); - await expect(db.completeClaim(claim.id)).rejects.toThrow('not active'); + await expect(db.completeClaim(claim.id)).rejects.toThrow('abandoned'); }); it('release rejects non-existent claim', async () => { @@ -109,12 +109,12 @@ describe('Edge Cases & Error Paths', () => { await expect(db.heartbeat('claim_nonexistent')).rejects.toThrow('not found'); }); - it('heartbeat rejects completed claim', async () => { + it('heartbeat rejects completed claim with status', async () => { const intent = await seedOpenIntent(); const { claim } = await db.claimWork({ intent_id: intent.id as string, claimed_by: 'pawel' }); await db.completeClaim(claim.id); - await expect(db.heartbeat(claim.id)).rejects.toThrow('not found'); + await expect(db.heartbeat(claim.id)).rejects.toThrow('completed'); }); // ─── Conflict Edge Cases ────────────────────────