1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Store } from "../src/store/db.ts";
// `claimSession` is the lock behind the endpoints that start work on a session:
// `POST /:id/feedback` (which enqueues a pipeline) and `POST /:id/close` (which
// files regression fixtures into the shared agent library and deletes the tmp
// tree). Both used to read the status, check it, then write โ safe today only
// because both handlers are fully synchronous.
//
// The property under test is that the check and the write cannot come apart:
// whatever happens between two callers, at most one of them is told it won. That
// is what lets a handler treat "claim returned true" as permission to do
// non-idempotent work.
const USER = 4242;
function withStore(fn: (store: Store) => void): void {
const dir = mkdtempSync(join(tmpdir(), "iris-claim-"));
try {
fn(new Store(join(dir, "iris.sqlite")));
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
// A session parked in `ready_for_review` โ the state both endpoints claim from.
function ready(store: Store, id = "ses_1"): string {
store.createSession({ session_id: id, github_user_id: USER, image_count: 1, iterations_max: 3 });
store.updateSession(id, { status: "ready_for_review" });
return id;
}
test("a claim from the expected status wins and applies the patch", () => {
withStore((store) => {
const id = ready(store);
// `phase` is deliberately NOT "extraction" here: createSession inserts that
// value, so patching to it would assert nothing โ the row would read
// "extraction" whether or not the patch was applied. "review" is a value only
// this claim could have written. (The feedback route does patch "extraction";
// what that route sets is its own concern, not this method's.)
assert.equal(store.claimSession(id, "ready_for_review", { status: "queued", phase: "review" }), true);
const s = store.getSession(id)!;
assert.equal(s.status, "queued");
assert.equal(s.phase, "review");
});
});
test("only the first of two claims wins", () => {
withStore((store) => {
const id = ready(store);
// The second call is what a concurrent caller does after the first one has
// landed โ the interleaving that matters, without needing real concurrency:
// the guard is the WHERE clause, so a serialized second attempt exercises it
// exactly as a raced one does.
const first = store.claimSession(id, "ready_for_review", { status: "queued", phase: "extraction" });
const second = store.claimSession(id, "ready_for_review", { status: "queued", phase: "extraction" });
assert.equal(first, true);
assert.equal(second, false, "two callers both got permission to start a run");
});
});
test("a losing claim leaves the winner's state untouched", () => {
withStore((store) => {
const id = ready(store);
assert.equal(store.claimSession(id, "ready_for_review", { status: "closed" }), true);
// A close and a feedback re-run racing: the re-run must not resurrect a
// closed session into `queued`, which would enqueue a pipeline writing over
// an accepted output.
assert.equal(store.claimSession(id, "ready_for_review", { status: "queued", phase: "extraction" }), false);
assert.equal(store.getSession(id)!.status, "closed");
});
});
test("a claim from the wrong status changes nothing", () => {
withStore((store) => {
const id = "ses_running";
store.createSession({ session_id: id, github_user_id: USER, image_count: 1, iterations_max: 3 });
store.updateSession(id, { status: "running", phase: "extraction" });
assert.equal(store.claimSession(id, "ready_for_review", { status: "queued", phase: "extraction" }), false);
const s = store.getSession(id)!;
assert.equal(s.status, "running");
assert.equal(s.phase, "extraction", "the patch was applied despite the claim failing");
});
});
test("a claim on a session that does not exist fails rather than throwing", () => {
withStore((store) => {
assert.equal(store.claimSession("ses_nope", "ready_for_review", { status: "queued" }), false);
});
});
test("an empty patch does not report a win", () => {
withStore((store) => {
// There is no UPDATE to build from zero keys, so there is nothing to serialize
// on. Returning true would hand out a claim no statement enforced โ every
// caller would "win". False is the honest answer; `updateSession` likewise
// treats an empty patch as a no-op.
const id = ready(store);
assert.equal(store.claimSession(id, "ready_for_review", {}), false);
assert.equal(store.getSession(id)!.status, "ready_for_review");
});
});
test("the claim stamps updated_at", () => {
withStore((store) => {
const id = ready(store);
// Backdate it first, and compare with a STRICT `>`. Written as
// `updated_at >= before` this asserted nothing: createSession already wrote a
// current timestamp, so dropping the stamp from the claim's UPDATE left that
// valid value in place and the test still passed. Forging an old value is what
// makes the comparison a measurement โ and it has to be forged, because two
// writes in the same millisecond are equal, not increasing.
(store as unknown as { db: { prepare(s: string): { run(...a: unknown[]): unknown } } }).db
.prepare(`UPDATE sessions SET updated_at = ? WHERE session_id = ?`)
.run("2020-01-01T00:00:00.000Z", id);
const before = store.getSession(id)!.updated_at;
// Same contract as updateSession: a client polling GET /:id uses updated_at to
// tell a progressing run from a stuck one, so the lock must not be a silent
// write.
assert.equal(store.claimSession(id, "ready_for_review", { status: "queued" }), true);
const after = store.getSession(id)!.updated_at;
assert.ok(after > before, `updated_at was not stamped (${before} -> ${after})`);
assert.ok(/^\d{4}-\d\d-\d\dT[\d:.]{9,}Z$/.test(after), `not an ISO timestamp: ${after}`);
});
});