@dharma-ai-labs/agent-fabric 0.2.186 → 0.2.187

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.
Files changed (37) hide show
  1. package/dist/index.d.ts +74 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +296 -12
  4. package/dist/index.js.map +1 -1
  5. package/dist/repositoryCandidateSync.d.ts +109 -2
  6. package/dist/repositoryCandidateSync.d.ts.map +1 -1
  7. package/dist/repositoryCandidateSync.js +327 -22
  8. package/dist/repositoryCandidateSync.js.map +1 -1
  9. package/dist/repositoryCandidateSync.test.js +912 -6
  10. package/dist/repositoryCandidateSync.test.js.map +1 -1
  11. package/dist/repositoryKnowledgeRetention.test.js +319 -3
  12. package/dist/repositoryKnowledgeRetention.test.js.map +1 -1
  13. package/dist/repositorySourceInventoryClient.d.ts +1 -0
  14. package/dist/repositorySourceInventoryClient.d.ts.map +1 -1
  15. package/dist/repositorySourceInventoryClient.js +4 -1
  16. package/dist/repositorySourceInventoryClient.js.map +1 -1
  17. package/dist/repositorySourceRestoration.d.ts +47 -0
  18. package/dist/repositorySourceRestoration.d.ts.map +1 -0
  19. package/dist/repositorySourceRestoration.js +140 -0
  20. package/dist/repositorySourceRestoration.js.map +1 -0
  21. package/dist/repositorySourceRestorationCommand.test.d.ts +2 -0
  22. package/dist/repositorySourceRestorationCommand.test.d.ts.map +1 -0
  23. package/dist/repositorySourceRestorationCommand.test.js +754 -0
  24. package/dist/repositorySourceRestorationCommand.test.js.map +1 -0
  25. package/dist/repositorySourceSync.d.ts +1 -0
  26. package/dist/repositorySourceSync.d.ts.map +1 -1
  27. package/dist/repositorySourceSync.js +10 -4
  28. package/dist/repositorySourceSync.js.map +1 -1
  29. package/dist/repositorySourceSync.test.js +38 -4
  30. package/dist/repositorySourceSync.test.js.map +1 -1
  31. package/dist/schemas/repository-candidate-fence-v1.schema.json +55 -0
  32. package/dist/schemas/repository-candidate-upload-v1.schema.json +60 -0
  33. package/dist/schemas/repository-knowledge-restoration-v1.schema.json +16 -0
  34. package/dist/usage.d.ts.map +1 -1
  35. package/dist/usage.js +3 -1
  36. package/dist/usage.js.map +1 -1
  37. package/package.json +1 -1
@@ -1,11 +1,16 @@
1
1
  import assert from 'node:assert/strict';
2
- import { createHash } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { promises as fs } from 'node:fs';
4
+ import { syncBuiltinESMExports } from 'node:module';
3
5
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
6
  import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ import { runInNewContext } from 'node:vm';
9
+ import { fileURLToPath } from 'node:url';
6
10
  import test from 'node:test';
7
- import { canonicalize } from '@dharma-ai-labs/agent-fabric-contracts';
8
- import { pollRepositoryCandidate, synchronizeRepositoryCandidate } from './repositoryCandidateSync.js';
11
+ import ts from 'typescript';
12
+ import { canonicalize, sha256, validateContract } from '@dharma-ai-labs/agent-fabric-contracts';
13
+ import { adoptRepositoryCandidate, checkpointRepositoryRestorationCandidate, checkpointRepositoryRestorationFence, checkpointRepositoryRestorationRegistry, fenceRepositoryRestorationUpload, readRepositoryRestorationAttempt, pollRepositoryCandidate, recoverRepositoryRestorationCandidate, synchronizeRepositoryCandidate } from './repositoryCandidateSync.js';
9
14
  import { inventoryRepositoryPackage } from './repositoryPackage.js';
10
15
  import { initializeRepositoryKnowledge } from './repositoryKnowledge.js';
11
16
  const ids = {
@@ -13,15 +18,386 @@ const ids = {
13
18
  repositoryBindingId: '73a95988-fd64-41ba-a0b9-6c8867d03788',
14
19
  repositoryAgentId: '57f61652-a5eb-46e4-930c-9478cd4a9c31',
15
20
  };
16
- async function snapshot(root) {
21
+ test('unadmitted restoration is fenced before checkpoint retirement and a distinct retry preserves its archive', async () => {
22
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-fence-'));
23
+ try {
24
+ const scope = { organizationId: 'org_fixture', ...ids }, calls = [];
25
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
26
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
27
+ const outboxRoot = join(root, 'outbox'), path = join(outboxRoot, `${ids.workspaceId}.json`);
28
+ const generation = '67f61652-a5eb-46e4-930c-9478cd4a9c31', fenceId = 'a7f61652-a5eb-46e4-930c-9478cd4a9c31';
29
+ const inventory = await snapshot(root);
30
+ let admitted = false;
31
+ const transport = {
32
+ signedGet: async () => ({ ok: true, organizationId: scope.organizationId, candidate: null }),
33
+ signedPost: async (route, raw) => {
34
+ const body = raw;
35
+ calls.push(route.endsWith('/fence') ? 'FENCE' : 'UPLOAD');
36
+ if (route.endsWith('/fence'))
37
+ return { ok: true, organizationId: scope.organizationId, candidate: null,
38
+ fence: { fenceId, operationId: body.operationId, workspaceId: scope.workspaceId,
39
+ repositoryBindingId: scope.repositoryBindingId, repositoryAgentId: scope.repositoryAgentId,
40
+ sourceSnapshotHash: body.sourceSnapshotHash, sourceManifestHash: body.sourceManifestHash,
41
+ policyGenerationId: generation, state: 'fenced', createdAt: '2030-01-02T00:00:00.000Z' } };
42
+ if (!admitted)
43
+ throw new Error('lost_upload');
44
+ assert.equal(body.restorationAttempt, fenceId);
45
+ return { ok: true, organizationId: scope.organizationId, candidate: {
46
+ candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
47
+ snapshotHash: body.sourceSnapshotHash, state: 'accepted', releaseId: null
48
+ } };
49
+ },
50
+ };
51
+ const input = { transport, outboxRoot, scope, snapshot: inventory, initialRepository: false,
52
+ expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
53
+ await assert.rejects(synchronizeRepositoryCandidate(input), /lost_upload/);
54
+ const before = await readFile(path, 'utf8'), old = JSON.parse(before);
55
+ await assert.rejects(fenceRepositoryRestorationUpload({ ...input, planHash: restoration.planHash,
56
+ policyGenerationId: generation, expectedPreviousOperationId: `sha256:${'d'.repeat(64)}` }), /checkpoint is invalid/);
57
+ assert.deepEqual(calls, ['UPLOAD']);
58
+ assert.equal(await readFile(path, 'utf8'), before);
59
+ const fenced = await fenceRepositoryRestorationUpload({ ...input, planHash: restoration.planHash,
60
+ policyGenerationId: generation, expectedPreviousOperationId: null });
61
+ const requested = canonicalize({ ...old, fenceRequestGenerationId: generation }) + '\n';
62
+ assert.equal(await readFile(path, 'utf8'), requested);
63
+ assert.equal(fenced.fence?.fenceId, fenceId);
64
+ await checkpointRepositoryRestorationFence({ outboxRoot, scope, fenced });
65
+ assert.equal(await readRepositoryRestorationAttempt({ outboxRoot, scope }), fenceId);
66
+ assert.equal(await pollRepositoryCandidate(input), null);
67
+ // A crash after durable archival but before the outbox rename must recover the same fence.
68
+ await writeFile(path, requested);
69
+ const recoveredFence = await fenceRepositoryRestorationUpload({ ...input, planHash: restoration.planHash,
70
+ policyGenerationId: generation, expectedPreviousOperationId: null });
71
+ await checkpointRepositoryRestorationFence({ outboxRoot, scope, fenced: recoveredFence });
72
+ const terminalBytes = await readFile(path, 'utf8');
73
+ const archivePath = join(outboxRoot, 'fenced', `${ids.workspaceId}.${fenceId}.json`);
74
+ const archivedBytes = await readFile(archivePath, 'utf8');
75
+ // Ordinary registry adoption must not consume the retry identity.
76
+ for (const state of ['published', 'blocked']) {
77
+ const ordinary = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31',
78
+ operationId: `sha256:${'e'.repeat(64)}`, snapshotHash: `sha256:${'f'.repeat(64)}`, state,
79
+ releaseId: state === 'published' ? '97f61652-a5eb-46e4-930c-9478cd4a9c31' : null };
80
+ assert.deepEqual(await adoptRepositoryCandidate({ scope, outboxRoot, candidate: ordinary,
81
+ sourceManifestHash: `sha256:${'d'.repeat(64)}`, consolidationMode: 'repository_update' }), ordinary);
82
+ assert.equal(await readFile(path, 'utf8'), terminalBytes);
83
+ assert.equal(await readFile(archivePath, 'utf8'), archivedBytes);
84
+ assert.equal(await readRepositoryRestorationAttempt({ outboxRoot, scope }), fenceId);
85
+ }
86
+ for (const state of ['accepted', 'processing']) {
87
+ await assert.rejects(adoptRepositoryCandidate({ scope, outboxRoot, sourceManifestHash: `sha256:${'d'.repeat(64)}`,
88
+ consolidationMode: 'repository_update', candidate: { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31',
89
+ operationId: `sha256:${'e'.repeat(64)}`, snapshotHash: `sha256:${'f'.repeat(64)}`, state, releaseId: null } }), /fenced restoration requires a terminal canonical candidate before adoption/);
90
+ assert.equal(await readFile(path, 'utf8'), terminalBytes);
91
+ assert.equal(await readRepositoryRestorationAttempt({ outboxRoot, scope }), fenceId);
92
+ }
93
+ admitted = true;
94
+ await assert.rejects(synchronizeRepositoryCandidate(input), /fenced operation requires/);
95
+ const retry = await synchronizeRepositoryCandidate({ ...input, restoration: { ...restoration, attemptId: fenceId } });
96
+ assert.notEqual(retry.operationId, old.operationId);
97
+ assert.deepEqual(calls, ['UPLOAD', 'FENCE', 'FENCE', 'UPLOAD']);
98
+ const archive = JSON.parse(await readFile(join(outboxRoot, 'fenced', `${ids.workspaceId}.${fenceId}.json`), 'utf8'));
99
+ assert.deepEqual(archive.checkpoint, JSON.parse(requested));
100
+ assert.equal(archive.fence.fenceId, fenceId);
101
+ }
102
+ finally {
103
+ await rm(root, { recursive: true, force: true });
104
+ }
105
+ });
106
+ test('partial archive writes never publish a poisoned terminal receipt and retry recovers', async () => {
107
+ const root = await mkdtemp(join(tmpdir(), 'dharma-archive-interruption-')), originalOpen = fs.open;
108
+ let injected = false;
109
+ try {
110
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
111
+ const path = join(outboxRoot, `${ids.workspaceId}.json`), generation = '67f61652-a5eb-46e4-930c-9478cd4a9c31';
112
+ const fenceId = 'a7f61652-a5eb-46e4-930c-9478cd4a9c31';
113
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
114
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
115
+ const transport = { signedGet: async () => ({}), signedPost: async (route, raw) => {
116
+ if (!route.endsWith('/fence'))
117
+ throw new Error('lost_upload');
118
+ const body = raw;
119
+ return { ok: true, organizationId: scope.organizationId, candidate: null, fence: { fenceId,
120
+ operationId: body.operationId, ...ids, sourceSnapshotHash: body.sourceSnapshotHash,
121
+ sourceManifestHash: body.sourceManifestHash, policyGenerationId: generation, state: 'fenced', createdAt: '2030-01-02T00:00:00.000Z' } };
122
+ } };
123
+ await assert.rejects(synchronizeRepositoryCandidate({ scope, outboxRoot, transport, snapshot: await snapshot(root),
124
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration }), /lost_upload/);
125
+ const fenced = await fenceRepositoryRestorationUpload({ scope, outboxRoot, transport, planHash: restoration.planHash,
126
+ policyGenerationId: generation, expectedPreviousOperationId: null });
127
+ const before = await readFile(path, 'utf8');
128
+ fs.open = async (...args) => {
129
+ const handle = await originalOpen(...args);
130
+ if (String(args[0]).replaceAll('\\', '/').includes('/fenced/') && !injected) {
131
+ const write = handle.writeFile.bind(handle);
132
+ handle.writeFile = async () => { injected = true; await write('partial'); throw new Error('partial_archive_write'); };
133
+ }
134
+ return handle;
135
+ };
136
+ syncBuiltinESMExports();
137
+ await assert.rejects(checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced }), /partial_archive_write/);
138
+ fs.open = originalOpen;
139
+ syncBuiltinESMExports();
140
+ assert.equal(injected, true);
141
+ assert.equal(await readFile(path, 'utf8'), before);
142
+ assert.deepEqual(await fs.readdir(join(outboxRoot, 'fenced')), []);
143
+ await checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced });
144
+ assert.equal(JSON.parse(await readFile(path, 'utf8')).state, 'fenced');
145
+ assert.deepEqual(await fs.readdir(join(outboxRoot, 'fenced')), [`${ids.workspaceId}.${fenceId}.json`]);
146
+ }
147
+ finally {
148
+ fs.open = originalOpen;
149
+ syncBuiltinESMExports();
150
+ await rm(root, { recursive: true, force: true });
151
+ }
152
+ });
153
+ test('uncertain, malformed or foreign fence responses preserve the pending checkpoint byte for byte', async () => {
154
+ const root = await mkdtemp(join(tmpdir(), 'dharma-fence-rejection-'));
155
+ try {
156
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
157
+ const path = join(outboxRoot, `${ids.workspaceId}.json`), generation = '67f61652-a5eb-46e4-930c-9478cd4a9c31';
158
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
159
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
160
+ const transport = { signedGet: async () => ({}), signedPost: async () => { throw new Error('lost_upload'); } };
161
+ const input = { transport, outboxRoot, scope, snapshot: await snapshot(root), initialRepository: false,
162
+ expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
163
+ await assert.rejects(synchronizeRepositoryCandidate(input), /lost_upload/);
164
+ const original = await readFile(path, 'utf8'), pending = { ...JSON.parse(original), fenceRequestGenerationId: generation };
165
+ const before = canonicalize(pending) + '\n';
166
+ const fence = { fenceId: 'a7f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: pending.operationId,
167
+ workspaceId: scope.workspaceId, repositoryBindingId: scope.repositoryBindingId, repositoryAgentId: scope.repositoryAgentId,
168
+ sourceSnapshotHash: pending.snapshotHash, sourceManifestHash: pending.sourceManifestHash, policyGenerationId: generation,
169
+ state: 'fenced', createdAt: '2030-01-02T00:00:00.000Z' };
170
+ for (const value of [null, { candidate: null, fence: null }, { candidate: {}, fence },
171
+ { candidate: null, fence: { ...fence, policyGenerationId: scope.workspaceId } },
172
+ { candidate: null, fence: { ...fence, sourceManifestHash: `sha256:${'f'.repeat(64)}` } },
173
+ { candidate: null, fence: { ...fence, privateKey: 'synthetic-must-not-be-retained' } }]) {
174
+ const transport = { signedGet: async () => ({}), signedPost: async () => {
175
+ if (!value)
176
+ throw new Error('lost_fence_reply');
177
+ return { ok: true, organizationId: scope.organizationId, ...value };
178
+ } };
179
+ await assert.rejects(fenceRepositoryRestorationUpload({ transport, outboxRoot, scope, planHash: restoration.planHash,
180
+ policyGenerationId: generation, expectedPreviousOperationId: null }));
181
+ assert.equal(await readFile(path, 'utf8'), before);
182
+ }
183
+ const transportWon = { signedGet: async () => ({}), signedPost: async () => ({ ok: true,
184
+ organizationId: scope.organizationId, fence: null, candidate: { candidateId: scope.repositoryAgentId,
185
+ operationId: pending.operationId, snapshotHash: pending.snapshotHash, state: 'accepted', releaseId: null } }) };
186
+ const won = await fenceRepositoryRestorationUpload({ transport: transportWon, outboxRoot, scope,
187
+ planHash: restoration.planHash, policyGenerationId: generation, expectedPreviousOperationId: null });
188
+ assert.equal(won.fence, null);
189
+ assert.equal(won.candidate?.state, 'accepted');
190
+ await assert.rejects(checkpointRepositoryRestorationFence({ outboxRoot, scope, fenced: won }), /checkpoint changed/);
191
+ assert.equal(await readFile(path, 'utf8'), before);
192
+ const transportFenced = { signedGet: async () => ({}), signedPost: async () => ({ ok: true,
193
+ organizationId: scope.organizationId, candidate: null, fence }) };
194
+ const terminal = await fenceRepositoryRestorationUpload({ transport: transportFenced, outboxRoot, scope,
195
+ planHash: restoration.planHash, policyGenerationId: generation, expectedPreviousOperationId: null });
196
+ const changed = JSON.stringify({ ...pending, restoration: { ...restoration, planHash: `sha256:${'e'.repeat(64)}` } });
197
+ await writeFile(path, changed);
198
+ await assert.rejects(checkpointRepositoryRestorationFence({ outboxRoot, scope, fenced: terminal }), /checkpoint changed/);
199
+ assert.equal(await readFile(path, 'utf8'), changed);
200
+ await writeFile(path, before);
201
+ await mkdir(join(outboxRoot, 'fenced'));
202
+ await writeFile(join(outboxRoot, 'fenced', `${ids.workspaceId}.${fence.fenceId}.json`), 'conflicting archive');
203
+ await assert.rejects(checkpointRepositoryRestorationFence({ outboxRoot, scope, fenced: terminal }), /archive changed/);
204
+ assert.equal(await readFile(path, 'utf8'), before);
205
+ }
206
+ finally {
207
+ await rm(root, { recursive: true, force: true });
208
+ }
209
+ });
210
+ test('durable restoration checkpoint recovers a terminal receipt by exact scoped GET without reposting', async () => {
211
+ for (const state of ['published', 'blocked']) {
212
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-replay-'));
213
+ try {
214
+ const scope = { organizationId: 'org_fixture', ...ids }, calls = [];
215
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
216
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
217
+ let accepted = {};
218
+ const transport = {
219
+ signedPost: async (_route, body) => {
220
+ calls.push('POST');
221
+ const request = body;
222
+ assert.equal(Object.hasOwn(request, 'restoration'), false);
223
+ const journal = JSON.parse(await readFile(join(root, 'outbox', `${ids.workspaceId}.json`), 'utf8'));
224
+ assert.deepEqual(journal.restoration, restoration);
225
+ assert.equal(journal.state, 'pending_upload');
226
+ accepted = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: request.operationId,
227
+ snapshotHash: request.sourceSnapshotHash, state, releaseId: state === 'published' ? '97f61652-a5eb-46e4-930c-9478cd4a9c31' : null };
228
+ return { ok: true, organizationId: scope.organizationId, candidate: accepted };
229
+ },
230
+ signedGet: async (route) => {
231
+ calls.push('GET');
232
+ assert.ok(route.includes(`/package-candidates/${accepted.candidateId}?workspaceId=${ids.workspaceId}&operationId=`));
233
+ return { ok: true, organizationId: scope.organizationId, candidate: accepted };
234
+ },
235
+ };
236
+ const input = { transport, outboxRoot: join(root, 'outbox'), scope,
237
+ snapshot: await snapshot(root), initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
238
+ await synchronizeRepositoryCandidate(input);
239
+ for (let replay = 0; replay < 2; replay++) {
240
+ const recovered = await recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash });
241
+ assert.deepEqual(recovered?.candidate, accepted);
242
+ assert.equal(recovered?.localSnapshotHash, restoration.localSnapshotHash);
243
+ }
244
+ assert.equal(await recoverRepositoryRestorationCandidate({ ...input, planHash: `sha256:${'d'.repeat(64)}` }), null);
245
+ assert.deepEqual(calls, ['POST', 'GET', 'GET']);
246
+ const path = join(root, 'outbox', `${ids.workspaceId}.json`), journal = JSON.parse(await readFile(path, 'utf8'));
247
+ journal.operationId = `sha256:${'e'.repeat(64)}`;
248
+ await writeFile(path, JSON.stringify(journal));
249
+ await assert.rejects(recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash }), /checkpoint operation/);
250
+ assert.deepEqual(calls, ['POST', 'GET', 'GET']);
251
+ journal.scope.repositoryAgentId = '97f61652-a5eb-46e4-930c-9478cd4a9c31';
252
+ await writeFile(path, JSON.stringify(journal));
253
+ await assert.rejects(recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash }), /outbox scope/);
254
+ }
255
+ finally {
256
+ await rm(root, { recursive: true, force: true });
257
+ }
258
+ }
259
+ });
260
+ test('review regression: encoded candidate respects the relay body limit before checkpoint or transport', async () => {
261
+ const root = await mkdtemp(join(tmpdir(), 'dharma-candidate-body-limit-'));
262
+ try {
263
+ const outboxRoot = join(root, 'outbox');
264
+ let posts = 0;
265
+ const inventory = await snapshot(root, 13);
266
+ const serializedBytes = Buffer.byteLength(canonicalize(inventory));
267
+ assert.ok(serializedBytes > 4_194_304 && serializedBytes < 8_388_608);
268
+ const transport = { signedGet: async () => ({}), signedPost: async () => {
269
+ posts++;
270
+ throw new Error('oversized upload reached transport');
271
+ } };
272
+ await assert.rejects(synchronizeRepositoryCandidate({ transport, outboxRoot,
273
+ scope: { organizationId: 'org_fixture', ...ids }, snapshot: inventory, initialRepository: true }), /upload byte limit/);
274
+ const bytesPerFile = 262_000 - Math.ceil((serializedBytes - 4_194_304 + 100) * 3 / 4 / 13);
275
+ const nearLimit = await snapshot(root, 13, bytesPerFile);
276
+ const remaining = 4_194_304 - Buffer.byteLength(canonicalize(nearLimit));
277
+ assert.ok(remaining > 0 && remaining < 200, `Snapshot boundary margin: ${remaining}`);
278
+ await assert.rejects(synchronizeRepositoryCandidate({ transport, outboxRoot,
279
+ scope: { organizationId: 'org_fixture', ...ids }, snapshot: nearLimit, initialRepository: true }), /upload byte limit/);
280
+ assert.equal(posts, 0);
281
+ await assert.rejects(readFile(join(outboxRoot, `${ids.workspaceId}.json`)), { code: 'ENOENT' });
282
+ }
283
+ finally {
284
+ await rm(root, { recursive: true, force: true });
285
+ }
286
+ });
287
+ test('lost fence response recovers durable request provenance across repeated policy rollover', async () => {
288
+ const root = await mkdtemp(join(tmpdir(), 'dharma-fence-lost-response-'));
289
+ try {
290
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
291
+ const path = join(outboxRoot, `${ids.workspaceId}.json`);
292
+ const firstGeneration = '67f61652-a5eb-46e4-930c-9478cd4a9c31';
293
+ const laterGeneration = '97f61652-a5eb-46e4-930c-9478cd4a9c31';
294
+ const fenceId = 'a7f61652-a5eb-46e4-930c-9478cd4a9c31';
295
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
296
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
297
+ let posts = 0;
298
+ const transport = { signedGet: async () => ({}), signedPost: async (route, raw) => {
299
+ if (!route.endsWith('/fence'))
300
+ throw new Error('lost_upload');
301
+ const body = raw;
302
+ const saved = JSON.parse(await readFile(path, 'utf8'));
303
+ assert.equal(saved.fenceRequestGenerationId, firstGeneration, 'request provenance must be durable before POST');
304
+ assert.equal(body.requestPolicyGenerationId, firstGeneration);
305
+ if (++posts === 1)
306
+ throw new Error('lost_successful_fence_response');
307
+ return { ok: true, organizationId: scope.organizationId, candidate: null, fence: { fenceId,
308
+ operationId: body.operationId, ...ids, sourceSnapshotHash: body.sourceSnapshotHash,
309
+ sourceManifestHash: body.sourceManifestHash, policyGenerationId: firstGeneration,
310
+ requestPolicyGenerationId: firstGeneration, state: 'fenced', createdAt: '2030-01-02T00:00:00.000Z' } };
311
+ } };
312
+ const upload = { scope, outboxRoot, transport, snapshot: await snapshot(root),
313
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
314
+ await assert.rejects(synchronizeRepositoryCandidate(upload), /lost_upload/);
315
+ const input = { scope, outboxRoot, transport, planHash: restoration.planHash,
316
+ policyGenerationId: firstGeneration, expectedPreviousOperationId: null };
317
+ await assert.rejects(fenceRepositoryRestorationUpload(input), /lost_successful_fence_response/);
318
+ const pending = await readFile(path, 'utf8');
319
+ await assert.rejects(synchronizeRepositoryCandidate(upload), /fence outcome must be reconciled/);
320
+ const recovered = await fenceRepositoryRestorationUpload({ ...input, policyGenerationId: laterGeneration });
321
+ assert.equal(await readFile(path, 'utf8'), pending);
322
+ assert.equal(recovered.fence?.policyGenerationId, firstGeneration);
323
+ await checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced: recovered });
324
+ const archivePath = join(outboxRoot, 'fenced', `${ids.workspaceId}.${fenceId}.json`);
325
+ const archive = await readFile(archivePath, 'utf8');
326
+ const replay = await fenceRepositoryRestorationUpload({ ...input, policyGenerationId: scope.workspaceId });
327
+ await checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced: replay });
328
+ assert.equal(posts, 2);
329
+ assert.equal(await readFile(archivePath, 'utf8'), archive);
330
+ }
331
+ finally {
332
+ await rm(root, { recursive: true, force: true });
333
+ }
334
+ });
335
+ test('review regression: checkpointed fence replay retains its admitted generation after policy rollover', async () => {
336
+ const root = await mkdtemp(join(tmpdir(), 'dharma-fence-policy-rollover-'));
337
+ try {
338
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
339
+ const generation = '67f61652-a5eb-46e4-930c-9478cd4a9c31';
340
+ const currentGeneration = '97f61652-a5eb-46e4-930c-9478cd4a9c31';
341
+ const fenceId = 'a7f61652-a5eb-46e4-930c-9478cd4a9c31';
342
+ let rolledOver = false, fencePosts = 0;
343
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
344
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
345
+ const transport = { signedGet: async () => ({}), signedPost: async (route, raw) => {
346
+ if (!route.endsWith('/fence'))
347
+ throw new Error('lost_upload');
348
+ fencePosts++;
349
+ if (rolledOver)
350
+ throw new Error('HTTP 403: fabric_repository_candidate_fence_binding_denied');
351
+ const body = raw;
352
+ return { ok: true, organizationId: scope.organizationId, candidate: null, fence: { fenceId,
353
+ operationId: body.operationId, ...ids, sourceSnapshotHash: body.sourceSnapshotHash,
354
+ sourceManifestHash: body.sourceManifestHash, policyGenerationId: generation,
355
+ state: 'fenced', createdAt: '2030-01-02T00:00:00.000Z' } };
356
+ } };
357
+ await assert.rejects(synchronizeRepositoryCandidate({ scope, outboxRoot, transport, snapshot: await snapshot(root),
358
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration }), /lost_upload/);
359
+ const input = { scope, outboxRoot, transport, planHash: restoration.planHash,
360
+ policyGenerationId: generation, expectedPreviousOperationId: null };
361
+ const fenced = await fenceRepositoryRestorationUpload(input);
362
+ await checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced });
363
+ const path = join(outboxRoot, `${ids.workspaceId}.json`);
364
+ const archivePath = join(outboxRoot, 'fenced', `${ids.workspaceId}.${fenceId}.json`);
365
+ const before = await readFile(path, 'utf8'), archiveBefore = await readFile(archivePath, 'utf8');
366
+ rolledOver = true;
367
+ const replay = await fenceRepositoryRestorationUpload({ ...input, policyGenerationId: currentGeneration });
368
+ assert.deepEqual(replay.fence, fenced.fence);
369
+ await checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced: replay });
370
+ assert.equal(fencePosts, 1);
371
+ assert.equal(await readFile(path, 'utf8'), before);
372
+ assert.equal(await readFile(archivePath, 'utf8'), archiveBefore);
373
+ const tampered = JSON.parse(before);
374
+ tampered.fence.policyGenerationId = currentGeneration;
375
+ tampered.fenceRequestGenerationId = currentGeneration;
376
+ await writeFile(path, canonicalize(tampered) + '\n');
377
+ const tamperedBytes = await readFile(path, 'utf8');
378
+ const changed = await fenceRepositoryRestorationUpload({ ...input, policyGenerationId: currentGeneration });
379
+ await assert.rejects(checkpointRepositoryRestorationFence({ scope, outboxRoot, fenced: changed }), /archive changed/);
380
+ assert.equal(await readFile(path, 'utf8'), tamperedBytes);
381
+ assert.equal(await readFile(archivePath, 'utf8'), archiveBefore);
382
+ assert.equal(fencePosts, 1);
383
+ }
384
+ finally {
385
+ await rm(root, { recursive: true, force: true });
386
+ }
387
+ });
388
+ async function snapshot(root, largeFiles = 0, largeFileBytes = 262_000) {
17
389
  const workspace = join(root, 'repository');
18
390
  await mkdir(join(workspace, '.agents/skills/dharma-agent-fabric'), { recursive: true });
19
391
  await writeFile(join(workspace, '.agents/skills/dharma-agent-fabric/.dharma-agent-fabric.json'), JSON.stringify({ managedBy: 'dharma-agent-fabric', workspaceId: ids.workspaceId }));
20
392
  await writeFile(join(workspace, 'README.md'), '# Governed repository\n');
393
+ const largePaths = Array.from({ length: largeFiles }, (_, index) => `source-${index}.txt`);
394
+ for (const [index, path] of largePaths.entries()) {
395
+ await writeFile(join(workspace, path), `${index}:` + 'x'.repeat(largeFileBytes));
396
+ }
21
397
  const operation = { action: 'authorize', confirmed: true,
22
398
  requestId: '5ff2ee1b-6cb3-459e-a977-ea99c757bf30', repositoryBindingId: ids.repositoryBindingId,
23
399
  expectedRevision: 0, allowedContentClasses: ['approved_outputs', 'repository_content', 'repository_skills'],
24
- approvedRepositoryPaths: ['README.md'], approvedOutputFolders: [], automaticValidatedPublication: true,
400
+ approvedRepositoryPaths: ['README.md', ...largePaths].sort(), approvedOutputFolders: [], automaticValidatedPublication: true,
25
401
  retentionDays: 30, maximumFileBytes: 262144, maximumSnapshotBytes: 4194304,
26
402
  maximumDailyUploadBytes: 1073741824, expiresAt: '2031-01-01T00:00:00.000Z' };
27
403
  const generationId = '67f61652-a5eb-46e4-930c-9478cd4a9c31';
@@ -36,6 +412,512 @@ async function snapshot(root) {
36
412
  await initializeRepositoryKnowledge(input);
37
413
  return inventoryRepositoryPackage(input);
38
414
  }
415
+ test('review regression: terminal restoration cannot be replaced before registry reconciliation', async () => {
416
+ for (const state of ['published', 'blocked']) {
417
+ const root = await mkdtemp(join(tmpdir(), 'dharma-terminal-restoration-'));
418
+ try {
419
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
420
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
421
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
422
+ let posts = 0;
423
+ const transport = { signedGet: async () => ({}), signedPost: async (_route, raw) => {
424
+ posts++;
425
+ const body = raw;
426
+ return { ok: true, organizationId: scope.organizationId, candidate: {
427
+ candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
428
+ snapshotHash: body.sourceSnapshotHash, state, releaseId: state === 'published' ? '97f61652-a5eb-46e4-930c-9478cd4a9c31' : null
429
+ } };
430
+ } };
431
+ const input = { scope, outboxRoot, transport, snapshot: await snapshot(root), initialRepository: false,
432
+ expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}` };
433
+ await synchronizeRepositoryCandidate({ ...input, restoration });
434
+ const path = join(outboxRoot, `${ids.workspaceId}.json`), before = await readFile(path, 'utf8');
435
+ await assert.rejects(synchronizeRepositoryCandidate({ ...input, expectedLatestSourceFingerprint: `sha256:${'e'.repeat(64)}` }), /registry reconciliation/);
436
+ assert.equal(posts, 1);
437
+ assert.equal(await readFile(path, 'utf8'), before);
438
+ const candidate = (await pollRepositoryCandidate(input));
439
+ await assert.rejects(adoptRepositoryCandidate({ ...input, candidate: { ...candidate, operationId: `sha256:${'e'.repeat(64)}` },
440
+ sourceManifestHash: JSON.parse(before).sourceManifestHash, consolidationMode: 'repository_update' }), /registry reconciliation/);
441
+ await adoptRepositoryCandidate({ ...input, candidate, sourceManifestHash: JSON.parse(before).sourceManifestHash,
442
+ consolidationMode: 'repository_update' });
443
+ assert.equal(await readFile(path, 'utf8'), before);
444
+ }
445
+ finally {
446
+ await rm(root, { recursive: true, force: true });
447
+ }
448
+ }
449
+ });
450
+ test('real guarded registry and durable outbox retain a terminal response across failed save and resumed scan', async () => {
451
+ const indexText = await readFile(new URL('../src/index.ts', import.meta.url), 'utf8');
452
+ const indexSource = ts.createSourceFile('index.ts', indexText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
453
+ const helpers = ['saveWorkspaceRecord', 'saveRegistry', 'writeJsonAtomic', 'acquirePidLock'];
454
+ const helperCode = ts.transpileModule(indexSource.statements.filter(node => ts.isFunctionDeclaration(node)
455
+ && node.name && helpers.includes(node.name.text)).map(node => node.getText(indexSource)).join('\n'), { compilerOptions: { target: ts.ScriptTarget.ES2023, module: ts.ModuleKind.CommonJS } }).outputText;
456
+ for (const state of ['published', 'blocked']) {
457
+ const root = await mkdtemp(join(tmpdir(), 'dharma-real-registry-outbox-'));
458
+ try {
459
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
460
+ const registryPath = join(root, 'registry', 'workspaces.json'), inventory = await snapshot(root);
461
+ const original = { ...scope, repositoryPackage: { operationId: null, state: 'published', generation: 18 } };
462
+ const sibling = { workspaceId: 'sibling', marker: 'preserve' };
463
+ await mkdir(dirname(registryPath), { recursive: true });
464
+ await writeFile(registryPath, JSON.stringify([original, sibling]));
465
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: inventory.manifest.snapshotHash,
466
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
467
+ let posts = 0, candidate;
468
+ const transport = { signedGet: async () => ({ ok: true, organizationId: scope.organizationId, candidate }),
469
+ signedPost: async (_route, raw) => {
470
+ posts++;
471
+ const body = raw;
472
+ return { ok: true, organizationId: scope.organizationId, candidate: {
473
+ candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
474
+ snapshotHash: body.sourceSnapshotHash, state, releaseId: state === 'published' ? '97f61652-a5eb-46e4-930c-9478cd4a9c31' : null
475
+ } };
476
+ } };
477
+ const input = { scope, outboxRoot, transport, snapshot: inventory, initialRepository: false,
478
+ expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}` };
479
+ const resumedScan = () => synchronizeRepositoryCandidate({ ...input, expectedLatestSourceFingerprint: `sha256:${'e'.repeat(64)}` });
480
+ let failRegistry = true, interleavingObserved = false;
481
+ const save = runInNewContext(helperCode + '\nsaveWorkspaceRecord', { exports: {}, ...fs, structuredClone, canonicalize,
482
+ dirname, resolve, process, randomUUID, setTimeout, currentBootstrapHostScope: () => undefined,
483
+ dharmaHome: () => root, workspaceRegistryPath: () => registryPath,
484
+ rename: async (from, to) => {
485
+ if (to === registryPath && failRegistry) {
486
+ await assert.rejects(resumedScan(), /registry reconciliation/);
487
+ interleavingObserved = true;
488
+ throw new Error('injected_registry_rename_failure');
489
+ }
490
+ return fs.rename(from, to);
491
+ },
492
+ }, { timeout: 1000, contextCodeGeneration: { strings: false, wasm: false } });
493
+ let admissionEntered = false;
494
+ await assert.rejects(save(original, { expected: { ...original, repositoryPackage: { operationId: 'stale' } },
495
+ preparePackage: async () => { admissionEntered = true; await synchronizeRepositoryCandidate({ ...input, restoration }); } }), /compare_and_swap/);
496
+ assert.equal(admissionEntered, false);
497
+ assert.equal(posts, 0);
498
+ const packageFor = (receipt) => ({ ...receipt,
499
+ sourceManifestHash: sha256(canonicalize(inventory.manifest)), consolidationMode: 'repository_update', generation: 18,
500
+ localBaselineSnapshotHash: state === 'blocked' ? null : restoration.localSnapshotHash,
501
+ publishedLocalSnapshotHash: state === 'published' ? restoration.localSnapshotHash : null,
502
+ pendingLocalSnapshotHash: null, pendingLocalOperationId: null });
503
+ await assert.rejects(save(original, { expected: original, preparePackage: async () => {
504
+ candidate = await synchronizeRepositoryCandidate({ ...input, restoration });
505
+ return packageFor(candidate);
506
+ }, afterSave: saved => checkpointRepositoryRestorationRegistry({ ...input, repositoryPackage: saved.repositoryPackage }) }), /injected_registry_rename_failure/);
507
+ assert.equal(interleavingObserved, true);
508
+ assert.equal(posts, 1);
509
+ assert.deepEqual(JSON.parse(await readFile(registryPath, 'utf8')), [original, sibling]);
510
+ const path = join(outboxRoot, `${ids.workspaceId}.json`), before = await readFile(path, 'utf8');
511
+ assert.equal(JSON.parse(before).registryReceiptHash, undefined);
512
+ await assert.rejects(resumedScan(), /registry reconciliation/);
513
+ assert.equal(await readFile(path, 'utf8'), before);
514
+ const recovered = (await recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash }));
515
+ failRegistry = false;
516
+ await save({ ...original, repositoryPackage: packageFor(recovered.candidate) }, { expected: original,
517
+ beforeSave: async () => { await checkpointRepositoryRestorationCandidate({ ...input, recovered }); },
518
+ afterSave: saved => checkpointRepositoryRestorationRegistry({ ...input, repositoryPackage: saved.repositoryPackage }) });
519
+ assert.equal(posts, 1);
520
+ assert.match(JSON.parse(await readFile(path, 'utf8')).registryReceiptHash, /^sha256:/);
521
+ const rows = JSON.parse(await readFile(registryPath, 'utf8'));
522
+ assert.deepEqual(rows.find((row) => row.workspaceId === 'sibling'), sibling);
523
+ assert.equal(rows.find((row) => row.workspaceId === scope.workspaceId).repositoryPackage.operationId, candidate.operationId);
524
+ await resumedScan();
525
+ assert.equal(posts, 2);
526
+ await assert.rejects(fs.lstat(`${registryPath}.mutation.lock`), { code: 'ENOENT' });
527
+ }
528
+ finally {
529
+ await rm(root, { recursive: true, force: true });
530
+ }
531
+ }
532
+ });
533
+ test('same-operation onboarding adopts retained restoration baselines and marks only a durable registry save', async () => {
534
+ const indexText = await readFile(new URL('../src/index.ts', import.meta.url), 'utf8');
535
+ const source = ts.createSourceFile('index.ts', indexText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
536
+ const onboard = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === 'onboard');
537
+ const statements = onboard.body.statements;
538
+ const candidateIndex = statements.findIndex(node => ts.isVariableStatement(node)
539
+ && node.declarationList.declarations.some(value => value.name.getText(source) === 'candidate'));
540
+ const providersIndex = statements.findIndex(node => ts.isVariableStatement(node)
541
+ && node.declarationList.declarations.some(value => value.name.getText(source) === 'providers'));
542
+ assert.ok(candidateIndex >= 0 && providersIndex > candidateIndex);
543
+ const tail = statements.slice(candidateIndex + 1, providersIndex).map(node => node.getText(source)).join('\n');
544
+ const returned = statements.findLast(node => ts.isReturnStatement(node));
545
+ const publicCandidate = returned.expression.properties.find(node => ts.isPropertyAssignment(node)
546
+ && node.name.getText(source) === 'repositoryCandidate');
547
+ const publicCode = ts.transpileModule(`function projectCandidate(candidate: any) { return ${publicCandidate.initializer.getText(source)}; }`, { compilerOptions: { target: ts.ScriptTarget.ES2023, module: ts.ModuleKind.CommonJS } }).outputText;
548
+ const helperNames = ['saveWorkspaceRecord', 'saveRegistry', 'writeJsonAtomic', 'acquirePidLock'];
549
+ const code = ts.transpileModule(source.statements.filter(node => ts.isFunctionDeclaration(node)
550
+ && node.name && helperNames.includes(node.name.text)).map(node => node.getText(source)).join('\n')
551
+ + `\nasync function saveAdoption() { ${tail}\nreturn registered; }`, { compilerOptions: { target: ts.ScriptTarget.ES2023, module: ts.ModuleKind.CommonJS } }).outputText;
552
+ for (const generation of [0, 18])
553
+ for (const state of ['published', 'blocked']) {
554
+ const root = await mkdtemp(join(tmpdir(), 'dharma-onboarding-restoration-reconcile-'));
555
+ try {
556
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
557
+ const inventory = await snapshot(root), path = join(outboxRoot, `${ids.workspaceId}.json`);
558
+ const registryPath = join(root, 'registry', 'workspaces.json');
559
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
560
+ baselineSnapshotHash: `sha256:${'d'.repeat(64)}`, previousOperationId: null,
561
+ priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
562
+ let posts = 0;
563
+ const input = { scope, outboxRoot, snapshot: inventory, initialRepository: false,
564
+ expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`,
565
+ transport: { signedGet: async () => ({}), signedPost: async (_route, raw) => {
566
+ posts++;
567
+ const body = raw;
568
+ return { ok: true, organizationId: scope.organizationId, candidate: {
569
+ candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
570
+ snapshotHash: body.sourceSnapshotHash, state,
571
+ releaseId: state === 'published' ? '97f61652-a5eb-46e4-930c-9478cd4a9c31' : null
572
+ } };
573
+ } } };
574
+ const receipt = await synchronizeRepositoryCandidate({ ...input, restoration });
575
+ const original = { ...scope, repositoryPackage: { ...receipt, generation,
576
+ sourceManifestHash: sha256(canonicalize(inventory.manifest)), consolidationMode: 'repository_update',
577
+ sourcePolicyGenerationId: inventory.manifest.sourceAuthorization.generationId } };
578
+ const sibling = { workspaceId: 'sibling', marker: 'preserve' };
579
+ await mkdir(dirname(registryPath), { recursive: true });
580
+ await writeFile(registryPath, JSON.stringify([original, sibling]));
581
+ const before = await readFile(path, 'utf8');
582
+ let failure = null, conflict = false;
583
+ const forged = JSON.parse(before);
584
+ forged.restoration.attemptId = 'a7f61652-a5eb-46e4-930c-9478cd4a9c31';
585
+ const forgedBytes = JSON.stringify(forged);
586
+ await writeFile(path, forgedBytes);
587
+ await assert.rejects(adoptRepositoryCandidate({ scope, outboxRoot, candidate: receipt,
588
+ sourceManifestHash: original.repositoryPackage.sourceManifestHash, consolidationMode: 'repository_update' }), /checkpoint operation is invalid/);
589
+ assert.equal(await readFile(path, 'utf8'), forgedBytes);
590
+ await writeFile(path, before);
591
+ const run = async () => {
592
+ const candidate = await adoptRepositoryCandidate({ scope, outboxRoot, candidate: receipt,
593
+ sourceManifestHash: original.repositoryPackage.sourceManifestHash, consolidationMode: 'repository_update' });
594
+ const projected = runInNewContext(publicCode + '\nprojectCandidate', { exports: {} }, { timeout: 1000, contextCodeGeneration: { strings: false, wasm: false } })(candidate);
595
+ assert.deepEqual(JSON.parse(JSON.stringify(projected)), receipt);
596
+ const rows = JSON.parse(await readFile(registryPath, 'utf8'));
597
+ const registered = rows.find((row) => row.workspaceId === scope.workspaceId);
598
+ return runInNewContext(code + '\nsaveAdoption', { exports: {}, ...fs, structuredClone, canonicalize,
599
+ dirname, resolve, process, randomUUID, setTimeout, currentBootstrapHostScope: () => undefined,
600
+ dharmaHome: () => root, workspaceRegistryPath: () => registryPath,
601
+ registered, canonicalPackage: original.repositoryPackage, candidate, submitPackage: false, refreshPolicy: false,
602
+ initialSnapshot: inventory, sourceAuthorization: inventory.manifest.sourceAuthorization, sha256,
603
+ candidateScope: scope, outboxRoot,
604
+ readFile: async (...args) => {
605
+ if (args[0] === registryPath && conflict) {
606
+ conflict = false;
607
+ await fs.writeFile(registryPath, JSON.stringify([{ ...registered,
608
+ repositoryPackage: { ...registered.repositoryPackage, generation: 99 } }, sibling]));
609
+ }
610
+ return fs.readFile(...args);
611
+ },
612
+ rename: async (from, to) => {
613
+ if (to === registryPath && failure === 'save')
614
+ throw new Error('injected_registry_save_failure');
615
+ return fs.rename(from, to);
616
+ }, checkpointRepositoryRestorationRegistry: async (value) => {
617
+ if (failure === 'marker')
618
+ throw new Error('injected_registry_marker_failure');
619
+ await checkpointRepositoryRestorationRegistry(value);
620
+ },
621
+ }, { timeout: 1000, contextCodeGeneration: { strings: false, wasm: false } })();
622
+ };
623
+ conflict = true;
624
+ await assert.rejects(run(), /compare_and_swap/);
625
+ assert.equal(JSON.parse(await readFile(path, 'utf8')).registryReceiptHash, undefined);
626
+ assert.equal(JSON.parse(await readFile(registryPath, 'utf8'))[0].repositoryPackage.generation, 99);
627
+ await writeFile(registryPath, JSON.stringify([original, sibling]));
628
+ failure = 'save';
629
+ await assert.rejects(run(), /injected_registry_save_failure/);
630
+ assert.equal(await readFile(path, 'utf8'), before);
631
+ assert.deepEqual(JSON.parse(await readFile(registryPath, 'utf8')), [original, sibling]);
632
+ failure = 'marker';
633
+ await assert.rejects(run(), /injected_registry_marker_failure/);
634
+ assert.equal(JSON.parse(await readFile(path, 'utf8')).registryReceiptHash, undefined);
635
+ await assert.rejects(synchronizeRepositoryCandidate({ ...input,
636
+ expectedLatestSourceFingerprint: `sha256:${'e'.repeat(64)}` }), /registry reconciliation/);
637
+ failure = null;
638
+ const saved = (await run()).repositoryPackage;
639
+ assert.equal(saved.localBaselineSnapshotHash, state === 'blocked' ? restoration.baselineSnapshotHash : restoration.localSnapshotHash);
640
+ assert.equal(saved.publishedLocalSnapshotHash, state === 'published' ? restoration.localSnapshotHash : restoration.baselineSnapshotHash);
641
+ assert.equal(saved.pendingLocalOperationId, null);
642
+ assert.equal(saved.pendingLocalSnapshotHash, null);
643
+ assert.match(JSON.parse(await readFile(path, 'utf8')).registryReceiptHash, /^sha256:/);
644
+ assert.equal(posts, 1);
645
+ assert.deepEqual(JSON.parse(await readFile(registryPath, 'utf8')).find((row) => row.workspaceId === 'sibling'), sibling);
646
+ await synchronizeRepositoryCandidate({ ...input, expectedLatestSourceFingerprint: `sha256:${'e'.repeat(64)}` });
647
+ assert.equal(posts, 2);
648
+ }
649
+ finally {
650
+ await rm(root, { recursive: true, force: true });
651
+ }
652
+ }
653
+ });
654
+ test('candidate upload schema validates actual legacy and restoration wire bodies and rejects malformed provenance', async () => {
655
+ const root = await mkdtemp(join(tmpdir(), 'dharma-candidate-upload-contract-'));
656
+ try {
657
+ const scope = { organizationId: 'org_fixture', ...ids }, inventory = await snapshot(root);
658
+ const attemptId = 'a7f61652-a5eb-46e4-930c-9478cd4a9c31', headCandidateId = 'b7f61652-a5eb-46e4-930c-9478cd4a9c31';
659
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: inventory.manifest.snapshotHash,
660
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31', attemptId, headCandidateId };
661
+ const schemaDirectory = new URL('./schemas/', import.meta.url);
662
+ for (const mode of ['legacy-initial', 'legacy-update', 'restoration']) {
663
+ let body = {};
664
+ await synchronizeRepositoryCandidate({ scope, snapshot: inventory, outboxRoot: join(root, mode),
665
+ initialRepository: mode === 'legacy-initial',
666
+ ...(mode !== 'legacy-initial' ? { expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}` } : {}),
667
+ ...(mode === 'restoration' ? { restoration } : {}),
668
+ transport: { signedGet: async () => ({}), signedPost: async (_route, raw) => {
669
+ body = raw;
670
+ return { ok: true, organizationId: scope.organizationId, candidate: {
671
+ candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
672
+ snapshotHash: body.sourceSnapshotHash, state: 'accepted', releaseId: null
673
+ } };
674
+ } } });
675
+ const validate = (value) => validateContract(fileURLToPath(schemaDirectory), 'https://schemas.dharma-ai.io/repository-candidate-upload/v1', value);
676
+ assert.equal((await validate(body)).ok, true);
677
+ if (mode === 'restoration') {
678
+ assert.equal(body.restorationAttempt, attemptId);
679
+ assert.equal(body.restorationContext.headCandidateId, headCandidateId);
680
+ for (const changed of [
681
+ { ...body, restorationAttempt: `${attemptId}\n` }, { ...body, restorationContext: { ...body.restorationContext, token: 'forbidden' } },
682
+ { ...body, restorationContext: { priorReleaseId: restoration.priorReleaseId } },
683
+ { ...body, consolidation: { ...body.consolidation, mode: 'initial_repository' } },
684
+ { ...body, consolidation: { mode: 'repository_update', includeApprovedOutputs: true, requireAtlasAssociation: true } },
685
+ { ...body, snapshot: { ...body.snapshot, privateKey: 'forbidden' } },
686
+ ])
687
+ assert.equal((await validate(changed)).ok, false);
688
+ }
689
+ else
690
+ assert.equal(body.restorationContext, undefined);
691
+ }
692
+ }
693
+ finally {
694
+ await rm(root, { recursive: true, force: true });
695
+ }
696
+ });
697
+ test('a terminal poll invalidates the previous registry marker and preserves provenance until its exact reconciliation', async () => {
698
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-marker-'));
699
+ try {
700
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox'), inventory = await snapshot(root);
701
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: inventory.manifest.snapshotHash,
702
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
703
+ let terminal = false;
704
+ let candidate;
705
+ const input = { scope, outboxRoot, snapshot: inventory, initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`,
706
+ transport: { signedPost: async (_route, raw) => {
707
+ const body = raw;
708
+ return { ok: true, organizationId: scope.organizationId, candidate: {
709
+ candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
710
+ snapshotHash: body.sourceSnapshotHash, state: 'accepted', releaseId: null
711
+ } };
712
+ }, signedGet: async () => ({ ok: true, organizationId: scope.organizationId,
713
+ candidate: terminal ? { ...candidate, state: 'published', releaseId: '97f61652-a5eb-46e4-930c-9478cd4a9c31' } : candidate }) } };
714
+ candidate = await synchronizeRepositoryCandidate({ ...input, restoration });
715
+ const saved = { ...candidate, sourceManifestHash: sha256(canonicalize(inventory.manifest)), consolidationMode: 'repository_update',
716
+ localBaselineSnapshotHash: restoration.localSnapshotHash, publishedLocalSnapshotHash: null,
717
+ pendingLocalSnapshotHash: restoration.localSnapshotHash, pendingLocalOperationId: candidate.operationId };
718
+ await checkpointRepositoryRestorationRegistry({ ...input, repositoryPackage: saved });
719
+ terminal = true;
720
+ candidate = (await pollRepositoryCandidate(input));
721
+ const path = join(outboxRoot, `${ids.workspaceId}.json`), before = await readFile(path, 'utf8');
722
+ await assert.rejects(synchronizeRepositoryCandidate({ ...input, expectedLatestSourceFingerprint: `sha256:${'e'.repeat(64)}` }), /registry reconciliation/);
723
+ await assert.rejects(checkpointRepositoryRestorationRegistry({ ...input, repositoryPackage: saved }), /registry receipt/);
724
+ assert.equal(await readFile(path, 'utf8'), before);
725
+ const terminalSaved = { ...saved, ...candidate, publishedLocalSnapshotHash: restoration.localSnapshotHash,
726
+ pendingLocalSnapshotHash: null, pendingLocalOperationId: null };
727
+ await assert.rejects(checkpointRepositoryRestorationRegistry({ ...input,
728
+ repositoryPackage: { ...terminalSaved, localBaselineSnapshotHash: `sha256:${'e'.repeat(64)}` } }), /registry receipt/);
729
+ assert.equal(await readFile(path, 'utf8'), before);
730
+ await checkpointRepositoryRestorationRegistry({ ...input, repositoryPackage: terminalSaved });
731
+ await synchronizeRepositoryCandidate({ ...input, expectedLatestSourceFingerprint: `sha256:${'e'.repeat(64)}` });
732
+ }
733
+ finally {
734
+ await rm(root, { recursive: true, force: true });
735
+ }
736
+ });
737
+ test('lost terminal POST response recovers by operation lookup without a second upload', async () => {
738
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-lost-response-'));
739
+ try {
740
+ const scope = { organizationId: 'org_fixture', ...ids }, calls = [];
741
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
742
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
743
+ let candidate = null;
744
+ const transport = {
745
+ signedPost: async (_route, value) => {
746
+ calls.push('POST');
747
+ const body = value;
748
+ candidate = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
749
+ snapshotHash: body.sourceSnapshotHash, state: 'published', releaseId: '97f61652-a5eb-46e4-930c-9478cd4a9c31' };
750
+ throw new Error('simulated_lost_POST_response');
751
+ },
752
+ signedGet: async (route) => {
753
+ calls.push('GET');
754
+ assert.ok(route.includes(`/package-candidates?workspaceId=${ids.workspaceId}&operationId=`));
755
+ return { ok: true, organizationId: scope.organizationId, candidate };
756
+ },
757
+ };
758
+ const input = { transport, outboxRoot: join(root, 'outbox'), scope, snapshot: await snapshot(root),
759
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
760
+ await assert.rejects(synchronizeRepositoryCandidate(input), /simulated_lost_POST_response/);
761
+ const journal = JSON.parse(await readFile(join(root, 'outbox', `${ids.workspaceId}.json`), 'utf8'));
762
+ assert.equal(journal.state, 'pending_upload');
763
+ assert.equal(journal.candidateId, null);
764
+ for (let replay = 0; replay < 2; replay++) {
765
+ assert.deepEqual((await recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash }))?.candidate, candidate);
766
+ }
767
+ assert.deepEqual(calls, ['POST', 'GET', 'GET']);
768
+ candidate = null;
769
+ const before = await readFile(join(root, 'outbox', `${ids.workspaceId}.json`), 'utf8');
770
+ await assert.rejects(recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash }), /upload_unresolved/);
771
+ assert.equal(await readFile(join(root, 'outbox', `${ids.workspaceId}.json`), 'utf8'), before);
772
+ }
773
+ finally {
774
+ await rm(root, { recursive: true, force: true });
775
+ }
776
+ });
777
+ test('lost pending response checkpoints recovery durably and normal polling reaches publication', async () => {
778
+ for (const initialState of ['accepted', 'processing']) {
779
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-pending-recovery-'));
780
+ try {
781
+ const scope = { organizationId: 'org_fixture', ...ids }, calls = [];
782
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
783
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
784
+ let candidate = {};
785
+ const transport = {
786
+ signedPost: async (_route, value) => {
787
+ calls.push('POST');
788
+ const body = value;
789
+ candidate = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: body.operationId,
790
+ snapshotHash: body.sourceSnapshotHash, state: initialState, releaseId: null };
791
+ throw new Error('simulated_lost_POST_response');
792
+ },
793
+ signedGet: async (route) => {
794
+ calls.push(route);
795
+ return { ok: true, organizationId: scope.organizationId, candidate };
796
+ },
797
+ };
798
+ const input = { transport, outboxRoot: join(root, 'outbox'), scope, snapshot: await snapshot(root),
799
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
800
+ await assert.rejects(synchronizeRepositoryCandidate(input), /simulated_lost_POST_response/);
801
+ assert.equal(await pollRepositoryCandidate(input), null);
802
+ const recovered = await recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash });
803
+ assert.ok(recovered);
804
+ await checkpointRepositoryRestorationCandidate({ ...input, recovered });
805
+ // Simulate a registry-write crash after the durable checkpoint: retry
806
+ // must recover by candidate ID with all restoration provenance intact.
807
+ const retried = await recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash });
808
+ assert.deepEqual(retried, recovered);
809
+ await checkpointRepositoryRestorationCandidate({ ...input, recovered: retried });
810
+ candidate = { ...candidate, state: 'processing' };
811
+ assert.equal((await pollRepositoryCandidate(input))?.state, 'processing');
812
+ candidate = { ...candidate, state: 'published', releaseId: '97f61652-a5eb-46e4-930c-9478cd4a9c31' };
813
+ assert.equal((await pollRepositoryCandidate(input))?.state, 'published');
814
+ const path = join(input.outboxRoot, `${scope.workspaceId}.json`), bytes = await readFile(path, 'utf8');
815
+ const stored = JSON.parse(bytes);
816
+ assert.equal(stored.candidateId, candidate.candidateId);
817
+ assert.equal(stored.state, 'published');
818
+ assert.deepEqual(stored.restoration, restoration);
819
+ assert.equal(calls.filter(call => call === 'POST').length, 1);
820
+ assert.ok(calls[1].includes('/package-candidates?workspaceId='));
821
+ assert.ok(calls.slice(2).every(call => call.includes(`/package-candidates/${candidate.candidateId}?`)));
822
+ await assert.rejects(checkpointRepositoryRestorationCandidate({ ...input, recovered }), /terminal state changed/);
823
+ assert.equal(await readFile(path, 'utf8'), bytes);
824
+ for (const changed of [
825
+ { ...retried, planHash: `sha256:${'e'.repeat(64)}` },
826
+ { ...retried, localSnapshotHash: `sha256:${'e'.repeat(64)}` },
827
+ { ...retried, sourceManifestHash: `sha256:${'e'.repeat(64)}` },
828
+ { ...retried, candidate: { ...retried.candidate, operationId: `sha256:${'e'.repeat(64)}` } },
829
+ { ...retried, candidate: { ...retried.candidate, snapshotHash: `sha256:${'e'.repeat(64)}` } },
830
+ { ...retried, candidate: { ...retried.candidate, candidateId: '97f61652-a5eb-46e4-930c-9478cd4a9c31' } },
831
+ ]) {
832
+ await assert.rejects(checkpointRepositoryRestorationCandidate({ ...input, recovered: changed }));
833
+ assert.equal(await readFile(path, 'utf8'), bytes);
834
+ }
835
+ await assert.rejects(checkpointRepositoryRestorationCandidate({ ...input, recovered: retried,
836
+ scope: { ...scope, repositoryAgentId: '97f61652-a5eb-46e4-930c-9478cd4a9c31' } }), /outbox scope/);
837
+ assert.equal(await readFile(path, 'utf8'), bytes);
838
+ }
839
+ finally {
840
+ await rm(root, { recursive: true, force: true });
841
+ }
842
+ }
843
+ });
844
+ test('empty lookup does not retire an unknown upload that can be accepted later', async () => {
845
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-late-admission-'));
846
+ try {
847
+ const scope = { organizationId: 'org_fixture', ...ids };
848
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
849
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
850
+ let serverCandidate = null, admitLate = () => { }, posts = 0;
851
+ const transport = { signedPost: async (_route, body) => {
852
+ posts++;
853
+ const request = body;
854
+ admitLate = () => {
855
+ serverCandidate = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: request.operationId,
856
+ snapshotHash: request.sourceSnapshotHash, state: 'processing', releaseId: null };
857
+ };
858
+ throw new Error('transport_outcome_unknown');
859
+ }, signedGet: async () => ({ ok: true, organizationId: scope.organizationId, candidate: serverCandidate }) };
860
+ const input = { transport, scope, outboxRoot: join(root, 'outbox'), snapshot: await snapshot(root),
861
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
862
+ await assert.rejects(synchronizeRepositoryCandidate(input), /outcome_unknown/);
863
+ const path = join(input.outboxRoot, `${scope.workspaceId}.json`), before = await readFile(path, 'utf8');
864
+ await assert.rejects(recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash }), /upload_unresolved/);
865
+ assert.equal(await readFile(path, 'utf8'), before);
866
+ admitLate();
867
+ const recovered = await recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash });
868
+ assert.ok(recovered);
869
+ await checkpointRepositoryRestorationCandidate({ ...input, recovered });
870
+ assert.equal((await pollRepositoryCandidate(input))?.state, 'processing');
871
+ assert.equal(posts, 1);
872
+ }
873
+ finally {
874
+ await rm(root, { recursive: true, force: true });
875
+ }
876
+ });
877
+ test('archived release identity is rejected at upload, recovery, checkpoint and normal polling', async () => {
878
+ for (const phase of ['upload', 'recovery', 'checkpoint', 'poll', 'terminal-poll']) {
879
+ const root = await mkdtemp(join(tmpdir(), 'dharma-restoration-archived-release-'));
880
+ try {
881
+ const scope = { organizationId: 'org_fixture', ...ids };
882
+ const restoration = { planHash: `sha256:${'b'.repeat(64)}`, localSnapshotHash: `sha256:${'c'.repeat(64)}`,
883
+ baselineSnapshotHash: null, previousOperationId: null, priorReleaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' };
884
+ let candidate = {};
885
+ const transport = { signedPost: async (_route, body) => {
886
+ const request = body;
887
+ candidate = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31', operationId: request.operationId,
888
+ snapshotHash: request.sourceSnapshotHash, state: 'published', releaseId: restoration.priorReleaseId };
889
+ return { ok: true, organizationId: scope.organizationId,
890
+ candidate: phase === 'upload' ? candidate : { ...candidate, state: 'accepted', releaseId: null } };
891
+ }, signedGet: async () => ({ ok: true, organizationId: scope.organizationId, candidate }) };
892
+ const input = { transport, scope, outboxRoot: join(root, 'outbox'), snapshot: await snapshot(root),
893
+ initialRepository: false, expectedLatestSourceFingerprint: `sha256:${'a'.repeat(64)}`, restoration };
894
+ const path = join(input.outboxRoot, `${scope.workspaceId}.json`);
895
+ if (phase === 'upload') {
896
+ await assert.rejects(synchronizeRepositoryCandidate(input), /archived release/);
897
+ assert.equal(JSON.parse(await readFile(path, 'utf8')).state, 'pending_upload');
898
+ continue;
899
+ }
900
+ await synchronizeRepositoryCandidate(input);
901
+ if (phase === 'terminal-poll') {
902
+ const stored = JSON.parse(await readFile(path, 'utf8'));
903
+ await writeFile(path, JSON.stringify({ ...stored, ...candidate }));
904
+ }
905
+ const before = await readFile(path, 'utf8');
906
+ const operation = phase === 'recovery'
907
+ ? recoverRepositoryRestorationCandidate({ ...input, planHash: restoration.planHash })
908
+ : phase === 'checkpoint' ? checkpointRepositoryRestorationCandidate({ ...input, recovered: {
909
+ ...restoration, sourceManifestHash: JSON.parse(before).sourceManifestHash,
910
+ candidate: candidate
911
+ } })
912
+ : pollRepositoryCandidate(input);
913
+ await assert.rejects(operation, /archived release/);
914
+ assert.equal(await readFile(path, 'utf8'), before);
915
+ }
916
+ finally {
917
+ await rm(root, { recursive: true, force: true });
918
+ }
919
+ }
920
+ });
39
921
  test('repository candidate upload persists an exact outbox and polls the same operation after a lost caller response', async () => {
40
922
  const root = await mkdtemp(join(tmpdir(), 'dharma-candidate-'));
41
923
  const calls = [];
@@ -164,6 +1046,30 @@ test('reconciled updates pin the latest source in the signed upload and durable
164
1046
  await rm(root, { recursive: true, force: true });
165
1047
  }
166
1048
  });
1049
+ test('ordinary in-flight candidate adoption without a fence retains a pollable target', async () => {
1050
+ const root = await mkdtemp(join(tmpdir(), 'dharma-candidate-adoption-'));
1051
+ try {
1052
+ const scope = { organizationId: 'org_fixture', ...ids }, outboxRoot = join(root, 'outbox');
1053
+ const candidate = { candidateId: '77f61652-a5eb-46e4-930c-9478cd4a9c31',
1054
+ operationId: `sha256:${'e'.repeat(64)}`, snapshotHash: `sha256:${'f'.repeat(64)}`, state: 'accepted', releaseId: null };
1055
+ await adoptRepositoryCandidate({ scope, outboxRoot, candidate, sourceManifestHash: `sha256:${'d'.repeat(64)}`,
1056
+ consolidationMode: 'repository_update' });
1057
+ let polls = 0;
1058
+ const current = await pollRepositoryCandidate({ scope, outboxRoot, transport: { signedGet: async (route) => {
1059
+ polls++;
1060
+ assert.ok(route.includes(candidate.candidateId));
1061
+ assert.ok(route.includes(encodeURIComponent(candidate.operationId)));
1062
+ return { ok: true, organizationId: scope.organizationId, candidate: { ...candidate, state: 'published',
1063
+ releaseId: '87f61652-a5eb-46e4-930c-9478cd4a9c31' } };
1064
+ } } });
1065
+ assert.equal(polls, 1);
1066
+ assert.equal(current?.state, 'published');
1067
+ assert.equal(JSON.parse(await readFile(join(outboxRoot, `${ids.workspaceId}.json`), 'utf8')).state, 'published');
1068
+ }
1069
+ finally {
1070
+ await rm(root, { recursive: true, force: true });
1071
+ }
1072
+ });
167
1073
  test('repository candidate status polling is independent of repository source observation', async () => {
168
1074
  const root = await mkdtemp(join(tmpdir(), 'dharma-candidate-'));
169
1075
  const candidateId = '77f61652-a5eb-46e4-930c-9478cd4a9c31';