-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspawn.test.ts
More file actions
199 lines (185 loc) · 6.67 KB
/
spawn.test.ts
File metadata and controls
199 lines (185 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
rmSync,
realpathSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveJitiCli, spawnServer } from "./spawn.ts";
/**
* Build a synthetic pnpm-like layout with `@mariozechner/pi-tui` and
* `@mariozechner/jiti` side by side. Returns the absolute path to
* pi-tui's package.json — the `anchor` the production code uses.
*
* realpathSync resolves the macOS `/var` ↔ `/private/var` symlink so
* string comparisons line up.
*/
function makePnpmLikeFixture() {
const root = realpathSync(mkdtempSync(join(tmpdir(), "vscbc-spawn-")));
const nm = join(root, "node_modules", "@mariozechner");
const piTuiDir = join(nm, "pi-tui");
mkdirSync(piTuiDir, { recursive: true });
writeFileSync(
join(piTuiDir, "package.json"),
JSON.stringify({ name: "@mariozechner/pi-tui" }),
);
const jitiDir = join(nm, "jiti");
mkdirSync(join(jitiDir, "lib"), { recursive: true });
writeFileSync(
join(jitiDir, "package.json"),
JSON.stringify({ name: "@mariozechner/jiti" }),
);
const cli = join(jitiDir, "lib", "jiti-cli.mjs");
writeFileSync(cli, "// fake");
return {
root,
piTuiPkgPath: join(piTuiDir, "package.json"),
jitiPkgPath: join(jitiDir, "package.json"),
expectedCli: cli,
jitiDir,
cleanup: () => rmSync(root, { recursive: true, force: true }),
};
}
/**
* Build a deterministic resolver that simulates Node's
* `createRequire(anchor).resolve(specifier)` without vitest's
* host-project pollution. The map is keyed by `"anchor||specifier"`.
*/
function fakeResolver(rules: Record<string, string | Error>) {
return vi.fn((specifier: string, anchor: string) => {
const key = `${anchor}||${specifier}`;
const v = rules[key];
if (v instanceof Error) throw v;
if (typeof v === "string") return v;
const err = new Error(`Cannot find module '${specifier}' from '${anchor}'`);
(err as Error & { code: string }).code = "MODULE_NOT_FOUND";
throw err;
});
}
describe("resolveJitiCli", () => {
let fixture: ReturnType<typeof makePnpmLikeFixture>;
beforeEach(() => {
fixture = makePnpmLikeFixture();
});
afterEach(() => fixture.cleanup());
it("finds jiti via an anchor package when the primary fromDir can't resolve it", () => {
// Simulates the Nix/pnpm-hoisted case: fromDir's chain fails (no
// jiti visible from the extension's own location), but the
// pi-tui anchor can resolve it as a sibling.
const anchor = fixture.piTuiPkgPath;
const resolve = fakeResolver({
// Primary anchor (fromDir/_anchor.js) fails for both specifiers.
// pi-tui anchor resolves @mariozechner/jiti/package.json only.
[`${anchor}||@mariozechner/jiti/package.json`]: fixture.jitiPkgPath,
});
const cli = resolveJitiCli({
fromDir: "/nowhere",
anchorPackages: [anchor],
resolve,
});
expect(cli).toBe(fixture.expectedCli);
});
it("prefers the primary fromDir resolution when it succeeds", () => {
const anchor = fixture.piTuiPkgPath;
// Both resolvers can find jiti, but fromDir should win (tried first).
// We encode that by only stubbing fromDir; the anchor rule is absent
// so would throw — the test asserts the anchor was never consulted.
const resolve = fakeResolver({
[`/some/dev/path/_anchor.js||@mariozechner/jiti/package.json`]:
fixture.jitiPkgPath,
});
const cli = resolveJitiCli({
fromDir: "/some/dev/path",
anchorPackages: [anchor],
resolve,
});
expect(cli).toBe(fixture.expectedCli);
// Called once for fromDir; never reached the pi-tui anchor.
expect(resolve).toHaveBeenCalledTimes(1);
expect(resolve).toHaveBeenCalledWith(
"@mariozechner/jiti/package.json",
"/some/dev/path/_anchor.js",
);
});
it("falls back to plain `jiti` after `@mariozechner/jiti` fails", () => {
// Rename the scoped pkg to plain 'jiti' in the fixture.
const plain = join(fixture.root, "node_modules", "jiti");
mkdirSync(join(plain, "lib"), { recursive: true });
writeFileSync(
join(plain, "package.json"),
JSON.stringify({ name: "jiti" }),
);
const plainCli = join(plain, "lib", "jiti-cli.mjs");
writeFileSync(plainCli, "// fake");
const anchor = fixture.piTuiPkgPath;
const resolve = fakeResolver({
[`${anchor}||jiti/package.json`]: join(plain, "package.json"),
});
const cli = resolveJitiCli({
fromDir: "/nowhere",
anchorPackages: [anchor],
resolve,
});
expect(cli).toBe(plainCli);
});
it("picks the first existing CLI variant (lib/jiti-cli.mjs → bin/jiti.mjs → bin/jiti.js)", () => {
rmSync(join(fixture.jitiDir, "lib"), { recursive: true, force: true });
mkdirSync(join(fixture.jitiDir, "bin"), { recursive: true });
const binMjs = join(fixture.jitiDir, "bin", "jiti.mjs");
writeFileSync(binMjs, "// fake");
const anchor = fixture.piTuiPkgPath;
const resolve = fakeResolver({
[`${anchor}||@mariozechner/jiti/package.json`]: fixture.jitiPkgPath,
});
const cli = resolveJitiCli({
fromDir: "/nowhere",
anchorPackages: [anchor],
resolve,
});
expect(cli).toBe(binMjs);
});
it("throws a helpful error when every candidate and anchor fails", () => {
const resolve = fakeResolver({});
expect(() =>
resolveJitiCli({
fromDir: "/nowhere",
anchorPackages: ["/also/nowhere/package.json"],
resolve,
}),
).toThrow(/could not resolve.*jiti/i);
});
});
describe("spawnServer", () => {
/**
* Build a fake child-process result so spawnServer's signature
* (which only reads `pid` and calls `unref`) is satisfied without
* touching real OS processes.
*/
function fakeChild(pid: number) {
return { pid, unref: vi.fn() };
}
it("invokes node with [jitiPath, mainPath, dataDir] and unrefs the child", () => {
// spawnServer is intentionally minimal: orphan-server cleanup
// happens inside the server's monitor tick, so this function
// just forks Node with the right argv and detaches.
let observed: { command: string; args: readonly string[] } | null = null;
const child = fakeChild(123);
const spawn = vi.fn((command: string, args: readonly string[]) => {
observed = { command, args };
return child;
});
const result = spawnServer("/path/main.ts", "/data", {
jitiPath: "/jiti.mjs",
spawn,
});
expect(result.pid).toBe(123);
expect(observed).toEqual({
command: process.execPath,
args: ["/jiti.mjs", "/path/main.ts", "/data"],
});
expect(child.unref).toHaveBeenCalledOnce();
});
});