@moldesk/core 0.0.1 → 0.0.3

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.
@@ -0,0 +1,74 @@
1
+ import { type InstallPlan } from "@moldesk/adapters";
2
+ import { type InstallCommandRunner, type InstallProgress, type AssetFetch, type MoldeskPaths } from "@moldesk/runtime";
3
+ import { type ModelManifestV1, type PlatformId, type RuntimeSpec, type SourceSpec } from "@moldesk/registry";
4
+ import { type InstalledModel } from "./install-state.js";
5
+ export interface InstallationPlanResult {
6
+ readonly manifest: ModelManifestV1;
7
+ /** The runtime entry actually selected for install: for a platform-aware python
8
+ * manifest (more than one python `runtimes[]` entry, or one declaring
9
+ * `platforms`), this is re-derived from `manifest.runtimes` for the current host
10
+ * platform, which may differ from the `runtime` argument passed in beyond its
11
+ * `kind`. Legacy single-entry, no-`platforms` manifests keep the argument as-is. */
12
+ readonly runtime: RuntimeSpec;
13
+ /** The runtime's effective source: its own per-entry `source` override when
14
+ * present, otherwise `manifest.source`. Always defined when planning succeeds. */
15
+ readonly effectiveSource: SourceSpec;
16
+ /** The host platform `runtime` was resolved for, when determinable. `undefined`
17
+ * for a docker runtime or a host outside the supported `PlatformId` set. */
18
+ readonly resolvedPlatform?: PlatformId;
19
+ readonly runtimeFingerprint: string;
20
+ readonly targetDir: string;
21
+ readonly plan: InstallPlan;
22
+ readonly alreadyInstalled: boolean;
23
+ readonly estimatedDownloadBytes: number;
24
+ readonly estimatedDiskBytes: number;
25
+ /** True when some contributor to the download estimate (an asset or the runtime
26
+ * itself) declares no size, e.g. a Python dependency resolution whose transfer
27
+ * size can't be known ahead of running `uv pip install`. An unknown size must be
28
+ * treated as cost-worthy, not as zero. */
29
+ readonly downloadSizeUnknown: boolean;
30
+ /** Same as {@link downloadSizeUnknown}, for additional disk usage. */
31
+ readonly diskSizeUnknown: boolean;
32
+ }
33
+ export interface InstallModelOptions {
34
+ paths?: MoldeskPaths;
35
+ reinstall?: boolean;
36
+ runner?: InstallCommandRunner;
37
+ fetch?: AssetFetch;
38
+ uvExecutable?: string;
39
+ onProgress?: (progress: InstallProgress) => void;
40
+ }
41
+ export interface InstallModelResult {
42
+ status: "installed" | "already-installed" | "reinstalled";
43
+ installation: InstalledModel;
44
+ }
45
+ export interface UninstallModelOptions {
46
+ paths?: MoldeskPaths;
47
+ runtime?: RuntimeSpec["kind"];
48
+ all?: boolean;
49
+ }
50
+ export declare function runtimeFingerprint(runtime: RuntimeSpec): string;
51
+ /** v0.1 defaults per the implementation spec: prompt at >= 1 GiB download or >= 5 GiB
52
+ * additional disk. Kept as constants so the policy can change without touching adapters. */
53
+ export declare const INSTALL_CONFIRMATION_DOWNLOAD_BYTES: number;
54
+ export declare const INSTALL_CONFIRMATION_DISK_BYTES: number;
55
+ export interface InstallCostEstimate {
56
+ downloadBytes: number;
57
+ downloadSizeUnknown: boolean;
58
+ diskBytes: number;
59
+ diskSizeUnknown: boolean;
60
+ }
61
+ /**
62
+ * Centralized, cost-aware install confirmation policy. An unknown size (as opposed
63
+ * to a size known to be zero) always requires confirmation, since it cannot be shown
64
+ * to be small. A destructive replacement (--reinstall of an existing installation)
65
+ * always requires confirmation regardless of size.
66
+ */
67
+ export declare function requiresInstallConfirmation(cost: InstallCostEstimate, options?: {
68
+ destructive?: boolean;
69
+ }): boolean;
70
+ export declare function createInstallationPlan(manifest: ModelManifestV1, runtime: RuntimeSpec, paths?: MoldeskPaths, options?: {
71
+ platform?: PlatformId;
72
+ }): Promise<InstallationPlanResult>;
73
+ export declare function installModel(planned: InstallationPlanResult, options?: InstallModelOptions): Promise<InstallModelResult>;
74
+ export declare function uninstallModel(modelName: string, options?: UninstallModelOptions): Promise<InstalledModel[]>;
@@ -0,0 +1,310 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { getAdapter } from "@moldesk/adapters";
6
+ import { cacheAsset, getMoldeskPaths, materializeAsset, managedUvDownloadBytes, modelInstallDir, prepareDockerImage, preparePythonEnvironment, temporaryInstallDir, } from "@moldesk/runtime";
7
+ import { detectHostPlatformId, effectiveRuntimeSource, selectPythonRuntime, } from "@moldesk/registry";
8
+ import { MoldeskError } from "./errors.js";
9
+ import { digest } from "./hash.js";
10
+ import { listInstalledModels, rebuildInstallState, writeInstallState, readInstallState, } from "./install-state.js";
11
+ export function runtimeFingerprint(runtime) {
12
+ return `${runtime.kind}-${digest({ runtime, platform: process.platform, architecture: process.arch })}`;
13
+ }
14
+ function environmentFingerprint(data) {
15
+ return digest(data);
16
+ }
17
+ /** v0.1 defaults per the implementation spec: prompt at >= 1 GiB download or >= 5 GiB
18
+ * additional disk. Kept as constants so the policy can change without touching adapters. */
19
+ export const INSTALL_CONFIRMATION_DOWNLOAD_BYTES = 1 * 1024 ** 3;
20
+ export const INSTALL_CONFIRMATION_DISK_BYTES = 5 * 1024 ** 3;
21
+ /**
22
+ * Centralized, cost-aware install confirmation policy. An unknown size (as opposed
23
+ * to a size known to be zero) always requires confirmation, since it cannot be shown
24
+ * to be small. A destructive replacement (--reinstall of an existing installation)
25
+ * always requires confirmation regardless of size.
26
+ */
27
+ export function requiresInstallConfirmation(cost, options = {}) {
28
+ if (options.destructive)
29
+ return true;
30
+ if (cost.downloadSizeUnknown || cost.diskSizeUnknown)
31
+ return true;
32
+ return cost.downloadBytes >= INSTALL_CONFIRMATION_DOWNLOAD_BYTES || cost.diskBytes >= INSTALL_CONFIRMATION_DISK_BYTES;
33
+ }
34
+ function installFailure(code, message, remediation, details) {
35
+ return new MoldeskError({ code, message, remediation, details });
36
+ }
37
+ /**
38
+ * Resolves the python runtime entry to actually install for `platform`.
39
+ *
40
+ * A legacy manifest whose `runtimes[]` has a single python entry with no
41
+ * `platforms` field (e.g. ProteinMPNN, LigandMPNN) trusts `requested` as-is,
42
+ * exactly as before per-platform runtime entries existed — this keeps every
43
+ * pre-existing manifest and caller (including tests that construct a synthetic
44
+ * runtime variant not literally present in `manifest.runtimes`, to exercise
45
+ * fingerprint coexistence) behaving identically.
46
+ *
47
+ * Any manifest that is platform-aware — more than one python `runtimes[]` entry,
48
+ * or a single entry that declares `platforms` — has its installed entry re-derived
49
+ * from `manifest.runtimes` for the current host platform via `selectPythonRuntime`,
50
+ * ignoring the literal identity of `requested` beyond its `kind`, and failing
51
+ * closed (before any staging directory is created) on zero or multiple matches.
52
+ */
53
+ function resolvePythonRuntimeForInstall(manifest, requested, platform) {
54
+ const pythonEntries = manifest.runtimes.filter((r) => r.kind === "python");
55
+ const platformAware = pythonEntries.length > 1 || pythonEntries.some((r) => r.platforms !== undefined);
56
+ if (!platformAware)
57
+ return requested;
58
+ return selectPythonRuntime(manifest.runtimes, platform);
59
+ }
60
+ async function withModelLock(paths, key, operation) {
61
+ if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(key)) {
62
+ throw installFailure("UNSAFE_MODEL_ID", `Unsafe installation lock key: ${JSON.stringify(key)}.`, "Use a lowercase model id from `moldesk list`.");
63
+ }
64
+ fs.mkdirSync(paths.locks, { recursive: true });
65
+ const lock = path.join(paths.locks, `${key}.lock`);
66
+ for (let attempt = 0; attempt < 2; attempt += 1) {
67
+ try {
68
+ fs.mkdirSync(lock);
69
+ break;
70
+ }
71
+ catch (error) {
72
+ if (error.code !== "EEXIST")
73
+ throw error;
74
+ let stale = false;
75
+ try {
76
+ const owner = JSON.parse(fs.readFileSync(path.join(lock, "owner.json"), "utf8"));
77
+ if (typeof owner.startedAt === "string" && Date.now() - Date.parse(owner.startedAt) > 24 * 60 * 60_000)
78
+ stale = true;
79
+ if (typeof owner.pid === "number") {
80
+ try {
81
+ process.kill(owner.pid, 0);
82
+ }
83
+ catch (error) {
84
+ stale ||= error.code === "ESRCH";
85
+ }
86
+ }
87
+ }
88
+ catch {
89
+ stale = true;
90
+ }
91
+ if (stale && attempt === 0) {
92
+ fs.rmSync(lock, { recursive: true, force: true });
93
+ continue;
94
+ }
95
+ throw installFailure("INSTALL_LOCKED", `Another operation is already changing ${key}.`, "Wait for it to finish, then retry.", { lock });
96
+ }
97
+ }
98
+ fs.writeFileSync(path.join(lock, "owner.json"), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
99
+ try {
100
+ return await operation();
101
+ }
102
+ finally {
103
+ fs.rmSync(lock, { recursive: true, force: true });
104
+ }
105
+ }
106
+ export async function createInstallationPlan(manifest, runtime, paths = getMoldeskPaths(), options = {}) {
107
+ const adapter = getAdapter(manifest.name);
108
+ if (!adapter) {
109
+ throw installFailure("ADAPTER_NOT_FOUND", `No installation adapter is registered for ${manifest.name}.`, "Choose an installable model from `moldesk list`.");
110
+ }
111
+ // Resolve the platform-specific runtime entry (and its effective source) before
112
+ // doing anything else, including before computing the fingerprint/targetDir that
113
+ // depend on it — fails closed via selectPythonRuntime's MoldeskError for a
114
+ // platform-aware manifest with zero or multiple matches.
115
+ const resolvedPlatform = options.platform ?? detectHostPlatformId();
116
+ const resolvedRuntime = runtime.kind === "python"
117
+ ? resolvePythonRuntimeForInstall(manifest, runtime, resolvedPlatform)
118
+ : runtime;
119
+ const effectiveSource = resolvedRuntime.kind === "python"
120
+ ? effectiveRuntimeSource(manifest, resolvedRuntime)
121
+ : manifest.source;
122
+ if (!effectiveSource) {
123
+ throw installFailure("SOURCE_PROVENANCE_MISSING", `${manifest.name} has no pinned source provenance.`, "The registry entry must include a repository and immutable revision.");
124
+ }
125
+ const fingerprint = runtimeFingerprint(resolvedRuntime);
126
+ const targetDir = modelInstallDir(manifest.name, manifest.modelVersion, fingerprint, { MOLDESK_HOME: paths.home });
127
+ const adapterPlan = await adapter.installPlan({ manifestName: manifest.name, modelDir: targetDir, assetsDir: path.join(targetDir, "assets") });
128
+ const plan = Object.freeze({ steps: Object.freeze([...adapterPlan.steps]) });
129
+ const assets = manifest.assets ?? [];
130
+ const assetDownloadBytes = assets.reduce((sum, asset) => sum + (asset.sizeBytes ?? 0), 0);
131
+ const assetSizeUnknown = assets.some((asset) => asset.sizeBytes === undefined);
132
+ // A Python runtime's estimatedDownloadBytes/estimatedDiskBytes describe dependency
133
+ // resolution cost that can't be known ahead of running `uv pip install`; treat an
134
+ // undeclared estimate as unknown rather than silently rounding it to zero.
135
+ const runtimeDownloadUnknown = resolvedRuntime.estimatedDownloadBytes === undefined;
136
+ const runtimeDiskUnknown = resolvedRuntime.estimatedDiskBytes === undefined;
137
+ return Object.freeze({
138
+ manifest,
139
+ runtime: resolvedRuntime,
140
+ effectiveSource,
141
+ resolvedPlatform: resolvedRuntime.kind === "python" ? resolvedPlatform : undefined,
142
+ runtimeFingerprint: fingerprint,
143
+ targetDir,
144
+ plan,
145
+ alreadyInstalled: listInstalledModels(paths).some((record) => record.installDir === targetDir),
146
+ estimatedDownloadBytes: assetDownloadBytes + (resolvedRuntime.estimatedDownloadBytes ?? 0) +
147
+ (resolvedRuntime.kind === "python" ? managedUvDownloadBytes(paths) : 0),
148
+ estimatedDiskBytes: resolvedRuntime.estimatedDiskBytes ?? 0,
149
+ downloadSizeUnknown: assetSizeUnknown || runtimeDownloadUnknown,
150
+ diskSizeUnknown: runtimeDiskUnknown,
151
+ });
152
+ }
153
+ export async function installModel(planned, options = {}) {
154
+ const paths = options.paths ?? getMoldeskPaths();
155
+ const { manifest, runtime, effectiveSource, resolvedPlatform, runtimeFingerprint: fingerprint, targetDir } = planned;
156
+ const key = `${manifest.name}-${manifest.modelVersion}-${fingerprint}`;
157
+ return withModelLock(paths, key, async () => {
158
+ const adapter = getAdapter(manifest.name);
159
+ if (!adapter || !effectiveSource)
160
+ throw installFailure("ADAPTER_NOT_FOUND", `Cannot install ${manifest.name}.`, "Repair the registry installation.");
161
+ const existing = listInstalledModels(paths).find((record) => record.installDir === targetDir);
162
+ if (existing && !options.reinstall) {
163
+ const verification = await adapter.verifyInstallation({ manifestName: manifest.name, modelDir: targetDir, assetsDir: path.join(targetDir, "assets"), runner: options.runner, ...(resolvedPlatform !== undefined ? { platform: resolvedPlatform } : {}) });
164
+ if (verification.passed)
165
+ return { status: "already-installed", installation: existing };
166
+ throw installFailure("INSTALLATION_DAMAGED", `${manifest.name} is installed but verification failed: ${verification.output ?? "unknown reason"}.`, `Run \`moldesk install ${manifest.name} --reinstall --yes\`.`);
167
+ }
168
+ fs.mkdirSync(path.dirname(targetDir), { recursive: true });
169
+ const staging = temporaryInstallDir(targetDir);
170
+ const backup = `${targetDir}.backup-${randomUUID()}`;
171
+ let promoted = false;
172
+ let backedUp = false;
173
+ try {
174
+ let dependencyLock = [];
175
+ let runtimeVersion = "";
176
+ let postInstall = [];
177
+ if (runtime.kind === "python") {
178
+ const prepared = await preparePythonEnvironment({
179
+ targetDir: staging,
180
+ repository: effectiveSource.repository,
181
+ revision: effectiveSource.revision,
182
+ runtime,
183
+ runner: options.runner,
184
+ fetch: options.fetch,
185
+ paths,
186
+ uvExecutable: options.uvExecutable,
187
+ onProgress: options.onProgress,
188
+ });
189
+ dependencyLock = prepared.lock;
190
+ runtimeVersion = prepared.pythonVersion;
191
+ postInstall = prepared.postInstall;
192
+ }
193
+ else {
194
+ const prepared = await prepareDockerImage(runtime, options.runner);
195
+ dependencyLock = [prepared.image];
196
+ runtimeVersion = prepared.digest;
197
+ }
198
+ fs.mkdirSync(path.join(staging, "assets"), { recursive: true });
199
+ for (const asset of manifest.assets ?? []) {
200
+ const object = await cacheAsset(asset, paths, { fetch: options.fetch, onProgress: options.onProgress });
201
+ await materializeAsset(asset, object, path.join(staging, "assets"));
202
+ }
203
+ options.onProgress?.({ step: "verify", message: "Verifying installation" });
204
+ const verification = await adapter.verifyInstallation({ manifestName: manifest.name, modelDir: staging, assetsDir: path.join(staging, "assets"), runner: options.runner, ...(resolvedPlatform !== undefined ? { platform: resolvedPlatform } : {}) });
205
+ if (!verification.passed) {
206
+ throw installFailure("INSTALL_VERIFICATION_FAILED", `Verification failed for ${manifest.name}: ${verification.output ?? "unknown reason"}.`, "Review logs and retry with --reinstall.");
207
+ }
208
+ const now = new Date().toISOString();
209
+ const record = {
210
+ schemaVersion: 1,
211
+ installationId: randomUUID(),
212
+ model: manifest.name,
213
+ displayName: manifest.displayName,
214
+ modelVersion: manifest.modelVersion,
215
+ adapterVersion: manifest.adapterVersion,
216
+ runtime: {
217
+ kind: runtime.kind,
218
+ fingerprint,
219
+ ...(runtime.kind === "python" ? {
220
+ python: {
221
+ version: runtimeVersion,
222
+ executable: path.join(targetDir, ".venv", "bin", "python"),
223
+ lockSha256: digest(dependencyLock),
224
+ ...(resolvedPlatform !== undefined ? { platform: resolvedPlatform } : {}),
225
+ platformSelected: runtime.platforms !== undefined,
226
+ postInstall,
227
+ },
228
+ } : { docker: { image: runtime.image, digest: runtime.digest } }),
229
+ },
230
+ environmentFingerprint: environmentFingerprint({
231
+ runtime,
232
+ runtimeVersion,
233
+ dependencyLock,
234
+ sourceRevision: effectiveSource.revision,
235
+ platform: process.platform,
236
+ architecture: process.arch,
237
+ release: os.release(),
238
+ }),
239
+ installDir: targetDir,
240
+ source: effectiveSource,
241
+ assets: (manifest.assets ?? []).map((asset) => ({
242
+ id: asset.id,
243
+ path: path.join(targetDir, "assets", asset.target),
244
+ sha256: asset.sha256.toLowerCase(),
245
+ sizeBytes: asset.sizeBytes ?? 0,
246
+ })),
247
+ manifestSha256: digest(manifest),
248
+ verification: { passed: true, ...(verification.output ? { output: verification.output } : {}) },
249
+ dependencyLock,
250
+ installedAt: existing?.installedAt ?? now,
251
+ verifiedAt: now,
252
+ status: "ready",
253
+ };
254
+ fs.writeFileSync(path.join(staging, "installation.json"), `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
255
+ if (fs.existsSync(targetDir)) {
256
+ fs.renameSync(targetDir, backup);
257
+ backedUp = true;
258
+ }
259
+ fs.renameSync(staging, targetDir);
260
+ promoted = true;
261
+ rebuildInstallState(paths);
262
+ if (backedUp)
263
+ fs.rmSync(backup, { recursive: true, force: true });
264
+ return { status: existing ? "reinstalled" : "installed", installation: record };
265
+ }
266
+ catch (error) {
267
+ if (promoted)
268
+ fs.rmSync(targetDir, { recursive: true, force: true });
269
+ if (backedUp && fs.existsSync(backup))
270
+ fs.renameSync(backup, targetDir);
271
+ fs.rmSync(staging, { recursive: true, force: true });
272
+ throw error;
273
+ }
274
+ });
275
+ }
276
+ export async function uninstallModel(modelName, options = {}) {
277
+ const paths = options.paths ?? getMoldeskPaths();
278
+ return withModelLock(paths, modelName, async () => {
279
+ const allRecords = readInstallState(paths).installations.filter((record) => record.model === modelName);
280
+ const records = options.runtime ? allRecords.filter((record) => record.runtime.kind === options.runtime) : allRecords;
281
+ if (records.length === 0)
282
+ return [];
283
+ if (!options.runtime && !options.all && records.length > 1) {
284
+ throw installFailure("RUNTIME_SELECTION_REQUIRED", `${modelName} has ${records.length} installed runtime variants.`, `Pass --runtime python|docker to remove one variant, or --all to remove all variants.`);
285
+ }
286
+ const moved = [];
287
+ const priorState = readInstallState(paths);
288
+ try {
289
+ for (const record of records) {
290
+ const expected = modelInstallDir(record.model, record.modelVersion, record.runtime.fingerprint, { MOLDESK_HOME: paths.home });
291
+ if (fs.existsSync(expected)) {
292
+ const trash = `${expected}.removing-${randomUUID()}`;
293
+ fs.renameSync(expected, trash);
294
+ moved.push({ from: expected, to: trash });
295
+ }
296
+ }
297
+ rebuildInstallState(paths);
298
+ for (const item of moved)
299
+ fs.rmSync(item.to, { recursive: true, force: true });
300
+ return records;
301
+ }
302
+ catch (error) {
303
+ for (const item of moved.reverse())
304
+ if (fs.existsSync(item.to))
305
+ fs.renameSync(item.to, item.from);
306
+ writeInstallState(priorState, paths);
307
+ throw error;
308
+ }
309
+ });
310
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,268 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { getMoldeskPaths } from "@moldesk/runtime";
6
+ import { listAvailableModels } from "@moldesk/registry";
7
+ import { listInstalledModels } from "./install-state.js";
8
+ import { createInstallationPlan, installModel, requiresInstallConfirmation, uninstallModel, INSTALL_CONFIRMATION_DISK_BYTES, INSTALL_CONFIRMATION_DOWNLOAD_BYTES, } from "./installation.js";
9
+ const homes = [];
10
+ function tempPaths() {
11
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "moldesk-core-install-"));
12
+ homes.push(home);
13
+ return getMoldeskPaths({ MOLDESK_HOME: home });
14
+ }
15
+ afterEach(() => {
16
+ for (const home of homes.splice(0))
17
+ fs.rmSync(home, { recursive: true, force: true });
18
+ });
19
+ function successfulRunner(revision) {
20
+ return async (command, args) => {
21
+ if (command === "git" && args[0] === "init") {
22
+ const source = args[1];
23
+ fs.mkdirSync(path.join(source, "vanilla_model_weights"), { recursive: true });
24
+ fs.mkdirSync(path.join(path.dirname(source), "assets", "vanilla_model_weights"), { recursive: true });
25
+ fs.writeFileSync(path.join(source, "protein_mpnn_run.py"), "# pinned source\n");
26
+ fs.writeFileSync(path.join(source, "vanilla_model_weights", "v_48_020.pt"), "weights");
27
+ fs.writeFileSync(path.join(path.dirname(source), "assets", "vanilla_model_weights", "v_48_020.pt"), "weights");
28
+ }
29
+ if (command === "git" && args.includes("rev-parse"))
30
+ return { code: 0, stdout: `${revision}\n`, stderr: "" };
31
+ if (command === "uv" && args[0] === "venv")
32
+ fs.mkdirSync(path.join(args.at(-1), "bin"), { recursive: true });
33
+ if (command === "uv" && args.includes("freeze"))
34
+ return { code: 0, stdout: "numpy==1.26.4\ntorch==2.2.1\n", stderr: "" };
35
+ if (args[0] === "--version")
36
+ return { code: 0, stdout: "Python 3.11.9\n", stderr: "" };
37
+ return { code: 0, stdout: "", stderr: "" };
38
+ };
39
+ }
40
+ describe("model installation lifecycle", () => {
41
+ it("installs atomically, persists state, is idempotent, reinstalls, and uninstalls", async () => {
42
+ const paths = tempPaths();
43
+ const manifest = { ...listAvailableModels().find((item) => item.name === "proteinmpnn"), assets: [] };
44
+ const runtime = manifest.runtimes.find((item) => item.kind === "python");
45
+ const planned = await createInstallationPlan(manifest, runtime, paths);
46
+ const runner = successfulRunner(manifest.source.revision);
47
+ const installed = await installModel(planned, { paths, runner, uvExecutable: "uv" });
48
+ expect(installed.status).toBe("installed");
49
+ expect(fs.existsSync(path.join(planned.targetDir, "installation.json"))).toBe(true);
50
+ expect(listInstalledModels(paths)).toHaveLength(1);
51
+ fs.writeFileSync(path.join(paths.state, "installed-models.json"), "not json");
52
+ expect(listInstalledModels(paths)).toHaveLength(1);
53
+ await expect(installModel(planned, { paths, runner, uvExecutable: "uv" })).resolves.toMatchObject({ status: "already-installed" });
54
+ const sentinel = path.join(planned.targetDir, "keep-on-failed-reinstall");
55
+ fs.writeFileSync(sentinel, "keep");
56
+ const failingRunner = async (command, args, options) => {
57
+ if (command === "uv" && args[0] === "pip" && args[1] === "install")
58
+ return { code: 2, stdout: "", stderr: "resolver failed" };
59
+ return runner(command, args, options);
60
+ };
61
+ await expect(installModel(planned, { paths, runner: failingRunner, uvExecutable: "uv", reinstall: true })).rejects.toMatchObject({ code: "INSTALL_COMMAND_FAILED" });
62
+ expect(fs.readFileSync(sentinel, "utf8")).toBe("keep");
63
+ fs.rmSync(path.join(planned.targetDir, "assets", "vanilla_model_weights", "v_48_020.pt"));
64
+ await expect(installModel(planned, { paths, runner, uvExecutable: "uv" })).rejects.toMatchObject({ code: "INSTALLATION_DAMAGED" });
65
+ await expect(installModel(planned, { paths, runner, uvExecutable: "uv", reinstall: true })).resolves.toMatchObject({ status: "reinstalled" });
66
+ const removed = await uninstallModel("proteinmpnn", { paths });
67
+ expect(removed).toHaveLength(1);
68
+ expect(fs.existsSync(planned.targetDir)).toBe(false);
69
+ expect(listInstalledModels(paths)).toEqual([]);
70
+ });
71
+ it("lets two runtime fingerprints of the same model version coexist and be selected independently", async () => {
72
+ const paths = tempPaths();
73
+ const base = listAvailableModels().find((item) => item.name === "proteinmpnn");
74
+ const manifest = { ...base, assets: [] };
75
+ const pythonRuntime = manifest.runtimes.find((item) => item.kind === "python");
76
+ const otherRuntime = { ...pythonRuntime, python: "3.12" };
77
+ const runner = successfulRunner(manifest.source.revision);
78
+ const plannedA = await createInstallationPlan(manifest, pythonRuntime, paths);
79
+ const plannedB = await createInstallationPlan(manifest, otherRuntime, paths);
80
+ expect(plannedA.runtimeFingerprint).not.toBe(plannedB.runtimeFingerprint);
81
+ expect(plannedA.targetDir).not.toBe(plannedB.targetDir);
82
+ const installedA = await installModel(plannedA, { paths, runner, uvExecutable: "uv" });
83
+ const installedB = await installModel(plannedB, { paths, runner, uvExecutable: "uv" });
84
+ expect(installedA.status).toBe("installed");
85
+ expect(installedB.status).toBe("installed");
86
+ expect(fs.existsSync(plannedA.targetDir)).toBe(true);
87
+ expect(fs.existsSync(plannedB.targetDir)).toBe(true);
88
+ const installed = listInstalledModels(paths);
89
+ expect(installed).toHaveLength(2);
90
+ expect(new Set(installed.map((record) => record.runtime.fingerprint)).size).toBe(2);
91
+ // Neither variant may be removed by an unqualified uninstall while more than one exists.
92
+ await expect(uninstallModel("proteinmpnn", { paths })).rejects.toMatchObject({ code: "RUNTIME_SELECTION_REQUIRED" });
93
+ const removedAll = await uninstallModel("proteinmpnn", { paths, all: true });
94
+ expect(removedAll).toHaveLength(2);
95
+ expect(fs.existsSync(plannedA.targetDir)).toBe(false);
96
+ expect(fs.existsSync(plannedB.targetDir)).toBe(false);
97
+ expect(listInstalledModels(paths)).toEqual([]);
98
+ });
99
+ it("rolls back a failed fresh installation", async () => {
100
+ const paths = tempPaths();
101
+ const manifest = { ...listAvailableModels().find((item) => item.name === "proteinmpnn"), assets: [] };
102
+ const runtime = manifest.runtimes.find((item) => item.kind === "python");
103
+ const planned = await createInstallationPlan(manifest, runtime, paths);
104
+ const base = successfulRunner(manifest.source.revision);
105
+ const runner = async (command, args, options) => {
106
+ if (command === "uv" && args[0] === "pip" && args[1] === "install")
107
+ return { code: 2, stdout: "", stderr: "resolver failed" };
108
+ return base(command, args, options);
109
+ };
110
+ await expect(installModel(planned, { paths, runner, uvExecutable: "uv" })).rejects.toMatchObject({ code: "INSTALL_COMMAND_FAILED" });
111
+ expect(fs.existsSync(planned.targetDir)).toBe(false);
112
+ expect(listInstalledModels(paths)).toEqual([]);
113
+ expect(fs.existsSync(path.dirname(planned.targetDir)) ? fs.readdirSync(path.dirname(planned.targetDir)).some((name) => name.includes(".partial-")) : false).toBe(false);
114
+ });
115
+ });
116
+ /** A synthetic two-platform manifest built on the registered "proteinmpnn" adapter
117
+ * (so `installPlan`/`verifyInstallation` resolve) but with two platform-scoped
118
+ * python runtime entries and distinct per-entry `source` overrides, exactly the
119
+ * shape a future dual-runtime manifest (e.g. Boltz) will use. */
120
+ function dualPlatformManifest() {
121
+ const base = listAvailableModels().find((item) => item.name === "proteinmpnn");
122
+ const darwinSource = { repository: "https://github.com/example/darwin-fork", revision: "a".repeat(40) };
123
+ const linuxSource = { repository: "https://github.com/example/linux-official", revision: "b".repeat(40) };
124
+ const runtimes = [
125
+ {
126
+ kind: "python",
127
+ python: "3.11",
128
+ installer: "uv",
129
+ platforms: ["darwin-arm64"],
130
+ source: darwinSource,
131
+ postInstall: ["boltz-fix-macos-libomp"],
132
+ requirements: [{ name: "torch", version: "2.6.0" }],
133
+ },
134
+ {
135
+ kind: "python",
136
+ python: "3.11",
137
+ installer: "uv",
138
+ platforms: ["linux-x64"],
139
+ source: linuxSource,
140
+ requirements: [{ name: "torch", version: "2.2.1" }],
141
+ },
142
+ ];
143
+ return { ...base, source: undefined, assets: [], runtimes };
144
+ }
145
+ function dualPlatformRunner() {
146
+ // Tracks the revision most recently fetched, so `rev-parse HEAD` can echo it back
147
+ // correctly regardless of which of the two entries' distinct revisions is being
148
+ // installed in a given call.
149
+ let lastFetchedRevision = "";
150
+ return async (command, args) => {
151
+ if (command === "git" && args[0] === "init") {
152
+ const source = args[1];
153
+ fs.mkdirSync(path.join(source, "vanilla_model_weights"), { recursive: true });
154
+ fs.mkdirSync(path.join(path.dirname(source), "assets", "vanilla_model_weights"), { recursive: true });
155
+ fs.writeFileSync(path.join(source, "protein_mpnn_run.py"), "# pinned source\n");
156
+ fs.writeFileSync(path.join(source, "vanilla_model_weights", "v_48_020.pt"), "weights");
157
+ fs.writeFileSync(path.join(path.dirname(source), "assets", "vanilla_model_weights", "v_48_020.pt"), "weights");
158
+ }
159
+ if (command === "git" && args.includes("fetch")) {
160
+ lastFetchedRevision = args.at(-1);
161
+ }
162
+ if (command === "git" && args.includes("rev-parse"))
163
+ return { code: 0, stdout: `${lastFetchedRevision}\n`, stderr: "" };
164
+ if (command === "uv" && args[0] === "venv")
165
+ fs.mkdirSync(path.join(args.at(-1), "bin"), { recursive: true });
166
+ if (command === "uv" && args.includes("freeze"))
167
+ return { code: 0, stdout: "torch==2.2.1\n", stderr: "" };
168
+ if (args[0] === "--version")
169
+ return { code: 0, stdout: "Python 3.11.9\n", stderr: "" };
170
+ if (command.endsWith("boltz-fix-macos-libomp"))
171
+ return { code: 0, stdout: "fixed libomp\n", stderr: "" };
172
+ return { code: 0, stdout: "", stderr: "" };
173
+ };
174
+ }
175
+ describe("platform-aware runtime selection", () => {
176
+ it("resolves the darwin-arm64 entry's source/fingerprint/postInstall, and the linux-x64 entry's separately, for the same manifest", async () => {
177
+ const paths = tempPaths();
178
+ const manifest = dualPlatformManifest();
179
+ const darwinPlanned = await createInstallationPlan(manifest, manifest.runtimes[0], paths, { platform: "darwin-arm64" });
180
+ expect(darwinPlanned.effectiveSource).toEqual({ repository: "https://github.com/example/darwin-fork", revision: "a".repeat(40) });
181
+ expect(darwinPlanned.resolvedPlatform).toBe("darwin-arm64");
182
+ expect(darwinPlanned.runtime.platforms).toEqual(["darwin-arm64"]);
183
+ const linuxPlanned = await createInstallationPlan(manifest, manifest.runtimes[1], paths, { platform: "linux-x64" });
184
+ expect(linuxPlanned.effectiveSource).toEqual({ repository: "https://github.com/example/linux-official", revision: "b".repeat(40) });
185
+ expect(linuxPlanned.resolvedPlatform).toBe("linux-x64");
186
+ expect(darwinPlanned.targetDir).not.toBe(linuxPlanned.targetDir);
187
+ expect(darwinPlanned.runtimeFingerprint).not.toBe(linuxPlanned.runtimeFingerprint);
188
+ });
189
+ it("resolves the correct entry regardless of which same-kind candidate the caller passed in (core re-derives from the manifest, not caller identity)", async () => {
190
+ const paths = tempPaths();
191
+ const manifest = dualPlatformManifest();
192
+ // Pass the LINUX entry as `runtime` while requesting the DARWIN platform — core
193
+ // must still resolve the darwin entry, proving selection is platform-driven.
194
+ const planned = await createInstallationPlan(manifest, manifest.runtimes[1], paths, { platform: "darwin-arm64" });
195
+ expect(planned.effectiveSource.repository).toBe("https://github.com/example/darwin-fork");
196
+ });
197
+ it("fails closed with RUNTIME_PLATFORM_UNSUPPORTED before creating any staging directory on an unsupported OS/arch", async () => {
198
+ const paths = tempPaths();
199
+ const manifest = dualPlatformManifest();
200
+ manifest.runtimes = [manifest.runtimes[1]]; // only the linux-x64 entry remains
201
+ await expect(createInstallationPlan(manifest, manifest.runtimes[0], paths, { platform: "darwin-arm64" }))
202
+ .rejects.toMatchObject({ code: "RUNTIME_PLATFORM_UNSUPPORTED" });
203
+ expect(fs.existsSync(paths.models) ? fs.readdirSync(paths.models) : []).toEqual([]);
204
+ });
205
+ it("fails closed with RUNTIME_SELECTION_AMBIGUOUS when two entries both match the same platform", async () => {
206
+ const paths = tempPaths();
207
+ const manifest = dualPlatformManifest();
208
+ manifest.runtimes = [
209
+ manifest.runtimes[0],
210
+ { ...manifest.runtimes[0], python: "3.12" },
211
+ ];
212
+ await expect(createInstallationPlan(manifest, manifest.runtimes[0], paths, { platform: "darwin-arm64" }))
213
+ .rejects.toMatchObject({ code: "RUNTIME_SELECTION_AMBIGUOUS" });
214
+ });
215
+ it("runs postInstall only for the platform entry that declares it, and records platform/postInstall/source provenance", async () => {
216
+ const paths = tempPaths();
217
+ const manifest = dualPlatformManifest();
218
+ const runner = dualPlatformRunner();
219
+ const darwinPlanned = await createInstallationPlan(manifest, manifest.runtimes[0], paths, { platform: "darwin-arm64" });
220
+ const darwinInstalled = await installModel(darwinPlanned, { paths, runner, uvExecutable: "uv" });
221
+ expect(darwinInstalled.installation.runtime.python?.platform).toBe("darwin-arm64");
222
+ expect(darwinInstalled.installation.runtime.python?.platformSelected).toBe(true);
223
+ expect(darwinInstalled.installation.runtime.python?.postInstall).toEqual([
224
+ { hook: "boltz-fix-macos-libomp", stdout: "fixed libomp\n", stderr: "" },
225
+ ]);
226
+ expect(darwinInstalled.installation.source).toEqual({ repository: "https://github.com/example/darwin-fork", revision: "a".repeat(40) });
227
+ const linuxPlanned = await createInstallationPlan(manifest, manifest.runtimes[1], paths, { platform: "linux-x64" });
228
+ const linuxInstalled = await installModel(linuxPlanned, { paths, runner, uvExecutable: "uv" });
229
+ expect(linuxInstalled.installation.runtime.python?.platform).toBe("linux-x64");
230
+ expect(linuxInstalled.installation.runtime.python?.postInstall).toEqual([]);
231
+ expect(linuxInstalled.installation.source).toEqual({ repository: "https://github.com/example/linux-official", revision: "b".repeat(40) });
232
+ });
233
+ it("keeps legacy single-entry, no-platforms manifests fully unaffected: the passed-in runtime is trusted as-is, no selection is performed", async () => {
234
+ const paths = tempPaths();
235
+ const manifest = { ...listAvailableModels().find((item) => item.name === "proteinmpnn"), assets: [] };
236
+ // A synthetic runtime variant not literally present in manifest.runtimes (mirrors
237
+ // the pre-existing "two fingerprints coexist" test) — since this manifest has
238
+ // exactly one python entry with no `platforms` field, it must be trusted as-is,
239
+ // not re-derived from manifest.runtimes.
240
+ const runtime = { ...manifest.runtimes.find((item) => item.kind === "python"), python: "3.12" };
241
+ const planned = await createInstallationPlan(manifest, runtime, paths, { platform: "linux-x64" });
242
+ expect(planned.runtime).toBe(runtime);
243
+ expect(planned.effectiveSource).toEqual(manifest.source);
244
+ expect(planned.resolvedPlatform).toBe("linux-x64");
245
+ });
246
+ });
247
+ describe("requiresInstallConfirmation", () => {
248
+ const knownSmall = { downloadBytes: 180_000_000, downloadSizeUnknown: false, diskBytes: 1_100_000_000, diskSizeUnknown: false };
249
+ it("does not require confirmation for a known small install", () => {
250
+ expect(requiresInstallConfirmation(knownSmall)).toBe(false);
251
+ });
252
+ it("requires confirmation at/above the download threshold", () => {
253
+ expect(requiresInstallConfirmation({ ...knownSmall, downloadBytes: INSTALL_CONFIRMATION_DOWNLOAD_BYTES })).toBe(true);
254
+ expect(requiresInstallConfirmation({ ...knownSmall, downloadBytes: INSTALL_CONFIRMATION_DOWNLOAD_BYTES - 1 })).toBe(false);
255
+ });
256
+ it("requires confirmation at/above the disk threshold", () => {
257
+ expect(requiresInstallConfirmation({ ...knownSmall, diskBytes: INSTALL_CONFIRMATION_DISK_BYTES })).toBe(true);
258
+ expect(requiresInstallConfirmation({ ...knownSmall, diskBytes: INSTALL_CONFIRMATION_DISK_BYTES - 1 })).toBe(false);
259
+ });
260
+ it("treats an unknown size as confirmation-worthy even when the known bytes are small", () => {
261
+ expect(requiresInstallConfirmation({ ...knownSmall, downloadSizeUnknown: true })).toBe(true);
262
+ expect(requiresInstallConfirmation({ ...knownSmall, diskSizeUnknown: true })).toBe(true);
263
+ });
264
+ it("always requires confirmation for a destructive replacement, regardless of size", () => {
265
+ expect(requiresInstallConfirmation(knownSmall, { destructive: true })).toBe(true);
266
+ expect(requiresInstallConfirmation({ downloadBytes: 0, downloadSizeUnknown: false, diskBytes: 0, diskSizeUnknown: false }, { destructive: true })).toBe(true);
267
+ });
268
+ });