fallow 3.36.0 → 3.37.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.
- package/README.md +1 -1
- package/capabilities.json +21 -7
- package/issue-registry.json +8 -4
- package/package.json +14 -11
- package/rule-pack-schema.json +264 -0
- package/schema.json +7 -0
- package/skills/fallow/SKILL.md +2 -2
- package/skills/fallow/references/cli-reference.md +11 -10
- package/skills/fallow/references/gotchas.md +2 -0
- package/types/output-contract.d.ts +59 -11
- package/scripts/lazy-verify.test.js +0 -564
- package/scripts/platform-package.test.js +0 -26
- package/scripts/run-binary.test.js +0 -293
- package/scripts/sentinel-path.test.js +0 -240
- package/scripts/verify-binary.test.js +0 -809
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
const test = require("node:test");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
|
|
4
|
-
const { getPlatformPackage } = require("./platform-package");
|
|
5
|
-
|
|
6
|
-
test("maps Windows x64 and arm64 to MSVC packages", () => {
|
|
7
|
-
assert.equal(getPlatformPackage("win32", "x64"), "@fallow-cli/win32-x64-msvc");
|
|
8
|
-
assert.equal(getPlatformPackage("win32", "arm64"), "@fallow-cli/win32-arm64-msvc");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("maps Linux packages with libc awareness", () => {
|
|
12
|
-
assert.equal(getPlatformPackage("linux", "x64", "glibc"), "@fallow-cli/linux-x64-gnu");
|
|
13
|
-
assert.equal(getPlatformPackage("linux", "x64", "musl"), "@fallow-cli/linux-x64-musl");
|
|
14
|
-
assert.equal(getPlatformPackage("linux", "x64", null), "@fallow-cli/linux-x64-musl");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("maps macOS packages by architecture", () => {
|
|
18
|
-
assert.equal(getPlatformPackage("darwin", "x64"), "@fallow-cli/darwin-x64");
|
|
19
|
-
assert.equal(getPlatformPackage("darwin", "arm64"), "@fallow-cli/darwin-arm64");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("returns null for unsupported targets", () => {
|
|
23
|
-
assert.equal(getPlatformPackage("win32", "ia32"), null);
|
|
24
|
-
assert.equal(getPlatformPackage("linux", "ppc64"), null);
|
|
25
|
-
assert.equal(getPlatformPackage("freebsd", "x64"), null);
|
|
26
|
-
});
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
const test = require("node:test");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
const { spawnSync } = require("node:child_process");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
|
|
8
|
-
const RUN_BINARY = path.join(__dirname, "run-binary.js");
|
|
9
|
-
const BIN_DIR = path.join(__dirname, "..", "bin");
|
|
10
|
-
|
|
11
|
-
function currentPlatformPackage() {
|
|
12
|
-
const { getPlatformPackage } = require("./platform-package");
|
|
13
|
-
if (process.platform !== "linux") {
|
|
14
|
-
return getPlatformPackage(process.platform, process.arch);
|
|
15
|
-
}
|
|
16
|
-
let libcFamily;
|
|
17
|
-
try {
|
|
18
|
-
libcFamily = require("detect-libc").familySync();
|
|
19
|
-
} catch {
|
|
20
|
-
libcFamily = undefined;
|
|
21
|
-
}
|
|
22
|
-
return getPlatformPackage(process.platform, process.arch, libcFamily);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Install a fake platform package whose `fallow` binary is the given Node
|
|
26
|
-
// script. Returns the scratch directory and the environment for the launcher.
|
|
27
|
-
function installFakeBinary(t, script) {
|
|
28
|
-
const work = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-launcher-"));
|
|
29
|
-
t.after(() => fs.rmSync(work, { recursive: true, force: true }));
|
|
30
|
-
|
|
31
|
-
const pkg = currentPlatformPackage();
|
|
32
|
-
assert.ok(pkg, "the test host must map to a supported platform package");
|
|
33
|
-
const pkgDir = path.join(work, "node_modules", ...pkg.split("/"));
|
|
34
|
-
fs.mkdirSync(pkgDir, { recursive: true });
|
|
35
|
-
fs.writeFileSync(
|
|
36
|
-
path.join(pkgDir, "package.json"),
|
|
37
|
-
JSON.stringify({ name: pkg, version: "3.5.0" }),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const binary = path.join(pkgDir, "fallow");
|
|
41
|
-
fs.writeFileSync(binary, `#!/usr/bin/env node\n${script}`);
|
|
42
|
-
fs.chmodSync(binary, 0o755);
|
|
43
|
-
|
|
44
|
-
const env = {
|
|
45
|
-
...process.env,
|
|
46
|
-
NODE_PATH: path.join(work, "node_modules"),
|
|
47
|
-
FALLOW_SKIP_BINARY_VERIFY: "1",
|
|
48
|
-
};
|
|
49
|
-
return { work, env };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function runLauncher(t, launcher, args) {
|
|
53
|
-
const { work, env } = installFakeBinary(
|
|
54
|
-
t,
|
|
55
|
-
'require("node:fs").writeFileSync(process.env.FALLOW_TEST_ARGS, JSON.stringify(process.argv.slice(2)));\n',
|
|
56
|
-
);
|
|
57
|
-
const argsFile = path.join(work, "args.json");
|
|
58
|
-
const result = spawnSync(process.execPath, [path.join(BIN_DIR, launcher), ...args], {
|
|
59
|
-
encoding: "utf8",
|
|
60
|
-
env: { ...env, FALLOW_TEST_ARGS: argsFile },
|
|
61
|
-
});
|
|
62
|
-
assert.equal(result.status, 0, result.stderr);
|
|
63
|
-
return JSON.parse(fs.readFileSync(argsFile, "utf8"));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// The fake binary writes a large payload the way the Rust binary does: plain
|
|
67
|
-
// write calls on fd 1, and it stops at the first write error but exits 0.
|
|
68
|
-
// A non-blocking inherited pipe makes a write fail with EAGAIN, so the reader
|
|
69
|
-
// gets a short payload (issue #3276).
|
|
70
|
-
const LARGE_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
71
|
-
const LARGE_OUTPUT_BINARY =
|
|
72
|
-
'const fs = require("node:fs");\n' +
|
|
73
|
-
`const total = ${LARGE_OUTPUT_BYTES};\n` +
|
|
74
|
-
'const chunk = Buffer.alloc(64 * 1024, "x");\n' +
|
|
75
|
-
"let written = 0;\n" +
|
|
76
|
-
"try {\n" +
|
|
77
|
-
" while (written < total) written += fs.writeSync(1, chunk, 0, Math.min(chunk.length, total - written));\n" +
|
|
78
|
-
"} catch {}\n";
|
|
79
|
-
|
|
80
|
-
function bunAvailable() {
|
|
81
|
-
const probe = spawnSync("bun", ["--version"], { encoding: "utf8" });
|
|
82
|
-
return probe.status === 0;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runLargeOutput(t, runtime, { mergeStderr = false } = {}) {
|
|
86
|
-
const { env } = installFakeBinary(t, LARGE_OUTPUT_BINARY);
|
|
87
|
-
const launcher = path.join(BIN_DIR, "fallow");
|
|
88
|
-
const [command, args] = mergeStderr
|
|
89
|
-
? ["/bin/sh", ["-c", 'exec "$0" "$@" 2>&1', runtime, launcher, "check"]]
|
|
90
|
-
: [runtime, [launcher, "check"]];
|
|
91
|
-
return spawnSync(command, args, { env, maxBuffer: 2 * LARGE_OUTPUT_BYTES });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Count only the payload bytes. In the merged case, stdout also holds the
|
|
95
|
-
// FALLOW_SKIP_BINARY_VERIFY warning line.
|
|
96
|
-
function payloadBytes(stdout) {
|
|
97
|
-
let count = 0;
|
|
98
|
-
for (const byte of stdout) if (byte === 0x78) count += 1;
|
|
99
|
-
return count;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const runtime of ["node", "bun"]) {
|
|
103
|
-
for (const mergeStderr of [false, true]) {
|
|
104
|
-
const name = mergeStderr ? "stdout and stderr on one pipe" : "stdout on its own pipe";
|
|
105
|
-
test(
|
|
106
|
-
`launcher under ${runtime} passes the complete binary output (${name})`,
|
|
107
|
-
{ skip: process.platform === "win32" || (runtime === "bun" && !bunAvailable()) },
|
|
108
|
-
(t) => {
|
|
109
|
-
const result = runLargeOutput(t, runtime === "node" ? process.execPath : "bun", {
|
|
110
|
-
mergeStderr,
|
|
111
|
-
});
|
|
112
|
-
assert.equal(result.status, 0, String(result.stderr));
|
|
113
|
-
assert.equal(payloadBytes(result.stdout), LARGE_OUTPUT_BYTES);
|
|
114
|
-
},
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Run a child that installs guardBrokenStdout, then emits a synthetic stdout
|
|
120
|
-
// 'error' with the given code. Node delivers a broken-pipe failure as exactly
|
|
121
|
-
// this event ("Emitted 'error' event on Socket instance"), so emitting it is a
|
|
122
|
-
// faithful reproduction of `fallow --version | head` without needing a live
|
|
123
|
-
// pipe or an installed @fallow-cli platform package. Requiring run-binary.js
|
|
124
|
-
// has no side effects beyond defining functions, so no binary is resolved.
|
|
125
|
-
function runGuardChild(errorCode) {
|
|
126
|
-
const script =
|
|
127
|
-
`const { guardBrokenStdout } = require(${JSON.stringify(RUN_BINARY)});` +
|
|
128
|
-
`guardBrokenStdout();` +
|
|
129
|
-
`process.stdout.emit("error", Object.assign(new Error("write ${errorCode}"), { code: "${errorCode}" }));` +
|
|
130
|
-
// Reached only if the guard neither exited (EPIPE) nor rethrew (other).
|
|
131
|
-
`process.exit(42);`;
|
|
132
|
-
return spawnSync(process.execPath, ["-e", script], { encoding: "utf8" });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
test("guardBrokenStdout swallows EPIPE on stdout and exits 0", () => {
|
|
136
|
-
const res = runGuardChild("EPIPE");
|
|
137
|
-
assert.equal(res.status, 0, "EPIPE on stdout should exit 0 cleanly, not crash");
|
|
138
|
-
assert.doesNotMatch(res.stderr, /EPIPE/, "no EPIPE stack trace on stderr");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("guardBrokenStdout rethrows non-EPIPE stdout errors (exit 1)", () => {
|
|
142
|
-
const res = runGuardChild("ENOSPC");
|
|
143
|
-
assert.equal(res.status, 1, "a non-EPIPE stdout error must surface, not be swallowed");
|
|
144
|
-
// Match the thrown error's header ("Error: write ENOSPC"), not just the
|
|
145
|
-
// message substring: a missing-guard TypeError would leak the script source
|
|
146
|
-
// (`new Error("write ENOSPC")`) into its code frame and match a looser regex,
|
|
147
|
-
// masking a regression. The colon-space header only appears on a real rethrow.
|
|
148
|
-
assert.match(res.stderr, /Error: write ENOSPC/, "the rethrown error reaches stderr");
|
|
149
|
-
assert.doesNotMatch(res.stderr, /is not a function/, "guard must be present, not absent");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("isVersionQuery recognizes --version, -V, and -v as the first argument", () => {
|
|
153
|
-
const { isVersionQuery } = require(RUN_BINARY);
|
|
154
|
-
assert.equal(isVersionQuery(["node", "fallow", "--version"]), true);
|
|
155
|
-
assert.equal(isVersionQuery(["node", "fallow", "-V"]), true);
|
|
156
|
-
assert.equal(
|
|
157
|
-
isVersionQuery(["node", "fallow", "-v"]),
|
|
158
|
-
true,
|
|
159
|
-
"-v must append the verified line too",
|
|
160
|
-
);
|
|
161
|
-
assert.equal(isVersionQuery(["node", "fallow"]), false);
|
|
162
|
-
assert.equal(isVersionQuery(["node", "fallow", "dead-code"]), false);
|
|
163
|
-
assert.equal(
|
|
164
|
-
isVersionQuery(["node", "fallow", "dead-code", "-v"]),
|
|
165
|
-
false,
|
|
166
|
-
"-v only counts as the first arg",
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("describeVerified annotates the resolved version's signing status", () => {
|
|
171
|
-
const { describeVerified } = require(RUN_BINARY);
|
|
172
|
-
const ok = { ok: true, sentinelPath: "/c/s" };
|
|
173
|
-
// Signed-era version: appended as `signed`.
|
|
174
|
-
assert.match(
|
|
175
|
-
describeVerified(ok, "2.83.0"),
|
|
176
|
-
/verified: yes \(sentinel \/c\/s\); fallow 2\.83\.0 signed/,
|
|
177
|
-
);
|
|
178
|
-
// Pre-signing version: the fleet pre-flight signal, most useful on skip.
|
|
179
|
-
const skipped = { skipped: true, reason: "FALLOW_SKIP_BINARY_VERIFY is set" };
|
|
180
|
-
assert.match(
|
|
181
|
-
describeVerified(skipped, "2.76.0"),
|
|
182
|
-
/verified: skipped \(.*\); fallow 2\.76\.0 unsigned \(predates 2\.77\.0\)/,
|
|
183
|
-
);
|
|
184
|
-
// Unknown / unreadable version: no annotation, version line stays intact.
|
|
185
|
-
assert.equal(describeVerified(ok, undefined), "verified: yes (sentinel /c/s)");
|
|
186
|
-
assert.equal(describeVerified(ok, ""), "verified: yes (sentinel /c/s)");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("exitCodeForChildFailure preserves status codes and maps signal deaths", () => {
|
|
190
|
-
const { exitCodeForChildFailure } = require(RUN_BINARY);
|
|
191
|
-
assert.equal(exitCodeForChildFailure({ status: 3 }), 3);
|
|
192
|
-
assert.equal(exitCodeForChildFailure({ status: 0 }), 0);
|
|
193
|
-
assert.equal(
|
|
194
|
-
exitCodeForChildFailure({ status: null, signal: "SIGSEGV" }),
|
|
195
|
-
128 + os.constants.signals.SIGSEGV,
|
|
196
|
-
);
|
|
197
|
-
assert.equal(
|
|
198
|
-
exitCodeForChildFailure({ status: null, signal: "SIGKILL" }),
|
|
199
|
-
128 + os.constants.signals.SIGKILL,
|
|
200
|
-
);
|
|
201
|
-
assert.equal(exitCodeForChildFailure({ status: null, signal: undefined }), 1);
|
|
202
|
-
assert.equal(exitCodeForChildFailure({ status: null, signal: "NOT_A_SIGNAL" }), 1);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("resolveTypeAwareCompanion accepts only an exact matching package", (t) => {
|
|
206
|
-
const { resolveTypeAwareCompanion } = require(RUN_BINARY);
|
|
207
|
-
const work = fs.mkdtempSync(path.join(os.tmpdir(), "fallow-type-aware-package-"));
|
|
208
|
-
t.after(() => fs.rmSync(work, { recursive: true, force: true }));
|
|
209
|
-
const manifestPath = path.join(work, "package.json");
|
|
210
|
-
const companionPath = path.join(work, "fallow-type-aware.mjs");
|
|
211
|
-
fs.writeFileSync(companionPath, "#!/usr/bin/env node\n");
|
|
212
|
-
const resolvePackage = () => manifestPath;
|
|
213
|
-
|
|
214
|
-
fs.writeFileSync(manifestPath, JSON.stringify({ version: "3.8.0" }));
|
|
215
|
-
assert.equal(resolveTypeAwareCompanion("3.8.0", resolvePackage), companionPath);
|
|
216
|
-
|
|
217
|
-
fs.writeFileSync(manifestPath, JSON.stringify({ version: "3.7.0" }));
|
|
218
|
-
assert.equal(resolveTypeAwareCompanion("3.8.0", resolvePackage), undefined);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("childEnvironment marks launcher-wired companions as npm-wrapper", (t) => {
|
|
222
|
-
const { childEnvironment } = require(RUN_BINARY);
|
|
223
|
-
const previousBin = process.env.FALLOW_TYPE_AWARE_BIN;
|
|
224
|
-
const previousScript = process.env.FALLOW_TYPE_AWARE_SCRIPT;
|
|
225
|
-
delete process.env.FALLOW_TYPE_AWARE_BIN;
|
|
226
|
-
delete process.env.FALLOW_TYPE_AWARE_SCRIPT;
|
|
227
|
-
t.after(() => {
|
|
228
|
-
if (previousBin === undefined) delete process.env.FALLOW_TYPE_AWARE_BIN;
|
|
229
|
-
else process.env.FALLOW_TYPE_AWARE_BIN = previousBin;
|
|
230
|
-
if (previousScript === undefined) delete process.env.FALLOW_TYPE_AWARE_SCRIPT;
|
|
231
|
-
else process.env.FALLOW_TYPE_AWARE_SCRIPT = previousScript;
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
const env = childEnvironment("3.8.0", () => "/tmp/fallow-type-aware.mjs", {
|
|
235
|
-
platform: "linux",
|
|
236
|
-
execPath: "/usr/bin/node",
|
|
237
|
-
});
|
|
238
|
-
assert.equal(env.FALLOW_TYPE_AWARE_BIN, "/tmp/fallow-type-aware.mjs");
|
|
239
|
-
assert.equal(env.FALLOW_TYPE_AWARE_SCRIPT, undefined);
|
|
240
|
-
assert.equal(env.FALLOW_TYPE_AWARE_BIN_SOURCE, "npm-wrapper");
|
|
241
|
-
|
|
242
|
-
const windowsEnv = childEnvironment("3.8.0", () => "C:\\pkg\\fallow-type-aware.mjs", {
|
|
243
|
-
platform: "win32",
|
|
244
|
-
execPath: "C:\\Program Files\\nodejs\\node.exe",
|
|
245
|
-
});
|
|
246
|
-
assert.equal(windowsEnv.FALLOW_TYPE_AWARE_BIN, "C:\\Program Files\\nodejs\\node.exe");
|
|
247
|
-
assert.equal(windowsEnv.FALLOW_TYPE_AWARE_SCRIPT, "C:\\pkg\\fallow-type-aware.mjs");
|
|
248
|
-
assert.equal(windowsEnv.FALLOW_TYPE_AWARE_BIN_SOURCE, "npm-wrapper");
|
|
249
|
-
|
|
250
|
-
// No resolvable companion: the environment passes through untouched.
|
|
251
|
-
const untouched = childEnvironment("3.8.0", () => undefined);
|
|
252
|
-
assert.equal(untouched, process.env);
|
|
253
|
-
assert.equal(untouched.FALLOW_TYPE_AWARE_BIN_SOURCE, undefined);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("childEnvironment leaves a user-set override unmarked", (t) => {
|
|
257
|
-
const { childEnvironment } = require(RUN_BINARY);
|
|
258
|
-
const previousBin = process.env.FALLOW_TYPE_AWARE_BIN;
|
|
259
|
-
const previousScript = process.env.FALLOW_TYPE_AWARE_SCRIPT;
|
|
260
|
-
process.env.FALLOW_TYPE_AWARE_BIN = "/opt/custom-sidecar";
|
|
261
|
-
process.env.FALLOW_TYPE_AWARE_SCRIPT = "/opt/custom-sidecar.mjs";
|
|
262
|
-
t.after(() => {
|
|
263
|
-
if (previousBin === undefined) delete process.env.FALLOW_TYPE_AWARE_BIN;
|
|
264
|
-
else process.env.FALLOW_TYPE_AWARE_BIN = previousBin;
|
|
265
|
-
if (previousScript === undefined) delete process.env.FALLOW_TYPE_AWARE_SCRIPT;
|
|
266
|
-
else process.env.FALLOW_TYPE_AWARE_SCRIPT = previousScript;
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const env = childEnvironment("3.8.0", () => "/tmp/fallow-type-aware.mjs");
|
|
270
|
-
assert.equal(env, process.env);
|
|
271
|
-
assert.equal(env.FALLOW_TYPE_AWARE_SCRIPT, "/opt/custom-sidecar.mjs");
|
|
272
|
-
assert.equal(env.FALLOW_TYPE_AWARE_BIN_SOURCE, undefined);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test(
|
|
276
|
-
"fallow-lsp executes the multicall binary with the lsp-server subcommand",
|
|
277
|
-
{ skip: process.platform === "win32" },
|
|
278
|
-
(t) => {
|
|
279
|
-
assert.deepEqual(runLauncher(t, "fallow-lsp", ["--stdio"]), ["lsp-server", "--stdio"]);
|
|
280
|
-
},
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
test(
|
|
284
|
-
"fallow-mcp executes the multicall binary with the mcp-server subcommand",
|
|
285
|
-
{ skip: process.platform === "win32" },
|
|
286
|
-
(t) => {
|
|
287
|
-
assert.deepEqual(runLauncher(t, "fallow-mcp", ["--transport", "stdio"]), [
|
|
288
|
-
"mcp-server",
|
|
289
|
-
"--transport",
|
|
290
|
-
"stdio",
|
|
291
|
-
]);
|
|
292
|
-
},
|
|
293
|
-
);
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
const test = require("node:test");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
SENTINEL_FILENAME,
|
|
9
|
-
packageIdToFilename,
|
|
10
|
-
resolveSentinelPath,
|
|
11
|
-
_isWritable,
|
|
12
|
-
} = require("./sentinel-path");
|
|
13
|
-
|
|
14
|
-
function mkTmp() {
|
|
15
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), "fallow-sentinel-path-"));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function cleanup(dir) {
|
|
19
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("packageIdToFilename normalises scoped package names", () => {
|
|
23
|
-
assert.equal(packageIdToFilename("@fallow-cli/darwin-arm64"), "fallow-cli__darwin-arm64.json");
|
|
24
|
-
assert.equal(packageIdToFilename("@fallow-cli/linux-x64-gnu"), "fallow-cli__linux-x64-gnu.json");
|
|
25
|
-
assert.equal(packageIdToFilename("fallow"), "fallow.json");
|
|
26
|
-
assert.equal(packageIdToFilename(""), "unknown.json");
|
|
27
|
-
assert.equal(packageIdToFilename(undefined), "unknown.json");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("packageIdToFilename strips backslashes too (Windows scoped paths)", () => {
|
|
31
|
-
assert.equal(packageIdToFilename("@scope\\name"), "scope__name.json");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("isWritable returns true for a writable tmpdir", () => {
|
|
35
|
-
const dir = mkTmp();
|
|
36
|
-
try {
|
|
37
|
-
assert.equal(_isWritable(dir), true);
|
|
38
|
-
} finally {
|
|
39
|
-
cleanup(dir);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("isWritable returns false for a non-existent path", () => {
|
|
44
|
-
assert.equal(_isWritable("/this/path/does/not/exist/probably"), false);
|
|
45
|
-
assert.equal(_isWritable(""), false);
|
|
46
|
-
assert.equal(_isWritable(undefined), false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("isWritable returns false when the target is a file, not a dir", () => {
|
|
50
|
-
const dir = mkTmp();
|
|
51
|
-
try {
|
|
52
|
-
const filePath = path.join(dir, "a-file");
|
|
53
|
-
fs.writeFileSync(filePath, "x");
|
|
54
|
-
assert.equal(_isWritable(filePath), false);
|
|
55
|
-
} finally {
|
|
56
|
-
cleanup(dir);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("resolveSentinelPath prefers the platform pkg dir when writable", () => {
|
|
61
|
-
const platformPkgDir = mkTmp();
|
|
62
|
-
try {
|
|
63
|
-
const result = resolveSentinelPath({
|
|
64
|
-
platformPkgDir,
|
|
65
|
-
packageName: "@fallow-cli/darwin-arm64",
|
|
66
|
-
env: {},
|
|
67
|
-
});
|
|
68
|
-
assert.equal(result.location, "platform-pkg");
|
|
69
|
-
assert.equal(result.writable, true);
|
|
70
|
-
assert.equal(result.path, path.join(platformPkgDir, SENTINEL_FILENAME));
|
|
71
|
-
} finally {
|
|
72
|
-
cleanup(platformPkgDir);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("resolveSentinelPath falls back to FALLOW_VERIFY_CACHE_DIR when platform pkg dir is non-writable", () => {
|
|
77
|
-
const cacheDir = mkTmp();
|
|
78
|
-
try {
|
|
79
|
-
const result = resolveSentinelPath({
|
|
80
|
-
platformPkgDir: path.join(cacheDir, "missing-platform-package"),
|
|
81
|
-
packageName: "@fallow-cli/darwin-arm64",
|
|
82
|
-
env: { FALLOW_VERIFY_CACHE_DIR: cacheDir },
|
|
83
|
-
});
|
|
84
|
-
assert.equal(result.location, "cache-dir-env");
|
|
85
|
-
assert.equal(result.writable, true);
|
|
86
|
-
assert.equal(result.path, path.join(cacheDir, "fallow-cli__darwin-arm64.json"));
|
|
87
|
-
} finally {
|
|
88
|
-
cleanup(cacheDir);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("resolveSentinelPath uses FALLOW_VERIFY_CACHE_DIR when platform pkg dir is unset", () => {
|
|
93
|
-
const cacheDir = mkTmp();
|
|
94
|
-
try {
|
|
95
|
-
const result = resolveSentinelPath({
|
|
96
|
-
platformPkgDir: undefined,
|
|
97
|
-
packageName: "@fallow-cli/linux-x64-gnu",
|
|
98
|
-
env: { FALLOW_VERIFY_CACHE_DIR: cacheDir },
|
|
99
|
-
});
|
|
100
|
-
assert.equal(result.location, "cache-dir-env");
|
|
101
|
-
assert.equal(result.path, path.join(cacheDir, "fallow-cli__linux-x64-gnu.json"));
|
|
102
|
-
} finally {
|
|
103
|
-
cleanup(cacheDir);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("resolveSentinelPath falls back to XDG_CACHE_HOME on Linux/macOS", () => {
|
|
108
|
-
const homeDir = mkTmp();
|
|
109
|
-
const xdg = path.join(homeDir, "xdg-cache");
|
|
110
|
-
fs.mkdirSync(xdg);
|
|
111
|
-
try {
|
|
112
|
-
const result = resolveSentinelPath({
|
|
113
|
-
platformPkgDir: undefined,
|
|
114
|
-
packageName: "@fallow-cli/darwin-arm64",
|
|
115
|
-
env: { XDG_CACHE_HOME: xdg },
|
|
116
|
-
homedir: homeDir,
|
|
117
|
-
platform: "darwin",
|
|
118
|
-
});
|
|
119
|
-
assert.equal(result.location, "xdg");
|
|
120
|
-
assert.equal(result.writable, true);
|
|
121
|
-
assert.equal(
|
|
122
|
-
result.path,
|
|
123
|
-
path.join(xdg, "fallow", "sentinels", "fallow-cli__darwin-arm64.json"),
|
|
124
|
-
);
|
|
125
|
-
// The fallow/sentinels/ subdir must have been created.
|
|
126
|
-
assert.equal(fs.existsSync(path.join(xdg, "fallow", "sentinels")), true);
|
|
127
|
-
} finally {
|
|
128
|
-
cleanup(homeDir);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("resolveSentinelPath falls back to ~/.cache on Linux/macOS when XDG_CACHE_HOME is unset", () => {
|
|
133
|
-
const homeDir = mkTmp();
|
|
134
|
-
try {
|
|
135
|
-
const result = resolveSentinelPath({
|
|
136
|
-
platformPkgDir: undefined,
|
|
137
|
-
packageName: "@fallow-cli/linux-x64-gnu",
|
|
138
|
-
env: {},
|
|
139
|
-
homedir: homeDir,
|
|
140
|
-
platform: "linux",
|
|
141
|
-
});
|
|
142
|
-
assert.equal(result.location, "home-cache");
|
|
143
|
-
assert.equal(
|
|
144
|
-
result.path,
|
|
145
|
-
path.join(homeDir, ".cache", "fallow", "sentinels", "fallow-cli__linux-x64-gnu.json"),
|
|
146
|
-
);
|
|
147
|
-
} finally {
|
|
148
|
-
cleanup(homeDir);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("resolveSentinelPath uses LOCALAPPDATA on Windows", () => {
|
|
153
|
-
const localAppData = mkTmp();
|
|
154
|
-
try {
|
|
155
|
-
const result = resolveSentinelPath({
|
|
156
|
-
platformPkgDir: undefined,
|
|
157
|
-
packageName: "@fallow-cli/win32-x64-msvc",
|
|
158
|
-
env: { LOCALAPPDATA: localAppData },
|
|
159
|
-
homedir: "/c/Users/test",
|
|
160
|
-
platform: "win32",
|
|
161
|
-
});
|
|
162
|
-
assert.equal(result.location, "localappdata");
|
|
163
|
-
assert.equal(result.writable, true);
|
|
164
|
-
assert.equal(
|
|
165
|
-
result.path,
|
|
166
|
-
path.join(localAppData, "fallow", "sentinels", "fallow-cli__win32-x64-msvc.json"),
|
|
167
|
-
);
|
|
168
|
-
} finally {
|
|
169
|
-
cleanup(localAppData);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("resolveSentinelPath returns no path when every location is non-writable", () => {
|
|
174
|
-
const result = resolveSentinelPath({
|
|
175
|
-
platformPkgDir: undefined,
|
|
176
|
-
packageName: "@fallow-cli/darwin-arm64",
|
|
177
|
-
env: {},
|
|
178
|
-
homedir: undefined,
|
|
179
|
-
platform: "darwin",
|
|
180
|
-
});
|
|
181
|
-
assert.equal(result.path, null);
|
|
182
|
-
assert.equal(result.location, "none");
|
|
183
|
-
assert.equal(result.writable, false);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("resolveSentinelPath returns no path on Windows without LOCALAPPDATA", () => {
|
|
187
|
-
const result = resolveSentinelPath({
|
|
188
|
-
platformPkgDir: undefined,
|
|
189
|
-
packageName: "@fallow-cli/win32-x64-msvc",
|
|
190
|
-
env: {},
|
|
191
|
-
homedir: undefined,
|
|
192
|
-
platform: "win32",
|
|
193
|
-
});
|
|
194
|
-
assert.equal(result.path, null);
|
|
195
|
-
assert.equal(result.location, "none");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("resolveSentinelPath honors injected isWritable + ensureDir for full test isolation", () => {
|
|
199
|
-
const calls = [];
|
|
200
|
-
const result = resolveSentinelPath({
|
|
201
|
-
platformPkgDir: "/synthetic/pkg-dir",
|
|
202
|
-
packageName: "@fallow-cli/darwin-arm64",
|
|
203
|
-
env: {},
|
|
204
|
-
isWritable: (dir) => {
|
|
205
|
-
calls.push(["isWritable", dir]);
|
|
206
|
-
return dir === "/synthetic/pkg-dir";
|
|
207
|
-
},
|
|
208
|
-
ensureDir: (dir) => {
|
|
209
|
-
calls.push(["ensureDir", dir]);
|
|
210
|
-
return true;
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
assert.equal(result.location, "platform-pkg");
|
|
214
|
-
assert.deepEqual(calls, [["isWritable", "/synthetic/pkg-dir"]]);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("resolveSentinelPath skips cache-dir-env when ensureDir fails for it", () => {
|
|
218
|
-
// FALLOW_VERIFY_CACHE_DIR points at a non-creatable path, XDG points at a
|
|
219
|
-
// creatable one. Confirm the resolver moves past the env override.
|
|
220
|
-
//
|
|
221
|
-
// A directory nested under a regular FILE is the portable way to make mkdir
|
|
222
|
-
// fail: `/dev/null/inside/a/file` only refuses on POSIX, and on Windows the
|
|
223
|
-
// recursive mkdir succeeds against the current drive, which both defeats the
|
|
224
|
-
// assertion and creates C:\dev outside any temp directory.
|
|
225
|
-
const homeDir = mkTmp();
|
|
226
|
-
const blocker = path.join(homeDir, "not-a-directory");
|
|
227
|
-
fs.writeFileSync(blocker, "");
|
|
228
|
-
try {
|
|
229
|
-
const result = resolveSentinelPath({
|
|
230
|
-
platformPkgDir: undefined,
|
|
231
|
-
packageName: "@fallow-cli/darwin-arm64",
|
|
232
|
-
env: { FALLOW_VERIFY_CACHE_DIR: path.join(blocker, "inside", "a", "file") },
|
|
233
|
-
homedir: homeDir,
|
|
234
|
-
platform: "darwin",
|
|
235
|
-
});
|
|
236
|
-
assert.equal(result.location, "home-cache");
|
|
237
|
-
} finally {
|
|
238
|
-
cleanup(homeDir);
|
|
239
|
-
}
|
|
240
|
-
});
|