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