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
@@ -21,34 +21,28 @@ fs.writeFileSync(
21
21
  );
22
22
 
23
23
  const { closeLocalDb } = require('../state/db');
24
- const checkpoint = require('../workspace/checkpoint');
25
- const { CHECKPOINT_CONTRACT, revisionOf } = checkpoint;
26
- const { locatorChecksum } = require('../workspace/objects')._test;
24
+ const { CHECKPOINT_CONTRACT, revisionOf } = require('../workspace/checkpoint');
27
25
  const {
28
26
  advanceIndex,
29
- applyHydrationBootstrap,
27
+ captureIsOnRail,
30
28
  hydrateRepo,
31
29
  localCardMayMaterialize,
32
30
  materializeRepo,
31
+ parkedForState,
33
32
  pathForRepo,
34
33
  readRepoState,
35
- readOfficialPlan,
36
- repairDelayForPass,
37
34
  reduceCheckpoint,
38
35
  reduceWall,
39
- snapshotIsCurrent,
40
36
  } = require('../workspace/reducer');
41
37
 
42
38
  test('a materialized baseline can follow the rail without publishing an echo', () => {
43
39
  assert.equal(localCardMayMaterialize(null, {
44
40
  published: false,
45
41
  reason: 'shared-state-covered',
46
- mayMaterialize: true,
47
42
  }), true);
48
43
  assert.equal(localCardMayMaterialize(null, {
49
44
  published: false,
50
45
  reason: 'project-not-current',
51
- mayMaterialize: true,
52
46
  }), true);
53
47
  assert.equal(localCardMayMaterialize({ positionVersion: 7 }, null), true);
54
48
  assert.equal(localCardMayMaterialize(null, {
@@ -58,10 +52,6 @@ test('a materialized baseline can follow the rail without publishing an echo', (
58
52
  published: false,
59
53
  reason: 'unchanged',
60
54
  }), false);
61
- assert.equal(localCardMayMaterialize(null, {
62
- published: false,
63
- reason: 'future-reason-that-must-not-change-control-flow',
64
- }), false);
65
55
  });
66
56
 
67
57
  test('a hydration failure still has a project path before a local card exists', () => {
@@ -69,59 +59,6 @@ test('a hydration failure still has a project path before a local card exists',
69
59
  assert.equal(pathForRepo({ path: '/projects/local' }, { path: '/projects/fresh' }), '/projects/local');
70
60
  });
71
61
 
72
- test('a reducer cursor belongs only to the rail that created it', () => {
73
- const repoKey = 'root:rail-scoped-cursor';
74
- const digest = require('crypto').createHash('sha256').update(repoKey).digest('hex').slice(0, 24);
75
- const file = path.join(process.env.AMALGM_DIR, 'state', 'repo-reducer', `${digest}.json`);
76
- fs.mkdirSync(path.dirname(file), { recursive: true });
77
- fs.writeFileSync(file, `${JSON.stringify({
78
- repoKey,
79
- resourceId: 'shr_old_project_rail',
80
- appliedThrough: 2764,
81
- })}\n`);
82
-
83
- assert.equal(readRepoState(repoKey, 'shr_old_project_rail').appliedThrough, 2764);
84
- assert.equal(readRepoState(repoKey, 'shr_new_project_rail'), null);
85
- });
86
-
87
- test('a received cursor cannot advance past the exact local project snapshot', () => {
88
- assert.equal(snapshotIsCurrent(51, 51), true);
89
- assert.equal(snapshotIsCurrent(51, 50), false);
90
- assert.equal(snapshotIsCurrent(51, 52), false);
91
- assert.equal(snapshotIsCurrent(null, 0), false);
92
- });
93
-
94
- test('an acknowledged event cannot hydrate an empty folder before its sealed snapshot arrives', () => {
95
- const target = path.join(tempRoot, 'hydrate-before-seal');
96
- const resourceId = '018f0000-0000-7000-8000-000000000020';
97
- const snapshot = path.join(tempRoot, 'snapshot-before-seal.json');
98
- fs.mkdirSync(target, { recursive: true });
99
- fs.writeFileSync(snapshot, JSON.stringify({
100
- schemaVersion: 2,
101
- project: resourceId,
102
- version: 0,
103
- card: null,
104
- checkpoint: null,
105
- }));
106
-
107
- const outcome = readOfficialPlan(
108
- 'root:snapshot-gate',
109
- { file: snapshot, resourceId },
110
- {
111
- publicationStatus: () => ({ receivedThrough: 1 }),
112
- readWall: () => [],
113
- },
114
- );
115
-
116
- assert.deepEqual(outcome, {
117
- state: 'waiting',
118
- reason: 'official-snapshot-behind',
119
- receivedThrough: 1,
120
- snapshotThrough: 0,
121
- });
122
- assert.deepEqual(fs.readdirSync(target), []);
123
- });
124
-
125
62
  test.after(() => {
126
63
  closeLocalDb();
127
64
  fs.rmSync(tempRoot, { recursive: true, force: true });
@@ -136,11 +73,6 @@ async function git(cwd, ...args) {
136
73
  return stdout;
137
74
  }
138
75
 
139
- async function gitPath(cwd, relativePath) {
140
- const value = (await git(cwd, 'rev-parse', '--git-path', relativePath)).trim();
141
- return path.isAbsolute(value) ? value : path.resolve(cwd, value);
142
- }
143
-
144
76
  async function makeRepo(name) {
145
77
  const repo = path.join(tempRoot, name);
146
78
  fs.mkdirSync(repo, { recursive: true });
@@ -183,7 +115,7 @@ function transition(machine, version, baseOid, from, to) {
183
115
 
184
116
  function card(repo, oid, version) {
185
117
  return {
186
- card: 'repo-card@3',
118
+ card: 'repo-card@2',
187
119
  path: repo,
188
120
  replica: 'rpl_local',
189
121
  machine: 'machine-local',
@@ -198,72 +130,14 @@ function card(repo, oid, version) {
198
130
  };
199
131
  }
200
132
 
201
- let trackedProjectNumber = 100;
202
-
203
- async function observedCard(base) {
204
- const headOid = (await git(base.path, 'rev-parse', 'HEAD')).trim();
205
- const branch = (await git(base.path, 'branch', '--show-current')).trim() || null;
206
- const refs = (await git(
207
- base.path,
208
- 'for-each-ref',
209
- '--format=%(refname:short) %(objectname)',
210
- 'refs/heads',
211
- )).trim().split('\n').filter(Boolean).map((line) => {
212
- const split = line.lastIndexOf(' ');
213
- return {
214
- name: line.slice(0, split),
215
- oid: line.slice(split + 1),
216
- objects: { kind: 'local' },
217
- };
218
- });
219
- return {
220
- ...base,
221
- head: { branch, oid: headOid, detached: branch === null },
222
- objects: { kind: 'local' },
223
- refs,
224
- };
225
- }
226
-
227
- async function establishTrackedBaseline(repo, oid, name) {
228
- trackedProjectNumber += 1;
229
- const resourceId = `018f0000-0000-7000-8000-${String(trackedProjectNumber).padStart(12, '0')}`;
230
- const project = require('../workspace/wall').bindProject(
231
- resourceId,
232
- '11111111-1111-4111-8111-111111111111',
233
- repo,
234
- );
235
- const localCard = await observedCard({
236
- ...card(repo, oid, 1),
237
- replica: `rpl_${name}`,
238
- });
239
- const baseline = {
240
- repoKey: project.repoKey,
241
- card: localCard,
242
- transitions: [],
243
- branches: localCard.refs.map((ref) => ({ ...ref, version: 1 })),
244
- };
245
- assert.equal((await materializeRepo(localCard, baseline, 1)).state, 'in-sync');
246
- return { project, repoKey: project.repoKey, localCard, baseline };
247
- }
248
-
249
- function reducerStateFile(repoKey) {
250
- const digest = require('crypto').createHash('sha256').update(repoKey).digest('hex').slice(0, 24);
251
- return path.join(process.env.AMALGM_DIR, 'state', 'repo-reducer', `${digest}.json`);
252
- }
253
-
254
- async function captureTrackedIntent(project, localCard) {
255
- const current = await observedCard(localCard);
256
- const captured = await checkpoint.captureIntentForCard(project, current);
257
- assert.equal(captured.state, 'captured');
258
- return current;
259
- }
260
-
261
133
  test('wall reduction chooses one rail winner and orders checkpoint transitions by rail', () => {
262
134
  const oldCard = card('/old', 'a'.repeat(40), 4);
263
135
  oldCard.checkpoints = [transition('machine-a', 9, oldCard.head.oid, '', 'a')];
264
136
  const newCard = card('/new', 'b'.repeat(40), 7);
265
137
  const later = transition('machine-b', 11, newCard.head.oid, 'b', 'c');
138
+ later.parked = [{ oid: 'd'.repeat(40) }];
266
139
  const earlier = transition('machine-b', 8, newCard.head.oid, '', 'b');
140
+ earlier.parked = [{ oid: 'c'.repeat(40) }];
267
141
  newCard.checkpoints = [later, earlier];
268
142
  const plans = reduceWall([
269
143
  { machine: 'machine-a', cards: [oldCard] },
@@ -272,50 +146,139 @@ test('wall reduction chooses one rail winner and orders checkpoint transitions b
272
146
  const plan = plans.get('root:test');
273
147
  assert.equal(plan.card.positionVersion, 7);
274
148
  assert.deepEqual(plan.transitions.map((item) => item.version), [8, 11]);
149
+ assert.deepEqual(plan.parked.map((item) => [item.oid, item.version]), [
150
+ ['c'.repeat(40), 8],
151
+ ['d'.repeat(40), 11],
152
+ ]);
153
+ });
154
+
155
+ test('a checkpoint base replaces covered history and keeps only newer mutations', async () => {
156
+ const { repo, oid } = await makeRepo('checkpoint-base');
157
+ const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
158
+ const patchAB = await patchFor(repo, { 'a.txt': 'a1\n', 'b.txt': 'b1\n' });
159
+ const current = card(repo, oid, 1);
160
+ current.checkpointBase = {
161
+ checkpoint: CHECKPOINT_CONTRACT,
162
+ baseOid: oid,
163
+ branch: 'main',
164
+ through: 4,
165
+ value: value(oid, patchA),
166
+ };
167
+ current.checkpoints = [
168
+ transition('machine-a', 2, oid, '', patchA),
169
+ transition('machine-a', 5, oid, patchA, patchAB),
170
+ ];
171
+ const plan = reduceWall([{ machine: 'machine-a', cards: [current] }]).get('root:test');
172
+ assert.equal(plan.checkpointBase.through, 4);
173
+ assert.deepEqual(plan.transitions.map((entry) => entry.version), [5]);
174
+ const reduced = await reduceCheckpoint(current, plan);
175
+ assert.equal(reduced.state, 'ready');
176
+ assert.equal(reduced.checkpointRevision, revisionOf(oid, patchAB, ''));
275
177
  });
276
178
 
277
- test('copies of one numbered card reduce to the copy with ready objects', () => {
179
+ test('an unfinished checkpoint base cannot replace the last complete base', () => {
278
180
  const oid = 'a'.repeat(40);
279
- const mutationId = 'mut_same_card_01';
280
- const pending = card('/pending', oid, 7);
281
- pending.replica = 'rpl_pending';
282
- pending.positionMutationId = mutationId;
283
- pending.objects = { kind: 'pending' };
284
- pending.branches = [{
285
- name: 'feature',
181
+ const complete = card('/complete', oid, 1);
182
+ complete.checkpointBase = {
183
+ checkpoint: CHECKPOINT_CONTRACT,
184
+ baseOid: oid,
185
+ branch: 'main',
186
+ through: 4,
187
+ value: value(oid, 'complete-patch'),
188
+ };
189
+ complete.checkpoints = [
190
+ transition('machine-a', 5, oid, 'complete-patch', 'later-patch'),
191
+ ];
192
+
193
+ const unfinished = card('/unfinished', oid, 1);
194
+ unfinished.checkpointBase = {
195
+ checkpoint: CHECKPOINT_CONTRACT,
196
+ baseOid: oid,
197
+ branch: 'main',
198
+ through: 6,
199
+ value: {
200
+ revision: 'b'.repeat(64),
201
+ payload: {
202
+ kind: 'pending',
203
+ contract: 'opaque-chunks@1',
204
+ checksum: 'c'.repeat(64),
205
+ },
206
+ },
207
+ };
208
+
209
+ const plan = reduceWall([
210
+ { machine: 'machine-a', cards: [complete] },
211
+ { machine: 'machine-b', cards: [unfinished] },
212
+ ]).get('root:test');
213
+
214
+ assert.equal(plan.checkpointBase.through, 4);
215
+ assert.equal(plan.checkpointBaseConflict, false);
216
+ assert.deepEqual(plan.transitions.map((entry) => entry.version), [5]);
217
+ });
218
+
219
+ test('different snapshots claiming the same rail position stop instead of choosing a winner', () => {
220
+ const oid = 'a'.repeat(40);
221
+ const first = card('/first', oid, 1);
222
+ first.checkpointBase = {
223
+ checkpoint: CHECKPOINT_CONTRACT,
224
+ baseOid: oid,
225
+ branch: 'main',
226
+ through: 4,
227
+ value: value(oid, 'patch-a'),
228
+ };
229
+ const second = card('/second', oid, 1);
230
+ second.checkpointBase = {
231
+ checkpoint: CHECKPOINT_CONTRACT,
232
+ baseOid: oid,
233
+ branch: 'main',
234
+ through: 4,
235
+ value: value(oid, 'patch-b'),
236
+ };
237
+ const plan = reduceWall([
238
+ { machine: 'machine-a', cards: [first] },
239
+ { machine: 'machine-b', cards: [second] },
240
+ ]).get('root:test');
241
+ assert.equal(plan.checkpointBase, null);
242
+ assert.equal(plan.checkpointBaseConflict, true);
243
+ });
244
+
245
+ test('a ready parked bundle never regresses to pending', () => {
246
+ const oid = 'c'.repeat(40);
247
+ const ready = card('/ready', 'a'.repeat(40), 1);
248
+ const readyCheckpoint = transition('machine-ready', 2, ready.head.oid, '', '');
249
+ readyCheckpoint.parked = [{
286
250
  oid,
287
- mutationId,
288
- version: 7,
289
- objects: { kind: 'pending' },
251
+ ref: `refs/amalgm/parked/${oid}`,
252
+ objects: { kind: 'bundle', checksum: 'd'.repeat(64) },
290
253
  }];
254
+ ready.checkpoints = [readyCheckpoint];
291
255
 
292
- const ready = card('/ready', oid, 7);
293
- ready.replica = 'rpl_ready';
294
- ready.positionMutationId = mutationId;
295
- ready.objects = { kind: 'bundle', checksum: 'b'.repeat(64) };
296
- ready.branches = [{
297
- name: 'feature',
256
+ const pending = card('/pending', ready.head.oid, 3);
257
+ const pendingCheckpoint = transition('machine-pending', 4, pending.head.oid, '', '');
258
+ pendingCheckpoint.parked = [{
298
259
  oid,
299
- mutationId,
300
- version: 7,
301
- objects: { kind: 'bundle', checksum: 'b'.repeat(64) },
260
+ ref: `refs/amalgm/parked/${oid}`,
261
+ objects: { kind: 'pending' },
302
262
  }];
263
+ pending.checkpoints = [pendingCheckpoint];
303
264
 
304
- for (const entries of [
305
- [
306
- { machine: 'machine-pending', cards: [pending] },
307
- { machine: 'machine-ready', cards: [ready] },
308
- ],
309
- [
310
- { machine: 'machine-ready', cards: [ready] },
311
- { machine: 'machine-pending', cards: [pending] },
312
- ],
313
- ]) {
314
- const plan = reduceWall(entries).get('root:test');
315
- assert.equal(plan.card.positionVersion, 7);
316
- assert.equal(plan.card.objects.kind, 'bundle');
317
- assert.equal(plan.branches.find((ref) => ref.name === 'feature').objects.kind, 'bundle');
318
- }
265
+ const plan = reduceWall([
266
+ { machine: 'machine-ready', cards: [ready] },
267
+ { machine: 'machine-pending', cards: [pending] },
268
+ ]).get('root:test');
269
+ assert.equal(plan.parked.length, 1);
270
+ assert.equal(plan.parked[0].objects.kind, 'bundle');
271
+ });
272
+
273
+ test('an unpublished local parked commit stays active until the rail records it', () => {
274
+ const oid = 'f'.repeat(40);
275
+ const local = { oid, objects: { kind: 'bundle', checksum: 'local' } };
276
+ assert.deepEqual(parkedForState([local], [], []), [local]);
277
+ assert.deepEqual(parkedForState([local], [{ ...local, version: 7 }], []), [{
278
+ ...local,
279
+ version: 7,
280
+ }]);
281
+ assert.deepEqual(parkedForState([{ ...local, version: 7 }], [], []), []);
319
282
  });
320
283
 
321
284
  test('a stale lower rail card cannot roll back a newer branch and commit', () => {
@@ -344,102 +307,29 @@ test('checkpoint reduction reads only the current rail contract', () => {
344
307
  assert.deepEqual(plan.transitions.map((item) => item.version), [3]);
345
308
  });
346
309
 
347
- test('an official checkpoint with a pending payload is terminal corruption', async () => {
310
+ test('an old pending payload does not block the later complete checkpoint transition', async () => {
348
311
  const { repo, oid } = await makeRepo('pending-payload');
349
312
  const diff = await patchFor(repo, { 'a.txt': 'a1\n' });
350
313
  const pending = transition('machine-a', 2, oid, '', '');
351
314
  pending.from = {
352
315
  revision: 'a'.repeat(64),
353
- payload: { kind: 'pending', contract: 'opaque-chunks@2', checksum: 'b'.repeat(64) },
316
+ payload: { kind: 'pending', contract: 'opaque-chunks@1', checksum: 'b'.repeat(64) },
354
317
  };
355
318
  pending.to = pending.from;
356
319
  const complete = transition('machine-a', 3, oid, '', diff);
357
- await assert.rejects(
358
- reduceCheckpoint(
359
- { path: repo },
360
- { card: card(repo, oid, 1), transitions: [pending, complete] },
361
- ),
362
- /official checkpoint is payload-pending/,
363
- );
364
- });
365
-
366
- test('hydration reimports official objects when its checkout is replaced before recovery', async () => {
367
- const { repo: source, oid } = await makeRepo('hydrate-replaced-source');
368
- const bundle = path.join(tempRoot, 'hydrate-replaced.bundle');
369
- await git(source, 'bundle', 'create', bundle, 'main');
370
- const bytes = fs.readFileSync(bundle);
371
- const digest = require('crypto').createHash('sha256').update(bytes).digest('hex');
372
- const locatorChunks = [{ sha256: digest, bytes: bytes.length }];
373
- const chunks = path.join(process.env.AMALGM_DIR, 'state', 'repo-objects', 'chunks');
374
- fs.mkdirSync(chunks, { recursive: true });
375
- fs.writeFileSync(path.join(chunks, `${digest}.bin`), bytes);
376
-
377
- const target = path.join(tempRoot, 'hydrate-replaced-target');
378
- fs.mkdirSync(target, { recursive: true });
379
- const resourceId = '018f0000-0000-7000-8000-000000000022';
380
- const project = { path: target, resourceId };
381
- const winning = card('/remote', oid, 1);
382
- winning.objects = {
383
- kind: 'bundle',
384
- contract: 'git-bundle-chunks@2',
385
- resourceId,
386
- oid,
387
- ref: `refs/amalgm/exports/${oid}`,
388
- checksum: locatorChecksum(locatorChunks),
389
- bytes: bytes.length,
390
- chunks: locatorChunks,
391
- };
392
- const plan = {
393
- repoKey: `code-folder:${resourceId}`,
394
- card: winning,
395
- branches: [],
396
- transitions: [],
397
- };
398
- require('../workspace/wall').bindProject(
399
- resourceId,
400
- '11111111-1111-4111-8111-111111111111',
401
- target,
320
+ const reduced = await reduceCheckpoint(
321
+ { path: repo },
322
+ { card: card(repo, oid, 1), transitions: [pending, complete] },
402
323
  );
403
-
404
- const transitions = require('../workspace/transition');
405
- const updateBranchRefs = transitions.updateBranchRefs;
406
- let interrupt = true;
407
- transitions.updateBranchRefs = async (...args) => {
408
- await updateBranchRefs(...args);
409
- if (interrupt) {
410
- interrupt = false;
411
- throw new Error('simulated process death before hydration execution');
412
- }
413
- };
414
- try {
415
- await assert.rejects(
416
- hydrateRepo(project, plan),
417
- /simulated process death before hydration execution/,
418
- );
419
- } finally {
420
- transitions.updateBranchRefs = updateBranchRefs;
421
- }
422
- assert.equal(readRepoState(plan.repoKey, resourceId).hydrating.contract, 'repo-hydrate@5');
423
-
424
- const displaced = `${target}.displaced`;
425
- fs.renameSync(target, displaced);
426
- fs.mkdirSync(target);
427
-
428
- const result = await hydrateRepo(project, plan);
429
- assert.equal(result.state, 'hydrated', JSON.stringify(result));
430
- assert.equal((await git(target, 'rev-parse', 'HEAD')).trim(), oid);
431
- await git(target, 'cat-file', '-e', 'HEAD^{commit}');
432
- await git(target, 'status', '--porcelain=v2', '--branch');
433
- await git(target, 'fsck', '--connectivity-only', '--no-dangling');
434
- assert.equal(readRepoState(plan.repoKey, resourceId).materialized, true);
324
+ assert.equal(reduced.state, 'ready');
325
+ assert.match(reduced.indexDiff, /a1/);
435
326
  });
436
327
 
437
- test('hydration preserves unknown files and a branch committed while recovery was down', async () => {
328
+ test('an empty project folder hydrates from the winning card bundle without cloning', async () => {
438
329
  const sourceRepo = await makeRepo('hydrate-source');
439
330
  const source = sourceRepo.repo;
440
331
  fs.mkdirSync(path.join(source, '.amalgam', 'context'), { recursive: true });
441
332
  fs.writeFileSync(path.join(source, '.amalgam', 'context', 'notes.md'), 'committed note\n');
442
- fs.symlinkSync('notes.md', path.join(source, '.amalgam', 'context', 'current.md'));
443
333
  await git(source, 'add', '.');
444
334
  await git(source, 'commit', '-m', 'track project memory');
445
335
  const oid = (await git(source, 'rev-parse', 'HEAD')).trim();
@@ -447,7 +337,6 @@ test('hydration preserves unknown files and a branch committed while recovery wa
447
337
  await git(source, 'bundle', 'create', bundle, 'main');
448
338
  const bytes = fs.readFileSync(bundle);
449
339
  const digest = require('crypto').createHash('sha256').update(bytes).digest('hex');
450
- const locatorChunks = [{ sha256: digest, bytes: bytes.length }];
451
340
  const chunks = path.join(process.env.AMALGM_DIR, 'state', 'repo-objects', 'chunks');
452
341
  fs.mkdirSync(chunks, { recursive: true });
453
342
  fs.writeFileSync(path.join(chunks, `${digest}.bin`), bytes);
@@ -455,350 +344,29 @@ test('hydration preserves unknown files and a branch committed while recovery wa
455
344
  const target = path.join(tempRoot, 'hydrate-target');
456
345
  fs.mkdirSync(path.join(target, '.amalgam', 'context'), { recursive: true });
457
346
  fs.writeFileSync(path.join(target, '.amalgam', 'context', 'notes.md'), 'local note\n');
458
- fs.symlinkSync('notes.md', path.join(target, '.amalgam', 'context', 'current.md'));
459
347
  const winning = card('/remote', oid, 1);
460
348
  winning.repo.remote = 'https://example.com/org/repo';
461
349
  winning.objects = {
462
350
  kind: 'bundle',
463
- contract: 'git-bundle-chunks@2',
351
+ contract: 'git-bundle-chunks@1',
464
352
  resourceId: 'shr_hydrationProof123456',
465
353
  oid,
466
354
  ref: `refs/amalgm/exports/${oid}`,
467
- checksum: locatorChecksum(locatorChunks),
355
+ checksum: digest,
468
356
  bytes: bytes.length,
469
- chunks: locatorChunks,
357
+ chunks: [{ sha256: digest, bytes: bytes.length }],
470
358
  };
471
359
 
472
- const resourceId = '018f0000-0000-7000-8000-000000000021';
473
- winning.objects.resourceId = resourceId;
474
- const repoKey = `code-folder:${resourceId}`;
475
- const project = { path: target, resourceId };
476
- const plan = {
477
- repoKey,
478
- card: winning,
479
- branches: [{ name: 'secondary', oid, version: 1 }],
480
- transitions: [],
481
- };
482
- require('../workspace/wall').bindProject(
483
- project.resourceId,
484
- '11111111-1111-4111-8111-111111111111',
485
- target,
486
- );
487
- const transitions = require('../workspace/transition');
488
- const updateBranchRefs = transitions.updateBranchRefs;
489
- let interruptBootstrap = true;
490
- transitions.updateBranchRefs = async (...args) => {
491
- await updateBranchRefs(...args);
492
- if (interruptBootstrap) {
493
- interruptBootstrap = false;
494
- assert.equal(
495
- transitions.gitMetadataReceiptCrossed(
496
- target,
497
- args[2],
498
- 'index',
499
- ),
500
- true,
501
- 'the bootstrap index receipt survives until hydration execution is sealed',
502
- );
503
- throw new Error('simulated process death during hydration');
504
- }
505
- };
506
- try {
507
- await assert.rejects(
508
- hydrateRepo(project, plan),
509
- /simulated process death/,
510
- );
511
- } finally {
512
- transitions.updateBranchRefs = updateBranchRefs;
513
- }
514
- assert.equal(fs.readFileSync(path.join(target, '.amalgam', 'context', 'notes.md'), 'utf8'), 'local note\n');
515
- const interrupted = readRepoState(repoKey, project.resourceId);
516
- assert.equal(interrupted.hydrating.contract, 'repo-hydrate@5');
517
- assert.equal(
518
- transitions.gitMetadataReceiptCrossed(
519
- target,
520
- interrupted.hydrating.operationId,
521
- 'index',
522
- ),
523
- true,
524
- );
525
- assert.equal(interrupted.materialized, false);
526
- assert.equal(
527
- require('../workspace/intent').readOwnedIntent(
528
- require('../workspace/wall').projectForResource(project.resourceId),
529
- ),
530
- null,
531
- 'the process died after the WAL and partial Git movement but before intent existed',
360
+ const result = await hydrateRepo(
361
+ { path: target },
362
+ { repoKey: 'https://example.com/org/repo', card: winning, transitions: [] },
532
363
  );
364
+ assert.equal(result.state, 'hydrated');
533
365
  assert.equal((await git(target, 'rev-parse', 'HEAD')).trim(), oid);
534
- await git(target, 'switch', '-c', 'feature');
535
- fs.writeFileSync(path.join(target, 'feature-commit.txt'), 'committed while down\n');
536
- await git(target, 'add', 'feature-commit.txt');
537
- await git(target, 'commit', '-m', 'feature committed while hydration was down');
538
- const featureOid = (await git(target, 'rev-parse', 'HEAD')).trim();
539
- fs.writeFileSync(path.join(target, '.amalgam', 'context', 'notes.md'), 'edited while down\n');
540
- fs.writeFileSync(path.join(target, 'downtime.txt'), 'created while down\n');
541
-
542
- const releaseBranchReceipt = transitions.releaseBranchReceipt;
543
- let interruptBootstrapCleanup = true;
544
- transitions.releaseBranchReceipt = async (...args) => {
545
- const result = await releaseBranchReceipt(...args);
546
- if (interruptBootstrapCleanup) {
547
- interruptBootstrapCleanup = false;
548
- throw new Error('simulated process death after hydration bootstrap cleanup');
549
- }
550
- return result;
551
- };
552
- try {
553
- await assert.rejects(
554
- materializeRepo({
555
- ...winning,
556
- path: target,
557
- replica: 'rpl_hydration_recovery',
558
- }, plan, 1),
559
- /simulated process death after hydration bootstrap cleanup/,
560
- );
561
- } finally {
562
- transitions.releaseBranchReceipt = releaseBranchReceipt;
563
- }
564
- const cleanupInterrupted = readRepoState(repoKey, project.resourceId);
565
- assert.equal(
566
- cleanupInterrupted.hydrating.bootstrapSealed,
567
- true,
568
- 'execution is durably sealed before bootstrap provenance is released',
569
- );
570
- assert.equal(cleanupInterrupted.hydrating.bootstrapReleased, undefined);
571
- await git(target, 'branch', '-D', 'secondary');
572
-
573
- const result = await materializeRepo({
574
- ...winning,
575
- path: target,
576
- replica: 'rpl_hydration_recovery',
577
- }, plan, 1);
578
- assert.equal(result.state, 'hydrated', JSON.stringify(result));
579
- assert.equal((await git(target, 'rev-parse', 'HEAD')).trim(), featureOid);
580
- assert.equal((await git(target, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
581
- assert.equal((await git(target, 'rev-parse', 'refs/heads/feature')).trim(), featureOid);
582
- assert.equal((await git(target, 'rev-parse', 'refs/heads/main')).trim(), oid);
583
- await assert.rejects(
584
- git(target, 'rev-parse', '--verify', 'refs/heads/secondary'),
585
- /Needed a single revision|unknown revision|not a valid object name/,
586
- 'a local deletion after bootstrap cleanup remains overlay on restart',
587
- );
588
- assert.equal(fs.readFileSync(path.join(target, 'feature-commit.txt'), 'utf8'), 'committed while down\n');
366
+ assert.equal((await git(target, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
589
367
  assert.equal(fs.readFileSync(path.join(target, 'a.txt'), 'utf8'), 'a0\n');
590
- assert.equal(fs.readFileSync(path.join(target, '.amalgam', 'context', 'notes.md'), 'utf8'), 'edited while down\n');
591
- assert.equal(fs.readlinkSync(path.join(target, '.amalgam', 'context', 'current.md')), 'notes.md');
592
- assert.equal(fs.readFileSync(path.join(target, 'downtime.txt'), 'utf8'), 'created while down\n');
368
+ assert.equal(fs.readFileSync(path.join(target, '.amalgam', 'context', 'notes.md'), 'utf8'), 'local note\n');
593
369
  assert.match(await git(target, 'status', '--porcelain=v1'), /\.amalgam\/context\/notes\.md/);
594
- assert.equal(readRepoState(repoKey, project.resourceId).hydrating, undefined);
595
- assert.equal(readRepoState(repoKey, project.resourceId).materialized, true);
596
- assert.equal(
597
- transitions.gitMetadataReceiptCrossed(
598
- target,
599
- interrupted.hydrating.operationId,
600
- 'index',
601
- ),
602
- false,
603
- );
604
- assert.equal(
605
- require('../workspace/intent').readOwnedIntent(
606
- require('../workspace/wall').projectForResource(project.resourceId),
607
- )?.base?.through,
608
- 1,
609
- 'unknown hydration bytes become durable intent over the official target',
610
- );
611
-
612
- const hydratedCard = await require('../workspace/card').buildRepoCard(target);
613
- const hydrationIntent = require('../workspace/intent').readOwnedIntent(
614
- require('../workspace/wall').projectForResource(project.resourceId),
615
- );
616
- assert.equal(readRepoState(repoKey, project.resourceId).replica, hydratedCard.replica);
617
- for (const tree of [
618
- hydrationIntent.base.indexTree,
619
- hydrationIntent.base.worktreeTree,
620
- hydrationIntent.local.indexTree,
621
- hydrationIntent.local.worktreeTree,
622
- ]) {
623
- assert.equal(await require('../workspace/merge').hasTree(target, tree), true);
624
- }
625
- const settled = await materializeRepo(hydratedCard, plan, 1);
626
- assert.equal(settled.state, 'in-sync', JSON.stringify(settled));
627
- assert.equal(
628
- require('../workspace/intent').readOwnedIntent(
629
- require('../workspace/wall').projectForResource(project.resourceId),
630
- )?.semanticDigest,
631
- hydrationIntent.semanticDigest,
632
- 're-entry retains ownership of the bytes captured after the hydration WAL',
633
- );
634
- assert.equal(
635
- fs.readFileSync(path.join(target, '.amalgam', 'context', 'notes.md'), 'utf8'),
636
- 'edited while down\n',
637
- 'a later reducer pass cannot overwrite the captured collision',
638
- );
639
- assert.equal(fs.readFileSync(path.join(target, 'downtime.txt'), 'utf8'), 'created while down\n');
640
- });
641
-
642
- test('hydration bootstrap preserves a branch-only write after ref crossing and before HEAD attachment', async () => {
643
- const source = await makeRepo('hydrate-branch-only-source');
644
- const localOid = source.oid;
645
- fs.writeFileSync(path.join(source.repo, 'official.txt'), 'official\n');
646
- await git(source.repo, 'add', 'official.txt');
647
- await git(source.repo, 'commit', '-m', 'official');
648
- const officialOid = (await git(source.repo, 'rev-parse', 'HEAD')).trim();
649
- const target = path.join(tempRoot, 'hydrate-branch-only-target');
650
- fs.mkdirSync(target);
651
- await git(target, 'init', '-b', 'main');
652
- await git(target, 'fetch', source.repo, 'main');
653
- const operationId = '7'.repeat(64);
654
- const hydrating = {
655
- operationId,
656
- bootstrap: {
657
- source: {
658
- head: { branch: 'main', oid: null },
659
- branches: [],
660
- },
661
- target: {
662
- head: { branch: 'feature', oid: officialOid },
663
- branches: [{ name: 'feature', oid: officialOid }],
664
- },
665
- },
666
- };
667
- const transitions = require('../workspace/transition');
668
- await transitions.ensureEmptyIndex(target, operationId);
669
- await transitions.updateBranchRefs(target, [
670
- { branch: 'feature', expectedOid: null, oid: officialOid },
671
- ], operationId);
672
- await git(target, 'update-ref', 'refs/heads/feature', localOid, officialOid);
673
-
674
- const result = await applyHydrationBootstrap(target, hydrating);
675
- assert.deepEqual(result, { state: 'ready' });
676
- assert.equal((await git(target, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
677
- assert.equal((await git(target, 'rev-parse', 'HEAD')).trim(), localOid);
678
- assert.equal((await git(target, 'rev-parse', 'refs/heads/feature')).trim(), localOid);
679
- });
680
-
681
- test('hydration bootstrap leaves a racing same-branch commit for execution reconciliation', async () => {
682
- const { repo, oid: sourceOid } = await makeRepo('hydrate-head-sealed-source');
683
- await git(repo, 'switch', '--detach', sourceOid);
684
- fs.writeFileSync(path.join(repo, 'official.txt'), 'official\n');
685
- await git(repo, 'add', 'official.txt');
686
- await git(repo, 'commit', '-m', 'official');
687
- const officialOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
688
- await git(repo, 'switch', '-C', 'main', sourceOid);
689
-
690
- const transitions = require('../workspace/transition');
691
- const updateBranchRefs = transitions.updateBranchRefs;
692
- let localOid = null;
693
- transitions.updateBranchRefs = async (...args) => {
694
- const result = await updateBranchRefs(...args);
695
- fs.writeFileSync(path.join(repo, 'racing-local.txt'), 'local commit\n');
696
- await git(repo, 'add', 'racing-local.txt');
697
- await git(repo, 'commit', '-m', 'local commit during hydration apply');
698
- localOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
699
- return result;
700
- };
701
- let result;
702
- try {
703
- result = await applyHydrationBootstrap(repo, {
704
- operationId: '6'.repeat(64),
705
- bootstrap: {
706
- source: {
707
- head: { branch: 'main', oid: sourceOid },
708
- branches: [{ name: 'main', oid: sourceOid }],
709
- },
710
- target: {
711
- head: { branch: 'main', oid: officialOid },
712
- branches: [{ name: 'main', oid: officialOid }],
713
- },
714
- },
715
- });
716
- } finally {
717
- transitions.updateBranchRefs = updateBranchRefs;
718
- }
719
-
720
- assert.deepEqual(result, { state: 'ready' });
721
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), localOid);
722
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), localOid);
723
- assert.equal(fs.readFileSync(path.join(repo, 'racing-local.txt'), 'utf8'), 'local commit\n');
724
- });
725
-
726
- test('index movement rejects an overlapping mode change made after the sealed tree', async () => {
727
- const { repo } = await makeRepo('index-mode-after-seal');
728
- const sourceTree = (await git(repo, 'write-tree')).trim();
729
- await git(repo, 'rm', '--cached', 'a.txt');
730
- const targetTree = (await git(repo, 'write-tree')).trim();
731
- await git(repo, 'reset', '--mixed', 'HEAD');
732
- await git(repo, 'update-index', '--chmod=+x', 'a.txt');
733
- assert.match(await git(repo, 'ls-files', '--stage', '--', 'a.txt'), /^100755 /);
734
-
735
- const result = await advanceIndex(
736
- repo,
737
- sourceTree,
738
- targetTree,
739
- '5'.repeat(64),
740
- );
741
-
742
- assert.deepEqual(result, {
743
- state: 'waiting',
744
- reason: 'local-index-moving',
745
- moved: false,
746
- });
747
- assert.match(
748
- await git(repo, 'ls-files', '--stage', '--', 'a.txt'),
749
- /^100755 /,
750
- 'a patch derived from the sealed regular-file tree cannot delete the later executable entry',
751
- );
752
- });
753
-
754
- test('ordinary index and HEAD lock contention remain bounded reducer waits', async () => {
755
- const indexRepo = await makeRepo('retryable-index-lock');
756
- const sourceTree = (await git(indexRepo.repo, 'write-tree')).trim();
757
- fs.writeFileSync(path.join(indexRepo.repo, 'a.txt'), 'target\n');
758
- await git(indexRepo.repo, 'add', 'a.txt');
759
- const targetTree = (await git(indexRepo.repo, 'write-tree')).trim();
760
- await git(indexRepo.repo, 'reset', '--mixed', 'HEAD');
761
- const indexFile = await gitPath(indexRepo.repo, 'index');
762
- fs.writeFileSync(`${indexFile}.lock`, 'user-owned lock\n');
763
- const indexResult = await advanceIndex(
764
- indexRepo.repo,
765
- sourceTree,
766
- targetTree,
767
- '8'.repeat(64),
768
- );
769
- assert.deepEqual(indexResult, {
770
- state: 'waiting',
771
- reason: 'local-index-moving',
772
- moved: false,
773
- });
774
- fs.rmSync(`${indexFile}.lock`);
775
-
776
- const headRepo = await makeRepo('retryable-head-lock');
777
- await git(headRepo.repo, 'branch', 'feature', headRepo.oid);
778
- const headFile = await gitPath(headRepo.repo, 'HEAD');
779
- fs.writeFileSync(`${headFile}.lock`, 'user-owned lock\n');
780
- const headResult = await applyHydrationBootstrap(headRepo.repo, {
781
- operationId: '9'.repeat(64),
782
- bootstrap: {
783
- source: {
784
- head: { branch: 'main', oid: headRepo.oid },
785
- branches: [
786
- { name: 'feature', oid: headRepo.oid },
787
- { name: 'main', oid: headRepo.oid },
788
- ],
789
- },
790
- target: {
791
- head: { branch: 'feature', oid: headRepo.oid },
792
- branches: [
793
- { name: 'feature', oid: headRepo.oid },
794
- { name: 'main', oid: headRepo.oid },
795
- ],
796
- },
797
- },
798
- });
799
- assert.deepEqual(headResult, { state: 'waiting', reason: 'local-head-moving' });
800
- fs.rmSync(`${headFile}.lock`);
801
- assert.equal(repairDelayForPass([indexResult, headResult]), 1000);
802
370
  });
803
371
 
804
372
  test('branch refs reduce per name; an absent stale list cannot delete another writer branch', () => {
@@ -876,13 +444,11 @@ test('one materializer converges to the reduced checkpoint and later removes a r
876
444
  const { repo, oid } = await makeRepo('materialize');
877
445
  const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
878
446
  const patchB = await patchFor(repo, { 'b.txt': 'b1\n' });
879
- const tracked = await establishTrackedBaseline(repo, oid, 'materialize');
880
- const { localCard, repoKey } = tracked;
447
+ const localCard = card(repo, oid, 1);
881
448
 
882
449
  const first = {
883
- repoKey,
450
+ repoKey: 'root:test',
884
451
  card: localCard,
885
- branches: tracked.baseline.branches,
886
452
  transitions: [
887
453
  transition('machine-a', 2, oid, '', patchA),
888
454
  transition('machine-b', 3, oid, patchA, `${patchA}${patchB}`),
@@ -890,15 +456,14 @@ test('one materializer converges to the reduced checkpoint and later removes a r
890
456
  };
891
457
  const applied = await materializeRepo(localCard, first, 3);
892
458
  assert.equal(applied.state, 'in-sync');
893
- assert.equal(readRepoState(repoKey, tracked.project.resourceId).appliedThrough, 3);
459
+ assert.equal(readRepoState('root:test').appliedThrough, 3);
894
460
  assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a1\n');
895
461
  assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
896
462
 
897
- const combined = readRepoState(repoKey, tracked.project.resourceId).indexDiff;
463
+ const combined = readRepoState('root:test').indexDiff;
898
464
  const second = {
899
- repoKey,
465
+ repoKey: 'root:test',
900
466
  card: localCard,
901
- branches: tracked.baseline.branches,
902
467
  transitions: [
903
468
  transition('machine-b', 3, oid, patchA, combined),
904
469
  transition('machine-a', 4, oid, combined, patchB),
@@ -906,65 +471,17 @@ test('one materializer converges to the reduced checkpoint and later removes a r
906
471
  };
907
472
  const reverted = await materializeRepo(localCard, second, 4);
908
473
  assert.equal(reverted.state, 'in-sync');
909
- assert.equal(readRepoState(repoKey, tracked.project.resourceId).appliedThrough, 4);
474
+ assert.equal(readRepoState('root:test').appliedThrough, 4);
910
475
  assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a0\n');
911
476
  assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
912
477
  });
913
478
 
914
- test('a linked worktree verifies its cursor and releases every apply receipt', async () => {
915
- const { repo, oid } = await makeRepo('linked-materialize-main');
916
- const linked = path.join(tempRoot, 'linked-materialize-worktree');
917
- await git(repo, 'worktree', 'add', '-b', 'linked-materialize', linked, oid);
918
- const officialPatch = await patchFor(linked, { 'a.txt': 'official linked\n' });
919
- const tracked = await establishTrackedBaseline(
920
- linked,
921
- oid,
922
- 'linked_materialize',
923
- );
924
- const plan = {
925
- ...tracked.baseline,
926
- transitions: [
927
- transition('machine-remote', 2, oid, '', officialPatch),
928
- ],
929
- };
930
-
931
- const outcome = await materializeRepo(tracked.localCard, plan, 2);
932
- assert.equal(outcome.state, 'in-sync', outcome.reason);
933
- assert.equal(fs.readFileSync(path.join(linked, 'a.txt'), 'utf8'), 'official linked\n');
934
- assert.equal(
935
- readRepoState(tracked.repoKey, tracked.project.resourceId).appliedThrough,
936
- 2,
937
- );
938
- assert.equal(
939
- fs.readdirSync(linked).some((name) => /^\.amalgm-apply-/.test(name)),
940
- false,
941
- );
942
- assert.doesNotMatch(
943
- await git(linked, 'ls-files', '--others', '--exclude-standard'),
944
- /\.amalgm-apply-/,
945
- );
946
- const metadataReceipts = path.join(
947
- path.dirname(await gitPath(linked, 'index')),
948
- 'amalgm-metadata-apply',
949
- );
950
- assert.equal(
951
- fs.existsSync(metadataReceipts)
952
- && fs.readdirSync(metadataReceipts).some((name) => name.endsWith('.json')),
953
- false,
954
- );
955
- assert.equal(
956
- (await git(linked, 'for-each-ref', '--format=%(refname)', 'refs/amalgm/apply')).trim(),
957
- '',
958
- );
959
- });
960
-
961
479
  test('a mode-only checkpoint makes the peer working file executable', async () => {
962
480
  const { repo, oid } = await makeRepo('materialize-mode');
963
481
  fs.chmodSync(path.join(repo, 'a.txt'), 0o755);
964
482
  const modePatch = await git(repo, 'diff', 'HEAD', '--binary', '--full-index');
965
483
  await git(repo, 'reset', '--hard', 'HEAD');
966
- const tracked = await establishTrackedBaseline(repo, oid, 'materialize_mode');
967
- const { localCard, repoKey } = tracked;
484
+ const localCard = card(repo, oid, 1);
968
485
  const clean = { revision: null, indexDiff: '', worktreeDiff: '' };
969
486
  const executable = {
970
487
  revision: revisionOf(oid, '', modePatch),
@@ -972,9 +489,8 @@ test('a mode-only checkpoint makes the peer working file executable', async () =
972
489
  worktreeDiff: modePatch,
973
490
  };
974
491
  const plan = {
975
- repoKey,
492
+ repoKey: 'root:test',
976
493
  card: localCard,
977
- branches: tracked.baseline.branches,
978
494
  transitions: [{
979
495
  checkpoint: CHECKPOINT_CONTRACT,
980
496
  machine: 'machine-remote',
@@ -995,16 +511,14 @@ test('a mode-only checkpoint makes the peer working file executable', async () =
995
511
  assert.equal(await git(repo, 'diff', '--cached', '--binary', '--full-index'), '');
996
512
  });
997
513
 
998
- test('once local work is on the rail, a later checkpoint replaces it exactly without echoing', async () => {
999
- const { repo, oid } = await makeRepo('materialize-later-wins');
514
+ test('once local work is on the rail, a checkpoint conflict materializes exactly without echoing', async () => {
515
+ const { repo, oid } = await makeRepo('materialize-conflict');
1000
516
  const localPatch = await patchFor(repo, { 'a.txt': 'local answer\n' });
1001
517
  const remotePatch = await patchFor(repo, { 'a.txt': 'remote answer\n' });
1002
518
  const localTransition = transition('machine-local', 2, oid, '', localPatch);
1003
- const tracked = await establishTrackedBaseline(repo, oid, 'materialize_later_wins');
1004
519
  const plan = {
1005
- repoKey: tracked.repoKey,
1006
- card: tracked.localCard,
1007
- branches: tracked.baseline.branches,
520
+ repoKey: 'root:test',
521
+ card: card(repo, oid, 1),
1008
522
  transitions: [
1009
523
  localTransition,
1010
524
  transition('machine-remote', 3, oid, '', remotePatch),
@@ -1013,24 +527,20 @@ test('once local work is on the rail, a later checkpoint replaces it exactly wit
1013
527
 
1014
528
  fs.writeFileSync(path.join(repo, 'a.txt'), 'local answer\n');
1015
529
  await git(repo, 'add', 'a.txt');
1016
- const localCard = await captureTrackedIntent(tracked.project, tracked.localCard);
1017
- const intents = require('../workspace/intent');
1018
- const reserved = intents.reservePublication(
1019
- tracked.project,
1020
- intents.readIntent(tracked.project),
1021
- );
1022
- intents.markPublicationFolded(
1023
- tracked.project,
1024
- reserved.publication.mutationId,
1025
- 2,
1026
- );
530
+ assert.equal(captureIsOnRail(plan, {
531
+ baseOid: oid,
532
+ indexDiff: localPatch,
533
+ worktreeDiff: '',
534
+ }), true);
1027
535
 
1028
- const outcome = await materializeRepo(localCard, plan, 3);
1029
- assert.equal(outcome.state, 'in-sync');
536
+ const outcome = await materializeRepo(plan.card, plan);
537
+ assert.equal(outcome.state, 'conflict');
1030
538
  const first = fs.readFileSync(path.join(repo, 'a.txt'), 'utf8');
1031
- assert.equal(first, 'remote answer\n');
539
+ assert.match(first, /local answer/);
540
+ assert.match(first, /remote answer/);
541
+ assert.match(first, /<<<<<<< current/);
1032
542
 
1033
- await materializeRepo(localCard, plan, 3);
543
+ await materializeRepo(plan.card, plan);
1034
544
  assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), first);
1035
545
  });
1036
546
 
@@ -1039,8 +549,9 @@ test('a writer publishing again cannot erase its earlier uncommitted work', asyn
1039
549
  const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
1040
550
  const patchAB = await patchFor(repo, { 'a.txt': 'a1\n', 'b.txt': 'b1\n' });
1041
551
  const patchABC = await patchFor(repo, {
1042
- 'a.txt': 'a2\n',
552
+ 'a.txt': 'a1\n',
1043
553
  'b.txt': 'b1\n',
554
+ 'c.txt': 'c1\n',
1044
555
  });
1045
556
  const machineA = card('/machine-a', oid, 1);
1046
557
  machineA.machine = 'machine-a';
@@ -1059,88 +570,126 @@ test('a writer publishing again cannot erase its earlier uncommitted work', asyn
1059
570
  { machine: 'machine-b', cards: [machineB] },
1060
571
  ]).get('root:test');
1061
572
  assert.deepEqual(plan.transitions.map((item) => item.version), [2, 3, 4]);
1062
- const tracked = await establishTrackedBaseline(repo, oid, 'checkpoint_history');
1063
- plan.repoKey = tracked.repoKey;
1064
- plan.card = tracked.localCard;
1065
- plan.branches = tracked.baseline.branches;
1066
573
 
1067
- const outcome = await materializeRepo(tracked.localCard, plan, 4);
574
+ const outcome = await materializeRepo(card(repo, oid, 1), plan);
1068
575
  assert.equal(outcome.state, 'in-sync');
1069
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a2\n');
576
+ assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a1\n');
1070
577
  assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
578
+ assert.equal(fs.readFileSync(path.join(repo, 'c.txt'), 'utf8'), 'c1\n');
1071
579
  });
1072
580
 
1073
- test('without verified provenance a later official card cannot overwrite unknown local bytes', async () => {
1074
- const rootCommit = 'unrelated-card-history';
1075
- const repoKey = `root:${rootCommit}`;
1076
- const { repo, oid: oldOid } = await makeRepo(rootCommit);
1077
- await git(repo, 'switch', '--orphan', 'winner');
1078
- fs.rmSync(path.join(repo, 'a.txt'), { force: true });
1079
- fs.rmSync(path.join(repo, 'b.txt'), { force: true });
1080
- fs.writeFileSync(path.join(repo, 'official.txt'), 'later official history\n');
1081
- await git(repo, 'add', '-A');
1082
- await git(repo, 'commit', '-m', 'unrelated official root');
1083
- const targetOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1084
- await git(repo, 'switch', 'main');
1085
- fs.writeFileSync(path.join(repo, 'local.txt'), 'pending local bytes\n');
581
+ test('a losing commit is parked, the winning card lands exactly, and losing work becomes uncommitted', async () => {
582
+ const { repo, oid: baseOid } = await makeRepo('simultaneous-commit');
583
+ fs.writeFileSync(path.join(repo, 'local-commit.txt'), 'losing commit work\n');
584
+ await git(repo, 'add', '.');
585
+ await git(repo, 'commit', '-m', 'local losing commit');
586
+ const losingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
587
+ fs.writeFileSync(path.join(repo, 'local-dirty.txt'), 'also uncommitted\n');
1086
588
 
1087
- const localCard = {
1088
- ...card(repo, oldOid, 1),
1089
- repo: { rootCommit, remote: null },
1090
- };
1091
- const targetCard = {
589
+ await git(repo, 'switch', '--detach', baseOid);
590
+ fs.writeFileSync(path.join(repo, 'winner.txt'), 'winning commit work\n');
591
+ await git(repo, 'add', '.');
592
+ await git(repo, 'commit', '-m', 'remote winning commit');
593
+ const winningOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
594
+ await git(repo, 'switch', '-C', 'main', losingOid);
595
+
596
+ const localCard = card(repo, losingOid, 1);
597
+ const winningCard = {
1092
598
  ...localCard,
1093
- head: { branch: 'main', oid: targetOid, detached: false },
599
+ head: { branch: 'main', oid: winningOid, detached: false },
1094
600
  positionVersion: 2,
1095
- positionMutationId: 'mut_unrelated_card',
601
+ positionMutationId: 'mut_card_winner_0001',
1096
602
  };
1097
603
  const outcome = await materializeRepo(localCard, {
1098
- repoKey,
1099
- card: targetCard,
604
+ repoKey: 'root:test',
605
+ card: winningCard,
1100
606
  transitions: [],
1101
- branches: [{ name: 'main', oid: targetOid, version: 2 }],
1102
- }, 2);
1103
-
1104
- assert.deepEqual(outcome, { state: 'waiting', reason: 'unknown-local-state' });
1105
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), oldOid);
1106
- assert.equal(fs.readFileSync(path.join(repo, 'local.txt'), 'utf8'), 'pending local bytes\n');
1107
- assert.equal(fs.existsSync(path.join(repo, 'official.txt')), false);
1108
- });
607
+ parked: [],
608
+ });
1109
609
 
1110
- test('missing provenance never authorizes replacing a clean unnumbered commit', async () => {
1111
- const { repo, oid } = await makeRepo('unknown-clean-commit');
1112
- const tracked = await establishTrackedBaseline(repo, oid, 'unknown_clean_commit');
1113
- fs.rmSync(reducerStateFile(tracked.repoKey), { force: true });
610
+ assert.equal(outcome.state, 'in-sync');
611
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
612
+ assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingOid}`)).trim(), losingOid);
613
+ assert.equal(fs.readFileSync(path.join(repo, 'winner.txt'), 'utf8'), 'winning commit work\n');
614
+ assert.equal(fs.readFileSync(path.join(repo, 'local-commit.txt'), 'utf8'), 'losing commit work\n');
615
+ assert.equal(fs.readFileSync(path.join(repo, 'local-dirty.txt'), 'utf8'), 'also uncommitted\n');
616
+ assert.equal((await git(repo, 'status', '--porcelain')).trim().length > 0, true);
617
+ assert.deepEqual(readRepoState('root:test').parked.map((entry) => entry.oid), [losingOid]);
618
+
619
+ const sharedParked = readRepoState('root:test').parked.map((entry) => ({
620
+ ...entry,
621
+ version: 2,
622
+ }));
623
+ await materializeRepo(card(repo, winningOid, 2), {
624
+ repoKey: 'root:test',
625
+ card: winningCard,
626
+ transitions: [],
627
+ parked: sharedParked,
628
+ });
1114
629
 
1115
- fs.writeFileSync(path.join(repo, 'local-commit.txt'), 'not on the rail\n');
1116
- await git(repo, 'add', 'local-commit.txt');
1117
- await git(repo, 'commit', '-m', 'local unnumbered commit');
1118
- const localOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1119
- const current = await observedCard(tracked.localCard);
630
+ await git(repo, 'add', '.');
631
+ await git(repo, 'commit', '-m', 'fold parked work');
632
+ const foldedOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
633
+ const foldedCard = card(repo, foldedOid, 3);
634
+ const folded = await materializeRepo(foldedCard, {
635
+ repoKey: 'root:test',
636
+ card: foldedCard,
637
+ transitions: [],
638
+ parked: [],
639
+ });
1120
640
 
1121
- assert.deepEqual(
1122
- await materializeRepo(current, tracked.baseline, 1),
1123
- { state: 'waiting', reason: 'unknown-local-state' },
641
+ assert.equal(folded.state, 'in-sync');
642
+ assert.deepEqual(readRepoState('root:test').parked, []);
643
+ await assert.rejects(
644
+ git(repo, 'rev-parse', '--verify', `refs/amalgm/parked/${losingOid}`),
1124
645
  );
1125
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), localOid);
1126
- assert.equal(fs.readFileSync(path.join(repo, 'local-commit.txt'), 'utf8'), 'not on the rail\n');
1127
646
  });
1128
647
 
1129
- test('missing provenance never authorizes replacing a branch-only local change', async () => {
1130
- const { repo, oid } = await makeRepo('unknown-local-branch');
1131
- const tracked = await establishTrackedBaseline(repo, oid, 'unknown_local_branch');
1132
- fs.rmSync(reducerStateFile(tracked.repoKey), { force: true });
648
+ test('rail-parked peer commits become one staged checkpoint on every machine', async () => {
649
+ const { repo, oid: baseOid } = await makeRepo('peer-parked-commits');
1133
650
 
1134
- await git(repo, 'branch', 'local-only');
1135
- const current = await observedCard(tracked.localCard);
651
+ await git(repo, 'switch', '-c', 'winner');
652
+ fs.writeFileSync(path.join(repo, 'winner.txt'), 'winning card\n');
653
+ await git(repo, 'add', '.');
654
+ await git(repo, 'commit', '-m', 'winning card');
655
+ const winningOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
656
+
657
+ await git(repo, 'switch', '-C', 'loser-a', baseOid);
658
+ fs.writeFileSync(path.join(repo, 'loser-a.txt'), 'first losing commit\n');
659
+ await git(repo, 'add', '.');
660
+ await git(repo, 'commit', '-m', 'first losing commit');
661
+ const losingAOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
662
+
663
+ await git(repo, 'switch', '-C', 'loser-b', baseOid);
664
+ fs.writeFileSync(path.join(repo, 'loser-b.txt'), 'second losing commit\n');
665
+ await git(repo, 'add', '.');
666
+ await git(repo, 'commit', '-m', 'second losing commit');
667
+ const losingBOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
668
+
669
+ await git(repo, 'switch', '-C', 'main', winningOid);
670
+ const winningCard = card(repo, winningOid, 4);
671
+ const outcome = await materializeRepo(winningCard, {
672
+ repoKey: 'root:test',
673
+ card: winningCard,
674
+ transitions: [],
675
+ parked: [
676
+ { oid: losingAOid, version: 2 },
677
+ { oid: losingBOid, version: 3 },
678
+ ],
679
+ });
680
+
681
+ assert.equal(outcome.state, 'in-sync');
682
+ assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
1136
683
  assert.deepEqual(
1137
- await materializeRepo(current, tracked.baseline, 1),
1138
- { state: 'waiting', reason: 'unknown-local-state' },
684
+ (await git(repo, 'diff', '--cached', '--name-only')).trim().split('\n'),
685
+ ['loser-a.txt', 'loser-b.txt'],
1139
686
  );
1140
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/local-only')).trim(), oid);
687
+ assert.equal((await git(repo, 'diff', '--name-only')).trim(), '');
688
+ assert.equal(fs.readFileSync(path.join(repo, 'loser-a.txt'), 'utf8'), 'first losing commit\n');
689
+ assert.equal(fs.readFileSync(path.join(repo, 'loser-b.txt'), 'utf8'), 'second losing commit\n');
1141
690
  });
1142
691
 
1143
- test('a local commit made after the verified card snapshot is never overwritten', async () => {
692
+ test('a local commit made after the verified card snapshot is never parked or overwritten', async () => {
1144
693
  const { repo, oid: baseOid } = await makeRepo('card-snapshot-race');
1145
694
  const verifiedCard = card(repo, baseOid, 1);
1146
695
 
@@ -1165,321 +714,30 @@ test('a local commit made after the verified card snapshot is never overwritten'
1165
714
  positionMutationId: 'mut_card_snapshot_race',
1166
715
  },
1167
716
  transitions: [],
717
+ parked: [],
1168
718
  });
1169
719
 
1170
720
  assert.equal(outcome.state, 'waiting');
1171
- assert.equal(outcome.reason, 'unknown-local-state');
721
+ assert.equal(outcome.reason, 'local-card-changed-after-verification');
1172
722
  assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), lateLocalOid);
723
+ await assert.rejects(
724
+ git(repo, 'rev-parse', `refs/amalgm/parked/${lateLocalOid}`),
725
+ );
1173
726
  });
1174
727
 
1175
- test('a local file write racing ordered apply waits, retries, and remains pending', async () => {
1176
- const { repo, oid } = await makeRepo('checkpoint-local-writer-race');
1177
- const tracked = await establishTrackedBaseline(repo, oid, 'checkpoint_local_writer_race');
1178
- const { localCard, baseline } = tracked;
1179
-
1180
- const inbound = await patchFor(repo, { 'a.txt': 'official inbound\n' });
1181
- const plan = {
1182
- ...baseline,
1183
- transitions: [transition('machine-remote', 2, oid, '', inbound)],
1184
- };
1185
- const transitions = require('../workspace/transition');
1186
- const applyFiles = transitions.applyFiles;
1187
- let raced = false;
1188
- transitions.applyFiles = async (...args) => {
1189
- if (!raced) {
1190
- raced = true;
1191
- fs.writeFileSync(path.join(repo, 'a.txt'), 'pending local writer\n');
1192
- }
1193
- return applyFiles(...args);
1194
- };
1195
- let first;
1196
- try {
1197
- first = await materializeRepo(localCard, plan, 2);
1198
- } finally {
1199
- transitions.applyFiles = applyFiles;
1200
- }
1201
-
1202
- assert.deepEqual(first, { state: 'waiting', reason: 'local-state-moving' });
1203
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'pending local writer\n');
1204
- assert.equal(readRepoState(tracked.repoKey, tracked.project.resourceId).appliedThrough, 1);
1205
-
1206
- const recovered = await materializeRepo(localCard, plan, 2);
1207
- assert.equal(recovered.state, 'in-sync', JSON.stringify(recovered));
1208
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'pending local writer\n');
1209
- const state = readRepoState(tracked.repoKey, tracked.project.resourceId);
1210
- assert.equal(state.appliedThrough, 2);
1211
- assert.equal(Object.hasOwn(state, 'pendingCheckpoint'), false);
1212
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
1213
- });
1214
-
1215
- test('restart resumes a card apply before advancing and preserves downtime writes', async () => {
1216
- const { repo, oid: baseOid } = await makeRepo('crash-safe-card-apply');
1217
- await git(repo, 'switch', '-c', 'incoming');
1218
- fs.writeFileSync(path.join(repo, 'remote.txt'), 'official target\n');
1219
- await git(repo, 'add', 'remote.txt');
1220
- await git(repo, 'commit', '-m', 'official target');
1221
- const targetOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1222
- await git(repo, 'switch', '-C', 'main', baseOid);
1223
- const tracked = await establishTrackedBaseline(repo, baseOid, 'crash_safe_card_apply');
1224
-
1225
- fs.writeFileSync(path.join(repo, 'a.txt'), 'staged before crash\n');
1226
- await git(repo, 'add', 'a.txt');
1227
- fs.writeFileSync(path.join(repo, 'b.txt'), 'working before crash\n');
1228
- fs.writeFileSync(path.join(repo, 'local.txt'), 'untracked before crash\n');
1229
-
1230
- const repoKey = tracked.repoKey;
1231
- const localCard = {
1232
- ...await captureTrackedIntent(tracked.project, tracked.localCard),
1233
- repo: { rootCommit: 'crash-safe-card-apply', remote: null },
1234
- };
1235
- const targetCard = {
1236
- ...localCard,
1237
- head: { branch: 'main', oid: targetOid, detached: false },
1238
- positionVersion: 2,
1239
- positionMutationId: 'mut_crash_safe_card',
1240
- };
1241
- const plan = {
1242
- repoKey,
1243
- card: targetCard,
1244
- transitions: [],
1245
- branches: [{ name: 'main', oid: targetOid, version: 2 }],
1246
- };
1247
-
1248
- const transitions = require('../workspace/transition');
1249
- const updateBranchRefs = transitions.updateBranchRefs;
1250
- transitions.updateBranchRefs = async (...args) => {
1251
- await updateBranchRefs(...args);
1252
- throw new Error('simulated process death after HEAD movement');
1253
- };
1254
- try {
1255
- await assert.rejects(
1256
- materializeRepo(localCard, plan, 2),
1257
- /simulated process death/,
1258
- );
1259
- } finally {
1260
- transitions.updateBranchRefs = updateBranchRefs;
1261
- }
1262
-
1263
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), targetOid);
1264
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'staged before crash\n');
1265
- assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'working before crash\n');
1266
- assert.equal(fs.readFileSync(path.join(repo, 'local.txt'), 'utf8'), 'untracked before crash\n');
1267
- assert.equal(fs.existsSync(path.join(repo, 'remote.txt')), false);
1268
- assert.equal(readRepoState(repoKey, tracked.project.resourceId).appliedThrough, 1);
1269
- assert.equal(readRepoState(repoKey, tracked.project.resourceId).applying.contract, 'repo-apply@4');
1270
-
1271
- fs.writeFileSync(path.join(repo, 'b.txt'), 'written while reducer was down\n');
1272
- fs.writeFileSync(path.join(repo, 'downtime.txt'), 'new while reducer was down\n');
1273
-
1274
- const recovered = await materializeRepo({
1275
- ...localCard,
1276
- head: { branch: 'main', oid: targetOid, detached: false },
1277
- positionVersion: 2,
1278
- repo: { rootCommit: 'crash-safe-card-apply', remote: null },
1279
- }, plan, 2);
1280
- assert.equal(recovered.state, 'in-sync', JSON.stringify(recovered));
1281
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), targetOid);
1282
- assert.equal(fs.readFileSync(path.join(repo, 'remote.txt'), 'utf8'), 'official target\n');
1283
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'staged before crash\n');
1284
- assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'written while reducer was down\n');
1285
- assert.equal(fs.readFileSync(path.join(repo, 'local.txt'), 'utf8'), 'untracked before crash\n');
1286
- assert.equal(fs.readFileSync(path.join(repo, 'downtime.txt'), 'utf8'), 'new while reducer was down\n');
1287
- assert.match(await git(repo, 'diff', '--cached', '--name-only'), /a\.txt/);
1288
- assert.equal(readRepoState(repoKey, tracked.project.resourceId).appliedThrough, 2);
1289
- assert.equal(readRepoState(repoKey, tracked.project.resourceId).applying, undefined);
1290
- });
1291
-
1292
- test('card apply replay is idempotent after file and index boundaries', async () => {
1293
- for (const boundary of ['files', 'index']) {
1294
- const rootCommit = `card-boundary-${boundary}`;
1295
- const repoKey = `root:${rootCommit}`;
1296
- const { repo, oid: baseOid } = await makeRepo(rootCommit);
1297
- await git(repo, 'switch', '-c', 'incoming');
1298
- fs.writeFileSync(path.join(repo, 'remote.txt'), `official ${boundary}\n`);
1299
- await git(repo, 'add', 'remote.txt');
1300
- await git(repo, 'commit', '-m', `official ${boundary}`);
1301
- const targetOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1302
- await git(repo, 'switch', '-C', 'main', baseOid);
1303
- const tracked = await establishTrackedBaseline(repo, baseOid, `card_boundary_${boundary}`);
1304
- fs.writeFileSync(path.join(repo, 'a.txt'), `staged ${boundary}\n`);
1305
- await git(repo, 'add', 'a.txt');
1306
- fs.writeFileSync(path.join(repo, 'b.txt'), `working ${boundary}\n`);
1307
-
1308
- const localCard = {
1309
- ...await captureTrackedIntent(tracked.project, tracked.localCard),
1310
- repo: { rootCommit, remote: null },
1311
- };
1312
- const targetCard = {
1313
- ...localCard,
1314
- head: { branch: 'main', oid: targetOid, detached: false },
1315
- positionVersion: 2,
1316
- positionMutationId: `mut_card_boundary_${boundary}`,
1317
- };
1318
- const plan = {
1319
- repoKey: tracked.repoKey,
1320
- card: targetCard,
1321
- transitions: [],
1322
- branches: [{ name: 'main', oid: targetOid, version: 2 }],
1323
- };
1324
-
1325
- const transitions = require('../workspace/transition');
1326
- const mergeModule = require('../workspace/merge');
1327
- const original = boundary === 'files'
1328
- ? mergeModule.advance
1329
- : transitions.applyIndexPatch;
1330
- let tripped = false;
1331
- if (boundary === 'files') {
1332
- mergeModule.advance = async (...args) => {
1333
- const result = await original(...args);
1334
- if (!tripped && /^[0-9a-f]{64}$/.test(args[3]?.label || '')) {
1335
- tripped = true;
1336
- throw new Error('simulated process death after files');
1337
- }
1338
- return result;
1339
- };
1340
- } else {
1341
- transitions.applyIndexPatch = async (...args) => {
1342
- const result = await original(...args);
1343
- if (!tripped) {
1344
- tripped = true;
1345
- throw new Error('simulated process death after index');
1346
- }
1347
- return result;
1348
- };
1349
- }
1350
- try {
1351
- await assert.rejects(
1352
- materializeRepo(localCard, plan, 2),
1353
- /simulated process death/,
1354
- );
1355
- } finally {
1356
- if (boundary === 'files') mergeModule.advance = original;
1357
- else transitions.applyIndexPatch = original;
1358
- }
1359
-
1360
- assert.equal(
1361
- readRepoState(tracked.repoKey, tracked.project.resourceId).appliedThrough,
1362
- 1,
1363
- );
1364
- fs.writeFileSync(path.join(repo, 'b.txt'), `downtime ${boundary}\n`);
1365
- const recovered = await materializeRepo({
1366
- ...localCard,
1367
- head: { branch: 'main', oid: targetOid, detached: false },
1368
- positionVersion: 2,
1369
- repo: { rootCommit, remote: null },
1370
- }, plan, 2);
1371
- assert.equal(recovered.state, 'in-sync');
1372
- assert.equal(fs.readFileSync(path.join(repo, 'remote.txt'), 'utf8'), `official ${boundary}\n`);
1373
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), `staged ${boundary}\n`);
1374
- assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), `downtime ${boundary}\n`);
1375
- assert.equal(readRepoState(tracked.repoKey, tracked.project.resourceId).appliedThrough, 2);
1376
- assert.equal(readRepoState(tracked.repoKey, tracked.project.resourceId).applying, undefined);
1377
- }
1378
- });
1379
-
1380
- test('a downtime commit stays pending while recovery finishes N before newer N+1', async () => {
1381
- const rootCommit = 'card-recovery-newer-rail';
1382
- const repoKey = `root:${rootCommit}`;
1383
- const { repo, oid: baseOid } = await makeRepo(rootCommit);
1384
- await git(repo, 'switch', '-c', 'incoming');
1385
- fs.writeFileSync(path.join(repo, 'official-n.txt'), 'official N\n');
1386
- await git(repo, 'add', 'official-n.txt');
1387
- await git(repo, 'commit', '-m', 'official N');
1388
- const targetN = (await git(repo, 'rev-parse', 'HEAD')).trim();
1389
- fs.writeFileSync(path.join(repo, 'official-n-plus-one.txt'), 'official N+1\n');
1390
- await git(repo, 'add', 'official-n-plus-one.txt');
1391
- await git(repo, 'commit', '-m', 'official N+1');
1392
- const targetN1 = (await git(repo, 'rev-parse', 'HEAD')).trim();
1393
- await git(repo, 'switch', '-C', 'main', baseOid);
1394
- const tracked = await establishTrackedBaseline(repo, baseOid, 'card_recovery_newer_rail');
1395
- fs.writeFileSync(path.join(repo, 'before-crash.txt'), 'local before crash\n');
1396
-
1397
- const localCard = {
1398
- ...await captureTrackedIntent(tracked.project, tracked.localCard),
1399
- repo: { rootCommit, remote: null },
1400
- };
1401
- const cardN = {
1402
- ...localCard,
1403
- head: { branch: 'main', oid: targetN, detached: false },
1404
- positionVersion: 2,
1405
- positionMutationId: 'mut_card_recovery_n',
1406
- };
1407
- const planN = {
1408
- repoKey: tracked.repoKey,
1409
- card: cardN,
1410
- transitions: [],
1411
- branches: [{ name: 'main', oid: targetN, version: 2 }],
1412
- };
1413
- const transitions = require('../workspace/transition');
1414
- const updateBranchRefs = transitions.updateBranchRefs;
1415
- transitions.updateBranchRefs = async (...args) => {
1416
- await updateBranchRefs(...args);
1417
- throw new Error('simulated process death before N finished');
1418
- };
1419
- try {
1420
- await assert.rejects(
1421
- materializeRepo(localCard, planN, 2),
1422
- /simulated process death/,
1423
- );
1424
- } finally {
1425
- transitions.updateBranchRefs = updateBranchRefs;
1426
- }
1427
-
1428
- fs.writeFileSync(path.join(repo, 'downtime-commit.txt'), 'committed while down\n');
1429
- await git(repo, 'add', '.');
1430
- await git(repo, 'commit', '-m', 'local commit while reducer is down');
1431
- const pendingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1432
-
1433
- const cardN1 = {
1434
- ...cardN,
1435
- head: { branch: 'main', oid: targetN1, detached: false },
1436
- positionVersion: 3,
1437
- positionMutationId: 'mut_card_recovery_n_plus_one',
1438
- };
1439
- const latest = {
1440
- repoKey: tracked.repoKey,
1441
- card: cardN1,
1442
- transitions: [],
1443
- branches: [{ name: 'main', oid: targetN1, version: 3 }],
1444
- };
1445
- const recovered = await materializeRepo({
1446
- ...localCard,
1447
- head: { branch: 'main', oid: pendingOid, detached: false },
1448
- positionVersion: null,
1449
- positionMutationId: null,
1450
- }, latest, 3);
1451
-
1452
- assert.equal(recovered.state, 'in-sync');
1453
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), pendingOid);
1454
- assert.equal(fs.readFileSync(path.join(repo, 'downtime-commit.txt'), 'utf8'), 'committed while down\n');
1455
- assert.equal(fs.readFileSync(path.join(repo, 'before-crash.txt'), 'utf8'), 'local before crash\n');
1456
- assert.equal(
1457
- fs.readFileSync(path.join(repo, 'official-n-plus-one.txt'), 'utf8'),
1458
- 'official N+1\n',
1459
- );
1460
- const state = readRepoState(tracked.repoKey, tracked.project.resourceId);
1461
- assert.equal(state.appliedThrough, 3);
1462
- assert.equal(Object.hasOwn(state, 'pendingCard'), false);
1463
- assert.equal(
1464
- require('../workspace/intent').readOwnedIntent(tracked.project).local.card.head.oid,
1465
- pendingOid,
1466
- );
1467
- assert.equal(state.applying, undefined);
1468
- });
1469
-
1470
- test('a pending descendant commit catches up without becoming a losing commit', async () => {
1471
- const { repo, oid: baseOid } = await makeRepo('pending-descendant-card');
1472
- const firstPatch = await patchFor(repo, { 'a.txt': 'before commit\n' });
1473
- const secondPatch = await patchFor(repo, {
1474
- 'a.txt': 'before commit\n',
1475
- 'b.txt': 'after commit\n',
1476
- });
1477
- const tracked = await establishTrackedBaseline(repo, baseOid, 'pending_descendant_card');
1478
- const winningCard = tracked.localCard;
1479
- const before = {
1480
- repoKey: tracked.repoKey,
1481
- card: winningCard,
1482
- transitions: [transition('machine-peer', 2, baseOid, '', firstPatch)],
728
+ test('a pending descendant commit catches up without becoming a losing commit', async () => {
729
+ const { repo, oid: baseOid } = await makeRepo('pending-descendant-card');
730
+ const firstPatch = await patchFor(repo, { 'a.txt': 'before commit\n' });
731
+ const secondPatch = await patchFor(repo, {
732
+ 'a.txt': 'before commit\n',
733
+ 'b.txt': 'after commit\n',
734
+ });
735
+ const winningCard = card(repo, baseOid, 1);
736
+ const before = {
737
+ repoKey: 'root:test',
738
+ card: winningCard,
739
+ transitions: [transition('machine-peer', 2, baseOid, '', firstPatch)],
740
+ parked: [],
1483
741
  };
1484
742
 
1485
743
  await materializeRepo(winningCard, before, 2);
@@ -1487,7 +745,7 @@ test('a pending descendant commit catches up without becoming a losing commit',
1487
745
  await git(repo, 'commit', '-m', 'local pending card');
1488
746
  const pendingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1489
747
 
1490
- const pendingCard = await captureTrackedIntent(tracked.project, winningCard);
748
+ const pendingCard = card(repo, pendingOid, null);
1491
749
  const caughtUp = await materializeRepo(pendingCard, {
1492
750
  ...before,
1493
751
  transitions: [
@@ -1503,55 +761,13 @@ test('a pending descendant commit catches up without becoming a losing commit',
1503
761
  const staged = (await git(repo, 'diff', '--cached', '--name-only')).trim();
1504
762
  const status = (await git(repo, 'status', '--short')).trim();
1505
763
  assert.equal(staged, 'b.txt', status);
1506
- const state = readRepoState(tracked.repoKey, tracked.project.resourceId);
1507
- assert.equal(state.baseOid, baseOid);
1508
- assert.equal(state.appliedThrough, 3);
1509
- assert.equal(
1510
- require('../workspace/intent').readOwnedIntent(tracked.project).local.card.head.oid,
1511
- pendingOid,
764
+ await assert.rejects(
765
+ git(repo, 'rev-parse', `refs/amalgm/parked/${pendingOid}`),
1512
766
  );
1513
- });
1514
-
1515
- test('a pending divergent commit is local intent, not a losing numbered card', async () => {
1516
- const { repo, oid: baseOid } = await makeRepo('pending-divergent-card');
1517
- const tracked = await establishTrackedBaseline(repo, baseOid, 'pending_divergent_card');
1518
- const { repoKey, localCard: baseCard } = tracked;
1519
-
1520
- await git(repo, 'switch', '-c', 'peer', baseOid);
1521
- fs.writeFileSync(path.join(repo, 'peer.txt'), 'peer\n');
1522
- await git(repo, 'add', '.');
1523
- await git(repo, 'commit', '-m', 'peer card');
1524
- const peerOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1525
-
1526
- await git(repo, 'switch', '-C', 'main', baseOid);
1527
- fs.writeFileSync(path.join(repo, 'local.txt'), 'local\n');
1528
- await git(repo, 'add', '.');
1529
- await git(repo, 'commit', '-m', 'local card while behind');
1530
- const localOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1531
- const localCard = await captureTrackedIntent(tracked.project, baseCard);
1532
- const birthMutationId = require('../workspace/intent')
1533
- .readOwnedIntent(tracked.project).birthMutationId;
1534
- const outcome = await materializeRepo(localCard, {
1535
- repoKey,
1536
- card: {
1537
- ...baseCard,
1538
- head: { branch: 'main', oid: peerOid, detached: false },
1539
- positionVersion: 2,
1540
- positionMutationId: 'mut_peer_card_0001',
1541
- },
1542
- transitions: [],
1543
- branches: [{ name: 'main', oid: peerOid, version: 2 }],
1544
- }, 2);
1545
-
1546
- assert.equal(outcome.state, 'in-sync');
1547
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), localOid);
1548
- assert.equal(fs.readFileSync(path.join(repo, 'local.txt'), 'utf8'), 'local\n');
1549
- assert.equal(fs.readFileSync(path.join(repo, 'peer.txt'), 'utf8'), 'peer\n');
1550
- const pending = require('../workspace/intent').readOwnedIntent(tracked.project);
1551
- assert.equal(pending.local.card.head.branch, 'main');
1552
- assert.equal(pending.local.card.head.oid, localOid);
1553
- assert.equal(pending.base.through, 2);
1554
- assert.equal(pending.birthMutationId, birthMutationId);
767
+ const state = readRepoState('root:test');
768
+ assert.equal(state.baseOid, pendingOid);
769
+ assert.equal(state.appliedThrough, 3);
770
+ assert.equal(state.cardVersion, null);
1555
771
  });
1556
772
 
1557
773
  test('an empty index advance cannot overwrite a commit that just finished', async () => {
@@ -1569,27 +785,22 @@ test('an empty index advance cannot overwrite a commit that just finished', asyn
1569
785
  assert.equal((await git(repo, 'status', '--porcelain')).trim(), '');
1570
786
  });
1571
787
 
1572
- test('an index advance stops before touching unrelated work staged after its seal', async () => {
788
+ test('an index advance preserves unrelated work staged by another writer', async () => {
1573
789
  const { repo } = await makeRepo('index-writers');
1574
- const operation = '8'.repeat(64);
1575
790
  const observedIndex = (await git(repo, 'write-tree')).trim();
1576
791
  const peerPatch = await patchFor(repo, { 'b.txt': 'peer\n' });
1577
792
  const peerTree = await require('../workspace/merge').treeOfPatch(repo, 'HEAD', peerPatch);
1578
793
 
1579
794
  fs.writeFileSync(path.join(repo, 'a.txt'), 'local\n');
1580
795
  await git(repo, 'add', 'a.txt');
1581
- const outcome = await advanceIndex(repo, observedIndex, peerTree, operation);
796
+ const outcome = await advanceIndex(repo, observedIndex, peerTree);
1582
797
 
1583
- assert.deepEqual(outcome, {
1584
- state: 'waiting',
1585
- reason: 'local-index-moving',
1586
- moved: false,
1587
- });
798
+ assert.deepEqual(outcome, { state: 'ready', moved: true });
1588
799
  const staged = await git(repo, 'diff', '--cached', '--name-only');
1589
- assert.deepEqual(staged.trim().split('\n'), ['a.txt']);
800
+ assert.deepEqual(staged.trim().split('\n').sort(), ['a.txt', 'b.txt']);
1590
801
  });
1591
802
 
1592
- test('a new card includes checkpoint work already on the rail instead of replaying it as local work', async () => {
803
+ test('a new card folds checkpoint work already on the rail instead of replaying it as local work', async () => {
1593
804
  const { repo, oid: baseOid } = await makeRepo('card-folds-shared-checkpoint');
1594
805
  const sharedPatch = await patchFor(repo, { 'a.txt': 'shared checkpoint\n' });
1595
806
 
@@ -1600,18 +811,16 @@ test('a new card includes checkpoint work already on the rail instead of replayi
1600
811
  const targetOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1601
812
  await git(repo, 'switch', 'main');
1602
813
 
1603
- const tracked = await establishTrackedBaseline(repo, baseOid, 'card_folds_shared_checkpoint');
1604
- const oldCard = tracked.localCard;
814
+ const oldCard = card(repo, baseOid, 1);
1605
815
  await materializeRepo(oldCard, {
1606
- repoKey: tracked.repoKey,
816
+ repoKey: 'root:test',
1607
817
  card: oldCard,
1608
818
  transitions: [transition('machine-remote', 2, baseOid, '', sharedPatch)],
1609
- branches: tracked.baseline.branches,
1610
- }, 2);
819
+ });
1611
820
  assert.notEqual((await git(repo, 'status', '--porcelain')).trim(), '');
1612
821
 
1613
822
  const outcome = await materializeRepo(oldCard, {
1614
- repoKey: tracked.repoKey,
823
+ repoKey: 'root:test',
1615
824
  card: {
1616
825
  ...oldCard,
1617
826
  head: { branch: 'main', oid: targetOid, detached: false },
@@ -1619,14 +828,14 @@ test('a new card includes checkpoint work already on the rail instead of replayi
1619
828
  positionMutationId: 'mut_card_shared_fold',
1620
829
  },
1621
830
  transitions: [],
1622
- branches: [{ name: 'main', oid: targetOid, version: 3 }],
1623
- }, 3);
1624
- assert.equal(outcome.state, 'in-sync', outcome.reason);
831
+ parked: [],
832
+ });
833
+ assert.equal(outcome.state, 'in-sync');
1625
834
  assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), targetOid);
1626
835
  assert.equal((await git(repo, 'status', '--porcelain')).trim(), '');
1627
836
  });
1628
837
 
1629
- test('checkpoint then commit advances through the same card movement and unrelated local work stays', async () => {
838
+ test('checkpoint then commit folds through the same card movement and unrelated local work stays', async () => {
1630
839
  const { repo, oid: baseOid } = await makeRepo('checkpoint-commit-fold');
1631
840
  await git(repo, 'switch', '-c', 'target');
1632
841
  fs.writeFileSync(path.join(repo, 'shared.txt'), 'already checkpointed\n');
@@ -1634,13 +843,12 @@ test('checkpoint then commit advances through the same card movement and unrelat
1634
843
  await git(repo, 'commit', '-m', 'commit checkpointed work');
1635
844
  const targetOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1636
845
  await git(repo, 'switch', '-C', 'main', baseOid);
1637
- const tracked = await establishTrackedBaseline(repo, baseOid, 'checkpoint_commit_fold');
1638
846
 
1639
847
  fs.writeFileSync(path.join(repo, 'shared.txt'), 'already checkpointed\n');
1640
848
  fs.writeFileSync(path.join(repo, 'local-only.txt'), 'keep me dirty\n');
1641
- const localCard = await captureTrackedIntent(tracked.project, tracked.localCard);
849
+ const localCard = card(repo, baseOid, 1);
1642
850
  const outcome = await materializeRepo(localCard, {
1643
- repoKey: tracked.repoKey,
851
+ repoKey: 'root:test',
1644
852
  card: {
1645
853
  ...localCard,
1646
854
  head: { branch: 'main', oid: targetOid, detached: false },
@@ -1648,10 +856,11 @@ test('checkpoint then commit advances through the same card movement and unrelat
1648
856
  positionMutationId: 'mut_card_fold_00001',
1649
857
  },
1650
858
  transitions: [],
859
+ parked: [],
1651
860
  branches: [{ name: 'main', oid: targetOid, version: 2 }],
1652
861
  });
1653
862
 
1654
- assert.equal(outcome.state, 'in-sync', outcome.reason);
863
+ assert.equal(outcome.state, 'in-sync');
1655
864
  assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), targetOid);
1656
865
  assert.equal(fs.readFileSync(path.join(repo, 'shared.txt'), 'utf8'), 'already checkpointed\n');
1657
866
  assert.equal(fs.readFileSync(path.join(repo, 'local-only.txt'), 'utf8'), 'keep me dirty\n');
@@ -1668,18 +877,18 @@ test('the materializer creates, moves, and deletes branch refs without checking
1668
877
  await git(repo, 'switch', 'main');
1669
878
  await git(repo, 'branch', '-D', 'feature');
1670
879
 
1671
- const tracked = await establishTrackedBaseline(repo, oid, 'branches');
1672
- const { localCard, repoKey } = tracked;
880
+ const localCard = card(repo, oid, 1);
1673
881
  const createdPlan = {
1674
- repoKey,
882
+ repoKey: 'root:test',
1675
883
  card: localCard,
1676
884
  transitions: [],
885
+ parked: [],
1677
886
  branches: [
1678
887
  { name: 'main', oid, version: 1 },
1679
888
  { name: 'feature', oid: featureOid, version: 2 },
1680
889
  ],
1681
890
  };
1682
- await materializeRepo(localCard, createdPlan, 2);
891
+ await materializeRepo(localCard, createdPlan);
1683
892
  assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureOid);
1684
893
  assert.equal((await git(repo, 'branch', '--show-current')).trim(), 'main');
1685
894
 
@@ -1689,81 +898,11 @@ test('the materializer creates, moves, and deletes branch refs without checking
1689
898
  { name: 'main', oid, version: 1 },
1690
899
  { name: 'feature', oid: null, version: 3 },
1691
900
  ],
1692
- }, 3);
901
+ });
1693
902
  await assert.rejects(() => git(repo, 'rev-parse', 'refs/heads/feature'));
1694
903
  assert.equal((await git(repo, 'branch', '--show-current')).trim(), 'main');
1695
904
  });
1696
905
 
1697
- test('an official branch without a complete object source is terminal corruption', async () => {
1698
- const { repo, oid } = await makeRepo('branch-event-waits');
1699
- const peer = path.join(tempRoot, 'branch-event-waits-peer');
1700
- await git(tempRoot, 'clone', '--quiet', repo, peer);
1701
- await git(peer, 'switch', '-c', 'feature');
1702
- fs.writeFileSync(path.join(peer, 'feature.txt'), 'feature\n');
1703
- await git(peer, 'add', '.');
1704
- await git(peer, 'commit', '-m', 'feature');
1705
- const featureOid = (await git(peer, 'rev-parse', 'HEAD')).trim();
1706
-
1707
- const tracked = await establishTrackedBaseline(repo, oid, 'branch_event_waits');
1708
- const { repoKey, localCard } = tracked;
1709
- const plan = {
1710
- repoKey,
1711
- card: localCard,
1712
- transitions: [],
1713
- branches: [
1714
- { name: 'main', oid, version: 1 },
1715
- { name: 'feature', oid: featureOid, version: 2 },
1716
- ],
1717
- };
1718
-
1719
- await assert.rejects(
1720
- materializeRepo(localCard, plan, 2),
1721
- /official branch feature has no complete bundle/,
1722
- );
1723
- assert.equal(readRepoState(repoKey).appliedThrough, 1);
1724
- await assert.rejects(() => git(repo, 'rev-parse', 'refs/heads/feature'));
1725
-
1726
- await git(repo, 'fetch', '--quiet', peer, 'feature:refs/amalgm-test/feature');
1727
- const applied = await materializeRepo(localCard, plan, 2);
1728
- assert.equal(applied.state, 'in-sync');
1729
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureOid);
1730
- assert.equal(readRepoState(repoKey).appliedThrough, 2);
1731
- });
1732
-
1733
- test('a malformed earlier checkpoint prevents a later ready branch from applying', async () => {
1734
- const { repo, oid } = await makeRepo('checkpoint-before-branch');
1735
- await git(repo, 'switch', '-c', 'feature');
1736
- fs.writeFileSync(path.join(repo, 'feature.txt'), 'feature\n');
1737
- await git(repo, 'add', '.');
1738
- await git(repo, 'commit', '-m', 'feature');
1739
- const featureOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1740
- await git(repo, 'switch', 'main');
1741
- await git(repo, 'branch', '-D', 'feature');
1742
-
1743
- const missing = transition('machine-peer', 2, oid, '', '');
1744
- missing.from = {
1745
- revision: 'a'.repeat(64),
1746
- payload: { kind: 'pending', contract: 'opaque-chunks@2', checksum: 'b'.repeat(64) },
1747
- };
1748
- missing.to = missing.from;
1749
- const repoKey = 'root:checkpoint-before-branch';
1750
- const localCard = card(repo, oid, 1);
1751
- await assert.rejects(
1752
- materializeRepo(localCard, {
1753
- repoKey,
1754
- card: localCard,
1755
- transitions: [missing],
1756
- branches: [
1757
- { name: 'main', oid, version: 1 },
1758
- { name: 'feature', oid: featureOid, version: 3 },
1759
- ],
1760
- }, 3),
1761
- /official checkpoint is payload-pending/,
1762
- );
1763
- await assert.rejects(() => git(repo, 'rev-parse', 'refs/heads/feature'));
1764
- assert.equal(readRepoState(repoKey), null);
1765
- });
1766
-
1767
906
  test('an unpublished local branch delete is not restored while catching up', async () => {
1768
907
  const { repo, oid } = await makeRepo('pending-branch-delete');
1769
908
  await git(repo, 'switch', '-c', 'feature');
@@ -1774,162 +913,27 @@ test('an unpublished local branch delete is not restored while catching up', asy
1774
913
  await git(repo, 'switch', 'main');
1775
914
  await git(repo, 'branch', '-D', 'feature');
1776
915
 
1777
- const tracked = await establishTrackedBaseline(repo, oid, 'pending_branch_delete');
1778
- const localCard = tracked.localCard;
916
+ const localCard = card(repo, oid, 1);
1779
917
  const plan = {
1780
- repoKey: tracked.repoKey,
918
+ repoKey: 'root:pending-branch-delete',
1781
919
  card: localCard,
1782
920
  transitions: [],
921
+ parked: [],
1783
922
  branches: [
1784
923
  { name: 'main', oid, version: 1 },
1785
924
  { name: 'feature', oid: featureOid, version: 2 },
1786
925
  ],
1787
926
  };
1788
- await materializeRepo(localCard, plan, 2);
927
+ await materializeRepo(localCard, plan);
1789
928
  assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureOid);
1790
929
 
1791
930
  await git(repo, 'branch', '-D', 'feature');
1792
- const deletedCard = await captureTrackedIntent(tracked.project, localCard);
1793
- await materializeRepo(deletedCard, plan, 2);
931
+ await materializeRepo(localCard, plan);
1794
932
 
1795
933
  await assert.rejects(() => git(repo, 'rev-parse', 'refs/heads/feature'));
1796
934
  assert.equal((await git(repo, 'branch', '--show-current')).trim(), 'main');
1797
935
  });
1798
936
 
1799
- test('an acknowledged old branch yields to the later official branch and only then advances', async () => {
1800
- const { repo, oid: baseOid } = await makeRepo('acknowledged-branch-yields');
1801
- await git(repo, 'switch', '-c', 'value-a');
1802
- fs.writeFileSync(path.join(repo, 'value.txt'), 'A\n');
1803
- await git(repo, 'add', '.');
1804
- await git(repo, 'commit', '-m', 'value A');
1805
- const oidA = (await git(repo, 'rev-parse', 'HEAD')).trim();
1806
- await git(repo, 'switch', '-C', 'value-b', baseOid);
1807
- fs.writeFileSync(path.join(repo, 'value.txt'), 'B\n');
1808
- await git(repo, 'add', '.');
1809
- await git(repo, 'commit', '-m', 'value B');
1810
- const oidB = (await git(repo, 'rev-parse', 'HEAD')).trim();
1811
- await git(repo, 'switch', 'main');
1812
- await git(repo, 'branch', '-D', 'value-a', 'value-b');
1813
-
1814
- const tracked = await establishTrackedBaseline(repo, baseOid, 'acknowledged_branch_yields');
1815
- const { repoKey, localCard } = tracked;
1816
- const officialA = {
1817
- name: 'storm/rebase',
1818
- oid: oidA,
1819
- version: 162,
1820
- mutationId: 'mut_branch_value_a',
1821
- };
1822
- const first = {
1823
- repoKey,
1824
- card: localCard,
1825
- transitions: [],
1826
- branches: [
1827
- { name: 'main', oid: baseOid, version: 1 },
1828
- officialA,
1829
- ],
1830
- };
1831
- assert.equal((await materializeRepo(localCard, first, 162)).state, 'in-sync');
1832
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/storm/rebase')).trim(), oidA);
1833
-
1834
- const officialB = {
1835
- name: 'storm/rebase',
1836
- oid: oidB,
1837
- version: 168,
1838
- mutationId: 'mut_branch_value_b',
1839
- };
1840
- const later = {
1841
- ...first,
1842
- branches: [
1843
- { name: 'main', oid: baseOid, version: 1 },
1844
- officialB,
1845
- ],
1846
- };
1847
- const applied = await materializeRepo(localCard, later, 172);
1848
- assert.equal(applied.state, 'in-sync');
1849
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/storm/rebase')).trim(), oidB);
1850
- const state = readRepoState(repoKey);
1851
- assert.equal(state.appliedThrough, 172);
1852
- assert.equal(Object.hasOwn(state, 'pendingRefs'), false);
1853
- assert.equal(state.materialized, true);
1854
- assert.equal(state.expectedPhysicalDigest, state.observedPhysicalDigest);
1855
-
1856
- const afterRestart = await materializeRepo(localCard, later, 172);
1857
- assert.equal(afterRestart.state, 'in-sync');
1858
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/storm/rebase')).trim(), oidB);
1859
- });
1860
-
1861
- test('a durable branch intent survives catch-up, then yields after ownership ends', async () => {
1862
- const { repo, oid: baseOid } = await makeRepo('explicit-pending-branch');
1863
- const commits = {};
1864
- for (const valueName of ['a', 'b', 'c', 'd']) {
1865
- await git(repo, 'switch', '-C', `value-${valueName}`, baseOid);
1866
- fs.writeFileSync(path.join(repo, 'value.txt'), `${valueName}\n`);
1867
- await git(repo, 'add', '.');
1868
- await git(repo, 'commit', '-m', `value ${valueName}`);
1869
- commits[valueName] = (await git(repo, 'rev-parse', 'HEAD')).trim();
1870
- }
1871
- await git(repo, 'switch', 'main');
1872
- await git(repo, 'branch', '-D', 'value-a', 'value-b', 'value-c', 'value-d');
1873
-
1874
- const tracked = await establishTrackedBaseline(repo, baseOid, 'explicit_pending_branch');
1875
- const { repoKey, localCard } = tracked;
1876
- const ref = (oid, version, machine) => ({
1877
- name: 'feature',
1878
- oid,
1879
- version,
1880
- mutationId: `mut_${machine}_${version}`,
1881
- });
1882
- const officialA = ref(commits.a, 10, 'local');
1883
- const planA = {
1884
- repoKey,
1885
- card: localCard,
1886
- transitions: [],
1887
- branches: [
1888
- { name: 'main', oid: baseOid, version: 1 },
1889
- officialA,
1890
- ],
1891
- };
1892
- await materializeRepo(localCard, planA, 10);
1893
- await git(repo, 'update-ref', 'refs/heads/feature', commits.c);
1894
- const intentCard = await captureTrackedIntent(tracked.project, localCard);
1895
-
1896
- const officialB = ref(commits.b, 11, 'remote');
1897
- const planB = {
1898
- ...planA,
1899
- branches: [
1900
- { name: 'main', oid: baseOid, version: 1 },
1901
- officialB,
1902
- ],
1903
- };
1904
- assert.equal((await materializeRepo(intentCard, planB, 11)).state, 'in-sync');
1905
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), commits.c);
1906
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
1907
-
1908
- const acknowledgedC = ref(commits.c, 12, 'local');
1909
- const planC = {
1910
- ...planB,
1911
- branches: [
1912
- { name: 'main', oid: baseOid, version: 1 },
1913
- acknowledgedC,
1914
- ],
1915
- };
1916
- assert.equal((await materializeRepo(localCard, planC, 12)).state, 'in-sync');
1917
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), commits.c);
1918
- require('../workspace/intent').clearIntent(tracked.project);
1919
-
1920
- const officialD = ref(commits.d, 13, 'remote');
1921
- const planD = {
1922
- ...planC,
1923
- branches: [
1924
- { name: 'main', oid: baseOid, version: 1 },
1925
- officialD,
1926
- ],
1927
- };
1928
- assert.equal((await materializeRepo(localCard, planD, 13)).state, 'in-sync');
1929
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), commits.d);
1930
- assert.equal(Object.hasOwn(readRepoState(repoKey), 'pendingRefs'), false);
1931
- });
1932
-
1933
937
  test('a card branch switch follows branch and OID together and leaves the old branch unchanged', async () => {
1934
938
  const { repo, oid: mainOid } = await makeRepo('card-branch-switch');
1935
939
  await git(repo, 'checkout', '-b', 'feature');
@@ -1939,13 +943,12 @@ test('a card branch switch follows branch and OID together and leaves the old br
1939
943
  const featureOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
1940
944
  await git(repo, 'checkout', 'main');
1941
945
 
1942
- const tracked = await establishTrackedBaseline(repo, mainOid, 'card_branch_switch');
1943
- const local = tracked.localCard;
946
+ const local = card(repo, mainOid, 1);
1944
947
  const winning = card('/peer', featureOid, 2);
1945
948
  winning.head.branch = 'feature';
1946
949
  winning.objects = { kind: 'local' };
1947
950
  const outcome = await materializeRepo(local, {
1948
- repoKey: tracked.repoKey,
951
+ repoKey: 'root:test',
1949
952
  card: winning,
1950
953
  transitions: [],
1951
954
  branches: [
@@ -1963,14 +966,13 @@ test('a card branch switch follows branch and OID together and leaves the old br
1963
966
  test('a branch-only card change at the same commit still switches the exact branch', async () => {
1964
967
  const { repo, oid } = await makeRepo('same-oid-branch-switch');
1965
968
  await git(repo, 'branch', 'feature');
1966
- const tracked = await establishTrackedBaseline(repo, oid, 'same_oid_branch_switch');
1967
- const local = tracked.localCard;
969
+ const local = card(repo, oid, 1);
1968
970
  const winning = card('/peer', oid, 2);
1969
971
  winning.head.branch = 'feature';
1970
972
  winning.objects = { kind: 'local' };
1971
973
 
1972
974
  const outcome = await materializeRepo(local, {
1973
- repoKey: tracked.repoKey,
975
+ repoKey: 'root:test',
1974
976
  card: winning,
1975
977
  transitions: [],
1976
978
  branches: [
@@ -1982,907 +984,3 @@ test('a branch-only card change at the same commit still switches the exact bran
1982
984
  assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
1983
985
  assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), oid);
1984
986
  });
1985
-
1986
- async function makePostRefHeadSwitchPlan(rootCommit) {
1987
- const { repo, oid: sourceOid } = await makeRepo(rootCommit);
1988
- await git(repo, 'switch', '--detach', sourceOid);
1989
- fs.writeFileSync(path.join(repo, 'main-target.txt'), 'main target\n');
1990
- await git(repo, 'add', 'main-target.txt');
1991
- await git(repo, 'commit', '-m', 'main target');
1992
- const mainTarget = (await git(repo, 'rev-parse', 'HEAD')).trim();
1993
- await git(repo, 'switch', '--detach', sourceOid);
1994
- fs.writeFileSync(path.join(repo, 'feature-target.txt'), 'feature target\n');
1995
- await git(repo, 'add', 'feature-target.txt');
1996
- await git(repo, 'commit', '-m', 'feature target');
1997
- const featureTarget = (await git(repo, 'rev-parse', 'HEAD')).trim();
1998
- await git(repo, 'switch', '-C', 'main', sourceOid);
1999
- const tracked = await establishTrackedBaseline(
2000
- repo,
2001
- sourceOid,
2002
- rootCommit.replaceAll('-', '_'),
2003
- );
2004
- const winning = {
2005
- ...tracked.localCard,
2006
- head: { branch: 'feature', oid: featureTarget, detached: false },
2007
- positionVersion: 2,
2008
- positionMutationId: 'mut_post_ref_head_switch',
2009
- };
2010
- const plan = {
2011
- repoKey: tracked.repoKey,
2012
- card: winning,
2013
- transitions: [],
2014
- branches: [
2015
- {
2016
- name: 'main',
2017
- oid: mainTarget,
2018
- version: 2,
2019
- mutationId: 'mut_post_ref_main_move',
2020
- },
2021
- {
2022
- name: 'feature',
2023
- oid: featureTarget,
2024
- version: 2,
2025
- mutationId: 'mut_post_ref_feature',
2026
- },
2027
- ],
2028
- };
2029
- return {
2030
- repo,
2031
- sourceOid,
2032
- mainTarget,
2033
- featureTarget,
2034
- tracked,
2035
- plan,
2036
- };
2037
- }
2038
-
2039
- test('a branch switch derives HEAD from the sealed post-ref plan when the old branch also moves', async () => {
2040
- const {
2041
- repo,
2042
- mainTarget,
2043
- featureTarget,
2044
- tracked,
2045
- plan,
2046
- } = await makePostRefHeadSwitchPlan('post-ref-head-switch');
2047
- const result = await materializeRepo(tracked.localCard, plan, 2);
2048
- assert.equal(result.state, 'in-sync', result.reason);
2049
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
2050
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), featureTarget);
2051
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), mainTarget);
2052
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureTarget);
2053
- assert.equal(require('../workspace/intent').readOwnedIntent(tracked.project), null);
2054
- });
2055
-
2056
- test('restart recognizes a sealed post-ref HEAD before finishing the branch switch', async () => {
2057
- const {
2058
- repo,
2059
- mainTarget,
2060
- featureTarget,
2061
- tracked,
2062
- plan,
2063
- } = await makePostRefHeadSwitchPlan('post-ref-head-switch-restart');
2064
- const transitions = require('../workspace/transition');
2065
- const moveHeadToCard = transitions.moveHeadToCard;
2066
- let crashed = false;
2067
- transitions.moveHeadToCard = async (...args) => {
2068
- if (!crashed) {
2069
- crashed = true;
2070
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2071
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), mainTarget);
2072
- assert.equal(
2073
- (await git(repo, 'rev-parse', 'refs/heads/feature')).trim(),
2074
- featureTarget,
2075
- );
2076
- throw new Error('simulated process death after ref batch before HEAD');
2077
- }
2078
- return moveHeadToCard(...args);
2079
- };
2080
- try {
2081
- await assert.rejects(
2082
- materializeRepo(tracked.localCard, plan, 2),
2083
- /simulated process death after ref batch before HEAD/,
2084
- );
2085
- } finally {
2086
- transitions.moveHeadToCard = moveHeadToCard;
2087
- }
2088
-
2089
- assert.equal(readRepoState(
2090
- tracked.repoKey,
2091
- tracked.project.resourceId,
2092
- ).appliedThrough, 1);
2093
- const recovered = await materializeRepo(
2094
- await observedCard(tracked.localCard),
2095
- plan,
2096
- 2,
2097
- );
2098
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2099
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
2100
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), featureTarget);
2101
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), mainTarget);
2102
- assert.equal(require('../workspace/intent').readOwnedIntent(tracked.project), null);
2103
- });
2104
-
2105
- test('a source-branch return after the ref receipt remains local during HEAD recovery', async () => {
2106
- const {
2107
- repo,
2108
- sourceOid,
2109
- mainTarget,
2110
- tracked,
2111
- plan,
2112
- } = await makePostRefHeadSwitchPlan('post-ref-head-switch-aba');
2113
- const transitions = require('../workspace/transition');
2114
- const moveHeadToCard = transitions.moveHeadToCard;
2115
- transitions.moveHeadToCard = async () => {
2116
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), mainTarget);
2117
- throw new Error('simulated process death before post-ref HEAD attachment');
2118
- };
2119
- try {
2120
- await assert.rejects(
2121
- materializeRepo(tracked.localCard, plan, 2),
2122
- /simulated process death before post-ref HEAD attachment/,
2123
- );
2124
- } finally {
2125
- transitions.moveHeadToCard = moveHeadToCard;
2126
- }
2127
-
2128
- await git(repo, 'update-ref', 'refs/heads/main', sourceOid, mainTarget);
2129
- const recovered = await materializeRepo(
2130
- await observedCard(tracked.localCard),
2131
- plan,
2132
- 2,
2133
- );
2134
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2135
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2136
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), sourceOid);
2137
- assert.equal(readRepoState(
2138
- tracked.repoKey,
2139
- tracked.project.resourceId,
2140
- ).appliedThrough, 2);
2141
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
2142
- });
2143
-
2144
- async function makeCheckedBranchDeletePlan(rootCommit) {
2145
- const { repo, oid: sourceOid } = await makeRepo(rootCommit);
2146
- await git(repo, 'switch', '--detach', sourceOid);
2147
- fs.writeFileSync(path.join(repo, 'feature-target.txt'), 'feature target\n');
2148
- await git(repo, 'add', 'feature-target.txt');
2149
- await git(repo, 'commit', '-m', 'feature target');
2150
- const featureTarget = (await git(repo, 'rev-parse', 'HEAD')).trim();
2151
- await git(repo, 'switch', '-C', 'main', sourceOid);
2152
- const tracked = await establishTrackedBaseline(
2153
- repo,
2154
- sourceOid,
2155
- rootCommit.replaceAll('-', '_'),
2156
- );
2157
- const winning = {
2158
- ...tracked.localCard,
2159
- head: { branch: 'feature', oid: featureTarget, detached: false },
2160
- positionVersion: 2,
2161
- positionMutationId: `mut_${rootCommit.replaceAll('-', '_')}`,
2162
- };
2163
- return {
2164
- repo,
2165
- sourceOid,
2166
- featureTarget,
2167
- tracked,
2168
- plan: {
2169
- repoKey: tracked.repoKey,
2170
- card: winning,
2171
- transitions: [],
2172
- branches: [{
2173
- name: 'feature',
2174
- oid: featureTarget,
2175
- version: 2,
2176
- mutationId: `mut_${rootCommit.replaceAll('-', '_')}_feature`,
2177
- }],
2178
- },
2179
- };
2180
- }
2181
-
2182
- test('checked branch deletion waits until HEAD can survive a crash', async () => {
2183
- const {
2184
- repo,
2185
- sourceOid,
2186
- featureTarget,
2187
- tracked,
2188
- plan,
2189
- } = await makeCheckedBranchDeletePlan('checked-delete-before-head');
2190
- const transitions = require('../workspace/transition');
2191
- const moveHeadToCard = transitions.moveHeadToCard;
2192
- transitions.moveHeadToCard = async () => {
2193
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2194
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), sourceOid);
2195
- assert.equal(
2196
- (await git(repo, 'rev-parse', 'refs/heads/main')).trim(),
2197
- sourceOid,
2198
- 'the checked source ref still exists before HEAD attachment',
2199
- );
2200
- throw new Error('simulated process death before checked branch deletion');
2201
- };
2202
- try {
2203
- await assert.rejects(
2204
- materializeRepo(tracked.localCard, plan, 2),
2205
- /simulated process death before checked branch deletion/,
2206
- );
2207
- } finally {
2208
- transitions.moveHeadToCard = moveHeadToCard;
2209
- }
2210
-
2211
- const recovered = await materializeRepo(
2212
- await observedCard(tracked.localCard),
2213
- plan,
2214
- 2,
2215
- );
2216
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2217
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
2218
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), featureTarget);
2219
- await assert.rejects(
2220
- git(repo, 'rev-parse', '--verify', 'refs/heads/main'),
2221
- /Needed a single revision|unknown revision|not a valid object name/,
2222
- );
2223
- assert.equal(require('../workspace/intent').readOwnedIntent(tracked.project), null);
2224
- });
2225
-
2226
- test('a source ref moved during deferred deletion remains local', async () => {
2227
- const {
2228
- repo,
2229
- sourceOid,
2230
- tracked,
2231
- plan,
2232
- } = await makeCheckedBranchDeletePlan('checked-delete-local-race');
2233
- const localOid = (
2234
- await git(repo, 'commit-tree', `${sourceOid}^{tree}`, '-p', sourceOid, '-m', 'local ref')
2235
- ).trim();
2236
- const transitions = require('../workspace/transition');
2237
- const updateBranchRefs = transitions.updateBranchRefs;
2238
- let raced = false;
2239
- transitions.updateBranchRefs = async (root, changes, operation) => {
2240
- if (
2241
- !raced
2242
- && changes.some((change) => (
2243
- change.branch === 'main'
2244
- && change.expectedOid === sourceOid
2245
- && change.oid === null
2246
- ))
2247
- ) {
2248
- raced = true;
2249
- await git(repo, 'update-ref', 'refs/heads/main', localOid, sourceOid);
2250
- }
2251
- return updateBranchRefs(root, changes, operation);
2252
- };
2253
- let interrupted;
2254
- try {
2255
- interrupted = await materializeRepo(tracked.localCard, plan, 2);
2256
- } finally {
2257
- transitions.updateBranchRefs = updateBranchRefs;
2258
- }
2259
- assert.deepEqual(interrupted, {
2260
- state: 'waiting',
2261
- reason: 'local-refs-moving',
2262
- });
2263
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), localOid);
2264
-
2265
- const recovered = await materializeRepo(
2266
- await observedCard(tracked.localCard),
2267
- plan,
2268
- 2,
2269
- );
2270
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2271
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), localOid);
2272
- assert.equal(readRepoState(
2273
- tracked.repoKey,
2274
- tracked.project.resourceId,
2275
- ).appliedThrough, 2);
2276
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
2277
- });
2278
-
2279
- test('a HEAD return before deferred deletion stays attached across restart', async () => {
2280
- const {
2281
- repo,
2282
- sourceOid,
2283
- featureTarget,
2284
- tracked,
2285
- plan,
2286
- } = await makeCheckedBranchDeletePlan('checked-delete-head-return');
2287
- const transitions = require('../workspace/transition');
2288
- const updateBranchRefs = transitions.updateBranchRefs;
2289
- let raced = false;
2290
- transitions.updateBranchRefs = async (root, changes, operation) => {
2291
- if (
2292
- !raced
2293
- && changes.some((change) => (
2294
- change.branch === 'main'
2295
- && change.expectedOid === sourceOid
2296
- && change.oid === null
2297
- ))
2298
- ) {
2299
- raced = true;
2300
- await git(repo, 'symbolic-ref', 'HEAD', 'refs/heads/main');
2301
- }
2302
- return updateBranchRefs(root, changes, operation);
2303
- };
2304
- let interrupted;
2305
- try {
2306
- interrupted = await materializeRepo(tracked.localCard, plan, 2);
2307
- } finally {
2308
- transitions.updateBranchRefs = updateBranchRefs;
2309
- }
2310
- assert.equal(raced, true);
2311
- assert.deepEqual(interrupted, {
2312
- state: 'waiting',
2313
- reason: 'local-refs-moving',
2314
- });
2315
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2316
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), sourceOid);
2317
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), sourceOid);
2318
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureTarget);
2319
-
2320
- let recovered;
2321
- for (let attempt = 0; attempt < 3; attempt += 1) {
2322
- recovered = await materializeRepo(tracked.localCard, plan, 2);
2323
- if (recovered.state === 'in-sync') break;
2324
- assert.equal(recovered.state, 'waiting', recovered.reason);
2325
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2326
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), sourceOid);
2327
- }
2328
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2329
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2330
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), sourceOid);
2331
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), sourceOid);
2332
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureTarget);
2333
- assert.equal(readRepoState(
2334
- tracked.repoKey,
2335
- tracked.project.resourceId,
2336
- ).appliedThrough, 2);
2337
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
2338
- });
2339
-
2340
- test('restart finishes a branch-only official apply before a newer branch event', async () => {
2341
- const rootCommit = 'branch-only-recovery-newer-rail';
2342
- const repoKey = `root:${rootCommit}`;
2343
- const { repo, oid: baseOid } = await makeRepo(rootCommit);
2344
- const branchOids = {};
2345
- for (const name of ['a', 'b']) {
2346
- await git(repo, 'switch', '-C', `source-${name}`, baseOid);
2347
- fs.writeFileSync(path.join(repo, 'branch-value.txt'), `${name}\n`);
2348
- await git(repo, 'add', 'branch-value.txt');
2349
- await git(repo, 'commit', '-m', `branch value ${name}`);
2350
- branchOids[name] = (await git(repo, 'rev-parse', 'HEAD')).trim();
2351
- }
2352
- await git(repo, 'switch', 'main');
2353
- await git(repo, 'branch', '-D', 'source-a', 'source-b');
2354
-
2355
- const localCard = {
2356
- ...card(repo, baseOid, 1),
2357
- repo: { rootCommit, remote: null },
2358
- };
2359
- const baseline = {
2360
- repoKey,
2361
- card: localCard,
2362
- transitions: [],
2363
- branches: [{ name: 'main', oid: baseOid, version: 1 }],
2364
- };
2365
- assert.equal((await materializeRepo(localCard, baseline, 1)).state, 'in-sync');
2366
-
2367
- const officialA = {
2368
- name: 'feature',
2369
- oid: branchOids.a,
2370
- version: 2,
2371
- mutationId: 'mut_branch_only_a',
2372
- };
2373
- const planN = {
2374
- ...baseline,
2375
- branches: [...baseline.branches, officialA],
2376
- };
2377
- const transitions = require('../workspace/transition');
2378
- const updateBranchRefs = transitions.updateBranchRefs;
2379
- let crashed = false;
2380
- transitions.updateBranchRefs = async (...args) => {
2381
- const result = await updateBranchRefs(...args);
2382
- if (!crashed) {
2383
- crashed = true;
2384
- throw new Error('simulated process death after branch-only apply');
2385
- }
2386
- return result;
2387
- };
2388
- try {
2389
- await assert.rejects(
2390
- materializeRepo(localCard, planN, 2),
2391
- /simulated process death/,
2392
- );
2393
- } finally {
2394
- transitions.updateBranchRefs = updateBranchRefs;
2395
- }
2396
-
2397
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), branchOids.a);
2398
- assert.equal(readRepoState(repoKey).appliedThrough, 1);
2399
- assert.equal(readRepoState(repoKey).applying?.through, 2);
2400
-
2401
- const officialB = {
2402
- name: 'feature',
2403
- oid: branchOids.b,
2404
- version: 3,
2405
- mutationId: 'mut_branch_only_b',
2406
- };
2407
- const latest = {
2408
- ...baseline,
2409
- branches: [...baseline.branches, officialB],
2410
- };
2411
- const recovered = await materializeRepo(localCard, latest, 3);
2412
- assert.equal(recovered.state, 'in-sync');
2413
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), branchOids.b);
2414
- assert.equal(readRepoState(repoKey).appliedThrough, 3);
2415
- assert.equal(readRepoState(repoKey).applying, undefined);
2416
- });
2417
-
2418
- test('restart finishes a checkpoint-only official apply before a newer checkpoint event', async () => {
2419
- const rootCommit = 'checkpoint-only-recovery-newer-rail';
2420
- const repoKey = `root:${rootCommit}`;
2421
- const { repo, oid: baseOid } = await makeRepo(rootCommit);
2422
- const patchN = await patchFor(repo, { 'a.txt': 'official N\n' });
2423
- const patchN1 = await patchFor(repo, {
2424
- 'a.txt': 'official N\n',
2425
- 'b.txt': 'official N+1\n',
2426
- });
2427
- const localCard = {
2428
- ...card(repo, baseOid, 1),
2429
- repo: { rootCommit, remote: null },
2430
- };
2431
- const baseline = {
2432
- repoKey,
2433
- card: localCard,
2434
- transitions: [],
2435
- branches: [{ name: 'main', oid: baseOid, version: 1 }],
2436
- };
2437
- assert.equal((await materializeRepo(localCard, baseline, 1)).state, 'in-sync');
2438
-
2439
- const operationN = transition('machine-remote', 2, baseOid, '', patchN);
2440
- const planN = { ...baseline, transitions: [operationN] };
2441
- const mergeModule = require('../workspace/merge');
2442
- const advance = mergeModule.advance;
2443
- let crashed = false;
2444
- mergeModule.advance = async (...args) => {
2445
- const result = await advance(...args);
2446
- if (!crashed && /^[0-9a-f]{64}$/.test(args[3]?.label || '')) {
2447
- crashed = true;
2448
- throw new Error('simulated process death after checkpoint-only apply');
2449
- }
2450
- return result;
2451
- };
2452
- try {
2453
- await assert.rejects(
2454
- materializeRepo(localCard, planN, 2),
2455
- /simulated process death/,
2456
- );
2457
- } finally {
2458
- mergeModule.advance = advance;
2459
- }
2460
-
2461
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'official N\n');
2462
- assert.equal(readRepoState(repoKey).appliedThrough, 1);
2463
- assert.equal(readRepoState(repoKey).applying?.through, 2);
2464
-
2465
- const operationN1 = transition('machine-remote', 3, baseOid, patchN, patchN1);
2466
- const latest = { ...baseline, transitions: [operationN, operationN1] };
2467
- const recoveryLabels = [];
2468
- mergeModule.advance = async (...args) => {
2469
- recoveryLabels.push(args[3]?.label);
2470
- return advance(...args);
2471
- };
2472
- let recovered;
2473
- try {
2474
- recovered = await materializeRepo(localCard, latest, 3);
2475
- } finally {
2476
- mergeModule.advance = advance;
2477
- }
2478
-
2479
- assert.equal(recovered.state, 'in-sync');
2480
- assert.equal(recoveryLabels.filter(Boolean).length, 2);
2481
- assert.equal(new Set(recoveryLabels.filter(Boolean)).size, 2);
2482
- assert.equal(
2483
- recoveryLabels.filter(Boolean).every((label) => /^[0-9a-f]{64}$/.test(label)),
2484
- true,
2485
- );
2486
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'official N\n');
2487
- assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'official N+1\n');
2488
- assert.equal(readRepoState(repoKey).appliedThrough, 3);
2489
- assert.equal(readRepoState(repoKey).applying, undefined);
2490
- });
2491
-
2492
- test('recovery HEAD movement cannot bless and overwrite a commit made after sealed observation', async () => {
2493
- const { repo, oid: sourceOid } = await makeRepo('recovery-head-sealed-source');
2494
- const tracked = await establishTrackedBaseline(repo, sourceOid, 'recovery_head_sealed_source');
2495
- await git(repo, 'switch', '--detach', sourceOid);
2496
- fs.writeFileSync(path.join(repo, 'official.txt'), 'official commit\n');
2497
- await git(repo, 'add', 'official.txt');
2498
- await git(repo, 'commit', '-m', 'official commit');
2499
- const officialOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
2500
- await git(repo, 'switch', '-C', 'main', sourceOid);
2501
- const winning = {
2502
- ...tracked.localCard,
2503
- head: { branch: 'main', oid: officialOid, detached: false },
2504
- positionVersion: 2,
2505
- positionMutationId: 'mut_recovery_head_sealed',
2506
- };
2507
- const plan = {
2508
- repoKey: tracked.repoKey,
2509
- card: winning,
2510
- transitions: [],
2511
- branches: [{
2512
- name: 'main',
2513
- oid: officialOid,
2514
- version: 2,
2515
- mutationId: 'mut_recovery_main_sealed',
2516
- }],
2517
- };
2518
-
2519
- const transitions = require('../workspace/transition');
2520
- const updateBranchRefs = transitions.updateBranchRefs;
2521
- let interruptBeforeRefs = true;
2522
- transitions.updateBranchRefs = async (...args) => {
2523
- if (interruptBeforeRefs) {
2524
- interruptBeforeRefs = false;
2525
- throw new Error('simulated process death before ref apply');
2526
- }
2527
- return updateBranchRefs(...args);
2528
- };
2529
- try {
2530
- await assert.rejects(
2531
- materializeRepo(tracked.localCard, plan, 2),
2532
- /simulated process death before ref apply/,
2533
- );
2534
- } finally {
2535
- transitions.updateBranchRefs = updateBranchRefs;
2536
- }
2537
- assert.equal(readRepoState(tracked.repoKey).applying?.through, 2);
2538
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), sourceOid);
2539
-
2540
- let localOid = null;
2541
- transitions.updateBranchRefs = async (...args) => {
2542
- const result = await updateBranchRefs(...args);
2543
- fs.writeFileSync(path.join(repo, 'racing-commit.txt'), 'local survives\n');
2544
- await git(repo, 'add', 'racing-commit.txt');
2545
- await git(repo, 'commit', '-m', 'local commit after sealed observation');
2546
- localOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
2547
- return result;
2548
- };
2549
- let raced;
2550
- try {
2551
- raced = await materializeRepo(tracked.localCard, plan, 2);
2552
- } finally {
2553
- transitions.updateBranchRefs = updateBranchRefs;
2554
- }
2555
- assert.deepEqual(raced, {
2556
- state: 'waiting',
2557
- reason: 'local-index-moving',
2558
- moved: false,
2559
- });
2560
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), localOid);
2561
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/main')).trim(), localOid);
2562
- assert.equal(fs.readFileSync(path.join(repo, 'racing-commit.txt'), 'utf8'), 'local survives\n');
2563
-
2564
- const recovered = await materializeRepo(
2565
- await observedCard(tracked.localCard),
2566
- plan,
2567
- 2,
2568
- );
2569
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2570
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), localOid);
2571
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
2572
- });
2573
-
2574
- test('recovery index movement preserves a staged file written after sealed observation', async () => {
2575
- const { repo, oid } = await makeRepo('recovery-index-sealed-source');
2576
- const tracked = await establishTrackedBaseline(repo, oid, 'recovery_index_sealed_source');
2577
- const officialPatch = await patchFor(repo, { 'a.txt': 'official staged value\n' });
2578
- const plan = {
2579
- ...tracked.baseline,
2580
- transitions: [transition('machine-remote', 2, oid, '', officialPatch)],
2581
- };
2582
- const mergeModule = require('../workspace/merge');
2583
- const advance = mergeModule.advance;
2584
- let interruptBeforeWorktree = true;
2585
- mergeModule.advance = async (...args) => {
2586
- if (interruptBeforeWorktree && /^[0-9a-f]{64}$/.test(args[3]?.label || '')) {
2587
- interruptBeforeWorktree = false;
2588
- throw new Error('simulated process death before worktree apply');
2589
- }
2590
- return advance(...args);
2591
- };
2592
- try {
2593
- await assert.rejects(
2594
- materializeRepo(tracked.localCard, plan, 2),
2595
- /simulated process death before worktree apply/,
2596
- );
2597
- } finally {
2598
- mergeModule.advance = advance;
2599
- }
2600
- assert.equal(readRepoState(tracked.repoKey).applying?.through, 2);
2601
-
2602
- let stagedDuringRecovery = false;
2603
- mergeModule.advance = async (...args) => {
2604
- const result = await advance(...args);
2605
- if (!stagedDuringRecovery && /^[0-9a-f]{64}$/.test(args[3]?.label || '')) {
2606
- stagedDuringRecovery = true;
2607
- fs.writeFileSync(path.join(repo, 'racing-staged.txt'), 'staged survives\n');
2608
- await git(repo, 'add', 'racing-staged.txt');
2609
- }
2610
- return result;
2611
- };
2612
- let raced;
2613
- try {
2614
- raced = await materializeRepo(tracked.localCard, plan, 2);
2615
- } finally {
2616
- mergeModule.advance = advance;
2617
- }
2618
- assert.deepEqual(raced, {
2619
- state: 'waiting',
2620
- reason: 'local-index-moving',
2621
- moved: false,
2622
- });
2623
- assert.match(await git(repo, 'diff', '--cached', '--', 'racing-staged.txt'), /staged survives/);
2624
-
2625
- const recovered = await materializeRepo(
2626
- await observedCard(tracked.localCard),
2627
- plan,
2628
- 2,
2629
- );
2630
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2631
- assert.match(await git(repo, 'diff', '--cached', '--', 'racing-staged.txt'), /staged survives/);
2632
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'official staged value\n');
2633
- assert.ok(require('../workspace/intent').readOwnedIntent(tracked.project));
2634
- });
2635
-
2636
- test('a byte-identical index refresh before apply is not an official crossing receipt', async () => {
2637
- const { repo, oid } = await makeRepo('index-refresh-is-not-crossing');
2638
- const tracked = await establishTrackedBaseline(repo, oid, 'index_refresh_not_crossing');
2639
- const officialPatch = await patchFor(repo, { 'a.txt': 'official index target\n' });
2640
- const plan = {
2641
- ...tracked.baseline,
2642
- transitions: [transition('machine-remote', 2, oid, '', officialPatch)],
2643
- };
2644
- const transitions = require('../workspace/transition');
2645
- const applyIndexPatch = transitions.applyIndexPatch;
2646
- const index = await gitPath(repo, 'index');
2647
- const before = fs.statSync(index, { bigint: true });
2648
- let crashed = false;
2649
- transitions.applyIndexPatch = async (...args) => {
2650
- if (!crashed) {
2651
- crashed = true;
2652
- const replacement = `${index}.semantic-refresh`;
2653
- fs.writeFileSync(replacement, fs.readFileSync(index));
2654
- fs.renameSync(replacement, index);
2655
- throw new Error('simulated process death before index apply');
2656
- }
2657
- return applyIndexPatch(...args);
2658
- };
2659
- try {
2660
- await assert.rejects(
2661
- materializeRepo(tracked.localCard, plan, 2),
2662
- /simulated process death before index apply/,
2663
- );
2664
- } finally {
2665
- transitions.applyIndexPatch = applyIndexPatch;
2666
- }
2667
- const after = fs.statSync(index, { bigint: true });
2668
- assert.notEqual(after.ino, before.ino);
2669
- assert.equal(readRepoState(tracked.repoKey).appliedThrough, 1);
2670
-
2671
- const recovered = await materializeRepo(tracked.localCard, plan, 2);
2672
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2673
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'official index target\n');
2674
- assert.match(await git(repo, 'diff', '--cached'), /official index target/);
2675
- assert.equal(
2676
- require('../workspace/intent').readOwnedIntent(tracked.project),
2677
- null,
2678
- );
2679
- });
2680
-
2681
- test('a byte-identical HEAD refresh before apply is not an official crossing receipt', async () => {
2682
- const { repo, oid } = await makeRepo('head-refresh-is-not-crossing');
2683
- await git(repo, 'branch', 'feature', oid);
2684
- const tracked = await establishTrackedBaseline(repo, oid, 'head_refresh_not_crossing');
2685
- const winning = {
2686
- ...tracked.localCard,
2687
- head: { branch: 'feature', oid, detached: false },
2688
- positionVersion: 2,
2689
- positionMutationId: 'mut_head_refresh_target',
2690
- };
2691
- const plan = {
2692
- repoKey: tracked.repoKey,
2693
- card: winning,
2694
- transitions: [],
2695
- branches: [
2696
- { name: 'main', oid, version: 1, mutationId: 'mut_head_refresh_main' },
2697
- { name: 'feature', oid, version: 2, mutationId: 'mut_head_refresh_feature' },
2698
- ],
2699
- };
2700
- const transitions = require('../workspace/transition');
2701
- const moveHeadToCard = transitions.moveHeadToCard;
2702
- const head = await gitPath(repo, 'HEAD');
2703
- const before = fs.statSync(head, { bigint: true });
2704
- let crashed = false;
2705
- transitions.moveHeadToCard = async (...args) => {
2706
- if (!crashed) {
2707
- crashed = true;
2708
- const replacement = `${head}.semantic-refresh`;
2709
- fs.writeFileSync(replacement, fs.readFileSync(head));
2710
- fs.renameSync(replacement, head);
2711
- throw new Error('simulated process death before HEAD apply');
2712
- }
2713
- return moveHeadToCard(...args);
2714
- };
2715
- try {
2716
- await assert.rejects(
2717
- materializeRepo(tracked.localCard, plan, 2),
2718
- /simulated process death before HEAD apply/,
2719
- );
2720
- } finally {
2721
- transitions.moveHeadToCard = moveHeadToCard;
2722
- }
2723
- const after = fs.statSync(head, { bigint: true });
2724
- assert.notEqual(after.ino, before.ino);
2725
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'main');
2726
-
2727
- const recovered = await materializeRepo(
2728
- await observedCard(tracked.localCard),
2729
- plan,
2730
- 2,
2731
- );
2732
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2733
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
2734
- assert.equal(
2735
- require('../workspace/intent').readOwnedIntent(tracked.project),
2736
- null,
2737
- );
2738
- });
2739
-
2740
- test('a same-value file revert after apply remains successor intent on recovery', async () => {
2741
- const { repo, oid } = await makeRepo('checkpoint-aba-revert');
2742
- const tracked = await establishTrackedBaseline(repo, oid, 'checkpoint_aba_revert');
2743
- const officialPatch = await patchFor(repo, { 'a.txt': 'official target\n' });
2744
- const plan = {
2745
- ...tracked.baseline,
2746
- transitions: [transition('machine-remote', 2, oid, '', officialPatch)],
2747
- };
2748
- const mergeModule = require('../workspace/merge');
2749
- const advance = mergeModule.advance;
2750
- let crashed = false;
2751
- mergeModule.advance = async (...args) => {
2752
- const result = await advance(...args);
2753
- if (!crashed && /^[0-9a-f]{64}$/.test(args[3]?.label || '')) {
2754
- crashed = true;
2755
- throw new Error('simulated process death after file target');
2756
- }
2757
- return result;
2758
- };
2759
- try {
2760
- await assert.rejects(
2761
- materializeRepo(tracked.localCard, plan, 2),
2762
- /simulated process death after file target/,
2763
- );
2764
- } finally {
2765
- mergeModule.advance = advance;
2766
- }
2767
-
2768
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'official target\n');
2769
- fs.writeFileSync(path.join(repo, 'a.txt'), 'a0\n');
2770
- const recovered = await materializeRepo(tracked.localCard, plan, 2);
2771
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2772
- assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), 'a0\n');
2773
- assert.equal(
2774
- readRepoState(tracked.repoKey, tracked.project.resourceId).appliedThrough,
2775
- 2,
2776
- );
2777
- const successor = require('../workspace/intent').readOwnedIntent(tracked.project);
2778
- assert.ok(successor);
2779
- assert.equal(successor.base.through, 2);
2780
- });
2781
-
2782
- test('deleting an official create after apply remains successor intent on recovery', async () => {
2783
- const { repo, oid } = await makeRepo('checkpoint-create-delete');
2784
- const tracked = await establishTrackedBaseline(repo, oid, 'checkpoint_create_delete');
2785
- fs.writeFileSync(path.join(repo, 'official.txt'), 'official target\n');
2786
- await git(repo, 'add', 'official.txt');
2787
- const officialPatch = await git(repo, 'diff', '--cached', 'HEAD', '--binary', '--full-index');
2788
- await git(repo, 'reset', '--hard', 'HEAD');
2789
- const plan = {
2790
- ...tracked.baseline,
2791
- transitions: [transition('machine-remote', 2, oid, '', officialPatch)],
2792
- };
2793
- const mergeModule = require('../workspace/merge');
2794
- const advance = mergeModule.advance;
2795
- let crashed = false;
2796
- mergeModule.advance = async (...args) => {
2797
- const result = await advance(...args);
2798
- if (!crashed && /^[0-9a-f]{64}$/.test(args[3]?.label || '')) {
2799
- crashed = true;
2800
- throw new Error('simulated process death after official create');
2801
- }
2802
- return result;
2803
- };
2804
- try {
2805
- await assert.rejects(
2806
- materializeRepo(tracked.localCard, plan, 2),
2807
- /simulated process death after official create/,
2808
- );
2809
- } finally {
2810
- mergeModule.advance = advance;
2811
- }
2812
-
2813
- assert.equal(fs.readFileSync(path.join(repo, 'official.txt'), 'utf8'), 'official target\n');
2814
- fs.unlinkSync(path.join(repo, 'official.txt'));
2815
- const recovered = await materializeRepo(tracked.localCard, plan, 2);
2816
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2817
- assert.equal(fs.existsSync(path.join(repo, 'official.txt')), false);
2818
- assert.equal(
2819
- readRepoState(tracked.repoKey, tracked.project.resourceId).appliedThrough,
2820
- 2,
2821
- );
2822
- const successor = require('../workspace/intent').readOwnedIntent(tracked.project);
2823
- assert.ok(successor);
2824
- assert.equal(successor.base.through, 2);
2825
- });
2826
-
2827
- test('a checked-out branch reverted after apply remains successor intent on recovery', async () => {
2828
- const { repo, oid: baseOid } = await makeRepo('branch-aba-revert');
2829
- await git(repo, 'switch', '-c', 'feature');
2830
- fs.writeFileSync(path.join(repo, 'feature.txt'), 'A\n');
2831
- await git(repo, 'add', 'feature.txt');
2832
- await git(repo, 'commit', '-m', 'feature A');
2833
- const oidA = (await git(repo, 'rev-parse', 'HEAD')).trim();
2834
- fs.writeFileSync(path.join(repo, 'feature.txt'), 'B\n');
2835
- await git(repo, 'add', 'feature.txt');
2836
- await git(repo, 'commit', '-m', 'feature B');
2837
- const oidB = (await git(repo, 'rev-parse', 'HEAD')).trim();
2838
- await git(repo, 'reset', '--hard', oidA);
2839
- const tracked = await establishTrackedBaseline(repo, oidA, 'branch_aba_revert');
2840
- const plan = {
2841
- repoKey: tracked.repoKey,
2842
- card: {
2843
- ...tracked.localCard,
2844
- head: { branch: 'feature', oid: oidB, detached: false },
2845
- positionVersion: 2,
2846
- positionMutationId: 'mut_branch_aba_target',
2847
- },
2848
- transitions: [],
2849
- branches: [
2850
- { name: 'main', oid: baseOid, version: 1 },
2851
- { name: 'feature', oid: oidB, version: 2 },
2852
- ],
2853
- };
2854
- const transitions = require('../workspace/transition');
2855
- const updateBranchRefs = transitions.updateBranchRefs;
2856
- let crashed = false;
2857
- transitions.updateBranchRefs = async (...args) => {
2858
- const result = await updateBranchRefs(...args);
2859
- if (!crashed) {
2860
- crashed = true;
2861
- throw new Error('simulated process death after branch target');
2862
- }
2863
- return result;
2864
- };
2865
- try {
2866
- await assert.rejects(
2867
- materializeRepo(tracked.localCard, plan, 2),
2868
- /simulated process death after branch target/,
2869
- );
2870
- } finally {
2871
- transitions.updateBranchRefs = updateBranchRefs;
2872
- }
2873
-
2874
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), oidB);
2875
- await git(repo, 'update-ref', 'refs/heads/feature', oidA, oidB);
2876
- const recovered = await materializeRepo(
2877
- await observedCard(tracked.localCard),
2878
- plan,
2879
- 2,
2880
- );
2881
- assert.equal(recovered.state, 'in-sync', recovered.reason);
2882
- assert.equal((await git(repo, 'symbolic-ref', '--short', 'HEAD')).trim(), 'feature');
2883
- assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), oidA);
2884
- assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), oidA);
2885
- const successor = require('../workspace/intent').readOwnedIntent(tracked.project);
2886
- assert.ok(successor);
2887
- assert.equal(successor.base.through, 2);
2888
- });