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