@axiom-lattice/pg-stores 5.0.0 → 5.0.1

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.
@@ -0,0 +1,575 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Pool } from "pg";
3
+
4
+ import { PostgreSQLEvalStore } from "../stores/PostgreSQLEvalStore";
5
+
6
+ const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL;
7
+ const describeIfDatabase = TEST_DATABASE_URL ? describe : describe.skip;
8
+
9
+ describeIfDatabase("PostgreSQLEvalStore integration", () => {
10
+ let store: PostgreSQLEvalStore;
11
+ let pool: Pool;
12
+ const tenants = new Set<string>();
13
+
14
+ beforeAll(async () => {
15
+ store = new PostgreSQLEvalStore({
16
+ poolConfig: TEST_DATABASE_URL!,
17
+ autoMigrate: true,
18
+ });
19
+ await store.initialize();
20
+ pool = new Pool({ connectionString: TEST_DATABASE_URL });
21
+ });
22
+
23
+ afterEach(async () => {
24
+ if (tenants.size === 0) {
25
+ return;
26
+ }
27
+ const ids = [...tenants];
28
+ await pool.query(
29
+ `DELETE FROM lattice_eval_run_results
30
+ WHERE run_id IN (SELECT id FROM lattice_eval_runs WHERE tenant_id = ANY($1::text[]))`,
31
+ [ids],
32
+ );
33
+ await pool.query("DELETE FROM lattice_eval_runs WHERE tenant_id = ANY($1::text[])", [ids]);
34
+ await pool.query("DELETE FROM lattice_eval_cases WHERE tenant_id = ANY($1::text[])", [ids]);
35
+ await pool.query("DELETE FROM lattice_eval_suites WHERE tenant_id = ANY($1::text[])", [ids]);
36
+ await pool.query("DELETE FROM lattice_eval_projects WHERE tenant_id = ANY($1::text[])", [ids]);
37
+ tenants.clear();
38
+ });
39
+
40
+ afterAll(async () => {
41
+ await pool.end();
42
+ await store.dispose();
43
+ });
44
+
45
+ function tenantId(): string {
46
+ const id = `eval-${randomUUID()}`;
47
+ tenants.add(id);
48
+ return id;
49
+ }
50
+
51
+ async function createProject(tenant: string, name = "Project") {
52
+ return store.createProject(tenant, randomUUID(), {
53
+ name,
54
+ description: "A test project",
55
+ version: "1.0.0",
56
+ judgeModelConfig: { provider: "openai", model: "gpt-4o" },
57
+ targetServerConfig: { baseUrl: "https://agent.example" },
58
+ concurrency: 2,
59
+ reportConfig: { format: "markdown" },
60
+ });
61
+ }
62
+
63
+ async function createSuite(tenant: string, projectId: string, name = "Suite") {
64
+ return store.createSuite(tenant, projectId, randomUUID(), { name });
65
+ }
66
+
67
+ async function createCase(tenant: string, suiteId: string) {
68
+ return store.createCase(tenant, suiteId, randomUUID(), {
69
+ inputMessage: "Approve the payment",
70
+ inputFiles: { receipt: "file-1" },
71
+ steps: [{ agent_id: "executor-1", override_message: "go" }],
72
+ outputType: "file_content",
73
+ contentAssertion: "payment executed",
74
+ rubrics: [{ name: "accuracy", weight: 0.5, description: "Correct result" }],
75
+ interruptPolicy: { mode: "canned-response", value: "同意" },
76
+ });
77
+ }
78
+
79
+ async function createRun(tenant: string, projectId: string) {
80
+ return store.createRun(tenant, projectId, randomUUID(), {
81
+ totalCases: 4,
82
+ concurrency: 2,
83
+ holdout: true,
84
+ envProjectId: "env-project",
85
+ envWorkspaceId: "env-workspace",
86
+ taskId: "task-1",
87
+ });
88
+ }
89
+
90
+ async function createResult(tenant: string, runId: string) {
91
+ return store.createRunResult(tenant, runId, randomUUID(), {
92
+ suiteName: "main",
93
+ pass: true,
94
+ score: 0.9,
95
+ summary: "looks good",
96
+ dimensionResults: [{ name: "quality", score: 0.9, reason: "clear" }],
97
+ durationMs: 120,
98
+ messages: [{ role: "assistant", content: "done", id: "m1" }],
99
+ logs: [{ timestamp: "2026-01-01T00:00:00.000Z", level: "info", message: "ran" }],
100
+ interrupted: false,
101
+ });
102
+ }
103
+
104
+ describe("projects", () => {
105
+ it("creates a project and reads it back by id", async () => {
106
+ const tenant = tenantId();
107
+ const project = await createProject(tenant, "Support bot");
108
+
109
+ expect(project.tenantId).toBe(tenant);
110
+ expect(project.name).toBe("Support bot");
111
+ expect(project.description).toBe("A test project");
112
+ expect(project.version).toBe("1.0.0");
113
+ expect(project.judgeModelConfig).toEqual({ provider: "openai", model: "gpt-4o" });
114
+ expect(project.targetServerConfig).toEqual({ baseUrl: "https://agent.example" });
115
+ expect(project.concurrency).toBe(2);
116
+ expect(project.reportConfig).toEqual({ format: "markdown" });
117
+ expect(project.createdAt).toBeInstanceOf(Date);
118
+ expect(project.updatedAt).toBeInstanceOf(Date);
119
+
120
+ const read = await store.getProjectById(tenant, project.id);
121
+ expect(read).toMatchObject({
122
+ id: project.id,
123
+ tenantId: tenant,
124
+ name: "Support bot",
125
+ concurrency: 2,
126
+ });
127
+ });
128
+
129
+ it("lists only the requested tenant's projects", async () => {
130
+ const tenantA = tenantId();
131
+ const tenantB = tenantId();
132
+ const a1 = await createProject(tenantA, "A1");
133
+ const a2 = await createProject(tenantA, "A2");
134
+ const b1 = await createProject(tenantB, "B1");
135
+
136
+ const listedA = await store.getProjectsByTenant(tenantA);
137
+ expect(listedA.map((p) => p.id).sort()).toEqual([a1.id, a2.id].sort());
138
+ expect(listedA.every((p) => p.tenantId === tenantA)).toBe(true);
139
+
140
+ const listedB = await store.getProjectsByTenant(tenantB);
141
+ expect(listedB.map((p) => p.id)).toEqual([b1.id]);
142
+ });
143
+
144
+ it("updates only the provided project fields", async () => {
145
+ const tenant = tenantId();
146
+ const project = await createProject(tenant, "Before");
147
+
148
+ const updated = await store.updateProject(tenant, project.id, {
149
+ name: "After",
150
+ concurrency: 5,
151
+ });
152
+
153
+ expect(updated).toMatchObject({
154
+ id: project.id,
155
+ name: "After",
156
+ concurrency: 5,
157
+ description: "A test project",
158
+ version: "1.0.0",
159
+ });
160
+ });
161
+
162
+ it("returns null / false for an absent project id", async () => {
163
+ const tenant = tenantId();
164
+ const missing = randomUUID();
165
+
166
+ expect(await store.getProjectById(tenant, missing)).toBeNull();
167
+ expect(await store.updateProject(tenant, missing, { name: "nope" })).toBeNull();
168
+ expect(await store.deleteProject(tenant, missing)).toBe(false);
169
+ expect(await store.getProjectReport(tenant, missing)).toBeNull();
170
+ });
171
+
172
+ it("deletes a project", async () => {
173
+ const tenant = tenantId();
174
+ const project = await createProject(tenant);
175
+
176
+ expect(await store.deleteProject(tenant, project.id)).toBe(true);
177
+ expect(await store.getProjectById(tenant, project.id)).toBeNull();
178
+ expect(await store.deleteProject(tenant, project.id)).toBe(false);
179
+ });
180
+
181
+ it("defaults an omitted judgeModelConfig to an empty object", async () => {
182
+ const tenant = tenantId();
183
+ const project = await store.createProject(tenant, randomUUID(), {
184
+ name: "No judge configured",
185
+ targetServerConfig: { baseUrl: "https://agent.example" },
186
+ });
187
+
188
+ expect(project.judgeModelConfig).toEqual({});
189
+
190
+ const read = await store.getProjectById(tenant, project.id);
191
+ expect(read!.judgeModelConfig).toEqual({});
192
+ });
193
+ });
194
+
195
+ describe("suites", () => {
196
+ it("creates suites scoped to a project and lists them with case counts", async () => {
197
+ const tenant = tenantId();
198
+ const project = await createProject(tenant);
199
+ const otherProject = await createProject(tenant, "Other");
200
+ const suiteA = await createSuite(tenant, project.id, "Smoke");
201
+ const suiteB = await createSuite(tenant, project.id, "Regression");
202
+ await createSuite(tenant, otherProject.id, "Unrelated");
203
+ await createCase(tenant, suiteA.id);
204
+
205
+ const listed = await store.getSuitesByProject(tenant, project.id);
206
+ expect(listed.map((s) => s.id).sort()).toEqual([suiteA.id, suiteB.id].sort());
207
+ expect(listed.find((s) => s.id === suiteA.id)?.caseCount).toBe(1);
208
+ expect(listed.find((s) => s.id === suiteB.id)?.caseCount).toBe(0);
209
+
210
+ const read = await store.getSuiteById(tenant, suiteA.id);
211
+ expect(read).toMatchObject({
212
+ id: suiteA.id,
213
+ tenantId: tenant,
214
+ projectId: project.id,
215
+ name: "Smoke",
216
+ caseCount: 1,
217
+ });
218
+ });
219
+
220
+ it("updates a suite and returns null / false for an absent id", async () => {
221
+ const tenant = tenantId();
222
+ const project = await createProject(tenant);
223
+ const suite = await createSuite(tenant, project.id, "Before");
224
+
225
+ const updated = await store.updateSuite(tenant, suite.id, { name: "After" });
226
+ expect(updated).toMatchObject({ id: suite.id, name: "After", projectId: project.id });
227
+
228
+ const missing = randomUUID();
229
+ expect(await store.getSuiteById(tenant, missing)).toBeNull();
230
+ expect(await store.updateSuite(tenant, missing, { name: "nope" })).toBeNull();
231
+ expect(await store.deleteSuite(tenant, missing)).toBe(false);
232
+ });
233
+
234
+ it("deletes a suite", async () => {
235
+ const tenant = tenantId();
236
+ const project = await createProject(tenant);
237
+ const suite = await createSuite(tenant, project.id);
238
+
239
+ expect(await store.deleteSuite(tenant, suite.id)).toBe(true);
240
+ expect(await store.getSuiteById(tenant, suite.id)).toBeNull();
241
+ });
242
+ });
243
+
244
+ describe("cases", () => {
245
+ it("round-trips case fields including interruptPolicy", async () => {
246
+ const tenant = tenantId();
247
+ const project = await createProject(tenant);
248
+ const suite = await createSuite(tenant, project.id);
249
+
250
+ const createdCase = await createCase(tenant, suite.id);
251
+
252
+ expect(createdCase).toMatchObject({
253
+ tenantId: tenant,
254
+ suiteId: suite.id,
255
+ inputMessage: "Approve the payment",
256
+ inputFiles: { receipt: "file-1" },
257
+ steps: [{ agent_id: "executor-1", override_message: "go" }],
258
+ outputType: "file_content",
259
+ contentAssertion: "payment executed",
260
+ rubrics: [{ name: "accuracy", weight: 0.5, description: "Correct result" }],
261
+ interruptPolicy: { mode: "canned-response", value: "同意" },
262
+ });
263
+
264
+ const read = await store.getCaseById(tenant, createdCase.id);
265
+ expect(read).toEqual(createdCase);
266
+
267
+ const listed = await store.getCasesBySuite(tenant, suite.id);
268
+ expect(listed.map((c) => c.id)).toEqual([createdCase.id]);
269
+ });
270
+
271
+ it("applies defaults for omitted optional case fields", async () => {
272
+ const tenant = tenantId();
273
+ const project = await createProject(tenant);
274
+ const suite = await createSuite(tenant, project.id);
275
+
276
+ const createdCase = await store.createCase(tenant, suite.id, randomUUID(), {
277
+ inputMessage: "hello",
278
+ steps: [{ agent_id: "executor-1" }],
279
+ outputType: "message_content",
280
+ contentAssertion: "",
281
+ });
282
+
283
+ expect(createdCase.inputFiles).toEqual({});
284
+ expect(createdCase.rubrics).toEqual([]);
285
+ expect(createdCase.contentAssertion).toBe("");
286
+ expect(createdCase.interruptPolicy).toBeUndefined();
287
+ });
288
+
289
+ it("updates and deletes a case", async () => {
290
+ const tenant = tenantId();
291
+ const project = await createProject(tenant);
292
+ const suite = await createSuite(tenant, project.id);
293
+ const createdCase = await createCase(tenant, suite.id);
294
+
295
+ const updated = await store.updateCase(tenant, createdCase.id, {
296
+ inputMessage: "updated",
297
+ interruptPolicy: { mode: "auto-approve" },
298
+ });
299
+
300
+ expect(updated).toMatchObject({
301
+ id: createdCase.id,
302
+ inputMessage: "updated",
303
+ interruptPolicy: { mode: "auto-approve" },
304
+ contentAssertion: "payment executed",
305
+ });
306
+
307
+ expect(await store.deleteCase(tenant, createdCase.id)).toBe(true);
308
+ expect(await store.getCaseById(tenant, createdCase.id)).toBeNull();
309
+
310
+ const missing = randomUUID();
311
+ expect(await store.getCaseById(tenant, missing)).toBeNull();
312
+ expect(await store.updateCase(tenant, missing, { inputMessage: "nope" })).toBeNull();
313
+ expect(await store.deleteCase(tenant, missing)).toBe(false);
314
+ });
315
+ });
316
+
317
+ describe("runs", () => {
318
+ it("creates runs and filters them by project and status", async () => {
319
+ const tenant = tenantId();
320
+ const project = await createProject(tenant);
321
+ const otherProject = await createProject(tenant, "Other");
322
+ const runA = await createRun(tenant, project.id);
323
+ const runB = await createRun(tenant, project.id);
324
+ const runC = await createRun(tenant, otherProject.id);
325
+ await store.updateRunStatus(tenant, runB.id, { status: "completed" });
326
+
327
+ expect(runA.status).toBe("running");
328
+ expect(runA.totalCases).toBe(4);
329
+ expect(runA.concurrency).toBe(2);
330
+ expect(runA.holdout).toBe(true);
331
+ expect(runA.envProjectId).toBe("env-project");
332
+ expect(runA.envWorkspaceId).toBe("env-workspace");
333
+ expect(runA.taskId).toBe("task-1");
334
+ expect(runA.startedAt).toBeInstanceOf(Date);
335
+
336
+ const byProject = await store.getRunsByTenant(tenant, { projectId: project.id });
337
+ expect(byProject.map((r) => r.id).sort()).toEqual([runA.id, runB.id].sort());
338
+
339
+ const completed = await store.getRunsByTenant(tenant, { status: "completed" });
340
+ expect(completed.map((r) => r.id)).toEqual([runB.id]);
341
+ expect(completed[0].id).not.toBe(runC.id);
342
+ });
343
+
344
+ it("updates run status counters and timestamps", async () => {
345
+ const tenant = tenantId();
346
+ const project = await createProject(tenant);
347
+ const run = await createRun(tenant, project.id);
348
+ const completedAt = new Date("2026-01-02T03:04:05.000Z");
349
+
350
+ const updated = await store.updateRunStatus(tenant, run.id, {
351
+ status: "completed",
352
+ passedCases: 2,
353
+ failedCases: 1,
354
+ interruptedCases: 1,
355
+ avgScore: 0.6,
356
+ completedAt,
357
+ });
358
+
359
+ expect(updated).toMatchObject({
360
+ id: run.id,
361
+ status: "completed",
362
+ totalCases: 4,
363
+ passedCases: 2,
364
+ failedCases: 1,
365
+ interruptedCases: 1,
366
+ avgScore: 0.6,
367
+ });
368
+ expect(updated!.completedAt?.toISOString()).toBe(completedAt.toISOString());
369
+
370
+ const read = await store.getRunById(tenant, run.id);
371
+ expect(read).toMatchObject({
372
+ status: "completed",
373
+ passedCases: 2,
374
+ interruptedCases: 1,
375
+ avgScore: 0.6,
376
+ });
377
+ });
378
+
379
+ it("requests cancellation only while the run is running", async () => {
380
+ const tenant = tenantId();
381
+ const project = await createProject(tenant);
382
+ const run = await createRun(tenant, project.id);
383
+
384
+ expect(await store.requestCancel(tenant, run.id)).toBe(true);
385
+ expect((await store.getRunById(tenant, run.id))?.cancelRequested).toBe(true);
386
+
387
+ await store.updateRunStatus(tenant, run.id, { status: "completed" });
388
+ expect(await store.requestCancel(tenant, run.id)).toBe(false);
389
+
390
+ const failedRun = await createRun(tenant, project.id);
391
+ await store.updateRunStatus(tenant, failedRun.id, { status: "failed" });
392
+ expect(await store.requestCancel(tenant, failedRun.id)).toBe(false);
393
+
394
+ const abortedRun = await createRun(tenant, project.id);
395
+ await store.updateRunStatus(tenant, abortedRun.id, { status: "aborted" });
396
+ expect(await store.requestCancel(tenant, abortedRun.id)).toBe(false);
397
+
398
+ expect(await store.requestCancel(tenant, randomUUID())).toBe(false);
399
+ });
400
+
401
+ it("deletes a run together with its results", async () => {
402
+ const tenant = tenantId();
403
+ const project = await createProject(tenant);
404
+ const run = await createRun(tenant, project.id);
405
+ await createResult(tenant, run.id);
406
+
407
+ expect(await store.deleteRun(tenant, run.id)).toBe(true);
408
+ expect(await store.getRunById(tenant, run.id)).toBeNull();
409
+
410
+ const remaining = await pool.query<{ count: string }>(
411
+ "SELECT COUNT(*) AS count FROM lattice_eval_run_results WHERE run_id = $1",
412
+ [run.id],
413
+ );
414
+ expect(remaining.rows[0].count).toBe("0");
415
+ });
416
+
417
+ it("returns null for an absent run id", async () => {
418
+ const tenant = tenantId();
419
+ const missing = randomUUID();
420
+
421
+ expect(await store.getRunById(tenant, missing)).toBeNull();
422
+ expect(await store.updateRunStatus(tenant, missing, { status: "completed" })).toBeNull();
423
+ expect(await store.deleteRun(tenant, missing)).toBe(false);
424
+ });
425
+ });
426
+
427
+ describe("run results", () => {
428
+ it("creates and lists run results", async () => {
429
+ const tenant = tenantId();
430
+ const project = await createProject(tenant);
431
+ const run = await createRun(tenant, project.id);
432
+
433
+ const result = await createResult(tenant, run.id);
434
+ expect(result).toMatchObject({
435
+ runId: run.id,
436
+ suiteName: "main",
437
+ pass: true,
438
+ score: 0.9,
439
+ summary: "looks good",
440
+ durationMs: 120,
441
+ interrupted: false,
442
+ });
443
+ expect(result.dimensionResults).toEqual([
444
+ { name: "quality", score: 0.9, reason: "clear" },
445
+ ]);
446
+ expect(result.messages).toEqual([{ role: "assistant", content: "done", id: "m1" }]);
447
+ expect(result.logs).toEqual([
448
+ { timestamp: "2026-01-01T00:00:00.000Z", level: "info", message: "ran" },
449
+ ]);
450
+
451
+ const listed = await store.getResultsByRun(tenant, run.id);
452
+ expect(listed.map((r) => r.id)).toEqual([result.id]);
453
+ });
454
+
455
+ it("updates and deletes a run result", async () => {
456
+ const tenant = tenantId();
457
+ const project = await createProject(tenant);
458
+ const run = await createRun(tenant, project.id);
459
+ const result = await createResult(tenant, run.id);
460
+
461
+ const updated = await store.updateRunResult(tenant, result.id, {
462
+ pass: false,
463
+ score: 0.25,
464
+ summary: "needs work",
465
+ durationMs: 500,
466
+ });
467
+
468
+ expect(updated).toMatchObject({
469
+ id: result.id,
470
+ runId: run.id,
471
+ pass: false,
472
+ score: 0.25,
473
+ summary: "needs work",
474
+ durationMs: 500,
475
+ });
476
+
477
+ expect(await store.deleteRunResult(tenant, result.id)).toBe(true);
478
+ expect(await store.deleteRunResult(tenant, result.id)).toBe(false);
479
+ expect(await store.getResultsByRun(tenant, run.id)).toEqual([]);
480
+ });
481
+
482
+ it("returns the updated interrupted flag from updateRunResult", async () => {
483
+ const tenant = tenantId();
484
+ const project = await createProject(tenant);
485
+ const run = await createRun(tenant, project.id);
486
+ const result = await createResult(tenant, run.id);
487
+
488
+ const updated = await store.updateRunResult(tenant, result.id, { interrupted: true });
489
+
490
+ expect(updated?.interrupted).toBe(true);
491
+ const [read] = await store.getResultsByRun(tenant, run.id);
492
+ expect(read.interrupted).toBe(true);
493
+ });
494
+
495
+ it("rejects moving a result to another run", async () => {
496
+ const tenant = tenantId();
497
+ const project = await createProject(tenant);
498
+ const run = await createRun(tenant, project.id);
499
+ const result = await createResult(tenant, run.id);
500
+
501
+ await expect(
502
+ store.updateRunResult(tenant, result.id, { runId: randomUUID() }),
503
+ ).rejects.toThrow("runId cannot be updated on an existing result");
504
+ });
505
+ });
506
+
507
+ describe("project report", () => {
508
+ it("averages scores and takes the pass rate from the newest run", async () => {
509
+ const tenant = tenantId();
510
+ const project = await createProject(tenant, "Reported");
511
+ const otherProject = await createProject(tenant, "Unrelated");
512
+ const olderRun = await createRun(tenant, project.id);
513
+ const newerRun = await createRun(tenant, project.id);
514
+ const otherRun = await createRun(tenant, otherProject.id);
515
+
516
+ await store.updateRunStatus(tenant, olderRun.id, {
517
+ status: "completed",
518
+ passedCases: 1,
519
+ failedCases: 3,
520
+ avgScore: 0.25,
521
+ });
522
+ await store.updateRunStatus(tenant, newerRun.id, {
523
+ status: "completed",
524
+ passedCases: 3,
525
+ failedCases: 1,
526
+ avgScore: 0.75,
527
+ });
528
+ await store.updateRunStatus(tenant, otherRun.id, {
529
+ status: "completed",
530
+ passedCases: 4,
531
+ avgScore: 1,
532
+ });
533
+ await pool.query("UPDATE lattice_eval_runs SET created_at = $1 WHERE id = $2", [
534
+ new Date("2026-01-01T00:00:00.000Z"),
535
+ olderRun.id,
536
+ ]);
537
+ await pool.query("UPDATE lattice_eval_runs SET created_at = $1 WHERE id = $2", [
538
+ new Date("2026-01-02T00:00:00.000Z"),
539
+ newerRun.id,
540
+ ]);
541
+
542
+ const report = await store.getProjectReport(tenant, project.id);
543
+
544
+ expect(report).toMatchObject({
545
+ projectId: project.id,
546
+ projectName: "Reported",
547
+ totalRuns: 2,
548
+ latestPassRate: 0.75,
549
+ avgScore: 0.5,
550
+ });
551
+ expect(report!.runs.map((r) => r.id)).toEqual([newerRun.id, olderRun.id]);
552
+ });
553
+ });
554
+
555
+ describe("tenant isolation", () => {
556
+ it("does not expose another tenant's project, case, suite, or run", async () => {
557
+ const tenantA = tenantId();
558
+ const tenantB = tenantId();
559
+ const project = await createProject(tenantA);
560
+ const suite = await createSuite(tenantA, project.id);
561
+ const testCase = await createCase(tenantA, suite.id);
562
+ const run = await createRun(tenantA, project.id);
563
+
564
+ expect(await store.getProjectById(tenantB, project.id)).toBeNull();
565
+ expect(await store.getSuiteById(tenantB, suite.id)).toBeNull();
566
+ expect(await store.getCaseById(tenantB, testCase.id)).toBeNull();
567
+ expect(await store.getRunById(tenantB, run.id)).toBeNull();
568
+ expect(await store.getSuitesByProject(tenantB, project.id)).toEqual([]);
569
+ expect(await store.getCasesBySuite(tenantB, suite.id)).toEqual([]);
570
+ expect(await store.getRunsByTenant(tenantB, { projectId: project.id })).toEqual([]);
571
+ expect(await store.updateProject(tenantB, project.id, { name: "hijack" })).toBeNull();
572
+ expect(await store.deleteProject(tenantB, project.id)).toBe(false);
573
+ });
574
+ });
575
+ });