amalgm 0.1.215 → 0.1.216
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/tests/workspace-publication-invariants.test.js +270 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +3 -1
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +6 -1
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +15 -0
package/package.json
CHANGED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-publication-test-'));
|
|
10
|
+
process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
|
|
11
|
+
process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
|
|
12
|
+
process.env.AMALGM_COMPUTER_ID = 'machine-a';
|
|
13
|
+
process.env.AMALGM_DEVICE_ID = 'device-a';
|
|
14
|
+
fs.mkdirSync(process.env.AMALGM_DIR, { recursive: true });
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(process.env.AMALGM_DIR, 'computer.json'),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
computer_id: 'machine-a',
|
|
19
|
+
user_id: '11111111-1111-4111-8111-111111111111',
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const { closeLocalDb } = require('../state/db');
|
|
24
|
+
const {
|
|
25
|
+
_test: checkpointTest,
|
|
26
|
+
CHECKPOINT_CONTRACT,
|
|
27
|
+
checkpointFile,
|
|
28
|
+
sameTransition,
|
|
29
|
+
} = require('../workspace/checkpoint');
|
|
30
|
+
const { parkedForState } = require('../workspace/reducer');
|
|
31
|
+
const { compactWallText, hasLocalContribution, stampCards } = require('../workspace/wall');
|
|
32
|
+
|
|
33
|
+
test.after(() => {
|
|
34
|
+
closeLocalDb();
|
|
35
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const rules = {
|
|
39
|
+
card: {
|
|
40
|
+
identity: 'branch-and-commit-content',
|
|
41
|
+
ownership: 'originator-publishes',
|
|
42
|
+
liveness: 'shared-wall-outbox',
|
|
43
|
+
gating: 'defines-winning-card',
|
|
44
|
+
},
|
|
45
|
+
branch: {
|
|
46
|
+
identity: 'ref-name-and-commit-content',
|
|
47
|
+
ownership: 'originator-publishes',
|
|
48
|
+
liveness: 'shared-wall-outbox',
|
|
49
|
+
gating: 'per-ref-rail-order',
|
|
50
|
+
},
|
|
51
|
+
checkpoint: {
|
|
52
|
+
identity: 'from-and-to-content',
|
|
53
|
+
ownership: 'originator-publishes',
|
|
54
|
+
liveness: 'shared-wall-outbox',
|
|
55
|
+
gating: 'winning-card-only',
|
|
56
|
+
},
|
|
57
|
+
parked: {
|
|
58
|
+
identity: 'commit-and-object-content',
|
|
59
|
+
ownership: 'holder-publishes-until-acknowledged',
|
|
60
|
+
liveness: 'shared-wall-outbox',
|
|
61
|
+
gating: 'inside-winning-card-checkpoint',
|
|
62
|
+
},
|
|
63
|
+
compaction: {
|
|
64
|
+
identity: 'derived-deletion',
|
|
65
|
+
ownership: 'any-machine-may-compact',
|
|
66
|
+
liveness: 'not-semantic-state',
|
|
67
|
+
gating: 'commutative-with-writers',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
test('the publication model has one explicit rule for every value and property', () => {
|
|
72
|
+
assert.deepEqual(Object.keys(rules), ['card', 'branch', 'checkpoint', 'parked', 'compaction']);
|
|
73
|
+
for (const rule of Object.values(rules)) {
|
|
74
|
+
assert.deepEqual(Object.keys(rule), ['identity', 'ownership', 'liveness', 'gating']);
|
|
75
|
+
assert.equal(Object.values(rule).every(Boolean), true);
|
|
76
|
+
}
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
new Set(['card', 'branch', 'checkpoint', 'parked'].map((name) => rules[name].liveness)),
|
|
79
|
+
new Set(['shared-wall-outbox']),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('server rail numbers do not change checkpoint or parked identity', () => {
|
|
84
|
+
const oid = 'a'.repeat(40);
|
|
85
|
+
const transition = {
|
|
86
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
87
|
+
machine: 'machine-a',
|
|
88
|
+
repoKey: 'root:test',
|
|
89
|
+
branch: 'main',
|
|
90
|
+
baseOid: oid,
|
|
91
|
+
from: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
92
|
+
to: { revision: 'work-a', indexDiff: '', worktreeDiff: 'patch-a' },
|
|
93
|
+
parked: [{
|
|
94
|
+
oid: 'b'.repeat(40),
|
|
95
|
+
ref: `refs/amalgm/parked/${'b'.repeat(40)}`,
|
|
96
|
+
objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
|
|
97
|
+
}],
|
|
98
|
+
};
|
|
99
|
+
assert.equal(sameTransition(transition, {
|
|
100
|
+
...transition,
|
|
101
|
+
mutationId: 'mut_server_metadata',
|
|
102
|
+
version: 41,
|
|
103
|
+
parked: transition.parked.map((entry) => ({ ...entry, version: 41 })),
|
|
104
|
+
}), true);
|
|
105
|
+
assert.equal(sameTransition(transition, {
|
|
106
|
+
...transition,
|
|
107
|
+
to: { ...transition.to, revision: 'different-work' },
|
|
108
|
+
}), false);
|
|
109
|
+
assert.equal(sameTransition(transition, {
|
|
110
|
+
...transition,
|
|
111
|
+
parked: transition.parked.map((entry) => ({
|
|
112
|
+
...entry,
|
|
113
|
+
objects: { ...entry.objects, checksum: 'd'.repeat(64) },
|
|
114
|
+
})),
|
|
115
|
+
}), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('receivers stay silent and only unacknowledged local parked work republishes', () => {
|
|
119
|
+
const head = 'a'.repeat(40);
|
|
120
|
+
const local = {
|
|
121
|
+
head: { branch: 'main', oid: head, detached: false },
|
|
122
|
+
refs: [{ name: 'main', oid: head }],
|
|
123
|
+
};
|
|
124
|
+
const transition = {
|
|
125
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
126
|
+
machine: 'machine-a',
|
|
127
|
+
repoKey: 'root:test',
|
|
128
|
+
branch: 'main',
|
|
129
|
+
baseOid: head,
|
|
130
|
+
from: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
131
|
+
to: { revision: 'shared-work', indexDiff: '', worktreeDiff: 'patch' },
|
|
132
|
+
parked: [],
|
|
133
|
+
};
|
|
134
|
+
const plan = {
|
|
135
|
+
card: { head: { branch: 'main', oid: head, detached: false } },
|
|
136
|
+
branches: [{ name: 'main', oid: head }],
|
|
137
|
+
transitions: [{ ...transition, version: 9 }],
|
|
138
|
+
};
|
|
139
|
+
const baseline = {
|
|
140
|
+
branch: 'main',
|
|
141
|
+
baseOid: head,
|
|
142
|
+
branches: [{ name: 'main', oid: head }],
|
|
143
|
+
};
|
|
144
|
+
assert.equal(hasLocalContribution(local, plan, baseline, transition), false);
|
|
145
|
+
|
|
146
|
+
const localParked = { oid: 'b'.repeat(40), objects: { kind: 'bundle', checksum: 'local' } };
|
|
147
|
+
const unsentParked = { oid: 'c'.repeat(40), objects: { kind: 'bundle', checksum: 'unsent' } };
|
|
148
|
+
const peerParked = {
|
|
149
|
+
oid: 'd'.repeat(40),
|
|
150
|
+
objects: { kind: 'bundle', checksum: 'peer' },
|
|
151
|
+
version: 9,
|
|
152
|
+
};
|
|
153
|
+
assert.deepEqual(
|
|
154
|
+
checkpointTest.localParkedEntries(
|
|
155
|
+
{ ...transition, parked: [localParked] },
|
|
156
|
+
{ parked: [peerParked, unsentParked] },
|
|
157
|
+
local,
|
|
158
|
+
),
|
|
159
|
+
[localParked, unsentParked],
|
|
160
|
+
);
|
|
161
|
+
assert.deepEqual(parkedForState([localParked], [], []), [localParked]);
|
|
162
|
+
assert.deepEqual(
|
|
163
|
+
parkedForState([localParked], [{ ...localParked, version: 10 }], []),
|
|
164
|
+
[{ ...localParked, version: 10 }],
|
|
165
|
+
);
|
|
166
|
+
assert.deepEqual(parkedForState([{ ...localParked, version: 10 }], [], []), []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('card, branch, checkpoint, and parked work ride one wall mutation', () => {
|
|
170
|
+
const repoKey = 'https://github.com/amalgm-inc/publication-invariant-test';
|
|
171
|
+
const head = 'a'.repeat(40);
|
|
172
|
+
const parkedOid = 'b'.repeat(40);
|
|
173
|
+
const checkpoint = {
|
|
174
|
+
checkpoint: CHECKPOINT_CONTRACT,
|
|
175
|
+
machine: 'machine-a',
|
|
176
|
+
repoKey,
|
|
177
|
+
branch: 'main',
|
|
178
|
+
baseOid: head,
|
|
179
|
+
from: { revision: null, indexDiff: '', worktreeDiff: '' },
|
|
180
|
+
to: { revision: 'work-a', indexDiff: '', worktreeDiff: 'patch-a' },
|
|
181
|
+
parked: [{
|
|
182
|
+
oid: parkedOid,
|
|
183
|
+
ref: `refs/amalgm/parked/${parkedOid}`,
|
|
184
|
+
objects: { kind: 'bundle', checksum: 'c'.repeat(64) },
|
|
185
|
+
}],
|
|
186
|
+
};
|
|
187
|
+
const file = checkpointFile('machine-a', repoKey);
|
|
188
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
189
|
+
fs.writeFileSync(file, `${JSON.stringify(checkpoint)}\n`);
|
|
190
|
+
|
|
191
|
+
const mutationId = 'mut_one_shared_wall_outbox';
|
|
192
|
+
const stamped = stampCards([{
|
|
193
|
+
card: 'repo-card@2',
|
|
194
|
+
machine: 'machine-a',
|
|
195
|
+
replica: 'rpl_publication_test',
|
|
196
|
+
kind: 'primary',
|
|
197
|
+
repo: { remote: repoKey },
|
|
198
|
+
head: { branch: 'main', oid: head, detached: false },
|
|
199
|
+
refs: [
|
|
200
|
+
{ name: 'main', oid: head },
|
|
201
|
+
{ name: 'feature', oid: head },
|
|
202
|
+
],
|
|
203
|
+
health: 'healthy',
|
|
204
|
+
}], [], null, null, mutationId, {
|
|
205
|
+
repoKey,
|
|
206
|
+
resourceId: 'shr_publication_invariant_test',
|
|
207
|
+
})[0];
|
|
208
|
+
|
|
209
|
+
assert.equal(stamped.positionMutationId, mutationId);
|
|
210
|
+
assert.equal(stamped.branches.every((ref) => ref.mutationId === mutationId), true);
|
|
211
|
+
assert.equal(stamped.checkpoints[0].mutationId, mutationId);
|
|
212
|
+
assert.equal(stamped.checkpoints[0].parked[0].oid, parkedOid);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('two concurrent compacting publishers converge without losing either latest value', () => {
|
|
216
|
+
const Y = require('yjs');
|
|
217
|
+
const repoKey = 'root:compaction-collision';
|
|
218
|
+
const line = (machine, value) => JSON.stringify({
|
|
219
|
+
wall: 'code-project@3',
|
|
220
|
+
project: repoKey,
|
|
221
|
+
machine,
|
|
222
|
+
mutationId: `mut_${machine}_${value}`,
|
|
223
|
+
cards: [{ value }],
|
|
224
|
+
});
|
|
225
|
+
const baseText = [
|
|
226
|
+
line('machine-a', 'old-a'),
|
|
227
|
+
line('machine-b', 'old-b'),
|
|
228
|
+
line('machine-a', 'current-a'),
|
|
229
|
+
line('machine-b', 'current-b'),
|
|
230
|
+
'',
|
|
231
|
+
].join('\n');
|
|
232
|
+
const compacted = compactWallText(baseText, repoKey);
|
|
233
|
+
const removedBytes = baseText.length - compacted.length;
|
|
234
|
+
assert.ok(removedBytes > 0);
|
|
235
|
+
|
|
236
|
+
const base = new Y.Doc();
|
|
237
|
+
base.getText('content').insert(0, baseText);
|
|
238
|
+
const baseUpdate = Y.encodeStateAsUpdate(base);
|
|
239
|
+
const baseVector = Y.encodeStateVector(base);
|
|
240
|
+
const publisherUpdate = (machine, value) => {
|
|
241
|
+
const writer = new Y.Doc();
|
|
242
|
+
Y.applyUpdate(writer, baseUpdate);
|
|
243
|
+
const text = writer.getText('content');
|
|
244
|
+
text.delete(0, removedBytes);
|
|
245
|
+
text.insert(text.length, `${line(machine, value)}\n`);
|
|
246
|
+
return Y.encodeStateAsUpdate(writer, baseVector);
|
|
247
|
+
};
|
|
248
|
+
const updateA = publisherUpdate('machine-a', 'latest-a');
|
|
249
|
+
const updateB = publisherUpdate('machine-b', 'latest-b');
|
|
250
|
+
const replicas = [
|
|
251
|
+
[updateA, updateB],
|
|
252
|
+
[updateB, updateA],
|
|
253
|
+
].map((order) => {
|
|
254
|
+
const replica = new Y.Doc();
|
|
255
|
+
Y.applyUpdate(replica, baseUpdate);
|
|
256
|
+
for (const update of order) Y.applyUpdate(replica, update);
|
|
257
|
+
return replica.getText('content').toString();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
assert.equal(replicas[0], replicas[1]);
|
|
261
|
+
const latest = new Map();
|
|
262
|
+
for (const raw of replicas[0].trim().split('\n')) {
|
|
263
|
+
const entry = JSON.parse(raw);
|
|
264
|
+
latest.set(entry.machine, entry.cards[0].value);
|
|
265
|
+
}
|
|
266
|
+
assert.deepEqual(Object.fromEntries(latest), {
|
|
267
|
+
'machine-a': 'latest-a',
|
|
268
|
+
'machine-b': 'latest-b',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -544,7 +544,9 @@ test('a losing commit is parked, the winning card lands exactly, and losing work
|
|
|
544
544
|
|
|
545
545
|
assert.equal(folded.state, 'in-sync');
|
|
546
546
|
assert.deepEqual(readRepoState('root:test').parked, []);
|
|
547
|
-
|
|
547
|
+
await assert.rejects(
|
|
548
|
+
git(repo, 'rev-parse', '--verify', `refs/amalgm/parked/${losingOid}`),
|
|
549
|
+
);
|
|
548
550
|
});
|
|
549
551
|
|
|
550
552
|
test('rail-parked peer commits become one staged checkpoint on every machine', async () => {
|
|
@@ -360,7 +360,8 @@ async function moveToWinningCard(localCard, targetCard) {
|
|
|
360
360
|
|
|
361
361
|
async function materializeParked(localCard, entries) {
|
|
362
362
|
const waiting = [];
|
|
363
|
-
|
|
363
|
+
const active = uniqueParked(entries);
|
|
364
|
+
for (const entry of active) {
|
|
364
365
|
if (!(await hasCommit(localCard.path, entry.oid))) {
|
|
365
366
|
if (entry.objects?.kind === 'bundle') {
|
|
366
367
|
const imported = await require('./objects').importFromLocator(localCard.path, entry.objects);
|
|
@@ -375,6 +376,10 @@ async function materializeParked(localCard, entries) {
|
|
|
375
376
|
}
|
|
376
377
|
await transition.parkCommit(localCard.path, entry.oid);
|
|
377
378
|
}
|
|
379
|
+
await transition.pruneParkedCommits(
|
|
380
|
+
localCard.path,
|
|
381
|
+
new Set(active.map((entry) => entry.oid)),
|
|
382
|
+
);
|
|
378
383
|
return waiting;
|
|
379
384
|
}
|
|
380
385
|
|
|
@@ -236,6 +236,20 @@ function parkCommit(worktreeRoot, oid) {
|
|
|
236
236
|
]).then(() => ref);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/** Delete internal parked refs that are no longer part of shared checkpoint state. */
|
|
240
|
+
async function pruneParkedCommits(worktreeRoot, keepOids) {
|
|
241
|
+
const output = await run('list-parked-commits', worktreeRoot, [
|
|
242
|
+
'-C', worktreeRoot, 'for-each-ref', '--format=%(refname)', 'refs/amalgm/parked',
|
|
243
|
+
], { journalResult: false });
|
|
244
|
+
const prefix = 'refs/amalgm/parked/';
|
|
245
|
+
for (const ref of output.trim().split('\n').filter(Boolean)) {
|
|
246
|
+
if (!ref.startsWith(prefix) || keepOids.has(ref.slice(prefix.length))) continue;
|
|
247
|
+
await run('remove-parked-commit', worktreeRoot, [
|
|
248
|
+
'-C', worktreeRoot, 'update-ref', '-d', ref,
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
239
253
|
function setBranchRef(worktreeRoot, branch, oid) {
|
|
240
254
|
return run('set-branch-ref', worktreeRoot, [
|
|
241
255
|
'-C', worktreeRoot, 'update-ref', `refs/heads/${branch}`, oid,
|
|
@@ -380,6 +394,7 @@ module.exports = {
|
|
|
380
394
|
initializeRepo,
|
|
381
395
|
moveToCard,
|
|
382
396
|
parkCommit,
|
|
397
|
+
pruneParkedCommits,
|
|
383
398
|
restoreInitialFiles,
|
|
384
399
|
setBranchRef,
|
|
385
400
|
setIndexTree,
|