Files
msd-core/tests/gsd-statusline-state.property.test.cjs
Cody Anderson 20ff405cb3 feat(#2162): opt-in compact GSD-state format for the statusline (#2175)
* feat(#2162): opt-in compact GSD-state format for the statusline

New statusline.state_format config, enum full|compact (default full —
existing rendering untouched). "compact" renders the state segment as
"<version> · P<phase>/<total> · <status>", e.g. "v1.12 · P7/12 ·
executing" — dropping the milestone name and progress bar (the two
biggest width costs) and collapsing narrative statuses to a single
keyword. Per the #2162 approval conditions, the keyword set is the
canonical vocabulary from normalizeStateStatus() in state-document.cjs
(discussing/planning/executing/verifying/completed/paused) — no
parallel hand-rolled list, so the vocabularies can't drift — and the
canonical stuck state "paused" renders uppercase as PAUSED (no new
"blocked" lifecycle state). Statuses the normalizer passes through
unrecognized fall back to their first word capped at 16 chars.
Lifecycle scenes preserved: active_phase wins over the body phase
number, milestone completion renders "complete", idle-with-next-action
renders "next <action> <phases>".

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* docs(#2162): changeset fragment for PR #2175

* fix(#2162): review fixes — ENUM_KEYS coverage, cap boundary tests, changeset format

- register statusline.state_format in the fix-1628 coercion-bypass matrix
- 15/16/17-char boundary tests for the shortGsdStatus fallback cap
- changeset body ends with the (#2162) citation per house convention

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* fix(#2162): round-2 review fixes — scene exclusivity, direct config-set coverage

- compact renderer gates the milestone-complete scene behind the absence of
  an in-flight phase id, mirroring formatGsdState's if/else precedence
  (Scene 1 beats Scene 3); regression test covers the non-atomic
  active_phase + percent=100 STATE.md shape
- direct config-set accept/reject test for statusline.state_format plain
  strings (ENUM_KEYS matrix covers only the JSON coercion shapes)

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* test: regenerate golden-install-parity fixtures for the statusline hook change

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* fix(#2162): complete-scene gate matches formatGsdState exactly (+property tests)

Re-review Major: gating done on !phaseId held completion back for the
legacy phaseNum shape — formatGsdState reaches Scene 3 on percent=100
regardless of phaseNum, so compact must too. Gate is now !s.activePhase.
The phaseNum-only test now expects 'complete' and cross-checks the full
renderer; a parity test feeds identical inputs to both renderers.
Re-review Minor: shortGsdStatus gets fast-check property coverage
(totality, canonical fixed points, separator safety, fallback shape).
Golden fixtures regenerated for the hook byte change.

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
2026-07-14 21:09:24 -04:00

67 lines
2.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Property tests for the compact GSD-state status normalizer (#2162).
*
* shortGsdStatus() collapses free-text STATE.md statuses to a canonical
* keyword (or a capped first-word fallback). As a parsing/normalization
* contract it gets property coverage per the repo testing rules, alongside
* the example-based cases in gsd-statusline.test.cjs.
*/
const { test, describe } = require('node:test');
const fc = require('./helpers/fast-check-setup.cjs');
const { shortGsdStatus } = require('../hooks/gsd-statusline.js');
const CANONICAL = ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused'];
describe('shortGsdStatus properties (#2162)', () => {
test('totality: any string input yields null or a short non-empty string', () => {
fc.assert(
fc.property(fc.string(), (s) => {
const out = shortGsdStatus(s);
if (out === null) return true;
return typeof out === 'string' && out.length > 0 && out.length <= 16;
}),
);
});
test('canonical statuses are fixed points (paused shouts as PAUSED)', () => {
fc.assert(
fc.property(fc.constantFrom(...CANONICAL), (canonical) => {
const out = shortGsdStatus(canonical);
return canonical === 'paused' ? out === 'PAUSED' : out === canonical;
}),
);
});
test('output never contains whitespace or separator characters', () => {
// The compact line joins segments with ' · ' — a status containing
// whitespace or the separator would corrupt the segment structure.
fc.assert(
fc.property(fc.string(), (s) => {
const out = shortGsdStatus(s);
return out === null || !/[\s·—–]/.test(out);
}),
);
});
test('unrecognized free text falls back to its first word, capped at 16', () => {
// Alphabetic words that are not canonical and don't contain canonical
// trigger substrings exercise the fallback path deterministically.
const word = fc.stringMatching(/^[A-Za-z]{1,32}$/).filter((w) => {
const lower = w.toLowerCase();
return !CANONICAL.some((c) => lower.includes(c.slice(0, 4)));
});
fc.assert(
fc.property(word, word, (first, second) => {
const out = shortGsdStatus(`${first} ${second}`);
// The normalizer may still map some phrasings to a canonical keyword
// (e.g. synonym tables); otherwise the first word survives, capped.
return out === null || CANONICAL.concat('PAUSED').includes(out) || out === first.slice(0, 16);
}),
);
});
});