@axiom-lattice/pg-stores 2.0.9 → 3.0.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "2.0.9",
3
+ "version": "3.0.0",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -25,8 +25,8 @@
25
25
  "@langchain/core": "1.1.30",
26
26
  "pg": "^8.16.3",
27
27
  "uuid": "^9.0.1",
28
- "@axiom-lattice/core": "3.1.1",
29
- "@axiom-lattice/protocols": "3.0.4"
28
+ "@axiom-lattice/core": "4.0.0",
29
+ "@axiom-lattice/protocols": "4.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/jest": "^29.5.14",
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Standalone-migration tests for PostgreSQLA2AApiKeyStore.
3
+ *
4
+ * When the store is constructed with its own `poolConfig` (no shared pool from
5
+ * `createPgStoreConfig`), its private MigrationManager must register every
6
+ * migration the store's queries depend on — including the `assistant_ids`
7
+ * column added after the original table creation.
8
+ */
9
+
10
+ import { describe, expect, it, beforeEach } from "@jest/globals";
11
+
12
+ const mockRecordedMigrationNames: string[] = [];
13
+ const mockClientSql: string[] = [];
14
+
15
+ jest.mock("@axiom-lattice/core", () => ({
16
+ encrypt: (value: string) => `enc:${value}`,
17
+ decrypt: (value: string) => value.replace(/^enc:/, ""),
18
+ }));
19
+
20
+ jest.mock("pg", () => {
21
+ const client = {
22
+ query: async (sql: string, params?: unknown[]) => {
23
+ mockClientSql.push(sql);
24
+ if (sql.includes("information_schema.tables")) {
25
+ return { rows: [{ exists: true }] };
26
+ }
27
+ if (sql.includes("pg_index")) {
28
+ // PK already on `name` — no legacy schema migration needed.
29
+ return { rows: [] };
30
+ }
31
+ if (sql.includes("INSERT INTO lattice_schema_migrations")) {
32
+ mockRecordedMigrationNames.push(params?.[0] as string);
33
+ return { rows: [] };
34
+ }
35
+ if (sql.includes("FROM lattice_schema_migrations")) {
36
+ return { rows: [] };
37
+ }
38
+ return { rows: [] };
39
+ },
40
+ release: () => undefined,
41
+ };
42
+ const pool = {
43
+ connect: async () => client,
44
+ query: async () => ({ rows: [] }),
45
+ end: async () => undefined,
46
+ };
47
+ return { Pool: function Pool() { return pool; } };
48
+ });
49
+
50
+ import { PostgreSQLA2AApiKeyStore } from "../stores/PostgreSQLA2AApiKeyStore";
51
+
52
+ describe("PostgreSQLA2AApiKeyStore standalone migrations", () => {
53
+ beforeEach(() => {
54
+ mockRecordedMigrationNames.length = 0;
55
+ mockClientSql.length = 0;
56
+ });
57
+
58
+ it("registers and applies both the table and assistant_ids migrations", async () => {
59
+ const store = new PostgreSQLA2AApiKeyStore({
60
+ poolConfig: "postgres://unused",
61
+ autoMigrate: false,
62
+ });
63
+
64
+ await store.initialize();
65
+
66
+ expect(mockRecordedMigrationNames).toEqual([
67
+ "create_a2a_api_keys_table",
68
+ "add_a2a_key_assistant_ids",
69
+ ]);
70
+ expect(
71
+ mockClientSql.some((sql) => sql.includes("ADD COLUMN IF NOT EXISTS assistant_ids")),
72
+ ).toBe(true);
73
+
74
+ await store.dispose();
75
+ });
76
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * PostgreSQLA2AApiKeyStore behavior tests
3
+ *
4
+ * Verifies the store aligns with the new A2A key scope model:
5
+ * - projectId is required (written directly, no null fallback)
6
+ * - assistantIds is persisted as JSONB and projected back
7
+ * - workspaceId is no longer read or written
8
+ */
9
+
10
+ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
11
+ import type { Pool } from "pg";
12
+
13
+ const mockQuery = jest.fn();
14
+
15
+ jest.mock("@axiom-lattice/core", () => ({
16
+ encrypt: (value: string) => `enc:${value}`,
17
+ decrypt: (value: string) => value.replace(/^enc:/, ""),
18
+ }));
19
+
20
+ import { PostgreSQLA2AApiKeyStore } from "../stores/PostgreSQLA2AApiKeyStore";
21
+
22
+ describe("PostgreSQLA2AApiKeyStore", () => {
23
+ const pool = { query: mockQuery } as unknown as Pool;
24
+
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ it("create writes assistant_ids and project_id, omits workspace_id", async () => {
30
+ const store = new PostgreSQLA2AApiKeyStore({ pool });
31
+ mockQuery.mockResolvedValueOnce({
32
+ rows: [{
33
+ id: "k1",
34
+ key_value: "enc:a2a_x",
35
+ tenant_id: "t1",
36
+ project_id: "p1",
37
+ assistant_ids: ["a1", "a2"],
38
+ label: "lbl",
39
+ enabled: true,
40
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
41
+ updated_at: new Date("2026-01-01T00:00:00.000Z"),
42
+ }],
43
+ });
44
+
45
+ const record = await store.create({
46
+ tenantId: "t1",
47
+ projectId: "p1",
48
+ assistantIds: ["a1", "a2"],
49
+ label: "lbl",
50
+ });
51
+
52
+ const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]];
53
+ expect(sql).toContain("assistant_ids");
54
+ expect(sql).not.toContain("workspace_id");
55
+ expect(params[2]).toBe("p1");
56
+ expect(params[3]).toBe(JSON.stringify(["a1", "a2"]));
57
+ expect(record.projectId).toBe("p1");
58
+ expect(record.assistantIds).toEqual(["a1", "a2"]);
59
+ expect("workspaceId" in record).toBe(false);
60
+ });
61
+
62
+ it("create stores null assistant_ids when not provided", async () => {
63
+ const store = new PostgreSQLA2AApiKeyStore({ pool });
64
+ mockQuery.mockResolvedValueOnce({
65
+ rows: [{
66
+ id: "k2",
67
+ key_value: "enc:a2a_y",
68
+ tenant_id: "t1",
69
+ project_id: "p1",
70
+ assistant_ids: null,
71
+ label: null,
72
+ enabled: true,
73
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
74
+ updated_at: new Date("2026-01-01T00:00:00.000Z"),
75
+ }],
76
+ });
77
+
78
+ const record = await store.create({ tenantId: "t1", projectId: "p1" });
79
+
80
+ const [, params] = mockQuery.mock.calls[0] as [string, unknown[]];
81
+ expect(params[3]).toBe(null);
82
+ expect(record.assistantIds).toBeUndefined();
83
+ });
84
+
85
+ it("findById returns the matching key record", async () => {
86
+ const store = new PostgreSQLA2AApiKeyStore({ pool });
87
+ mockQuery.mockResolvedValueOnce({
88
+ rows: [{
89
+ id: "k1",
90
+ key_value: "enc:a2a_x",
91
+ tenant_id: "t1",
92
+ project_id: "p1",
93
+ assistant_ids: null,
94
+ label: null,
95
+ enabled: true,
96
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
97
+ updated_at: new Date("2026-01-01T00:00:00.000Z"),
98
+ }],
99
+ });
100
+
101
+ const record = await store.findById("k1");
102
+
103
+ expect(mockQuery).toHaveBeenCalledWith(
104
+ expect.stringContaining("WHERE id = $1"),
105
+ ["k1"],
106
+ );
107
+ expect(record?.tenantId).toBe("t1");
108
+ });
109
+
110
+ it("loadIntoMap projects assistantIds and omits workspaceId", async () => {
111
+ const store = new PostgreSQLA2AApiKeyStore({ pool });
112
+ mockQuery.mockResolvedValueOnce({
113
+ rows: [{
114
+ id: "k1",
115
+ key_value: "enc:a2a_x",
116
+ tenant_id: "t1",
117
+ project_id: "p1",
118
+ assistant_ids: ["a1"],
119
+ label: null,
120
+ enabled: true,
121
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
122
+ updated_at: new Date("2026-01-01T00:00:00.000Z"),
123
+ }],
124
+ });
125
+
126
+ const map = await store.loadIntoMap();
127
+ const entry = map.get("a2a_x");
128
+ expect(entry).toBeDefined();
129
+ expect(entry?.projectId).toBe("p1");
130
+ expect(entry?.assistantIds).toEqual(["a1"]);
131
+ expect("workspaceId" in (entry ?? {})).toBe(false);
132
+ });
133
+ });
@@ -1,4 +1,5 @@
1
1
  import { PostgreSQLChannelInstallationStore } from "../stores/PostgreSQLChannelInstallationStore";
2
+ import type { Pool } from "pg";
2
3
 
3
4
  const mockQuery = jest.fn();
4
5
 
@@ -20,8 +21,7 @@ describe("PostgreSQLChannelInstallationStore", () => {
20
21
 
21
22
  it("retrieves and decrypts an installation by id", async () => {
22
23
  const store = new PostgreSQLChannelInstallationStore({
23
- poolConfig: "postgres://test:test@localhost:5432/test",
24
- autoMigrate: false,
24
+ pool: { query: mockQuery } as unknown as Pool,
25
25
  });
26
26
 
27
27
  mockQuery.mockResolvedValueOnce({
@@ -51,4 +51,39 @@ describe("PostgreSQLChannelInstallationStore", () => {
51
51
  expect(installation?.config.appSecret).toBe("secret");
52
52
  expect(installation?.config.verificationToken).toBe("token-1");
53
53
  });
54
+
55
+ it("rejects a duplicate id without changing the original installation", async () => {
56
+ const store = new PostgreSQLChannelInstallationStore({
57
+ pool: { query: mockQuery } as unknown as Pool,
58
+ });
59
+ const originalRow = {
60
+ id: "install-1",
61
+ tenant_id: "tenant-a",
62
+ channel: "lark",
63
+ name: "Original",
64
+ config: { appId: "app-1", appSecret: "enc:secret-1" },
65
+ enabled: true,
66
+ fallback_agent_id: null,
67
+ reject_when_no_binding: true,
68
+ created_at: new Date("2026-04-10T00:00:00.000Z"),
69
+ updated_at: new Date("2026-04-10T00:00:00.000Z"),
70
+ };
71
+ const duplicateError = Object.assign(new Error("duplicate key value violates unique constraint"), {
72
+ code: "23505",
73
+ });
74
+ mockQuery.mockRejectedValueOnce(duplicateError).mockResolvedValueOnce({ rows: [originalRow] });
75
+
76
+ await expect(
77
+ store.createInstallation("tenant-a", "install-1", {
78
+ channel: "lark",
79
+ name: "Replacement",
80
+ config: { appId: "app-2", appSecret: "secret-2" },
81
+ }),
82
+ ).rejects.toMatchObject({ code: "23505" });
83
+ await expect(store.getInstallationById("install-1")).resolves.toMatchObject({
84
+ id: "install-1",
85
+ name: "Original",
86
+ config: { appId: "app-1", appSecret: "secret-1" },
87
+ });
88
+ });
54
89
  });
@@ -0,0 +1,90 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Pool } from "pg";
3
+ import { PostgreSQLTaskStore } from "../stores/PostgreSQLTaskStore";
4
+
5
+ const databaseUrl = process.env.TEST_DATABASE_URL;
6
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
7
+ const CANONICAL_UTC_MILLIS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
8
+
9
+ describeIfDatabase("PostgreSQLTaskStore timestamp contract", () => {
10
+ let pool: Pool;
11
+ let store: PostgreSQLTaskStore;
12
+ const tenantId = `timestamp-contract-${randomUUID()}`;
13
+
14
+ beforeAll(async () => {
15
+ pool = new Pool({ connectionString: databaseUrl });
16
+ store = new PostgreSQLTaskStore({ poolConfig: databaseUrl });
17
+ await store.initialize();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await pool.query("DELETE FROM lattice_tasks WHERE tenant_id = $1", [tenantId]);
22
+ });
23
+
24
+ afterAll(async () => {
25
+ await store.dispose();
26
+ await pool.end();
27
+ });
28
+
29
+ it("updates canonical VARCHAR timestamps monotonically as UTC ISO text", async () => {
30
+ const task = await store.create({
31
+ tenantId,
32
+ ownerType: "user",
33
+ ownerId: "user-1",
34
+ title: "Canonical timestamp",
35
+ });
36
+
37
+ const updated = await store.update(tenantId, task.id, { metadata: { trainingRound: true } });
38
+ const row = await pool.query<{ updated_at: string }>(
39
+ "SELECT updated_at FROM lattice_tasks WHERE tenant_id = $1 AND id = $2",
40
+ [tenantId, task.id],
41
+ );
42
+
43
+ expect(updated!.updatedAt.getTime()).toBeGreaterThan(task.updatedAt.getTime());
44
+ expect(row.rows[0].updated_at).toMatch(CANONICAL_UTC_MILLIS);
45
+ });
46
+
47
+ it("repairs shape-valid impossible and plainly malformed timestamps without cast errors", async () => {
48
+ const task = await store.create({
49
+ tenantId,
50
+ ownerType: "user",
51
+ ownerId: "user-1",
52
+ title: "Repair timestamp",
53
+ });
54
+
55
+ for (const malformed of ["2026-99-99T00:00:00.000Z", "not-a-date"]) {
56
+ await pool.query(
57
+ "UPDATE lattice_tasks SET updated_at = $1 WHERE tenant_id = $2 AND id = $3",
58
+ [malformed, tenantId, task.id],
59
+ );
60
+
61
+ await expect(store.update(tenantId, task.id, { metadata: { repaired: malformed } })).resolves.toBeTruthy();
62
+ const row = await pool.query<{ updated_at: string }>(
63
+ "SELECT updated_at FROM lattice_tasks WHERE tenant_id = $1 AND id = $2",
64
+ [tenantId, task.id],
65
+ );
66
+ expect(row.rows[0].updated_at).toMatch(CANONICAL_UTC_MILLIS);
67
+ }
68
+ });
69
+
70
+ it("makes snapshot CAS safely miss malformed stored timestamps", async () => {
71
+ const task = await store.create({
72
+ tenantId,
73
+ ownerType: "user",
74
+ ownerId: "user-1",
75
+ title: "Malformed snapshot",
76
+ });
77
+ await pool.query(
78
+ "UPDATE lattice_tasks SET updated_at = $1 WHERE tenant_id = $2 AND id = $3",
79
+ ["2026-02-31T00:00:00.000Z", tenantId, task.id],
80
+ );
81
+
82
+ await expect(store.updateIfStatusAndUpdatedAt(
83
+ tenantId,
84
+ task.id,
85
+ { description: "must not write" },
86
+ ["pending"],
87
+ task.updatedAt,
88
+ )).resolves.toBeNull();
89
+ });
90
+ });
@@ -0,0 +1,287 @@
1
+ /**
2
+ * PostgreSQLTaskStore.create tests
3
+ *
4
+ * Verifies create() honors a caller-provided id (single-ID mapping
5
+ * with external systems such as A2A) and falls back to a generated
6
+ * UUID when omitted.
7
+ */
8
+
9
+ import { describe, expect, it, jest, beforeEach } from "@jest/globals";
10
+ import type { Pool } from "pg";
11
+
12
+ const mockQuery = jest.fn();
13
+
14
+ import { PostgreSQLTaskStore } from "../stores/PostgreSQLTaskStore";
15
+
16
+ const fullRow = (id: string) => ({
17
+ id,
18
+ tenant_id: "t1",
19
+ owner_type: "agent",
20
+ owner_id: "owner-1",
21
+ title: "t",
22
+ description: null,
23
+ status: "pending",
24
+ priority: "medium",
25
+ due_date: null,
26
+ metadata: null,
27
+ parent_id: null,
28
+ source_id: null,
29
+ context: null,
30
+ require_review: null,
31
+ dependencies: null,
32
+ result: null,
33
+ failure_reason: null,
34
+ workspace_id: null,
35
+ project_id: null,
36
+ files: null,
37
+ created_at: "2026-01-01T00:00:00.000Z",
38
+ updated_at: "2026-01-01T00:00:00.000Z",
39
+ });
40
+
41
+ describe("PostgreSQLTaskStore.create", () => {
42
+ const pool = { query: mockQuery } as unknown as Pool;
43
+
44
+ beforeEach(() => {
45
+ jest.clearAllMocks();
46
+ });
47
+
48
+ it("honors a caller-provided id", async () => {
49
+ const store = new PostgreSQLTaskStore({ pool });
50
+ mockQuery
51
+ .mockResolvedValueOnce({ rows: [] }) // INSERT
52
+ .mockResolvedValueOnce({ rows: [fullRow("a2a-task-1")] }); // getById
53
+
54
+ const result = await store.create({
55
+ id: "a2a-task-1",
56
+ tenantId: "t1",
57
+ ownerType: "agent",
58
+ ownerId: "owner-1",
59
+ title: "t",
60
+ });
61
+
62
+ expect(result.id).toBe("a2a-task-1");
63
+ const insertParams = mockQuery.mock.calls[0][1] as unknown[];
64
+ expect(insertParams[0]).toBe("a2a-task-1");
65
+ });
66
+
67
+ it("generates a uuid when id is omitted", async () => {
68
+ const store = new PostgreSQLTaskStore({ pool });
69
+ mockQuery
70
+ .mockResolvedValueOnce({ rows: [] }) // INSERT
71
+ .mockResolvedValueOnce({ rows: [fullRow("some-uuid")] }); // getById
72
+
73
+ await store.create({
74
+ tenantId: "t1",
75
+ ownerType: "agent",
76
+ ownerId: "owner-1",
77
+ title: "t",
78
+ });
79
+
80
+ const insertParams = mockQuery.mock.calls[0][1] as unknown[];
81
+ expect(insertParams[0]).toMatch(
82
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
83
+ );
84
+ });
85
+
86
+ it("uses one conditional UPDATE that blocks current statuses", async () => {
87
+ const store = new PostgreSQLTaskStore({ pool });
88
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 });
89
+
90
+ const result = await store.updateIfStatusNotIn(
91
+ "t1",
92
+ "task-1",
93
+ { status: "completed", result: "done" },
94
+ ["cancelled"],
95
+ );
96
+
97
+ expect(result?.id).toBe("task-1");
98
+ expect(mockQuery).toHaveBeenCalledTimes(1);
99
+ const [sql, params] = mockQuery.mock.calls[0] as unknown as [string, unknown[]];
100
+ expect(sql).toContain("status <> ALL");
101
+ expect(sql).toContain("RETURNING *");
102
+ expect(params).toContainEqual(["cancelled"]);
103
+ expect(sql).toContain("updated_at::timestamptz + interval '1 millisecond'");
104
+ });
105
+
106
+ it("casts varchar updated_at before interval arithmetic on ordinary metadata updates", async () => {
107
+ const store = new PostgreSQLTaskStore({ pool });
108
+ mockQuery
109
+ .mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 })
110
+ .mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 });
111
+
112
+ await store.update("t1", "task-1", { metadata: { trainingRound: true } });
113
+
114
+ const updateSql = mockQuery.mock.calls[1][0] as string;
115
+ expect(updateSql).toContain("updated_at::timestamptz + interval '1 millisecond'");
116
+ expect(updateSql).toContain("to_char");
117
+ expect(updateSql).toMatch(/CASE\s+WHEN updated_at ~/);
118
+ expect(updateSql).toContain("substring(updated_at FROM 6 FOR 2)::integer BETWEEN 1 AND 12");
119
+ expect(updateSql).toContain("substring(updated_at FROM 9 FOR 2)::integer BETWEEN 1 AND CASE");
120
+ expect(updateSql).toContain("updated_at <> '9999-12-31T23:59:59.999Z'");
121
+ expect(updateSql).not.toContain("updated_at + interval '1 millisecond'");
122
+ expect(updateSql).toContain("RETURNING *");
123
+ });
124
+
125
+ it("returns null when ordinary update cannot advance the maximum timestamp", async () => {
126
+ const store = new PostgreSQLTaskStore({ pool });
127
+ mockQuery
128
+ .mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 })
129
+ .mockResolvedValueOnce({ rows: [], rowCount: 0 });
130
+
131
+ await expect(store.update("t1", "task-1", { title: "Cannot advance" })).resolves.toBeNull();
132
+ });
133
+
134
+ it("treats context undefined as an ordinary update no-op", async () => {
135
+ const store = new PostgreSQLTaskStore({ pool });
136
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 });
137
+
138
+ const result = await store.update("t1", "task-1", { context: undefined });
139
+
140
+ expect(result?.id).toBe("task-1");
141
+ expect(mockQuery).toHaveBeenCalledTimes(1);
142
+ });
143
+
144
+ it("returns null when the conditional update is blocked", async () => {
145
+ const store = new PostgreSQLTaskStore({ pool });
146
+ mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
147
+
148
+ await expect(
149
+ store.updateIfStatusNotIn("t1", "task-1", { status: "completed" }, ["cancelled"]),
150
+ ).resolves.toBeNull();
151
+ });
152
+
153
+ it("uses one conditional UPDATE restricted to expected statuses", async () => {
154
+ const store = new PostgreSQLTaskStore({ pool });
155
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 });
156
+
157
+ await store.updateIfStatusIn(
158
+ "t1", "task-1", { status: "completed" }, ["in_progress", "interrupted"],
159
+ );
160
+
161
+ expect(mockQuery).toHaveBeenCalledTimes(1);
162
+ const [sql, params] = mockQuery.mock.calls[0] as unknown as [string, unknown[]];
163
+ expect(sql).toContain("status = ANY");
164
+ expect(sql).toContain("RETURNING *");
165
+ expect(params).toContainEqual(["in_progress", "interrupted"]);
166
+ expect(sql).toContain("updated_at::timestamptz + interval '1 millisecond'");
167
+ });
168
+
169
+ it("returns null when no expected status matches or the task is missing", async () => {
170
+ const store = new PostgreSQLTaskStore({ pool });
171
+ mockQuery.mockResolvedValue({ rows: [], rowCount: 0 });
172
+
173
+ await expect(store.updateIfStatusIn(
174
+ "t1", "task-1", { status: "completed" }, ["in_progress"],
175
+ )).resolves.toBeNull();
176
+ await expect(store.updateIfStatusIn(
177
+ "t1", "missing", { status: "completed" }, ["in_progress"],
178
+ )).resolves.toBeNull();
179
+ });
180
+
181
+ it("uses one conditional UPDATE restricted to status and updatedAt snapshot", async () => {
182
+ const store = new PostgreSQLTaskStore({ pool });
183
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 });
184
+ const snapshot = new Date("2026-01-01T00:00:00.000Z");
185
+
186
+ const result = await store.updateIfStatusAndUpdatedAt(
187
+ "t1", "task-1", { status: "completed" }, ["in_progress"], snapshot,
188
+ );
189
+
190
+ expect(result?.id).toBe("task-1");
191
+ expect(mockQuery).toHaveBeenCalledTimes(1);
192
+ const [sql, params] = mockQuery.mock.calls[0] as unknown as [string, unknown[]];
193
+ expect(sql).toContain("status = ANY");
194
+ expect(sql).toContain("updated_at =");
195
+ expect(sql).toContain("date_trunc('milliseconds'");
196
+ expect(sql).toContain("updated_at::timestamptz + interval '1 millisecond'");
197
+ expect(sql).toMatch(/CASE WHEN CASE\s+WHEN updated_at ~/);
198
+ expect(sql).toContain("THEN updated_at::timestamptz =");
199
+ expect(sql).toContain("ELSE FALSE");
200
+ expect(sql).toContain("RETURNING *");
201
+ expect(params).toContain(snapshot.toISOString());
202
+ });
203
+
204
+ it("returns null when the status matches but updatedAt snapshot is stale", async () => {
205
+ const store = new PostgreSQLTaskStore({ pool });
206
+ mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
207
+
208
+ await expect(store.updateIfStatusAndUpdatedAt(
209
+ "t1", "task-1", { description: "stale overwrite" }, ["in_progress"], "2026-01-01T00:00:00.000Z",
210
+ )).resolves.toBeNull();
211
+ });
212
+
213
+ it("lets only one caller update the same snapshot", async () => {
214
+ const store = new PostgreSQLTaskStore({ pool });
215
+ mockQuery
216
+ .mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 })
217
+ .mockResolvedValueOnce({ rows: [], rowCount: 0 });
218
+ const snapshot = "2026-01-01T00:00:00.000Z";
219
+
220
+ const [first, second] = await Promise.all([
221
+ store.updateIfStatusAndUpdatedAt("t1", "task-1", { description: "first" }, ["in_progress"], snapshot),
222
+ store.updateIfStatusAndUpdatedAt("t1", "task-1", { description: "second" }, ["in_progress"], snapshot),
223
+ ]);
224
+
225
+ expect(first?.id).toBe("task-1");
226
+ expect(second).toBeNull();
227
+ });
228
+
229
+ it("passes null lifecycle outputs through a snapshot update as SQL NULL", async () => {
230
+ const store = new PostgreSQLTaskStore({ pool });
231
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("task-1")], rowCount: 1 });
232
+
233
+ await store.updateIfStatusAndUpdatedAt(
234
+ "t1",
235
+ "task-1",
236
+ { result: null, failureReason: null, context: null },
237
+ ["failed"],
238
+ "2026-01-01T00:00:00.000Z",
239
+ );
240
+
241
+ const [sql, params] = mockQuery.mock.calls[0] as unknown as [string, unknown[]];
242
+ expect(sql).toContain("result =");
243
+ expect(sql).toContain("failure_reason =");
244
+ expect(sql).toContain("context =");
245
+ expect(params.filter((value) => value === null)).toHaveLength(3);
246
+ });
247
+
248
+ it("uses one UPDATE with exact child and parent snapshot predicates", async () => {
249
+ const store = new PostgreSQLTaskStore({ pool });
250
+ mockQuery.mockResolvedValueOnce({ rows: [fullRow("child-1")], rowCount: 1 });
251
+ const childSnapshot = "2026-01-01T00:00:00.000Z";
252
+ const parentSnapshot = "2026-01-01T00:00:01.000Z";
253
+
254
+ const result = await store.updateIfStatusUpdatedAtAndParentUpdatedAt(
255
+ "t1", "child-1", { status: "completed" }, ["in_progress"], childSnapshot,
256
+ "parent-1", parentSnapshot,
257
+ );
258
+
259
+ expect(result?.id).toBe("child-1");
260
+ expect(mockQuery).toHaveBeenCalledTimes(1);
261
+ const [sql, params] = mockQuery.mock.calls[0] as unknown as [string, unknown[]];
262
+ expect(sql).toContain("UPDATE lattice_tasks AS child");
263
+ expect(sql).toContain("EXISTS");
264
+ expect(sql).toContain("parent.tenant_id = child.tenant_id");
265
+ expect(sql).toContain("parent.id =");
266
+ expect(sql).toContain("child.updated_at::timestamptz + interval '1 millisecond'");
267
+ expect(sql).toMatch(/CASE WHEN CASE\s+WHEN child\.updated_at ~/);
268
+ expect(sql).toContain("THEN child.updated_at::timestamptz =");
269
+ expect(sql).toMatch(/CASE WHEN CASE\s+WHEN parent\.updated_at ~/);
270
+ expect(sql).toContain("THEN parent.updated_at::timestamptz =");
271
+ expect(sql.match(/<> '9999-12-31T23:59:59\.999Z'/g)).toHaveLength(2);
272
+ expect(params).toEqual(expect.arrayContaining([
273
+ "t1", "child-1", ["in_progress"], childSnapshot, "parent-1", parentSnapshot,
274
+ ]));
275
+ });
276
+
277
+ it("returns null when the parent snapshot predicate does not match", async () => {
278
+ const store = new PostgreSQLTaskStore({ pool });
279
+ mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 });
280
+
281
+ await expect(store.updateIfStatusUpdatedAtAndParentUpdatedAt(
282
+ "t1", "child-1", { status: "completed" }, ["in_progress"], "2026-01-01T00:00:00.000Z",
283
+ "missing-parent", "2026-01-01T00:00:01.000Z",
284
+ )).resolves.toBeNull();
285
+ });
286
+
287
+ });