| … | … | @@ -17,7 +17,7 @@ import test from 'brittle' |
| 17 | 17 | import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from '#node:fs' |
| 18 | 18 | import { join } from '#node:path' |
| 19 | 19 | import { tmpdir } from '#node:os' |
| 20 | | import { resolveBundledRuntime } from '../lib/runtime-factory.js' |
| 20 | import { resolveBundledRuntime, defaultRuntimePath } from '../lib/runtime-factory.js' |
| 21 | 21 | |
| 22 | 22 | const HOST = 'linux-x64' |
| 23 | 23 | function store (t) { |
| … | … | @@ -88,3 +88,44 @@ test('a node app is unaffected', (t) => { |
| 88 | 88 | t.is(resolveBundledRuntime(appDir, 'node', HOST, { execPath: '/usr/bin/node' }), '/usr/bin/node', |
| 89 | 89 | 'node resolves to the running node; the bare-only sibling search never applies') |
| 90 | 90 | }) |
| 91 | |
| 92 | // defaultRuntimePath was a THIRD copy of this logic, and one that SHADOWED the fixed resolver: |
| 93 | // finalizeInstall does `runtimePath || resolveBundledRuntime(...)`, and defaultRuntimePath's return is |
| 94 | // always truthy, so on the OTA install/update path the five-candidate resolver was never consulted. It |
| 95 | // checked ONE candidate (apps/pgh/runtime/bare) and otherwise returned the bare name — the same |
| 96 | // PATH-dependent launcher this file exists to prevent, reachable by a different route. |
| 97 | function fakeStore (h) { |
| 98 | return { appsDir: h.apps, appPath: (n) => join(h.apps, n) } |
| 99 | } |
| 100 | |
| 101 | test('defaultRuntimePath delegates — it finds the app\'s OWN bundled runtime', (t) => { |
| 102 | const h = store(t) |
| 103 | // The old single-candidate version could not see this: it only ever looked at apps/pgh. |
| 104 | const own = h.bare('myapp') |
| 105 | t.is(defaultRuntimePath(fakeStore(h), 'myapp'), own, 'the install\'s own runtime wins') |
| 106 | }) |
| 107 | |
| 108 | // The sibling is candidate 4, so it is only REACHED when the CLI is not itself running under bare — |
| 109 | // under bare, process.execPath (candidate 3) legitimately wins first. That ordering is the pre-existing |
| 110 | // contract (see 'falls back to a SIBLING app\'s runtime when the CLI runs under node' above); pinning it |
| 111 | // here proves the delegation honours the real order rather than reimplementing a different one. |
| 112 | test('defaultRuntimePath still finds a sibling pgh runtime (no regression)', (t) => { |
| 113 | const h = store(t) |
| 114 | const sib = h.bare('pgh') |
| 115 | h.app('myapp') |
| 116 | t.is(defaultRuntimePath(fakeStore(h), 'myapp', { execPath: '/usr/bin/node' }), sib, 'the sibling is used when the app ships none and the CLI runs under node') |
| 117 | }) |
| 118 | |
| 119 | // The case that would have saved ashatron: an OTA update is applied BY a process already running under |
| 120 | // bare, so execPath is absolute and runnable even when every bundled lookup misses. |
| 121 | test('defaultRuntimePath falls back to the runtime executing us, not the bare NAME', (t) => { |
| 122 | const h = store(t) |
| 123 | h.app('myapp') |
| 124 | t.is(defaultRuntimePath(fakeStore(h), 'myapp', { execPath: '/opt/bare/bin/bare' }), '/opt/bare/bin/bare', 'execPath is used before giving up') |
| 125 | }) |
| 126 | |
| 127 | test('defaultRuntimePath returns the bare name only when genuinely unresolvable', (t) => { |
| 128 | const h = store(t) |
| 129 | h.app('myapp') |
| 130 | t.is(defaultRuntimePath(fakeStore(h), 'myapp', { execPath: '/usr/bin/node' }), 'bare', 'last resort is unchanged') |
| 131 | }) |