@intx/hub-sessions 0.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.
@@ -0,0 +1,1169 @@
1
+ import { describe, test, expect, afterAll, beforeAll } from "bun:test";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import git from "isomorphic-git";
6
+ import { generateKeyPair } from "@intx/crypto-node";
7
+ import type { KeyPair } from "@intx/types/runtime";
8
+ import { createRepoStore } from "./store";
9
+ import type {
10
+ AuthorizeFn,
11
+ KindHandler,
12
+ Principal,
13
+ RepoAction,
14
+ RepoId,
15
+ ValidatePushResult,
16
+ } from "./types";
17
+
18
+ const tempDirs: string[] = [];
19
+
20
+ async function makeTempDir(prefix: string): Promise<string> {
21
+ const d = await fs.promises.mkdtemp(path.join(os.tmpdir(), prefix));
22
+ tempDirs.push(d);
23
+ return d;
24
+ }
25
+
26
+ let signingKey: KeyPair;
27
+
28
+ beforeAll(async () => {
29
+ signingKey = await generateKeyPair();
30
+ });
31
+
32
+ afterAll(async () => {
33
+ for (const d of tempDirs.splice(0)) {
34
+ await fs.promises.rm(d, { recursive: true, force: true }).catch((_e) => {
35
+ /* best effort cleanup */
36
+ });
37
+ }
38
+ });
39
+
40
+ type RefUpdateRecord = {
41
+ repoId: RepoId;
42
+ ref: string;
43
+ oldSha: string | null;
44
+ newSha: string;
45
+ };
46
+
47
+ type TestHandler = KindHandler & {
48
+ onRefUpdatedCalls: RefUpdateRecord[];
49
+ };
50
+
51
+ function createTestHandler(opts?: {
52
+ allowTopLevelPaths?: (topLevelTreePaths: string[]) => boolean;
53
+ }): TestHandler {
54
+ const onRefUpdatedCalls: RefUpdateRecord[] = [];
55
+ const allowFn = opts?.allowTopLevelPaths;
56
+ return {
57
+ kind: "agent-state",
58
+ directoryPrefix: "repos-under-test",
59
+ validatePush({ topLevelTreePaths }): ValidatePushResult {
60
+ // Ignore the readBlob argument: this fixture only ever needs path-level
61
+ // checks. Real handlers that need blob contents (e.g. skillKindHandler)
62
+ // exercise readBlob in their own dedicated tests.
63
+ if (allowFn === undefined) {
64
+ return { ok: true };
65
+ }
66
+ if (allowFn(topLevelTreePaths)) {
67
+ return { ok: true };
68
+ }
69
+ return { ok: false, reason: "stub rejected push" };
70
+ },
71
+ onRefUpdated(args) {
72
+ onRefUpdatedCalls.push(args);
73
+ },
74
+ onRefUpdatedCalls,
75
+ };
76
+ }
77
+
78
+ const allowAll: AuthorizeFn = () => ({ allowed: true });
79
+
80
+ const principal: Principal = { kind: "test" };
81
+
82
+ const repoId: RepoId = { kind: "agent-state", id: "subject" };
83
+
84
+ const REF = "refs/heads/test";
85
+
86
+ async function readTreePaths(dir: string, treeOid: string): Promise<string[]> {
87
+ const { tree } = await git.readTree({ fs, dir, oid: treeOid });
88
+ return tree.map((e) => e.path);
89
+ }
90
+
91
+ describe("RepoStore", () => {
92
+ test("initRepo is idempotent", async () => {
93
+ const dataDir = await makeTempDir("repo-store-init-");
94
+ const handler = createTestHandler();
95
+ const store = createRepoStore({
96
+ dataDir,
97
+ signingKey,
98
+ handlers: { "agent-state": handler },
99
+ authorize: allowAll,
100
+ });
101
+
102
+ await store.initRepo(repoId);
103
+ await store.initRepo(repoId);
104
+
105
+ const gitDir = path.join(
106
+ dataDir,
107
+ handler.directoryPrefix,
108
+ repoId.id,
109
+ ".git",
110
+ );
111
+ const stat = await fs.promises.stat(gitDir);
112
+ expect(stat.isDirectory()).toBe(true);
113
+ });
114
+
115
+ test("writeTree writes files, signs the commit, advances the ref, calls onRefUpdated", async () => {
116
+ const dataDir = await makeTempDir("repo-store-write-");
117
+ const handler = createTestHandler();
118
+ const store = createRepoStore({
119
+ dataDir,
120
+ signingKey,
121
+ handlers: { "agent-state": handler },
122
+ authorize: allowAll,
123
+ });
124
+
125
+ const { commitSha } = await store.writeTree(principal, repoId, REF, {
126
+ files: {
127
+ "deploy/prompt.md": "hello",
128
+ "workspace/example.txt": "example",
129
+ },
130
+ message: "initial",
131
+ });
132
+
133
+ expect(commitSha).toMatch(/^[0-9a-f]{40}$/);
134
+
135
+ const dir = path.join(dataDir, handler.directoryPrefix, repoId.id);
136
+ const resolved = await git.resolveRef({ fs, dir, ref: REF });
137
+ expect(resolved).toBe(commitSha);
138
+
139
+ const { commit } = await git.readCommit({ fs, dir, oid: commitSha });
140
+ expect(commit.gpgsig).toBeDefined();
141
+ expect(commit.gpgsig).toContain("-----BEGIN SSH SIGNATURE-----");
142
+
143
+ expect(handler.onRefUpdatedCalls).toHaveLength(1);
144
+ const call = handler.onRefUpdatedCalls[0];
145
+ if (!call) throw new Error("unreachable");
146
+ expect(call.ref).toBe(REF);
147
+ expect(call.oldSha).toBeNull();
148
+ expect(call.newSha).toBe(commitSha);
149
+ expect(call.repoId).toEqual(repoId);
150
+ });
151
+
152
+ test("writeTree on an existing ref advances from that ref", async () => {
153
+ const dataDir = await makeTempDir("repo-store-advance-");
154
+ const handler = createTestHandler();
155
+ const store = createRepoStore({
156
+ dataDir,
157
+ signingKey,
158
+ handlers: { "agent-state": handler },
159
+ authorize: allowAll,
160
+ });
161
+
162
+ const first = await store.writeTree(principal, repoId, REF, {
163
+ files: { "deploy/a.md": "v1" },
164
+ message: "first",
165
+ });
166
+ const second = await store.writeTree(principal, repoId, REF, {
167
+ files: { "deploy/a.md": "v2" },
168
+ message: "second",
169
+ });
170
+
171
+ expect(second.commitSha).not.toBe(first.commitSha);
172
+
173
+ const dir = path.join(dataDir, handler.directoryPrefix, repoId.id);
174
+ const { commit } = await git.readCommit({
175
+ fs,
176
+ dir,
177
+ oid: second.commitSha,
178
+ });
179
+ expect(commit.parent).toContain(first.commitSha);
180
+
181
+ expect(handler.onRefUpdatedCalls).toHaveLength(2);
182
+ const secondCall = handler.onRefUpdatedCalls[1];
183
+ if (!secondCall) throw new Error("unreachable");
184
+ expect(secondCall.oldSha).toBe(first.commitSha);
185
+ expect(secondCall.newSha).toBe(second.commitSha);
186
+ });
187
+
188
+ test("writeTree with clearPrefix clears stale tracked files under that prefix", async () => {
189
+ const dataDir = await makeTempDir("repo-store-clear-");
190
+ const handler = createTestHandler();
191
+ const store = createRepoStore({
192
+ dataDir,
193
+ signingKey,
194
+ handlers: { "agent-state": handler },
195
+ authorize: allowAll,
196
+ });
197
+
198
+ await store.writeTree(principal, repoId, REF, {
199
+ files: { "deploy/a.md": "A", "deploy/b.md": "B" },
200
+ clearPrefix: "deploy/",
201
+ message: "first",
202
+ });
203
+ const second = await store.writeTree(principal, repoId, REF, {
204
+ files: { "deploy/a.md": "A2" },
205
+ clearPrefix: "deploy/",
206
+ message: "second",
207
+ });
208
+
209
+ const dir = path.join(dataDir, handler.directoryPrefix, repoId.id);
210
+ const { commit } = await git.readCommit({
211
+ fs,
212
+ dir,
213
+ oid: second.commitSha,
214
+ });
215
+ const rootEntries = await readTreePaths(dir, commit.tree);
216
+ expect(rootEntries).toContain("deploy");
217
+
218
+ const { tree: rootTree } = await git.readTree({
219
+ fs,
220
+ dir,
221
+ oid: commit.tree,
222
+ });
223
+ const deployEntry = rootTree.find((e) => e.path === "deploy");
224
+ if (!deployEntry) throw new Error("deploy subtree missing");
225
+ const deployPaths = await readTreePaths(dir, deployEntry.oid);
226
+ expect(deployPaths).toContain("a.md");
227
+ expect(deployPaths).not.toContain("b.md");
228
+ });
229
+
230
+ test("writeTree without clearPrefix is purely additive", async () => {
231
+ const dataDir = await makeTempDir("repo-store-additive-");
232
+ const handler = createTestHandler();
233
+ const store = createRepoStore({
234
+ dataDir,
235
+ signingKey,
236
+ handlers: { "agent-state": handler },
237
+ authorize: allowAll,
238
+ });
239
+
240
+ await store.writeTree(principal, repoId, REF, {
241
+ files: { "x/a": "1" },
242
+ message: "first",
243
+ });
244
+ const second = await store.writeTree(principal, repoId, REF, {
245
+ files: { "x/b": "2" },
246
+ message: "second",
247
+ });
248
+
249
+ const dir = path.join(dataDir, handler.directoryPrefix, repoId.id);
250
+ const { commit } = await git.readCommit({
251
+ fs,
252
+ dir,
253
+ oid: second.commitSha,
254
+ });
255
+ const { tree: rootTree } = await git.readTree({
256
+ fs,
257
+ dir,
258
+ oid: commit.tree,
259
+ });
260
+ const xEntry = rootTree.find((e) => e.path === "x");
261
+ if (!xEntry) throw new Error("x subtree missing");
262
+ const xPaths = await readTreePaths(dir, xEntry.oid);
263
+ expect(xPaths).toContain("a");
264
+ expect(xPaths).toContain("b");
265
+ });
266
+
267
+ test("receivePack accepts a valid pack and rejects one failing validatePush", async () => {
268
+ const sourceDataDir = await makeTempDir("repo-store-pack-source-");
269
+ const sourceHandler = createTestHandler();
270
+ const sourceStore = createRepoStore({
271
+ dataDir: sourceDataDir,
272
+ signingKey,
273
+ handlers: { "agent-state": sourceHandler },
274
+ authorize: allowAll,
275
+ });
276
+ await sourceStore.writeTree(principal, repoId, REF, {
277
+ files: { "deploy/a.md": "from-source" },
278
+ message: "source content",
279
+ });
280
+ const { pack, commitSha } = await sourceStore.createPack(
281
+ principal,
282
+ repoId,
283
+ REF,
284
+ );
285
+
286
+ const acceptDir = await makeTempDir("repo-store-pack-accept-");
287
+ const acceptHandler = createTestHandler({
288
+ allowTopLevelPaths: () => true,
289
+ });
290
+ const acceptStore = createRepoStore({
291
+ dataDir: acceptDir,
292
+ signingKey,
293
+ handlers: { "agent-state": acceptHandler },
294
+ authorize: allowAll,
295
+ });
296
+ await acceptStore.receivePack(
297
+ principal,
298
+ repoId,
299
+ REF,
300
+ pack,
301
+ commitSha,
302
+ null,
303
+ );
304
+ expect(await acceptStore.resolveRef(principal, repoId, REF)).toBe(
305
+ commitSha,
306
+ );
307
+ expect(acceptHandler.onRefUpdatedCalls).toHaveLength(1);
308
+
309
+ const rejectDir = await makeTempDir("repo-store-pack-reject-");
310
+ const rejectHandler = createTestHandler({
311
+ allowTopLevelPaths: () => false,
312
+ });
313
+ const rejectStore = createRepoStore({
314
+ dataDir: rejectDir,
315
+ signingKey,
316
+ handlers: { "agent-state": rejectHandler },
317
+ authorize: allowAll,
318
+ });
319
+ await expect(
320
+ rejectStore.receivePack(principal, repoId, REF, pack, commitSha, null),
321
+ ).rejects.toThrow(/^path_violation/);
322
+ expect(await rejectStore.resolveRef(principal, repoId, REF)).toBeNull();
323
+ });
324
+
325
+ test("createPack and receivePack round-trip", async () => {
326
+ const sourceDir = await makeTempDir("repo-store-rt-source-");
327
+ const sourceHandler = createTestHandler();
328
+ const sourceStore = createRepoStore({
329
+ dataDir: sourceDir,
330
+ signingKey,
331
+ handlers: { "agent-state": sourceHandler },
332
+ authorize: allowAll,
333
+ });
334
+ const { commitSha } = await sourceStore.writeTree(principal, repoId, REF, {
335
+ files: { "deploy/payload.txt": "round-trip body" },
336
+ message: "rt",
337
+ });
338
+ const { pack } = await sourceStore.createPack(principal, repoId, REF);
339
+
340
+ const targetDir = await makeTempDir("repo-store-rt-target-");
341
+ const targetHandler = createTestHandler({
342
+ allowTopLevelPaths: () => true,
343
+ });
344
+ const targetStore = createRepoStore({
345
+ dataDir: targetDir,
346
+ signingKey,
347
+ handlers: { "agent-state": targetHandler },
348
+ authorize: allowAll,
349
+ });
350
+
351
+ await targetStore.receivePack(
352
+ principal,
353
+ repoId,
354
+ REF,
355
+ pack,
356
+ commitSha,
357
+ null,
358
+ );
359
+ const resolved = await targetStore.resolveRef(principal, repoId, REF);
360
+ expect(resolved).toBe(commitSha);
361
+ });
362
+
363
+ test("resolveRef returns null for a missing ref", async () => {
364
+ const dataDir = await makeTempDir("repo-store-resolve-missing-");
365
+ const handler = createTestHandler();
366
+ const store = createRepoStore({
367
+ dataDir,
368
+ signingKey,
369
+ handlers: { "agent-state": handler },
370
+ authorize: allowAll,
371
+ });
372
+ await store.initRepo(repoId);
373
+ const resolved = await store.resolveRef(
374
+ principal,
375
+ repoId,
376
+ "refs/heads/nonexistent",
377
+ );
378
+ expect(resolved).toBeNull();
379
+ });
380
+
381
+ test("authorize gates each authorize-gated operation", async () => {
382
+ const sourceDir = await makeTempDir("repo-store-authz-source-");
383
+ const sourceHandler = createTestHandler();
384
+ const sourceStore = createRepoStore({
385
+ dataDir: sourceDir,
386
+ signingKey,
387
+ handlers: { "agent-state": sourceHandler },
388
+ authorize: allowAll,
389
+ });
390
+ const { commitSha: existingSha, ...rest } = await sourceStore.writeTree(
391
+ principal,
392
+ repoId,
393
+ REF,
394
+ {
395
+ files: { "deploy/a.md": "pack-source" },
396
+ message: "source",
397
+ },
398
+ );
399
+ void rest;
400
+ const { pack: validPack } = await sourceStore.createPack(
401
+ principal,
402
+ repoId,
403
+ REF,
404
+ );
405
+
406
+ const denyDir = await makeTempDir("repo-store-authz-deny-");
407
+ let denyCallCount = 0;
408
+ const denyAuthorize: AuthorizeFn = () => {
409
+ denyCallCount += 1;
410
+ return { allowed: false, reason: "denied" };
411
+ };
412
+ const denyHandler = createTestHandler({ allowTopLevelPaths: () => true });
413
+ const denyStore = createRepoStore({
414
+ dataDir: denyDir,
415
+ signingKey,
416
+ handlers: { "agent-state": denyHandler },
417
+ authorize: denyAuthorize,
418
+ });
419
+
420
+ await denyStore.initRepo(repoId);
421
+ expect(denyCallCount).toBe(0);
422
+
423
+ await expect(
424
+ denyStore.writeTree(principal, repoId, REF, {
425
+ files: { a: "1" },
426
+ message: "x",
427
+ }),
428
+ ).rejects.toThrow(/^authorize_denied:.*denied/);
429
+ await expect(
430
+ denyStore.receivePack(
431
+ principal,
432
+ repoId,
433
+ REF,
434
+ validPack,
435
+ existingSha,
436
+ null,
437
+ ),
438
+ ).rejects.toThrow(/^authorize_denied:.*denied/);
439
+ await expect(denyStore.createPack(principal, repoId, REF)).rejects.toThrow(
440
+ /^authorize_denied:.*denied/,
441
+ );
442
+ await expect(denyStore.resolveRef(principal, repoId, REF)).rejects.toThrow(
443
+ /^authorize_denied:.*denied/,
444
+ );
445
+
446
+ const partialDir = await makeTempDir("repo-store-authz-partial-");
447
+ let partialCallCount = 0;
448
+ const allowOnlyResolve: AuthorizeFn = (
449
+ _p,
450
+ _r,
451
+ _ref,
452
+ action: RepoAction,
453
+ ) => {
454
+ partialCallCount += 1;
455
+ if (action === "resolveRef") return { allowed: true };
456
+ return { allowed: false, reason: "denied" };
457
+ };
458
+ const partialHandler = createTestHandler({
459
+ allowTopLevelPaths: () => true,
460
+ });
461
+ const partialStore = createRepoStore({
462
+ dataDir: partialDir,
463
+ signingKey,
464
+ handlers: { "agent-state": partialHandler },
465
+ authorize: allowOnlyResolve,
466
+ });
467
+
468
+ await partialStore.initRepo(repoId);
469
+ expect(partialCallCount).toBe(0);
470
+
471
+ const resolvedMissing = await partialStore.resolveRef(
472
+ principal,
473
+ repoId,
474
+ REF,
475
+ );
476
+ expect(resolvedMissing).toBeNull();
477
+ expect(partialCallCount).toBe(1);
478
+
479
+ await expect(
480
+ partialStore.writeTree(principal, repoId, REF, {
481
+ files: { a: "1" },
482
+ message: "x",
483
+ }),
484
+ ).rejects.toThrow(/^authorize_denied/);
485
+ await expect(
486
+ partialStore.receivePack(
487
+ principal,
488
+ repoId,
489
+ REF,
490
+ validPack,
491
+ existingSha,
492
+ null,
493
+ ),
494
+ ).rejects.toThrow(/^authorize_denied/);
495
+ await expect(
496
+ partialStore.createPack(principal, repoId, REF),
497
+ ).rejects.toThrow(/^authorize_denied/);
498
+ });
499
+
500
+ test("onRefUpdated is not called on a failed receivePack", async () => {
501
+ const sourceDir = await makeTempDir("repo-store-fail-source-");
502
+ const sourceHandler = createTestHandler();
503
+ const sourceStore = createRepoStore({
504
+ dataDir: sourceDir,
505
+ signingKey,
506
+ handlers: { "agent-state": sourceHandler },
507
+ authorize: allowAll,
508
+ });
509
+ const { commitSha } = await sourceStore.writeTree(principal, repoId, REF, {
510
+ files: { "deploy/a.md": "pack-source" },
511
+ message: "source",
512
+ });
513
+ const { pack } = await sourceStore.createPack(principal, repoId, REF);
514
+
515
+ const targetDir = await makeTempDir("repo-store-fail-target-");
516
+ const targetHandler = createTestHandler({
517
+ allowTopLevelPaths: () => false,
518
+ });
519
+ const targetStore = createRepoStore({
520
+ dataDir: targetDir,
521
+ signingKey,
522
+ handlers: { "agent-state": targetHandler },
523
+ authorize: allowAll,
524
+ });
525
+
526
+ await expect(
527
+ targetStore.receivePack(principal, repoId, REF, pack, commitSha, null),
528
+ ).rejects.toThrow(/^path_violation/);
529
+
530
+ expect(targetHandler.onRefUpdatedCalls).toHaveLength(0);
531
+ const resolved = await targetStore.resolveRef(principal, repoId, REF);
532
+ expect(resolved).toBeNull();
533
+ });
534
+
535
+ test("writeTree rejects when the handler's validatePush rejects", async () => {
536
+ const dataDir = await makeTempDir("repo-store-writetree-reject-");
537
+ const handler = createTestHandler({
538
+ allowTopLevelPaths: () => false,
539
+ });
540
+ const store = createRepoStore({
541
+ dataDir,
542
+ signingKey,
543
+ handlers: { "agent-state": handler },
544
+ authorize: allowAll,
545
+ });
546
+
547
+ await expect(
548
+ store.writeTree(principal, repoId, REF, {
549
+ files: { "deploy/a.md": "rejected" },
550
+ message: "should be rejected",
551
+ }),
552
+ ).rejects.toThrow(/^path_violation/);
553
+
554
+ expect(handler.onRefUpdatedCalls).toHaveLength(0);
555
+ const resolved = await store.resolveRef(principal, repoId, REF);
556
+ expect(resolved).toBeNull();
557
+ });
558
+
559
+ test("writeTree passes the readBlob callback that resolves declared file contents", async () => {
560
+ const dataDir = await makeTempDir("repo-store-writetree-readblob-");
561
+ const captured: { paths: string[] | null; blob: Uint8Array | null } = {
562
+ paths: null,
563
+ blob: null,
564
+ };
565
+ const handler: TestHandler = {
566
+ kind: "agent-state",
567
+ directoryPrefix: "repos-under-test",
568
+ async validatePush({ topLevelTreePaths, readBlob }) {
569
+ captured.paths = topLevelTreePaths;
570
+ captured.blob = await readBlob("deploy/a.md");
571
+ return { ok: true };
572
+ },
573
+ onRefUpdated() {
574
+ /* no-op */
575
+ },
576
+ onRefUpdatedCalls: [],
577
+ };
578
+ const store = createRepoStore({
579
+ dataDir,
580
+ signingKey,
581
+ handlers: { "agent-state": handler },
582
+ authorize: allowAll,
583
+ });
584
+
585
+ await store.writeTree(principal, repoId, REF, {
586
+ files: { "deploy/a.md": "blob-body", "top.txt": "top" },
587
+ message: "with readBlob",
588
+ });
589
+
590
+ expect(captured.paths?.sort()).toEqual(["deploy", "top.txt"]);
591
+ expect(captured.blob).not.toBeNull();
592
+ expect(new TextDecoder().decode(captured.blob ?? new Uint8Array())).toBe(
593
+ "blob-body",
594
+ );
595
+ });
596
+
597
+ test("validatePush runs on receivePack regardless of authorize verdict", async () => {
598
+ const sourceDir = await makeTempDir("repo-store-bypass-source-");
599
+ const sourceHandler = createTestHandler();
600
+ const sourceStore = createRepoStore({
601
+ dataDir: sourceDir,
602
+ signingKey,
603
+ handlers: { "agent-state": sourceHandler },
604
+ authorize: allowAll,
605
+ });
606
+ const { commitSha } = await sourceStore.writeTree(principal, repoId, REF, {
607
+ files: { "deploy/a.md": "pack-source" },
608
+ message: "source",
609
+ });
610
+ const { pack } = await sourceStore.createPack(principal, repoId, REF);
611
+
612
+ const targetDir = await makeTempDir("repo-store-bypass-target-");
613
+ const targetHandler = createTestHandler({
614
+ allowTopLevelPaths: () => false,
615
+ });
616
+ const targetStore = createRepoStore({
617
+ dataDir: targetDir,
618
+ signingKey,
619
+ handlers: { "agent-state": targetHandler },
620
+ authorize: allowAll,
621
+ });
622
+
623
+ await expect(
624
+ targetStore.receivePack(principal, repoId, REF, pack, commitSha, null),
625
+ ).rejects.toThrow(/^path_violation/);
626
+ });
627
+
628
+ test("receivePack accepts a fresh ref when expectedOldSha is null", async () => {
629
+ const sourceDir = await makeTempDir("repo-store-cas-fresh-source-");
630
+ const sourceHandler = createTestHandler();
631
+ const sourceStore = createRepoStore({
632
+ dataDir: sourceDir,
633
+ signingKey,
634
+ handlers: { "agent-state": sourceHandler },
635
+ authorize: allowAll,
636
+ });
637
+ const { commitSha } = await sourceStore.writeTree(principal, repoId, REF, {
638
+ files: { "deploy/a.md": "v1" },
639
+ message: "v1",
640
+ });
641
+ const { pack } = await sourceStore.createPack(principal, repoId, REF);
642
+
643
+ const targetDir = await makeTempDir("repo-store-cas-fresh-target-");
644
+ const targetHandler = createTestHandler({
645
+ allowTopLevelPaths: () => true,
646
+ });
647
+ const targetStore = createRepoStore({
648
+ dataDir: targetDir,
649
+ signingKey,
650
+ handlers: { "agent-state": targetHandler },
651
+ authorize: allowAll,
652
+ });
653
+
654
+ await targetStore.receivePack(
655
+ principal,
656
+ repoId,
657
+ REF,
658
+ pack,
659
+ commitSha,
660
+ null,
661
+ );
662
+
663
+ expect(await targetStore.resolveRef(principal, repoId, REF)).toBe(
664
+ commitSha,
665
+ );
666
+ expect(targetHandler.onRefUpdatedCalls).toHaveLength(1);
667
+ const call = targetHandler.onRefUpdatedCalls[0];
668
+ if (!call) throw new Error("unreachable");
669
+ expect(call.oldSha).toBeNull();
670
+ expect(call.newSha).toBe(commitSha);
671
+ });
672
+
673
+ test("receivePack rejects when expectedOldSha is stale", async () => {
674
+ const sourceDir = await makeTempDir("repo-store-cas-stale-source-");
675
+ const sourceHandler = createTestHandler();
676
+ const sourceStore = createRepoStore({
677
+ dataDir: sourceDir,
678
+ signingKey,
679
+ handlers: { "agent-state": sourceHandler },
680
+ authorize: allowAll,
681
+ });
682
+ const { commitSha: firstSha } = await sourceStore.writeTree(
683
+ principal,
684
+ repoId,
685
+ REF,
686
+ { files: { "deploy/a.md": "v1" }, message: "v1" },
687
+ );
688
+ const { pack: firstPack } = await sourceStore.createPack(
689
+ principal,
690
+ repoId,
691
+ REF,
692
+ );
693
+ const { commitSha: secondSha } = await sourceStore.writeTree(
694
+ principal,
695
+ repoId,
696
+ REF,
697
+ { files: { "deploy/a.md": "v2" }, message: "v2" },
698
+ );
699
+ const { pack: secondPack } = await sourceStore.createPack(
700
+ principal,
701
+ repoId,
702
+ REF,
703
+ );
704
+
705
+ const targetDir = await makeTempDir("repo-store-cas-stale-target-");
706
+ const targetHandler = createTestHandler({
707
+ allowTopLevelPaths: () => true,
708
+ });
709
+ const targetStore = createRepoStore({
710
+ dataDir: targetDir,
711
+ signingKey,
712
+ handlers: { "agent-state": targetHandler },
713
+ authorize: allowAll,
714
+ });
715
+ await targetStore.receivePack(
716
+ principal,
717
+ repoId,
718
+ REF,
719
+ firstPack,
720
+ firstSha,
721
+ null,
722
+ );
723
+
724
+ await expect(
725
+ targetStore.receivePack(
726
+ principal,
727
+ repoId,
728
+ REF,
729
+ secondPack,
730
+ secondSha,
731
+ null,
732
+ ),
733
+ ).rejects.toThrow(/^non_fast_forward:/);
734
+
735
+ expect(await targetStore.resolveRef(principal, repoId, REF)).toBe(firstSha);
736
+ expect(targetHandler.onRefUpdatedCalls).toHaveLength(1);
737
+ });
738
+
739
+ test("receivePack feeds the substrate-returned oldSha into onRefUpdated", async () => {
740
+ const sourceDir = await makeTempDir("repo-store-oldsha-source-");
741
+ const sourceHandler = createTestHandler();
742
+ const sourceStore = createRepoStore({
743
+ dataDir: sourceDir,
744
+ signingKey,
745
+ handlers: { "agent-state": sourceHandler },
746
+ authorize: allowAll,
747
+ });
748
+ const { commitSha: firstSha } = await sourceStore.writeTree(
749
+ principal,
750
+ repoId,
751
+ REF,
752
+ { files: { "deploy/a.md": "v1" }, message: "v1" },
753
+ );
754
+ const { pack: firstPack } = await sourceStore.createPack(
755
+ principal,
756
+ repoId,
757
+ REF,
758
+ );
759
+ const { commitSha: secondSha } = await sourceStore.writeTree(
760
+ principal,
761
+ repoId,
762
+ REF,
763
+ { files: { "deploy/a.md": "v2" }, message: "v2" },
764
+ );
765
+ const { pack: secondPack } = await sourceStore.createPack(
766
+ principal,
767
+ repoId,
768
+ REF,
769
+ );
770
+
771
+ const targetDir = await makeTempDir("repo-store-oldsha-target-");
772
+ const targetHandler = createTestHandler({
773
+ allowTopLevelPaths: () => true,
774
+ });
775
+ const targetStore = createRepoStore({
776
+ dataDir: targetDir,
777
+ signingKey,
778
+ handlers: { "agent-state": targetHandler },
779
+ authorize: allowAll,
780
+ });
781
+ await targetStore.receivePack(
782
+ principal,
783
+ repoId,
784
+ REF,
785
+ firstPack,
786
+ firstSha,
787
+ null,
788
+ );
789
+ await targetStore.receivePack(
790
+ principal,
791
+ repoId,
792
+ REF,
793
+ secondPack,
794
+ secondSha,
795
+ firstSha,
796
+ );
797
+
798
+ expect(targetHandler.onRefUpdatedCalls).toHaveLength(2);
799
+ const second = targetHandler.onRefUpdatedCalls[1];
800
+ if (!second) throw new Error("unreachable");
801
+ expect(second.oldSha).toBe(firstSha);
802
+ expect(second.newSha).toBe(secondSha);
803
+ });
804
+
805
+ test("concurrent receivePack against the same repo serializes", async () => {
806
+ const sourceDir = await makeTempDir("repo-store-serial-source-");
807
+ const sourceHandler = createTestHandler();
808
+ const sourceStore = createRepoStore({
809
+ dataDir: sourceDir,
810
+ signingKey,
811
+ handlers: { "agent-state": sourceHandler },
812
+ authorize: allowAll,
813
+ });
814
+ const { commitSha: firstSha } = await sourceStore.writeTree(
815
+ principal,
816
+ repoId,
817
+ REF,
818
+ { files: { "deploy/a.md": "v1" }, message: "v1" },
819
+ );
820
+ const { pack: firstPack } = await sourceStore.createPack(
821
+ principal,
822
+ repoId,
823
+ REF,
824
+ );
825
+ const { commitSha: secondSha } = await sourceStore.writeTree(
826
+ principal,
827
+ repoId,
828
+ REF,
829
+ { files: { "deploy/a.md": "v2" }, message: "v2" },
830
+ );
831
+ const { pack: secondPack } = await sourceStore.createPack(
832
+ principal,
833
+ repoId,
834
+ REF,
835
+ );
836
+
837
+ const targetDir = await makeTempDir("repo-store-serial-target-");
838
+ const events: string[] = [];
839
+ const slowHandler: TestHandler = {
840
+ kind: "agent-state",
841
+ directoryPrefix: "repos-under-test",
842
+ async validatePush() {
843
+ events.push("validate");
844
+ await new Promise((resolve) => setTimeout(resolve, 50));
845
+ return { ok: true };
846
+ },
847
+ onRefUpdated({ newSha }) {
848
+ events.push(`update:${newSha.substring(0, 7)}`);
849
+ },
850
+ onRefUpdatedCalls: [],
851
+ };
852
+ const targetStore = createRepoStore({
853
+ dataDir: targetDir,
854
+ signingKey,
855
+ handlers: { "agent-state": slowHandler },
856
+ authorize: allowAll,
857
+ });
858
+
859
+ const firstP = targetStore.receivePack(
860
+ principal,
861
+ repoId,
862
+ REF,
863
+ firstPack,
864
+ firstSha,
865
+ null,
866
+ );
867
+ const secondP = targetStore.receivePack(
868
+ principal,
869
+ repoId,
870
+ REF,
871
+ secondPack,
872
+ secondSha,
873
+ firstSha,
874
+ );
875
+
876
+ await Promise.all([firstP, secondP]);
877
+
878
+ expect(events).toEqual([
879
+ "validate",
880
+ `update:${firstSha.substring(0, 7)}`,
881
+ "validate",
882
+ `update:${secondSha.substring(0, 7)}`,
883
+ ]);
884
+ expect(await targetStore.resolveRef(principal, repoId, REF)).toBe(
885
+ secondSha,
886
+ );
887
+ });
888
+
889
+ test("concurrent receivePack against distinct repos runs in parallel", async () => {
890
+ const sourceDir = await makeTempDir("repo-store-parallel-source-");
891
+ const sourceHandler = createTestHandler();
892
+ const sourceStore = createRepoStore({
893
+ dataDir: sourceDir,
894
+ signingKey,
895
+ handlers: { "agent-state": sourceHandler },
896
+ authorize: allowAll,
897
+ });
898
+ const repoA: RepoId = { kind: "agent-state", id: "alpha" };
899
+ const repoB: RepoId = { kind: "agent-state", id: "beta" };
900
+ const { commitSha: shaA } = await sourceStore.writeTree(
901
+ principal,
902
+ repoA,
903
+ REF,
904
+ { files: { "deploy/a.md": "a" }, message: "a" },
905
+ );
906
+ const { pack: packA } = await sourceStore.createPack(principal, repoA, REF);
907
+ const { commitSha: shaB } = await sourceStore.writeTree(
908
+ principal,
909
+ repoB,
910
+ REF,
911
+ { files: { "deploy/b.md": "b" }, message: "b" },
912
+ );
913
+ const { pack: packB } = await sourceStore.createPack(principal, repoB, REF);
914
+
915
+ const targetDir = await makeTempDir("repo-store-parallel-target-");
916
+ const enters: number[] = [];
917
+ let activeConcurrent = 0;
918
+ let observedMaxConcurrent = 0;
919
+ const trackingHandler: TestHandler = {
920
+ kind: "agent-state",
921
+ directoryPrefix: "repos-under-test",
922
+ async validatePush() {
923
+ activeConcurrent += 1;
924
+ observedMaxConcurrent = Math.max(
925
+ observedMaxConcurrent,
926
+ activeConcurrent,
927
+ );
928
+ enters.push(Date.now());
929
+ await new Promise((resolve) => setTimeout(resolve, 100));
930
+ activeConcurrent -= 1;
931
+ return { ok: true };
932
+ },
933
+ onRefUpdated() {
934
+ /* no-op */
935
+ },
936
+ onRefUpdatedCalls: [],
937
+ };
938
+ const targetStore = createRepoStore({
939
+ dataDir: targetDir,
940
+ signingKey,
941
+ handlers: { "agent-state": trackingHandler },
942
+ authorize: allowAll,
943
+ });
944
+
945
+ const start = Date.now();
946
+ await Promise.all([
947
+ targetStore.receivePack(principal, repoA, REF, packA, shaA, null),
948
+ targetStore.receivePack(principal, repoB, REF, packB, shaB, null),
949
+ ]);
950
+ const elapsed = Date.now() - start;
951
+
952
+ expect(observedMaxConcurrent).toBe(2);
953
+ // Serialized work would take ~200ms; parallel work completes well
954
+ // under 180ms. The bound is generous to absorb scheduler jitter on
955
+ // contended CI runners.
956
+ expect(elapsed).toBeLessThan(180);
957
+ });
958
+
959
+ test("getRepoDir returns dataDir/<directoryPrefix>/<id>", async () => {
960
+ const dataDir = await makeTempDir("repo-store-dir-");
961
+ const handler = createTestHandler();
962
+ const store = createRepoStore({
963
+ dataDir,
964
+ signingKey,
965
+ handlers: { "agent-state": handler },
966
+ authorize: allowAll,
967
+ });
968
+
969
+ const expected = path.join(dataDir, handler.directoryPrefix, repoId.id);
970
+ expect(store.getRepoDir(repoId)).toBe(expected);
971
+ });
972
+
973
+ test("getRepoDir rejects an unsafe repo id without touching the filesystem", () => {
974
+ const dataDir = path.join(os.tmpdir(), "repo-store-dir-unsafe-");
975
+ const handler = createTestHandler();
976
+ const store = createRepoStore({
977
+ dataDir,
978
+ signingKey,
979
+ handlers: { "agent-state": handler },
980
+ authorize: allowAll,
981
+ });
982
+
983
+ expect(() =>
984
+ store.getRepoDir({ kind: "agent-state", id: "../escape" }),
985
+ ).toThrow(/^repo_id_invalid/);
986
+ });
987
+
988
+ test("listRefs returns the genesis branch on a freshly-initialised repo", async () => {
989
+ const dataDir = await makeTempDir("repo-store-list-genesis-");
990
+ const handler = createTestHandler();
991
+ const store = createRepoStore({
992
+ dataDir,
993
+ signingKey,
994
+ handlers: { "agent-state": handler },
995
+ authorize: allowAll,
996
+ });
997
+
998
+ await store.initRepo(repoId);
999
+ const refs = await store.listRefs(principal, repoId);
1000
+ expect(refs.length).toBeGreaterThanOrEqual(1);
1001
+
1002
+ const main = refs.find((r) => r.name === "refs/heads/main");
1003
+ expect(main).toBeDefined();
1004
+ expect(main?.sha).toMatch(/^[0-9a-f]{40}$/);
1005
+
1006
+ const dir = store.getRepoDir(repoId);
1007
+ const tipFromGit = await git.resolveRef({
1008
+ fs,
1009
+ dir,
1010
+ ref: "refs/heads/main",
1011
+ });
1012
+ expect(main?.sha).toBe(tipFromGit);
1013
+ });
1014
+
1015
+ test("listRefs returns names sorted lexicographically including tags", async () => {
1016
+ const dataDir = await makeTempDir("repo-store-list-sort-");
1017
+ const handler = createTestHandler();
1018
+ const store = createRepoStore({
1019
+ dataDir,
1020
+ signingKey,
1021
+ handlers: { "agent-state": handler },
1022
+ authorize: allowAll,
1023
+ });
1024
+
1025
+ await store.writeTree(principal, repoId, "refs/heads/main", {
1026
+ files: { "a.md": "one" },
1027
+ message: "first",
1028
+ });
1029
+ await store.writeTree(principal, repoId, "refs/heads/zzz-branch", {
1030
+ files: { "b.md": "two" },
1031
+ message: "second",
1032
+ });
1033
+
1034
+ const dir = store.getRepoDir(repoId);
1035
+ const mainSha = await git.resolveRef({
1036
+ fs,
1037
+ dir,
1038
+ ref: "refs/heads/main",
1039
+ });
1040
+ await git.writeRef({
1041
+ fs,
1042
+ dir,
1043
+ ref: "refs/tags/v1",
1044
+ value: mainSha,
1045
+ force: true,
1046
+ });
1047
+
1048
+ const refs = await store.listRefs(principal, repoId);
1049
+ const names = refs.map((r) => r.name);
1050
+ const sorted = [...names].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1051
+ expect(names).toEqual(sorted);
1052
+ expect(names).toContain("refs/heads/main");
1053
+ expect(names).toContain("refs/heads/zzz-branch");
1054
+ expect(names).toContain("refs/tags/v1");
1055
+ });
1056
+
1057
+ test("listRefs returns the empty list before initRepo is called", async () => {
1058
+ const dataDir = await makeTempDir("repo-store-list-empty-");
1059
+ const handler = createTestHandler();
1060
+ const store = createRepoStore({
1061
+ dataDir,
1062
+ signingKey,
1063
+ handlers: { "agent-state": handler },
1064
+ authorize: allowAll,
1065
+ });
1066
+
1067
+ const refs = await store.listRefs(principal, repoId);
1068
+ expect(refs).toEqual([]);
1069
+ });
1070
+
1071
+ test("listRefs is gated under the resolveRef authorize action", async () => {
1072
+ const dataDir = await makeTempDir("repo-store-list-deny-");
1073
+ const seenActions: RepoAction[] = [];
1074
+ const denyAuthorize: AuthorizeFn = (_p, _r, _ref, action) => {
1075
+ seenActions.push(action);
1076
+ return { allowed: false, reason: "denied" };
1077
+ };
1078
+ const handler = createTestHandler();
1079
+ const store = createRepoStore({
1080
+ dataDir,
1081
+ signingKey,
1082
+ handlers: { "agent-state": handler },
1083
+ authorize: denyAuthorize,
1084
+ });
1085
+
1086
+ await expect(store.listRefs(principal, repoId)).rejects.toThrow(
1087
+ /^authorize_denied/,
1088
+ );
1089
+ expect(seenActions).toEqual(["resolveRef"]);
1090
+ });
1091
+
1092
+ test("initRepo forwards a per-call gitignore override into the genesis tree", async () => {
1093
+ const dataDir = await makeTempDir("repo-store-init-gitignore-");
1094
+ const handler = createTestHandler();
1095
+ const store = createRepoStore({
1096
+ dataDir,
1097
+ signingKey,
1098
+ handlers: { "agent-state": handler },
1099
+ authorize: allowAll,
1100
+ });
1101
+
1102
+ const customBody = ".DS_Store\nnode_modules/\nkeys/\n";
1103
+ await store.initRepo(repoId, { gitignore: customBody });
1104
+
1105
+ const dir = store.getRepoDir(repoId);
1106
+ const onDisk = await fs.promises.readFile(
1107
+ path.join(dir, ".gitignore"),
1108
+ "utf-8",
1109
+ );
1110
+ expect(onDisk).toBe(customBody);
1111
+ });
1112
+
1113
+ test("withRepoLock releases on substrate exception and the map drains", async () => {
1114
+ const sourceDir = await makeTempDir("repo-store-release-source-");
1115
+ const sourceHandler = createTestHandler();
1116
+ const sourceStore = createRepoStore({
1117
+ dataDir: sourceDir,
1118
+ signingKey,
1119
+ handlers: { "agent-state": sourceHandler },
1120
+ authorize: allowAll,
1121
+ });
1122
+ const { commitSha } = await sourceStore.writeTree(principal, repoId, REF, {
1123
+ files: { "deploy/a.md": "v1" },
1124
+ message: "v1",
1125
+ });
1126
+ const { pack } = await sourceStore.createPack(principal, repoId, REF);
1127
+
1128
+ const targetDir = await makeTempDir("repo-store-release-target-");
1129
+ let rejectOnce = true;
1130
+ const flakyHandler: TestHandler = {
1131
+ kind: "agent-state",
1132
+ directoryPrefix: "repos-under-test",
1133
+ validatePush() {
1134
+ if (rejectOnce) {
1135
+ rejectOnce = false;
1136
+ return { ok: false, reason: "first attempt rejected" };
1137
+ }
1138
+ return { ok: true };
1139
+ },
1140
+ onRefUpdated() {
1141
+ /* no-op */
1142
+ },
1143
+ onRefUpdatedCalls: [],
1144
+ };
1145
+ const targetStore = createRepoStore({
1146
+ dataDir: targetDir,
1147
+ signingKey,
1148
+ handlers: { "agent-state": flakyHandler },
1149
+ authorize: allowAll,
1150
+ });
1151
+
1152
+ await expect(
1153
+ targetStore.receivePack(principal, repoId, REF, pack, commitSha, null),
1154
+ ).rejects.toThrow(/^path_violation/);
1155
+
1156
+ // The second call would deadlock if the lock were never released.
1157
+ await targetStore.receivePack(
1158
+ principal,
1159
+ repoId,
1160
+ REF,
1161
+ pack,
1162
+ commitSha,
1163
+ null,
1164
+ );
1165
+ expect(await targetStore.resolveRef(principal, repoId, REF)).toBe(
1166
+ commitSha,
1167
+ );
1168
+ });
1169
+ });