amalgm 0.1.239 → 0.1.241
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/lib/shared-realtime-tunnel.js +18 -4
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +13 -0
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +98 -4
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +156 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +185 -55
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
|
@@ -25,16 +25,38 @@ fs.writeFileSync(
|
|
|
25
25
|
const { closeLocalDb } = require('../state/db');
|
|
26
26
|
const { hasCommit, headOid } = require('../workspace/git');
|
|
27
27
|
const {
|
|
28
|
+
_test: { locatorChecksum },
|
|
28
29
|
acknowledgeUpload,
|
|
29
30
|
claimDownload,
|
|
30
31
|
claimUpload,
|
|
32
|
+
failUpload,
|
|
31
33
|
importFromLocator,
|
|
32
34
|
objectSourceFor,
|
|
35
|
+
prepareBundleImport,
|
|
33
36
|
publishPayload,
|
|
34
37
|
readPayload,
|
|
35
38
|
receiveDownload,
|
|
36
39
|
} = require('../workspace/objects');
|
|
37
40
|
|
|
41
|
+
const PROJECT = '018f0000-0000-7000-8000-000000000030';
|
|
42
|
+
const PROJECT_BINDING = require('../workspace/wall').bindProject(
|
|
43
|
+
PROJECT,
|
|
44
|
+
'11111111-1111-4111-8111-111111111111',
|
|
45
|
+
tempRoot,
|
|
46
|
+
);
|
|
47
|
+
const REPO_KEY = PROJECT_BINDING.repoKey;
|
|
48
|
+
|
|
49
|
+
test('a locator checksum binds the exact ordered chunk list', () => {
|
|
50
|
+
const first = { sha256: 'a'.repeat(64), bytes: 3 };
|
|
51
|
+
const second = { sha256: 'b'.repeat(64), bytes: 4 };
|
|
52
|
+
assert.equal(locatorChecksum([first, second]), locatorChecksum([first, second]));
|
|
53
|
+
assert.notEqual(locatorChecksum([first, second]), locatorChecksum([second, first]));
|
|
54
|
+
assert.notEqual(
|
|
55
|
+
locatorChecksum([first, second]),
|
|
56
|
+
locatorChecksum([first, { ...second, bytes: 5 }]),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
38
60
|
test.after(() => {
|
|
39
61
|
closeLocalDb();
|
|
40
62
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
@@ -49,7 +71,7 @@ async function git(cwd, ...args) {
|
|
|
49
71
|
return stdout;
|
|
50
72
|
}
|
|
51
73
|
|
|
52
|
-
test('
|
|
74
|
+
test('every committed card travels as project-owned chunks and imports without moving a ref', async () => {
|
|
53
75
|
const origin = path.join(tempRoot, 'origin.git');
|
|
54
76
|
await git(tempRoot, 'init', '--bare', '-b', 'main', origin);
|
|
55
77
|
const source = path.join(tempRoot, 'source');
|
|
@@ -61,12 +83,12 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
|
|
|
61
83
|
|
|
62
84
|
const destination = path.join(tempRoot, 'destination');
|
|
63
85
|
await git(tempRoot, 'clone', '-q', origin, destination);
|
|
64
|
-
assert.equal((await objectSourceFor(source,
|
|
86
|
+
assert.equal((await objectSourceFor(source, REPO_KEY, await headOid(source))).kind, 'pending');
|
|
65
87
|
|
|
66
88
|
fs.appendFileSync(path.join(source, 'app.js'), 'two\n');
|
|
67
89
|
await git(source, 'commit', '-am', 'two');
|
|
68
90
|
const unpushedOid = await headOid(source);
|
|
69
|
-
assert.equal((await objectSourceFor(source,
|
|
91
|
+
assert.equal((await objectSourceFor(source, REPO_KEY, unpushedOid)).kind, 'pending');
|
|
70
92
|
|
|
71
93
|
const uploaded = new Map();
|
|
72
94
|
const deadline = Date.now() + 10_000;
|
|
@@ -78,10 +100,10 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
|
|
|
78
100
|
}
|
|
79
101
|
uploaded.set(upload.sha256, upload.dataBase64);
|
|
80
102
|
acknowledgeUpload(upload.resourceId, upload.sha256);
|
|
81
|
-
const sourceState = await objectSourceFor(source,
|
|
103
|
+
const sourceState = await objectSourceFor(source, REPO_KEY, unpushedOid);
|
|
82
104
|
if (sourceState.kind === 'bundle') break;
|
|
83
105
|
}
|
|
84
|
-
const locator = await objectSourceFor(source,
|
|
106
|
+
const locator = await objectSourceFor(source, REPO_KEY, unpushedOid);
|
|
85
107
|
assert.equal(locator.kind, 'bundle');
|
|
86
108
|
assert.ok(locator.chunks.length > 0);
|
|
87
109
|
|
|
@@ -103,7 +125,7 @@ test('an unpushed commit travels as card-owned chunks and imports without moving
|
|
|
103
125
|
|
|
104
126
|
test('a large checkpoint uses the same verified chunk lane', () => {
|
|
105
127
|
const content = Buffer.alloc(1_300_000, 0x61);
|
|
106
|
-
assert.equal(publishPayload('large-checkpoint', content,
|
|
128
|
+
assert.equal(publishPayload('large-checkpoint', content, REPO_KEY).kind, 'pending');
|
|
107
129
|
|
|
108
130
|
const uploaded = new Map();
|
|
109
131
|
while (true) {
|
|
@@ -112,7 +134,7 @@ test('a large checkpoint uses the same verified chunk lane', () => {
|
|
|
112
134
|
uploaded.set(upload.sha256, upload.dataBase64);
|
|
113
135
|
acknowledgeUpload(upload.resourceId, upload.sha256);
|
|
114
136
|
}
|
|
115
|
-
const locator = publishPayload('large-checkpoint', content,
|
|
137
|
+
const locator = publishPayload('large-checkpoint', content, REPO_KEY);
|
|
116
138
|
assert.equal(locator.kind, 'chunks');
|
|
117
139
|
assert.ok(locator.chunks.length >= 3);
|
|
118
140
|
|
|
@@ -130,9 +152,135 @@ test('a large checkpoint uses the same verified chunk lane', () => {
|
|
|
130
152
|
assert.equal(received.state, 'ready');
|
|
131
153
|
assert.deepEqual(received.bytes, content);
|
|
132
154
|
assert.equal(
|
|
133
|
-
publishPayload('same-content-after-download', content,
|
|
155
|
+
publishPayload('same-content-after-download', content, REPO_KEY).kind,
|
|
134
156
|
'chunks',
|
|
135
157
|
'verified downloaded content is immediately reusable by a later checkpoint',
|
|
136
158
|
);
|
|
137
159
|
assert.equal(claimUpload(), null);
|
|
138
160
|
});
|
|
161
|
+
|
|
162
|
+
test('zero-byte chunks are not legal transport work', () => {
|
|
163
|
+
const chunks = [
|
|
164
|
+
{ sha256: '0'.repeat(64), bytes: 0 },
|
|
165
|
+
{ sha256: '1'.repeat(64), bytes: 1 },
|
|
166
|
+
];
|
|
167
|
+
const common = {
|
|
168
|
+
resourceId: PROJECT,
|
|
169
|
+
checksum: locatorChecksum(chunks),
|
|
170
|
+
bytes: 1,
|
|
171
|
+
chunks,
|
|
172
|
+
};
|
|
173
|
+
assert.throws(
|
|
174
|
+
() => readPayload({
|
|
175
|
+
kind: 'chunks',
|
|
176
|
+
contract: 'opaque-chunks@2',
|
|
177
|
+
...common,
|
|
178
|
+
}),
|
|
179
|
+
/invalid checkpoint payload locator/,
|
|
180
|
+
);
|
|
181
|
+
assert.throws(
|
|
182
|
+
() => prepareBundleImport({
|
|
183
|
+
kind: 'bundle',
|
|
184
|
+
contract: 'git-bundle-chunks@2',
|
|
185
|
+
oid: 'a'.repeat(40),
|
|
186
|
+
ref: `refs/amalgm/exports/${'a'.repeat(40)}`,
|
|
187
|
+
...common,
|
|
188
|
+
}),
|
|
189
|
+
/invalid git bundle locator/,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('a corrupt pending upload is retired and rebuilt from the writer', () => {
|
|
194
|
+
const content = Buffer.from('rebuild this upload');
|
|
195
|
+
assert.equal(publishPayload('corrupt-upload', content, REPO_KEY).kind, 'pending');
|
|
196
|
+
const first = claimUpload();
|
|
197
|
+
assert.ok(first);
|
|
198
|
+
const chunkFile = path.join(
|
|
199
|
+
process.env.AMALGM_DIR,
|
|
200
|
+
'state',
|
|
201
|
+
'repo-objects',
|
|
202
|
+
'chunks',
|
|
203
|
+
`${first.sha256}.bin`,
|
|
204
|
+
);
|
|
205
|
+
fs.writeFileSync(chunkFile, 'corrupt');
|
|
206
|
+
assert.equal(claimUpload(), null, 'the corrupt manifest is retired instead of retrying forever');
|
|
207
|
+
|
|
208
|
+
assert.equal(publishPayload('corrupt-upload', content, REPO_KEY).kind, 'pending');
|
|
209
|
+
const rebuilt = claimUpload();
|
|
210
|
+
assert.equal(rebuilt.sha256, first.sha256);
|
|
211
|
+
assert.equal(Buffer.from(rebuilt.dataBase64, 'base64').toString(), content.toString());
|
|
212
|
+
acknowledgeUpload(rebuilt.resourceId, rebuilt.sha256);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('one malformed download request cannot block valid requests behind it', () => {
|
|
216
|
+
const content = Buffer.from('download me');
|
|
217
|
+
const chunks = [{
|
|
218
|
+
sha256: require('crypto').createHash('sha256').update(content).digest('hex'),
|
|
219
|
+
bytes: content.length,
|
|
220
|
+
}];
|
|
221
|
+
const locator = {
|
|
222
|
+
kind: 'chunks',
|
|
223
|
+
contract: 'opaque-chunks@2',
|
|
224
|
+
resourceId: PROJECT,
|
|
225
|
+
checksum: locatorChecksum(chunks),
|
|
226
|
+
bytes: content.length,
|
|
227
|
+
chunks,
|
|
228
|
+
};
|
|
229
|
+
fs.rmSync(path.join(
|
|
230
|
+
process.env.AMALGM_DIR,
|
|
231
|
+
'state',
|
|
232
|
+
'repo-objects',
|
|
233
|
+
'chunks',
|
|
234
|
+
`${chunks[0].sha256}.bin`,
|
|
235
|
+
), { force: true });
|
|
236
|
+
assert.equal(readPayload(locator).state, 'downloading');
|
|
237
|
+
const requests = path.join(process.env.AMALGM_DIR, 'state', 'repo-objects', 'requests');
|
|
238
|
+
fs.writeFileSync(path.join(requests, '000-malformed.json'), '{');
|
|
239
|
+
|
|
240
|
+
const claimed = claimDownload();
|
|
241
|
+
assert.equal(claimed.resourceId, PROJECT);
|
|
242
|
+
assert.equal(claimed.sha256, chunks[0].sha256);
|
|
243
|
+
assert.equal(fs.existsSync(path.join(requests, '000-malformed.json')), false);
|
|
244
|
+
receiveDownload(PROJECT, chunks[0].sha256, content.toString('base64'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('failed uploads yield the bulk lane to later durable objects', () => {
|
|
248
|
+
for (const content of ['first queued object', 'second queued object', 'third queued object']) {
|
|
249
|
+
assert.equal(
|
|
250
|
+
publishPayload(`fair-${content}`, Buffer.from(content), REPO_KEY).kind,
|
|
251
|
+
'pending',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const first = claimUpload();
|
|
256
|
+
assert.ok(first);
|
|
257
|
+
const retry = failUpload(first.resourceId, first.sha256, {
|
|
258
|
+
retryable: true,
|
|
259
|
+
code: 'temporary_object_failure',
|
|
260
|
+
message: 'try this object later',
|
|
261
|
+
});
|
|
262
|
+
assert.equal(retry.changed, true);
|
|
263
|
+
assert.equal(retry.terminal, false);
|
|
264
|
+
assert.ok(Date.parse(retry.retryAfter) > Date.now());
|
|
265
|
+
|
|
266
|
+
const second = claimUpload();
|
|
267
|
+
assert.ok(second);
|
|
268
|
+
assert.notEqual(second.sha256, first.sha256);
|
|
269
|
+
const terminal = failUpload(second.resourceId, second.sha256, {
|
|
270
|
+
retryable: false,
|
|
271
|
+
code: 'shared_resource_read_forbidden',
|
|
272
|
+
message: 'the stale rail is no longer readable',
|
|
273
|
+
});
|
|
274
|
+
assert.equal(terminal.changed, true);
|
|
275
|
+
assert.equal(terminal.terminal, true);
|
|
276
|
+
|
|
277
|
+
const third = claimUpload();
|
|
278
|
+
assert.ok(third);
|
|
279
|
+
assert.notEqual(third.sha256, first.sha256);
|
|
280
|
+
assert.notEqual(third.sha256, second.sha256);
|
|
281
|
+
|
|
282
|
+
acknowledgeUpload(first.resourceId, first.sha256);
|
|
283
|
+
acknowledgeUpload(second.resourceId, second.sha256);
|
|
284
|
+
acknowledgeUpload(third.resourceId, third.sha256);
|
|
285
|
+
assert.equal(claimUpload(), null);
|
|
286
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
|
|
6
|
+
const provenance = require('../workspace/provenance');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
ACTION,
|
|
10
|
+
CLASS,
|
|
11
|
+
STATE,
|
|
12
|
+
TRANSITIONS,
|
|
13
|
+
allStates,
|
|
14
|
+
provenanceClass,
|
|
15
|
+
publicationState,
|
|
16
|
+
requireTransition,
|
|
17
|
+
} = provenance;
|
|
18
|
+
|
|
19
|
+
const key = (from, action, to) => `${from} --${action}--> ${to}`;
|
|
20
|
+
|
|
21
|
+
test('managed bytes have exactly three provenance classes', () => {
|
|
22
|
+
assert.deepEqual(Object.values(CLASS), [
|
|
23
|
+
'officially-applied',
|
|
24
|
+
'journaled-local-intent',
|
|
25
|
+
'unknown-provenance',
|
|
26
|
+
]);
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
[...new Set(allStates.map(provenanceClass))].sort(),
|
|
29
|
+
Object.values(CLASS).sort(),
|
|
30
|
+
);
|
|
31
|
+
assert.equal(allStates.every((state) => provenanceClass(state) !== null), true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('the complete provenance transition relation has no implicit branch', () => {
|
|
35
|
+
const legal = new Set(TRANSITIONS.map((entry) => key(
|
|
36
|
+
entry.from,
|
|
37
|
+
entry.action,
|
|
38
|
+
entry.to,
|
|
39
|
+
)));
|
|
40
|
+
assert.equal(legal.size, TRANSITIONS.length, 'every transition is named once');
|
|
41
|
+
|
|
42
|
+
for (const from of allStates) {
|
|
43
|
+
for (const action of Object.values(ACTION)) {
|
|
44
|
+
for (const to of allStates) {
|
|
45
|
+
const transitionKey = key(from, action, to);
|
|
46
|
+
if (legal.has(transitionKey)) {
|
|
47
|
+
const entry = TRANSITIONS.find((candidate) => (
|
|
48
|
+
key(candidate.from, candidate.action, candidate.to) === transitionKey
|
|
49
|
+
));
|
|
50
|
+
assert.ok(entry.facts.length > 0, `${transitionKey} names its durable facts`);
|
|
51
|
+
assert.equal(
|
|
52
|
+
requireTransition(from, action, to, entry.facts),
|
|
53
|
+
entry,
|
|
54
|
+
transitionKey,
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
assert.throws(
|
|
58
|
+
() => requireTransition(from, action, to),
|
|
59
|
+
/illegal provenance transition/,
|
|
60
|
+
transitionKey,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('publication substates keep numbering, folding, and physical apply distinct', () => {
|
|
69
|
+
const publication = {
|
|
70
|
+
mutationId: 'mutation_provenance_state_01',
|
|
71
|
+
numberedVersion: null,
|
|
72
|
+
numberedAt: null,
|
|
73
|
+
foldedAt: null,
|
|
74
|
+
};
|
|
75
|
+
assert.equal(publicationState(null), STATE.CAPTURED);
|
|
76
|
+
assert.equal(publicationState(publication), STATE.RESERVED);
|
|
77
|
+
assert.equal(publicationState({
|
|
78
|
+
...publication,
|
|
79
|
+
numberedVersion: 51,
|
|
80
|
+
numberedAt: '2026-07-27T00:00:00.000Z',
|
|
81
|
+
}), STATE.NUMBERED);
|
|
82
|
+
assert.equal(publicationState({
|
|
83
|
+
...publication,
|
|
84
|
+
numberedVersion: 51,
|
|
85
|
+
numberedAt: '2026-07-27T00:00:00.000Z',
|
|
86
|
+
foldedAt: '2026-07-27T00:00:01.000Z',
|
|
87
|
+
}), STATE.FOLDED);
|
|
88
|
+
|
|
89
|
+
assert.throws(
|
|
90
|
+
() => requireTransition(
|
|
91
|
+
STATE.RESERVED,
|
|
92
|
+
ACTION.NUMBER,
|
|
93
|
+
STATE.NUMBERED,
|
|
94
|
+
['publication-id', 'official-version'],
|
|
95
|
+
),
|
|
96
|
+
/server-receipt/,
|
|
97
|
+
);
|
|
98
|
+
assert.throws(
|
|
99
|
+
() => requireTransition(
|
|
100
|
+
STATE.NUMBERED,
|
|
101
|
+
ACTION.APPLY,
|
|
102
|
+
STATE.OFFICIAL,
|
|
103
|
+
[],
|
|
104
|
+
),
|
|
105
|
+
/illegal provenance transition/,
|
|
106
|
+
'numbered work must fold before physical apply',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('unknown bytes can only verify, capture, or remain unknown', () => {
|
|
111
|
+
const outgoing = TRANSITIONS
|
|
112
|
+
.filter((entry) => entry.from === STATE.UNKNOWN)
|
|
113
|
+
.map((entry) => [entry.action, entry.to])
|
|
114
|
+
.sort();
|
|
115
|
+
assert.deepEqual(outgoing, [
|
|
116
|
+
[ACTION.CAPTURE, STATE.CAPTURED],
|
|
117
|
+
[ACTION.OBSERVE_UNKNOWN, STATE.UNKNOWN],
|
|
118
|
+
[ACTION.RECOVER, STATE.UNKNOWN],
|
|
119
|
+
[ACTION.SNAPSHOT, STATE.OFFICIAL],
|
|
120
|
+
[ACTION.SNAPSHOT, STATE.UNKNOWN],
|
|
121
|
+
[ACTION.VERIFY, STATE.OFFICIAL],
|
|
122
|
+
].sort());
|
|
123
|
+
assert.equal(
|
|
124
|
+
outgoing.some(([action]) => (
|
|
125
|
+
action === ACTION.RESERVE
|
|
126
|
+
|| action === ACTION.RETRY
|
|
127
|
+
|| action === ACTION.APPLY
|
|
128
|
+
)),
|
|
129
|
+
false,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('server compaction, local journal prune, and intent retirement are distinct', () => {
|
|
134
|
+
const prunes = TRANSITIONS.filter((entry) => entry.action === ACTION.PRUNE);
|
|
135
|
+
assert.deepEqual(prunes[0], {
|
|
136
|
+
from: STATE.FOLDED,
|
|
137
|
+
action: ACTION.PRUNE,
|
|
138
|
+
to: STATE.FOLDED,
|
|
139
|
+
facts: [
|
|
140
|
+
'official-fold',
|
|
141
|
+
'durable-folded-intent',
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
assert.deepEqual(prunes[1], {
|
|
145
|
+
from: STATE.OFFICIAL,
|
|
146
|
+
action: ACTION.PRUNE,
|
|
147
|
+
to: STATE.OFFICIAL,
|
|
148
|
+
facts: [
|
|
149
|
+
'official-fold',
|
|
150
|
+
'physical-verification',
|
|
151
|
+
'applied-cursor',
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
const compact = TRANSITIONS.find((entry) => entry.action === ACTION.COMPACT);
|
|
155
|
+
assert.deepEqual(compact.facts, [
|
|
156
|
+
'retained-server-receipt',
|
|
157
|
+
'verified-server-snapshot',
|
|
158
|
+
]);
|
|
159
|
+
const physicalApply = TRANSITIONS.find((entry) => (
|
|
160
|
+
entry.from === STATE.FOLDED
|
|
161
|
+
&& entry.action === ACTION.APPLY
|
|
162
|
+
&& entry.to === STATE.OFFICIAL
|
|
163
|
+
));
|
|
164
|
+
assert.deepEqual(physicalApply.facts.slice(-3), [
|
|
165
|
+
'physical-verification',
|
|
166
|
+
'applied-cursor',
|
|
167
|
+
'intent-retired',
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('WAL recovery keeps source and target provenance but exposes any third value', () => {
|
|
172
|
+
for (const state of allStates.filter((candidate) => candidate !== STATE.UNKNOWN)) {
|
|
173
|
+
assert.ok(TRANSITIONS.some((entry) => (
|
|
174
|
+
entry.from === state
|
|
175
|
+
&& entry.action === ACTION.RECOVER
|
|
176
|
+
&& entry.to === state
|
|
177
|
+
)));
|
|
178
|
+
assert.ok(TRANSITIONS.some((entry) => (
|
|
179
|
+
entry.from === state
|
|
180
|
+
&& entry.action === ACTION.RECOVER
|
|
181
|
+
&& entry.to === STATE.UNKNOWN
|
|
182
|
+
&& entry.facts.includes('exact-third-value-observation')
|
|
183
|
+
)));
|
|
184
|
+
}
|
|
185
|
+
});
|