@axiom-lattice/pg-stores 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.d.mts +283 -23
  4. package/dist/index.d.ts +283 -23
  5. package/dist/index.js +1867 -224
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1854 -217
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/ChannelBindingStore.test.ts +122 -0
  11. package/src/__tests__/PostgreSQLCapabilityBundleStore.migrations.test.ts +67 -0
  12. package/src/__tests__/PostgreSQLCapabilityBundleStore.test.ts +383 -0
  13. package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +16 -0
  14. package/src/__tests__/PostgreSQLProjectBotMembershipStore.test.ts +117 -0
  15. package/src/__tests__/PostgreSQLProjectMembershipStore.test.ts +235 -0
  16. package/src/__tests__/PostgreSQLProjectRoomMessageStore.test.ts +103 -0
  17. package/src/__tests__/PostgreSQLProjectRoomStore.migrations.test.ts +114 -0
  18. package/src/__tests__/PostgreSQLProjectRoomStore.test.ts +48 -0
  19. package/src/__tests__/PostgreSQLTaskStore.integration.test.ts +27 -0
  20. package/src/__tests__/PostgreSQLTaskStore.test.ts +57 -0
  21. package/src/__tests__/PostgreSQLTaskWorkItemStore.integration.test.ts +107 -0
  22. package/src/__tests__/PostgreSQLTaskWorkItemStore.migrations.test.ts +62 -0
  23. package/src/__tests__/PostgreSQLTaskWorkItemStore.test.ts +162 -1
  24. package/src/__tests__/ThreadMessageQueueStore.migrations.test.ts +63 -0
  25. package/src/__tests__/ThreadMessageQueueStore.test.ts +209 -4
  26. package/src/__tests__/add_workspace_project_to_queue.test.ts +15 -0
  27. package/src/__tests__/migration-name-version-compatibility.test.ts +67 -0
  28. package/src/__tests__/task-files.test.ts +4 -3
  29. package/src/createPgStoreConfig.ts +25 -1
  30. package/src/index.ts +13 -0
  31. package/src/migrations/add_trusted_run_context_column.ts +18 -0
  32. package/src/migrations/capability_bundle_migration.ts +20 -0
  33. package/src/migrations/migration.ts +2 -1
  34. package/src/migrations/project_room_migration.ts +128 -0
  35. package/src/migrations/task_migration.ts +15 -0
  36. package/src/migrations/task_work_items_migration.ts +45 -1
  37. package/src/stores/ChannelBindingStore.ts +99 -59
  38. package/src/stores/PostgreSQLCapabilityBundleStore.ts +306 -0
  39. package/src/stores/PostgreSQLChannelInstallationStore.ts +12 -30
  40. package/src/stores/PostgreSQLProjectBotMembershipStore.ts +199 -0
  41. package/src/stores/PostgreSQLProjectMembershipStore.ts +140 -0
  42. package/src/stores/PostgreSQLProjectRoomMessageStore.ts +177 -0
  43. package/src/stores/PostgreSQLProjectRoomStore.ts +57 -0
  44. package/src/stores/PostgreSQLProjectStore.ts +230 -50
  45. package/src/stores/PostgreSQLTaskStore.ts +89 -3
  46. package/src/stores/PostgreSQLTaskWorkItemStore.ts +198 -8
  47. package/src/stores/ThreadMessageQueueStore.ts +130 -32
@@ -87,4 +87,31 @@ describeIfDatabase("PostgreSQLTaskStore timestamp contract", () => {
87
87
  task.updatedAt,
88
88
  )).resolves.toBeNull();
89
89
  });
90
+
91
+ it("lists exact JSONB dependents and nonproject tasks in deterministic order", async () => {
92
+ const common = {
93
+ tenantId, ownerType: "agent", ownerId: "agent-1", workspaceId: "workspace-1", projectId: "project-1",
94
+ } as const;
95
+ for (const [id, status, dependencies] of [
96
+ ["dependent-a", "pending", ["dependency-1"]],
97
+ ["dependent-b", "interrupted", ["dependency-1", "dependency-1"]],
98
+ ["dependent-c", "pending", ["dependency-1"]],
99
+ ] as const) {
100
+ await store.create({ ...common, id, title: id, status, dependencies });
101
+ }
102
+ await pool.query("UPDATE lattice_tasks SET created_at = $1 WHERE tenant_id = $2", ["2026-01-01T00:00:00.000Z", tenantId]);
103
+
104
+ const dependents = await store.listDependents({
105
+ tenantId, workspaceId: "workspace-1", projectId: "project-1", dependencyTaskId: "dependency-1",
106
+ statuses: ["pending", "interrupted"], limit: 2, offset: 0,
107
+ });
108
+ expect(dependents.map(({ id }) => id)).toEqual(["dependent-c", "dependent-b"]);
109
+
110
+ for (const [id, projectId] of [["nonproject-d", undefined], ["nonproject-c", ""], ["nonproject-b", "default"]] as const) {
111
+ await store.create({ id, tenantId, ownerType: "user", ownerId: "user-1", title: id, projectId });
112
+ }
113
+ await pool.query("UPDATE lattice_tasks SET created_at = $1 WHERE tenant_id = $2 AND id LIKE 'nonproject-%'", ["2026-01-02T00:00:00.000Z", tenantId]);
114
+ const nonproject = await store.list({ tenantId, projectId: null, limit: 2, offset: 1 });
115
+ expect(nonproject.map(({ id }) => id)).toEqual(["nonproject-c", "nonproject-b"]);
116
+ });
90
117
  });
@@ -210,6 +210,44 @@ describe("PostgreSQLTaskStore.create", () => {
210
210
  )).resolves.toBeNull();
211
211
  });
212
212
 
213
+ it("uses exact owner and project snapshot predicates for update and delete", async () => {
214
+ const store = new PostgreSQLTaskStore({ pool });
215
+ mockQuery
216
+ .mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 })
217
+ .mockResolvedValueOnce({ rows: [], rowCount: 1 });
218
+ const snapshot = {
219
+ status: "pending" as const, updatedAt: "2026-01-01T00:00:00.000Z",
220
+ ownerType: "agent" as const, ownerId: "owner-1", workspaceId: "workspace-1", projectId: "project-1",
221
+ };
222
+
223
+ await store.updateIfSnapshot("t1", "task-1", { title: "Updated" }, snapshot);
224
+ await store.deleteIfSnapshot("t1", "task-1", snapshot);
225
+
226
+ for (const [sql, params] of mockQuery.mock.calls as unknown as Array<[string, unknown[]]>) {
227
+ expect(sql).toContain("status =");
228
+ expect(sql).toContain("updated_at");
229
+ expect(sql).toContain("owner_type =");
230
+ expect(sql).toContain("owner_id =");
231
+ expect(sql).toContain("workspace_id IS NOT DISTINCT FROM");
232
+ expect(sql).toContain("project_id IS NOT DISTINCT FROM");
233
+ expect(params).toEqual(expect.arrayContaining([
234
+ "pending", snapshot.updatedAt, "agent", "owner-1", "workspace-1", "project-1",
235
+ ]));
236
+ }
237
+ });
238
+
239
+ it("uses null-safe predicates for a nonproject snapshot", async () => {
240
+ const store = new PostgreSQLTaskStore({ pool });
241
+ mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
242
+ await store.updateIfSnapshot("t1", "task-1", { title: "Updated" }, {
243
+ status: "pending", updatedAt: "2026-01-01T00:00:00.000Z", ownerType: "user", ownerId: "user-1",
244
+ workspaceId: null, projectId: null,
245
+ });
246
+ const [sql, params] = mockQuery.mock.calls[0] as unknown as [string, unknown[]];
247
+ expect(sql).toContain("workspace_id IS NOT DISTINCT FROM");
248
+ expect(params).toEqual(expect.arrayContaining([null]));
249
+ });
250
+
213
251
  it("lets only one caller update the same snapshot", async () => {
214
252
  const store = new PostgreSQLTaskStore({ pool });
215
253
  mockQuery
@@ -284,4 +322,23 @@ describe("PostgreSQLTaskStore.create", () => {
284
322
  )).resolves.toBeNull();
285
323
  });
286
324
 
325
+ it("queries exact dependents with strict bounds and deterministic ordering", async () => {
326
+ const store = new PostgreSQLTaskStore({ pool });
327
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("dependent-c"), fullRow("dependent-b")] });
328
+ const query = { tenantId: "t1", workspaceId: "workspace-1", projectId: "project-1", dependencyTaskId: "dependency-1", statuses: ["pending", "interrupted"], limit: 2, offset: 0 } as const;
329
+
330
+ expect((await store.listDependents(query)).map(({ id }) => id)).toEqual(["dependent-c", "dependent-b"]);
331
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringMatching(/dependencies @> \$4::jsonb[\s\S]+status = ANY\(\$5::text\[\]\)[\s\S]+ORDER BY created_at DESC, id DESC LIMIT \$6 OFFSET \$7/), ["t1", "workspace-1", "project-1", '["dependency-1"]', ["pending", "interrupted"], 2, 0]);
332
+ await expect(store.listDependents({ ...query, statuses: [] })).rejects.toThrow(RangeError);
333
+ await expect(store.listDependents({ ...query, statuses: ["unknown"] as never })).rejects.toThrow(RangeError);
334
+ await expect(store.listDependents({ ...query, limit: 101 })).rejects.toThrow(RangeError);
335
+ });
336
+
337
+ it("uses null-project semantics before deterministic task pagination", async () => {
338
+ const store = new PostgreSQLTaskStore({ pool });
339
+ mockQuery.mockResolvedValueOnce({ rows: [] });
340
+ await store.list({ tenantId: "t1", projectId: null, limit: 2, offset: 1 });
341
+ expect(mockQuery).toHaveBeenCalledWith(expect.stringMatching(/\(project_id IS NULL OR project_id = '' OR project_id = 'default'\)[\s\S]+ORDER BY created_at DESC, id DESC/), ["t1", 2, 1]);
342
+ });
343
+
287
344
  });
@@ -0,0 +1,107 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Pool } from "pg";
3
+ import { PostgreSQLTaskWorkItemStore } from "../stores/PostgreSQLTaskWorkItemStore";
4
+
5
+ const databaseUrl = process.env.TEST_DATABASE_URL;
6
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
7
+
8
+ describeIfDatabase("PostgreSQLTaskWorkItemStore pending key grammar", () => {
9
+ let pool: Pool;
10
+
11
+ beforeAll(() => {
12
+ pool = new Pool({ connectionString: databaseUrl });
13
+ });
14
+
15
+ afterAll(async () => {
16
+ await pool.end();
17
+ });
18
+
19
+ it("uses C collation to accept canonical ASCII and reject non-ASCII keys", async () => {
20
+ const result = await pool.query<{ event_key: string }>(
21
+ `SELECT event_key
22
+ FROM (VALUES
23
+ ('execution-result:ascii-._:09'),
24
+ ('execution-result:cafeé'),
25
+ ('execution-result:任务')
26
+ ) AS candidates(event_key)
27
+ WHERE event_key COLLATE "C" ~ '^execution-result:[A-Za-z0-9._:-]+$'
28
+ ORDER BY event_key COLLATE "C"`,
29
+ );
30
+
31
+ expect(result.rows.map((row) => row.event_key)).toEqual([
32
+ "execution-result:ascii-._:09",
33
+ ]);
34
+ });
35
+ });
36
+
37
+ describeIfDatabase("PostgreSQLTaskWorkItemStore lifecycle pagination", () => {
38
+ let adminPool: Pool;
39
+ let pool: Pool;
40
+ let store: PostgreSQLTaskWorkItemStore;
41
+ const tenantId = `work-item-pagination-${randomUUID()}`;
42
+ const schema = `work_item_${randomUUID().replace(/-/g, "")}`;
43
+
44
+ beforeAll(async () => {
45
+ adminPool = new Pool({ connectionString: databaseUrl });
46
+ await adminPool.query(`CREATE SCHEMA ${schema}`);
47
+ const poolConfig = { connectionString: databaseUrl, options: `-c search_path=${schema}` };
48
+ pool = new Pool(poolConfig);
49
+ store = new PostgreSQLTaskWorkItemStore({ poolConfig, autoMigrate: false });
50
+ await store.initialize();
51
+ });
52
+
53
+ afterEach(async () => {
54
+ await pool.query("DELETE FROM lattice_task_work_items WHERE tenant_id = $1 OR tenant_id = $2", [tenantId, `${tenantId}-other`]);
55
+ });
56
+
57
+ afterAll(async () => {
58
+ await store.dispose();
59
+ await pool.end();
60
+ await adminPool.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
61
+ await adminPool.end();
62
+ });
63
+
64
+ it("pages same-millisecond events with exact scope and action filters", async () => {
65
+ const common = { tenantId, actor: "agent:a", workspaceId: "workspace-1", projectId: "project-1" } as const;
66
+ for (const [taskId, action, eventKey] of [
67
+ ["task-a", "in_progress", "event:a"],
68
+ ["task-b", "completed", "event:b"],
69
+ ["task-c", "in_progress", "event:c"],
70
+ ] as const) await store.createIfAbsentByEventKey({ ...common, taskId, action, eventKey });
71
+ await store.createIfAbsentByEventKey({ ...common, taskId: "wrong-action", action: "activity", eventKey: "event:action" });
72
+ await store.createIfAbsentByEventKey({ ...common, taskId: "wrong-project", action: "completed", eventKey: "event:project", projectId: "project-2" });
73
+ await store.createIfAbsentByEventKey({ ...common, taskId: "wrong-workspace", action: "completed", eventKey: "event:workspace", workspaceId: "workspace-2" });
74
+ await store.createIfAbsentByEventKey({ ...common, taskId: "wrong-tenant", action: "completed", eventKey: "event:tenant", tenantId: `${tenantId}-other` });
75
+ await pool.query(
76
+ `UPDATE lattice_task_work_items
77
+ SET id = CASE task_id WHEN 'task-a' THEN 'event-a' WHEN 'task-b' THEN 'event-b' ELSE 'event-c' END,
78
+ created_at = '2026-01-02T00:00:00.123Z'
79
+ WHERE tenant_id = $1 AND task_id IN ('task-a', 'task-b', 'task-c')`,
80
+ [tenantId],
81
+ );
82
+ const query = { tenantId, workspaceId: "workspace-1", projectId: "project-1", actions: ["in_progress", "completed"], limit: 2 } as const;
83
+
84
+ const page1 = await store.listProjectLifecycleEvents(query);
85
+ const page2 = await store.listProjectLifecycleEvents({ ...query, before: { createdAt: page1[1].createdAt, id: page1[1].id } });
86
+
87
+ expect(page1.map(({ id }) => id)).toEqual(["event-c", "event-b"]);
88
+ expect(page2.map(({ id }) => id)).toEqual(["event-a"]);
89
+ expect(page1.every(({ createdAt }) => createdAt.toISOString().endsWith(".123Z"))).toBe(true);
90
+ });
91
+
92
+ it("serves immediate pending evidence and Project lifecycle history from one migrated table", async () => {
93
+ const common = { tenantId, taskId: "combined-task", workspaceId: "workspace-1", projectId: "project-1" } as const;
94
+ await store.createIfAbsentByEventKey({
95
+ ...common, eventKey: "execution-result:ordinary-thread", action: "execution_result", actor: "agent:worker",
96
+ });
97
+ await store.createIfAbsentByEventKey({
98
+ ...common, eventKey: "project-completed:combined-task", action: "completed", actor: "agent:project-worker",
99
+ });
100
+
101
+ await expect(store.listPendingExecutionResults({ tenantId, taskId: common.taskId, limit: 10 }))
102
+ .resolves.toEqual([expect.objectContaining({ eventKey: "execution-result:ordinary-thread" })]);
103
+ await expect(store.listProjectLifecycleEvents({
104
+ tenantId, workspaceId: common.workspaceId, projectId: common.projectId, actions: ["completed"], limit: 10,
105
+ })).resolves.toEqual([expect.objectContaining({ eventKey: "project-completed:combined-task" })]);
106
+ });
107
+ });
@@ -0,0 +1,62 @@
1
+ import { beforeEach, describe, expect, it } from "@jest/globals";
2
+
3
+ const recordedMigrationNames: string[] = [];
4
+ const clientSql: string[] = [];
5
+
6
+ jest.mock("pg", () => {
7
+ const client = {
8
+ query: async (sql: string, params?: unknown[]) => {
9
+ clientSql.push(sql);
10
+ if (sql.includes("information_schema.tables")) return { rows: [{ exists: true }] };
11
+ if (sql.includes("pg_index")) return { rows: [] };
12
+ if (sql.includes("INSERT INTO lattice_schema_migrations")) {
13
+ recordedMigrationNames.push(params?.[0] as string);
14
+ }
15
+ return { rows: [] };
16
+ },
17
+ release: () => undefined,
18
+ };
19
+ const pool = {
20
+ connect: async () => client,
21
+ query: async () => ({ rows: [] }),
22
+ end: async () => undefined,
23
+ };
24
+ return { Pool: function Pool() { return pool; } };
25
+ });
26
+
27
+ import { PostgreSQLTaskWorkItemStore } from "../stores/PostgreSQLTaskWorkItemStore";
28
+
29
+ describe("PostgreSQLTaskWorkItemStore standalone migrations", () => {
30
+ beforeEach(() => {
31
+ recordedMigrationNames.length = 0;
32
+ clientSql.length = 0;
33
+ });
34
+
35
+ it("applies the complete work-item migration chain", async () => {
36
+ const store = new PostgreSQLTaskWorkItemStore({
37
+ poolConfig: "postgres://unused",
38
+ autoMigrate: false,
39
+ });
40
+
41
+ await store.initialize();
42
+
43
+ expect(recordedMigrationNames).toEqual([
44
+ "create_task_work_items_table",
45
+ "add_task_work_item_project_fields",
46
+ "add_task_work_item_event_key",
47
+ "add_task_work_item_pending_indexes",
48
+ "add_project_lifecycle_event_index",
49
+ ]);
50
+ expect(clientSql.find((sql) => sql.includes("information_schema.tables")))
51
+ .toContain("table_schema = current_schema()");
52
+ expect(clientSql.some((sql) => sql.includes("CREATE TABLE IF NOT EXISTS lattice_task_work_items"))).toBe(true);
53
+ expect(clientSql.some((sql) => sql.includes(
54
+ "created_at TIMESTAMPTZ NOT NULL DEFAULT date_trunc('milliseconds', clock_timestamp())",
55
+ ))).toBe(true);
56
+ expect(clientSql.some((sql) => sql.includes(
57
+ "SET created_at = date_trunc('milliseconds', created_at)",
58
+ ))).toBe(true);
59
+ expect(clientSql.some((sql) => sql.includes("idx_task_work_items_project_lifecycle"))).toBe(true);
60
+ await store.dispose();
61
+ });
62
+ });
@@ -1,6 +1,10 @@
1
1
  import type { Pool } from "pg";
2
2
  import { PostgreSQLTaskWorkItemStore } from "../stores/PostgreSQLTaskWorkItemStore";
3
- import { addTaskWorkItemEventKeyMigration } from "../migrations/task_work_items_migration";
3
+ import {
4
+ addProjectLifecycleEventIndex,
5
+ addTaskWorkItemEventKeyMigration,
6
+ addTaskWorkItemPendingIndexesMigration,
7
+ } from "../migrations/task_work_items_migration";
4
8
 
5
9
  describe("PostgreSQLTaskWorkItemStore", () => {
6
10
  const row = {
@@ -10,6 +14,10 @@ describe("PostgreSQLTaskWorkItemStore", () => {
10
14
  created_at: "2026-01-01T00:00:00.000Z",
11
15
  };
12
16
 
17
+ it("requires a pool or connection configuration", () => {
18
+ expect(() => new PostgreSQLTaskWorkItemStore({})).toThrow("Either pool or poolConfig must be provided");
19
+ });
20
+
13
21
  it.each([
14
22
  [undefined, "ASC"],
15
23
  ["asc", "ASC"],
@@ -74,6 +82,25 @@ describe("PostgreSQLTaskWorkItemStore", () => {
74
82
  expect(result[0].eventKey).toBeUndefined();
75
83
  });
76
84
 
85
+ it.each([null, [], "legacy", 1] as const)("maps non-object detail %p to undefined", async (detail) => {
86
+ const query = jest.fn().mockResolvedValue({ rows: [{ ...row, detail }] });
87
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
88
+
89
+ const result = await store.list({ tenantId: "tenant-1", taskId: "task-1" });
90
+
91
+ expect(result[0].detail).toBeUndefined();
92
+ });
93
+
94
+ it("preserves plain-object detail", async () => {
95
+ const detail = { markdown: "status" };
96
+ const query = jest.fn().mockResolvedValue({ rows: [{ ...row, detail }] });
97
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
98
+
99
+ const result = await store.list({ tenantId: "tenant-1", taskId: "task-1" });
100
+
101
+ expect(result[0].detail).toEqual(detail);
102
+ });
103
+
77
104
  it("atomically inserts by task-scoped event key or returns the existing row", async () => {
78
105
  const query = jest.fn().mockResolvedValue({ rows: [row] });
79
106
  const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
@@ -89,6 +116,96 @@ describe("PostgreSQLTaskWorkItemStore", () => {
89
116
  expect.arrayContaining(["tenant-1", "task-1", "evidence:key"]),
90
117
  );
91
118
  expect(result.eventKey).toBe("evidence:key");
119
+ expect(query).toHaveBeenCalledWith(
120
+ expect.stringMatching(/created_at[\s\S]+date_trunc\('milliseconds', clock_timestamp\(\)\)/),
121
+ expect.any(Array),
122
+ );
123
+ });
124
+
125
+ it("creates ordinary work items with an explicit millisecond timestamp", async () => {
126
+ const query = jest.fn().mockResolvedValue({ rows: [row] });
127
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
128
+
129
+ await store.create({ tenantId: "tenant-1", taskId: "task-1", action: "activity", actor: "agent:a" });
130
+
131
+ expect(query).toHaveBeenCalledWith(
132
+ expect.stringMatching(/created_at[\s\S]+date_trunc\('milliseconds', clock_timestamp\(\)\)/),
133
+ expect.any(Array),
134
+ );
135
+ });
136
+
137
+ it("atomically inserts activity from an exact task snapshot", async () => {
138
+ const query = jest.fn().mockResolvedValue({ rows: [row] });
139
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
140
+ const snapshot = {
141
+ status: "in_progress" as const, updatedAt: "2026-01-01T00:00:00.000Z", ownerType: "agent" as const,
142
+ ownerId: "agent-1", workspaceId: "workspace-1", projectId: "project-1",
143
+ };
144
+ await store.createIfTaskSnapshot({
145
+ tenantId: "tenant-1", taskId: "task-1", action: "activity", actor: "agent:agent-1",
146
+ workspaceId: "workspace-1", projectId: "project-1",
147
+ }, snapshot);
148
+ expect(query).toHaveBeenCalledWith(
149
+ expect.stringMatching(/INSERT INTO lattice_task_work_items[\s\S]+SELECT[\s\S]+FROM lattice_tasks[\s\S]+status = [\s\S]+updated_at[\s\S]+owner_type = [\s\S]+owner_id = [\s\S]+workspace_id IS NOT DISTINCT FROM[\s\S]+project_id IS NOT DISTINCT FROM[\s\S]+RETURNING/),
150
+ expect.arrayContaining(["tenant-1", "task-1", "in_progress", snapshot.updatedAt, "agent", "agent-1", "workspace-1", "project-1"]),
151
+ );
152
+ });
153
+
154
+ it("uses null-safe scope predicates for nonproject activity", async () => {
155
+ const query = jest.fn().mockResolvedValue({ rows: [] });
156
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
157
+ await store.createIfTaskSnapshot({ tenantId: "tenant-1", taskId: "task-1", action: "activity", actor: "user:user-1" }, {
158
+ status: "pending", updatedAt: "2026-01-01T00:00:00.000Z", ownerType: "user", ownerId: "user-1",
159
+ workspaceId: null, projectId: null,
160
+ });
161
+ expect(query).toHaveBeenCalledWith(expect.stringContaining("workspace_id IS NOT DISTINCT FROM"), expect.arrayContaining([null]));
162
+ });
163
+
164
+ it("uses one bounded deterministic anti-join with C-collated ASCII keys that reject non-ASCII", async () => {
165
+ const pendingRows = [
166
+ { ...row, id: "item-3", event_key: "execution-result:c", action: "execution_result" },
167
+ { ...row, id: "item-1", event_key: "execution-result:a", action: "execution_result" },
168
+ ];
169
+ const query = jest.fn().mockResolvedValue({ rows: pendingRows });
170
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
171
+
172
+ const result = await store.listPendingExecutionResults({
173
+ tenantId: "tenant-1", taskId: "task-1", limit: 2,
174
+ });
175
+
176
+ expect(query).toHaveBeenCalledTimes(1);
177
+ expect(query).toHaveBeenCalledWith(
178
+ expect.stringMatching(
179
+ /action = 'execution_result'[\s\S]+event_key COLLATE "C" ~ '\^execution-result:\[A-Za-z0-9\._:-\]\+\$'[\s\S]+NOT EXISTS[\s\S]+reconciled\.detail ->> 'executionResultId' = result\.event_key[\s\S]+ORDER BY result\.created_at DESC, result\.id DESC[\s\S]+LIMIT \$3/,
180
+ ),
181
+ ["tenant-1", "task-1", 2],
182
+ );
183
+ expect(result.map((item) => item.eventKey)).toEqual([
184
+ "execution-result:c", "execution-result:a",
185
+ ]);
186
+ });
187
+
188
+ it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1, 1_001])(
189
+ "rejects invalid pending execution result limit %s",
190
+ async (limit) => {
191
+ const query = jest.fn();
192
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
193
+
194
+ await expect(store.listPendingExecutionResults({
195
+ tenantId: "tenant-1", taskId: "task-1", limit,
196
+ })).rejects.toMatchObject({ name: "RangeError", code: "INVALID_LIMIT" });
197
+ expect(query).not.toHaveBeenCalled();
198
+ },
199
+ );
200
+
201
+ it("returns no pending execution results for limit zero without querying", async () => {
202
+ const query = jest.fn();
203
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
204
+
205
+ await expect(store.listPendingExecutionResults({
206
+ tenantId: "tenant-1", taskId: "task-1", limit: 0,
207
+ })).resolves.toEqual([]);
208
+ expect(query).not.toHaveBeenCalled();
92
209
  });
93
210
 
94
211
  it("migrates a task-scoped unique event key", async () => {
@@ -101,4 +218,48 @@ describe("PostgreSQLTaskWorkItemStore", () => {
101
218
  "(tenant_id, task_id, event_key) WHERE event_key IS NOT NULL",
102
219
  ));
103
220
  });
221
+
222
+ it("adds ordered pending and partial reconciliation indexes in a new migration", async () => {
223
+ const query = jest.fn().mockResolvedValue({ rows: [] });
224
+
225
+ await addTaskWorkItemPendingIndexesMigration.up({ query } as never);
226
+
227
+ expect(addTaskWorkItemPendingIndexesMigration.version)
228
+ .toBeGreaterThan(addTaskWorkItemEventKeyMigration.version);
229
+ expect(query).toHaveBeenCalledWith(expect.stringContaining(
230
+ "(tenant_id, task_id, action, created_at DESC, id DESC)",
231
+ ));
232
+ expect(query).toHaveBeenCalledWith(expect.stringMatching(
233
+ /\(tenant_id, task_id, \(detail ->> 'executionResultId'\)\)[\s\S]+WHERE action = 'execution_reconciled'/,
234
+ ));
235
+ });
236
+
237
+ it("queries project lifecycle events with strict bounds and exclusive cursor", async () => {
238
+ const query = jest.fn().mockResolvedValue({ rows: [{ ...row, id: "event-c", action: "completed", workspace_id: "workspace-1", project_id: "project-1" }] });
239
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
240
+ const before = { createdAt: new Date("2026-01-02T00:00:00.000Z"), id: "event-b" };
241
+ const request = { tenantId: "tenant-1", workspaceId: "workspace-1", projectId: "project-1", actions: ["in_progress", "completed"], before, limit: 2 } as const;
242
+
243
+ await expect(store.listProjectLifecycleEvents(request)).resolves.toHaveLength(1);
244
+ expect(query).toHaveBeenCalledWith(expect.stringMatching(/action = ANY\(\$4::text\[\]\)[\s\S]+event_key IS NOT NULL AND event_key <> ''[\s\S]+created_at < \$5::timestamptz[\s\S]+id < \$6[\s\S]+ORDER BY created_at DESC, id DESC LIMIT \$7/), ["tenant-1", "workspace-1", "project-1", ["in_progress", "completed"], before.createdAt, "event-b", 2]);
245
+ await expect(store.listProjectLifecycleEvents({ ...request, actions: [] })).rejects.toThrow(RangeError);
246
+ await expect(store.listProjectLifecycleEvents({ ...request, actions: ["unknown"] as never })).rejects.toThrow(RangeError);
247
+ await expect(store.listProjectLifecycleEvents({ ...request, limit: 101 })).rejects.toThrow(RangeError);
248
+ });
249
+
250
+ it("rejects malformed lifecycle event outer rows", async () => {
251
+ const query = jest.fn().mockResolvedValue({ rows: [{ ...row, id: "", workspace_id: "workspace-1", project_id: "project-1" }] });
252
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
253
+ await expect(store.listProjectLifecycleEvents({ tenantId: "tenant-1", workspaceId: "workspace-1", projectId: "project-1", actions: ["completed"], limit: 10 }))
254
+ .rejects.toThrow("Invalid project lifecycle event row");
255
+ });
256
+
257
+ it("creates the project lifecycle event index", async () => {
258
+ const query = jest.fn().mockResolvedValue({ rows: [] });
259
+ await addProjectLifecycleEventIndex.up({ query } as never);
260
+ expect(query.mock.calls[0][0]).toMatch(
261
+ /UPDATE lattice_task_work_items\s+SET created_at = date_trunc\('milliseconds', created_at\)/,
262
+ );
263
+ expect(query).toHaveBeenCalledWith(expect.stringMatching(/created_at DESC,\s*id DESC[\s\S]+WHERE event_key IS NOT NULL/));
264
+ });
104
265
  });
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it, jest } from "@jest/globals";
2
+ import type { PoolClient } from "pg";
3
+
4
+ const registeredMigrationNames: string[] = [];
5
+
6
+ jest.mock("@axiom-lattice/core", () => ({}));
7
+ jest.mock("pg", () => ({ Pool: class Pool {} }));
8
+ jest.mock("../migrations/migration", () => ({
9
+ MigrationManager: class MigrationManager {
10
+ register(migration: { name: string }): void {
11
+ registeredMigrationNames.push(migration.name);
12
+ }
13
+ async migrate(): Promise<void> {
14
+ throw new Error("stop after migrations");
15
+ }
16
+ },
17
+ }));
18
+
19
+ import { createPgStoreConfig } from "../createPgStoreConfig";
20
+ import { addTrustedRunContextColumn } from "../migrations/add_trusted_run_context_column";
21
+ import { ThreadMessageQueueStore } from "../stores/ThreadMessageQueueStore";
22
+
23
+ function normalizeSql(sql: string): string {
24
+ return sql.replace(/\s+/g, " ").trim();
25
+ }
26
+
27
+ describe("thread queue trusted context migration", () => {
28
+ it("uses the reserved identity and is centrally registered exactly once", async () => {
29
+ expect(addTrustedRunContextColumn.version).toBe(173);
30
+ expect(addTrustedRunContextColumn.name).toBe("add_thread_queue_trusted_run_context");
31
+ registeredMigrationNames.length = 0;
32
+
33
+ await expect(createPgStoreConfig("postgres://unused")).rejects.toThrow("stop after migrations");
34
+
35
+ expect(registeredMigrationNames.filter((name) => name === addTrustedRunContextColumn.name)).toHaveLength(1);
36
+ });
37
+
38
+ it("registers the migration in the standalone queue store", () => {
39
+ registeredMigrationNames.length = 0;
40
+
41
+ new ThreadMessageQueueStore({ poolConfig: { connectionString: "postgres://unused" }, autoMigrate: false });
42
+
43
+ expect(registeredMigrationNames.filter((name) => name === addTrustedRunContextColumn.name)).toHaveLength(1);
44
+ });
45
+
46
+ it("adds and removes both durable columns", async () => {
47
+ const statements: string[] = [];
48
+ const client = { query: async (sql: string) => {
49
+ statements.push(normalizeSql(sql));
50
+ return { rows: [] };
51
+ } } as unknown as PoolClient;
52
+
53
+ await addTrustedRunContextColumn.up(client);
54
+ expect(statements.join(" ")).toContain("ADD COLUMN IF NOT EXISTS trusted_run_context JSONB");
55
+ expect(statements.join(" ")).toContain("ADD COLUMN IF NOT EXISTS execution_mode VARCHAR(20)");
56
+ expect(statements.join(" ")).toContain("execution_mode IS NULL OR execution_mode = 'followup'");
57
+
58
+ statements.length = 0;
59
+ await addTrustedRunContextColumn.down?.(client);
60
+ expect(statements.join(" ")).toContain("DROP COLUMN IF EXISTS execution_mode");
61
+ expect(statements.join(" ")).toContain("DROP COLUMN IF EXISTS trusted_run_context");
62
+ });
63
+ });