mandrel 1.60.0 → 1.62.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.
Files changed (49) hide show
  1. package/.agents/README.md +74 -32
  2. package/.agents/docs/SDLC.md +18 -12
  3. package/.agents/docs/configuration.md +61 -4
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +3 -4
  6. package/.agents/runtime-deps.json +2 -2
  7. package/.agents/scripts/README.md +1 -1
  8. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  9. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +46 -0
  10. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  11. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  12. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  13. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  14. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  15. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  16. package/.agents/scripts/lib/errors/index.js +4 -4
  17. package/.agents/scripts/lib/label-taxonomy.js +2 -2
  18. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  19. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  20. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  21. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  22. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  23. package/.agents/workflows/agents-update.md +14 -29
  24. package/.agents/workflows/deliver.md +87 -26
  25. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  26. package/.agents/workflows/helpers/deliver-epic.md +12 -5
  27. package/.agents/workflows/helpers/deliver-stories.md +13 -7
  28. package/.agents/workflows/plan.md +48 -4
  29. package/README.md +18 -30
  30. package/bin/mandrel.js +235 -16
  31. package/docs/CHANGELOG.md +36 -0
  32. package/lib/cli/doctor.js +45 -3
  33. package/lib/cli/init.js +66 -7
  34. package/lib/cli/registry.js +42 -146
  35. package/lib/cli/sync.js +122 -23
  36. package/lib/cli/uninstall.js +42 -7
  37. package/lib/cli/update.js +257 -198
  38. package/lib/cli/version-helpers.js +59 -0
  39. package/package.json +6 -6
  40. package/.agents/workflows/onboard.md +0 -208
  41. package/lib/cli/__tests__/migrate.test.js +0 -268
  42. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  43. package/lib/cli/__tests__/sync.test.js +0 -372
  44. package/lib/cli/__tests__/update-changelog-surface.test.js +0 -357
  45. package/lib/cli/__tests__/update-major.test.js +0 -217
  46. package/lib/cli/__tests__/update-reexec.test.js +0 -513
  47. package/lib/cli/__tests__/update.test.js +0 -696
  48. package/lib/cli/__tests__/version-check.test.js +0 -398
  49. package/lib/migrations/__tests__/index.test.js +0 -216
@@ -1,357 +0,0 @@
1
- // lib/cli/__tests__/update-changelog-surface.test.js
2
- /**
3
- * Unit tests for the `defaultSurfaceChangelog` fallback chain and
4
- * `fetchChangelogFromGitHub` introduced by Story #4035 — mandrel update
5
- * changelog surface (ship or fetch).
6
- *
7
- * The three paths under test (via the `run` entrypoint):
8
- * 1. Packaged file present → reads and prints the matching section(s).
9
- * 2. Packaged file absent, GitHub fetch succeeds → prints the section(s)
10
- * from the fetched content.
11
- * 3. Both sources unavailable → emits an actionable warning with a link
12
- * to the GitHub Releases page; never throws.
13
- *
14
- * Plus unit coverage for `fetchChangelogFromGitHub` itself:
15
- * - Resolves the content from the first tag that returns 2xx.
16
- * - Tries the namespaced tag (`mandrel-v<ver>`) first, then bare (`v<ver>`).
17
- * - Throws when both tag forms return non-2xx.
18
- * - Throws when the HTTP request errors out.
19
- *
20
- * `defaultSurfaceChangelog` is exercised through the `run` default export
21
- * so the full wiring from `deps` through to output is verified. The
22
- * `fetchChangelog` seam (and `https` in `fetchChangelogFromGitHub`) are
23
- * injected so no real HTTP call occurs (testing-standards § Unit: mock all I/O).
24
- *
25
- * Tier: unit (testing-standards § Unit). All I/O — filesystem and network
26
- * — is mocked via injectable seams.
27
- *
28
- * Security (security-baseline § 5 — Data Leakage & Logging): fixtures carry
29
- * only version strings and file paths; no tokens or credentials are used.
30
- */
31
-
32
- import assert from 'node:assert/strict';
33
- import { EventEmitter } from 'node:events';
34
- import { describe, it } from 'node:test';
35
-
36
- import run, { fetchChangelogFromGitHub } from '../update.js';
37
-
38
- // ---------------------------------------------------------------------------
39
- // Shared fixtures
40
- // ---------------------------------------------------------------------------
41
-
42
- const CURRENT_VERSION = '1.58.0';
43
- const TARGET_VERSION = '1.59.0';
44
- const CACHE_PATH = '/virtual/temp/version-check.json';
45
- const CHANGELOG_PATH = '/virtual/docs/CHANGELOG.md';
46
-
47
- const CHANGELOG_CONTENT = `# Changelog
48
-
49
- ## [1.59.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.58.0...mandrel-v1.59.0) (2026-06-11)
50
-
51
- ### Added
52
-
53
- * **cli:** add mandrel init one-command cold start
54
-
55
- ## [1.58.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.57.0...mandrel-v1.58.0) (2026-05-30)
56
-
57
- ### Fixed
58
-
59
- * an older fix from 1.58
60
- `;
61
-
62
- /**
63
- * Minimal in-memory fs fake. When `changelogContent` is provided, the
64
- * changelog path resolves; otherwise it throws ENOENT (simulating the
65
- * absent-from-tarball scenario pre-Story #4035).
66
- *
67
- * @param {{ changelogContent?: string }} [opts]
68
- */
69
- function makeFs({ changelogContent } = {}) {
70
- const files = new Map([
71
- [
72
- CACHE_PATH,
73
- JSON.stringify({
74
- latestVersion: TARGET_VERSION,
75
- checkedAt: '2026-06-11T00:00:00.000Z',
76
- }),
77
- ],
78
- ...(changelogContent ? [[CHANGELOG_PATH, changelogContent]] : []),
79
- ]);
80
- return {
81
- readFileSync(p, _enc) {
82
- if (!files.has(p)) {
83
- throw Object.assign(new Error(`ENOENT: ${p}`), { code: 'ENOENT' });
84
- }
85
- return files.get(p);
86
- },
87
- writeFileSync() {},
88
- mkdirSync() {},
89
- existsSync(p) {
90
- return files.has(p);
91
- },
92
- };
93
- }
94
-
95
- /** Capture stdout/stderr writes. */
96
- function makeCapture() {
97
- const out = [];
98
- const err = [];
99
- return {
100
- out,
101
- err,
102
- write: (s) => out.push(s),
103
- writeErr: (s) => err.push(s),
104
- exit: () => {},
105
- };
106
- }
107
-
108
- /**
109
- * Build the stubbed `deps` for `run`. Downstream seams (sync, migrate,
110
- * doctor) are no-ops — the boundary under test is the changelog surface.
111
- *
112
- * @param {{
113
- * fs: ReturnType<typeof makeFs>,
114
- * cap: ReturnType<typeof makeCapture>,
115
- * fetchChangelog?: (v: string) => Promise<string>,
116
- * }} opts
117
- */
118
- function makeDeps(fs, cap, fetchChangelog) {
119
- return {
120
- currentVersion: CURRENT_VERSION,
121
- cachePath: CACHE_PATH,
122
- changelogPath: CHANGELOG_PATH,
123
- fs,
124
- now: new Date('2026-06-11T00:30:00.000Z'),
125
- versionRunner: () => TARGET_VERSION,
126
- runInstall: () => ({ status: 0, stderr: '' }),
127
- runSync: () => ({ copied: 0, planned: 0, dryRun: false }),
128
- runMigrations: () => ({ applied: [], skipped: [] }),
129
- runDoctor: async () => ({ ok: true, results: [] }),
130
- write: cap.write,
131
- writeErr: cap.writeErr,
132
- exit: cap.exit,
133
- ...(fetchChangelog !== undefined ? { fetchChangelog } : {}),
134
- };
135
- }
136
-
137
- // ---------------------------------------------------------------------------
138
- // Path 1 — packaged file present
139
- // ---------------------------------------------------------------------------
140
-
141
- describe('defaultSurfaceChangelog — packaged file present (Story #4035)', () => {
142
- it('prints the matching in-range section from the packaged changelog', async () => {
143
- const fs = makeFs({ changelogContent: CHANGELOG_CONTENT });
144
- const cap = makeCapture();
145
-
146
- await run([], makeDeps(fs, cap));
147
-
148
- const joined = cap.out.join('');
149
- // In-range (1.59.0) section surfaced.
150
- assert.match(joined, /Changelog for v1\.59\.0/);
151
- assert.match(joined, /mandrel init one-command cold start/);
152
- // Out-of-range section (1.58.0) must NOT appear.
153
- assert.doesNotMatch(joined, /an older fix from 1\.58/);
154
- });
155
-
156
- it('does not invoke the GitHub fetch seam when the packaged file is readable', async () => {
157
- const fs = makeFs({ changelogContent: CHANGELOG_CONTENT });
158
- const cap = makeCapture();
159
- let fetchCalled = false;
160
- const fetchChangelog = async () => {
161
- fetchCalled = true;
162
- return CHANGELOG_CONTENT;
163
- };
164
-
165
- await run([], makeDeps(fs, cap, fetchChangelog));
166
-
167
- assert.equal(
168
- fetchCalled,
169
- false,
170
- 'GitHub fetch must not run when packaged file is present',
171
- );
172
- });
173
- });
174
-
175
- // ---------------------------------------------------------------------------
176
- // Path 2 — packaged file absent, GitHub fetch succeeds
177
- // ---------------------------------------------------------------------------
178
-
179
- describe('defaultSurfaceChangelog — GitHub fetch fallback (Story #4035)', () => {
180
- it('fetches from GitHub and prints the matching section when the packaged file is absent', async () => {
181
- const fs = makeFs(); // no changelog seeded → ENOENT
182
- const cap = makeCapture();
183
- const fetchChangelog = async (_version) => CHANGELOG_CONTENT;
184
-
185
- await run([], makeDeps(fs, cap, fetchChangelog));
186
-
187
- const joined = cap.out.join('');
188
- assert.match(joined, /Changelog for v1\.59\.0/);
189
- assert.match(joined, /mandrel init one-command cold start/);
190
- assert.doesNotMatch(joined, /an older fix from 1\.58/);
191
- // No error written — successful fallback.
192
- assert.deepEqual(cap.err, []);
193
- });
194
-
195
- it('passes the target version to the fetchChangelog seam', async () => {
196
- const fs = makeFs();
197
- const cap = makeCapture();
198
- const fetched = [];
199
- const fetchChangelog = async (version) => {
200
- fetched.push(version);
201
- return CHANGELOG_CONTENT;
202
- };
203
-
204
- await run([], makeDeps(fs, cap, fetchChangelog));
205
-
206
- assert.deepEqual(fetched, [TARGET_VERSION]);
207
- });
208
- });
209
-
210
- // ---------------------------------------------------------------------------
211
- // Path 3 — both sources unavailable → actionable degradation message
212
- // ---------------------------------------------------------------------------
213
-
214
- describe('defaultSurfaceChangelog — actionable degradation (Story #4035)', () => {
215
- it('emits a message with the GitHub Releases URL when both sources fail', async () => {
216
- const fs = makeFs(); // no changelog → ENOENT
217
- const cap = makeCapture();
218
- const fetchChangelog = async () => {
219
- throw new Error('HTTP 404');
220
- };
221
-
222
- await run([], makeDeps(fs, cap, fetchChangelog));
223
-
224
- const errJoined = cap.err.join('');
225
- // Must mention v1.59.0 and include the releases link.
226
- assert.match(errJoined, /v1\.59\.0/);
227
- assert.match(errJoined, /github\.com\/dsj1984\/mandrel\/releases/);
228
- // Must NOT be the old bare "not found … skipping" message.
229
- assert.doesNotMatch(errJoined, /skipping changelog surface/);
230
- });
231
-
232
- it('never throws even when both the packaged file and GitHub fetch fail', async () => {
233
- const fs = makeFs();
234
- const cap = makeCapture();
235
- const fetchChangelog = async () => {
236
- throw new Error('network error');
237
- };
238
-
239
- // Must not reject.
240
- await assert.doesNotReject(() =>
241
- run([], makeDeps(fs, cap, fetchChangelog)),
242
- );
243
- });
244
-
245
- it('emits an actionable message when the changelog has no matching section', async () => {
246
- // Changelog only has a 1.57.0 entry — no 1.59.0 section.
247
- const sparseChangelog = `# Changelog\n\n## [1.57.0](https://example.test) (2026-04-01)\n\n### Fixed\n\n* old fix\n`;
248
- const fs = makeFs({ changelogContent: sparseChangelog });
249
- const cap = makeCapture();
250
-
251
- await run([], makeDeps(fs, cap));
252
-
253
- const errJoined = cap.err.join('');
254
- assert.match(errJoined, /v1\.59\.0/);
255
- assert.match(errJoined, /github\.com\/dsj1984\/mandrel\/releases/);
256
- assert.doesNotMatch(errJoined, /skipping changelog surface/);
257
- });
258
- });
259
-
260
- // ---------------------------------------------------------------------------
261
- // fetchChangelogFromGitHub unit tests
262
- // ---------------------------------------------------------------------------
263
-
264
- /**
265
- * Build a minimal fake `node:https` module. Each `response` entry defines
266
- * `{ status, body }` for successive calls to `https.get`. When an entry has
267
- * `error: true` the fake emits an 'error' event on the request object instead
268
- * of responding.
269
- *
270
- * @param {Array<{ status?: number, body?: string, error?: boolean }>} responses
271
- */
272
- function makeHttpsFake(responses) {
273
- let callIdx = 0;
274
- const capturedUrls = [];
275
- return {
276
- capturedUrls,
277
- https: {
278
- get(url, callback) {
279
- capturedUrls.push(url);
280
- const entry = responses[callIdx] ?? { status: 404, body: '' };
281
- callIdx += 1;
282
-
283
- const req = new EventEmitter();
284
-
285
- if (entry.error) {
286
- // Emit 'error' asynchronously so the Promise constructor has time to
287
- // attach the `.on('error')` listener.
288
- setImmediate(() =>
289
- req.emit('error', new Error('connection refused')),
290
- );
291
- } else {
292
- const res = new EventEmitter();
293
- res.statusCode = entry.status ?? 200;
294
- setImmediate(() => {
295
- callback(res);
296
- res.emit('data', Buffer.from(entry.body ?? ''));
297
- res.emit('end');
298
- });
299
- }
300
-
301
- return req;
302
- },
303
- },
304
- };
305
- }
306
-
307
- describe('fetchChangelogFromGitHub — HTTP seam (Story #4035)', () => {
308
- it('returns the body from a 200 response on the namespaced tag', async () => {
309
- const { https, capturedUrls } = makeHttpsFake([
310
- { status: 200, body: '# Changelog\n\n## [1.59.0] content' },
311
- ]);
312
-
313
- const result = await fetchChangelogFromGitHub('1.59.0', { https });
314
-
315
- assert.match(result, /\[1\.59\.0\] content/);
316
- // Must have tried the namespaced tag first.
317
- assert.ok(
318
- capturedUrls[0].includes('mandrel-v1.59.0'),
319
- 'namespaced tag tried first',
320
- );
321
- });
322
-
323
- it('falls back to bare vX.Y.Z tag when the namespaced tag returns 404', async () => {
324
- const { https, capturedUrls } = makeHttpsFake([
325
- { status: 404, body: 'Not Found' }, // mandrel-v1.59.0 → 404
326
- { status: 200, body: '# Changelog\n## [1.59.0] bare' }, // v1.59.0 → 200
327
- ]);
328
-
329
- const result = await fetchChangelogFromGitHub('1.59.0', { https });
330
-
331
- assert.match(result, /\[1\.59\.0\] bare/);
332
- assert.equal(capturedUrls.length, 2);
333
- assert.ok(capturedUrls[0].includes('mandrel-v1.59.0'));
334
- assert.ok(capturedUrls[1].includes('/v1.59.0/'));
335
- });
336
-
337
- it('throws when both tag forms return non-2xx', async () => {
338
- const { https } = makeHttpsFake([
339
- { status: 404, body: 'Not Found' },
340
- { status: 404, body: 'Not Found' },
341
- ]);
342
-
343
- await assert.rejects(
344
- () => fetchChangelogFromGitHub('1.59.0', { https }),
345
- /non-2xx for all tag forms/,
346
- );
347
- });
348
-
349
- it('throws when the HTTP request emits an error', async () => {
350
- const { https } = makeHttpsFake([{ error: true }]);
351
-
352
- await assert.rejects(
353
- () => fetchChangelogFromGitHub('1.59.0', { https }),
354
- /connection refused/,
355
- );
356
- });
357
- });
@@ -1,217 +0,0 @@
1
- // lib/cli/__tests__/update-major.test.js
2
- /**
3
- * Unit tests for the major-version gate in lib/cli/update.js.
4
- *
5
- * Every test drives runUpdate through injectable seams; no real npm process,
6
- * filesystem I/O, or network call occurs (testing-standards § Unit).
7
- *
8
- * Coverage contract (Story #3503 AC — major gate):
9
- * - When the newest version crosses a major boundary and `--major` is
10
- * absent, run declines, prints the docs/upgrade-major.md runbook pointer,
11
- * exits non-zero, and invokes NO npm-update / sync / migration / doctor
12
- * seam.
13
- * - When `--major` is passed, run applies the major target and prints the
14
- * runbook inline.
15
- * - --dry-run on a major target prints the plan without applying.
16
- */
17
-
18
- import assert from 'node:assert/strict';
19
- import { describe, it } from 'node:test';
20
-
21
- import { runUpdate } from '../update.js';
22
-
23
- // ---------------------------------------------------------------------------
24
- // Capture + seam helpers
25
- // ---------------------------------------------------------------------------
26
-
27
- /** Capture stdout/stderr writes and the exit code. */
28
- function makeCapture() {
29
- const out = [];
30
- const err = [];
31
- let exitCode = null;
32
- return {
33
- out,
34
- err,
35
- get exitCode() {
36
- return exitCode;
37
- },
38
- write: (s) => out.push(s),
39
- writeErr: (s) => err.push(s),
40
- exit: (code) => {
41
- exitCode = code;
42
- },
43
- };
44
- }
45
-
46
- /**
47
- * Build recording seams for a MAJOR crossing (1.43.0 → 2.0.0). Every effectful
48
- * seam records into `calls` so a test can assert that none of them ran when
49
- * the gate refuses.
50
- */
51
- function makeMajorSeams({ target = '2.0.0', doctorOk = true } = {}) {
52
- const calls = [];
53
- return {
54
- calls,
55
- currentVersion: '1.43.0',
56
- resolveTargetVersion: async () => {
57
- calls.push('resolveTargetVersion');
58
- return target;
59
- },
60
- npmUpdate: async (version) => {
61
- calls.push(`npmUpdate:${version}`);
62
- },
63
- runSync: (_opts) => {
64
- calls.push('runSync');
65
- return { copied: 0, planned: 0, dryRun: false };
66
- },
67
- runMigrations: ({ fromVersion, toVersion }) => {
68
- calls.push(`runMigrations:${fromVersion}->${toVersion}`);
69
- return { applied: [], skipped: [] };
70
- },
71
- runDoctor: async () => {
72
- calls.push('runDoctor');
73
- return {
74
- ok: doctorOk,
75
- results: [{ name: 'node-version', ok: doctorOk }],
76
- };
77
- },
78
- surfaceChangelog: async (version) => {
79
- calls.push(`surfaceChangelog:${version}`);
80
- },
81
- };
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // AC — major boundary without --major is refused
86
- // ---------------------------------------------------------------------------
87
-
88
- describe('runUpdate — major gate without --major', () => {
89
- it('declines, exits non-zero, and invokes no effectful seam', async () => {
90
- const seams = makeMajorSeams({ target: '2.0.0' });
91
- const cap = makeCapture();
92
-
93
- const result = await runUpdate({
94
- argv: [],
95
- ...seams,
96
- write: cap.write,
97
- writeErr: cap.writeErr,
98
- exit: cap.exit,
99
- });
100
-
101
- assert.equal(result.ok, false);
102
- assert.equal(result.action, 'declined-major');
103
- assert.equal(result.major, true);
104
- assert.deepEqual(result.stepsRun, []);
105
- assert.equal(cap.exitCode, 1);
106
-
107
- // No npm-update / sync / migration / doctor seam fired — only the resolve.
108
- assert.deepEqual(seams.calls, ['resolveTargetVersion']);
109
- });
110
-
111
- it('prints the docs/upgrade-major.md runbook pointer and the available version', async () => {
112
- const seams = makeMajorSeams({ target: '2.0.0' });
113
- const cap = makeCapture();
114
-
115
- await runUpdate({
116
- argv: [],
117
- ...seams,
118
- write: cap.write,
119
- writeErr: cap.writeErr,
120
- exit: cap.exit,
121
- });
122
-
123
- const joined = cap.err.join('');
124
- assert.match(joined, /docs\/upgrade-major\.md/);
125
- assert.match(joined, /2\.0\.0/);
126
- assert.match(joined, /--major/);
127
- });
128
-
129
- it('gates a 1.x → 3.0 leap the same way', async () => {
130
- const seams = makeMajorSeams({ target: '3.0.0' });
131
- const cap = makeCapture();
132
-
133
- const result = await runUpdate({
134
- argv: [],
135
- ...seams,
136
- write: cap.write,
137
- writeErr: cap.writeErr,
138
- exit: cap.exit,
139
- });
140
-
141
- assert.equal(result.action, 'declined-major');
142
- assert.equal(cap.exitCode, 1);
143
- assert.deepEqual(seams.calls, ['resolveTargetVersion']);
144
- });
145
- });
146
-
147
- // ---------------------------------------------------------------------------
148
- // AC — --major applies the major target and prints the runbook inline
149
- // ---------------------------------------------------------------------------
150
-
151
- describe('runUpdate — major gate with --major', () => {
152
- it('applies the major target, driving the ordered steps', async () => {
153
- const seams = makeMajorSeams({ target: '2.0.0', doctorOk: true });
154
- const cap = makeCapture();
155
-
156
- const result = await runUpdate({
157
- argv: ['--major'],
158
- ...seams,
159
- write: cap.write,
160
- writeErr: cap.writeErr,
161
- exit: cap.exit,
162
- });
163
-
164
- assert.equal(result.ok, true);
165
- assert.equal(result.action, 'updated');
166
- assert.equal(result.major, true);
167
- assert.deepEqual(result.stepsRun, [
168
- 'npm-update',
169
- 'runSync',
170
- 'runMigrations',
171
- 'doctor',
172
- ]);
173
- assert.ok(seams.calls.includes('npmUpdate:2.0.0'));
174
- assert.ok(seams.calls.includes('runMigrations:1.43.0->2.0.0'));
175
- assert.equal(cap.exitCode, null);
176
- });
177
-
178
- it('prints the runbook pointer inline on the applied-major path', async () => {
179
- const seams = makeMajorSeams({ target: '2.0.0' });
180
- const cap = makeCapture();
181
-
182
- await runUpdate({
183
- argv: ['--major'],
184
- ...seams,
185
- write: cap.write,
186
- writeErr: cap.writeErr,
187
- exit: cap.exit,
188
- });
189
-
190
- assert.match(cap.out.join(''), /docs\/upgrade-major\.md/);
191
- });
192
- });
193
-
194
- // ---------------------------------------------------------------------------
195
- // --dry-run on a major target previews without applying
196
- // ---------------------------------------------------------------------------
197
-
198
- describe('runUpdate — --major --dry-run', () => {
199
- it('prints the plan and invokes no effectful seam', async () => {
200
- const seams = makeMajorSeams({ target: '2.0.0' });
201
- const cap = makeCapture();
202
-
203
- const result = await runUpdate({
204
- argv: ['--major', '--dry-run'],
205
- ...seams,
206
- write: cap.write,
207
- writeErr: cap.writeErr,
208
- exit: cap.exit,
209
- });
210
-
211
- assert.equal(result.action, 'dry-run');
212
- assert.match(cap.out.join(''), /1\.43\.0 → v2\.0\.0/);
213
- assert.match(cap.out.join(''), /major upgrade/);
214
- assert.deepEqual(seams.calls, ['resolveTargetVersion']);
215
- assert.equal(cap.exitCode, null);
216
- });
217
- });