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,247 +0,0 @@
1
- // lib/cli/__tests__/sync-local-zone.test.js
2
- /**
3
- * Unit tests for the `.agents/local/` sync-exempt local-additions zone
4
- * (Story #3498, f-drift-local-zone).
5
- *
6
- * Contract under test (Story #3498 AC):
7
- * 1. runSync skips any path under `.agents/local/` when copying the
8
- * package payload — proven by pre-populating a consumer-authored
9
- * `.agents/local/custom.md` in the destination via an injected fs
10
- * seam and asserting it is left byte-identical after a sync.
11
- * 2. `mandrel sync` writes no file into `.agents/local/` from the package
12
- * payload — proven by seeding a (hypothetical) payload file under the
13
- * source `.agents/local/` and asserting it is never enumerated, never
14
- * copied, and never appears in the dry-run plan.
15
- *
16
- * Every test drives runSync through the same injectable seams used by
17
- * sync.test.js (resolvePackageRoot, fs, cwd, write, writeErr, exit) backed
18
- * by an in-memory filesystem fake, so no real package resolution and no
19
- * real disk I/O occur (testing-standards § Unit: all filesystem I/O MUST
20
- * be mocked; sync.js injectable-seam style — no real child processes).
21
- */
22
-
23
- import assert from 'node:assert/strict';
24
- import path from 'node:path';
25
- import { describe, it } from 'node:test';
26
-
27
- import { runSync } from '../sync.js';
28
-
29
- // ---------------------------------------------------------------------------
30
- // In-memory filesystem fake (mirrors sync.test.js)
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Build an in-memory fs whose `seed` maps absolute file paths → contents.
35
- * Directories are inferred from the seeded file paths.
36
- *
37
- * Tracks writes (copyFileSync) and guards against symlink creation.
38
- */
39
- function makeFs(seed = {}) {
40
- const files = new Map(Object.entries(seed));
41
- const symlinkCalls = [];
42
-
43
- function dirEntries(dir) {
44
- const norm = dir.endsWith(path.sep) ? dir.slice(0, -1) : dir;
45
- const children = new Map(); // name → isDir
46
- for (const abs of files.keys()) {
47
- if (!abs.startsWith(norm + path.sep)) continue;
48
- const rest = abs.slice(norm.length + 1);
49
- const segments = rest.split(path.sep);
50
- const name = segments[0];
51
- children.set(name, segments.length > 1);
52
- }
53
- return [...children.entries()].map(([name, isDir]) => ({
54
- name,
55
- isDirectory: () => isDir,
56
- }));
57
- }
58
-
59
- return {
60
- files,
61
- symlinkCalls,
62
- readdirSync(dir, _opts) {
63
- return dirEntries(dir);
64
- },
65
- existsSync(p) {
66
- const norm = p.endsWith(path.sep) ? p.slice(0, -1) : p;
67
- if (files.has(norm)) return true;
68
- for (const abs of files.keys()) {
69
- if (abs.startsWith(norm + path.sep)) return true;
70
- }
71
- return false;
72
- },
73
- mkdirSync(_dir, _opts) {
74
- // No-op: directories are implied by file paths in this fake.
75
- },
76
- copyFileSync(src, dest) {
77
- if (!files.has(src)) {
78
- throw new Error(`copyFileSync: source missing ${src}`);
79
- }
80
- files.set(dest, files.get(src));
81
- },
82
- symlinkSync(target, p) {
83
- symlinkCalls.push({ target, path: p });
84
- },
85
- };
86
- }
87
-
88
- /** Capture stdout/stderr writes and the exit code. */
89
- function makeCapture() {
90
- const out = [];
91
- const err = [];
92
- let exitCode = null;
93
- return {
94
- out,
95
- err,
96
- get exitCode() {
97
- return exitCode;
98
- },
99
- write: (s) => out.push(s),
100
- writeErr: (s) => err.push(s),
101
- exit: (code) => {
102
- exitCode = code;
103
- },
104
- };
105
- }
106
-
107
- const PROJECT = path.join(path.sep, 'proj');
108
- const PKG_ROOT = path.join(PROJECT, 'node_modules', 'mandrel');
109
- const SRC_AGENTS = path.join(PKG_ROOT, '.agents');
110
- const resolveToPkg = () => PKG_ROOT;
111
-
112
- /** Seed a normal package payload of two non-local files under <pkg>/.agents/. */
113
- function seedPackagePayload() {
114
- return {
115
- [path.join(SRC_AGENTS, 'instructions.md')]: '# instructions\n',
116
- [path.join(SRC_AGENTS, 'rules', 'security-baseline.md')]: '# security\n',
117
- };
118
- }
119
-
120
- const baseOpts = (fs, cap) => ({
121
- argv: [],
122
- resolvePackageRoot: resolveToPkg,
123
- fs,
124
- cwd: () => PROJECT,
125
- write: cap.write,
126
- writeErr: cap.writeErr,
127
- exit: cap.exit,
128
- });
129
-
130
- // ---------------------------------------------------------------------------
131
- // AC1 — pre-existing consumer .agents/local/ additions survive sync
132
- // ---------------------------------------------------------------------------
133
-
134
- describe('runSync — .agents/local/ consumer additions survive', () => {
135
- it('leaves a pre-existing .agents/local/custom.md untouched', () => {
136
- // Arrange: a normal payload plus a consumer-authored file already living
137
- // in the destination's local zone.
138
- const localAddition = path.join(PROJECT, '.agents', 'local', 'custom.md');
139
- const fs = makeFs({
140
- ...seedPackagePayload(),
141
- [localAddition]: '# my custom local note\n',
142
- });
143
- const cap = makeCapture();
144
-
145
- // Act
146
- const result = runSync(baseOpts(fs, cap));
147
-
148
- // Assert: the local addition is byte-identical and the regular payload
149
- // still materialized.
150
- assert.equal(fs.files.get(localAddition), '# my custom local note\n');
151
- assert.equal(
152
- fs.files.get(path.join(PROJECT, '.agents', 'instructions.md')),
153
- '# instructions\n',
154
- );
155
- assert.equal(result.copied, 2);
156
- assert.equal(cap.exitCode, null);
157
- });
158
-
159
- it('never writes into the destination .agents/local/ subtree', () => {
160
- const localAddition = path.join(PROJECT, '.agents', 'local', 'custom.md');
161
- const fs = makeFs({
162
- ...seedPackagePayload(),
163
- [localAddition]: '# my custom local note\n',
164
- });
165
- const cap = makeCapture();
166
-
167
- runSync(baseOpts(fs, cap));
168
-
169
- // The only destination file under .agents/local/ is the one the consumer
170
- // authored; sync added nothing there.
171
- const localPrefix = path.join(PROJECT, '.agents', 'local') + path.sep;
172
- const localDestFiles = [...fs.files.keys()].filter((k) =>
173
- k.startsWith(localPrefix),
174
- );
175
- assert.deepEqual(localDestFiles, [localAddition]);
176
- });
177
- });
178
-
179
- // ---------------------------------------------------------------------------
180
- // AC2 — a payload file under .agents/local/ is never materialized
181
- // ---------------------------------------------------------------------------
182
-
183
- describe('runSync — payload .agents/local/ is skipped on copy', () => {
184
- it('does not copy a source .agents/local/ file into the destination', () => {
185
- // Arrange: a payload that (hypothetically) ships a file under local/.
186
- // The published payload ships none, but the skip must hold defensively.
187
- const srcLocal = path.join(SRC_AGENTS, 'local', 'should-not-copy.md');
188
- const fs = makeFs({
189
- ...seedPackagePayload(),
190
- [srcLocal]: '# payload local file\n',
191
- });
192
- const cap = makeCapture();
193
-
194
- // Act
195
- const result = runSync(baseOpts(fs, cap));
196
-
197
- // Assert: the local payload file was never copied to the destination.
198
- const destLocal = path.join(
199
- PROJECT,
200
- '.agents',
201
- 'local',
202
- 'should-not-copy.md',
203
- );
204
- assert.equal(fs.files.has(destLocal), false);
205
- // Only the two non-local payload files were materialized.
206
- assert.equal(result.copied, 2);
207
- });
208
-
209
- it('omits .agents/local/ paths from the --dry-run plan', () => {
210
- const srcLocal = path.join(SRC_AGENTS, 'local', 'should-not-copy.md');
211
- const fs = makeFs({
212
- ...seedPackagePayload(),
213
- [srcLocal]: '# payload local file\n',
214
- });
215
- const cap = makeCapture();
216
-
217
- const result = runSync({ ...baseOpts(fs, cap), argv: ['--dry-run'] });
218
-
219
- const joined = cap.out.join('');
220
- assert.doesNotMatch(joined, /local/);
221
- assert.match(joined, /Dry run: 2 file\(s\)/);
222
- assert.equal(result.planned, 2);
223
- });
224
-
225
- it('still materializes a deeper non-top-level directory named local', () => {
226
- // The skip is scoped to the top-level .agents/local/ only — a nested
227
- // rules/local/ must still copy.
228
- const nestedLocal = path.join(SRC_AGENTS, 'rules', 'local', 'note.md');
229
- const fs = makeFs({
230
- ...seedPackagePayload(),
231
- [nestedLocal]: '# nested local note\n',
232
- });
233
- const cap = makeCapture();
234
-
235
- const result = runSync(baseOpts(fs, cap));
236
-
237
- const destNested = path.join(
238
- PROJECT,
239
- '.agents',
240
- 'rules',
241
- 'local',
242
- 'note.md',
243
- );
244
- assert.equal(fs.files.get(destNested), '# nested local note\n');
245
- assert.equal(result.copied, 3);
246
- });
247
- });
@@ -1,372 +0,0 @@
1
- // lib/cli/__tests__/sync.test.js
2
- /**
3
- * Unit tests for lib/cli/sync.js — the `mandrel sync` materializer.
4
- *
5
- * Every test drives runSync through injectable seams (resolvePackageRoot,
6
- * fs, cwd, write, writeErr, exit) backed by an in-memory filesystem fake,
7
- * so no real package resolution and no real disk I/O occur (testing-standards
8
- * § Unit: all filesystem I/O MUST be mocked).
9
- *
10
- * Coverage contract (per Story #3467 AC):
11
- * 1. Copies the package .agents/ tree into ./.agents/ as plain files
12
- * (no symlinks created — symlinkSync is never called).
13
- * 2. Re-running is idempotent — a second run leaves ./.agents/
14
- * byte-identical.
15
- * 3. Exits non-zero with an actionable message when mandrel is
16
- * not resolvable in node_modules.
17
- * 4. --dry-run reports planned copies and writes nothing.
18
- * 5. Module shape: runSync named export + default function export.
19
- */
20
-
21
- import assert from 'node:assert/strict';
22
- import path from 'node:path';
23
- import { describe, it } from 'node:test';
24
-
25
- import sync, { runSync } from '../sync.js';
26
-
27
- // ---------------------------------------------------------------------------
28
- // In-memory filesystem fake
29
- // ---------------------------------------------------------------------------
30
-
31
- /**
32
- * Build an in-memory fs whose `seed` maps absolute file paths → contents.
33
- * Directories are inferred from the seeded file paths.
34
- *
35
- * Tracks writes (copyFileSync) and guards against symlink creation so tests
36
- * can prove the materializer never produces symlinks.
37
- */
38
- function makeFs(seed = {}) {
39
- // Live store: absolute path → contents. Seeded entries plus anything the
40
- // code under test writes via copyFileSync.
41
- const files = new Map(Object.entries(seed));
42
- const symlinkCalls = [];
43
-
44
- function dirEntries(dir) {
45
- const norm = dir.endsWith(path.sep) ? dir.slice(0, -1) : dir;
46
- const children = new Map(); // name → isDir
47
- for (const abs of files.keys()) {
48
- if (!abs.startsWith(norm + path.sep)) continue;
49
- const rest = abs.slice(norm.length + 1);
50
- const segments = rest.split(path.sep);
51
- const name = segments[0];
52
- children.set(name, segments.length > 1);
53
- }
54
- return [...children.entries()].map(([name, isDir]) => ({
55
- name,
56
- isDirectory: () => isDir,
57
- }));
58
- }
59
-
60
- return {
61
- files,
62
- symlinkCalls,
63
- readdirSync(dir, _opts) {
64
- return dirEntries(dir);
65
- },
66
- existsSync(p) {
67
- const norm = p.endsWith(path.sep) ? p.slice(0, -1) : p;
68
- if (files.has(norm)) return true;
69
- // A directory "exists" if any seeded file lives under it.
70
- for (const abs of files.keys()) {
71
- if (abs.startsWith(norm + path.sep)) return true;
72
- }
73
- return false;
74
- },
75
- mkdirSync(_dir, _opts) {
76
- // No-op: directories are implied by file paths in this fake.
77
- },
78
- copyFileSync(src, dest) {
79
- if (!files.has(src)) {
80
- throw new Error(`copyFileSync: source missing ${src}`);
81
- }
82
- files.set(dest, files.get(src));
83
- },
84
- symlinkSync(target, p) {
85
- symlinkCalls.push({ target, path: p });
86
- },
87
- };
88
- }
89
-
90
- /** Capture stdout/stderr writes and the exit code. */
91
- function makeCapture() {
92
- const out = [];
93
- const err = [];
94
- let exitCode = null;
95
- return {
96
- out,
97
- err,
98
- get exitCode() {
99
- return exitCode;
100
- },
101
- write: (s) => out.push(s),
102
- writeErr: (s) => err.push(s),
103
- exit: (code) => {
104
- exitCode = code;
105
- },
106
- };
107
- }
108
-
109
- const PROJECT = path.join(path.sep, 'proj');
110
- const PKG_ROOT = path.join(PROJECT, 'node_modules', 'mandrel');
111
-
112
- /** Seed a package payload of two files under <pkg>/.agents/. */
113
- function seedPackagePayload() {
114
- const agentsDir = path.join(PKG_ROOT, '.agents');
115
- return {
116
- [path.join(agentsDir, 'instructions.md')]: '# instructions\n',
117
- [path.join(agentsDir, 'rules', 'security-baseline.md')]: '# security\n',
118
- };
119
- }
120
-
121
- const resolveToPkg = () => PKG_ROOT;
122
-
123
- // ---------------------------------------------------------------------------
124
- // Module shape
125
- // ---------------------------------------------------------------------------
126
-
127
- describe('sync module exports', () => {
128
- it('exports runSync as a named export', () => {
129
- assert.equal(typeof runSync, 'function');
130
- });
131
-
132
- it('exports a default function for bin/mandrel.js dispatch', () => {
133
- assert.equal(typeof sync, 'function');
134
- });
135
- });
136
-
137
- // ---------------------------------------------------------------------------
138
- // AC1 — copies the tree as plain files, no symlinks
139
- // ---------------------------------------------------------------------------
140
-
141
- describe('runSync — copies .agents/ payload as plain files', () => {
142
- it('copies every package file into ./.agents/', () => {
143
- const fs = makeFs(seedPackagePayload());
144
- const cap = makeCapture();
145
- const result = runSync({
146
- argv: [],
147
- resolvePackageRoot: resolveToPkg,
148
- fs,
149
- cwd: () => PROJECT,
150
- write: cap.write,
151
- writeErr: cap.writeErr,
152
- exit: cap.exit,
153
- });
154
-
155
- const destInstr = path.join(PROJECT, '.agents', 'instructions.md');
156
- const destRule = path.join(
157
- PROJECT,
158
- '.agents',
159
- 'rules',
160
- 'security-baseline.md',
161
- );
162
- assert.equal(fs.files.get(destInstr), '# instructions\n');
163
- assert.equal(fs.files.get(destRule), '# security\n');
164
- assert.equal(result.copied, 2);
165
- assert.equal(cap.exitCode, null);
166
- });
167
-
168
- it('never creates a symlink', () => {
169
- const fs = makeFs(seedPackagePayload());
170
- const cap = makeCapture();
171
- runSync({
172
- argv: [],
173
- resolvePackageRoot: resolveToPkg,
174
- fs,
175
- cwd: () => PROJECT,
176
- write: cap.write,
177
- writeErr: cap.writeErr,
178
- exit: cap.exit,
179
- });
180
- assert.equal(fs.symlinkCalls.length, 0);
181
- });
182
-
183
- it('reports the materialized file count on stdout', () => {
184
- const fs = makeFs(seedPackagePayload());
185
- const cap = makeCapture();
186
- runSync({
187
- argv: [],
188
- resolvePackageRoot: resolveToPkg,
189
- fs,
190
- cwd: () => PROJECT,
191
- write: cap.write,
192
- writeErr: cap.writeErr,
193
- exit: cap.exit,
194
- });
195
- assert.match(cap.out.join(''), /Materialized 2 file\(s\)/);
196
- });
197
- });
198
-
199
- // ---------------------------------------------------------------------------
200
- // AC2 — idempotency
201
- // ---------------------------------------------------------------------------
202
-
203
- describe('runSync — idempotent re-run', () => {
204
- it('leaves ./.agents/ byte-identical after a second run', () => {
205
- const fs = makeFs(seedPackagePayload());
206
- const opts = {
207
- argv: [],
208
- resolvePackageRoot: resolveToPkg,
209
- fs,
210
- cwd: () => PROJECT,
211
- write: () => {},
212
- writeErr: () => {},
213
- exit: () => {},
214
- };
215
-
216
- runSync(opts);
217
- // Snapshot the destination tree after the first run.
218
- const destPrefix = path.join(PROJECT, '.agents') + path.sep;
219
- const snapshot = JSON.stringify(
220
- [...fs.files.entries()].filter(([k]) => k.startsWith(destPrefix)).sort(),
221
- );
222
-
223
- runSync(opts);
224
- const after = JSON.stringify(
225
- [...fs.files.entries()].filter(([k]) => k.startsWith(destPrefix)).sort(),
226
- );
227
-
228
- assert.equal(after, snapshot);
229
- });
230
- });
231
-
232
- // ---------------------------------------------------------------------------
233
- // AC3 — missing package → non-zero exit with actionable message
234
- // ---------------------------------------------------------------------------
235
-
236
- describe('runSync — mandrel not resolvable', () => {
237
- function resolveThrows() {
238
- const err = new Error("Cannot find module 'mandrel/package.json'");
239
- err.code = 'MODULE_NOT_FOUND';
240
- throw err;
241
- }
242
-
243
- it('exits non-zero', () => {
244
- const fs = makeFs();
245
- const cap = makeCapture();
246
- runSync({
247
- argv: [],
248
- resolvePackageRoot: resolveThrows,
249
- fs,
250
- cwd: () => PROJECT,
251
- write: cap.write,
252
- writeErr: cap.writeErr,
253
- exit: cap.exit,
254
- });
255
- assert.equal(cap.exitCode, 1);
256
- });
257
-
258
- it('emits an actionable message naming the package and install command', () => {
259
- const fs = makeFs();
260
- const cap = makeCapture();
261
- runSync({
262
- argv: [],
263
- resolvePackageRoot: resolveThrows,
264
- fs,
265
- cwd: () => PROJECT,
266
- write: cap.write,
267
- writeErr: cap.writeErr,
268
- exit: cap.exit,
269
- });
270
- const joined = cap.err.join('');
271
- assert.match(joined, /mandrel/);
272
- assert.match(joined, /npm install mandrel/);
273
- });
274
-
275
- it('writes nothing to the destination', () => {
276
- const fs = makeFs();
277
- const cap = makeCapture();
278
- runSync({
279
- argv: [],
280
- resolvePackageRoot: resolveThrows,
281
- fs,
282
- cwd: () => PROJECT,
283
- write: cap.write,
284
- writeErr: cap.writeErr,
285
- exit: cap.exit,
286
- });
287
- assert.equal(fs.files.size, 0);
288
- });
289
- });
290
-
291
- // ---------------------------------------------------------------------------
292
- // AC3b — package resolvable but ships no .agents/ payload
293
- // ---------------------------------------------------------------------------
294
-
295
- describe('runSync — package present but no .agents/ payload', () => {
296
- it('exits non-zero with an actionable message', () => {
297
- // Seed a package whose only file is its package.json (no .agents/ tree).
298
- const fs = makeFs({
299
- [path.join(PKG_ROOT, 'package.json')]: '{}',
300
- });
301
- const cap = makeCapture();
302
- runSync({
303
- argv: [],
304
- resolvePackageRoot: resolveToPkg,
305
- fs,
306
- cwd: () => PROJECT,
307
- write: cap.write,
308
- writeErr: cap.writeErr,
309
- exit: cap.exit,
310
- });
311
- assert.equal(cap.exitCode, 1);
312
- assert.match(cap.err.join(''), /no .agents\/ payload/);
313
- });
314
- });
315
-
316
- // ---------------------------------------------------------------------------
317
- // AC4 — --dry-run reports planned copies, writes nothing
318
- // ---------------------------------------------------------------------------
319
-
320
- describe('runSync — --dry-run', () => {
321
- it('writes nothing to the destination', () => {
322
- const fs = makeFs(seedPackagePayload());
323
- const cap = makeCapture();
324
- const before = fs.files.size;
325
- runSync({
326
- argv: ['--dry-run'],
327
- resolvePackageRoot: resolveToPkg,
328
- fs,
329
- cwd: () => PROJECT,
330
- write: cap.write,
331
- writeErr: cap.writeErr,
332
- exit: cap.exit,
333
- });
334
- // No new files written: store size is unchanged from the seed.
335
- assert.equal(fs.files.size, before);
336
- });
337
-
338
- it('reports the planned copies and a dry-run summary', () => {
339
- const fs = makeFs(seedPackagePayload());
340
- const cap = makeCapture();
341
- const result = runSync({
342
- argv: ['--dry-run'],
343
- resolvePackageRoot: resolveToPkg,
344
- fs,
345
- cwd: () => PROJECT,
346
- write: cap.write,
347
- writeErr: cap.writeErr,
348
- exit: cap.exit,
349
- });
350
- const joined = cap.out.join('');
351
- assert.match(joined, /would copy/);
352
- assert.match(joined, /instructions\.md/);
353
- assert.match(joined, /Dry run: 2 file\(s\)/);
354
- assert.equal(result.planned, 2);
355
- assert.equal(result.copied, 0);
356
- });
357
-
358
- it('does not call exit on the dry-run happy path', () => {
359
- const fs = makeFs(seedPackagePayload());
360
- const cap = makeCapture();
361
- runSync({
362
- argv: ['--dry-run'],
363
- resolvePackageRoot: resolveToPkg,
364
- fs,
365
- cwd: () => PROJECT,
366
- write: cap.write,
367
- writeErr: cap.writeErr,
368
- exit: cap.exit,
369
- });
370
- assert.equal(cap.exitCode, null);
371
- });
372
- });