amalgm 0.1.243 → 0.1.245

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/supervisor.js CHANGED
@@ -174,6 +174,8 @@ const SAFE_PROCESS_ENV_KEYS = [
174
174
  'LANG',
175
175
  'LC_ALL',
176
176
  'LOGNAME',
177
+ 'AMALGM_AUTO_UPDATE',
178
+ 'AMALGM_DISABLE_AUTO_UPDATE',
177
179
  'AMALGM_NATIVE_NODE_MODULES',
178
180
  'AMALGM_SKIP_NATIVE_INSTALL',
179
181
  'ELECTRON_RUN_AS_NODE',
@@ -1343,5 +1345,6 @@ module.exports = {
1343
1345
  liveDeclaredRuntimePorts,
1344
1346
  runtimeStateFilesUnder,
1345
1347
  runtimeStateSearchRoots,
1348
+ safeBaseProcessEnv,
1346
1349
  },
1347
1350
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.243",
3
+ "version": "0.1.245",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -363,6 +363,77 @@ test('an official checkpoint with a pending payload is terminal corruption', asy
363
363
  );
364
364
  });
365
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,
402
+ );
403
+
404
+ const transitions = require('../workspace/transition');
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);
435
+ });
436
+
366
437
  test('hydration preserves unknown files and a branch committed while recovery was down', async () => {
367
438
  const sourceRepo = await makeRepo('hydrate-source');
368
439
  const source = sourceRepo.repo;
@@ -440,19 +440,17 @@ async function hydrateRepo(project, plan) {
440
440
  const targetCard = activePlan.card;
441
441
  if (!targetCard?.head?.oid) throw new Error('official card has no commit');
442
442
  await transition.initializeRepo(project.path, plan.card.repo?.remote || null);
443
+ // Object readiness belongs to the physical checkout, not to the pathname or
444
+ // the durable hydration record. The directory at a registered spelling can
445
+ // be replaced while the runtime is down, leaving a valid recovery plan but
446
+ // a fresh object database. Re-prove the complete current plan on every
447
+ // attempt before recovery is allowed to move HEAD, refs, index, or files.
448
+ const prepared = await preparePlan({ path: project.path }, plan);
449
+ if (prepared.state !== 'ready') return prepared;
443
450
  let reduced = pending?.reduced || null;
444
451
  let through = pending?.through || null;
445
452
  if (!pending) {
446
- if (plan.card.objects?.kind !== 'bundle') {
447
- throw new Error('official card has no complete bundle');
448
- }
449
- const bundle = require('./objects').prepareBundleImport(plan.card.objects);
450
- if (bundle.state !== 'ready') {
451
- return { state: 'waiting', reason: `objects-${bundle.state}` };
452
- }
453
- await transition.importBundle(project.path, bundle.file);
454
- reduced = await preparePlan({ path: project.path }, plan);
455
- if (reduced.state !== 'ready') return reduced;
453
+ reduced = prepared;
456
454
  through = planThrough(plan);
457
455
  }
458
456
  const requestDigest = applyingRequestDigest(through, activePlan);