@dzhechkov/p-replicator 1.9.0 → 1.10.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/.dz-manifest.json +42 -18
- package/CHANGELOG.md +49 -0
- package/package.json +3 -3
- package/sbom.json +77 -17
- package/src/commands/doctor.js +43 -31
- package/src/commands/verify.js +27 -2
- package/src/utils.js +25 -0
- package/templates/.claude/commands/myinsights.md +22 -5
- package/templates/.claude/hooks/check-docs-complete.cjs +34 -6
- package/templates/.claude/hooks/check-ports.cjs +36 -4
- package/templates/.claude/hooks/statusline.cjs +15 -4
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-code.sh +40 -7
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-tests.sh +38 -11
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md +40 -4
- package/tests/snapshot/baseline.json +8 -8
- package/tests/unit/absence-is-not-emptiness.test.js +255 -0
- package/tests/unit/assess-scripts.test.js +150 -0
- package/tests/unit/check-docs-complete.test.js +43 -0
- package/tests/unit/check-ports.test.js +99 -0
- package/tests/unit/generated-guard-templates.test.js +134 -0
- package/tests/unit/guard-forms.test.js +302 -0
- package/tests/unit/insights-docs-tell-the-truth.test.js +84 -0
- package/tests/unit/module-copy-identity.test.js +31 -1
- package/tests/unit/shipped-suite-context.test.js +142 -0
- package/tests/unit/sync-templates-guard.test.js +31 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-27T14:30:37.254Z",
|
|
3
3
|
"totalFiles": 138,
|
|
4
4
|
"files": {
|
|
5
5
|
".claude/agents/doc-validator.md": "e57950758cd4764b2cb54b3f1905567e9dbc1ed20ac6ecd30998614894689f4f",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
".claude/commands/feature.md": "3b605c004ae6d794dee30892956dc0a9536195e828929bdaff134f3e952e87bb",
|
|
12
12
|
".claude/commands/go.md": "655dc9cad37b61a9f283cef15e435c492ee7cbc88a6ce59295ac32c975dee858",
|
|
13
13
|
".claude/commands/harvest.md": "c074fe7799f594dda44c5f8166da707f25cee51b12f758dea562a8c1e6ac5dcd",
|
|
14
|
-
".claude/commands/myinsights.md": "
|
|
14
|
+
".claude/commands/myinsights.md": "93258a3340f99a0f14b374746ae2934b01d7cefe06d575434868eb98da2d854a",
|
|
15
15
|
".claude/commands/next.md": "1e4984306c3aa80dfc8734dc1c975dfe5fdf2cf6396ddd759e4c7635646107b8",
|
|
16
16
|
".claude/commands/plan.md": "6fef889f51168981bf4a98b8392a0d595e3d199e1cfaa8e08c0a181ec6c440e4",
|
|
17
17
|
".claude/commands/replicate.md": "8862d0dc117815b1c3fc7f6e929996e2458783f1ac443eb70bc2a7ebe63a67c3",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
".claude/hooks/autocommit-insights.cjs": "5e7cef87b24d457f25315d79fe15f0dfaf94d9269495d638817f0faee2593c6f",
|
|
21
21
|
".claude/hooks/autocommit-plans.cjs": "9bb404b7f4fda5de03e6c2f16bf8939a79ec1944538767b04bcccc6cf50cc03d",
|
|
22
22
|
".claude/hooks/autocommit-roadmap.cjs": "f4dcc9ffd9a3aed91dd56e011e699fcf8af903cc81594e4ab1ef61e5432a36ad",
|
|
23
|
-
".claude/hooks/check-docs-complete.cjs": "
|
|
23
|
+
".claude/hooks/check-docs-complete.cjs": "27c05c2adcedc806e395b30b455243193a4fbd0968ce96e415679e1f78db951a",
|
|
24
24
|
".claude/hooks/check-growth-trace.cjs": "91994fc109c8440306c9dd3d5eea176c533eb0818fcdb09bac2b309397d9908a",
|
|
25
|
-
".claude/hooks/check-ports.cjs": "
|
|
25
|
+
".claude/hooks/check-ports.cjs": "1813aa60e4d15c5f6ce54a9b3471c3772efe60c332316183c7ad8f5b619f2169",
|
|
26
26
|
".claude/hooks/session-insights.cjs": "46f8357e3886a6d54db1ab4af969b9b8643ae5937ba59038a5651ffbcfd0d30e",
|
|
27
27
|
".claude/hooks/state-update.cjs": "47146a76b768ac2273abe8f2f017856a0e28b45afaf90048148da417dd19604e",
|
|
28
|
-
".claude/hooks/statusline.cjs": "
|
|
28
|
+
".claude/hooks/statusline.cjs": "465190ab21610357094fd6eba188ba9f7086dc5a933568f40a7f7c715ccd6745",
|
|
29
29
|
".claude/rules/docker-ports.md": "bef41c25c875479341f68783c2d64572800d4710f55401e55519245f03d22efb",
|
|
30
30
|
".claude/rules/feature-lifecycle.md": "f9ddc8c889ab7938a7207b36ae46c36be05e9b870956ef16617bf31b2f78c758",
|
|
31
31
|
".claude/rules/git-workflow.md": "7d8d2ded47fcc0fb95da3dc226efa6cb3c80d6adf7296977588c181dfce20192",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
".claude/skills/brutal-honesty-review/resources/assessment-rubrics.md": "dac09c91c765e2211799cc230738fb72a966f47511edb872306a906e49640cbe",
|
|
39
39
|
".claude/skills/brutal-honesty-review/resources/review-template.md": "95182c8605b1d821360257e1c5dbac4300bda333d861785de417cbb84090b25b",
|
|
40
40
|
".claude/skills/brutal-honesty-review/schemas/output.json": "2e252a3497b86322e1a6e6bd1aca8708b44389db3a2d81a6f893d467b6351210",
|
|
41
|
-
".claude/skills/brutal-honesty-review/scripts/assess-code.sh": "
|
|
42
|
-
".claude/skills/brutal-honesty-review/scripts/assess-tests.sh": "
|
|
41
|
+
".claude/skills/brutal-honesty-review/scripts/assess-code.sh": "2b10d2972b1379adde9a68ddf8bcdb399b7d65075e458a54523ffd8d99ddc746",
|
|
42
|
+
".claude/skills/brutal-honesty-review/scripts/assess-tests.sh": "d33e760a0739e9291f6087adf189f01eb7c4a1136d9128390dd6e3ad8eadda6f",
|
|
43
43
|
".claude/skills/brutal-honesty-review/scripts/validate-config.json": "7418284839f89735d01f50eefdb488b013e0076f6003a67683062429c14984c6",
|
|
44
44
|
".claude/skills/brutal-honesty-review/SKILL.md": "21e41129dac8e99485b501dcb3767d5f4461ce527782fa7bc71f89dabe5bd2bd",
|
|
45
45
|
".claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md": "85c72f6daa5af88b4a7ff520a387f47a7c92b81cb1f0757550c8b58100a97be6",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
".claude/skills/cc-toolkit-generator-enhanced/references/security-patterns-library.md": "5f1d650c426cbd2c9536b385306605ea62a5a520ee4566d23ca7254409c672a5",
|
|
60
60
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md": "23b8204fdc70b5afc5430cbe503af709a04b26716d2d2e07889283a755858fee",
|
|
61
61
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md": "c91f81da197a072b57378fd0018eea26c35d909a558bdca6f070eb60153c97b3",
|
|
62
|
-
".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md": "
|
|
62
|
+
".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md": "8398f2493434db101476300e514a34bf8124a51eb732cf81923f9a5b5a3d5db4",
|
|
63
63
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md": "2f1019d5da7d695983aee79186a47fcefccbfa24db48219c04f9bb9feb757d3a",
|
|
64
64
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md": "db6898b7202b84965cbff9195a6ce780cadd6aeb64adae3fbfd951443a6bc43d",
|
|
65
65
|
".claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md": "eb9025b687f0408b33280bf5fcc6c8be9b1670a544dd90b510557b89ae6cc123",
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A thing that is there but says nothing was reported as fine, in two places a caller reads for the
|
|
4
|
+
// same purpose: "is my toolkit intact?".
|
|
5
|
+
//
|
|
6
|
+
// REPRODUCED 2026-08-27 before the fix: 31 files truncated to zero bytes — every SKILL.md, command,
|
|
7
|
+
// rule and agent — and `verify` printed "[OK] All artifacts verified." with exit 0, `doctor` likewise.
|
|
8
|
+
// DELETING a file was caught (exit 1). Cause: fileExists() is fs.accessSync — it asks whether the
|
|
9
|
+
// path resolves, never what is in it.
|
|
10
|
+
//
|
|
11
|
+
// Why that is worse than it looks, and the evidence is from the same day: a twin test against the
|
|
12
|
+
// harness's own registration event measured that a SKILL.md without YAML frontmatter is NOT
|
|
13
|
+
// registered (35 -> 36 -> 37 as frontmatter was added). A zero-byte SKILL.md certainly has none. So
|
|
14
|
+
// "all artifacts verified" over 31 empty files asserts the integrity of a toolkit that cannot load.
|
|
15
|
+
|
|
16
|
+
const { test, describe } = require('node:test');
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
24
|
+
const CLI = path.join(PKG, 'bin', 'cli.js');
|
|
25
|
+
|
|
26
|
+
/** A fresh install in a throwaway directory. */
|
|
27
|
+
function project() {
|
|
28
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-empty-')));
|
|
29
|
+
execFileSync(process.execPath, [CLI, 'init'], { cwd: dir, stdio: 'ignore' });
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const run = (dir, cmd) => {
|
|
34
|
+
const r = spawnSync(process.execPath, [CLI, cmd], { cwd: dir, encoding: 'utf8' });
|
|
35
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const cleanup = (d) => fs.rmSync(d, { recursive: true, force: true });
|
|
39
|
+
|
|
40
|
+
describe('an artifact that is there but says nothing is not verified', () => {
|
|
41
|
+
test('P1 - truncated artifacts fail both health checks and are named', () => {
|
|
42
|
+
const dir = project();
|
|
43
|
+
try {
|
|
44
|
+
const victims = [
|
|
45
|
+
path.join(dir, '.claude', 'rules', 'docker-ports.md'),
|
|
46
|
+
path.join(dir, '.claude', 'commands', 'replicate.md'),
|
|
47
|
+
path.join(dir, '.claude', 'skills', 'explore', 'SKILL.md'),
|
|
48
|
+
];
|
|
49
|
+
for (const f of victims) fs.writeFileSync(f, '');
|
|
50
|
+
|
|
51
|
+
const v = run(dir, 'verify');
|
|
52
|
+
assert.notEqual(v.code, 0,
|
|
53
|
+
'a toolkit whose skills cannot load is not verified: ' + v.out);
|
|
54
|
+
const d = run(dir, 'doctor');
|
|
55
|
+
assert.notEqual(d.code, 0, 'doctor must agree with verify: ' + d.out);
|
|
56
|
+
|
|
57
|
+
// NAMED, not counted. A count sends a reader to inspect files by hand.
|
|
58
|
+
for (const f of victims) {
|
|
59
|
+
const base = path.basename(f) === 'SKILL.md' ? 'explore' : path.basename(f, '.md');
|
|
60
|
+
assert.ok(v.out.includes(base),
|
|
61
|
+
'verify must name the offending artifact ' + base + ': ' + v.out);
|
|
62
|
+
}
|
|
63
|
+
} finally { cleanup(dir); }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('P2 - whitespace-only counts as empty', () => {
|
|
67
|
+
// A file holding a newline is exactly as dead as one holding nothing, and a naive size check
|
|
68
|
+
// would pass it.
|
|
69
|
+
const dir = project();
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(path.join(dir, '.claude', 'rules', 'git-workflow.md'), '\n \n\t\n');
|
|
72
|
+
const v = run(dir, 'verify');
|
|
73
|
+
assert.notEqual(v.code, 0, 'whitespace is not content: ' + v.out);
|
|
74
|
+
} finally { cleanup(dir); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('P3 - missing and empty stay SEPARABLE words', () => {
|
|
78
|
+
// Their cures differ: missing -> run update; empty -> something truncated your file, and update
|
|
79
|
+
// would silently repair it without you ever learning that. Naming both "missing" moves the
|
|
80
|
+
// silence up one level instead of removing it.
|
|
81
|
+
const dirA = project();
|
|
82
|
+
const dirB = project();
|
|
83
|
+
try {
|
|
84
|
+
fs.rmSync(path.join(dirA, '.claude', 'rules', 'docker-ports.md'));
|
|
85
|
+
fs.writeFileSync(path.join(dirB, '.claude', 'rules', 'docker-ports.md'), '');
|
|
86
|
+
const a = run(dirA, 'verify');
|
|
87
|
+
const b = run(dirB, 'verify');
|
|
88
|
+
assert.notEqual(a.code, 0);
|
|
89
|
+
assert.notEqual(b.code, 0);
|
|
90
|
+
assert.match(a.out, /missing/i, 'a deleted artifact is missing: ' + a.out);
|
|
91
|
+
assert.match(b.out, /empty|пуст/i, 'a truncated artifact is EMPTY, not missing: ' + b.out);
|
|
92
|
+
assert.ok(!/\bempty\b/i.test(a.out.split('\n').filter((l) => l.includes('docker-ports')).join('\n')),
|
|
93
|
+
'the deleted one must not be called empty: ' + a.out);
|
|
94
|
+
} finally { cleanup(dirA); cleanup(dirB); }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('P4 - fileExists keeps its old meaning', () => {
|
|
98
|
+
// 31 call sites. Several ask a genuine presence question about files that may legitimately hold
|
|
99
|
+
// nothing. This asserts the old predicate was NOT repurposed — a future reader must not "fix" it.
|
|
100
|
+
const { fileExists } = require(path.join(PKG, 'src', 'utils.js'));
|
|
101
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-fe-')));
|
|
102
|
+
try {
|
|
103
|
+
const f = path.join(dir, 'empty.txt');
|
|
104
|
+
fs.writeFileSync(f, '');
|
|
105
|
+
assert.equal(fileExists(f), true,
|
|
106
|
+
'fileExists answers about PRESENCE and must keep doing so');
|
|
107
|
+
assert.equal(fileExists(path.join(dir, 'nope.txt')), false);
|
|
108
|
+
} finally { cleanup(dir); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('P5 - artifactState reports three states', () => {
|
|
112
|
+
const { artifactState } = require(path.join(PKG, 'src', 'utils.js'));
|
|
113
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-as-')));
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(path.join(dir, 'empty.txt'), '');
|
|
116
|
+
fs.writeFileSync(path.join(dir, 'ws.txt'), ' \n\t ');
|
|
117
|
+
fs.writeFileSync(path.join(dir, 'real.txt'), 'content');
|
|
118
|
+
assert.equal(artifactState(path.join(dir, 'nope.txt')), 'missing');
|
|
119
|
+
assert.equal(artifactState(path.join(dir, 'empty.txt')), 'empty');
|
|
120
|
+
assert.equal(artifactState(path.join(dir, 'ws.txt')), 'empty');
|
|
121
|
+
assert.equal(artifactState(path.join(dir, 'real.txt')), 'present');
|
|
122
|
+
} finally { cleanup(dir); }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('P6 - none of the three insights states changes the exit code', () => {
|
|
126
|
+
// A project that has recorded no insight is a NEW project. A check that refuses a new project is
|
|
127
|
+
// a check people disable, and then the real signal goes with it. The defect was never the
|
|
128
|
+
// emptiness — it was that emptiness and ABSENCE were indistinguishable, so a carrier that never
|
|
129
|
+
// existed looked exactly like one being used and found empty. That is how 27 recorded insights
|
|
130
|
+
// became 0 without anyone noticing.
|
|
131
|
+
const dir = project();
|
|
132
|
+
try {
|
|
133
|
+
const idx = path.join(dir, '.claude', 'insights', 'index.md');
|
|
134
|
+
|
|
135
|
+
const absent = run(dir, 'doctor');
|
|
136
|
+
assert.equal(absent.code, 0, 'a fresh project must not fail: ' + absent.out);
|
|
137
|
+
|
|
138
|
+
fs.mkdirSync(path.dirname(idx), { recursive: true });
|
|
139
|
+
fs.writeFileSync(idx, '# Insights\n');
|
|
140
|
+
const zero = run(dir, 'doctor');
|
|
141
|
+
assert.equal(zero.code, 0, 'zero entries must not fail either: ' + zero.out);
|
|
142
|
+
|
|
143
|
+
fs.writeFileSync(idx, '# Insights\n\n## 2026-08-27 — a rake\n\nBody.\n');
|
|
144
|
+
const some = run(dir, 'doctor');
|
|
145
|
+
assert.equal(some.code, 0);
|
|
146
|
+
|
|
147
|
+
// Three states, three DIFFERENT strings. Two that read alike are the defect.
|
|
148
|
+
// Specific: the component list also contains the word 'insights' (/myinsights, session-insights.cjs).
|
|
149
|
+
const lineOf = (out) => (out.split('\n').find((l) => /insights carrier/i.test(l)) || '').trim();
|
|
150
|
+
const [a, z, s] = [lineOf(absent.out), lineOf(zero.out), lineOf(some.out)];
|
|
151
|
+
assert.ok(a && z && s, 'doctor must say something about insights in all three states: '
|
|
152
|
+
+ JSON.stringify([a, z, s]));
|
|
153
|
+
assert.notEqual(a, z, 'ABSENT and ZERO-ENTRY must not read alike: ' + JSON.stringify([a, z]));
|
|
154
|
+
assert.notEqual(z, s, 'ZERO-ENTRY and POPULATED must not read alike: ' + JSON.stringify([z, s]));
|
|
155
|
+
} finally { cleanup(dir); }
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('P8 - doctor says EMPTY for every component kind, settings included', () => {
|
|
159
|
+
// Cross-family review found my first pass swapped the CONDITION to artifactState and left the
|
|
160
|
+
// two-way branch, so a whitespace-only artifact was reported as "missing" — the exact collapse
|
|
161
|
+
// AR-4 forbids. Worse: settings.json and the hooks were still on fileExists, so a ZERO-BYTE
|
|
162
|
+
// settings.json — no hooks wired at all — received a checkmark.
|
|
163
|
+
//
|
|
164
|
+
// Matched on the EXACT label the line carries, not a substring: `replicate` also occurs inside
|
|
165
|
+
// `replicate-coordinator`, and a loose matcher read a passing line as the failing one. Third
|
|
166
|
+
// time this class has bitten today.
|
|
167
|
+
const dir = project();
|
|
168
|
+
try {
|
|
169
|
+
const victims = {
|
|
170
|
+
'/replicate': path.join(dir, '.claude', 'commands', 'replicate.md'),
|
|
171
|
+
'doc-validator': path.join(dir, '.claude', 'agents', 'doc-validator.md'),
|
|
172
|
+
'docker-ports': path.join(dir, '.claude', 'rules', 'docker-ports.md'),
|
|
173
|
+
'explore': path.join(dir, '.claude', 'skills', 'explore', 'SKILL.md'),
|
|
174
|
+
'settings.json': path.join(dir, '.claude', 'settings.json'),
|
|
175
|
+
};
|
|
176
|
+
for (const f of Object.values(victims)) fs.writeFileSync(f, ' \n\t ');
|
|
177
|
+
|
|
178
|
+
const d = run(dir, 'doctor');
|
|
179
|
+
assert.notEqual(d.code, 0, d.out);
|
|
180
|
+
for (const label of Object.keys(victims)) {
|
|
181
|
+
const line = d.out.split('\n')
|
|
182
|
+
.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').trim())
|
|
183
|
+
.find((l) => l.startsWith('\u2717 ' + label + ' ') || l === '\u2717 ' + label);
|
|
184
|
+
assert.ok(line, label + ' must appear as a FAILING line: ' + d.out);
|
|
185
|
+
assert.match(line, /EMPTY, cannot load/,
|
|
186
|
+
label + ' must be EMPTY, not missing and never a checkmark: ' + JSON.stringify(line));
|
|
187
|
+
}
|
|
188
|
+
} finally { cleanup(dir); }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('P10 - a zero-byte HOOK is reported too, when settings.json still parses', () => {
|
|
192
|
+
// The hook checks live inside the branch that runs only when settings.json is readable, so P8
|
|
193
|
+
// cannot reach them: killing settings.json hides its own children. Asserted separately, which
|
|
194
|
+
// is the honest shape — and it proves the hook path is on artifactState, not fileExists.
|
|
195
|
+
const dir = project();
|
|
196
|
+
try {
|
|
197
|
+
fs.writeFileSync(path.join(dir, '.claude', 'hooks', 'check-ports.cjs'), '');
|
|
198
|
+
const d = run(dir, 'doctor');
|
|
199
|
+
assert.notEqual(d.code, 0, d.out);
|
|
200
|
+
const line = d.out.split('\n')
|
|
201
|
+
.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').trim())
|
|
202
|
+
.find((l) => l.startsWith('\u2717 check-ports.cjs'));
|
|
203
|
+
assert.ok(line, 'a zero-byte hook must not receive a checkmark: ' + d.out);
|
|
204
|
+
assert.match(line, /EMPTY, cannot load/, line);
|
|
205
|
+
} finally { cleanup(dir); }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('P9 - verify reports the carrier in all three states, failing none', () => {
|
|
209
|
+
// FR-5 asks for three SURFACES. The first pass shipped two and left verify producing identical
|
|
210
|
+
// output for absent, empty and populated — the same indistinguishability, one surface over.
|
|
211
|
+
const dir = project();
|
|
212
|
+
try {
|
|
213
|
+
const idx = path.join(dir, '.claude', 'insights', 'index.md');
|
|
214
|
+
const seen = [];
|
|
215
|
+
const carrierLine = (out) => (out.split('\n').find((l) => /insights carrier/i.test(l)) || '').trim();
|
|
216
|
+
|
|
217
|
+
let r = run(dir, 'verify'); seen.push(carrierLine(r.out));
|
|
218
|
+
assert.equal(r.code, 0, 'a fresh project must not fail verify: ' + r.out);
|
|
219
|
+
|
|
220
|
+
fs.mkdirSync(path.dirname(idx), { recursive: true });
|
|
221
|
+
fs.writeFileSync(idx, '# Insights\n');
|
|
222
|
+
r = run(dir, 'verify'); seen.push(carrierLine(r.out));
|
|
223
|
+
assert.equal(r.code, 0, 'zero entries must not fail verify: ' + r.out);
|
|
224
|
+
|
|
225
|
+
fs.writeFileSync(idx, '# Insights\n\n## 2026-08-27 — a rake\n\nBody.\n');
|
|
226
|
+
r = run(dir, 'verify'); seen.push(carrierLine(r.out));
|
|
227
|
+
assert.equal(r.code, 0);
|
|
228
|
+
|
|
229
|
+
assert.ok(seen.every(Boolean), 'verify must say something in all three states: ' + JSON.stringify(seen));
|
|
230
|
+
assert.equal(new Set(seen).size, 3, 'three states, three DIFFERENT lines: ' + JSON.stringify(seen));
|
|
231
|
+
} finally { cleanup(dir); }
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('P7 - the statusline distinguishes no-carrier from zero-entries', () => {
|
|
235
|
+
const dir = project();
|
|
236
|
+
try {
|
|
237
|
+
const hook = path.join(dir, '.claude', 'hooks', 'statusline.cjs');
|
|
238
|
+
const strip = new RegExp('\\x1b\\[[0-9;]*m', 'g');
|
|
239
|
+
const render = () => spawnSync(process.execPath, [hook],
|
|
240
|
+
{ cwd: dir, env: { ...process.env, CLAUDE_PROJECT_DIR: dir }, encoding: 'utf8' })
|
|
241
|
+
.stdout.replace(strip, '');
|
|
242
|
+
|
|
243
|
+
const absent = render();
|
|
244
|
+
const idx = path.join(dir, '.claude', 'insights', 'index.md');
|
|
245
|
+
fs.mkdirSync(path.dirname(idx), { recursive: true });
|
|
246
|
+
fs.writeFileSync(idx, '# Insights\n');
|
|
247
|
+
const zero = render();
|
|
248
|
+
|
|
249
|
+
const seg = (out) => (out.split('\n').find((l) => /Insight/i.test(l)) || '');
|
|
250
|
+
assert.notEqual(seg(absent), seg(zero),
|
|
251
|
+
'a carrier that never existed rendered identically to one found empty — that is the defect: '
|
|
252
|
+
+ JSON.stringify([seg(absent), seg(zero)]));
|
|
253
|
+
} finally { cleanup(dir); }
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// brutal-honesty-review is Phase 4 of /feature — where a course participant meets it. Its two
|
|
4
|
+
// assessment scripts print verdicts and CANNOT FAIL. MEASURED 2026-08-27:
|
|
5
|
+
//
|
|
6
|
+
// assess-code.sh on TODO/FIXME/BUG/HACK + nested for(;;) + empty catch + eval → 2 red, exit 0
|
|
7
|
+
// assess-tests.sh on two tests, one timing-based, no edge cases → 6 red, exit 0
|
|
8
|
+
// assess-tests.sh /nope/nope → exit 1
|
|
9
|
+
//
|
|
10
|
+
// The semantics is INVERTED: "I could not check" is louder than "I found six violations". A
|
|
11
|
+
// participant sees red on screen and any automation reads success.
|
|
12
|
+
//
|
|
13
|
+
// Both scripts DETECT correctly — seven and nine verdict sites, all firing on the right inputs.
|
|
14
|
+
// Nothing about the detection is wrong; nobody counts. These tests RUN the real scripts.
|
|
15
|
+
|
|
16
|
+
const { test, describe } = require('node:test');
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const { spawnSync } = require('node:child_process');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
24
|
+
const SCRIPTS = path.join(PKG, 'templates', '.claude', 'skills', 'brutal-honesty-review', 'scripts');
|
|
25
|
+
const CODE = path.join(SCRIPTS, 'assess-code.sh');
|
|
26
|
+
const TESTS = path.join(SCRIPTS, 'assess-tests.sh');
|
|
27
|
+
|
|
28
|
+
/** Build a throwaway target and run one script over it. `arg: null` passes no argument at all. */
|
|
29
|
+
function run(script, files, arg) {
|
|
30
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-assess-')));
|
|
31
|
+
try {
|
|
32
|
+
for (const [name, body] of Object.entries(files || {})) {
|
|
33
|
+
const abs = path.join(dir, name);
|
|
34
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
35
|
+
fs.writeFileSync(abs, body);
|
|
36
|
+
}
|
|
37
|
+
const argv = arg === null ? [script] : [script, path.join(dir, arg === undefined ? 'target' : arg)];
|
|
38
|
+
const r = spawnSync('bash', argv, { cwd: dir, encoding: 'utf8' });
|
|
39
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
40
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const AWFUL = 'function f(){ /* TODO FIXME BUG HACK */ for(;;){for(;;){}} try{}catch(e){} eval(x); }\n';
|
|
44
|
+
// "Clean" BY THIS SCRIPT'S OWN STANDARDS, which are stricter than they first look: it wants
|
|
45
|
+
// try/catch present and a tests directory to exist. My first fixture had neither and produced two
|
|
46
|
+
// legitimate findings — the fixture was wrong, not the detection, which AR-1 forbids touching.
|
|
47
|
+
const CLEAN = 'export function add(a, b) {\n'
|
|
48
|
+
+ ' try {\n if (typeof a !== "number") throw new Error("bad");\n return a + b;\n'
|
|
49
|
+
+ ' } catch (e) {\n console.error(e);\n throw e;\n }\n}\n';
|
|
50
|
+
// `tests/` sits at the fixture ROOT, not under target/: the script's testability check reads
|
|
51
|
+
// `[ -d "tests" ]` against the CURRENT WORKING DIRECTORY rather than against $TARGET. That is a
|
|
52
|
+
// pre-existing quirk — arguably its own defect — and AR-1 forbids changing detection in this
|
|
53
|
+
// change, so the fixture matches the script rather than the other way round. Filed separately.
|
|
54
|
+
const CLEAN_TREE = { 'target/add.js': CLEAN, 'tests/add.test.js': "test('adds', () => {});\n" };
|
|
55
|
+
const BAD_TESTS = "test('t1', () => {});\ntest('t2', () => { setTimeout(() => {}, 1) });\n";
|
|
56
|
+
|
|
57
|
+
describe('an assessment script that prints verdicts can refuse', () => {
|
|
58
|
+
test('P1 - awful code exits 1, and the count is printed', () => {
|
|
59
|
+
const r = run(CODE, { 'target/awful.js': AWFUL });
|
|
60
|
+
assert.equal(r.code, 1, 'red verdicts on screen must reach the exit code: ' + r.out);
|
|
61
|
+
assert.match(r.out, /🔴/, 'and the verdicts themselves must still appear: ' + r.out);
|
|
62
|
+
assert.match(r.out, /\b[1-9]\d*\b.*(?:finding|issue|проблем|нарушен)/i,
|
|
63
|
+
'the count must be stated, so a human sees the same answer as the exit code: ' + r.out);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('P2 - bad tests exit 1', () => {
|
|
67
|
+
const r = run(TESTS, { 'target/a.test.js': BAD_TESTS });
|
|
68
|
+
assert.equal(r.code, 1, 'six red verdicts must not read as success: ' + r.out);
|
|
69
|
+
assert.match(r.out, /🔴/, r.out);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('P3 - could-not-check is 2, and no argument is 2', () => {
|
|
73
|
+
// THE inversion. Today a nonexistent path exits 1 while six violations exit 0, so a gate
|
|
74
|
+
// reading 1 cannot tell blindness from findings. A caller who passed nothing and one pointing
|
|
75
|
+
// at a missing directory made the same class of mistake: the script could not look.
|
|
76
|
+
for (const script of [CODE, TESTS]) {
|
|
77
|
+
const missing = run(script, {}, 'nope');
|
|
78
|
+
assert.equal(missing.code, 2,
|
|
79
|
+
path.basename(script) + ': a target that does not exist is could-not-check: ' + missing.out);
|
|
80
|
+
const noArg = run(script, {}, null);
|
|
81
|
+
assert.equal(noArg.code, 2,
|
|
82
|
+
path.basename(script) + ': no argument is could-not-check too: ' + noArg.out);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('P4 - clean input exits 0', () => {
|
|
87
|
+
// A script that only fails is as useless as one that only passes, and without this the suite
|
|
88
|
+
// could not tell them apart.
|
|
89
|
+
const r = run(CODE, CLEAN_TREE);
|
|
90
|
+
assert.equal(r.code, 0, 'clean code must pass: ' + r.out);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('P5 - detection is unchanged', () => {
|
|
94
|
+
// The change adds counting. If a verdict that used to fire stops firing, the exit code is right
|
|
95
|
+
// for the wrong reason — and this suite would still be green.
|
|
96
|
+
const r = run(CODE, { 'target/awful.js': AWFUL });
|
|
97
|
+
// Only markers that DID fire before the change. `Found N loops` needs more than five lines
|
|
98
|
+
// matching `for.*{` and never fired on this input — asserting it would have been a test for
|
|
99
|
+
// behaviour the script never had.
|
|
100
|
+
for (const marker of [/TODO\/FIXME\/BUG\/HACK/, /No test directory found/]) {
|
|
101
|
+
assert.match(r.out, marker, 'a verdict that fired before must still fire: ' + r.out);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('P6 - the closing voice survives', () => {
|
|
106
|
+
// The Ramsay-voice lines are the skill's character and a participant reads them. The verdict is
|
|
107
|
+
// ADDED, not substituted.
|
|
108
|
+
const r = run(CODE, CLEAN_TREE);
|
|
109
|
+
assert.match(r.out, /wouldn't deploy this to production/i,
|
|
110
|
+
'the closing prose must not be replaced by a machine verdict: ' + r.out);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('P7 - all three verdicts from ONE script in one run', () => {
|
|
114
|
+
// assess-code.sh reaches all three from fixtures.
|
|
115
|
+
const seen = [
|
|
116
|
+
run(CODE, CLEAN_TREE).code,
|
|
117
|
+
run(CODE, { 'target/awful.js': AWFUL }).code,
|
|
118
|
+
run(CODE, {}, 'nope').code,
|
|
119
|
+
];
|
|
120
|
+
assert.deepEqual(seen, [0, 1, 2], 'assess-code.sh: ' + JSON.stringify(seen));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('P8 - assess-tests.sh reaches 1 and 2; its 0 path is proven by the shared footer', () => {
|
|
124
|
+
// HONEST LIMIT, stated rather than faked. assess-tests.sh RUNS the suite it is pointed at
|
|
125
|
+
// ("Tests don't even pass", "Tests failed 3/3 times"), so reaching exit 0 needs a real,
|
|
126
|
+
// installed, passing project — out of reach of a unit fixture. Constructing a fake green here
|
|
127
|
+
// would be the class of lie this whole file exists to remove.
|
|
128
|
+
//
|
|
129
|
+
// What IS proven: both non-zero verdicts from fixtures, and that the exit-0 branch is the SAME
|
|
130
|
+
// mechanism proven reachable in P7 — the two scripts carry a byte-identical verdict footer, so
|
|
131
|
+
// demonstrating it in one demonstrates the mechanism in both.
|
|
132
|
+
const seen = [
|
|
133
|
+
run(TESTS, { 'target/a.test.js': BAD_TESTS }).code,
|
|
134
|
+
run(TESTS, {}, 'nope').code,
|
|
135
|
+
];
|
|
136
|
+
assert.deepEqual(seen, [1, 2], 'assess-tests.sh: ' + JSON.stringify(seen));
|
|
137
|
+
|
|
138
|
+
const footer = (f) => {
|
|
139
|
+
const src = fs.readFileSync(f, 'utf-8');
|
|
140
|
+
const at = src.indexOf('# ── Verdict ─');
|
|
141
|
+
assert.ok(at > 0, f + ' must carry the verdict footer');
|
|
142
|
+
return src.slice(at);
|
|
143
|
+
};
|
|
144
|
+
assert.equal(footer(TESTS), footer(CODE),
|
|
145
|
+
'the verdict footer must be identical in both, or P7 proves nothing about this script');
|
|
146
|
+
assert.match(footer(TESTS), /VERDICT: 0 findings\./,
|
|
147
|
+
'and the exit-0 branch must exist in it');
|
|
148
|
+
assert.match(footer(TESTS), /\nexit 0\n?$/, 'ending in the successful exit');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -130,6 +130,49 @@ describe('the cheap question is asked before the swarm is spent', () => {
|
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
test('P16 - a markdown task-list checkbox is NOT a placeholder', () => {
|
|
134
|
+
// MEASURED against a real completed /replicate project: 17 lines like `- [ ] AC покрыты
|
|
135
|
+
// автотестами` were reported as "possibly unfilled". Worse, that project's own Completion.md
|
|
136
|
+
// TEACHES the notation — "флажки [ ] при каждом FR-GROWTH-00N" — so this warning fired on the
|
|
137
|
+
// convention the pipeline itself prescribes. Noise on a legitimate convention trains people to
|
|
138
|
+
// ignore warnings, which is the failure this checker exists to prevent, one level down.
|
|
139
|
+
const withBoxes = '# Specification\n\n'
|
|
140
|
+
+ '- [ ] AC покрыты автотестами\n'
|
|
141
|
+
+ '- [x] События пишутся в аналитику\n'
|
|
142
|
+
+ '* [ ] Anti-fraud сценарий задокументирован\n'
|
|
143
|
+
+ '| `invite_shown` | [ ] | Share-CTA показан в момент первого рендера |\n\n' + REAL;
|
|
144
|
+
const r = check(complete({ 'Specification.md': withBoxes }));
|
|
145
|
+
assert.equal(r.code, 0, 'checkboxes must not block: ' + r.out);
|
|
146
|
+
assert.ok(!/Specification\.md: возможно незаполнено/.test(r.out),
|
|
147
|
+
'and must not even WARN — a warning on the pipeline own convention is noise: ' + r.out);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('P17 - a real bracketed placeholder on a checkbox LINE is still seen', () => {
|
|
151
|
+
// The narrowing must not become an escape hatch: skipping the whole line would let a genuine
|
|
152
|
+
// placeholder hide behind a checkbox prefix.
|
|
153
|
+
const r = check(complete({
|
|
154
|
+
'Specification.md': '# Spec\n\n- [ ] [описание критерия приёмки]\n\n' + REAL,
|
|
155
|
+
}));
|
|
156
|
+
assert.match(r.out, /возможно незаполнено/,
|
|
157
|
+
'a bracketed placeholder AFTER a checkbox must still be reported: ' + r.out);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('P18 - Final_Summary.md is reported when absent, and does not block', () => {
|
|
161
|
+
// MEASURED: the pipeline promises it in three places — replicate.md and two sites in
|
|
162
|
+
// sparc-prd-mini, including a whole SYNTHESIS phase — and a real completed project produced
|
|
163
|
+
// 8 of 9 promised documents WITHOUT it. One project is not enough evidence to decide whether
|
|
164
|
+
// the pipeline is broken or the document is optional in practice, and BLOCKING on it would
|
|
165
|
+
// have refused every project that ran like that one.
|
|
166
|
+
// The shared fixture DOES write it, so this case removes it deliberately — the state a real
|
|
167
|
+
// completed project was measured in.
|
|
168
|
+
const files = complete();
|
|
169
|
+
delete files['Final_Summary.md'];
|
|
170
|
+
const r = check(files);
|
|
171
|
+
assert.equal(r.code, 0, 'its absence must not block: ' + r.out);
|
|
172
|
+
assert.match(r.out, /Final_Summary\.md: отсутствует, хотя конвейер его обещает/,
|
|
173
|
+
'but the discrepancy must be NAMED, not silently tolerated: ' + r.out);
|
|
174
|
+
});
|
|
175
|
+
|
|
133
176
|
test('P7 - no docs directory exits 2, and an empty one does too', () => {
|
|
134
177
|
const none = check({}, { noDocs: true });
|
|
135
178
|
assert.equal(none.code, 2, 'nothing to check is not "complete": ' + none.out);
|
|
@@ -176,6 +176,105 @@ describe('the storage-port rule finally has a check that can fail', () => {
|
|
|
176
176
|
'a non-storage service with no proxy present is not this rule\'s business: ' + r.out);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
test('P13 — every argument shape resolves in one frame', { skip: !DOCKER }, () => {
|
|
180
|
+
// The defect: `-f` stayed RELATIVE while the same spawnSync call overrode docker's cwd to the
|
|
181
|
+
// directory that relative path already named, so docker re-applied the directory component.
|
|
182
|
+
// MEASURED before the fix: `check-ports.cjs projects/01` from the parent reported
|
|
183
|
+
// "open /tmp/X/projects/01/projects/01/docker-compose.yml".
|
|
184
|
+
//
|
|
185
|
+
// PR-019 diagnosed a project-root join. There is no such join in the file — proven in pure shell:
|
|
186
|
+
// the SAME `-f projects/01/docker-compose.yml` succeeds from /tmp/X and doubles from
|
|
187
|
+
// /tmp/X/projects/01. The cwd override IS the mechanism.
|
|
188
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-frame-')));
|
|
189
|
+
try {
|
|
190
|
+
const proj = path.join(root, 'projects', '01');
|
|
191
|
+
fs.mkdirSync(proj, { recursive: true });
|
|
192
|
+
fs.writeFileSync(path.join(proj, 'docker-compose.yml'),
|
|
193
|
+
'services:\n db:\n image: postgres:16\n');
|
|
194
|
+
// Every shape in ONE run: a matrix asserted from one side only cannot be told from a constant.
|
|
195
|
+
const shapes = [
|
|
196
|
+
['projects/01', root, 'a bare relative directory — the shape that was broken'],
|
|
197
|
+
['./projects/01', root, 'the ./-prefixed form, which path.join normalises identically'],
|
|
198
|
+
['projects/01/docker-compose.yml', root, 'a relative path to the file itself'],
|
|
199
|
+
['.', proj, 'the documented invocation — worked before only because it is idempotent'],
|
|
200
|
+
[null, proj, 'no argument at all'],
|
|
201
|
+
['../01', proj, 'a sibling-relative path'],
|
|
202
|
+
[proj, root, 'absolute, from an unrelated cwd'],
|
|
203
|
+
[proj, proj, 'absolute, from inside'],
|
|
204
|
+
];
|
|
205
|
+
for (const [arg, cwd, why] of shapes) {
|
|
206
|
+
const argv = arg === null ? [CHECK] : [CHECK, arg];
|
|
207
|
+
const r = spawnSync(process.execPath, argv, { encoding: 'utf8', cwd });
|
|
208
|
+
assert.equal(r.status, 0,
|
|
209
|
+
'shape must resolve (' + why + '): arg=' + JSON.stringify(arg) + ' cwd=' + cwd
|
|
210
|
+
+ ' -> exit ' + r.status + ': ' + (r.stdout || '') + (r.stderr || ''));
|
|
211
|
+
}
|
|
212
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('P14 — run from a PARENT cwd with a relative argument', { skip: !DOCKER }, () => {
|
|
216
|
+
// The blind spot that let this ship green. Every other case here builds its fixture with
|
|
217
|
+
// realpathSync(mkdtempSync(...)) and passes it whole — ALWAYS ABSOLUTE — so all 12 tests passed
|
|
218
|
+
// against the live defect. A regression test that cannot reproduce the bug it guards is not a
|
|
219
|
+
// guard. This case is the one that could.
|
|
220
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-parent-')));
|
|
221
|
+
try {
|
|
222
|
+
const proj = path.join(root, 'projects', '01');
|
|
223
|
+
fs.mkdirSync(proj, { recursive: true });
|
|
224
|
+
fs.writeFileSync(path.join(proj, 'docker-compose.yml'),
|
|
225
|
+
'services:\n db:\n image: postgres:16\n ports: ["5432:5432"]\n');
|
|
226
|
+
const r = spawnSync(process.execPath, [CHECK, 'projects/01'], { encoding: 'utf8', cwd: root });
|
|
227
|
+
// A real violation must be REPORTED, not lost behind a path error.
|
|
228
|
+
assert.equal(r.status, 1, 'expected the published-storage violation: ' + r.stdout + r.stderr);
|
|
229
|
+
assert.match(r.stdout, /Правило №0/, r.stdout);
|
|
230
|
+
assert.ok(!/projects\/01\/projects\/01/.test(r.stdout + r.stderr),
|
|
231
|
+
'the doubled path must be gone: ' + r.stdout + r.stderr);
|
|
232
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('P15 — a required variable is reported as itself, not masked', { skip: !DOCKER }, () => {
|
|
236
|
+
// The defect fired BEFORE docker parsed the file, so a genuine config error came back as
|
|
237
|
+
// "no such file". Second harm, and the one a user would waste the most time on.
|
|
238
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-mask-')));
|
|
239
|
+
try {
|
|
240
|
+
const proj = path.join(root, 'projects', '04');
|
|
241
|
+
fs.mkdirSync(proj, { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(proj, 'docker-compose.yml'),
|
|
243
|
+
'services:\n db:\n image: postgres:${PGTAG:?PGTAG must be set}\n');
|
|
244
|
+
const r = spawnSync(process.execPath, [CHECK, 'projects/04'], { encoding: 'utf8', cwd: root });
|
|
245
|
+
assert.equal(r.status, 2, 'an unreadable config is still exit 2: ' + r.stdout);
|
|
246
|
+
assert.match(r.stdout, /PGTAG/, 'the REAL cause must reach the user: ' + r.stdout);
|
|
247
|
+
assert.ok(!/no such file/i.test(r.stdout),
|
|
248
|
+
'the path error must not stand in for the config error: ' + r.stdout);
|
|
249
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('P16 — the spawn overrides no cwd, and the hint neither guesses nor refutes itself', () => {
|
|
253
|
+
// Structural, so the removed line cannot creep back. MEASURED on Compose v5.1.1: project name,
|
|
254
|
+
// relative build contexts, .env discovery and env_file are ALL derived from the first -f file's
|
|
255
|
+
// directory — a .env in the process cwd was not even used as a fallback. The override's only
|
|
256
|
+
// demonstrated effect in this file's history is the defect.
|
|
257
|
+
const src = fs.readFileSync(CHECK, 'utf-8');
|
|
258
|
+
const at = src.indexOf('spawnSync(');
|
|
259
|
+
const spawnCall = src.slice(at, at + 300);
|
|
260
|
+
assert.ok(!/cwd:/.test(spawnCall),
|
|
261
|
+
'the cwd override must be GONE, not neutralised — a provably-useless line still encodes the '
|
|
262
|
+
+ 'false premise that regrows the class: ' + spawnCall);
|
|
263
|
+
// The guessed cause named an UNREACHABLE case: a plain unset ${VAR} makes compose exit 0.
|
|
264
|
+
//
|
|
265
|
+
// Asserted on CODE, not on the file: the fix's own comment QUOTES the removed phrase to explain
|
|
266
|
+
// why it went, and a whole-file `includes` cannot tell a mention from a use. That is the same
|
|
267
|
+
// trap this repo has hit repeatedly, and it caught this test on its first run.
|
|
268
|
+
const code = src.split('\n')
|
|
269
|
+
.filter((l) => !l.trim().startsWith('//') && !l.trim().startsWith('*') && !l.trim().startsWith('/*'))
|
|
270
|
+
.join('\n');
|
|
271
|
+
assert.ok(!code.includes('обычно это незаданная переменная'),
|
|
272
|
+
'the hint must not name a cause that cannot produce this exit');
|
|
273
|
+
assert.match(code, /повторить ровно то, что делали мы/,
|
|
274
|
+
'and the cure must be presented as a reproduction of OUR invocation');
|
|
275
|
+
assert.match(src, /path\.resolve\(process\.cwd\(\)/,
|
|
276
|
+
'the argument must be absolutised once, at the boundary');
|
|
277
|
+
});
|
|
179
278
|
test('P9 — a clean compose exits 0', { skip: !DOCKER }, () => {
|
|
180
279
|
const r = check('services:\n db:\n image: postgres:16\n'
|
|
181
280
|
+ ' web:\n image: node:22\n ports: ["3000:3000"]\n');
|