fix(state): serialize work-item claims and preserve existing ownership

This commit is contained in:
haelyra committed 2026-10-11 02:08:19 -04:00
1 parent 6233b648f9
commit e611cf66d0
4 files changed
+46 -5

No files matched your search

@@ -68,6 +68,12 @@ function claimWorkItem(store, { id, owner, assigneeKind, sessionId, status } = {
if (!isOpenStatus(target.status)) {
throw new Error(`Work item ${target.id} is already done; cannot claim.`);
}
if (target.owner) {
if (target.owner !== owner) {
throw new Error(`Work item ${target.id} is already owned by ${target.owner}; cannot claim.`);
}
return { claimed: true, item: target };
}
const metadata = { ...(target.metadata || {}) };
if (kind) {
metadata.assigneeKind = kind;
+2 -2
View File
@@ -399,13 +399,13 @@ function claimWorkItemCli(store, options) {
if (assigneeKind && assigneeKind !== 'agent' && assigneeKind !== 'human') {
throw new Error("--as must be 'agent' or 'human'.");
}
return claimWorkItem(store, {
return store._database.transaction(() => claimWorkItem(store, {
id: resolveWorkItemId(options),
owner: options.owner,
assigneeKind,
sessionId: options.sessionId,
status: options.status
});
}))();
}
async function main() {
+6
View File
@@ -698,6 +698,12 @@ async function runTests() {
assert.strictEqual(claim.item.status, 'running');
assert.strictEqual(claim.item.metadata.assigneeKind, 'human');
const repeated = await post(app, 'wi-1/claim', { owner: 'alice', as: 'agent' }).then(r => r.json());
assert.deepStrictEqual(repeated.item, claim.item, 'same-owner retries must preserve the original claim');
const takeover = await post(app, 'wi-1/claim', { owner: 'bob' });
assert.strictEqual(takeover.status, 400, 'another owner must be rejected');
assert.match(JSON.stringify(await takeover.json()), /already owned/i);
const move = await post(app, 'wi-2/move', { lane: 'blocked' }).then(r => r.json());
assert.strictEqual(move.ok, true);
assert.strictEqual(move.item.status, 'blocked');
+32 -3
View File
@@ -7,7 +7,7 @@ const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const { spawnSync, spawn } = require('child_process');
const { createStateStore } = require('../../scripts/lib/state-store');
@@ -122,14 +122,43 @@ async function run() {
await test('claim of a specific id works and rejects a missing id', async () => {
const owned = runClaim(dbPath, ['wi-owned', '--owner', 'carol']);
assert.strictEqual(owned.status, 0, owned.stderr);
assert.strictEqual(JSON.parse(owned.stdout).item.owner, 'carol', 'explicit id can be re-claimed');
assert.notStrictEqual(owned.status, 0, 'an explicit id must not replace another owner');
assert.match(owned.stderr, /already owned/i);
const store = await createStateStore({ dbPath });
try { assert.strictEqual(store.getWorkItemById('wi-owned').owner, 'codex'); } finally { store.close(); }
const missing = runClaim(dbPath, ['nope-404', '--owner', 'carol']);
assert.notStrictEqual(missing.status, 0, 'missing id should fail');
assert.ok(/not found/i.test(missing.stderr), 'reports not found');
});
await test('repeat claim by the same owner is idempotent', async () => {
const before = await createStateStore({ dbPath });
let original;
try { original = before.getWorkItemById('wi-owned'); } finally { before.close(); }
const repeated = runClaim(dbPath, ['wi-owned', '--owner', original.owner, '--as', 'human']);
assert.strictEqual(repeated.status, 0, repeated.stderr);
assert.deepStrictEqual(JSON.parse(repeated.stdout).item, original);
});
await test('concurrent explicit claims have one winner and preserve its owner', async () => {
const setup = await createStateStore({ dbPath });
try { setup.upsertWorkItem({ id: 'wi-race', source: 'manual', title: 'Race', status: 'open', owner: null }); } finally { setup.close(); }
const results = await Promise.all(['left', 'right'].map(owner => new Promise((resolve, reject) => {
const child = spawn(process.execPath, [CLI, 'claim', 'wi-race', '--owner', owner, '--db', dbPath, '--json']);
let stdout = ''; let stderr = '';
child.stdout.on('data', chunk => { stdout += chunk; });
child.stderr.on('data', chunk => { stderr += chunk; });
child.on('error', reject);
child.on('close', status => resolve({ owner, status, stdout, stderr }));
})));
const winners = results.filter(result => result.status === 0);
assert.strictEqual(winners.length, 1, JSON.stringify(results));
assert.match(results.find(result => result.status !== 0).stderr, /already owned/i);
const store = await createStateStore({ dbPath });
try { assert.strictEqual(store.getWorkItemById('wi-race').owner, winners[0].owner); } finally { store.close(); }
});
await test('claim requires --owner and validates --as', async () => {
const noOwner = runClaim(dbPath, ['wi-done']);
assert.notStrictEqual(noOwner.status, 0);