@mmnto/cli 2.0.0 → 2.2.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/dist/commands/describe.d.ts +13 -1
- package/dist/commands/describe.d.ts.map +1 -1
- package/dist/commands/describe.js +29 -1
- package/dist/commands/describe.js.map +1 -1
- package/dist/commands/describe.test.js +86 -3
- package/dist/commands/describe.test.js.map +1 -1
- package/dist/commands/doctor-parity-fetch.d.ts +93 -2
- package/dist/commands/doctor-parity-fetch.d.ts.map +1 -1
- package/dist/commands/doctor-parity-fetch.js +332 -11
- package/dist/commands/doctor-parity-fetch.js.map +1 -1
- package/dist/commands/doctor-parity-fetch.test.d.ts +6 -0
- package/dist/commands/doctor-parity-fetch.test.d.ts.map +1 -1
- package/dist/commands/doctor-parity-fetch.test.js +405 -1
- package/dist/commands/doctor-parity-fetch.test.js.map +1 -1
- package/dist/commands/doctor-parity.d.ts.map +1 -1
- package/dist/commands/doctor-parity.js +27 -1
- package/dist/commands/doctor-parity.js.map +1 -1
- package/dist/commands/doctor-parity.test.js +67 -0
- package/dist/commands/doctor-parity.test.js.map +1 -1
- package/dist/commands/eject-gate-needle.test.d.ts +2 -0
- package/dist/commands/eject-gate-needle.test.d.ts.map +1 -0
- package/dist/commands/eject-gate-needle.test.js +48 -0
- package/dist/commands/eject-gate-needle.test.js.map +1 -0
- package/dist/commands/eject.d.ts +10 -0
- package/dist/commands/eject.d.ts.map +1 -1
- package/dist/commands/eject.js +67 -12
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +113 -0
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/gate-install.d.ts +37 -5
- package/dist/commands/gate-install.d.ts.map +1 -1
- package/dist/commands/gate-install.js +26 -38
- package/dist/commands/gate-install.js.map +1 -1
- package/dist/commands/gate-install.test.js +294 -48
- package/dist/commands/gate-install.test.js.map +1 -1
- package/dist/commands/gate.d.ts +24 -9
- package/dist/commands/gate.d.ts.map +1 -1
- package/dist/commands/gate.js +36 -17
- package/dist/commands/gate.js.map +1 -1
- package/dist/commands/gate.test.js +74 -3
- package/dist/commands/gate.test.js.map +1 -1
- package/dist/commands/host-hooks.d.ts +8 -2
- package/dist/commands/host-hooks.d.ts.map +1 -1
- package/dist/commands/host-hooks.js.map +1 -1
- package/dist/commands/init-templates.d.ts +3 -3
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +107 -48
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -16
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install-hooks.d.ts.map +1 -1
- package/dist/commands/install-hooks.js +159 -27
- package/dist/commands/install-hooks.js.map +1 -1
- package/dist/commands/install-hooks.test.js +460 -12
- package/dist/commands/install-hooks.test.js.map +1 -1
- package/dist/commands/legs.d.ts +35 -7
- package/dist/commands/legs.d.ts.map +1 -1
- package/dist/commands/legs.js +26 -7
- package/dist/commands/legs.js.map +1 -1
- package/dist/commands/legs.test.js +87 -0
- package/dist/commands/legs.test.js.map +1 -1
- package/dist/commands/release-train-shape.test.d.ts +2 -0
- package/dist/commands/release-train-shape.test.d.ts.map +1 -0
- package/dist/commands/release-train-shape.test.js +226 -0
- package/dist/commands/release-train-shape.test.js.map +1 -0
- package/dist/commands/tools-hook-parity.test.js +200 -0
- package/dist/commands/tools-hook-parity.test.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -2946,22 +2946,33 @@ describe('buildPrePushHook — the review-leg floor arm (mmnto-ai/totem#2698)',
|
|
|
2946
2946
|
// queued `merge-gate` verb (mmnto-ai/totem#2708) would satisfy it.
|
|
2947
2947
|
expect(hook).not.toContain(`$TOTEM_CMD legs --help`);
|
|
2948
2948
|
});
|
|
2949
|
-
it('invokes the bare gate on strict and the advisory form otherwise', () => {
|
|
2949
|
+
it('invokes the bare gate on strict and the advisory form otherwise, recording the status on BOTH arms', () => {
|
|
2950
2950
|
expect(hook).toContain('$TOTEM_CMD legs gate\n');
|
|
2951
|
-
expect(hook).toContain('$TOTEM_CMD legs gate --advisory');
|
|
2952
|
-
expect(hook).toContain('legs_status=$?');
|
|
2951
|
+
expect(hook).toContain('$TOTEM_CMD legs gate --advisory\n legs_status=$?');
|
|
2952
|
+
expect(hook).toContain('legs_strict=1\n $TOTEM_CMD legs gate\n legs_status=$?');
|
|
2953
2953
|
});
|
|
2954
|
-
it('blocks on exit 3 and
|
|
2955
|
-
expect(hook).toContain(`echo "[Totem] BLOCKED: this push is legs-owed and carries no fresh falsification-leg deposit — run the leg, then 'totem legs deposit --sha HEAD --from <findings.json>' (mmnto-ai/totem#2698
|
|
2956
|
-
expect(hook).toContain('echo "[Totem] BLOCKED: the legs gate could not derive (totem legs gate exit status
|
|
2954
|
+
it('blocks on exit 3 and exit 2 at ANY tier with DISTINCT lines that name the knob; any other non-zero blocks on strict only (mmnto-ai/totem#2771)', () => {
|
|
2955
|
+
expect(hook).toContain(`echo "[Totem] BLOCKED: this push is legs-owed and carries no fresh falsification-leg deposit — run the leg, then 'totem legs deposit --sha HEAD --from <findings.json>' (mmnto-ai/totem#2698; strict mode, an agent seat, or hooks.legsOwed.enforce: block)"`);
|
|
2956
|
+
expect(hook).toContain('echo "[Totem] BLOCKED: the legs gate could not derive (totem legs gate exit status 2) — fix the checkout and retry (strict mode, an agent seat, or hooks.legsOwed.enforce: block)"');
|
|
2957
|
+
expect(hook).toContain('echo "[Totem] BLOCKED: the legs gate failed before deriving (totem legs gate exit status $legs_status) — fix the config or the CLI and retry (strict mode or an agent seat)"');
|
|
2957
2958
|
expect(hook).toContain('if [ "$legs_status" = "3" ]; then');
|
|
2958
|
-
expect(hook).toContain('elif [ "$legs_status"
|
|
2959
|
+
expect(hook).toContain('elif [ "$legs_status" = "2" ]; then');
|
|
2960
|
+
expect(hook).toContain('elif [ "$legs_status" != "0" ] && [ "$legs_strict" = "1" ]; then');
|
|
2961
|
+
// The mapping sits AFTER both invocations, outside the tier guard: the
|
|
2962
|
+
// advisory arm falls through to the same three checks the strict arm does.
|
|
2963
|
+
const advisoryIdx = hook.indexOf('$TOTEM_CMD legs gate --advisory');
|
|
2964
|
+
const mappingIdx = hook.indexOf('if [ "$legs_status" = "3" ]; then');
|
|
2965
|
+
expect(advisoryIdx).toBeGreaterThan(-1);
|
|
2966
|
+
expect(mappingIdx).toBeGreaterThan(advisoryIdx);
|
|
2967
|
+
// And the hook never reads the config itself — the knob reaches it only as
|
|
2968
|
+
// an exit code (the gate is the one reader of hooks.legsOwed.enforce).
|
|
2969
|
+
expect(hook).not.toContain('totem.config');
|
|
2959
2970
|
});
|
|
2960
2971
|
it('FAILS CLOSED on strict when the resolved CLI lacks the verb, compat-opens otherwise', () => {
|
|
2961
2972
|
// The OPPOSITE of the `--gate` / `--scope-to-diff` precedent, by ruling
|
|
2962
2973
|
// (mmnto-ai/totem#2698 OQ2): those flags degrade to a form that still runs;
|
|
2963
2974
|
// an absent VERB has no degraded form, so strict blocks with the cure.
|
|
2964
|
-
expect(hook).toContain(`echo "[Totem] BLOCKED: this hook expects 'totem legs gate' (mmnto-ai/totem#2698) but the resolved CLI lacks it — 'npm i -g @mmnto/cli@latest' (strict mode)" >&2`);
|
|
2975
|
+
expect(hook).toContain(`echo "[Totem] BLOCKED: this hook expects 'totem legs gate' (mmnto-ai/totem#2698) but the resolved CLI lacks it — 'npm i -g @mmnto/cli@latest' (strict mode or an agent seat)" >&2`);
|
|
2965
2976
|
expect(hook).toContain(`echo "[totem] Hook running without the legs gate (CLI predates 'totem legs'); 'npm i -g @mmnto/cli@latest' enables it." >&2`);
|
|
2966
2977
|
});
|
|
2967
2978
|
it('runs BEFORE the shield block and inside the $TOTEM_CMD guard', () => {
|
|
@@ -3119,8 +3130,8 @@ describe('the review-leg floor arm executed under sh (mmnto-ai/totem#2698)', ()
|
|
|
3119
3130
|
expect(invocations()).not.toContain('legs gate --advisory');
|
|
3120
3131
|
expect(r.status).toBe(1);
|
|
3121
3132
|
});
|
|
3122
|
-
it.skipIf(!shellOk)('standard tier, no agent: --advisory is passed and exit
|
|
3123
|
-
writeStub({ helpText: HELP_WITH_GATE, gateExit:
|
|
3133
|
+
it.skipIf(!shellOk)('standard tier, no agent: --advisory is passed and exit 0 proceeds past the arm', () => {
|
|
3134
|
+
writeStub({ helpText: HELP_WITH_GATE, gateExit: 0 });
|
|
3124
3135
|
const r = runHook({ tier: 'standard', agent: false });
|
|
3125
3136
|
// Read from the recorded argv, not from the hook text: the flag has to
|
|
3126
3137
|
// have reached the CLI, not merely appear in the template.
|
|
@@ -3129,6 +3140,44 @@ describe('the review-leg floor arm executed under sh (mmnto-ai/totem#2698)', ()
|
|
|
3129
3140
|
expect(r.status).toBe(0);
|
|
3130
3141
|
expect(r.stdout).not.toContain('BLOCKED');
|
|
3131
3142
|
});
|
|
3143
|
+
// Since mmnto-ai/totem#2771 the advisory arm's exit code is READ. A real gate
|
|
3144
|
+
// under --advisory returns 3 or 2 only when hooks.legsOwed.enforce says
|
|
3145
|
+
// block, so a stub exiting 3 here IS the knob's shape as the hook sees it.
|
|
3146
|
+
it.skipIf(!shellOk)('standard tier + gate exit 3 (the enforce: block shape): BLOCKS with the line that names the knob', () => {
|
|
3147
|
+
writeStub({ helpText: HELP_WITH_GATE, gateExit: 3 });
|
|
3148
|
+
const r = runHook({ tier: 'standard', agent: false });
|
|
3149
|
+
expect(invocations()).toContain('legs gate --advisory');
|
|
3150
|
+
expect(r.status).toBe(1);
|
|
3151
|
+
expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed');
|
|
3152
|
+
expect(r.stdout).toContain('strict mode, an agent seat, or hooks.legsOwed.enforce: block');
|
|
3153
|
+
expect(r.stdout).toContain('[Totem] legs: stub gate line');
|
|
3154
|
+
});
|
|
3155
|
+
it.skipIf(!shellOk)('standard tier + gate exit 2: blocks with the DISTINCT not-derived line', () => {
|
|
3156
|
+
writeStub({ helpText: HELP_WITH_GATE, gateExit: 2 });
|
|
3157
|
+
const r = runHook({ tier: 'standard', agent: false });
|
|
3158
|
+
expect(invocations()).toContain('legs gate --advisory');
|
|
3159
|
+
expect(r.status).toBe(1);
|
|
3160
|
+
expect(r.stdout).toContain('the legs gate could not derive (totem legs gate exit status 2)');
|
|
3161
|
+
expect(r.stdout).not.toContain('this push is legs-owed');
|
|
3162
|
+
});
|
|
3163
|
+
// A PIN, not a falsifier: this case holds on the pre-knob hook too (whose
|
|
3164
|
+
// standard arm never read the status at all). It is here so the one
|
|
3165
|
+
// behaviour the knob must NOT change stays asserted next to the ones it does.
|
|
3166
|
+
it.skipIf(!shellOk)('standard tier + gate exit 1 (a failure BEFORE the derivation): passes, byte for byte the pre-knob behaviour', () => {
|
|
3167
|
+
writeStub({ helpText: HELP_WITH_GATE, gateExit: 1 });
|
|
3168
|
+
const r = runHook({ tier: 'standard', agent: false });
|
|
3169
|
+
expect(invocations()).toContain('legs gate --advisory');
|
|
3170
|
+
expect(r.status).toBe(0);
|
|
3171
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3172
|
+
});
|
|
3173
|
+
it.skipIf(!shellOk)('strict tier + gate exit 1: blocks with the failed-before-deriving line, not the not-derived one', () => {
|
|
3174
|
+
writeStub({ helpText: HELP_WITH_GATE, gateExit: 1 });
|
|
3175
|
+
const r = runHook({ tier: 'strict', agent: false });
|
|
3176
|
+
expect(invocations()).toContain('legs gate');
|
|
3177
|
+
expect(r.status).toBe(1);
|
|
3178
|
+
expect(r.stdout).toContain('the legs gate failed before deriving (totem legs gate exit status 1)');
|
|
3179
|
+
expect(r.stdout).not.toContain('could not derive');
|
|
3180
|
+
});
|
|
3132
3181
|
});
|
|
3133
3182
|
// The arm COMPOSED with the real gate (mmnto-ai/totem#2698 fold 2, MATERIAL).
|
|
3134
3183
|
//
|
|
@@ -3247,8 +3296,8 @@ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2
|
|
|
3247
3296
|
process.chdir(realCwd);
|
|
3248
3297
|
cleanTmpDir(tmpDir);
|
|
3249
3298
|
});
|
|
3250
|
-
function runHook() {
|
|
3251
|
-
fs.writeFileSync(path.join(repoDir, 'pre-push'), buildPrePushHook({ ...RENDER, tier
|
|
3299
|
+
function runHook(tier = 'strict') {
|
|
3300
|
+
fs.writeFileSync(path.join(repoDir, 'pre-push'), buildPrePushHook({ ...RENDER, tier }));
|
|
3252
3301
|
const env = {
|
|
3253
3302
|
...process.env,
|
|
3254
3303
|
PATH: [binDir, process.env['PATH'] ?? ''].join(path.delimiter),
|
|
@@ -3259,6 +3308,20 @@ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2
|
|
|
3259
3308
|
const result = spawnSync('sh', ['./pre-push'], { cwd: repoDir, env, encoding: 'utf-8' });
|
|
3260
3309
|
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
3261
3310
|
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Rewrite the fixture's config with the knob — in the WORKING TREE only. The
|
|
3313
|
+
* gate judges the committed branch diff, so an uncommitted config edit never
|
|
3314
|
+
* enters the owed set; the gate's config LOAD reads the working file.
|
|
3315
|
+
*/
|
|
3316
|
+
function setEnforce(enforce) {
|
|
3317
|
+
fs.writeFileSync(path.join(repoDir, 'totem.config.ts'), [
|
|
3318
|
+
'export default {',
|
|
3319
|
+
" targets: [{ glob: 'docs/**/*.md', type: 'spec', strategy: 'markdown-heading' }],",
|
|
3320
|
+
` hooks: { legsOwed: { globs: ['docs/**'], enforce: '${enforce}' } },`,
|
|
3321
|
+
'};',
|
|
3322
|
+
'',
|
|
3323
|
+
].join(NL));
|
|
3324
|
+
}
|
|
3262
3325
|
const invocations = () => {
|
|
3263
3326
|
const native = logPath.split('/').join(path.sep);
|
|
3264
3327
|
return fs.existsSync(native) ? fs.readFileSync(native, 'utf-8').trim().split(NL) : [];
|
|
@@ -3374,6 +3437,391 @@ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2
|
|
|
3374
3437
|
expect(r.stdout).toContain('[Totem] legs: not owed — no changed path matched hooks.legsOwed.globs (1 globs; head');
|
|
3375
3438
|
expect(r.stdout).not.toContain('BLOCKED');
|
|
3376
3439
|
});
|
|
3440
|
+
// The knob, end to end (mmnto-ai/totem#2771): the config declares it, the
|
|
3441
|
+
// REAL gate reads it and maps the exit, the STANDARD-tier hook — which passes
|
|
3442
|
+
// --advisory and, before the knob, ignored the status entirely — blocks on
|
|
3443
|
+
// what comes back. No stub scripts the 3 here; the derivation produces it.
|
|
3444
|
+
it.skipIf(!composedOk)("OWED, no deposit, STANDARD tier, hooks.legsOwed.enforce: 'block': the hook blocks and names the knob", () => {
|
|
3445
|
+
setEnforce('block');
|
|
3446
|
+
const r = runHook('standard');
|
|
3447
|
+
// The advisory FLAG was passed — the tier did not change — and the gate
|
|
3448
|
+
// still returned 3 because the knob overrode it.
|
|
3449
|
+
expect(invocations()).toContain('legs gate --advisory');
|
|
3450
|
+
expect(invocations()).not.toContain('legs gate');
|
|
3451
|
+
expect(r.status).toBe(1);
|
|
3452
|
+
expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed (docs/** →');
|
|
3453
|
+
expect(r.stdout).toContain('[Totem] legs: hooks.legsOwed.enforce = block');
|
|
3454
|
+
expect(r.stdout).toContain('strict mode, an agent seat, or hooks.legsOwed.enforce: block');
|
|
3455
|
+
// The shield block after the arm was never reached.
|
|
3456
|
+
expect(invocations()).not.toContain('doctor --strict');
|
|
3457
|
+
});
|
|
3458
|
+
it.skipIf(!composedOk)("OWED, no deposit, STRICT tier, hooks.legsOwed.enforce: 'advisory': the gate's text prints, the hook passes", () => {
|
|
3459
|
+
setEnforce('advisory');
|
|
3460
|
+
const r = runHook('strict');
|
|
3461
|
+
expect(invocations()).toContain('legs gate');
|
|
3462
|
+
// The gate's own verdict text is byte-identical to the blocking case —
|
|
3463
|
+
// only the exit code moved — and the knob line says why.
|
|
3464
|
+
expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed (docs/** →');
|
|
3465
|
+
expect(r.stdout).toContain('[Totem] legs: hooks.legsOwed.enforce = advisory');
|
|
3466
|
+
// The HOOK's block line never fires, and the hook proceeds to the shield block.
|
|
3467
|
+
expect(r.stdout).not.toContain('strict mode, an agent seat, or hooks.legsOwed.enforce: block');
|
|
3468
|
+
expect(invocations()).toContain('doctor --strict');
|
|
3469
|
+
expect(r.status).toBe(0);
|
|
3470
|
+
});
|
|
3471
|
+
it.skipIf(!composedOk)("OWED with a covering ANCESTOR deposit under enforce: 'block' on the STANDARD tier: the hook passes on evidence", () => {
|
|
3472
|
+
setEnforce('block');
|
|
3473
|
+
deposit(ancestorSha);
|
|
3474
|
+
const r = runHook('standard');
|
|
3475
|
+
expect(invocations()).toContain('legs gate --advisory');
|
|
3476
|
+
expect(r.stdout).toContain('[Totem] legs evidence: .totem/artifacts/legs/');
|
|
3477
|
+
expect(r.stdout).toContain('[Totem] legs: hooks.legsOwed.enforce = block');
|
|
3478
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3479
|
+
expect(r.status).toBe(0);
|
|
3480
|
+
});
|
|
3481
|
+
});
|
|
3482
|
+
// ─── Fences and bare hash lines (mmnto-ai/totem#2769) ─────
|
|
3483
|
+
//
|
|
3484
|
+
// Two holes the mmnto-ai/totem#2759 round declined as pre-existing. Each
|
|
3485
|
+
// NEGATIVE case here passes on the reader as it stood on main (a lone `####`
|
|
3486
|
+
// counted as body; a promised heading inside a fenced block matched); each must
|
|
3487
|
+
// BLOCK after. The POSITIVE cases pin what the fix must not take with it: a
|
|
3488
|
+
// section whose only content is a code block is a written section, and a
|
|
3489
|
+
// fenced quote of a heading beside a real section changes nothing.
|
|
3490
|
+
describe('buildPreCommitHook spec-gate reader — fences and bare hash lines (mmnto-ai/totem#2769)', () => {
|
|
3491
|
+
const shellOk = spawnSync('sh', ['-c', 'command -v node >/dev/null 2>&1'], { encoding: 'utf-8' }).status === 0;
|
|
3492
|
+
const PROMISED = SPEC_SYSTEM_PROMPT.split('\n').filter((line) => line.startsWith('### '));
|
|
3493
|
+
function promised(prefix) {
|
|
3494
|
+
const found = PROMISED.find((heading) => heading.startsWith(prefix));
|
|
3495
|
+
if (found === undefined)
|
|
3496
|
+
throw new Error(`no promised heading starts with ${prefix}`);
|
|
3497
|
+
return found;
|
|
3498
|
+
}
|
|
3499
|
+
const EDGE_CASES = promised('### Edge Cases');
|
|
3500
|
+
const EXECUTION_FLOW = promised('### Execution Flow');
|
|
3501
|
+
const TEST_PLAN = promised('### Test Plan');
|
|
3502
|
+
/** Three backticks, built rather than authored (three quoting layers sit between here and the reader). */
|
|
3503
|
+
const FENCE = String.fromCharCode(96, 96, 96);
|
|
3504
|
+
const NL = String.fromCharCode(10);
|
|
3505
|
+
function nineBodied(overrides = {}) {
|
|
3506
|
+
return PROMISED.map((heading) => {
|
|
3507
|
+
const override = overrides[heading];
|
|
3508
|
+
return override === undefined
|
|
3509
|
+
? `${heading}${NL}${NL}A non-blank body under ${heading}.${NL}`
|
|
3510
|
+
: override;
|
|
3511
|
+
})
|
|
3512
|
+
.filter((block) => block.length > 0)
|
|
3513
|
+
.join(NL);
|
|
3514
|
+
}
|
|
3515
|
+
let tmpDir;
|
|
3516
|
+
beforeEach(() => {
|
|
3517
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-hook-2769-'));
|
|
3518
|
+
execSync('git init -q', { cwd: tmpDir, stdio: 'ignore' });
|
|
3519
|
+
execSync('git checkout -q -b feat/fence-suite', { cwd: tmpDir, stdio: 'ignore' });
|
|
3520
|
+
fs.writeFileSync(path.join(tmpDir, 'pre-commit'), buildPreCommitHook(RENDER));
|
|
3521
|
+
});
|
|
3522
|
+
afterEach(() => {
|
|
3523
|
+
cleanTmpDir(tmpDir);
|
|
3524
|
+
});
|
|
3525
|
+
function judge(content) {
|
|
3526
|
+
const dir = path.join(tmpDir, '.totem', 'artifacts', 'runs');
|
|
3527
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3528
|
+
fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({ output: { content } }), null, 2));
|
|
3529
|
+
const r = spawnSync('sh', ['./pre-commit'], {
|
|
3530
|
+
cwd: tmpDir,
|
|
3531
|
+
encoding: 'utf-8',
|
|
3532
|
+
env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
|
|
3533
|
+
});
|
|
3534
|
+
return { status: r.status, stdout: r.stdout };
|
|
3535
|
+
}
|
|
3536
|
+
// ── Negative: pass today, must block ──
|
|
3537
|
+
it.skipIf(!shellOk)('a lone #### line is not a body (hole 1)', () => {
|
|
3538
|
+
const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}####${NL}` }));
|
|
3539
|
+
expect(r.status).toBe(1);
|
|
3540
|
+
expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
|
|
3541
|
+
});
|
|
3542
|
+
it.skipIf(!shellOk)('a run of # characters with only whitespace around it is not a body either', () => {
|
|
3543
|
+
const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL} ### ${NL}#${NL}` }));
|
|
3544
|
+
expect(r.status).toBe(1);
|
|
3545
|
+
expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
|
|
3546
|
+
});
|
|
3547
|
+
it.skipIf(!shellOk)('the promised skeleton quoted inside a fenced block is not the sections it names (hole 2)', () => {
|
|
3548
|
+
const r = judge(`Some prose about the draft.${NL}${NL}${FENCE}${NL}${nineBodied()}${NL}${FENCE}${NL}`);
|
|
3549
|
+
expect(r.status).toBe(1);
|
|
3550
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
|
|
3551
|
+
});
|
|
3552
|
+
it.skipIf(!shellOk)('a tilde fence hides a heading the same way', () => {
|
|
3553
|
+
const r = judge(nineBodied({ [TEST_PLAN]: `~~~${NL}${TEST_PLAN}${NL}${NL}quoted, not written${NL}~~~${NL}` }));
|
|
3554
|
+
expect(r.status).toBe(1);
|
|
3555
|
+
expect(r.stdout).toContain(`is missing heading ${TEST_PLAN}`);
|
|
3556
|
+
});
|
|
3557
|
+
it.skipIf(!shellOk)('a heading whose only content is an EMPTY fence is empty', () => {
|
|
3558
|
+
const r = judge(nineBodied({ [EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}${NL}${FENCE}${NL}` }));
|
|
3559
|
+
expect(r.status).toBe(1);
|
|
3560
|
+
expect(r.stdout).toContain(`has an empty heading ${EXECUTION_FLOW}`);
|
|
3561
|
+
});
|
|
3562
|
+
// ── Positive: must keep passing ──
|
|
3563
|
+
it.skipIf(!shellOk)('a section whose only content is a code block is a written section', () => {
|
|
3564
|
+
const r = judge(nineBodied({
|
|
3565
|
+
[EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}mermaid${NL}A --> B${NL}${FENCE}${NL}`,
|
|
3566
|
+
}));
|
|
3567
|
+
expect(r.status).toBe(0);
|
|
3568
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3569
|
+
});
|
|
3570
|
+
it.skipIf(!shellOk)('a fenced quote of a heading beside the real section changes nothing', () => {
|
|
3571
|
+
const r = judge(nineBodied({
|
|
3572
|
+
[TEST_PLAN]: `${TEST_PLAN}${NL}${NL}Real plan text.${NL}${NL}${FENCE}${NL}${EDGE_CASES}${NL}(quoted as an example)${NL}${FENCE}${NL}`,
|
|
3573
|
+
}));
|
|
3574
|
+
expect(r.status).toBe(0);
|
|
3575
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3576
|
+
expect(r.stdout).not.toContain('tolerated');
|
|
3577
|
+
});
|
|
3578
|
+
it.skipIf(!shellOk)('a heading that appears fenced first and real later is found at the real line — the disclosure proves which', () => {
|
|
3579
|
+
// The EXACT promised heading sits inside a fence; the real section below
|
|
3580
|
+
// is written in the TOLERANT form. On the pre-fix reader the exact pass
|
|
3581
|
+
// matched the fenced copy and the pass line carried no tolerance; skipping
|
|
3582
|
+
// the fenced copy makes the tolerant pass find the real line and SAY so.
|
|
3583
|
+
const r = judge(nineBodied({
|
|
3584
|
+
[EXECUTION_FLOW]: `${FENCE}${NL}${EXECUTION_FLOW}${NL}${FENCE}${NL}${NL}### Execution Flow${NL}${NL}The real section, in the tolerant spelling.${NL}`,
|
|
3585
|
+
}));
|
|
3586
|
+
expect(r.status).toBe(0);
|
|
3587
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3588
|
+
expect(r.stdout).toContain(`tolerated ${EXECUTION_FLOW} ~ ### Execution Flow`);
|
|
3589
|
+
});
|
|
3590
|
+
it.skipIf(!shellOk)('an unclosed fence runs to the end of the draft, and the block says where it opened', () => {
|
|
3591
|
+
// Everything after an unclosed opener is inside the fence: the headings
|
|
3592
|
+
// quoted there are not candidates, so the draft is missing them — and the
|
|
3593
|
+
// reason names the fence, so the cure is never a heading the draft contains.
|
|
3594
|
+
const r = judge(`Intro.${NL}${FENCE}${NL}${nineBodied()}`);
|
|
3595
|
+
expect(r.status).toBe(1);
|
|
3596
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
|
|
3597
|
+
expect(r.stdout).toContain('it appears only inside a fenced code block opened at line 2');
|
|
3598
|
+
expect(r.stdout).toContain('that fence is never closed');
|
|
3599
|
+
});
|
|
3600
|
+
// ── The fence state's edges (the leg's probes, pinned) ──
|
|
3601
|
+
it.skipIf(!shellOk)('a three-backtick line does not close a four-backtick fence', () => {
|
|
3602
|
+
// CommonMark: the closer is the same character, at least as long. A draft
|
|
3603
|
+
// wrapped in a four-backtick fence with one nested three-backtick example
|
|
3604
|
+
// is one code block with zero headings.
|
|
3605
|
+
const FENCE4 = FENCE + String.fromCharCode(96);
|
|
3606
|
+
const r = judge(`Intro prose.${NL}${FENCE4}${NL}quoted example:${NL}${FENCE}${NL}${nineBodied()}${NL}${FENCE4}${NL}`);
|
|
3607
|
+
expect(r.status).toBe(1);
|
|
3608
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
|
|
3609
|
+
expect(r.stdout).toContain('inside a fenced code block opened at line 2');
|
|
3610
|
+
});
|
|
3611
|
+
it.skipIf(!shellOk)('a four-backtick closer closes a three-backtick fence', () => {
|
|
3612
|
+
const FENCE4 = FENCE + String.fromCharCode(96);
|
|
3613
|
+
const r = judge(nineBodied({
|
|
3614
|
+
[EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}${NL}A --> B${NL}${FENCE4}${NL}`,
|
|
3615
|
+
}));
|
|
3616
|
+
expect(r.status).toBe(0);
|
|
3617
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3618
|
+
});
|
|
3619
|
+
it.skipIf(!shellOk)('a fence line indented four spaces is not a fence (an indented code block, or a lazy continuation after a paragraph)', () => {
|
|
3620
|
+
// CommonMark: up to three spaces of indentation keeps a fence a fence; four
|
|
3621
|
+
// makes it an indented code block. The DISCRIMINATING shape: an indented
|
|
3622
|
+
// fence line with the whole skeleton after it and no closer. A reader that
|
|
3623
|
+
// treated the indented line as an opener would swallow every heading; this
|
|
3624
|
+
// one reads it as text, so the draft passes.
|
|
3625
|
+
const r = judge(`Intro.${NL} ${FENCE}${NL}${nineBodied()}`);
|
|
3626
|
+
expect(r.status).toBe(0);
|
|
3627
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3628
|
+
});
|
|
3629
|
+
it.skipIf(!shellOk)('a backtick opener whose info string carries a backtick is not a fence', () => {
|
|
3630
|
+
const r = judge(nineBodied({
|
|
3631
|
+
[EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}js${String.fromCharCode(96)}x${NL}`,
|
|
3632
|
+
}));
|
|
3633
|
+
expect(r.status).toBe(0);
|
|
3634
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3635
|
+
});
|
|
3636
|
+
it.skipIf(!shellOk)('a run of tildes used as a divider opens a fence in Markdown too, and the block says so', () => {
|
|
3637
|
+
// Twenty tildes are a fence opener under CommonMark (a thematic break is
|
|
3638
|
+
// ---, *** or ___); every heading after it is code until a closer at
|
|
3639
|
+
// least as long. The block names the fence rather than a heading the
|
|
3640
|
+
// draft contains verbatim.
|
|
3641
|
+
const rule = '~'.repeat(20);
|
|
3642
|
+
const r = judge(nineBodied({ [PROMISED[1]]: `${PROMISED[1]}${NL}${NL}Body.${NL}${NL}${rule}${NL}` }));
|
|
3643
|
+
expect(r.status).toBe(1);
|
|
3644
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[2]}`);
|
|
3645
|
+
// Line 9: three lines for the first section, one blank, then the override's
|
|
3646
|
+
// heading, blank, body, blank, and the divider.
|
|
3647
|
+
expect(r.stdout).toContain('it appears only inside a fenced code block opened at line 9');
|
|
3648
|
+
expect(r.stdout).toContain('that fence is never closed');
|
|
3649
|
+
});
|
|
3650
|
+
it.skipIf(!shellOk)('a fenced promised heading in its TOLERANT spelling is not body either', () => {
|
|
3651
|
+
// The matcher treats the parenthetical-dropped spelling as the same heading;
|
|
3652
|
+
// the body rule now does too, whatever the indentation inside the fence.
|
|
3653
|
+
const tolerant = EXECUTION_FLOW.replace(/ \([^)]*\)$/, '');
|
|
3654
|
+
expect(tolerant).not.toBe(EXECUTION_FLOW);
|
|
3655
|
+
const r = judge(nineBodied({
|
|
3656
|
+
[EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL} ${tolerant}${NL}${FENCE}${NL}`,
|
|
3657
|
+
}));
|
|
3658
|
+
expect(r.status).toBe(1);
|
|
3659
|
+
expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
|
|
3660
|
+
expect(r.stdout).toContain('nothing under it counts as body: a promised heading quoted inside a fenced code block');
|
|
3661
|
+
});
|
|
3662
|
+
it.skipIf(!shellOk)('an INDENTED fenced copy of a missing heading is still found and named', () => {
|
|
3663
|
+
const indented = nineBodied()
|
|
3664
|
+
.split(NL)
|
|
3665
|
+
.map((line) => ` ${line}`)
|
|
3666
|
+
.join(NL);
|
|
3667
|
+
const r = judge(`Prose.${NL}${FENCE}${NL}${indented}${NL}${FENCE}${NL}`);
|
|
3668
|
+
expect(r.status).toBe(1);
|
|
3669
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
|
|
3670
|
+
expect(r.stdout).toContain('it appears only inside a fenced code block opened at line 2');
|
|
3671
|
+
// Closed fence: the cure is to write the section outside it, not to close it.
|
|
3672
|
+
expect(r.stdout).toContain('write the section outside the fence');
|
|
3673
|
+
expect(r.stdout).not.toContain('never closed');
|
|
3674
|
+
});
|
|
3675
|
+
it.skipIf(!shellOk)('a lone #### under a heading names the rule that emptied the section', () => {
|
|
3676
|
+
const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}####${NL}` }));
|
|
3677
|
+
expect(r.status).toBe(1);
|
|
3678
|
+
expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES} — nothing under it counts as body: a line of # characters`);
|
|
3679
|
+
});
|
|
3680
|
+
it.skipIf(!shellOk)('every rule that emptied a section is named, and an empty fence pair is one of them', () => {
|
|
3681
|
+
// Both rules at once: a bare hash line and a fenced promised heading.
|
|
3682
|
+
const both = judge(nineBodied({
|
|
3683
|
+
[EDGE_CASES]: `${EDGE_CASES}${NL}####${NL}${FENCE}${NL}${TEST_PLAN}${NL}${FENCE}${NL}`,
|
|
3684
|
+
}));
|
|
3685
|
+
expect(both.status).toBe(1);
|
|
3686
|
+
expect(both.stdout).toContain('nothing under it counts as body: a promised heading quoted inside a fenced code block; a line of # characters');
|
|
3687
|
+
// An empty fence pair is a rule this slice introduced; it names itself too.
|
|
3688
|
+
const empty = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL}${FENCE}${NL}` }));
|
|
3689
|
+
expect(empty.status).toBe(1);
|
|
3690
|
+
expect(empty.stdout).toContain('nothing under it counts as body: an empty fenced code block');
|
|
3691
|
+
});
|
|
3692
|
+
it.skipIf(!shellOk)('a closing fence that repeats the info string does not close (the cure names a bare closer)', () => {
|
|
3693
|
+
// A common model output error: ```mermaid … ```mermaid. Under CommonMark the
|
|
3694
|
+
// second line is not a closer, so every heading after it is fenced.
|
|
3695
|
+
const idx = PROMISED.indexOf(EXECUTION_FLOW);
|
|
3696
|
+
const r = judge(nineBodied({
|
|
3697
|
+
[EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}mermaid${NL}A --> B${NL}${FENCE}mermaid${NL}`,
|
|
3698
|
+
}));
|
|
3699
|
+
expect(r.status).toBe(1);
|
|
3700
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[idx + 1]}`);
|
|
3701
|
+
expect(r.stdout).toContain('close it with a bare closing fence');
|
|
3702
|
+
});
|
|
3703
|
+
it.skipIf(!shellOk)('the never-closed cure is offered only for the fence the copy sits in', () => {
|
|
3704
|
+
// Test Plan exists only as a copy inside a CLOSED fence; an unrelated fence
|
|
3705
|
+
// is left open at the end of the draft. The cure must not blame that one.
|
|
3706
|
+
const r = judge(`${nineBodied({
|
|
3707
|
+
[EDGE_CASES]: `${EDGE_CASES}${NL}${NL}A real body line.${NL}${NL}${FENCE}${NL}${TEST_PLAN}${NL}${FENCE}${NL}`,
|
|
3708
|
+
[TEST_PLAN]: '',
|
|
3709
|
+
})}${NL}${FENCE}${NL}stray, never closed${NL}`);
|
|
3710
|
+
expect(r.status).toBe(1);
|
|
3711
|
+
expect(r.stdout).toContain(`is missing heading ${TEST_PLAN}`);
|
|
3712
|
+
expect(r.stdout).toContain('write the section outside the fence');
|
|
3713
|
+
expect(r.stdout).not.toContain('never closed');
|
|
3714
|
+
});
|
|
3715
|
+
it.skipIf(!shellOk)('RECORD: a bound record whose only headings sit inside a fence has no heading with a body', () => {
|
|
3716
|
+
const abs = path.join(tmpDir, '.totem', 'specs', 'rec.md');
|
|
3717
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
3718
|
+
fs.writeFileSync(abs, `${FENCE}yaml${NL}# Problem${NL}problem: the thing${NL}${FENCE}${NL}`, 'utf-8');
|
|
3719
|
+
const sha256 = crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex');
|
|
3720
|
+
const dir = path.join(tmpDir, '.totem', 'artifacts', 'runs');
|
|
3721
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3722
|
+
fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({
|
|
3723
|
+
grounding: {
|
|
3724
|
+
anchor: { kind: GROUNDING_ANCHOR_RECORD, ref: '.totem/specs/rec.md', sha256 },
|
|
3725
|
+
},
|
|
3726
|
+
}), null, 2));
|
|
3727
|
+
const r = spawnSync('sh', ['./pre-commit'], {
|
|
3728
|
+
cwd: tmpDir,
|
|
3729
|
+
encoding: 'utf-8',
|
|
3730
|
+
env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
|
|
3731
|
+
});
|
|
3732
|
+
expect(r.status).toBe(1);
|
|
3733
|
+
expect(r.stdout).toContain('the bound record at .totem/specs/rec.md has no heading with a body');
|
|
3734
|
+
// A real heading above the fence cures it — the record is edited, not re-run.
|
|
3735
|
+
fs.writeFileSync(abs, `# Record${NL}${NL}${FENCE}yaml${NL}problem: the thing${NL}${FENCE}${NL}`, 'utf-8');
|
|
3736
|
+
const sha2 = crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex');
|
|
3737
|
+
fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({
|
|
3738
|
+
grounding: {
|
|
3739
|
+
anchor: { kind: GROUNDING_ANCHOR_RECORD, ref: '.totem/specs/rec.md', sha256: sha2 },
|
|
3740
|
+
},
|
|
3741
|
+
}), null, 2));
|
|
3742
|
+
const ok = spawnSync('sh', ['./pre-commit'], {
|
|
3743
|
+
cwd: tmpDir,
|
|
3744
|
+
encoding: 'utf-8',
|
|
3745
|
+
env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
|
|
3746
|
+
});
|
|
3747
|
+
expect(ok.status).toBe(0);
|
|
3748
|
+
expect(ok.stdout).toContain('record sha256 matches');
|
|
3749
|
+
});
|
|
3750
|
+
it.skipIf(!shellOk)('#### inside a fence is code, and code is body', () => {
|
|
3751
|
+
const r = judge(nineBodied({
|
|
3752
|
+
[EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL}####${NL}#####${NL}${FENCE}${NL}`,
|
|
3753
|
+
}));
|
|
3754
|
+
expect(r.status).toBe(0);
|
|
3755
|
+
expect(r.stdout).not.toContain('BLOCKED');
|
|
3756
|
+
});
|
|
3757
|
+
it.skipIf(!shellOk)('seven hashes are a bare hash line, not a heading and not a body', () => {
|
|
3758
|
+
const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}#######${NL}` }));
|
|
3759
|
+
expect(r.status).toBe(1);
|
|
3760
|
+
expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
|
|
3761
|
+
});
|
|
3762
|
+
it.skipIf(!shellOk)('a fenced promised heading is not body for the section above it', () => {
|
|
3763
|
+
// Zero authored words: the only content under Edge Cases is the next
|
|
3764
|
+
// promised heading, quoted. Not a written section.
|
|
3765
|
+
const r = judge(nineBodied({
|
|
3766
|
+
[EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL}${PROMISED[PROMISED.indexOf(EDGE_CASES) + 1]}${NL}${FENCE}${NL}`,
|
|
3767
|
+
}));
|
|
3768
|
+
expect(r.status).toBe(1);
|
|
3769
|
+
expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
|
|
3770
|
+
});
|
|
3771
|
+
it.skipIf(!shellOk)('a closer followed by a non-breaking space does not close the fence', () => {
|
|
3772
|
+
// CommonMark permits only spaces and tabs after a closing fence; JS trim()
|
|
3773
|
+
// would accept U+00A0 and close a fence Markdown keeps open, letting the
|
|
3774
|
+
// skeleton after it reach the gate.
|
|
3775
|
+
const NBSP = String.fromCharCode(160);
|
|
3776
|
+
const r = judge(`Intro.${NL}${FENCE}${NL}quoted${NL}${FENCE}${NBSP}${NL}${nineBodied()}`);
|
|
3777
|
+
expect(r.status).toBe(1);
|
|
3778
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
|
|
3779
|
+
expect(r.stdout).toContain('that fence is never closed');
|
|
3780
|
+
// A closer followed by ordinary spaces and a tab still closes.
|
|
3781
|
+
const TAB = String.fromCharCode(9);
|
|
3782
|
+
const ok = judge(`Intro.${NL}${FENCE}${NL}quoted${NL}${FENCE} ${TAB}${NL}${NL}${nineBodied()}`);
|
|
3783
|
+
expect(ok.status).toBe(0);
|
|
3784
|
+
});
|
|
3785
|
+
it.skipIf(!shellOk)('a matching delimiter indented four spaces does not close the fence', () => {
|
|
3786
|
+
// CommonMark: a closing fence may be indented at most three spaces. The
|
|
3787
|
+
// reader counts the run only when the indent is under four, so a
|
|
3788
|
+
// four-space delimiter is text inside the fence and the skeleton after it
|
|
3789
|
+
// stays fenced (Greptile follow-up on mmnto-ai/totem#2769).
|
|
3790
|
+
const r = judge(`Intro.${NL}${FENCE}${NL}quoted${NL} ${FENCE}${NL}${nineBodied()}`);
|
|
3791
|
+
expect(r.status).toBe(1);
|
|
3792
|
+
expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
|
|
3793
|
+
expect(r.stdout).toContain('that fence is never closed');
|
|
3794
|
+
// Three spaces still close.
|
|
3795
|
+
const ok = judge(`Intro.${NL}${FENCE}${NL}quoted${NL} ${FENCE}${NL}${NL}${nineBodied()}`);
|
|
3796
|
+
expect(ok.status).toBe(0);
|
|
3797
|
+
});
|
|
3798
|
+
// ── The DOCUMENT arm (overridden prompt, bound record) under fences ──
|
|
3799
|
+
function judgeDocument(content) {
|
|
3800
|
+
const dir = path.join(tmpDir, '.totem', 'artifacts', 'runs');
|
|
3801
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3802
|
+
fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({
|
|
3803
|
+
admission: { runMetadata: { caller: 'spec', promptSource: PROMPT_SOURCE_OVERRIDE } },
|
|
3804
|
+
output: { content },
|
|
3805
|
+
}), null, 2));
|
|
3806
|
+
const r = spawnSync('sh', ['./pre-commit'], {
|
|
3807
|
+
cwd: tmpDir,
|
|
3808
|
+
encoding: 'utf-8',
|
|
3809
|
+
env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
|
|
3810
|
+
});
|
|
3811
|
+
return { status: r.status, stdout: r.stdout };
|
|
3812
|
+
}
|
|
3813
|
+
it.skipIf(!shellOk)('DOCUMENT: a heading that exists only inside a fence is no heading', () => {
|
|
3814
|
+
// A YAML document whose "# Problem" lines sit inside a fence has no
|
|
3815
|
+
// markdown heading at all; the pre-fix reader read the comments as headings.
|
|
3816
|
+
const r = judgeDocument(`The spec, as YAML:${NL}${NL}${FENCE}yaml${NL}# Problem${NL}problem: the thing${NL}# Tasks${NL}tasks: [a, b]${NL}${FENCE}${NL}`);
|
|
3817
|
+
expect(r.status).toBe(1);
|
|
3818
|
+
expect(r.stdout).toContain('has no heading with a body');
|
|
3819
|
+
});
|
|
3820
|
+
it.skipIf(!shellOk)('DOCUMENT: a real heading whose body is a code block is bodied', () => {
|
|
3821
|
+
const r = judgeDocument(`## The spec${NL}${NL}${FENCE}yaml${NL}problem: the thing${NL}${FENCE}${NL}`);
|
|
3822
|
+
expect(r.status).toBe(0);
|
|
3823
|
+
expect(r.stdout).toContain('shape DOCUMENT');
|
|
3824
|
+
});
|
|
3377
3825
|
});
|
|
3378
3826
|
// ─── The frozen mutation suite (mmnto-ai/totem#2737) ─────
|
|
3379
3827
|
//
|