fallow 3.17.0 → 3.19.0

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.
@@ -16,6 +16,9 @@ const { _verifyWithKey, SKIP_ENV } = require("./verify-binary");
16
16
 
17
17
  // ---- shared fixtures ------------------------------------------------------
18
18
 
19
+ const DEFAULT_PLATFORM = "linux";
20
+ const DEFAULT_PACKAGE_NAME = "@fallow-cli/test-platform";
21
+
19
22
  function makeKeypair() {
20
23
  const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
21
24
  const spki = publicKey.export({ format: "der", type: "spki" });
@@ -23,18 +26,18 @@ function makeKeypair() {
23
26
  return { privateKey, rawPub };
24
27
  }
25
28
 
26
- function ext() {
27
- return process.platform === "win32" ? ".exe" : "";
29
+ function packageNameForPlatform(platform) {
30
+ return platform === "win32" ? "@fallow-cli/win32-x64-msvc" : DEFAULT_PACKAGE_NAME;
28
31
  }
29
32
 
30
- function binaryNames() {
31
- // Platform packages ship a single multicall `fallow` binary.
32
- return [`fallow${ext()}`];
33
+ function binaryNames(platform) {
34
+ const ext = platform === "win32" ? ".exe" : "";
35
+ return [`fallow${ext}`, `fallow-similar-code${ext}`];
33
36
  }
34
37
 
35
- function computeDigestsForDir(dir) {
38
+ function computeDigestsForDir(dir, platform) {
36
39
  const out = {};
37
- for (const base of binaryNames()) {
40
+ for (const base of binaryNames(platform)) {
38
41
  const full = path.join(dir, base);
39
42
  out[base] = "sha256:" + crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex");
40
43
  }
@@ -43,8 +46,9 @@ function computeDigestsForDir(dir) {
43
46
 
44
47
  function mkPlatformDir(privateKey, options) {
45
48
  const opts = options || {};
49
+ const platform = opts.platform || DEFAULT_PLATFORM;
46
50
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-lazy-test-"));
47
- for (const base of binaryNames()) {
51
+ for (const base of binaryNames(platform)) {
48
52
  const binaryPath = path.join(dir, base);
49
53
  const content = Buffer.from(`mock ${base}`);
50
54
  fs.writeFileSync(binaryPath, content);
@@ -56,9 +60,9 @@ function mkPlatformDir(privateKey, options) {
56
60
  fs.writeFileSync(
57
61
  path.join(dir, "package.json"),
58
62
  JSON.stringify({
59
- name: opts.packageName || "@fallow-cli/test-platform",
63
+ name: opts.packageName || packageNameForPlatform(platform),
60
64
  version: opts.version || "2.81.0",
61
- fallowDigests: opts.skipDigests ? undefined : computeDigestsForDir(dir),
65
+ fallowDigests: opts.skipDigests ? undefined : computeDigestsForDir(dir, platform),
62
66
  }),
63
67
  );
64
68
  return dir;
@@ -88,13 +92,15 @@ function setupCacheRoot(t) {
88
92
  }
89
93
 
90
94
  function baseInput(dir, verifyFn, extras) {
95
+ const manifestPath = path.join(dir, "package.json");
96
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
91
97
  return {
92
98
  platformPkgDir: dir,
93
- packageName: "@fallow-cli/test-platform",
94
- manifestPath: path.join(dir, "package.json"),
99
+ packageName: manifest.name,
100
+ manifestPath,
95
101
  verifyFn,
96
102
  env: {},
97
- platform: process.platform,
103
+ platform: DEFAULT_PLATFORM,
98
104
  ...extras,
99
105
  };
100
106
  }
@@ -117,7 +123,30 @@ test("ensureVerified verifies on cache miss and writes the sentinel", (t) => {
117
123
  assert.equal(sentinel.schemaVersion, SENTINEL_SCHEMA_VERSION);
118
124
  assert.equal(sentinel.packageVersion, "2.81.0");
119
125
  assert.equal(sentinel.packageName, "@fallow-cli/test-platform");
120
- assert.equal(Object.keys(sentinel.binaries).length, 1);
126
+ assert.equal(Object.keys(sentinel.binaries).length, 2);
127
+ });
128
+
129
+ test("ensureVerified verifies and caches a win32 executable on any host", (t) => {
130
+ _resetWarningState();
131
+ const { privateKey, rawPub } = makeKeypair();
132
+ const dir = mkPlatformDir(privateKey, { platform: "win32" });
133
+ t.after(() => cleanup(dir));
134
+ const input = baseInput(dir, (binaryPath) => _verifyWithKey(binaryPath, rawPub), {
135
+ platform: "win32",
136
+ });
137
+
138
+ const verified = ensureVerified(input);
139
+ assert.equal(verified.ok, true);
140
+ assert.equal(verified.cached, false);
141
+ const sentinel = JSON.parse(fs.readFileSync(verified.sentinelPath, "utf8"));
142
+ assert.deepEqual(Object.keys(sentinel.binaries), ["fallow.exe", "fallow-similar-code.exe"]);
143
+
144
+ const cached = ensureVerified({
145
+ ...input,
146
+ verifyFn: () => assert.fail("signature verification must not run on a cache hit"),
147
+ });
148
+ assert.equal(cached.ok, true);
149
+ assert.equal(cached.cached, true);
121
150
  });
122
151
 
123
152
  test("ensureVerified returns cached:true on a valid sentinel", (t) => {
@@ -154,7 +183,7 @@ test("ensureVerified invalidates sentinel on mtime drift", (t) => {
154
183
 
155
184
  // Bump the mtime of one binary; sentinel should now be stale.
156
185
  const newTime = new Date(Date.now() + 10_000);
157
- fs.utimesSync(path.join(dir, `fallow${ext()}`), newTime, newTime);
186
+ fs.utimesSync(path.join(dir, binaryNames(DEFAULT_PLATFORM)[0]), newTime, newTime);
158
187
 
159
188
  let verifyCallCount = 0;
160
189
  const result = ensureVerified(
@@ -165,7 +194,11 @@ test("ensureVerified invalidates sentinel on mtime drift", (t) => {
165
194
  );
166
195
  assert.equal(result.ok, true);
167
196
  assert.equal(result.cached, false);
168
- assert.equal(verifyCallCount, 1, "verify should rerun for the single binary");
197
+ assert.equal(
198
+ verifyCallCount,
199
+ binaryNames(DEFAULT_PLATFORM).length,
200
+ "verify should rerun for every shipped binary",
201
+ );
169
202
  });
170
203
 
171
204
  test("ensureVerified invalidates sentinel on packageVersion drift", (t) => {
@@ -215,7 +248,9 @@ test("ensureVerified invalidates sentinel on packageName drift", (t) => {
215
248
  packageVersion: "2.81.0",
216
249
  packageName: "@fallow-cli/wrong-name",
217
250
  binaries: {
218
- [`fallow${ext()}`]: { mtimeMs: fs.statSync(path.join(dir, `fallow${ext()}`)).mtimeMs },
251
+ [binaryNames(DEFAULT_PLATFORM)[0]]: {
252
+ mtimeMs: fs.statSync(path.join(dir, binaryNames(DEFAULT_PLATFORM)[0])).mtimeMs,
253
+ },
219
254
  },
220
255
  }),
221
256
  );
@@ -261,7 +296,7 @@ test("ensureVerified invalidates sentinel on schemaVersion drift", (t) => {
261
296
  test("ensureVerified returns sig-invalid on a tampered signature", (t) => {
262
297
  _resetWarningState();
263
298
  const { privateKey, rawPub } = makeKeypair();
264
- const dir = mkPlatformDir(privateKey, { corruptSigFor: `fallow${ext()}` });
299
+ const dir = mkPlatformDir(privateKey, { corruptSigFor: binaryNames(DEFAULT_PLATFORM)[0] });
265
300
  t.after(() => cleanup(dir));
266
301
 
267
302
  const result = ensureVerified(baseInput(dir, (p) => _verifyWithKey(p, rawPub)));
@@ -289,89 +324,43 @@ test("ensureVerified returns digest-unavailable on a pre-#597 manifest", (t) =>
289
324
 
290
325
  test("ensureVerified honors FALLOW_VERIFY_CACHE_DIR when platform pkg dir is non-writable", (t) => {
291
326
  _resetWarningState();
292
- if (process.platform === "win32") {
293
- t.skip("Windows ACL chmod is not portable; covered by sentinel-path tests");
294
- return;
295
- }
296
327
  const { privateKey, rawPub } = makeKeypair();
297
328
  const dir = mkPlatformDir(privateKey);
298
329
  const cacheRoot = setupCacheRoot(t);
330
+ t.after(() => cleanup(dir));
299
331
 
300
- fs.chmodSync(dir, 0o555);
301
- try {
302
- const result = ensureVerified({
303
- ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
304
- env: { FALLOW_VERIFY_CACHE_DIR: cacheRoot },
305
- });
306
- assert.equal(result.ok, true);
307
- assert.equal(result.cached, false);
308
- assert.match(result.sentinelPath, new RegExp(cacheRoot.replace(/\\/g, "\\\\")));
309
- } finally {
310
- fs.chmodSync(dir, 0o755);
311
- cleanup(dir);
312
- }
332
+ const result = ensureVerified({
333
+ ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
334
+ env: { FALLOW_VERIFY_CACHE_DIR: cacheRoot },
335
+ isWritable: (candidate) => candidate !== dir,
336
+ });
337
+ assert.equal(result.ok, true);
338
+ assert.equal(result.cached, false);
339
+ assert.match(result.sentinelPath, new RegExp(cacheRoot.replace(/\\/g, "\\\\")));
313
340
  });
314
341
 
315
- test("ensureVerified emits a single warning when sentinel write fails", (t) => {
342
+ test("ensureVerified emits a single warning when no sentinel location is writable", (t) => {
316
343
  _resetWarningState();
317
- if (process.platform === "win32") {
318
- t.skip("Windows ACL chmod is not portable");
319
- return;
320
- }
321
344
  const { privateKey, rawPub } = makeKeypair();
322
345
  const dir = mkPlatformDir(privateKey);
323
346
  const stderr = captureStderr(t);
347
+ t.after(() => cleanup(dir));
348
+ const input = {
349
+ ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
350
+ homedir: undefined,
351
+ isWritable: () => false,
352
+ };
324
353
 
325
- // Make platform pkg dir non-writable AND point FALLOW_VERIFY_CACHE_DIR at
326
- // a non-existent path. resolveSentinelPath will still find the XDG / home
327
- // fallback, but we can simulate "every cache location read-only" by giving
328
- // it a writable dir that we then chmod down right before ensureVerified
329
- // runs writeSentinel. Simpler: instead of fighting the cascade in env,
330
- // simulate write failure via a non-writable FALLOW_VERIFY_CACHE_DIR that
331
- // PASSES isWritable() but then has its perms revoked between resolve and
332
- // write. Since that race is hard to script, we test the warn-once helper
333
- // via a synthetic path that fails the rename atomically: pass a sentinel-
334
- // chmod-locked dir.
335
- //
336
- // The portable shape: chmod the platform pkg dir read-only AND pass a
337
- // FALLOW_VERIFY_CACHE_DIR that is a regular file (not a dir). isWritable
338
- // returns false for both, so resolveSentinelPath falls through to XDG. If
339
- // the user has no HOME (synthetic env), the cascade returns null. We can
340
- // achieve this only by injecting both env.HOME='' AND env.XDG_CACHE_HOME=''
341
- // AND ensuring os.homedir() is not consulted; ensureVerified does not
342
- // accept a homedir override, so the warn-once path is unreachable via env
343
- // alone on a machine with a real HOME. Skip this test on machines with a
344
- // real homedir; the warn-once helper is otherwise covered by the
345
- // "no-writable-location" branch in sentinel-path tests.
346
- if (os.homedir() && os.homedir().length > 0) {
347
- t.skip(
348
- "warn-once-on-no-writable-cache requires homedir-override knob (covered by sentinel-path unit test)",
349
- );
350
- cleanup(dir);
351
- return;
352
- }
354
+ const result = ensureVerified(input);
355
+ assert.equal(result.sentinelPath, null);
356
+ const warnings = stderr.lines.filter((line) => line.includes("no writable cache location"));
357
+ assert.equal(warnings.length, 1);
353
358
 
354
- fs.chmodSync(dir, 0o555);
355
- try {
356
- const env = { HOME: "", XDG_CACHE_HOME: "" };
357
- const result = ensureVerified({
358
- ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)),
359
- env,
360
- });
361
- if (result.sentinelPath !== null) {
362
- t.diagnostic(`sentinel landed at ${result.sentinelPath} despite empty HOME; skipping`);
363
- return;
364
- }
365
- const warnings = stderr.lines.filter((l) => l.includes("no writable cache location"));
366
- assert.equal(warnings.length, 1);
367
- // Second call: no new warning.
368
- ensureVerified({ ...baseInput(dir, (p) => _verifyWithKey(p, rawPub)), env });
369
- const warnings2 = stderr.lines.filter((l) => l.includes("no writable cache location"));
370
- assert.equal(warnings2.length, 1);
371
- } finally {
372
- fs.chmodSync(dir, 0o755);
373
- cleanup(dir);
374
- }
359
+ ensureVerified(input);
360
+ const repeatedWarnings = stderr.lines.filter((line) =>
361
+ line.includes("no writable cache location"),
362
+ );
363
+ assert.equal(repeatedWarnings.length, 1);
375
364
  });
376
365
 
377
366
  // ---- FALLOW_SKIP_BINARY_VERIFY -------------------------------------------
@@ -457,10 +446,6 @@ test("ensureVerified is idempotent under concurrent first-runs", async (t) => {
457
446
 
458
447
  test("ensureVerified rejects a sentinel written for a different install dir", (t) => {
459
448
  _resetWarningState();
460
- if (process.platform === "win32") {
461
- t.skip("chmod-based read-only platform pkg dir is not portable on Windows");
462
- return;
463
- }
464
449
  // Two installs of the same package + version. install A is clean and writes
465
450
  // a sentinel to a shared cache; install B has a tampered binary at the same
466
451
  // package name + version. B must NOT trust A's sentinel via cache hit even
@@ -471,17 +456,14 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
471
456
  // Tamper install B's fallow binary AFTER mkPlatformDir wrote a valid sig
472
457
  // for the original bytes (mkPlatformDir does not expose a corrupt-binary
473
458
  // option, so simulate the attack by overwriting bytes here).
474
- fs.writeFileSync(path.join(installB, `fallow${ext()}`), Buffer.from("tampered bytes"));
459
+ fs.writeFileSync(
460
+ path.join(installB, binaryNames(DEFAULT_PLATFORM)[0]),
461
+ Buffer.from("tampered bytes"),
462
+ );
475
463
  const sharedCache = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-shared-cache-"));
464
+ const isWritable = (candidate) => candidate !== installA && candidate !== installB;
476
465
 
477
- // Force the shared-cache cascade by making both platform pkg dirs
478
- // non-writable (simulates yarn PnP / Docker layered images / pnpm
479
- // verify-store invariants).
480
- fs.chmodSync(installA, 0o555);
481
- fs.chmodSync(installB, 0o555);
482
466
  t.after(() => {
483
- fs.chmodSync(installA, 0o755);
484
- fs.chmodSync(installB, 0o755);
485
467
  cleanup(installA);
486
468
  cleanup(installB);
487
469
  cleanup(sharedCache);
@@ -492,6 +474,7 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
492
474
  const resultA = ensureVerified({
493
475
  ...baseInput(installA, (p) => _verifyWithKey(p, rawPub)),
494
476
  env: { FALLOW_VERIFY_CACHE_DIR: sharedCache },
477
+ isWritable,
495
478
  });
496
479
  assert.equal(resultA.ok, true);
497
480
  assert.match(resultA.sentinelPath, new RegExp(sharedCache.replace(/\\/g, "\\\\")));
@@ -502,7 +485,6 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
502
485
  // + SHA-256 binding must prevent that.
503
486
  for (const name of binaryNames()) {
504
487
  const aStat = fs.statSync(path.join(installA, name));
505
- fs.chmodSync(path.join(installB, name), 0o644);
506
488
  fs.utimesSync(path.join(installB, name), aStat.atime, aStat.mtime);
507
489
  }
508
490
 
@@ -513,6 +495,7 @@ test("ensureVerified rejects a sentinel written for a different install dir", (t
513
495
  return _verifyWithKey(p, rawPub);
514
496
  }),
515
497
  env: { FALLOW_VERIFY_CACHE_DIR: sharedCache },
498
+ isWritable,
516
499
  });
517
500
  assert.equal(resultB.ok, false);
518
501
  assert.equal(resultB.code, "sig-invalid");
@@ -530,7 +513,7 @@ test("ensureVerified rejects a sentinel where bytes drift but mtime stays", (t)
530
513
 
531
514
  // Tamper the binary in place AND restore the prior mtime, so the mtime
532
515
  // pre-filter matches but the bytes do not.
533
- const binPath = path.join(dir, `fallow${ext()}`);
516
+ const binPath = path.join(dir, binaryNames(DEFAULT_PLATFORM)[0]);
534
517
  const before = fs.statSync(binPath);
535
518
  fs.writeFileSync(binPath, Buffer.from("tampered"));
536
519
  fs.utimesSync(binPath, before.atime, before.mtime);
@@ -77,7 +77,7 @@ test("resolveSentinelPath falls back to FALLOW_VERIFY_CACHE_DIR when platform pk
77
77
  const cacheDir = mkTmp();
78
78
  try {
79
79
  const result = resolveSentinelPath({
80
- platformPkgDir: "/dev/null/not-a-dir",
80
+ platformPkgDir: path.join(cacheDir, "missing-platform-package"),
81
81
  packageName: "@fallow-cli/darwin-arm64",
82
82
  env: { FALLOW_VERIFY_CACHE_DIR: cacheDir },
83
83
  });
@@ -220,12 +220,19 @@ test("resolveSentinelPath honors injected isWritable + ensureDir for full test i
220
220
  test("resolveSentinelPath skips cache-dir-env when ensureDir fails for it", () => {
221
221
  // FALLOW_VERIFY_CACHE_DIR points at a non-creatable path, XDG points at a
222
222
  // creatable one. Confirm the resolver moves past the env override.
223
+ //
224
+ // A directory nested under a regular FILE is the portable way to make mkdir
225
+ // fail: `/dev/null/inside/a/file` only refuses on POSIX, and on Windows the
226
+ // recursive mkdir succeeds against the current drive, which both defeats the
227
+ // assertion and creates C:\dev outside any temp directory.
223
228
  const homeDir = mkTmp();
229
+ const blocker = path.join(homeDir, "not-a-directory");
230
+ fs.writeFileSync(blocker, "");
224
231
  try {
225
232
  const result = resolveSentinelPath({
226
233
  platformPkgDir: undefined,
227
234
  packageName: "@fallow-cli/darwin-arm64",
228
- env: { FALLOW_VERIFY_CACHE_DIR: "/dev/null/inside/a/file" },
235
+ env: { FALLOW_VERIFY_CACHE_DIR: path.join(blocker, "inside", "a", "file") },
229
236
  homedir: homeDir,
230
237
  platform: "darwin",
231
238
  });
@@ -293,15 +293,16 @@ function binaryTargetsForPlatform(platformId) {
293
293
  // and tests can synthesize a Windows verify without running on Windows.
294
294
  const isWindows = typeof platformId === "string" && platformId.startsWith("win32");
295
295
  const ext = isWindows ? ".exe" : "";
296
- // Platform packages ship ONE binary now: the multicall `fallow` (CLI + LSP +
297
- // MCP in one engine). The bundled `fallow-lsp` / `fallow-mcp` launchers live
298
- // in the wrapper and spawn `fallow lsp-server` / `fallow mcp-server`. The
299
- // authoritative digest is the `fallowDigests.fallow` field the release
300
- // pipeline embeds for every bundled package; the `asset` below is only the
301
- // legacy GitHub-release fallback for pre-embedded-digest packages, which
302
- // never shipped this bundled binary, so it is inert for bundled installs and
303
- // fails closed (digest-mismatch) in the impossible case it is ever reached.
304
- return [{ binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` }];
296
+ // The platform package ships the multicall CLI plus the local-only
297
+ // similar-code provider. Both are signed and digest-pinned. Verifying them
298
+ // together keeps the Rust sibling lookup free of project or PATH overrides.
299
+ return [
300
+ { binary: `fallow${ext}`, asset: `fallow-${platformId}${ext}` },
301
+ {
302
+ binary: `fallow-similar-code${ext}`,
303
+ asset: `fallow-similar-code-${platformId}${ext}`,
304
+ },
305
+ ];
305
306
  }
306
307
 
307
308
  function isSkipRequested() {
@@ -239,11 +239,21 @@ test("verifyBinaryAt uses the embedded production public key", () => {
239
239
  }
240
240
  });
241
241
 
242
+ // Mirror binaryTargetsForPlatform: the suffix comes from the platformId under
243
+ // test, never from the live process.platform. A `dirOverride` run without an
244
+ // explicit platformId is labelled "test-platform" by
245
+ // resolvePlatformPackageForVerify, so the binary it looks for is plain `fallow`
246
+ // on every host -- a fixture keyed off process.platform writes `fallow.exe` on a
247
+ // Windows host and the verify then finds nothing.
248
+ function extForPlatformId(platformId) {
249
+ return typeof platformId === "string" && platformId.startsWith("win32") ? ".exe" : "";
250
+ }
251
+
242
252
  function makePlatformDir(privateKey, options) {
243
253
  const opts = options || {};
244
254
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-vbtest-"));
245
- const ext = process.platform === "win32" ? ".exe" : "";
246
- for (const base of ["fallow"]) {
255
+ const ext = extForPlatformId(opts.platformId);
256
+ for (const base of ["fallow", "fallow-similar-code"]) {
247
257
  const binaryPath = path.join(dir, `${base}${ext}`);
248
258
  const content = Buffer.from(`mock ${base} contents`);
249
259
  fs.writeFileSync(binaryPath, content);
@@ -328,6 +338,42 @@ test("verifyInstalled with dirOverride returns ok when every binary verifies", a
328
338
  assert.equal(result.package, "<override>");
329
339
  });
330
340
 
341
+ // binaryTargetsForPlatform reads windows-ness off the platformId so a Windows
342
+ // verify can be synthesized anywhere. Nothing exercised that, which left the
343
+ // `.exe` target unverified on Linux CI and unverified on Windows too.
344
+ test("verifyInstalled verifies the .exe target for a win32 platformId on any host", async (t) => {
345
+ const { privateKey, rawPub } = makeKeypair();
346
+ const platformId = "win32-x64-msvc";
347
+ const dir = makePlatformDir(privateKey, { platformId });
348
+ t.after(() => cleanup(dir));
349
+ assert.ok(fs.existsSync(path.join(dir, "fallow.exe")), "fixture must write the .exe target");
350
+
351
+ const result = await verifyInstalled({
352
+ dirOverride: dir,
353
+ platformId,
354
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
355
+ digestProvider: makeDigestProvider(dir),
356
+ });
357
+ assert.equal(result.ok, true);
358
+ });
359
+
360
+ test("verifyInstalled reports the .exe name when a win32 signature is absent", async (t) => {
361
+ const { privateKey, rawPub } = makeKeypair();
362
+ const platformId = "win32-arm64-msvc";
363
+ const dir = makePlatformDir(privateKey, { platformId, skipSigFor: "fallow" });
364
+ t.after(() => cleanup(dir));
365
+
366
+ const result = await verifyInstalled({
367
+ dirOverride: dir,
368
+ platformId,
369
+ verifyFn: (p) => _verifyWithKey(p, rawPub),
370
+ digestProvider: makeDigestProvider(dir),
371
+ });
372
+ assert.equal(result.ok, false);
373
+ assert.equal(result.code, "sig-missing");
374
+ assert.match(result.message, /fallow\.exe/);
375
+ });
376
+
331
377
  test("verifyInstalled resolves a global npm install from the fallow package directory", async (t) => {
332
378
  const pkg = currentPlatformPackage();
333
379
  if (!pkg) {
@@ -349,7 +395,7 @@ test("verifyInstalled resolves a global npm install from the fallow package dire
349
395
  );
350
396
 
351
397
  const ext = process.platform === "win32" ? ".exe" : "";
352
- for (const base of ["fallow"]) {
398
+ for (const base of ["fallow", "fallow-similar-code"]) {
353
399
  const binaryPath = path.join(platformDir, `${base}${ext}`);
354
400
  const content = Buffer.from(`global install ${base}`);
355
401
  fs.writeFileSync(binaryPath, content);
@@ -460,10 +506,10 @@ test("verifyInstalled honors FALLOW_SKIP_BINARY_VERIFY", async (t) => {
460
506
  assert.equal(result.skipped, true);
461
507
  });
462
508
 
463
- function computeDigestsForDir(dir) {
464
- const ext = process.platform === "win32" ? ".exe" : "";
509
+ function computeDigestsForDir(dir, platformId) {
510
+ const ext = extForPlatformId(platformId);
465
511
  const out = {};
466
- for (const base of ["fallow"]) {
512
+ for (const base of ["fallow", "fallow-similar-code"]) {
467
513
  const fileName = `${base}${ext}`;
468
514
  const full = path.join(dir, fileName);
469
515
  out[fileName] =
@@ -549,7 +595,7 @@ test("verifyInstalled falls back to the provider when the embedded digest is mis
549
595
  },
550
596
  });
551
597
  assert.equal(result.ok, true);
552
- assert.equal(providerCalls, 1);
598
+ assert.equal(providerCalls, 2);
553
599
  });
554
600
 
555
601
  test("verifyInstalled falls back to the provider when fallowDigests is partial / malformed", async (t) => {
@@ -573,15 +619,15 @@ test("verifyInstalled falls back to the provider when fallowDigests is partial /
573
619
  },
574
620
  });
575
621
  assert.equal(result.ok, true);
576
- // One call for the single shipped binary because its entry fails to normalize.
577
- assert.equal(providerCalls, 1);
622
+ // Both shipped binaries fall back because their embedded entries are absent or malformed.
623
+ assert.equal(providerCalls, 2);
578
624
  });
579
625
 
580
626
  test("verifyInstalled returns digest-mismatch when the embedded digest disagrees with the binary", async (t) => {
581
627
  const { privateKey, rawPub } = makeKeypair();
582
628
  const dir = makePlatformDir(privateKey);
583
629
  t.after(() => cleanup(dir));
584
- const ext = process.platform === "win32" ? ".exe" : "";
630
+ const ext = extForPlatformId();
585
631
  writeManifest(dir, {
586
632
  name: "@fallow-cli/x",
587
633
  version: "1.0.0",