amalgm 0.1.139 → 0.1.140

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.139",
3
+ "version": "0.1.140",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -713,8 +713,6 @@ function envForBuild(
713
713
  AMALGM_DESKTOP_RUNTIME_DELIVERY: desktopRuntimeDelivery(baseEnv),
714
714
  ...(desktopRendererDelivery(baseEnv) === 'bundled-next' ? {
715
715
  NEXT_IMAGE_UNOPTIMIZED: '1',
716
- NEXT_PUBLIC_SUPABASE_URL: '',
717
- NEXT_PUBLIC_SUPABASE_ANON_KEY: '',
718
716
  } : {}),
719
717
  AMALGM_RUNTIME_LABEL: request.lane === 'preview' ? 'preview' : 'main',
720
718
  AMALGM_BRANCH: request.lane,
@@ -916,6 +914,41 @@ function updateChannelArtifactDigest(channelPath, filePath) {
916
914
  }));
917
915
  }
918
916
 
917
+ function readChannelInfo(channelPath) {
918
+ if (!channelPath) return null;
919
+ const raw = fs.readFileSync(channelPath, 'utf8');
920
+ const info = yaml.load(raw);
921
+ if (!info || typeof info !== 'object' || Array.isArray(info)) {
922
+ throw new Error(`Desktop release channel file is not a YAML object: ${channelPath}`);
923
+ }
924
+ return info;
925
+ }
926
+
927
+ function descriptorForChannelArtifact(channelInfo, kind) {
928
+ if (!channelInfo || typeof channelInfo !== 'object' || Array.isArray(channelInfo)) return null;
929
+ const extension = kind === 'zip' ? '.zip' : '.dmg';
930
+ const files = Array.isArray(channelInfo.files) ? channelInfo.files : [];
931
+ const match = files.find((file) => (
932
+ file
933
+ && typeof file === 'object'
934
+ && !Array.isArray(file)
935
+ && cleanString(file.url).endsWith(extension)
936
+ ));
937
+ if (!match) return null;
938
+
939
+ const artifact = cleanString(match.url);
940
+ const size = Number(match.size);
941
+ const sha512 = cleanString(match.sha512);
942
+ if (!artifact || !sha512 || !Number.isFinite(size) || size <= 0) return null;
943
+ return {
944
+ platform: 'macos',
945
+ kind,
946
+ artifact,
947
+ size,
948
+ sha512,
949
+ };
950
+ }
951
+
919
952
  function regenerateArtifactBlockmap(engineDir, artifactPath, env = process.env) {
920
953
  const blockmapPath = `${artifactPath}.blockmap`;
921
954
  run(appBuilderBinaryPath(engineDir, env), [
@@ -1433,7 +1466,10 @@ function publishLegacyGithubBridgeRelease({
1433
1466
  }
1434
1467
  }
1435
1468
 
1436
- function desktopReleaseMetadata({ tag, version, request, refs, shas, sourcePackageVersion, publishTarget, artifacts }) {
1469
+ function desktopReleaseMetadata({ tag, version, request, refs, shas, sourcePackageVersion, publishTarget, artifacts, channelPath }) {
1470
+ const channelInfo = readChannelInfo(channelPath);
1471
+ const primaryDownload = descriptorForChannelArtifact(channelInfo, 'dmg');
1472
+ const updateArtifact = descriptorForChannelArtifact(channelInfo, 'zip');
1437
1473
  return {
1438
1474
  provider: publishTarget.provider,
1439
1475
  updateProvider: publishTarget.updateProvider,
@@ -1451,6 +1487,8 @@ function desktopReleaseMetadata({ tag, version, request, refs, shas, sourcePacka
1451
1487
  sourceRunId: request.delivery || `amalgm-automation-${version}`,
1452
1488
  artifacts: artifacts.map((filePath) => path.basename(filePath)),
1453
1489
  publishedAt: new Date().toISOString(),
1490
+ ...(primaryDownload ? { primaryDownload } : {}),
1491
+ ...(updateArtifact ? { updateArtifact } : {}),
1454
1492
  };
1455
1493
  }
1456
1494
 
@@ -1768,8 +1806,6 @@ function publishDesktopRelease(request, options = {}) {
1768
1806
  env: {
1769
1807
  ...releaseEnv,
1770
1808
  NEXT_IMAGE_UNOPTIMIZED: '1',
1771
- NEXT_PUBLIC_SUPABASE_URL: '',
1772
- NEXT_PUBLIC_SUPABASE_ANON_KEY: '',
1773
1809
  },
1774
1810
  });
1775
1811
  }
@@ -1812,7 +1848,17 @@ function publishDesktopRelease(request, options = {}) {
1812
1848
  releaseUrl = `https://github.com/${releaseRepository.fullName}/releases/tag/${tag}`;
1813
1849
  legacyBridge = desktopLegacyBridgePlan(buildEnv, publishTarget);
1814
1850
  } else {
1815
- const metadata = desktopReleaseMetadata({ tag, version, request, refs, shas, sourcePackageVersion, publishTarget, artifacts });
1851
+ const metadata = desktopReleaseMetadata({
1852
+ tag,
1853
+ version,
1854
+ request,
1855
+ refs,
1856
+ shas,
1857
+ sourcePackageVersion,
1858
+ publishTarget,
1859
+ artifacts,
1860
+ channelPath,
1861
+ });
1816
1862
  uploadFlyReleaseAssets({ artifactPaths: artifacts, cwd: engineDir, env: buildEnv, publishTarget, metadata });
1817
1863
  legacyBridge = publishLegacyGithubBridgeRelease({
1818
1864
  tag,
@@ -1884,6 +1930,7 @@ module.exports = {
1884
1930
  desktopReleaseProvider,
1885
1931
  desktopReleasePublishTarget,
1886
1932
  desktopReleaseToolEnv,
1933
+ desktopReleaseMetadata,
1887
1934
  desktopUpdateUrlForLane,
1888
1935
  desktopVersion,
1889
1936
  desktopReleaseArtifactPaths,
@@ -344,6 +344,22 @@ function normalizeModelSettings(value) {
344
344
  return normalized;
345
345
  }
346
346
 
347
+ function normalizeAgentPreferences(value) {
348
+ if (!isPlainObject(value)) return {};
349
+ const normalized = {};
350
+ for (const [selectionId, preference] of Object.entries(value)) {
351
+ if (!selectionId || !isPlainObject(preference)) continue;
352
+ const modelSettings = normalizeModelSettings({ [selectionId]: preference.modelSettings })[selectionId];
353
+ normalized[selectionId] = {
354
+ ...(cleanString(preference.harness) ? { harness: cleanString(preference.harness) } : {}),
355
+ ...(cleanString(preference.model) ? { model: cleanString(preference.model) } : {}),
356
+ ...(cleanString(preference.authMethod) ? { authMethod: cleanString(preference.authMethod) } : {}),
357
+ ...(modelSettings ? { modelSettings } : {}),
358
+ };
359
+ }
360
+ return normalized;
361
+ }
362
+
347
363
  function normalizeReasoningEffort(value) {
348
364
  if (typeof value !== 'string') return null;
349
365
  const normalized = value.trim().toLowerCase().replace(/_/g, '-');
@@ -422,6 +438,7 @@ function normalizePreferences(input) {
422
438
 
423
439
  const rawLastUsed = isPlainObject(raw.last_used) ? raw.last_used : {};
424
440
  const lastUsedHarness = coerceHarnessId(rawLastUsed.harness, 'claude_code');
441
+ const lastUsedAgent = cleanString(rawLastUsed.agent) || lastUsedHarness;
425
442
  const rawLastModel =
426
443
  rawLastUsed.model ||
427
444
  recentPrefs[lastUsedHarness] ||
@@ -429,10 +446,13 @@ function normalizePreferences(input) {
429
446
  const lastUsedModel =
430
447
  normalizeModelSelection(lastUsedHarness, rawLastModel).modelId ||
431
448
  rawLastModel;
432
- recentPrefs[lastUsedHarness] = lastUsedModel;
449
+ if (lastUsedAgent === lastUsedHarness) {
450
+ recentPrefs[lastUsedHarness] = lastUsedModel;
451
+ }
433
452
 
434
453
  return {
435
454
  last_used: {
455
+ agent: lastUsedAgent,
436
456
  harness: lastUsedHarness,
437
457
  model: lastUsedModel,
438
458
  cwd: normalizePreferenceCwd(rawLastUsed.cwd ?? raw.last_cwd ?? raw.selected_cwd),
@@ -440,6 +460,7 @@ function normalizePreferences(input) {
440
460
  usage,
441
461
  recent_prefs: recentPrefs,
442
462
  model_settings: normalizeModelSettings(raw.model_settings),
463
+ agent_preferences: normalizeAgentPreferences(raw.agent_preferences),
443
464
  };
444
465
  }
445
466
 
@@ -510,6 +531,10 @@ function mergePreferencesPatch(current, patch) {
510
531
  bodyLastUsed.harness,
511
532
  current.last_used.harness,
512
533
  );
534
+ const lastUsedAgent =
535
+ cleanString(bodyLastUsed.agent)
536
+ || current.last_used.agent
537
+ || lastUsedHarness;
513
538
  const bodyRecentPrefs = isPlainObject(body.recent_prefs) ? body.recent_prefs : {};
514
539
  const recentPrefs = {
515
540
  ...current.recent_prefs,
@@ -528,6 +553,7 @@ function mergePreferencesPatch(current, patch) {
528
553
 
529
554
  return normalizePreferences({
530
555
  last_used: {
556
+ agent: lastUsedAgent,
531
557
  harness: lastUsedHarness,
532
558
  model: lastUsedModel,
533
559
  cwd,
@@ -537,6 +563,7 @@ function mergePreferencesPatch(current, patch) {
537
563
  : current.usage,
538
564
  recent_prefs: recentPrefs,
539
565
  model_settings: mergeModelSettingsPatch(current.model_settings, body.model_settings),
566
+ agent_preferences: mergeAgentPreferencesPatch(current.agent_preferences, body.agent_preferences),
540
567
  });
541
568
  }
542
569
 
@@ -553,6 +580,29 @@ function mergeModelSettingsPatch(currentModelSettings, patchModelSettings) {
553
580
  return next;
554
581
  }
555
582
 
583
+ function mergeAgentPreferencesPatch(currentAgentPreferences, patchAgentPreferences) {
584
+ if (!isPlainObject(patchAgentPreferences)) return currentAgentPreferences;
585
+ const next = normalizeAgentPreferences(currentAgentPreferences);
586
+ for (const [selectionId, value] of Object.entries(patchAgentPreferences)) {
587
+ if (!isPlainObject(value)) continue;
588
+ const current = isPlainObject(next[selectionId]) ? { ...next[selectionId] } : {};
589
+ if (Object.prototype.hasOwnProperty.call(value, 'modelSettings')) {
590
+ delete current.modelSettings;
591
+ }
592
+ if (Object.prototype.hasOwnProperty.call(value, 'model') && !cleanString(value.model)) {
593
+ delete current.model;
594
+ }
595
+ if (Object.prototype.hasOwnProperty.call(value, 'authMethod') && !cleanString(value.authMethod)) {
596
+ delete current.authMethod;
597
+ }
598
+ next[selectionId] = {
599
+ ...current,
600
+ ...normalizeAgentPreferences({ [selectionId]: value })[selectionId],
601
+ };
602
+ }
603
+ return next;
604
+ }
605
+
556
606
  function updateUserPreferences(patch, options = {}) {
557
607
  const current = readUserPreferences();
558
608
  return writeUserPreferences(mergePreferencesPatch(current, patch), options);
@@ -9,7 +9,7 @@ async function handlePreferenceRoutes(ctx) {
9
9
  }
10
10
  if (ctx.pathname === '/user-preferences' && ctx.method === 'PUT') {
11
11
  const preferences = prefsStore.updateUserPreferences(await ctx.readJsonBody());
12
- ctx.sendJson(200, { success: true, preferences });
12
+ ctx.sendJson(200, preferences);
13
13
  return true;
14
14
  }
15
15
  return false;
@@ -10,7 +10,7 @@ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-agents-source-tes
10
10
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
11
11
 
12
12
  const { closeLocalDb } = require('../state/db');
13
- const { writeUserPreferences } = require('../lib/prefs');
13
+ const { readUserPreferences, updateUserPreferences, writeUserPreferences } = require('../lib/prefs');
14
14
  const {
15
15
  createAgent,
16
16
  getAllAgentsWithBuiltins,
@@ -81,6 +81,57 @@ test('custom agent auth and model settings survive normalization', () => {
81
81
  assert.equal(Object.prototype.hasOwnProperty.call(agent, 'tools'), false);
82
82
  });
83
83
 
84
+ test('user preferences preserve the last-used custom agent selection across patches', () => {
85
+ writeUserPreferences({
86
+ last_used: {
87
+ agent: 'custom-review-agent',
88
+ harness: 'codex',
89
+ model: 'openai/gpt-5.5',
90
+ cwd: tempRoot,
91
+ },
92
+ });
93
+
94
+ updateUserPreferences({
95
+ last_used: {
96
+ model: 'openai/gpt-5.5-mini',
97
+ },
98
+ });
99
+
100
+ const preferences = readUserPreferences();
101
+ assert.equal(preferences.last_used.agent, 'custom-review-agent');
102
+ assert.equal(preferences.last_used.harness, 'codex');
103
+ assert.equal(preferences.last_used.model, 'openai/gpt-5.5-mini');
104
+ });
105
+
106
+ test('user preferences preserve per-agent model/auth/settings overlays across patches', () => {
107
+ writeUserPreferences({
108
+ agent_preferences: {
109
+ 'custom-review-agent': {
110
+ harness: 'codex',
111
+ model: 'openai/gpt-5.5',
112
+ authMethod: 'provider_auth',
113
+ modelSettings: { effort: 'medium' },
114
+ },
115
+ },
116
+ });
117
+
118
+ updateUserPreferences({
119
+ agent_preferences: {
120
+ 'custom-review-agent': {
121
+ authMethod: 'amalgm',
122
+ },
123
+ },
124
+ });
125
+
126
+ const preferences = readUserPreferences();
127
+ assert.deepEqual(preferences.agent_preferences['custom-review-agent'], {
128
+ harness: 'codex',
129
+ model: 'openai/gpt-5.5',
130
+ authMethod: 'amalgm',
131
+ modelSettings: { effort: 'medium' },
132
+ });
133
+ });
134
+
84
135
  function captureSendJson() {
85
136
  const response = {};
86
137
  return {
@@ -18,6 +18,7 @@ const {
18
18
  desktopLegacyBridgePlan,
19
19
  desktopLegacyBridgeRepository,
20
20
  desktopReleaseArtifactPaths,
21
+ desktopReleaseMetadata,
21
22
  desktopReleaseProvider,
22
23
  desktopReleasePublishTarget,
23
24
  desktopReleaseRepository,
@@ -222,6 +223,8 @@ test('desktop release runner preserves explicit bundled desktop builds', () => {
222
223
  {
223
224
  GH_TOKEN: 'token',
224
225
  AMALGM_DESKTOP_THIN: '0',
226
+ NEXT_PUBLIC_SUPABASE_URL: 'https://example.supabase.co',
227
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: 'anon-key',
225
228
  },
226
229
  { lane: 'main', delivery: 'delivery-id' },
227
230
  '0.1.124',
@@ -235,8 +238,8 @@ test('desktop release runner preserves explicit bundled desktop builds', () => {
235
238
  assert.equal(env.AMALGM_DESKTOP_RENDERER_DELIVERY, 'bundled-next');
236
239
  assert.equal(env.AMALGM_DESKTOP_RUNTIME_DELIVERY, 'bundled-with-npm-handoff');
237
240
  assert.equal(env.NEXT_IMAGE_UNOPTIMIZED, '1');
238
- assert.equal(env.NEXT_PUBLIC_SUPABASE_URL, '');
239
- assert.equal(env.NEXT_PUBLIC_SUPABASE_ANON_KEY, '');
241
+ assert.equal(env.NEXT_PUBLIC_SUPABASE_URL, 'https://example.supabase.co');
242
+ assert.equal(env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 'anon-key');
240
243
  });
241
244
 
242
245
  test('desktop release runner supports bundled UI with npm runtime', () => {
@@ -245,6 +248,8 @@ test('desktop release runner supports bundled UI with npm runtime', () => {
245
248
  GH_TOKEN: 'token',
246
249
  AMALGM_DESKTOP_RENDERER_DELIVERY: 'bundled-next',
247
250
  AMALGM_DESKTOP_RUNTIME_DELIVERY: 'npm',
251
+ NEXT_PUBLIC_SUPABASE_URL: 'https://example.supabase.co',
252
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: 'anon-key',
248
253
  },
249
254
  { lane: 'preview', delivery: 'delivery-id' },
250
255
  '0.1.124-canary.1',
@@ -257,8 +262,8 @@ test('desktop release runner supports bundled UI with npm runtime', () => {
257
262
  assert.equal(env.AMALGM_DESKTOP_RENDERER_DELIVERY, 'bundled-next');
258
263
  assert.equal(env.AMALGM_DESKTOP_RUNTIME_DELIVERY, 'npm');
259
264
  assert.equal(env.NEXT_IMAGE_UNOPTIMIZED, '1');
260
- assert.equal(env.NEXT_PUBLIC_SUPABASE_URL, '');
261
- assert.equal(env.NEXT_PUBLIC_SUPABASE_ANON_KEY, '');
265
+ assert.equal(env.NEXT_PUBLIC_SUPABASE_URL, 'https://example.supabase.co');
266
+ assert.equal(env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 'anon-key');
262
267
  });
263
268
 
264
269
  test('desktop release runner omits blank certificate file secrets', () => {
@@ -507,6 +512,77 @@ test('desktop release runner rewrites bridge channel YAML to absolute Fly asset
507
512
  assert.equal(info.packages.arm64.path, 'https://amalgm-desktop-releases.fly.dev/main/amalgm-0.1.124000001-arm64.pkg');
508
513
  });
509
514
 
515
+ test('desktop release runner records primary download and updater artifacts in release metadata', () => {
516
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-metadata-test-'));
517
+ const channelPath = path.join(tempRoot, 'latest-mac.yml');
518
+ const artifacts = [
519
+ path.join(tempRoot, 'amalgm-local-ui-0.1.124000001-arm64-mac.zip'),
520
+ path.join(tempRoot, 'amalgm-local-ui-0.1.124000001-arm64.dmg'),
521
+ channelPath,
522
+ ];
523
+
524
+ try {
525
+ fs.writeFileSync(channelPath, [
526
+ 'version: 0.1.124000001',
527
+ 'files:',
528
+ ' - url: amalgm-local-ui-0.1.124000001-arm64-mac.zip',
529
+ ' sha512: zip-sha',
530
+ ' size: 123',
531
+ ' - url: amalgm-local-ui-0.1.124000001-arm64.dmg',
532
+ ' sha512: dmg-sha',
533
+ ' size: 456',
534
+ 'path: amalgm-local-ui-0.1.124000001-arm64-mac.zip',
535
+ 'sha512: zip-sha',
536
+ '',
537
+ ].join('\n'));
538
+
539
+ const metadata = desktopReleaseMetadata({
540
+ tag: 'v0.1.124000001',
541
+ version: '0.1.124000001',
542
+ request: {
543
+ lane: 'main',
544
+ branch: 'main',
545
+ repository: 'amalgm-inc/amalgm-engine',
546
+ delivery: 'unit-test',
547
+ },
548
+ refs: {
549
+ engineRef: 'main',
550
+ uiRef: 'main',
551
+ },
552
+ shas: {
553
+ engineSha: '1'.repeat(40),
554
+ uiSha: '2'.repeat(40),
555
+ },
556
+ sourcePackageVersion: '0.1.124',
557
+ publishTarget: {
558
+ provider: 'fly',
559
+ updateProvider: 'generic',
560
+ updateUrl: 'https://amalgm-desktop-releases.fly.dev/main/',
561
+ channelUrl: 'https://amalgm-desktop-releases.fly.dev/main/latest-mac.yml',
562
+ },
563
+ artifacts,
564
+ channelPath,
565
+ });
566
+
567
+ assert.deepEqual(metadata.primaryDownload, {
568
+ platform: 'macos',
569
+ kind: 'dmg',
570
+ artifact: 'amalgm-local-ui-0.1.124000001-arm64.dmg',
571
+ size: 456,
572
+ sha512: 'dmg-sha',
573
+ });
574
+ assert.deepEqual(metadata.updateArtifact, {
575
+ platform: 'macos',
576
+ kind: 'zip',
577
+ artifact: 'amalgm-local-ui-0.1.124000001-arm64-mac.zip',
578
+ size: 123,
579
+ sha512: 'zip-sha',
580
+ });
581
+ } finally {
582
+ fs.rmSync(tempRoot, { recursive: true, force: true });
583
+ }
584
+ });
585
+
510
586
  test('desktop release runner requires all updater artifacts', () => {
511
587
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-artifacts-test-'));
512
588
  const version = '0.1.124000001-preview.1';
@@ -10,6 +10,7 @@ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-desktop-release-s
10
10
  process.env.DATA_ROOT = tempRoot;
11
11
 
12
12
  const {
13
+ channelInfoFromMetadata,
13
14
  cleanupReleaseLane,
14
15
  createServer,
15
16
  latestArtifactName,
@@ -22,10 +23,10 @@ test.after(() => {
22
23
  fs.rmSync(tempRoot, { recursive: true, force: true });
23
24
  });
24
25
 
25
- function writeReleaseMetadata(lane, artifacts) {
26
+ function writeReleaseMetadata(lane, artifacts, extra = {}) {
26
27
  const laneDir = path.join(tempRoot, lane);
27
28
  fs.mkdirSync(laneDir, { recursive: true });
28
- fs.writeFileSync(path.join(laneDir, 'desktop-release.json'), `${JSON.stringify({ lane, artifacts }, null, 2)}\n`);
29
+ fs.writeFileSync(path.join(laneDir, 'desktop-release.json'), `${JSON.stringify({ lane, artifacts, ...extra }, null, 2)}\n`);
29
30
  }
30
31
 
31
32
  function writeLaneFile(lane, fileName, mtimeOffsetMs = 0) {
@@ -53,6 +54,10 @@ function writeReleaseFiles(lane, version, mtimeOffsetMs = 0) {
53
54
 
54
55
  test('desktop release server resolves stable bridge download targets from metadata', () => {
55
56
  const artifacts = [
57
+ 'amalgm-local-ui-0.1.1-arm64-mac.zip',
58
+ 'amalgm-local-ui-0.1.1-arm64-mac.zip.blockmap',
59
+ 'amalgm-local-ui-0.1.1-arm64.dmg',
60
+ 'amalgm-local-ui-0.1.1-arm64.dmg.blockmap',
56
61
  'amalgm-thin-0.1.1-arm64-mac.zip',
57
62
  'amalgm-thin-0.1.1-arm64-mac.zip.blockmap',
58
63
  'amalgm-thin-0.1.1-arm64.dmg',
@@ -69,13 +74,41 @@ test('desktop release server resolves stable bridge download targets from metada
69
74
  assert.deepEqual(parseDownloadPath('/main/download/zip'), { lane: 'main', kind: 'zip' });
70
75
  assert.deepEqual(parseDownloadPath('/main/latest.dmg'), { lane: 'main', kind: 'dmg' });
71
76
  assert.equal(parseDownloadPath('/main/latest-mac.yml'), null);
72
- assert.equal(latestArtifactName({ artifacts }, 'dmg'), 'amalgm-thin-0.1.1-arm64.dmg');
77
+ assert.equal(latestArtifactName({ artifacts }, 'dmg'), 'amalgm-local-ui-0.1.1-arm64.dmg');
73
78
  assert.deepEqual(latestDownloadTarget('main', 'zip'), {
74
79
  metadata: { lane: 'main', artifacts },
75
- artifactName: 'amalgm-thin-0.1.1-arm64-mac.zip',
80
+ artifactName: 'amalgm-local-ui-0.1.1-arm64-mac.zip',
76
81
  });
77
82
  });
78
83
 
84
+ test('desktop release server honors explicit metadata artifact descriptors', () => {
85
+ const metadata = {
86
+ artifacts: [
87
+ 'amalgm-local-ui-0.1.1-arm64-mac.zip',
88
+ 'amalgm-local-ui-0.1.1-arm64.dmg',
89
+ 'amalgm-local-ui-0.1.0-arm64-mac.zip',
90
+ 'amalgm-local-ui-0.1.0-arm64.dmg',
91
+ ],
92
+ primaryDownload: {
93
+ platform: 'macos',
94
+ kind: 'dmg',
95
+ artifact: 'amalgm-local-ui-0.1.1-arm64.dmg',
96
+ size: 456,
97
+ sha512: 'dmg-sha',
98
+ },
99
+ updateArtifact: {
100
+ platform: 'macos',
101
+ kind: 'zip',
102
+ artifact: 'amalgm-local-ui-0.1.1-arm64-mac.zip',
103
+ size: 123,
104
+ sha512: 'zip-sha',
105
+ },
106
+ };
107
+
108
+ assert.equal(latestArtifactName(metadata, 'dmg'), 'amalgm-local-ui-0.1.1-arm64.dmg');
109
+ assert.equal(latestArtifactName(metadata, 'zip'), 'amalgm-local-ui-0.1.1-arm64-mac.zip');
110
+ });
111
+
79
112
  test('desktop release server groups versioned release artifacts', () => {
80
113
  assert.equal(
81
114
  releaseKeyForArtifact('amalgm-0.1.139881837-arm64-mac.zip.blockmap'),
@@ -89,6 +122,14 @@ test('desktop release server groups versioned release artifacts', () => {
89
122
  releaseKeyForArtifact('amalgm-preview-thin-0.1.139882282-preview.1-arm64.dmg'),
90
123
  'amalgm-preview-thin@0.1.139882282-preview.1',
91
124
  );
125
+ assert.equal(
126
+ releaseKeyForArtifact('amalgm-local-ui-0.1.150623855-arm64.dmg'),
127
+ 'amalgm-local-ui@0.1.150623855',
128
+ );
129
+ assert.equal(
130
+ releaseKeyForArtifact('amalgm-preview-local-ui-0.1.139882282-preview.1-arm64.dmg'),
131
+ 'amalgm-preview-local-ui@0.1.139882282-preview.1',
132
+ );
92
133
  assert.equal(releaseKeyForArtifact('latest-mac.yml'), '');
93
134
  });
94
135
 
@@ -121,6 +162,8 @@ test('desktop release server prunes stale lane artifacts after publish', () => {
121
162
 
122
163
  test('desktop release server redirects public bridge download URLs to current assets', async () => {
123
164
  writeReleaseMetadata('preview', [
165
+ 'amalgm-preview-local-ui-0.1.1-preview.1-arm64.zip',
166
+ 'amalgm-preview-local-ui-0.1.1-preview.1-arm64.dmg',
124
167
  'amalgm-preview-thin-0.1.1-preview.1-arm64.zip',
125
168
  'amalgm-preview-thin-0.1.1-preview.1-arm64.dmg',
126
169
  'amalgm-preview-0.1.1-preview.1-arm64.zip',
@@ -137,7 +180,7 @@ test('desktop release server redirects public bridge download URLs to current as
137
180
  assert.equal(response.status, 302);
138
181
  assert.equal(
139
182
  response.headers.get('location'),
140
- `http://127.0.0.1:${port}/preview/amalgm-preview-thin-0.1.1-preview.1-arm64.dmg`,
183
+ `http://127.0.0.1:${port}/preview/amalgm-preview-local-ui-0.1.1-preview.1-arm64.dmg?download=amalgm-preview-macOS.dmg`,
141
184
  );
142
185
 
143
186
  const zipResponse = await fetch(`http://127.0.0.1:${port}/preview/latest.zip`, {
@@ -147,7 +190,7 @@ test('desktop release server redirects public bridge download URLs to current as
147
190
  assert.equal(zipResponse.status, 302);
148
191
  assert.equal(
149
192
  zipResponse.headers.get('location'),
150
- `http://127.0.0.1:${port}/preview/amalgm-preview-thin-0.1.1-preview.1-arm64.zip`,
193
+ `http://127.0.0.1:${port}/preview/amalgm-preview-local-ui-0.1.1-preview.1-arm64.zip?download=amalgm-preview-macOS.zip`,
151
194
  );
152
195
 
153
196
  const forwardedResponse = await fetch(`http://127.0.0.1:${port}/preview/download`, {
@@ -160,9 +203,54 @@ test('desktop release server redirects public bridge download URLs to current as
160
203
  assert.equal(forwardedResponse.status, 302);
161
204
  assert.equal(
162
205
  forwardedResponse.headers.get('location'),
163
- 'https://amalgm-desktop-releases.fly.dev/preview/amalgm-preview-thin-0.1.1-preview.1-arm64.dmg',
206
+ 'https://amalgm-desktop-releases.fly.dev/preview/amalgm-preview-local-ui-0.1.1-preview.1-arm64.dmg?download=amalgm-preview-macOS.dmg',
164
207
  );
165
208
  } finally {
166
209
  await new Promise((resolve) => server.close(resolve));
167
210
  }
168
211
  });
212
+
213
+ test('desktop release server synthesizes updater channel YAML from current metadata and files', async () => {
214
+ const zipName = 'amalgm-local-ui-0.1.200000001-arm64-mac.zip';
215
+ const dmgName = 'amalgm-local-ui-0.1.200000001-arm64.dmg';
216
+ writeLaneFile('main', zipName, -5_000);
217
+ writeLaneFile('main', dmgName, -5_000);
218
+ fs.writeFileSync(path.join(tempRoot, 'main', 'latest-mac.yml'), [
219
+ 'version: 0.1.100000000',
220
+ 'path: stale.zip',
221
+ 'sha512: stale-sha',
222
+ '',
223
+ ].join('\n'));
224
+ writeReleaseMetadata('main', [zipName, dmgName, 'latest-mac.yml'], {
225
+ version: '0.1.200000001',
226
+ publishedAt: '2026-07-05T20:00:00.000Z',
227
+ });
228
+
229
+ const info = channelInfoFromMetadata('main', {
230
+ lane: 'main',
231
+ version: '0.1.200000001',
232
+ artifacts: [zipName, dmgName, 'latest-mac.yml'],
233
+ publishedAt: '2026-07-05T20:00:00.000Z',
234
+ });
235
+ assert.equal(info.version, '0.1.200000001');
236
+ assert.equal(info.path, zipName);
237
+ assert.equal(info.files[0].url, zipName);
238
+ assert.equal(info.files[1].url, dmgName);
239
+
240
+ const server = createServer();
241
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
242
+ const { port } = server.address();
243
+
244
+ try {
245
+ const response = await fetch(`http://127.0.0.1:${port}/main/latest-mac.yml`);
246
+ assert.equal(response.status, 200);
247
+ const parsed = require('js-yaml').load(await response.text());
248
+ assert.equal(parsed.version, '0.1.200000001');
249
+ assert.equal(parsed.path, zipName);
250
+ assert.equal(parsed.files[0].url, zipName);
251
+ assert.equal(parsed.files[1].url, dmgName);
252
+ assert.equal(parsed.releaseDate, '2026-07-05T20:00:00.000Z');
253
+ } finally {
254
+ await new Promise((resolve) => server.close(resolve));
255
+ }
256
+ });