* 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
This commit is contained in:
5
.changeset/proud-zebras-bark.md
Normal file
5
.changeset/proud-zebras-bark.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 2175
|
||||
---
|
||||
**Opt-in compact GSD-state statusline format** — new `statusline.state_format` config, enum `full`|`compact` (default `full`, the existing rendering). `compact` renders "<version> · P<phase>/<total> · <status>" (e.g. "v1.12 · P7/12 · executing"), dropping the milestone name and progress bar and collapsing narrative statuses to the canonical vocabulary from `normalizeStateStatus()` — the canonical stuck state `paused` renders uppercase as `PAUSED`. Solves the unbounded-width problem where free-text status sentences push the context meter off the line. (#2162)
|
||||
@@ -453,6 +453,7 @@ If `.planning/` is in `.gitignore`, `commit_docs` is automatically `false` regar
|
||||
| `statusline.show_last_command` | boolean | `false` | Append `last: /<cmd>` suffix to the statusline showing the most recently invoked slash command. Opt-in; reads the active session transcript to extract the latest `<command-name>` tag (closes #2538) |
|
||||
| `statusline.context_position` | string | `"end"` | Position of the context-window meter. `"end"` (default) renders at line tail; `"front"` renders immediately after the model name so the meter stays visible in narrow terminals. Closes #2937 |
|
||||
| `statusline.show_context_tokens` | boolean | `false` | Append the absolute token count (e.g. `(156k)`) after the context meter's percentage. Sums input, cache-creation, cache-read, and output tokens from the hook payload — a broader basis than the meter's percentage (which excludes output tokens), so the two figures can diverge slightly. Opt-in; the meter is unchanged when the flag is absent |
|
||||
| `statusline.state_format` | string | `"full"` | Format of the GSD-state segment. `"full"` (default) is the existing rendering with milestone name and progress bar. `"compact"` renders `<version> · P<phase>/<total> · <status>` (e.g. `v1.12 · P7/12 · executing`) — drops the milestone name and bar, and collapses narrative statuses to the canonical keyword set from `normalizeStateStatus()` (`paused` — the canonical stuck state — renders uppercase as `PAUSED`) |
|
||||
| `statusline.show_git` | boolean | `false` | Append a git segment after the directory: current branch plus compact work-state markers (`+staged` `~unstaged` `?untracked` `↑ahead` `↓behind`, or `✓` when clean and in sync). One `git status --porcelain=v2` call per render; the segment is absent outside a git repo or when git is unavailable |
|
||||
|
||||
The prompt injection guard hook (`gsd-prompt-guard.js`) is always active and cannot be disabled — it's a security feature, not a workflow toggle.
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"statusline.show_last_command",
|
||||
"statusline.context_position",
|
||||
"statusline.show_context_tokens",
|
||||
"statusline.state_format",
|
||||
"statusline.show_git",
|
||||
"workflow.max_discuss_passes",
|
||||
"features.thinking_partner",
|
||||
|
||||
@@ -11,6 +11,7 @@ const os = require('os');
|
||||
const childProcess = require('child_process');
|
||||
const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
|
||||
const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
|
||||
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
|
||||
// --- Config + last-command readers ------------------------------------------
|
||||
|
||||
@@ -319,6 +320,78 @@ function contextTokenSuffix(currentUsage) {
|
||||
return total > 0 ? ` (${formatTokens(total)})` : '';
|
||||
}
|
||||
|
||||
// --- Compact state format (opt-in) ---------------------------------------------
|
||||
|
||||
/**
|
||||
* Collapse GSD's free-text status (often a multi-sentence narrative) to a
|
||||
* single keyword, built on the canonical normalizer (#2162 approval
|
||||
* condition): normalizeStateStatus() in state-document.cjs owns the status
|
||||
* vocabulary (discussing / planning / executing / verifying / completed /
|
||||
* paused) so the two can't drift. "paused" — the canonical stuck state — is
|
||||
* uppercased to PAUSED, the one state worth shouting about. Statuses the
|
||||
* normalizer passes through unrecognized fall back to their first word,
|
||||
* capped at 16 chars so a rogue STATE.md can't blow up the line.
|
||||
* Returns null for empty input.
|
||||
*/
|
||||
const CANONICAL_STATUSES = ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused'];
|
||||
|
||||
function shortGsdStatus(status) {
|
||||
if (!status) return null;
|
||||
const norm = normalizeStateStatus(status, null);
|
||||
if (CANONICAL_STATUSES.includes(norm)) {
|
||||
return norm === 'paused' ? 'PAUSED' : norm;
|
||||
}
|
||||
// Unrecognized free text passes through normalizeStateStatus verbatim —
|
||||
// fall back to the first word, capped.
|
||||
const first = String(norm).trim().split(/[\s\u2014\u2013-]+/)[0] || '';
|
||||
return first ? first.slice(0, 16) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact alternative to formatGsdState, selected via
|
||||
* `statusline.state_format: "compact"`:
|
||||
*
|
||||
* "v1.12 · P7/12 · executing" (phase active)
|
||||
* "v2.0 · P4.5 · BLOCKED" (no total known)
|
||||
* "v2.0 · complete" (milestone done)
|
||||
* "v2.0 · next execute-phase 4.5" (idle with a queued action)
|
||||
*
|
||||
* Drops the milestone name and progress bar — the biggest width costs in the
|
||||
* default format — and collapses narrative statuses via shortGsdStatus().
|
||||
* The default "full" format is untouched.
|
||||
*/
|
||||
function formatGsdStateCompact(s) {
|
||||
const parts = [];
|
||||
|
||||
if (s.milestone) parts.push(s.milestone);
|
||||
|
||||
const phaseId = s.activePhase || s.phaseNum;
|
||||
if (phaseId) {
|
||||
parts.push(s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`);
|
||||
}
|
||||
|
||||
// Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
|
||||
// phase (Scene 1, gated on activePhase ONLY — the legacy phaseNum shape
|
||||
// still completes) wins over milestone-complete (Scene 3), even if a
|
||||
// non-atomic STATE.md edit leaves percent=100 alongside a lifecycle phase.
|
||||
const done = !s.activePhase && (Number(s.percent) === 100 ||
|
||||
(s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases));
|
||||
|
||||
if (done) {
|
||||
parts.push('complete');
|
||||
} else {
|
||||
const st = shortGsdStatus(s.status);
|
||||
if (st) {
|
||||
parts.push(st);
|
||||
} else if (!phaseId && s.nextAction) {
|
||||
const phasesStr = (s.nextPhases && s.nextPhases.length > 0) ? s.nextPhases.join('/') : '';
|
||||
parts.push(`next ${s.nextAction}${phasesStr ? ' ' + phasesStr : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' \u00b7 ');
|
||||
}
|
||||
|
||||
// --- Model name --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -529,8 +602,9 @@ function runStatusline() {
|
||||
}
|
||||
}
|
||||
|
||||
// GSD state (milestone · status · phase) — shown when no todo task
|
||||
const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {});
|
||||
// GSD state (milestone · status · phase) — shown when no todo task.
|
||||
// Format resolved below once config is read (statusline.state_format).
|
||||
let gsdStateStr = '';
|
||||
|
||||
// GSD update available?
|
||||
// Read only the per-package shared cache file (#607). The legacy
|
||||
@@ -558,6 +632,7 @@ function runStatusline() {
|
||||
// Failure here must never break the statusline — wrap the entire lookup.
|
||||
let lastCmdSuffix = '';
|
||||
let position = 'end';
|
||||
let stateFormat = 'full';
|
||||
let gitSuffix = '';
|
||||
try {
|
||||
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
||||
@@ -569,6 +644,7 @@ function runStatusline() {
|
||||
}
|
||||
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
||||
if (cfgPos != null) position = cfgPos;
|
||||
if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
|
||||
if (getConfigValue(cfg, 'statusline.show_git') === true) {
|
||||
gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
|
||||
}
|
||||
@@ -576,6 +652,11 @@ function runStatusline() {
|
||||
// Never break the statusline on config/transcript/git errors
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
const state = readGsdState(dir) || {};
|
||||
gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
||||
}
|
||||
|
||||
// Output
|
||||
const dirname = path.basename(dir);
|
||||
const middle = task
|
||||
@@ -675,6 +756,7 @@ module.exports = {
|
||||
evaluateUpdateCache,
|
||||
formatTokens,
|
||||
contextTokenSuffix,
|
||||
shortGsdStatus, formatGsdStateCompact,
|
||||
compactModelName,
|
||||
readGitStatus, parseGitStatus, buildGitSegment,
|
||||
};
|
||||
@@ -690,6 +772,7 @@ function renderStatusline(data) {
|
||||
|
||||
let lastCmdSuffix = '';
|
||||
let position = 'end';
|
||||
let stateFormat = 'full';
|
||||
let gitSuffix = '';
|
||||
try {
|
||||
const cfg = readGsdConfig(dir);
|
||||
@@ -701,12 +784,14 @@ function renderStatusline(data) {
|
||||
}
|
||||
const cfgPos = getConfigValue(cfg, 'statusline.context_position');
|
||||
if (cfgPos != null) position = cfgPos;
|
||||
if (getConfigValue(cfg, 'statusline.state_format') === 'compact') stateFormat = 'compact';
|
||||
if (getConfigValue(cfg, 'statusline.show_git') === true) {
|
||||
gitSuffix = buildGitSegment(parseGitStatus(readGitStatus(dir)));
|
||||
}
|
||||
} catch (e) { /* swallow */ }
|
||||
|
||||
const gsdStateStr = formatGsdState(readGsdState(dir) || {});
|
||||
const state = readGsdState(dir) || {};
|
||||
const gsdStateStr = stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
|
||||
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
||||
return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position });
|
||||
}
|
||||
|
||||
@@ -770,6 +770,10 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
|
||||
}
|
||||
}
|
||||
|
||||
// Statusline GSD-state format enum validation
|
||||
const VALID_STATE_FORMATS = ['full', 'compact'];
|
||||
if (kp === 'statusline.state_format') assertEnumValue(parsedValue, val, VALID_STATE_FORMATS, 'statusline.state_format');
|
||||
|
||||
// statusline.show_git — boolean only
|
||||
if (kp === 'statusline.show_git') {
|
||||
if (typeof parsedValue !== 'boolean') {
|
||||
|
||||
@@ -5952,6 +5952,7 @@ const ENUM_KEYS = [
|
||||
{ key: 'workflow.human_verify_mode', member: 'mid-flight' },
|
||||
{ key: 'workflow.context_guard_mode', member: 'off' },
|
||||
{ key: 'statusline.context_position', member: 'front' },
|
||||
{ key: 'statusline.state_format', member: 'compact' },
|
||||
{ key: 'code_quality.fallow.scope', member: 'phase' },
|
||||
{ key: 'code_quality.fallow.profile', member: 'standard' },
|
||||
{ key: 'plan_review.source_grounding_authority', member: 'grep' },
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "efc88e7691c6c3e6",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c",
|
||||
@@ -328,7 +328,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "eefea61f9b0e464c",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "e284419f4ce60383",
|
||||
"hooks/gsd-statusline.js": "ba8422027f710711",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -399,7 +399,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "c8800819f7443a15",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "05144fab95be6e45",
|
||||
"hooks/gsd-statusline.js": "85141ec6a067fce2",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -398,7 +398,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "5d2c06224db20d23",
|
||||
"hooks/gsd-statusline.js": "bc03b97ef19328c0",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -327,7 +327,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "5d2c06224db20d23",
|
||||
"hooks/gsd-statusline.js": "bc03b97ef19328c0",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "49dfaa890fdd5627",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -399,7 +399,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "7f7a7615b303369a",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "49fe17737e4565f9",
|
||||
"hooks/gsd-statusline.js": "7cdf1ae0e5b17969",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "efc88e7691c6c3e6",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "50658d517405cd63",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "12ee14a48b678d2b",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -328,7 +328,7 @@
|
||||
"hooks/gsd-read-guard.js": "1f58b020a91f032b",
|
||||
"hooks/gsd-read-injection-scanner.js": "f358eca3fa1eab24",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "815e107016e994a1",
|
||||
"hooks/gsd-statusline.js": "3f59c6becf124608",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
".kimi/hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
".kimi/hooks/gsd-read-injection-scanner.js": "c519598b9257aafa",
|
||||
".kimi/hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
".kimi/hooks/gsd-statusline.js": "e0a50e21a1e9aeb6",
|
||||
".kimi/hooks/gsd-statusline.js": "fb90ca297b60bddf",
|
||||
".kimi/hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
".kimi/hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
".kimi/hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
@@ -105,7 +105,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -399,7 +399,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "f72060dfe035f706",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "f93d64704cba14fd",
|
||||
"hooks/gsd-statusline.js": "982cbb17444935fa",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
4
tests/fixtures/golden-install-parity/pi.json
vendored
4
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -8,7 +8,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -295,7 +295,7 @@
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "f454242c010804cf",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "239c1b15d1ff4ef9",
|
||||
"hooks/gsd-statusline.js": "b59f79b77f53b2a0",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "205830afac36f33a",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
@@ -328,7 +328,7 @@
|
||||
"hooks/gsd-read-guard.js": "2c8d417d12b51040",
|
||||
"hooks/gsd-read-injection-scanner.js": "396574bd25e99ff9",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "e9cfc9ddfffabe4d",
|
||||
"hooks/gsd-statusline.js": "390b3601312345ae",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "c8283c0c8888e357",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "f21bb9ba5e55f642",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
66
tests/gsd-statusline-state.property.test.cjs
Normal file
66
tests/gsd-statusline-state.property.test.cjs
Normal file
@@ -0,0 +1,66 @@
|
||||
'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);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1365,6 +1365,244 @@ test('config-set statusline.show_context_tokens yes → rejected', () => {
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Compact GSD-state format (statusline.state_format)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
const statusline = require('../hooks/gsd-statusline.js');
|
||||
const { shortGsdStatus, formatGsdStateCompact, formatGsdState } = statusline;
|
||||
const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config-schema.cjs');
|
||||
|
||||
describe('config schema: statusline.state_format', () => {
|
||||
test('registers statusline.state_format', () => {
|
||||
assert.ok(
|
||||
VALID_CONFIG_KEYS.has('statusline.state_format'),
|
||||
'statusline.state_format must be in VALID_CONFIG_KEYS',
|
||||
);
|
||||
});
|
||||
// Direct config-set write-path coverage, mirroring the sibling
|
||||
// context_position tests (the ENUM_KEYS matrix covers only the JSON
|
||||
// coercion-bypass shapes, not the plain-string paths).
|
||||
test('config-set accepts "compact" and rejects an invalid plain string', () => {
|
||||
const tmpDir = createTempProject();
|
||||
try {
|
||||
const ok = runGsdTools(['config-set', 'statusline.state_format', 'compact'], tmpDir);
|
||||
assert.ok(ok.success, ok.error);
|
||||
const bad = runGsdTools(['config-set', 'statusline.state_format', 'tiny'], tmpDir);
|
||||
assert.equal(bad.success, false, 'invalid enum value must be rejected');
|
||||
assert.ok(
|
||||
/statusline\.state_format|Invalid/i.test(bad.error),
|
||||
`stderr must reference key or "Invalid"; got: ${bad.error}`,
|
||||
);
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortGsdStatus', () => {
|
||||
test('returns null for empty input', () => {
|
||||
assert.equal(shortGsdStatus(null), null);
|
||||
assert.equal(shortGsdStatus(''), null);
|
||||
assert.equal(shortGsdStatus(undefined), null);
|
||||
});
|
||||
test('paused — the canonical stuck state — wins and renders uppercase (#2162 condition)', () => {
|
||||
assert.equal(shortGsdStatus('paused — waiting on credentials'), 'PAUSED');
|
||||
assert.equal(shortGsdStatus('stopped by user'), 'PAUSED');
|
||||
});
|
||||
test('collapses lifecycle narratives to canonical keywords via normalizeStateStatus', () => {
|
||||
assert.equal(shortGsdStatus('Executing phase 7 of the parser milestone'), 'executing');
|
||||
assert.equal(shortGsdStatus('Ready to plan next phase'), 'planning');
|
||||
assert.equal(shortGsdStatus('Discussing scope with user'), 'discussing');
|
||||
assert.equal(shortGsdStatus('Verifying UAT criteria'), 'verifying');
|
||||
assert.equal(shortGsdStatus('Work complete'), 'completed');
|
||||
});
|
||||
test('matches the canonical vocabulary exactly — no drift from normalizeStateStatus', () => {
|
||||
const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
for (const canonical of ['discussing', 'planning', 'executing', 'verifying', 'completed', 'paused']) {
|
||||
const rendered = shortGsdStatus(canonical);
|
||||
const expected = canonical === 'paused' ? 'PAUSED' : canonical;
|
||||
assert.equal(rendered, expected);
|
||||
assert.equal(normalizeStateStatus(canonical, null), canonical,
|
||||
`canonical vocabulary changed upstream: ${canonical}`);
|
||||
}
|
||||
});
|
||||
test('unknown shapes fall back to the first word, capped at 16 chars', () => {
|
||||
assert.equal(shortGsdStatus('reticulating splines'), 'reticulating');
|
||||
assert.equal(shortGsdStatus('supercalifragilisticexpialidocious state'), 'supercalifragili');
|
||||
});
|
||||
test('16-char cap boundary: limit-1 / limit / limit+1', () => {
|
||||
assert.equal(shortGsdStatus('x'.repeat(15)), 'x'.repeat(15));
|
||||
assert.equal(shortGsdStatus('x'.repeat(16)), 'x'.repeat(16));
|
||||
assert.equal(shortGsdStatus('x'.repeat(17)), 'x'.repeat(16));
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatGsdStateCompact', () => {
|
||||
test('renders version · phase/total · status', () => {
|
||||
const out = formatGsdStateCompact({
|
||||
milestone: 'v1.12', phaseNum: '7', phaseTotal: '12',
|
||||
status: 'Executing phase 7 — building the parser',
|
||||
});
|
||||
assert.equal(out, 'v1.12 · P7/12 · executing');
|
||||
});
|
||||
test('prefers lifecycle active_phase over body phase number', () => {
|
||||
const out = formatGsdStateCompact({
|
||||
milestone: 'v2.0', activePhase: '4.5', phaseNum: '4', status: 'executing',
|
||||
});
|
||||
assert.equal(out, 'v2.0 · P4.5 · executing');
|
||||
});
|
||||
test('paused state renders uppercase in the compact line', () => {
|
||||
const out = formatGsdStateCompact({
|
||||
milestone: 'v2.0', activePhase: '4.5', status: 'paused — waiting on review',
|
||||
});
|
||||
assert.equal(out, 'v2.0 · P4.5 · PAUSED');
|
||||
});
|
||||
test('milestone completion renders "complete"', () => {
|
||||
assert.equal(formatGsdStateCompact({ milestone: 'v2.0', percent: '100' }), 'v2.0 · complete');
|
||||
assert.equal(
|
||||
formatGsdStateCompact({ milestone: 'v2.0', completedPhases: '5', totalPhases: '5' }),
|
||||
'v2.0 · complete');
|
||||
});
|
||||
test('scene exclusivity: an in-flight phase wins over milestone-complete', () => {
|
||||
// Non-atomic STATE.md edits can leave active_phase populated alongside
|
||||
// percent=100 — the compact format must mirror formatGsdState's
|
||||
// if/else-chain precedence (Scene 1 beats Scene 3), never render both.
|
||||
const state = {
|
||||
milestone: 'v2.0', activePhase: '4.5', percent: '100', status: 'executing',
|
||||
};
|
||||
assert.equal(formatGsdStateCompact(state), 'v2.0 · P4.5 · executing');
|
||||
const full = formatGsdState(state);
|
||||
assert.ok(!/(complete)/.test(full) || !/4\.5/.test(full),
|
||||
`full format must not co-render phase and complete either; got: ${full}`);
|
||||
// The legacy body-phase shape (phaseNum, no activePhase) does NOT hold
|
||||
// completion back — formatGsdState reaches Scene 3 on percent=100
|
||||
// regardless of phaseNum, and compact must agree (#2175 re-review Major).
|
||||
const legacyDone = { milestone: 'v2.0', phaseNum: '5', phaseTotal: '5', percent: '100', status: 'verifying' };
|
||||
assert.equal(formatGsdStateCompact(legacyDone), 'v2.0 · P5/5 · complete');
|
||||
assert.ok(formatGsdState(legacyDone).includes('milestone complete'),
|
||||
'parity: full format must render Scene 3 for the same input');
|
||||
});
|
||||
test('parity: both renderers agree on completion for the same input', () => {
|
||||
// Feed identical state objects to both renderers and require they agree
|
||||
// on whether the milestone reads as complete — the drift guard for the
|
||||
// parallel rendering surfaces.
|
||||
const cases = [
|
||||
{ milestone: 'v2.0', percent: '100' },
|
||||
{ milestone: 'v2.0', phaseNum: '5', phaseTotal: '5', percent: '100', status: 'verifying' },
|
||||
{ milestone: 'v2.0', completedPhases: '5', totalPhases: '5' },
|
||||
{ milestone: 'v2.0', activePhase: '4.5', percent: '100', status: 'executing' },
|
||||
{ milestone: 'v1.9', percent: '40', status: 'executing', phaseNum: '2', phaseTotal: '5' },
|
||||
];
|
||||
for (const s of cases) {
|
||||
const fullDone = formatGsdState(s).includes('milestone complete');
|
||||
const compactDone = / complete$|^complete$/.test(formatGsdStateCompact(s));
|
||||
assert.equal(compactDone, fullDone,
|
||||
`completion parity diverged for ${JSON.stringify(s)}`);
|
||||
}
|
||||
});
|
||||
test('idle with queued next action renders "next <action> <phases>"', () => {
|
||||
const out = formatGsdStateCompact({
|
||||
milestone: 'v2.0', nextAction: 'execute-phase', nextPhases: ['4.5', '4.6'],
|
||||
});
|
||||
assert.equal(out, 'v2.0 · next execute-phase 4.5/4.6');
|
||||
});
|
||||
test('empty state renders empty string', () => {
|
||||
assert.equal(formatGsdStateCompact({}), '');
|
||||
});
|
||||
test('drops the milestone name and progress bar the full format shows', () => {
|
||||
const state = {
|
||||
milestone: 'v1.9', milestoneName: 'Code Quality', percent: '40',
|
||||
status: 'executing', phaseNum: '2', phaseTotal: '5',
|
||||
};
|
||||
const full = formatGsdState(state);
|
||||
const compact = formatGsdStateCompact(state);
|
||||
assert.ok(full.includes('Code Quality'), `full keeps name; got: ${full}`);
|
||||
assert.ok(!compact.includes('Code Quality'), `compact drops name; got: ${compact}`);
|
||||
assert.ok(!compact.includes('█'), `compact drops bar; got: ${compact}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state_format via renderStatusline', () => {
|
||||
function makeProject(stateFormat) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'state-fmt-'));
|
||||
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
||||
if (stateFormat !== undefined) {
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.planning', 'config.json'),
|
||||
JSON.stringify({ statusline: { state_format: stateFormat } }),
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(path.join(dir, '.planning', 'STATE.md'), [
|
||||
'---',
|
||||
'milestone: v1.9',
|
||||
'milestone_name: Code Quality',
|
||||
'status: executing',
|
||||
'---',
|
||||
'',
|
||||
'Phase: 2 of 5 (parser-rewrite)',
|
||||
'',
|
||||
].join('\n'));
|
||||
return dir;
|
||||
}
|
||||
|
||||
test('compact format drops the milestone name', () => {
|
||||
const dir = makeProject('compact');
|
||||
try {
|
||||
const out = statusline.renderStatusline({
|
||||
model: { display_name: 'Claude' },
|
||||
workspace: { current_dir: dir },
|
||||
});
|
||||
assert.ok(out.includes('v1.9 · P2/5 · executing'), `expected compact state; got: ${out}`);
|
||||
assert.ok(!out.includes('Code Quality'), `expected no milestone name; got: ${out}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('default (key absent) keeps the full format unchanged', () => {
|
||||
const dir = makeProject(undefined);
|
||||
try {
|
||||
const out = statusline.renderStatusline({
|
||||
model: { display_name: 'Claude' },
|
||||
workspace: { current_dir: dir },
|
||||
});
|
||||
assert.ok(out.includes('Code Quality'), `expected full format; got: ${out}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('explicit "full" matches the default rendering', () => {
|
||||
const dirDefault = makeProject(undefined);
|
||||
const dirFull = makeProject('full');
|
||||
try {
|
||||
const input = (dir) => ({
|
||||
model: { display_name: 'Claude' },
|
||||
workspace: { current_dir: dir },
|
||||
});
|
||||
const a = statusline.renderStatusline(input(dirDefault));
|
||||
const b = statusline.renderStatusline(input(dirFull));
|
||||
// Same STATE.md content → same rendered middle segment (the trailing
|
||||
// directory basename differs per temp dir, so compare with it removed)
|
||||
assert.equal(
|
||||
a.replace(path.basename(dirDefault), ''),
|
||||
b.replace(path.basename(dirFull), ''));
|
||||
} finally {
|
||||
cleanup(dirDefault);
|
||||
cleanup(dirFull);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Compact 1M model badge
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user