amalgm 0.1.221 → 0.1.235
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.
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/state/attachments.js +7 -1
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +39 -2
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +180 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +61 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +28 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +152 -12
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +6 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +105 -67
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +183 -23
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +99 -206
- package/runtime/scripts/amalgm-mcp/workspace/card.js +33 -9
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +3 -2
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +194 -20
- package/runtime/scripts/amalgm-mcp/workspace/git.js +23 -5
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +24 -186
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +13 -1
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +323 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +115 -101
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +106 -137
|
@@ -29,11 +29,12 @@ const {
|
|
|
29
29
|
} = require('../workspace/checkpoint');
|
|
30
30
|
const { parkedForState } = require('../workspace/reducer');
|
|
31
31
|
const {
|
|
32
|
-
|
|
32
|
+
_test: wallTest,
|
|
33
33
|
hasLocalContribution,
|
|
34
34
|
publicationStatus,
|
|
35
35
|
stampCards,
|
|
36
36
|
} = require('../workspace/wall');
|
|
37
|
+
const projectStream = require('../workspace/project-stream');
|
|
37
38
|
|
|
38
39
|
test.after(() => {
|
|
39
40
|
closeLocalDb();
|
|
@@ -65,16 +66,16 @@ const rules = {
|
|
|
65
66
|
liveness: 'shared-wall-outbox',
|
|
66
67
|
gating: 'inside-winning-card-checkpoint',
|
|
67
68
|
},
|
|
68
|
-
|
|
69
|
-
identity: '
|
|
70
|
-
ownership: '
|
|
71
|
-
liveness: '
|
|
72
|
-
gating: '
|
|
69
|
+
snapshot: {
|
|
70
|
+
identity: 'state-through-official-version',
|
|
71
|
+
ownership: 'authority-derived',
|
|
72
|
+
liveness: 'hydrates-then-replays-tail',
|
|
73
|
+
gating: 'never-a-new-event',
|
|
73
74
|
},
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
test('the publication model has one explicit rule for every value and property', () => {
|
|
77
|
-
assert.deepEqual(Object.keys(rules), ['card', 'branch', 'checkpoint', 'parked', '
|
|
78
|
+
assert.deepEqual(Object.keys(rules), ['card', 'branch', 'checkpoint', 'parked', 'snapshot']);
|
|
78
79
|
for (const rule of Object.values(rules)) {
|
|
79
80
|
assert.deepEqual(Object.keys(rule), ['identity', 'ownership', 'liveness', 'gating']);
|
|
80
81
|
assert.equal(Object.values(rule).every(Boolean), true);
|
|
@@ -87,15 +88,15 @@ test('the publication model has one explicit rule for every value and property',
|
|
|
87
88
|
|
|
88
89
|
test('publication requires one current applied rail position and no unresolved send', () => {
|
|
89
90
|
const repoKey = 'root:currentness';
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
const project = require('../workspace/wall').projectForRepo(repoKey);
|
|
92
|
+
const resourceId = project.resourceId;
|
|
93
|
+
const file = project.file;
|
|
94
|
+
projectStream.writeSnapshot(file, projectStream._test.emptySnapshot(repoKey));
|
|
94
95
|
require('../state/replicas').registerReplica({
|
|
95
96
|
resourceId,
|
|
96
|
-
localResource:
|
|
97
|
+
localResource: `${projectStream.LOCAL_RESOURCE_PREFIX}${resourceId}`,
|
|
97
98
|
localPath: file,
|
|
98
|
-
contract:
|
|
99
|
+
contract: projectStream.STREAM_CONTRACT,
|
|
99
100
|
schemaVersion: 1,
|
|
100
101
|
authorityEpoch: 1,
|
|
101
102
|
appliedVersion: 12,
|
|
@@ -113,7 +114,14 @@ test('publication requires one current applied rail position and no unresolved s
|
|
|
113
114
|
pending: 0,
|
|
114
115
|
current: true,
|
|
115
116
|
});
|
|
116
|
-
|
|
117
|
+
projectStream.publish(project, {
|
|
118
|
+
wall: 'code-project@6',
|
|
119
|
+
project: repoKey,
|
|
120
|
+
machine: 'machine-a',
|
|
121
|
+
mutationId: 'mut_currentness_pending_01',
|
|
122
|
+
publishedAt: '2026-07-26T00:00:00.000Z',
|
|
123
|
+
cards: [],
|
|
124
|
+
});
|
|
117
125
|
assert.deepEqual(publicationStatus(repoKey, { appliedThrough: 12 }), {
|
|
118
126
|
receivedThrough: 12,
|
|
119
127
|
appliedThrough: 12,
|
|
@@ -195,10 +203,10 @@ test('receivers stay silent and only unacknowledged local parked work republishe
|
|
|
195
203
|
assert.deepEqual(
|
|
196
204
|
checkpointTest.localParkedEntries(
|
|
197
205
|
{ ...transition, parked: [localParked] },
|
|
198
|
-
{ parked: [peerParked, unsentParked] },
|
|
206
|
+
{ parked: [{ ...localParked, version: 10 }, peerParked, unsentParked] },
|
|
199
207
|
local,
|
|
200
208
|
),
|
|
201
|
-
[
|
|
209
|
+
[unsentParked],
|
|
202
210
|
);
|
|
203
211
|
assert.deepEqual(parkedForState([localParked], [], []), [localParked]);
|
|
204
212
|
assert.deepEqual(
|
|
@@ -254,59 +262,89 @@ test('card, branch, checkpoint, and parked work ride one wall mutation', () => {
|
|
|
254
262
|
assert.equal(stamped.checkpoints[0].parked[0].oid, parkedOid);
|
|
255
263
|
});
|
|
256
264
|
|
|
257
|
-
test('
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
'',
|
|
273
|
-
|
|
274
|
-
const compacted = compactWallText(baseText, repoKey);
|
|
275
|
-
const removedBytes = baseText.length - compacted.length;
|
|
276
|
-
assert.ok(removedBytes > 0);
|
|
265
|
+
test('a new card waits for its bundle instead of publishing transport metadata twice', () => {
|
|
266
|
+
const oldOid = 'a'.repeat(40);
|
|
267
|
+
const newOid = 'b'.repeat(40);
|
|
268
|
+
const plan = {
|
|
269
|
+
card: { head: { branch: 'main', oid: oldOid, detached: false } },
|
|
270
|
+
branches: [{ name: 'main', oid: oldOid }],
|
|
271
|
+
};
|
|
272
|
+
const pending = {
|
|
273
|
+
head: { branch: 'main', oid: newOid, detached: false },
|
|
274
|
+
objects: { kind: 'pending' },
|
|
275
|
+
refs: [{ name: 'main', oid: newOid, objects: { kind: 'pending' } }],
|
|
276
|
+
};
|
|
277
|
+
const ready = {
|
|
278
|
+
...pending,
|
|
279
|
+
objects: { kind: 'bundle' },
|
|
280
|
+
refs: pending.refs.map((ref) => ({ ...ref, objects: { kind: 'bundle' } })),
|
|
281
|
+
};
|
|
277
282
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
text.insert(text.length, `${line(machine, value)}\n`);
|
|
288
|
-
return Y.encodeStateAsUpdate(writer, baseVector);
|
|
283
|
+
assert.equal(wallTest.hasPendingContributionObjects(pending, plan), true);
|
|
284
|
+
assert.equal(wallTest.hasPendingContributionObjects(ready, plan), false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('an unnumbered branch waits for its bundle before sharing the card', () => {
|
|
288
|
+
const oid = 'b'.repeat(40);
|
|
289
|
+
const card = {
|
|
290
|
+
head: { branch: 'main', oid: 'a'.repeat(40) },
|
|
291
|
+
refs: [{ name: 'topic', oid, objects: { kind: 'pending' } }],
|
|
289
292
|
};
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
].
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
for (const update of order) Y.applyUpdate(replica, update);
|
|
299
|
-
return replica.getText('content').toString();
|
|
300
|
-
});
|
|
293
|
+
const plan = {
|
|
294
|
+
card: { head: { branch: 'main', oid: 'a'.repeat(40) } },
|
|
295
|
+
branches: [{ name: 'topic', oid, version: null }],
|
|
296
|
+
};
|
|
297
|
+
assert.equal(wallTest.hasPendingContributionObjects(card, plan), true);
|
|
298
|
+
plan.branches[0].version = 50;
|
|
299
|
+
assert.equal(wallTest.hasPendingContributionObjects(card, plan), false);
|
|
300
|
+
});
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
302
|
+
test('transport metadata without a real card, branch, or checkpoint change is silent', () => {
|
|
303
|
+
const previousMutation = 'mut_previous_content';
|
|
304
|
+
const metadataOnly = [{
|
|
305
|
+
positionMutationId: previousMutation,
|
|
306
|
+
branches: [{ name: 'main', mutationId: previousMutation }],
|
|
307
|
+
checkpoints: [{ mutationId: previousMutation }],
|
|
308
|
+
}];
|
|
309
|
+
assert.equal(wallTest.cardsUseMutation(metadataOnly, 'mut_new_transport_metadata'), false);
|
|
310
|
+
assert.equal(wallTest.cardsUseMutation([{
|
|
311
|
+
...metadataOnly[0],
|
|
312
|
+
checkpoints: [{ mutationId: 'mut_real_checkpoint' }],
|
|
313
|
+
}], 'mut_real_checkpoint'), true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('a snapshot is exactly one latest numbered record per machine', () => {
|
|
317
|
+
const project = 'root:snapshot';
|
|
318
|
+
const snapshot = projectStream._test.emptySnapshot(project);
|
|
319
|
+
const record = (machine, mutationId, value) => ({
|
|
320
|
+
wall: 'code-project@6',
|
|
321
|
+
project,
|
|
322
|
+
machine,
|
|
323
|
+
mutationId,
|
|
324
|
+
publishedAt: '2026-07-26T00:00:00.000Z',
|
|
325
|
+
cards: [{ value }],
|
|
311
326
|
});
|
|
327
|
+
const afterA = projectStream._test.applyRecord(
|
|
328
|
+
snapshot,
|
|
329
|
+
record('machine-a', 'mut_snapshot_machine_a1', 'old-a'),
|
|
330
|
+
1,
|
|
331
|
+
);
|
|
332
|
+
const afterB = projectStream._test.applyRecord(
|
|
333
|
+
afterA,
|
|
334
|
+
record('machine-b', 'mut_snapshot_machine_b1', 'current-b'),
|
|
335
|
+
2,
|
|
336
|
+
);
|
|
337
|
+
const compacted = projectStream._test.applyRecord(
|
|
338
|
+
afterB,
|
|
339
|
+
record('machine-a', 'mut_snapshot_machine_a2', 'current-a'),
|
|
340
|
+
3,
|
|
341
|
+
);
|
|
342
|
+
assert.equal(compacted.version, 3);
|
|
343
|
+
assert.deepEqual(
|
|
344
|
+
compacted.records.map((entry) => [entry.machine, entry.version, entry.cards[0].value]),
|
|
345
|
+
[
|
|
346
|
+
['machine-a', 3, 'current-a'],
|
|
347
|
+
['machine-b', 2, 'current-b'],
|
|
348
|
+
],
|
|
349
|
+
);
|
|
312
350
|
});
|
|
@@ -152,6 +152,50 @@ test('wall reduction chooses one rail winner and orders checkpoint transitions b
|
|
|
152
152
|
]);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
test('copies of one numbered card reduce to the copy with ready objects', () => {
|
|
156
|
+
const oid = 'a'.repeat(40);
|
|
157
|
+
const mutationId = 'mut_same_card_01';
|
|
158
|
+
const pending = card('/pending', oid, 7);
|
|
159
|
+
pending.replica = 'rpl_pending';
|
|
160
|
+
pending.positionMutationId = mutationId;
|
|
161
|
+
pending.objects = { kind: 'pending' };
|
|
162
|
+
pending.branches = [{
|
|
163
|
+
name: 'feature',
|
|
164
|
+
oid,
|
|
165
|
+
mutationId,
|
|
166
|
+
version: 7,
|
|
167
|
+
objects: { kind: 'pending' },
|
|
168
|
+
}];
|
|
169
|
+
|
|
170
|
+
const ready = card('/ready', oid, 7);
|
|
171
|
+
ready.replica = 'rpl_ready';
|
|
172
|
+
ready.positionMutationId = mutationId;
|
|
173
|
+
ready.objects = { kind: 'bundle', checksum: 'b'.repeat(64) };
|
|
174
|
+
ready.branches = [{
|
|
175
|
+
name: 'feature',
|
|
176
|
+
oid,
|
|
177
|
+
mutationId,
|
|
178
|
+
version: 7,
|
|
179
|
+
objects: { kind: 'bundle', checksum: 'b'.repeat(64) },
|
|
180
|
+
}];
|
|
181
|
+
|
|
182
|
+
for (const entries of [
|
|
183
|
+
[
|
|
184
|
+
{ machine: 'machine-pending', cards: [pending] },
|
|
185
|
+
{ machine: 'machine-ready', cards: [ready] },
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
{ machine: 'machine-ready', cards: [ready] },
|
|
189
|
+
{ machine: 'machine-pending', cards: [pending] },
|
|
190
|
+
],
|
|
191
|
+
]) {
|
|
192
|
+
const plan = reduceWall(entries).get('root:test');
|
|
193
|
+
assert.equal(plan.card.positionVersion, 7);
|
|
194
|
+
assert.equal(plan.card.objects.kind, 'bundle');
|
|
195
|
+
assert.equal(plan.branches.find((ref) => ref.name === 'feature').objects.kind, 'bundle');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
155
199
|
test('a checkpoint base replaces covered history and keeps only newer mutations', async () => {
|
|
156
200
|
const { repo, oid } = await makeRepo('checkpoint-base');
|
|
157
201
|
const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
|
|
@@ -511,8 +555,8 @@ test('a mode-only checkpoint makes the peer working file executable', async () =
|
|
|
511
555
|
assert.equal(await git(repo, 'diff', '--cached', '--binary', '--full-index'), '');
|
|
512
556
|
});
|
|
513
557
|
|
|
514
|
-
test('once local work is on the rail, a checkpoint
|
|
515
|
-
const { repo, oid } = await makeRepo('materialize-
|
|
558
|
+
test('once local work is on the rail, a later checkpoint replaces it exactly without echoing', async () => {
|
|
559
|
+
const { repo, oid } = await makeRepo('materialize-later-wins');
|
|
516
560
|
const localPatch = await patchFor(repo, { 'a.txt': 'local answer\n' });
|
|
517
561
|
const remotePatch = await patchFor(repo, { 'a.txt': 'remote answer\n' });
|
|
518
562
|
const localTransition = transition('machine-local', 2, oid, '', localPatch);
|
|
@@ -534,16 +578,44 @@ test('once local work is on the rail, a checkpoint conflict materializes exactly
|
|
|
534
578
|
}), true);
|
|
535
579
|
|
|
536
580
|
const outcome = await materializeRepo(plan.card, plan);
|
|
537
|
-
assert.equal(outcome.state, '
|
|
581
|
+
assert.equal(outcome.state, 'in-sync');
|
|
538
582
|
const first = fs.readFileSync(path.join(repo, 'a.txt'), 'utf8');
|
|
539
|
-
assert.
|
|
540
|
-
assert.match(first, /remote answer/);
|
|
541
|
-
assert.match(first, /<<<<<<< current/);
|
|
583
|
+
assert.equal(first, 'remote answer\n');
|
|
542
584
|
|
|
543
585
|
await materializeRepo(plan.card, plan);
|
|
544
586
|
assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), first);
|
|
545
587
|
});
|
|
546
588
|
|
|
589
|
+
test('a snapshot cannot hide that local work was already numbered', async () => {
|
|
590
|
+
const { repo, oid } = await makeRepo('materialize-snapshot-ack');
|
|
591
|
+
const localPatch = await patchFor(repo, { 'a.txt': 'local answer\n' });
|
|
592
|
+
const remotePatch = await patchFor(repo, { 'a.txt': 'remote answer\n' });
|
|
593
|
+
const localTransition = transition('machine-local', 2, oid, '', localPatch);
|
|
594
|
+
const plan = {
|
|
595
|
+
repoKey: 'root:test',
|
|
596
|
+
card: card(repo, oid, 1),
|
|
597
|
+
checkpointBase: {
|
|
598
|
+
through: 2,
|
|
599
|
+
baseOid: oid,
|
|
600
|
+
branch: 'main',
|
|
601
|
+
value: localTransition.to,
|
|
602
|
+
},
|
|
603
|
+
transitions: [transition('machine-remote', 3, oid, localPatch, remotePatch)],
|
|
604
|
+
};
|
|
605
|
+
const captured = {
|
|
606
|
+
baseOid: oid,
|
|
607
|
+
indexDiff: localPatch,
|
|
608
|
+
worktreeDiff: '',
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
assert.equal(captureIsOnRail(plan, captured, 'machine-local'), false);
|
|
612
|
+
assert.equal(captureIsOnRail(plan, captured, 'machine-local', {
|
|
613
|
+
...localTransition,
|
|
614
|
+
mutationId: 'mut_local_numbered',
|
|
615
|
+
version: 2,
|
|
616
|
+
}), true);
|
|
617
|
+
});
|
|
618
|
+
|
|
547
619
|
test('a writer publishing again cannot erase its earlier uncommitted work', async () => {
|
|
548
620
|
const { repo, oid } = await makeRepo('checkpoint-history');
|
|
549
621
|
const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
|
|
@@ -578,13 +650,12 @@ test('a writer publishing again cannot erase its earlier uncommitted work', asyn
|
|
|
578
650
|
assert.equal(fs.readFileSync(path.join(repo, 'c.txt'), 'utf8'), 'c1\n');
|
|
579
651
|
});
|
|
580
652
|
|
|
581
|
-
test('a losing commit is
|
|
653
|
+
test('a losing commit is archived, the winning card lands exactly, and later dirty work stays', async () => {
|
|
582
654
|
const { repo, oid: baseOid } = await makeRepo('simultaneous-commit');
|
|
583
655
|
fs.writeFileSync(path.join(repo, 'local-commit.txt'), 'losing commit work\n');
|
|
584
656
|
await git(repo, 'add', '.');
|
|
585
657
|
await git(repo, 'commit', '-m', 'local losing commit');
|
|
586
658
|
const losingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
587
|
-
fs.writeFileSync(path.join(repo, 'local-dirty.txt'), 'also uncommitted\n');
|
|
588
659
|
|
|
589
660
|
await git(repo, 'switch', '--detach', baseOid);
|
|
590
661
|
fs.writeFileSync(path.join(repo, 'winner.txt'), 'winning commit work\n');
|
|
@@ -592,6 +663,7 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
|
|
|
592
663
|
await git(repo, 'commit', '-m', 'remote winning commit');
|
|
593
664
|
const winningOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
594
665
|
await git(repo, 'switch', '-C', 'main', losingOid);
|
|
666
|
+
fs.writeFileSync(path.join(repo, 'local-dirty.txt'), 'also uncommitted\n');
|
|
595
667
|
|
|
596
668
|
const localCard = card(repo, losingOid, 1);
|
|
597
669
|
const winningCard = {
|
|
@@ -611,7 +683,7 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
|
|
|
611
683
|
assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
|
|
612
684
|
assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingOid}`)).trim(), losingOid);
|
|
613
685
|
assert.equal(fs.readFileSync(path.join(repo, 'winner.txt'), 'utf8'), 'winning commit work\n');
|
|
614
|
-
assert.equal(fs.
|
|
686
|
+
assert.equal(fs.existsSync(path.join(repo, 'local-commit.txt')), false);
|
|
615
687
|
assert.equal(fs.readFileSync(path.join(repo, 'local-dirty.txt'), 'utf8'), 'also uncommitted\n');
|
|
616
688
|
assert.equal((await git(repo, 'status', '--porcelain')).trim().length > 0, true);
|
|
617
689
|
assert.deepEqual(readRepoState('root:test').parked.map((entry) => entry.oid), [losingOid]);
|
|
@@ -626,26 +698,28 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
|
|
|
626
698
|
transitions: [],
|
|
627
699
|
parked: sharedParked,
|
|
628
700
|
});
|
|
701
|
+
assert.equal(fs.existsSync(path.join(repo, 'local-commit.txt')), false);
|
|
702
|
+
assert.equal(fs.readFileSync(path.join(repo, 'local-dirty.txt'), 'utf8'), 'also uncommitted\n');
|
|
629
703
|
|
|
630
704
|
await git(repo, 'add', '.');
|
|
631
|
-
await git(repo, 'commit', '-m', '
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
const
|
|
705
|
+
await git(repo, 'commit', '-m', 'commit later dirty work');
|
|
706
|
+
const nextOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
707
|
+
const nextCard = card(repo, nextOid, 3);
|
|
708
|
+
const advanced = await materializeRepo(nextCard, {
|
|
635
709
|
repoKey: 'root:test',
|
|
636
|
-
card:
|
|
710
|
+
card: nextCard,
|
|
637
711
|
transitions: [],
|
|
638
712
|
parked: [],
|
|
639
713
|
});
|
|
640
714
|
|
|
641
|
-
assert.equal(
|
|
715
|
+
assert.equal(advanced.state, 'in-sync');
|
|
642
716
|
assert.deepEqual(readRepoState('root:test').parked, []);
|
|
643
717
|
await assert.rejects(
|
|
644
718
|
git(repo, 'rev-parse', '--verify', `refs/amalgm/parked/${losingOid}`),
|
|
645
719
|
);
|
|
646
720
|
});
|
|
647
721
|
|
|
648
|
-
test('rail-parked peer commits
|
|
722
|
+
test('rail-parked peer commits are archived without changing the winning card tree', async () => {
|
|
649
723
|
const { repo, oid: baseOid } = await makeRepo('peer-parked-commits');
|
|
650
724
|
|
|
651
725
|
await git(repo, 'switch', '-c', 'winner');
|
|
@@ -680,13 +754,61 @@ test('rail-parked peer commits become one staged checkpoint on every machine', a
|
|
|
680
754
|
|
|
681
755
|
assert.equal(outcome.state, 'in-sync');
|
|
682
756
|
assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
|
|
757
|
+
assert.equal((await git(repo, 'diff', '--cached', '--name-only')).trim(), '');
|
|
758
|
+
assert.equal((await git(repo, 'diff', '--name-only')).trim(), '');
|
|
759
|
+
assert.equal(fs.existsSync(path.join(repo, 'loser-a.txt')), false);
|
|
760
|
+
assert.equal(fs.existsSync(path.join(repo, 'loser-b.txt')), false);
|
|
761
|
+
assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingAOid}`)).trim(), losingAOid);
|
|
762
|
+
assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingBOid}`)).trim(), losingBOid);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test('a local losing commit stays pending while a peer parked bundle is missing', async () => {
|
|
766
|
+
const { repo, oid: baseOid } = await makeRepo('parked-while-peer-bundle-waits');
|
|
767
|
+
|
|
768
|
+
await git(repo, 'switch', '-c', 'winner');
|
|
769
|
+
fs.writeFileSync(path.join(repo, 'winner.txt'), 'winning card\n');
|
|
770
|
+
await git(repo, 'add', '.');
|
|
771
|
+
await git(repo, 'commit', '-m', 'winning card');
|
|
772
|
+
const winningOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
773
|
+
|
|
774
|
+
await git(repo, 'switch', '-C', 'main', baseOid);
|
|
775
|
+
fs.writeFileSync(path.join(repo, 'local-loser.txt'), 'local losing commit\n');
|
|
776
|
+
await git(repo, 'add', '.');
|
|
777
|
+
await git(repo, 'commit', '-m', 'local losing commit');
|
|
778
|
+
const losingOid = (await git(repo, 'rev-parse', 'HEAD')).trim();
|
|
779
|
+
|
|
780
|
+
const localCard = card(repo, losingOid, 1);
|
|
781
|
+
const winningCard = {
|
|
782
|
+
...localCard,
|
|
783
|
+
head: { branch: 'main', oid: winningOid, detached: false },
|
|
784
|
+
positionVersion: 2,
|
|
785
|
+
positionMutationId: 'mut_card_winner_waiting',
|
|
786
|
+
};
|
|
787
|
+
const peerOid = 'f'.repeat(40);
|
|
788
|
+
const plan = {
|
|
789
|
+
repoKey: 'root:test',
|
|
790
|
+
card: winningCard,
|
|
791
|
+
transitions: [],
|
|
792
|
+
parked: [{ oid: peerOid, version: 3 }],
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const first = await materializeRepo(localCard, plan, 3);
|
|
796
|
+
assert.equal(first.state, 'waiting');
|
|
797
|
+
assert.equal(first.reason, 'parked-objects-missing');
|
|
798
|
+
assert.equal((await git(repo, 'rev-parse', 'HEAD')).trim(), winningOid);
|
|
799
|
+
assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingOid}`)).trim(), losingOid);
|
|
683
800
|
assert.deepEqual(
|
|
684
|
-
(
|
|
685
|
-
[
|
|
801
|
+
readRepoState('root:test').parked.map((entry) => entry.oid).sort(),
|
|
802
|
+
[losingOid, peerOid].sort(),
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
const second = await materializeRepo(card(repo, winningOid, 2), plan, 3);
|
|
806
|
+
assert.equal(second.state, 'waiting');
|
|
807
|
+
assert.equal((await git(repo, 'rev-parse', `refs/amalgm/parked/${losingOid}`)).trim(), losingOid);
|
|
808
|
+
assert.deepEqual(
|
|
809
|
+
readRepoState('root:test').parked.map((entry) => entry.oid).sort(),
|
|
810
|
+
[losingOid, peerOid].sort(),
|
|
686
811
|
);
|
|
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');
|
|
690
812
|
});
|
|
691
813
|
|
|
692
814
|
test('a local commit made after the verified card snapshot is never parked or overwritten', async () => {
|
|
@@ -800,7 +922,7 @@ test('an index advance preserves unrelated work staged by another writer', async
|
|
|
800
922
|
assert.deepEqual(staged.trim().split('\n').sort(), ['a.txt', 'b.txt']);
|
|
801
923
|
});
|
|
802
924
|
|
|
803
|
-
test('a new card
|
|
925
|
+
test('a new card includes checkpoint work already on the rail instead of replaying it as local work', async () => {
|
|
804
926
|
const { repo, oid: baseOid } = await makeRepo('card-folds-shared-checkpoint');
|
|
805
927
|
const sharedPatch = await patchFor(repo, { 'a.txt': 'shared checkpoint\n' });
|
|
806
928
|
|
|
@@ -835,7 +957,7 @@ test('a new card folds checkpoint work already on the rail instead of replaying
|
|
|
835
957
|
assert.equal((await git(repo, 'status', '--porcelain')).trim(), '');
|
|
836
958
|
});
|
|
837
959
|
|
|
838
|
-
test('checkpoint then commit
|
|
960
|
+
test('checkpoint then commit advances through the same card movement and unrelated local work stays', async () => {
|
|
839
961
|
const { repo, oid: baseOid } = await makeRepo('checkpoint-commit-fold');
|
|
840
962
|
await git(repo, 'switch', '-c', 'target');
|
|
841
963
|
fs.writeFileSync(path.join(repo, 'shared.txt'), 'already checkpointed\n');
|
|
@@ -903,6 +1025,44 @@ test('the materializer creates, moves, and deletes branch refs without checking
|
|
|
903
1025
|
assert.equal((await git(repo, 'branch', '--show-current')).trim(), 'main');
|
|
904
1026
|
});
|
|
905
1027
|
|
|
1028
|
+
test('a branch event is not applied until its commit is available', async () => {
|
|
1029
|
+
const { repo, oid } = await makeRepo('branch-event-waits');
|
|
1030
|
+
const peer = path.join(tempRoot, 'branch-event-waits-peer');
|
|
1031
|
+
await git(tempRoot, 'clone', '--quiet', repo, peer);
|
|
1032
|
+
await git(peer, 'switch', '-c', 'feature');
|
|
1033
|
+
fs.writeFileSync(path.join(peer, 'feature.txt'), 'feature\n');
|
|
1034
|
+
await git(peer, 'add', '.');
|
|
1035
|
+
await git(peer, 'commit', '-m', 'feature');
|
|
1036
|
+
const featureOid = (await git(peer, 'rev-parse', 'HEAD')).trim();
|
|
1037
|
+
|
|
1038
|
+
const repoKey = 'root:branch-event-waits';
|
|
1039
|
+
const localCard = card(repo, oid, 1);
|
|
1040
|
+
const plan = {
|
|
1041
|
+
repoKey,
|
|
1042
|
+
card: localCard,
|
|
1043
|
+
transitions: [],
|
|
1044
|
+
parked: [],
|
|
1045
|
+
branches: [
|
|
1046
|
+
{ name: 'main', oid, version: 1 },
|
|
1047
|
+
{ name: 'feature', oid: featureOid, version: 2 },
|
|
1048
|
+
],
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
const waiting = await materializeRepo(localCard, plan, 2);
|
|
1052
|
+
assert.deepEqual(waiting, {
|
|
1053
|
+
state: 'waiting',
|
|
1054
|
+
reason: 'branch-objects-missing',
|
|
1055
|
+
});
|
|
1056
|
+
assert.equal(readRepoState(repoKey), null);
|
|
1057
|
+
await assert.rejects(() => git(repo, 'rev-parse', 'refs/heads/feature'));
|
|
1058
|
+
|
|
1059
|
+
await git(repo, 'fetch', '--quiet', peer, 'feature:refs/amalgm-test/feature');
|
|
1060
|
+
const applied = await materializeRepo(localCard, plan, 2);
|
|
1061
|
+
assert.equal(applied.state, 'in-sync');
|
|
1062
|
+
assert.equal((await git(repo, 'rev-parse', 'refs/heads/feature')).trim(), featureOid);
|
|
1063
|
+
assert.equal(readRepoState(repoKey).appliedThrough, 2);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
906
1066
|
test('an unpublished local branch delete is not restored while catching up', async () => {
|
|
907
1067
|
const { repo, oid } = await makeRepo('pending-branch-delete');
|
|
908
1068
|
await git(repo, 'switch', '-c', 'feature');
|