amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -394,42 +394,27 @@ test('workspace removal drops the project_context row', () => {
394
394
  assert.equal(store.getProjectContext(workspace.path, { reconcile: false }), null);
395
395
  });
396
396
 
397
- test('the bounded watcher schedule reconciles a dropped notification and retries the watcher', async () => {
397
+ test('watcher reconciles direct disk edits', async () => {
398
398
  const { projectPath, workspace } = makeProject('watch');
399
- const originalWatch = fs.watch;
400
- let watchAttempts = 0;
401
- fs.watch = (...args) => {
402
- watchAttempts += 1;
403
- if (watchAttempts === 1) {
404
- const error = new Error('simulated dropped watcher');
405
- error.code = 'EMFILE';
406
- throw error;
407
- }
408
- return originalWatch.apply(fs, args);
409
- };
410
-
411
- try {
412
- store.ensureProjectContext(workspace);
413
- const before = store.getProjectContext(projectPath, { reconcile: false }).contextRevision;
399
+ store.ensureProjectContext(workspace);
400
+ const before = store.getProjectContext(projectPath, { reconcile: false }).contextRevision;
414
401
 
415
- fs.writeFileSync(
416
- path.join(projectPath, '.amalgm/context/project-state.md'),
417
- '# Project State\n\nWatched edit.\n',
418
- );
402
+ fs.writeFileSync(
403
+ path.join(projectPath, '.amalgm/context/project-state.md'),
404
+ '# Project State\n\nWatched edit.\n',
405
+ );
419
406
 
420
- const deadline = Date.now() + 7_000;
421
- let row = store.getProjectContext(projectPath, { reconcile: false });
422
- while (row?.contextRevision === before && Date.now() < deadline) {
423
- await new Promise((resolve) => setTimeout(resolve, 50));
424
- row = store.getProjectContext(projectPath, { reconcile: false });
425
- }
426
- assert.ok(row, 'row exists');
427
- assert.notEqual(row.contextRevision, before, 'scheduled verification updated the row');
428
- assert.ok(row.prompt.projectState.includes('Watched edit.'));
429
- assert.ok(watchAttempts >= 2, 'the scheduled pass retried the failed watcher');
430
- } finally {
431
- fs.watch = originalWatch;
407
+ // fs.watch + 200ms debounce; poll the row instead of guessing exact timing.
408
+ const deadline = Date.now() + 3000;
409
+ let row = null;
410
+ while (Date.now() < deadline) {
411
+ row = store.getProjectContext(projectPath, { reconcile: false });
412
+ if (row && row.contextRevision !== before) break;
413
+ await new Promise((resolve) => setTimeout(resolve, 100));
432
414
  }
415
+ assert.ok(row, 'row exists');
416
+ assert.notEqual(row.contextRevision, before, 'watcher updated the row');
417
+ assert.ok(row.prompt.projectState.includes('Watched edit.'));
433
418
  });
434
419
 
435
420
  // ---------------------------------------------------------------------------
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The production-owned vertical slice:
5
+ *
6
+ * amalgm add [path] → POST /entities/add → registration.add(path)
7
+ * → Watch + Detect + Record
8
+ *
9
+ * The wall already proves detection. What is unproven here is the
10
+ * WIRING — the parts that only exist because production owns the stack:
11
+ * which policies are actually injected, where state lands, that the
12
+ * observer's events genuinely reach the boundary, that identity
13
+ * survives a process restart over the same directory, and that the
14
+ * endpoint reports refusal as refusal.
15
+ *
16
+ * Everything runs over a temporary AMALGM_DIR, set before config loads,
17
+ * so the singleton this route uses can never touch a real installation.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const test = require('node:test');
24
+ const assert = require('node:assert');
25
+
26
+ const tempRoot = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-add-')));
27
+ process.env.AMALGM_DIR = path.join(tempRoot, 'home');
28
+
29
+ const { classifyFile } = require('../registration/classify');
30
+ const { ENTITIES_DIR, createRegistrationService, registrationService } = require('../registration/service');
31
+ const { handleEntityRoutes } = require('../server/routes/entities');
32
+ const { createLocalServiceRouter } = require('../server/local-service-router');
33
+
34
+ const services = [];
35
+
36
+ test.after(() => {
37
+ for (const service of services) service.stop();
38
+ fs.rmSync(tempRoot, { recursive: true, force: true });
39
+ });
40
+
41
+ function makeService(name) {
42
+ const service = createRegistrationService({ dir: path.join(tempRoot, `state-${name}`) });
43
+ services.push(service);
44
+ return service;
45
+ }
46
+
47
+ /** A directory of real ground, outside the Amalgm home. */
48
+ function makeGround(name) {
49
+ const dir = path.join(tempRoot, `ground-${name}`);
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ return fs.realpathSync(dir);
52
+ }
53
+
54
+ // --- the injected type policy ---------------------------------------------
55
+
56
+ test('classify: ordinary bytes are text', () => {
57
+ const file = path.join(makeGround('classify-text'), 'notes.md');
58
+ fs.writeFileSync(file, '# hello\nplain words\n');
59
+ assert.equal(classifyFile(file), 'file.text');
60
+ });
61
+
62
+ test('classify: a NUL in the opening bytes means bytes, never text', () => {
63
+ const file = path.join(makeGround('classify-binary'), 'blob.bin');
64
+ fs.writeFileSync(file, Buffer.from([0x89, 0x50, 0x00, 0x4e, 0x47]));
65
+ assert.equal(classifyFile(file), 'file.binary');
66
+ });
67
+
68
+ test('classify: ground that cannot be read supports no structural claim', () => {
69
+ // The weaker of the two answers, never a throw: the caller is
70
+ // mid-transaction over a hash it already holds.
71
+ assert.equal(classifyFile(path.join(tempRoot, 'never-existed')), 'file.binary');
72
+ });
73
+
74
+ test('classify: a NUL past the sniff window does not retroactively unmake text', () => {
75
+ // The law is about the OPENING bytes, stated exactly so a huge text
76
+ // file is never re-read whole to answer a hint.
77
+ const file = path.join(makeGround('classify-late'), 'long.txt');
78
+ fs.writeFileSync(file, Buffer.concat([Buffer.alloc(9000, 0x61), Buffer.from([0])]));
79
+ assert.equal(classifyFile(file), 'file.text');
80
+ });
81
+
82
+ // --- composition: what production actually builds --------------------------
83
+
84
+ test('the service composes one observer and one boundary over one directory', () => {
85
+ const service = makeService('compose');
86
+ assert.ok(fs.existsSync(path.join(service.dir, 'observer.db')), 'evidence database');
87
+ assert.ok(fs.existsSync(path.join(service.dir, 'identity.db')), 'identity database');
88
+ assert.ok(fs.existsSync(path.join(service.dir, 'identity.epoch')), 'the identity sentinel');
89
+ });
90
+
91
+ test('the singleton lives under the Amalgm home, and is one', () => {
92
+ assert.equal(ENTITIES_DIR, path.join(process.env.AMALGM_DIR, 'entities'));
93
+ assert.equal(registrationService(), registrationService(), 'built once, returned forever');
94
+ services.push(registrationService());
95
+ });
96
+
97
+ test('add registers the named ground and every file beneath it', () => {
98
+ const service = makeService('add');
99
+ const ground = makeGround('add');
100
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
101
+ fs.mkdirSync(path.join(ground, 'nested'));
102
+ fs.writeFileSync(path.join(ground, 'nested', 'b.bin'), Buffer.from([0, 1, 2]));
103
+
104
+ const { rootPath, record } = service.add(ground);
105
+ assert.equal(rootPath, ground);
106
+ assert.ok(record.uuid, 'the tree has a permanent identity');
107
+ assert.equal(record.parentUUID, null, 'a tree is a parentless record');
108
+
109
+ const registry = service.registration.tree(ground).registry;
110
+ const named = new Map(registry.children(record.uuid).map((row) => [row.name, row]));
111
+ assert.deepEqual([...named.keys()].sort(), ['a.txt', 'nested']);
112
+ assert.equal(named.get('a.txt').type, 'file.text', 'the real classifier ran');
113
+
114
+ const nested = registry.children(named.get('nested').uuid);
115
+ assert.equal(nested.length, 1);
116
+ assert.equal(nested[0].type, 'file.binary', 'and it distinguishes bytes from text');
117
+ });
118
+
119
+ test('watching is armed: add begins the Watch rail, not just the walk', () => {
120
+ const service = makeService('watching');
121
+ const ground = makeGround('watching');
122
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
123
+ service.add(ground);
124
+
125
+ const [root] = service.observer.status();
126
+ assert.equal(root.path, ground);
127
+ assert.equal(root.watching, true, 'the doorbell is live');
128
+ assert.equal(root.degraded, false);
129
+ });
130
+
131
+ test('the Detect → Record wire is live: a later change reaches the registry', () => {
132
+ const service = makeService('wire');
133
+ const ground = makeGround('wire');
134
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
135
+ const { record } = service.add(ground);
136
+ const tree = service.registration.tree(ground);
137
+
138
+ // Ground that appeared after registration. The scan publishes, the
139
+ // boundary records — cut the emit wire in service.js and this fails.
140
+ fs.writeFileSync(path.join(ground, 'later.txt'), 'arrived after');
141
+ service.observer.scanNow(service.observer.listRoots()[0].rootId);
142
+ service.registration.flushIfDirty();
143
+
144
+ const names = tree.registry.children(record.uuid).map((row) => row.name).sort();
145
+ assert.deepEqual(names, ['a.txt', 'later.txt']);
146
+ });
147
+
148
+ test('identity survives a restart over the same directory', () => {
149
+ const dir = path.join(tempRoot, 'state-restart');
150
+ const ground = makeGround('restart');
151
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
152
+
153
+ const first = createRegistrationService({ dir });
154
+ const before = first.add(ground).record.uuid;
155
+ first.stop();
156
+
157
+ const second = createRegistrationService({ dir });
158
+ services.push(second);
159
+ const after = second.registration.tree(ground).root().uuid;
160
+ assert.equal(after, before, 'the tree keeps its permanent UUID across processes');
161
+ });
162
+
163
+ // --- the policies are the real ones ---------------------------------------
164
+
165
+ test("amalgm's enrollment policy is in force, and refuses loudly", () => {
166
+ const service = makeService('policy');
167
+ const excluded = path.join(makeGround('policy'), 'node_modules');
168
+ fs.mkdirSync(excluded);
169
+ assert.throws(() => service.add(excluded), /enrollment policy excludes it/);
170
+ });
171
+
172
+ test('adding ground already inside a registered tree returns that tree', () => {
173
+ const service = makeService('covered');
174
+ const ground = makeGround('covered');
175
+ fs.mkdirSync(path.join(ground, 'inner'));
176
+ fs.writeFileSync(path.join(ground, 'inner', 'a.txt'), 'alpha');
177
+
178
+ const tree = service.add(ground);
179
+ const inner = service.add(path.join(ground, 'inner'));
180
+ assert.equal(inner.rootPath, ground, 'one owner per path — no second tree is born');
181
+ assert.equal(inner.record.uuid, tree.record.uuid);
182
+ });
183
+
184
+ // --- the endpoint ----------------------------------------------------------
185
+
186
+ function fakeCtx(pathname, method, body) {
187
+ const sent = {};
188
+ return {
189
+ ctx: {
190
+ pathname,
191
+ method,
192
+ readJsonBody: async () => body,
193
+ sendJson: (status, payload) => Object.assign(sent, { status, payload }),
194
+ },
195
+ sent,
196
+ };
197
+ }
198
+
199
+ test('POST /entities/add registers and answers with the tree', async () => {
200
+ const ground = makeGround('route');
201
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
202
+
203
+ const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: ground });
204
+ assert.equal(await handleEntityRoutes(ctx), true);
205
+ assert.equal(sent.status, 200);
206
+ assert.equal(sent.payload.rootPath, ground);
207
+ assert.ok(sent.payload.record.uuid);
208
+ });
209
+
210
+ test('the endpoint reports a refusal as a refusal, with the reason', async () => {
211
+ const excluded = path.join(makeGround('route-refused'), '.ssh');
212
+ fs.mkdirSync(excluded);
213
+
214
+ const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: excluded });
215
+ assert.equal(await handleEntityRoutes(ctx), true);
216
+ assert.equal(sent.status, 400, 'the request named ground the boundary refuses');
217
+ assert.match(sent.payload.error, /enrollment policy excludes it/);
218
+ });
219
+
220
+ test('a missing path is answered, not registered', async () => {
221
+ const { ctx, sent } = fakeCtx('/entities/add', 'POST', {});
222
+ assert.equal(await handleEntityRoutes(ctx), true);
223
+ assert.equal(sent.status, 400);
224
+ assert.match(sent.payload.error, /path is required/);
225
+ });
226
+
227
+ test('ground that is not there is a refusal, never a crash', async () => {
228
+ const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: path.join(tempRoot, 'no-such-ground') });
229
+ assert.equal(await handleEntityRoutes(ctx), true);
230
+ assert.equal(sent.status, 400);
231
+ assert.ok(sent.payload.error);
232
+ });
233
+
234
+ test('the route is MOUNTED: the runtime dispatcher reaches it', async () => {
235
+ // A handler nothing routes to is a handler that does not exist. This
236
+ // is the local dispatcher production actually serves, unmodified.
237
+ const ground = makeGround('mounted');
238
+ fs.writeFileSync(path.join(ground, 'a.txt'), 'alpha');
239
+
240
+ const { ctx, sent } = fakeCtx('/entities/add', 'POST', { path: ground });
241
+ assert.equal(await createLocalServiceRouter().handle(ctx), true);
242
+ assert.equal(sent.status, 200);
243
+ assert.equal(sent.payload.rootPath, ground);
244
+ });
245
+
246
+ test('the route group claims only its own request', async () => {
247
+ const others = [
248
+ fakeCtx('/entities/add', 'GET', {}),
249
+ fakeCtx('/workspace/projects', 'POST', {}),
250
+ fakeCtx('/entities', 'POST', {}),
251
+ ];
252
+ for (const { ctx, sent } of others) {
253
+ assert.equal(await handleEntityRoutes(ctx), false);
254
+ assert.equal(sent.status, undefined, 'an unclaimed request is never answered');
255
+ }
256
+ });