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
@@ -6,186 +6,789 @@ const crypto = require('crypto');
6
6
  const fs = require('fs');
7
7
  const os = require('os');
8
8
  const path = require('path');
9
+ const { execFile } = require('child_process');
10
+ const { promisify } = require('util');
11
+
12
+ const execFileAsync = promisify(execFile);
9
13
 
10
14
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-wall-test-'));
11
15
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
12
16
  process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
13
17
  process.env.AMALGM_COMPUTER_ID = 'machine-a';
14
- process.env.AMALGM_DEVICE_ID = 'machine-a';
18
+ process.env.AMALGM_DEVICE_ID = 'device-machine-a';
19
+
15
20
  fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
16
21
  fs.writeFileSync(
17
22
  path.join(process.env.AMALGM_DIR, 'computer.json'),
18
- JSON.stringify({
19
- computer_id: 'machine-a',
20
- user_id: '11111111-1111-4111-8111-111111111111',
21
- }),
23
+ JSON.stringify({ computer_id: 'machine-a', user_id: '11111111-1111-4111-8111-111111111111' }),
22
24
  );
23
25
 
24
26
  const { closeLocalDb } = require('../state/db');
25
- const mutations = require('../state/mutations');
26
- const replicas = require('../state/replicas');
27
- const checkpoint = require('../workspace/checkpoint');
28
- const intent = require('../workspace/intent');
29
- const projectStream = require('../workspace/project-stream');
30
- const wall = require('../workspace/wall');
31
-
32
- const PROJECT = '018f0000-0000-7000-8000-00000000babe';
33
- const HEAD = 'a'.repeat(40);
34
- const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
35
- const REPO_KEY = `code-folder:${PROJECT}`;
27
+ const { appendStateEvent } = require('../state/events');
28
+ const {
29
+ acknowledgeCloudMutation,
30
+ listPendingCloudMutations,
31
+ } = require('../state/mutations');
32
+ const { registerReplica } = require('../state/replicas');
33
+ const { openDoc, getDocText, writeDocText } = require('../state/docs');
34
+ const { upsertWorkspace } = require('../workspace/store');
35
+ const { refreshRepoCards } = require('../workspace/cards');
36
+ const { checkpointFile, repoKeyOfCard } = require('../workspace/checkpoint');
37
+ const {
38
+ _test: wallTest,
39
+ bindProject,
40
+ compactWallText,
41
+ hasLocalContribution,
42
+ isCommittedCard,
43
+ projectForRepo,
44
+ projectResourceId,
45
+ publishedCardFor,
46
+ publishWall,
47
+ readWall,
48
+ startWallService,
49
+ stampCards,
50
+ stopWallService,
51
+ wallStats,
52
+ } = require('../workspace/wall');
53
+
54
+ let repoKey;
55
+ let project;
56
+ let nextCloudVersion = 1;
57
+
58
+ function markReducerApplied(version) {
59
+ const digest = crypto.createHash('sha256').update(repoKey).digest('hex').slice(0, 24);
60
+ const file = path.join(process.env.AMALGM_DIR, 'state', 'repo-reducer', `${digest}.json`);
61
+ fs.mkdirSync(path.dirname(file), { recursive: true });
62
+ fs.writeFileSync(file, `${JSON.stringify({ repoKey, appliedThrough: version })}\n`);
63
+ }
64
+
65
+ function settleProject() {
66
+ const versions = [];
67
+ for (const mutation of listPendingCloudMutations(project.resourceId)) {
68
+ const version = nextCloudVersion;
69
+ nextCloudVersion += 1;
70
+ acknowledgeCloudMutation(project.resourceId, mutation.mutationId, {
71
+ version,
72
+ committedAt: `2026-07-23T00:00:${String(version).padStart(2, '0')}.000Z`,
73
+ });
74
+ versions.push(version);
75
+ }
76
+ if (versions.length > 0) markReducerApplied(versions.at(-1));
77
+ return versions;
78
+ }
36
79
 
37
80
  test.after(() => {
38
81
  closeLocalDb();
39
82
  fs.rmSync(tempRoot, { recursive: true, force: true });
40
83
  });
41
84
 
42
- function bundle(oid = HEAD, byte = 'b') {
43
- const chunks = [{ sha256: byte.repeat(64), bytes: 1 }];
44
- return {
45
- kind: 'bundle',
46
- contract: projectStream.BUNDLE_CONTRACT,
47
- resourceId: PROJECT,
48
- oid,
49
- ref: `refs/amalgm/exports/${oid}`,
50
- checksum: crypto.createHash('sha256').update(Buffer.from(JSON.stringify(chunks))).digest('hex'),
51
- bytes: 1,
52
- chunks,
53
- };
85
+ async function run(cwd, ...args) {
86
+ await execFileAsync(
87
+ 'git',
88
+ ['-c', 'user.email=test@amalgm.test', '-c', 'user.name=Amalgm Test', '-c', 'commit.gpgsign=false', ...args],
89
+ { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', LC_ALL: 'C' } },
90
+ );
54
91
  }
55
92
 
56
- function localCard() {
57
- return {
58
- card: projectStream.CARD_CONTRACT,
93
+ test('a project id is stable for one owner and repo, and distinct for another owner or repo', () => {
94
+ const userA = '11111111-1111-4111-8111-111111111111';
95
+ const userB = '22222222-2222-4222-8222-222222222222';
96
+ assert.equal(projectResourceId('repo-a', userA), projectResourceId('repo-a', userA));
97
+ assert.notEqual(projectResourceId('repo-a', userA), projectResourceId('repo-a', userB));
98
+ assert.notEqual(projectResourceId('repo-a', userA), projectResourceId('repo-b', userA));
99
+ assert.match(projectResourceId('repo-a', userA), /^shr_[A-Za-z0-9_-]{16,128}$/);
100
+ assert.equal(projectResourceId(null, userA), null);
101
+ });
102
+
103
+ test('no commit means no shared card', () => {
104
+ assert.equal(isCommittedCard({ head: { branch: 'main', oid: null } }), false);
105
+ assert.equal(isCommittedCard({ head: { branch: 'main', oid: 'a'.repeat(40) } }), true);
106
+ });
107
+
108
+ test('only a value different from both the materialized baseline and the rail is new work', () => {
109
+ const oidA = 'a'.repeat(40);
110
+ const oidB = 'b'.repeat(40);
111
+ const oidC = 'c'.repeat(40);
112
+ const local = {
113
+ head: { branch: 'main', oid: oidA, detached: false },
114
+ refs: [{ name: 'main', oid: oidA }],
115
+ };
116
+ const plan = {
117
+ card: { head: { branch: 'main', oid: oidA, detached: false } },
118
+ branches: [
119
+ { name: 'main', oid: oidA },
120
+ { name: 'feature', oid: oidB },
121
+ ],
122
+ transitions: [],
123
+ };
124
+ const baseline = {
125
+ branch: 'main',
126
+ baseOid: oidA,
127
+ branches: [{ name: 'main', oid: oidA }],
128
+ };
129
+
130
+ assert.equal(hasLocalContribution(local, plan, baseline), false);
131
+ assert.equal(hasLocalContribution({
132
+ ...local,
133
+ refs: [...local.refs, { name: 'feature', oid: oidB }],
134
+ }, plan, baseline), false);
135
+ assert.equal(hasLocalContribution({
136
+ ...local,
137
+ head: { branch: 'main', oid: oidC, detached: false },
138
+ refs: [{ name: 'main', oid: oidC }],
139
+ }, plan, baseline), true);
140
+ assert.equal(hasLocalContribution(local, plan, {
141
+ ...baseline,
142
+ branches: [...baseline.branches, { name: 'feature', oid: oidB }],
143
+ }), true);
144
+
145
+ const remoteAdvance = {
146
+ ...plan,
147
+ card: { head: { branch: 'main', oid: oidC, detached: false } },
148
+ branches: [{ name: 'main', oid: oidC }],
149
+ };
150
+ assert.equal(hasLocalContribution(local, remoteAdvance, baseline), false);
151
+ assert.equal(hasLocalContribution({
152
+ ...local,
153
+ head: { branch: 'main', oid: oidB, detached: false },
154
+ refs: [{ name: 'main', oid: oidB }],
155
+ }, remoteAdvance, {
156
+ ...baseline,
157
+ baseOid: oidB,
158
+ pendingCard: true,
159
+ }), true);
160
+ assert.equal(hasLocalContribution(local, remoteAdvance, baseline, {
161
+ checkpoint: 'repo-checkpoint@8',
59
162
  machine: 'machine-a',
60
- replica: 'replica-a',
61
- path: path.join(tempRoot, 'repo'),
62
- repo: { remote: null, rootCommit: 'explicit-binding' },
163
+ branch: 'main',
164
+ baseOid: oidA,
165
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
166
+ to: { revision: 'old-work', indexDiff: '', worktreeDiff: 'patch' },
167
+ parked: [],
168
+ }), false);
169
+ assert.equal(hasLocalContribution(
170
+ {
171
+ head: { branch: 'main', oid: oidB, detached: false },
172
+ refs: [{ name: 'main', oid: oidB }],
173
+ },
174
+ remoteAdvance,
175
+ {
176
+ branch: 'main',
177
+ baseOid: oidB,
178
+ branches: [{ name: 'main', oid: oidB }],
179
+ },
180
+ null,
181
+ {
182
+ head: { branch: 'main', oid: oidA, detached: false },
183
+ branches: [{ name: 'main', oid: oidA }],
184
+ },
185
+ ), false);
186
+ assert.equal(hasLocalContribution(
187
+ {
188
+ head: { branch: 'main', oid: oidB, detached: false },
189
+ refs: [{ name: 'main', oid: oidB }],
190
+ },
191
+ remoteAdvance,
192
+ baseline,
193
+ null,
194
+ {
195
+ positionVersion: 10,
196
+ head: { branch: 'main', oid: oidB, detached: false },
197
+ branches: [{ name: 'main', oid: oidB, version: 10 }],
198
+ },
199
+ ), false);
200
+ assert.equal(hasLocalContribution(
201
+ {
202
+ head: { branch: 'main', oid: oidB, detached: false },
203
+ refs: [{ name: 'main', oid: oidB }],
204
+ },
205
+ remoteAdvance,
206
+ baseline,
207
+ null,
208
+ {
209
+ positionVersion: null,
210
+ head: { branch: 'main', oid: oidB, detached: false },
211
+ branches: [{ name: 'main', oid: oidB, version: null }],
212
+ },
213
+ ), true);
214
+ assert.equal(hasLocalContribution(
215
+ local,
216
+ plan,
217
+ {
218
+ ...baseline,
219
+ branches: [...baseline.branches, { name: 'feature', oid: null }],
220
+ },
221
+ null,
222
+ {
223
+ head: { branch: 'main', oid: oidA, detached: false },
224
+ branches: [
225
+ { name: 'main', oid: oidA, version: 10 },
226
+ { name: 'feature', oid: oidB, version: 10 },
227
+ ],
228
+ },
229
+ ), true);
230
+ assert.equal(hasLocalContribution(local, plan, baseline, {
231
+ checkpoint: 'repo-checkpoint@8',
232
+ machine: 'machine-a',
233
+ branch: 'main',
234
+ baseOid: oidA,
235
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
236
+ to: { revision: 'local-work', indexDiff: '', worktreeDiff: 'patch' },
237
+ parked: [],
238
+ }), true);
239
+ });
240
+
241
+ test('checkpoint work on the materialized baseline keeps the baseline card and branch numbers', () => {
242
+ const baselineOid = 'a'.repeat(40);
243
+ const railOid = 'b'.repeat(40);
244
+ const card = {
245
+ card: 'repo-card@2',
246
+ machine: 'machine-a',
247
+ replica: 'rpl_baseline',
63
248
  kind: 'primary',
249
+ head: { branch: 'main', oid: baselineOid, detached: false },
250
+ refs: [{ name: 'main', oid: baselineOid }],
64
251
  health: 'healthy',
65
- operation: 'none',
66
- head: { branch: 'main', oid: HEAD, detached: false },
67
- objects: bundle(),
68
- refs: [{ name: 'main', oid: HEAD, objects: bundle() }],
69
- dirty: { staged: 0, unstaged: 0, untracked: 0, conflicted: 0, submodulesChanged: 0 },
70
252
  };
71
- }
253
+ const stamped = stampCards(
254
+ [card],
255
+ [{
256
+ ...card,
257
+ head: { branch: 'old', oid: 'c'.repeat(40), detached: false },
258
+ positionMutationId: 'mut_stale_card_01',
259
+ positionVersion: 8,
260
+ branches: [],
261
+ }],
262
+ {
263
+ card: {
264
+ ...card,
265
+ head: { branch: 'main', oid: railOid, detached: false },
266
+ positionMutationId: 'mut_winning_card1',
267
+ positionVersion: 12,
268
+ },
269
+ branches: [{
270
+ name: 'main',
271
+ oid: railOid,
272
+ mutationId: 'mut_winning_ref01',
273
+ version: 12,
274
+ }],
275
+ },
276
+ {
277
+ branch: 'main',
278
+ baseOid: baselineOid,
279
+ cardMutationId: 'mut_baseline_card',
280
+ cardVersion: 9,
281
+ branches: [{
282
+ name: 'main',
283
+ oid: baselineOid,
284
+ mutationId: 'mut_baseline_ref1',
285
+ version: 9,
286
+ }],
287
+ },
288
+ 'mut_checkpoint_only',
289
+ { repoKey: 'root:baseline-stamp', resourceId: 'shr_baseline_stamp_test' },
290
+ )[0];
291
+
292
+ assert.equal(stamped.positionMutationId, 'mut_baseline_card');
293
+ assert.equal(stamped.positionVersion, 9);
294
+ assert.equal(stamped.branches[0].mutationId, 'mut_baseline_ref1');
295
+ assert.equal(stamped.branches[0].version, 9);
296
+ });
297
+
298
+ test('a collaborator binds the same repo to the owner project, not a parallel project', () => {
299
+ const shared = projectResourceId('repo-shared', '22222222-2222-4222-8222-222222222222');
300
+ const localPath = path.join(tempRoot, 'projects/shared');
301
+ const bound = bindProject(
302
+ 'repo-shared',
303
+ shared,
304
+ '22222222-2222-4222-8222-222222222222',
305
+ localPath,
306
+ );
307
+ assert.equal(bound.resourceId, shared);
308
+ assert.equal(projectForRepo('repo-shared').resourceId, shared);
309
+ assert.equal(projectForRepo('repo-shared').ownerUserId, '22222222-2222-4222-8222-222222222222');
310
+ assert.equal(projectForRepo('repo-shared').path, localPath);
311
+ });
312
+
313
+ test('a repo project appends immutable machine records', async () => {
314
+ const repo = path.join(tempRoot, 'projects/app');
315
+ fs.mkdirSync(repo, { recursive: true });
316
+ await run(repo, 'init', '-b', 'main');
317
+ fs.writeFileSync(path.join(repo, 'README.md'), '# app\n');
318
+ await run(repo, 'add', '.');
319
+ await run(repo, 'commit', '-m', 'initial');
320
+ upsertWorkspace({ path: path.join(tempRoot, 'projects'), name: 'Projects', source: 'computer' });
321
+ const refreshed = await refreshRepoCards();
322
+ repoKey = repoKeyOfCard(refreshed.cards[0]);
323
+ project = projectForRepo(repoKey);
324
+
325
+ fs.mkdirSync(path.dirname(project.file), { recursive: true });
326
+ fs.writeFileSync(project.file, '');
327
+ const opened = openDoc(project.file);
328
+ registerReplica({
329
+ resourceId: project.resourceId,
330
+ localResource: opened.resource,
331
+ localPath: project.file,
332
+ contract: 'text-yjs@1',
333
+ schemaVersion: 1,
334
+ authorityEpoch: 1,
335
+ });
72
336
 
73
- test('only an explicit folder UUID creates a code project', () => {
74
- assert.equal(wall.projectForRepo(REPO_KEY), null);
75
- assert.throws(
76
- () => wall.bindProject('shr_legacy_or_derived_identity', null, localCard().path),
77
- /folder UUID/,
337
+ const first = publishWall(repoKey);
338
+ assert.equal(first.published, true);
339
+ const entries = readWall(repoKey);
340
+ assert.equal(entries.length, 1);
341
+ assert.equal(entries[0].wall, 'code-project@4');
342
+ assert.equal(entries[0].project, repoKey);
343
+ assert.equal(entries[0].machine, 'machine-a');
344
+ assert.equal(entries[0].cards.length, 1);
345
+ assert.equal(entries[0].cards[0].head.branch, 'main');
346
+ assert.equal(entries[0].cards[0].positionMutationId, first.mutationId);
347
+ assert.equal(entries[0].cards[0].positionVersion, null);
348
+ assert.equal(publishedCardFor(refreshed.cards[0]), null);
349
+ const awaitingNumber = publishWall(repoKey);
350
+ assert.equal(awaitingNumber.published, false);
351
+ assert.equal(awaitingNumber.reason, 'unchanged');
352
+ assert.equal(
353
+ require('../workspace/reducer').localCardMayMaterialize(null, awaitingNumber),
354
+ false,
78
355
  );
79
- const bound = wall.bindProject(
80
- PROJECT,
81
- '11111111-1111-4111-8111-111111111111',
82
- localCard().path,
356
+
357
+ assert.deepEqual(settleProject(), [1]);
358
+ const sealed = publishWall(repoKey);
359
+ assert.equal(sealed.published, false);
360
+ assert.equal(sealed.reason, 'shared-state-covered');
361
+ const sealedCard = readWall(repoKey)[0].cards[0];
362
+ assert.equal(sealedCard.positionMutationId, first.mutationId);
363
+ assert.equal(sealedCard.positionVersion, 1);
364
+ const sealedLines = getDocText(project.file).trim().split('\n');
365
+ assert.equal(sealedLines.length, 1);
366
+ assert.equal(
367
+ JSON.parse(sealedLines.at(-1)).cards[0].positionVersion,
368
+ null,
83
369
  );
84
- assert.equal(bound.resourceId, PROJECT);
85
- assert.equal(wall.projectForRepo(REPO_KEY).resourceId, PROJECT);
86
- assert.equal(wall.projectForRepo('root:identical-unregistered-clone'), null);
370
+ assert.equal(publishedCardFor(refreshed.cards[0]).positionVersion, 1);
371
+ const readyObjects = {
372
+ ...refreshed.cards[0],
373
+ objects: { kind: 'bundle' },
374
+ refs: refreshed.cards[0].refs.map((ref) => ({
375
+ ...ref,
376
+ objects: { kind: 'bundle' },
377
+ })),
378
+ };
379
+ assert.equal(publishedCardFor(readyObjects), null);
380
+
381
+ const again = publishWall(repoKey);
382
+ assert.equal(again.published, false);
383
+ assert.equal(again.reason, 'shared-state-covered');
384
+
385
+ fs.writeFileSync(path.join(repo, 'new.txt'), 'x\n');
386
+ await refreshRepoCards();
387
+ const republished = publishWall(repoKey);
388
+ assert.equal(republished.published, false);
389
+ const after = readWall(repoKey);
390
+ assert.equal(after.length, 1);
391
+ assert.equal(Object.hasOwn(after[0].cards[0], 'dirty'), false);
392
+ assert.equal(after[0].cards[0].positionMutationId, first.mutationId);
393
+ assert.equal(after[0].cards[0].positionVersion, 1);
394
+ assert.equal(getDocText(project.file).trim().split('\n').length, 1);
87
395
  });
88
396
 
89
- test('card identity is Git state, not bundle transport metadata', () => {
90
- const first = localCard();
91
- const second = {
92
- ...first,
93
- objects: bundle(HEAD, 'c'),
94
- refs: [{ ...first.refs[0], objects: bundle(HEAD, 'd') }],
397
+ test('a checkpoint base replaces history already covered by the applied rail', async () => {
398
+ const card = readWall(repoKey)[0].cards[0];
399
+ const file = checkpointFile('machine-a', repoKey);
400
+ fs.mkdirSync(path.dirname(file), { recursive: true });
401
+ const firstCheckpoint = {
402
+ checkpoint: 'repo-checkpoint@8',
403
+ machine: 'machine-a',
404
+ repoKey,
405
+ branch: card.head.branch,
406
+ baseOid: card.head.oid,
407
+ from: { revision: null, indexDiff: '', worktreeDiff: '' },
408
+ to: { revision: 'revision-a', indexDiff: '', worktreeDiff: 'patch-a' },
409
+ parked: [],
410
+ capturedAt: '2026-07-23T00:00:01.000Z',
95
411
  };
96
- assert.equal(wall._test.sameCardState(first, second), true);
97
- assert.equal(wall._test.sameCardState(first, {
98
- ...second,
99
- refs: [{ ...second.refs[0], oid: 'e'.repeat(40) }],
100
- }), false);
412
+ fs.writeFileSync(file, `${JSON.stringify(firstCheckpoint)}\n`);
413
+ const first = publishWall(repoKey);
414
+ assert.equal(first.published, true);
415
+ assert.equal(readWall(repoKey)[0].cards[0].checkpoints.length, 1);
416
+ assert.equal(readWall(repoKey)[0].cards[0].checkpoints[0].mutationId, first.mutationId);
417
+
418
+ assert.deepEqual(settleProject(), [2]);
419
+ assert.equal(readWall(repoKey)[0].cards[0].checkpoints[0].version, 2);
420
+ assert.equal(publishWall(repoKey).published, false);
421
+ const checkpointLines = getDocText(project.file).trim().split('\n');
422
+ assert.equal(
423
+ JSON.parse(checkpointLines.at(-1)).cards[0].checkpoints[0].version ?? null,
424
+ null,
425
+ );
426
+ const reducerFile = path.join(
427
+ process.env.AMALGM_DIR,
428
+ 'state',
429
+ 'repo-reducer',
430
+ `${crypto.createHash('sha256').update(repoKey).digest('hex').slice(0, 24)}.json`,
431
+ );
432
+ fs.writeFileSync(reducerFile, `${JSON.stringify({
433
+ repoKey,
434
+ baseOid: card.head.oid,
435
+ branch: card.head.branch,
436
+ appliedThrough: 2,
437
+ indexDiff: '',
438
+ worktreeDiff: 'patch-a',
439
+ })}\n`);
440
+
441
+ fs.writeFileSync(file, `${JSON.stringify({
442
+ ...firstCheckpoint,
443
+ from: firstCheckpoint.to,
444
+ to: { revision: 'revision-b', indexDiff: '', worktreeDiff: 'patch-b' },
445
+ capturedAt: '2026-07-23T00:00:03.000Z',
446
+ })}\n`);
447
+ const second = publishWall(repoKey);
448
+ assert.equal(second.published, true);
449
+ const latestCard = readWall(repoKey)[0].cards[0];
450
+ assert.equal(latestCard.checkpointBase.through, 2);
451
+ assert.equal(latestCard.checkpoints.length, 1);
452
+ assert.deepEqual(latestCard.checkpoints.map((entry) => entry.mutationId), [second.mutationId]);
453
+ assert.deepEqual(settleProject(), [3]);
101
454
  });
102
455
 
103
- test('a physical card publishes only after an explicit local intent capture', () => {
104
- const project = wall.projectForRepo(REPO_KEY);
105
- projectStream.writeSnapshot(project.file, projectStream.emptySnapshot(PROJECT));
106
- replicas.registerReplica({
107
- resourceId: PROJECT,
108
- localResource: `${projectStream.LOCAL_RESOURCE_PREFIX}${PROJECT}`,
109
- localPath: project.file,
110
- contract: projectStream.STREAM_CONTRACT,
111
- schemaVersion: 2,
112
- authorityEpoch: 1,
113
- appliedVersion: 0,
114
- snapshotVersion: 0,
456
+ test('an uploading checkpoint base is not announced as complete', () => {
457
+ const card = readWall(repoKey)[0].cards[0];
458
+ const [stamped] = stampCards([card], [], { card, branches: [], transitions: [] }, {
459
+ repoKey,
460
+ baseOid: card.head.oid,
461
+ branch: card.head.branch,
462
+ appliedThrough: 3,
463
+ indexDiff: 'x'.repeat(600 * 1024),
464
+ worktreeDiff: '',
465
+ }, 'mut_project_pending_base', project);
466
+
467
+ assert.equal(stamped.checkpointBase, undefined);
468
+ });
469
+
470
+ test('one failed project publisher cannot block the next repo', () => {
471
+ const selected = [{ repoKey: 'root:first' }, { repoKey: 'root:second' }];
472
+ const calls = [];
473
+ const results = wallTest.publishSelected(selected, [], (selectedProject) => {
474
+ calls.push(selectedProject.repoKey);
475
+ if (selectedProject.repoKey === 'root:first') throw new Error('too large');
476
+ return { published: true, repoKey: selectedProject.repoKey };
115
477
  });
478
+ assert.deepEqual(calls, ['root:first', 'root:second']);
479
+ assert.equal(results[0].reason, 'publish-failed');
480
+ assert.equal(results[1].published, true);
481
+ });
116
482
 
117
- const card = localCard();
118
- const withoutIntent = wall._test.publishProject(project, [card]);
119
- assert.equal(withoutIntent.published, false);
120
- assert.equal(withoutIntent.reason, 'shared-state-covered');
121
- assert.equal(mutations.listPendingCloudMutations(PROJECT).length, 0);
122
-
123
- intent.writeIntent(project, {
124
- base: {
125
- through: 0,
126
- card: null,
127
- indexTree: EMPTY_TREE,
128
- worktreeTree: EMPTY_TREE,
129
- },
130
- local: {
131
- card: {
132
- head: card.head,
133
- objects: card.objects,
134
- branches: card.refs,
135
- },
136
- checkpoint: {
137
- checkpoint: checkpoint.CHECKPOINT_CONTRACT,
138
- baseOid: card.head.oid,
139
- branch: card.head.branch,
140
- value: { revision: null, indexDiff: '', worktreeDiff: '' },
141
- },
142
- indexTree: HEAD,
143
- worktreeTree: HEAD,
144
- physicalDigest: 'captured-physical-state',
145
- },
483
+ test('side branches keep one rail operation while the checked-out card advances', async () => {
484
+ const repo = path.join(tempRoot, 'projects/app');
485
+ const before = readWall(repoKey)[0].cards[0];
486
+ const branch = 'storm/persistent-ref';
487
+
488
+ await run(repo, 'branch', branch);
489
+ await refreshRepoCards();
490
+ const beforeMaterialization = getDocText(project.file);
491
+ const blocked = await require('../workspace/reducer').withMaterialization(
492
+ repoKey,
493
+ async () => publishWall(repoKey),
494
+ );
495
+ assert.equal(blocked.published, false);
496
+ assert.equal(blocked.reason, 'repo-materializing');
497
+ assert.equal(getDocText(project.file), beforeMaterialization);
498
+
499
+ const created = publishWall(repoKey);
500
+ assert.equal(created.published, true);
501
+ let card = readWall(repoKey)[0].cards[0];
502
+ const ref = card.branches.find((entry) => entry.name === branch);
503
+ assert.equal(card.positionMutationId, before.positionMutationId);
504
+ assert.equal(ref.mutationId, created.mutationId);
505
+
506
+ assert.deepEqual(settleProject(), [4]);
507
+ await run(repo, 'add', 'new.txt');
508
+ await run(repo, 'commit', '-m', 'advance without moving side branch');
509
+ await refreshRepoCards();
510
+ const advanced = publishWall(repoKey);
511
+ assert.equal(advanced.published, true);
512
+ card = readWall(repoKey)[0].cards[0];
513
+ assert.equal(card.positionMutationId, advanced.mutationId);
514
+ assert.equal(card.branches.find((entry) => entry.name === branch).mutationId, created.mutationId);
515
+
516
+ assert.deepEqual(settleProject(), [5]);
517
+ await run(repo, 'branch', '-D', branch);
518
+ await refreshRepoCards();
519
+ const deleted = publishWall(repoKey);
520
+ assert.equal(deleted.published, true);
521
+ card = readWall(repoKey)[0].cards[0];
522
+ assert.equal(card.branches.find((entry) => entry.name === branch).oid, null);
523
+ assert.equal(card.branches.find((entry) => entry.name === branch).mutationId, deleted.mutationId);
524
+ assert.deepEqual(settleProject(), [6]);
525
+ });
526
+
527
+ test('peer records and earlier local history survive later publishes', async () => {
528
+ const beforeLines = getDocText(project.file).trim().split('\n');
529
+ const peerLine = JSON.stringify({
530
+ wall: 'code-project@4',
531
+ project: repoKey,
532
+ machine: 'machine-b',
533
+ mutationId: 'mut_project_peer',
534
+ publishedAt: '2026-07-19T00:00:00.000Z',
535
+ cards: [{ card: 'repo-card@2', machine: 'machine-b', replica: 'rpl_peer', path: '/work/app', head: { branch: 'preview' } }],
146
536
  });
537
+ writeDocText(project.file, `${getDocText(project.file)}${peerLine}\n`);
538
+ assert.deepEqual(settleProject(), [7]);
147
539
 
148
- const result = wall._test.publishProject(project, [card]);
540
+ const repo = path.join(tempRoot, 'projects/app');
541
+ fs.writeFileSync(path.join(repo, 'another.txt'), 'y\n');
542
+ await run(repo, 'add', '.');
543
+ await run(repo, 'commit', '-m', 'advance card');
544
+ await refreshRepoCards();
545
+ const result = publishWall(repoKey);
149
546
  assert.equal(result.published, true);
150
- assert.equal(result.delta, 'card.set');
151
- assert.equal(projectStream.readSnapshot(project.file, PROJECT).version, 0);
152
- const pending = mutations.listPendingCloudMutations(PROJECT);
153
- assert.equal(pending.length, 1);
154
- assert.equal(pending[0].operation.delta.kind, 'card.set');
547
+
548
+ const lines = getDocText(project.file).trim().split('\n');
549
+ assert.equal(lines.length, beforeLines.length + 2);
550
+ for (const line of beforeLines) assert.ok(lines.includes(line));
551
+ assert.ok(lines.includes(peerLine));
552
+ assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
553
+ assert.deepEqual(settleProject(), [8]);
554
+ });
555
+
556
+ test('three concurrent append records stay whole under Yjs ordering', () => {
557
+ const Y = require('yjs');
558
+ const base = new Y.Doc();
559
+ base.getText('content').insert(0, '{"machine":"existing"}\n');
560
+ const baseUpdate = Y.encodeStateAsUpdate(base);
561
+ const updates = ['machine-a', 'machine-b', 'machine-c'].map((machine) => {
562
+ const writer = new Y.Doc();
563
+ Y.applyUpdate(writer, baseUpdate);
564
+ let update = null;
565
+ writer.on('update', (value) => {
566
+ update = value;
567
+ });
568
+ const text = writer.getText('content');
569
+ text.insert(text.length, `${JSON.stringify({ machine })}\n`);
570
+ return update;
571
+ });
572
+ const replicas = [updates, [...updates].reverse()].map((order) => {
573
+ const replica = new Y.Doc();
574
+ Y.applyUpdate(replica, baseUpdate);
575
+ for (const update of order) Y.applyUpdate(replica, update);
576
+ return replica.getText('content').toString();
577
+ });
578
+ assert.equal(replicas[0], replicas[1]);
155
579
  assert.deepEqual(
156
- Object.keys(pending[0].operation.delta).sort(),
157
- ['branches', 'card', 'checkpoint', 'head', 'kind', 'objects'].sort(),
580
+ replicas[0].trim().split('\n').map((line) => JSON.parse(line).machine).sort(),
581
+ ['existing', 'machine-a', 'machine-b', 'machine-c'],
158
582
  );
159
583
  });
160
584
 
161
- test('catch-up coverage creates no second outbox event', () => {
162
- const project = wall.projectForRepo(REPO_KEY);
163
- const snapshot = projectStream._test.fold(
164
- projectStream.emptySnapshot(PROJECT),
165
- mutations.listPendingCloudMutations(PROJECT)[0].operation,
166
- { machine: 'machine-a', mutationId: 'mut_official_card_0001', version: 1 },
585
+ test('wall compaction deletes only records superseded for every machine', () => {
586
+ const line = (machine, value) => JSON.stringify({
587
+ wall: 'code-project@4',
588
+ project: repoKey,
589
+ machine,
590
+ mutationId: `mut_project_${machine}_${value}`,
591
+ cards: [{ value }],
592
+ });
593
+ const oldA = line('machine-a', 'old-a');
594
+ const oldB = line('machine-b', 'old-b');
595
+ const latestA = line('machine-a', 'latest-a');
596
+ const torn = '{"wall":"code-project@4","machine":"torn"';
597
+ const latestB = line('machine-b', 'latest-b');
598
+ const current = `${oldA}\n${oldB}\n${latestA}\n${torn}\n${latestB}\n`;
599
+ const compacted = compactWallText(current, repoKey);
600
+
601
+ assert.equal(compacted, `${latestA}\n${torn}\n${latestB}\n`);
602
+ assert.equal(compacted.includes(oldA), false);
603
+ assert.equal(compacted.includes(oldB), false);
604
+ });
605
+
606
+ test('an unordered legacy record cannot pin wall compaction', () => {
607
+ const unordered = JSON.stringify({
608
+ wall: 'code-project@4',
609
+ project: repoKey,
610
+ machine: '16.612Z',
611
+ cards: [{ value: 'corrupted-legacy-record' }],
612
+ });
613
+ const ordered = (machine) => JSON.stringify({
614
+ wall: 'code-project@4',
615
+ project: repoKey,
616
+ machine,
617
+ mutationId: `mut_project_${machine}`,
618
+ cards: [{ value: machine }],
619
+ });
620
+ const latestA = ordered('machine-a');
621
+ const latestB = ordered('machine-b');
622
+
623
+ assert.equal(
624
+ compactWallText(`${unordered}\n${latestA}\n${latestB}\n`, repoKey),
625
+ `${latestA}\n${latestB}\n`,
167
626
  );
168
- projectStream.writeSnapshot(project.file, snapshot);
169
- const covered = wall._test.sameCardState(localCard(), snapshot.card);
170
- assert.equal(covered, true);
171
627
  });
172
628
 
173
- test('one failed project publisher cannot block another project', () => {
174
- const first = wall.projectForRepo(REPO_KEY);
175
- const second = wall.bindProject(
176
- '018f0000-0000-7000-8000-00000000baaf',
177
- '11111111-1111-4111-8111-111111111111',
178
- path.join(tempRoot, 'repo-second'),
629
+ test('retired machine values stay represented without pinning obsolete active history', () => {
630
+ const line = (machine, value) => JSON.stringify({
631
+ wall: 'code-project@4',
632
+ project: repoKey,
633
+ machine,
634
+ mutationId: `mut_project_${machine}_${value}`,
635
+ cards: [{ value }],
636
+ });
637
+ const retiredA = line('retired-a', 'latest-retired-a');
638
+ const retiredB = line('retired-b', 'latest-retired-b');
639
+ const oldA = line('machine-a', 'old-a');
640
+ const oldB = line('machine-b', 'old-b');
641
+ const latestA = line('machine-a', 'latest-a');
642
+ const latestB = line('machine-b', 'latest-b');
643
+
644
+ assert.equal(
645
+ compactWallText(
646
+ `${retiredA}\n${retiredB}\n${oldA}\n${oldB}\n${latestA}\n${latestB}\n`,
647
+ repoKey,
648
+ ),
649
+ `${retiredA}\n${retiredB}\n${latestA}\n${latestB}\n`,
179
650
  );
180
- const cards = [first, second].map((project, index) => ({
181
- ...localCard(),
182
- path: project.path,
183
- replica: `replica-${index}`,
184
- }));
185
- const results = wall._test.publishSelected([first, second], cards, (project) => {
186
- if (project.resourceId === first.resourceId) throw new Error('too large');
187
- return { published: true, repoKey: project.repoKey };
651
+ });
652
+
653
+ test('a concurrent deletion-only compaction and append commute in Yjs', () => {
654
+ const Y = require('yjs');
655
+ const line = (machine, value) => JSON.stringify({
656
+ wall: 'code-project@4',
657
+ project: repoKey,
658
+ machine,
659
+ mutationId: `mut_project_${machine}_${value}`,
660
+ cards: [{ value }],
188
661
  });
189
- assert.equal(results[0].published, false);
190
- assert.equal(results[1].published, true);
662
+ const oldA = line('machine-a', 'old-a');
663
+ const oldB = line('machine-b', 'old-b');
664
+ const latestA = line('machine-a', 'latest-a');
665
+ const latestB = line('machine-b', 'latest-b');
666
+ const baseText = `${oldA}\n${oldB}\n${latestA}\n${latestB}\n`;
667
+ const compacted = compactWallText(baseText, repoKey);
668
+ const base = new Y.Doc();
669
+ base.getText('content').insert(0, baseText);
670
+ const baseUpdate = Y.encodeStateAsUpdate(base);
671
+
672
+ const writerUpdate = (change) => {
673
+ const writer = new Y.Doc();
674
+ Y.applyUpdate(writer, baseUpdate);
675
+ let update = null;
676
+ writer.on('update', (value) => {
677
+ update = value;
678
+ });
679
+ change(writer.getText('content'));
680
+ return update;
681
+ };
682
+ const compactUpdate = writerUpdate((text) => {
683
+ text.delete(0, baseText.length - compacted.length);
684
+ });
685
+ const appendLine = `${line('machine-c', 'latest-c')}\n`;
686
+ const appendUpdate = writerUpdate((text) => {
687
+ text.insert(text.length, appendLine);
688
+ });
689
+
690
+ const replicas = [
691
+ [compactUpdate, appendUpdate],
692
+ [appendUpdate, compactUpdate],
693
+ ].map((order) => {
694
+ const replica = new Y.Doc();
695
+ Y.applyUpdate(replica, baseUpdate);
696
+ for (const update of order) Y.applyUpdate(replica, update);
697
+ return replica.getText('content').toString();
698
+ });
699
+ assert.equal(replicas[0], replicas[1]);
700
+ assert.deepEqual(
701
+ replicas[0].trim().split('\n').map((entry) => JSON.parse(entry).cards[0].value),
702
+ ['latest-a', 'latest-b', 'latest-c'],
703
+ );
704
+ });
705
+
706
+ test('a torn line is ignored by readers, never fatal', () => {
707
+ writeDocText(
708
+ project.file,
709
+ `${getDocText(project.file)}{"wall":"code-project@4","project":"${repoKey}","machine":"machine-c","car\n`,
710
+ );
711
+ settleProject();
712
+ assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
713
+ });
714
+
715
+ test('publishing compacts an oversized history before appending the new record', async () => {
716
+ const before = getDocText(project.file);
717
+ writeDocText(project.file, `${'obsolete\n'.repeat(132_000)}${before}`);
718
+ settleProject();
719
+ assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') > 1024 * 1024);
720
+
721
+ const repo = path.join(tempRoot, 'projects/app');
722
+ fs.writeFileSync(path.join(repo, 'after-compaction.txt'), 'bounded\n');
723
+ await run(repo, 'add', '.');
724
+ await run(repo, 'commit', '-m', 'publish after compaction');
725
+ await refreshRepoCards();
726
+ const result = publishWall(repoKey);
727
+
728
+ assert.equal(result.published, true);
729
+ assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') < 1024 * 1024);
730
+ assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
731
+ settleProject();
732
+ });
733
+
734
+ test('project stats name every project resource, binding, and publish counter', () => {
735
+ const stats = wallStats();
736
+ const row = stats.projects.find((entry) => entry.repoKey === repoKey);
737
+ assert.equal(stats.contract, 'code-project@4');
738
+ assert.equal(row.resourceId, project.resourceId);
739
+ assert.equal(row.replica.appliedVersion >= 0, true);
740
+ assert.equal(row.machines, 2);
741
+ assert.ok(stats.publishes >= 2);
742
+ assert.ok(stats.publishSkips >= 1);
743
+ assert.ok(stats.paths.includes(project.file));
744
+ });
745
+
746
+ test('ready repo objects wake the card publisher', async () => {
747
+ startWallService();
748
+ await new Promise((resolve) => setTimeout(resolve, 100));
749
+ const before = wallStats();
750
+ appendStateEvent({ resource: 'repo-objects', op: 'update', id: 'ready-bundle' });
751
+ await new Promise((resolve) => setTimeout(resolve, 100));
752
+ stopWallService();
753
+ const after = wallStats();
754
+ assert.ok(
755
+ after.publishes + after.publishSkips
756
+ > before.publishes + before.publishSkips,
757
+ );
758
+ });
759
+
760
+ test('a restarted wall publishes once even when the project is already attached', async () => {
761
+ const before = wallStats();
762
+ startWallService();
763
+ await new Promise((resolve) => setTimeout(resolve, 500));
764
+ stopWallService();
765
+ const after = wallStats();
766
+ assert.ok(
767
+ after.publishes + after.publishSkips
768
+ > before.publishes + before.publishSkips,
769
+ );
770
+ });
771
+
772
+ test('the periodic project check retries an unsent wall', async () => {
773
+ const originalSetInterval = global.setInterval;
774
+ let periodicCheck = null;
775
+ global.setInterval = (callback) => {
776
+ periodicCheck = callback;
777
+ return { unref() {} };
778
+ };
779
+ try {
780
+ startWallService();
781
+ await new Promise((resolve) => setTimeout(resolve, 100));
782
+ const before = wallStats();
783
+ periodicCheck();
784
+ await new Promise((resolve) => setTimeout(resolve, 100));
785
+ const after = wallStats();
786
+ assert.ok(
787
+ after.publishes + after.publishSkips
788
+ > before.publishes + before.publishSkips,
789
+ );
790
+ } finally {
791
+ stopWallService();
792
+ global.setInterval = originalSetInterval;
793
+ }
191
794
  });