mandrel 1.60.0 → 1.61.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/.agents/README.md +74 -32
- package/.agents/docs/SDLC.md +8 -9
- package/.agents/docs/configuration.md +61 -4
- package/.agents/docs/quality-gates.md +796 -0
- package/.agents/docs/workflows.md +2 -3
- package/.agents/runtime-deps.json +2 -2
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/agents-bootstrap-github.js +23 -119
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
- package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
- package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/detect-package-manager.js +72 -0
- package/.agents/scripts/lib/errors/index.js +4 -4
- package/.agents/scripts/lib/label-taxonomy.js +2 -2
- package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
- package/.agents/scripts/lib/onboard/init-tail.js +218 -0
- package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
- package/.agents/workflows/agents-update.md +14 -29
- package/.agents/workflows/helpers/agents-sync-config.md +3 -2
- package/.agents/workflows/plan.md +45 -3
- package/README.md +18 -30
- package/bin/mandrel.js +235 -16
- package/docs/CHANGELOG.md +24 -0
- package/lib/cli/doctor.js +45 -3
- package/lib/cli/init.js +66 -7
- package/lib/cli/registry.js +41 -145
- package/lib/cli/sync.js +122 -23
- package/lib/cli/uninstall.js +42 -7
- package/lib/cli/update.js +145 -192
- package/lib/cli/version-helpers.js +59 -0
- package/package.json +6 -6
- package/.agents/workflows/onboard.md +0 -208
- package/lib/cli/__tests__/migrate.test.js +0 -268
- package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
- package/lib/cli/__tests__/sync.test.js +0 -372
- package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
- package/lib/cli/__tests__/update-major.test.js +0 -217
- package/lib/cli/__tests__/update-reexec.test.js +0 -513
- package/lib/cli/__tests__/update.test.js +0 -696
- package/lib/cli/__tests__/version-check.test.js +0 -398
- package/lib/migrations/__tests__/index.test.js +0 -216
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
// lib/cli/__tests__/update-reexec.test.js
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the Story #4034 re-exec fix in lib/cli/update.js.
|
|
4
|
-
*
|
|
5
|
-
* After `npm-update` lands the new version, the post-install phases
|
|
6
|
-
* (sync, migrate, doctor) MUST run from the newly-installed binary rather
|
|
7
|
-
* than in the still-running old process. The `spawnPhase` seam is the
|
|
8
|
-
* injectable boundary that makes this fully testable without a real npm
|
|
9
|
-
* install.
|
|
10
|
-
*
|
|
11
|
-
* Coverage contract (Story #4034):
|
|
12
|
-
* - When `spawnPhase` is injected, post-install phases run through it
|
|
13
|
-
* (re-exec path) instead of the in-process runSync/runMigrations/runDoctor.
|
|
14
|
-
* - Phases are spawned in the correct order: sync → migrate → doctor.
|
|
15
|
-
* - `mandrel migrate` receives `--from <current>` and `--to <target>`.
|
|
16
|
-
* - The new bin path passed to `spawnPhase` is resolved from the cwd's
|
|
17
|
-
* `node_modules/.bin/mandrel` (the production path).
|
|
18
|
-
* - A failing sync phase throws and exits non-zero (never silently continues).
|
|
19
|
-
* - A failing migrate phase throws and exits non-zero.
|
|
20
|
-
* - A non-zero doctor phase maps to `action: 'doctor-failed'` + exit 1.
|
|
21
|
-
* - `resolveNewBinPath` returns the correct platform path.
|
|
22
|
-
* - `defaultSpawnPhase` routes stdout/stderr through the write sinks and
|
|
23
|
-
* throws on spawn error.
|
|
24
|
-
*
|
|
25
|
-
* Tier: unit (testing-standards § Unit). All seams — including `spawnPhase`
|
|
26
|
-
* — are injected; no real child process, filesystem, or network call occurs.
|
|
27
|
-
*
|
|
28
|
-
* Security (security-baseline § 5 — Data Leakage & Logging): fixtures carry
|
|
29
|
-
* only version strings and paths; no tokens, credentials, or env values.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import assert from 'node:assert/strict';
|
|
33
|
-
import path from 'node:path';
|
|
34
|
-
import { describe, it } from 'node:test';
|
|
35
|
-
|
|
36
|
-
import { defaultSpawnPhase, resolveNewBinPath, runUpdate } from '../update.js';
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Helpers
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/** Capture stdout/stderr writes and the exit code. */
|
|
43
|
-
function makeCapture() {
|
|
44
|
-
const out = [];
|
|
45
|
-
const err = [];
|
|
46
|
-
let exitCode = null;
|
|
47
|
-
return {
|
|
48
|
-
out,
|
|
49
|
-
err,
|
|
50
|
-
get exitCode() {
|
|
51
|
-
return exitCode;
|
|
52
|
-
},
|
|
53
|
-
write: (s) => out.push(s),
|
|
54
|
-
writeErr: (s) => err.push(s),
|
|
55
|
-
exit: (code) => {
|
|
56
|
-
exitCode = code;
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Build a minimal seam set for a minor-ahead non-dry-run run.
|
|
63
|
-
* `spawnPhase` replaces the in-process runSync/runMigrations/runDoctor.
|
|
64
|
-
*/
|
|
65
|
-
function makeReExecSeams({
|
|
66
|
-
target = '1.44.0',
|
|
67
|
-
current = '1.43.0',
|
|
68
|
-
spawnResults = {},
|
|
69
|
-
} = {}) {
|
|
70
|
-
const calls = [];
|
|
71
|
-
return {
|
|
72
|
-
calls,
|
|
73
|
-
currentVersion: current,
|
|
74
|
-
resolveTargetVersion: async () => {
|
|
75
|
-
calls.push('resolve');
|
|
76
|
-
return target;
|
|
77
|
-
},
|
|
78
|
-
npmUpdate: async (version) => {
|
|
79
|
-
calls.push(`npm-update:${version}`);
|
|
80
|
-
},
|
|
81
|
-
spawnPhase: async (phase, args, opts) => {
|
|
82
|
-
calls.push({ phase, args, binPath: opts.binPath, cwd: opts.cwd });
|
|
83
|
-
const result = spawnResults[phase] ?? {
|
|
84
|
-
ok: true,
|
|
85
|
-
stdout: '',
|
|
86
|
-
stderr: '',
|
|
87
|
-
};
|
|
88
|
-
return result;
|
|
89
|
-
},
|
|
90
|
-
surfaceChangelog: async (version) => {
|
|
91
|
-
calls.push(`changelog:${version}`);
|
|
92
|
-
},
|
|
93
|
-
cwd: () => '/fake/consumer',
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// AC: spawnPhase is invoked for post-install phases when injected
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
describe('runUpdate — re-exec path via spawnPhase', () => {
|
|
102
|
-
it('routes post-install phases through spawnPhase, not in-process seams', async () => {
|
|
103
|
-
const cap = makeCapture();
|
|
104
|
-
const seams = makeReExecSeams({ target: '1.44.0', current: '1.43.0' });
|
|
105
|
-
|
|
106
|
-
const result = await runUpdate({
|
|
107
|
-
argv: [],
|
|
108
|
-
...seams,
|
|
109
|
-
write: cap.write,
|
|
110
|
-
writeErr: cap.writeErr,
|
|
111
|
-
exit: cap.exit,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// npm-update ran, then all three phases were spawned via spawnPhase.
|
|
115
|
-
const phaseNames = seams.calls
|
|
116
|
-
.filter((c) => typeof c === 'object' && c.phase)
|
|
117
|
-
.map((c) => c.phase);
|
|
118
|
-
assert.deepEqual(phaseNames, ['sync', 'migrate', 'doctor']);
|
|
119
|
-
|
|
120
|
-
// The in-process runSync/runMigrations/runDoctor were NOT invoked directly
|
|
121
|
-
// (there are no 'runSync' / 'runMigrations' / 'runDoctor' string entries).
|
|
122
|
-
assert.ok(
|
|
123
|
-
!seams.calls.some(
|
|
124
|
-
(c) => c === 'runSync' || c === 'runMigrations' || c === 'runDoctor',
|
|
125
|
-
),
|
|
126
|
-
'in-process seams must NOT be called when spawnPhase is injected',
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
assert.equal(result.ok, true);
|
|
130
|
-
assert.equal(result.action, 'updated');
|
|
131
|
-
assert.deepEqual(result.stepsRun, [
|
|
132
|
-
'npm-update',
|
|
133
|
-
'runSync',
|
|
134
|
-
'runMigrations',
|
|
135
|
-
'doctor',
|
|
136
|
-
]);
|
|
137
|
-
assert.equal(cap.exitCode, null);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('passes correct ordered argv to each phase: sync→migrate(--from,--to)→doctor', async () => {
|
|
141
|
-
const cap = makeCapture();
|
|
142
|
-
const seams = makeReExecSeams({ target: '1.50.0', current: '1.43.0' });
|
|
143
|
-
|
|
144
|
-
await runUpdate({
|
|
145
|
-
argv: [],
|
|
146
|
-
...seams,
|
|
147
|
-
write: cap.write,
|
|
148
|
-
writeErr: cap.writeErr,
|
|
149
|
-
exit: cap.exit,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
|
|
153
|
-
assert.equal(phases.length, 3);
|
|
154
|
-
|
|
155
|
-
assert.equal(phases[0].phase, 'sync');
|
|
156
|
-
assert.deepEqual(phases[0].args, []);
|
|
157
|
-
|
|
158
|
-
assert.equal(phases[1].phase, 'migrate');
|
|
159
|
-
assert.deepEqual(phases[1].args, ['--from', '1.43.0', '--to', '1.50.0']);
|
|
160
|
-
|
|
161
|
-
assert.equal(phases[2].phase, 'doctor');
|
|
162
|
-
assert.deepEqual(phases[2].args, []);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('resolves new bin path from cwd/node_modules/.bin/mandrel', async () => {
|
|
166
|
-
const cap = makeCapture();
|
|
167
|
-
const seams = makeReExecSeams({ target: '1.44.0', current: '1.43.0' });
|
|
168
|
-
|
|
169
|
-
await runUpdate({
|
|
170
|
-
argv: [],
|
|
171
|
-
...seams,
|
|
172
|
-
write: cap.write,
|
|
173
|
-
writeErr: cap.writeErr,
|
|
174
|
-
exit: cap.exit,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
|
|
178
|
-
const expectedBin = resolveNewBinPath('/fake/consumer');
|
|
179
|
-
for (const p of phases) {
|
|
180
|
-
assert.equal(
|
|
181
|
-
p.binPath,
|
|
182
|
-
expectedBin,
|
|
183
|
-
`phase '${p.phase}' binPath should resolve to new bin`,
|
|
184
|
-
);
|
|
185
|
-
assert.equal(p.cwd, '/fake/consumer');
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('throws when sync phase exits non-zero (never silently materialises stale payload)', async () => {
|
|
190
|
-
const cap = makeCapture();
|
|
191
|
-
const seams = makeReExecSeams({
|
|
192
|
-
target: '1.44.0',
|
|
193
|
-
current: '1.43.0',
|
|
194
|
-
spawnResults: { sync: { ok: false, stdout: '', stderr: 'sync failed' } },
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
await assert.rejects(
|
|
198
|
-
() =>
|
|
199
|
-
runUpdate({
|
|
200
|
-
argv: [],
|
|
201
|
-
...seams,
|
|
202
|
-
write: cap.write,
|
|
203
|
-
writeErr: cap.writeErr,
|
|
204
|
-
exit: cap.exit,
|
|
205
|
-
}),
|
|
206
|
-
/mandrel sync.*new binary exited non-zero/,
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
// migrate and doctor must NOT have been called after sync failure.
|
|
210
|
-
const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
|
|
211
|
-
assert.deepEqual(
|
|
212
|
-
phases.map((p) => p.phase),
|
|
213
|
-
['sync'],
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('throws when migrate phase exits non-zero', async () => {
|
|
218
|
-
const cap = makeCapture();
|
|
219
|
-
const seams = makeReExecSeams({
|
|
220
|
-
target: '1.44.0',
|
|
221
|
-
current: '1.43.0',
|
|
222
|
-
spawnResults: {
|
|
223
|
-
migrate: { ok: false, stdout: '', stderr: 'migrate failed' },
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
await assert.rejects(
|
|
228
|
-
() =>
|
|
229
|
-
runUpdate({
|
|
230
|
-
argv: [],
|
|
231
|
-
...seams,
|
|
232
|
-
write: cap.write,
|
|
233
|
-
writeErr: cap.writeErr,
|
|
234
|
-
exit: cap.exit,
|
|
235
|
-
}),
|
|
236
|
-
/mandrel migrate.*new binary exited non-zero/,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// doctor must NOT have been called after migrate failure.
|
|
240
|
-
const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
|
|
241
|
-
assert.deepEqual(
|
|
242
|
-
phases.map((p) => p.phase),
|
|
243
|
-
['sync', 'migrate'],
|
|
244
|
-
);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('maps non-zero doctor to doctor-failed + exit 1', async () => {
|
|
248
|
-
const cap = makeCapture();
|
|
249
|
-
const seams = makeReExecSeams({
|
|
250
|
-
target: '1.44.0',
|
|
251
|
-
current: '1.43.0',
|
|
252
|
-
spawnResults: {
|
|
253
|
-
doctor: {
|
|
254
|
-
ok: false,
|
|
255
|
-
stdout: '',
|
|
256
|
-
stderr: 'agents-drift: drift detected',
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const result = await runUpdate({
|
|
262
|
-
argv: [],
|
|
263
|
-
...seams,
|
|
264
|
-
write: cap.write,
|
|
265
|
-
writeErr: cap.writeErr,
|
|
266
|
-
exit: cap.exit,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
assert.equal(result.ok, false);
|
|
270
|
-
assert.equal(result.action, 'doctor-failed');
|
|
271
|
-
assert.equal(cap.exitCode, 1);
|
|
272
|
-
assert.match(cap.err.join(''), /doctor reported failures/);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('still runs the changelog seam after a successful re-exec cycle', async () => {
|
|
276
|
-
const cap = makeCapture();
|
|
277
|
-
const seams = makeReExecSeams({ target: '1.44.0', current: '1.43.0' });
|
|
278
|
-
|
|
279
|
-
await runUpdate({
|
|
280
|
-
argv: [],
|
|
281
|
-
...seams,
|
|
282
|
-
write: cap.write,
|
|
283
|
-
writeErr: cap.writeErr,
|
|
284
|
-
exit: cap.exit,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
assert.ok(
|
|
288
|
-
seams.calls.includes('changelog:1.44.0'),
|
|
289
|
-
'changelog seam must fire after successful re-exec phases',
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
// AC: in-process seams still work when spawnPhase is NOT injected
|
|
296
|
-
// (backward compatibility — pre-Story-#4034 tests must stay green)
|
|
297
|
-
// ---------------------------------------------------------------------------
|
|
298
|
-
|
|
299
|
-
describe('runUpdate — in-process backward-compat path (no spawnPhase)', () => {
|
|
300
|
-
it('uses injected runSync/runMigrations/runDoctor when spawnPhase is absent', async () => {
|
|
301
|
-
const calls = [];
|
|
302
|
-
const cap = makeCapture();
|
|
303
|
-
|
|
304
|
-
const result = await runUpdate({
|
|
305
|
-
argv: [],
|
|
306
|
-
currentVersion: '1.43.0',
|
|
307
|
-
resolveTargetVersion: async () => '1.44.0',
|
|
308
|
-
npmUpdate: async (v) => {
|
|
309
|
-
calls.push(`npm-update:${v}`);
|
|
310
|
-
},
|
|
311
|
-
runSync: (_opts) => {
|
|
312
|
-
calls.push('runSync');
|
|
313
|
-
return {};
|
|
314
|
-
},
|
|
315
|
-
runMigrations: ({ fromVersion, toVersion }) => {
|
|
316
|
-
calls.push(`runMigrations:${fromVersion}->${toVersion}`);
|
|
317
|
-
return { applied: [], skipped: [] };
|
|
318
|
-
},
|
|
319
|
-
runDoctor: async () => {
|
|
320
|
-
calls.push('runDoctor');
|
|
321
|
-
return { ok: true, results: [] };
|
|
322
|
-
},
|
|
323
|
-
surfaceChangelog: async (v) => {
|
|
324
|
-
calls.push(`changelog:${v}`);
|
|
325
|
-
},
|
|
326
|
-
write: cap.write,
|
|
327
|
-
writeErr: cap.writeErr,
|
|
328
|
-
exit: cap.exit,
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
assert.deepEqual(calls, [
|
|
332
|
-
'npm-update:1.44.0',
|
|
333
|
-
'runSync',
|
|
334
|
-
'runMigrations:1.43.0->1.44.0',
|
|
335
|
-
'runDoctor',
|
|
336
|
-
'changelog:1.44.0',
|
|
337
|
-
]);
|
|
338
|
-
assert.equal(result.ok, true);
|
|
339
|
-
assert.equal(result.action, 'updated');
|
|
340
|
-
assert.equal(cap.exitCode, null);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
// ---------------------------------------------------------------------------
|
|
345
|
-
// AC: resolveNewBinPath returns the correct platform path
|
|
346
|
-
// ---------------------------------------------------------------------------
|
|
347
|
-
|
|
348
|
-
describe('resolveNewBinPath', () => {
|
|
349
|
-
it('returns node_modules/.bin/mandrel on POSIX', () => {
|
|
350
|
-
// We cannot force-test win32 on non-win32 but we can verify the POSIX shape.
|
|
351
|
-
if (process.platform !== 'win32') {
|
|
352
|
-
const p = resolveNewBinPath('/some/project');
|
|
353
|
-
assert.equal(
|
|
354
|
-
p,
|
|
355
|
-
path.join('/some', 'project', 'node_modules', '.bin', 'mandrel'),
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('returns a path that ends with mandrel or mandrel.cmd', () => {
|
|
361
|
-
const p = resolveNewBinPath('/project');
|
|
362
|
-
assert.ok(
|
|
363
|
-
p.endsWith('mandrel') || p.endsWith('mandrel.cmd'),
|
|
364
|
-
`expected path to end with mandrel[.cmd], got: ${p}`,
|
|
365
|
-
);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('contains node_modules/.bin in the path', () => {
|
|
369
|
-
const p = resolveNewBinPath('/project');
|
|
370
|
-
assert.ok(
|
|
371
|
-
p.includes(path.join('node_modules', '.bin')),
|
|
372
|
-
`expected node_modules/.bin in path, got: ${p}`,
|
|
373
|
-
);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// ---------------------------------------------------------------------------
|
|
378
|
-
// AC: defaultSpawnPhase routes output through write sinks and throws on error
|
|
379
|
-
// ---------------------------------------------------------------------------
|
|
380
|
-
|
|
381
|
-
describe('defaultSpawnPhase — output routing and error handling', () => {
|
|
382
|
-
it('routes child stdout through the write sink', () => {
|
|
383
|
-
const out = [];
|
|
384
|
-
const err = [];
|
|
385
|
-
const fakeSpawn = (_bin, _args, _opts) => ({
|
|
386
|
-
status: 0,
|
|
387
|
-
stdout: 'Materialized 832 files\n',
|
|
388
|
-
stderr: '',
|
|
389
|
-
error: undefined,
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
defaultSpawnPhase('sync', [], {
|
|
393
|
-
binPath: '/fake/bin/mandrel',
|
|
394
|
-
cwd: '/project',
|
|
395
|
-
write: (s) => out.push(s),
|
|
396
|
-
writeErr: (s) => err.push(s),
|
|
397
|
-
spawnFn: fakeSpawn,
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
assert.deepEqual(out, ['Materialized 832 files\n']);
|
|
401
|
-
assert.deepEqual(err, []);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('routes child stderr through the writeErr sink', () => {
|
|
405
|
-
const out = [];
|
|
406
|
-
const err = [];
|
|
407
|
-
const fakeSpawn = () => ({
|
|
408
|
-
status: 0,
|
|
409
|
-
stdout: '',
|
|
410
|
-
stderr: 'warning: something\n',
|
|
411
|
-
error: undefined,
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
defaultSpawnPhase('doctor', [], {
|
|
415
|
-
binPath: '/fake/bin/mandrel',
|
|
416
|
-
cwd: '/project',
|
|
417
|
-
write: (s) => out.push(s),
|
|
418
|
-
writeErr: (s) => err.push(s),
|
|
419
|
-
spawnFn: fakeSpawn,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
assert.deepEqual(out, []);
|
|
423
|
-
assert.deepEqual(err, ['warning: something\n']);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('returns ok:false when child exits non-zero', () => {
|
|
427
|
-
const fakeSpawn = () => ({
|
|
428
|
-
status: 1,
|
|
429
|
-
stdout: '',
|
|
430
|
-
stderr: 'drift detected',
|
|
431
|
-
error: undefined,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const result = defaultSpawnPhase('doctor', [], {
|
|
435
|
-
binPath: '/fake/bin/mandrel',
|
|
436
|
-
cwd: '/project',
|
|
437
|
-
write: () => {},
|
|
438
|
-
writeErr: () => {},
|
|
439
|
-
spawnFn: fakeSpawn,
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
assert.equal(result.ok, false);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('returns ok:true when child exits 0', () => {
|
|
446
|
-
const fakeSpawn = () => ({
|
|
447
|
-
status: 0,
|
|
448
|
-
stdout: 'all good',
|
|
449
|
-
stderr: '',
|
|
450
|
-
error: undefined,
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
const result = defaultSpawnPhase('sync', [], {
|
|
454
|
-
binPath: '/fake/bin/mandrel',
|
|
455
|
-
cwd: '/project',
|
|
456
|
-
write: () => {},
|
|
457
|
-
writeErr: () => {},
|
|
458
|
-
spawnFn: fakeSpawn,
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
assert.equal(result.ok, true);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it('throws a descriptive error when the spawn itself fails (ENOENT)', () => {
|
|
465
|
-
const fakeSpawn = () => ({
|
|
466
|
-
status: null,
|
|
467
|
-
stdout: '',
|
|
468
|
-
stderr: '',
|
|
469
|
-
error: new Error('spawnSync mandrel ENOENT'),
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
assert.throws(
|
|
473
|
-
() =>
|
|
474
|
-
defaultSpawnPhase('sync', [], {
|
|
475
|
-
binPath: '/fake/bin/mandrel',
|
|
476
|
-
cwd: '/project',
|
|
477
|
-
write: () => {},
|
|
478
|
-
writeErr: () => {},
|
|
479
|
-
spawnFn: fakeSpawn,
|
|
480
|
-
}),
|
|
481
|
-
/failed to spawn.*mandrel sync.*new binary.*ENOENT/,
|
|
482
|
-
);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it('passes the correct argv vector to the spawn function', () => {
|
|
486
|
-
const calls = [];
|
|
487
|
-
const fakeSpawn = (bin, args, opts) => {
|
|
488
|
-
calls.push({ bin, args, opts });
|
|
489
|
-
return { status: 0, stdout: '', stderr: '', error: undefined };
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
defaultSpawnPhase('migrate', ['--from', '1.43.0', '--to', '1.44.0'], {
|
|
493
|
-
binPath: '/nm/.bin/mandrel',
|
|
494
|
-
cwd: '/proj',
|
|
495
|
-
write: () => {},
|
|
496
|
-
writeErr: () => {},
|
|
497
|
-
spawnFn: fakeSpawn,
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
assert.equal(calls.length, 1);
|
|
501
|
-
assert.equal(calls[0].bin, '/nm/.bin/mandrel');
|
|
502
|
-
assert.deepEqual(calls[0].args, [
|
|
503
|
-
'migrate',
|
|
504
|
-
'--from',
|
|
505
|
-
'1.43.0',
|
|
506
|
-
'--to',
|
|
507
|
-
'1.44.0',
|
|
508
|
-
]);
|
|
509
|
-
assert.equal(calls[0].opts.cwd, '/proj');
|
|
510
|
-
// Shell flag is win32-gated: true only on Windows.
|
|
511
|
-
assert.equal(calls[0].opts.shell, process.platform === 'win32');
|
|
512
|
-
});
|
|
513
|
-
});
|