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 +1 -1
- package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +53 -6
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +51 -1
- package/runtime/scripts/amalgm-mcp/server/routes/preferences.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/agents-source-of-truth.test.js +52 -1
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +80 -4
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-server.test.js +95 -7
package/package.json
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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,
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
+
});
|