Merge pull request #2183 from arakasi1/feat/2163-statusline-git-segment
feat(#2163): opt-in git branch/status segment in the statusline
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "9e64af477e774de6",
|
||||
"hooks/gsd-statusline.js": "25996df685a0dac9",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "3e633cd082663b99",
|
||||
"hooks/gsd-statusline.js": "2fab68f4fd190331",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "920558a55eb86587",
|
||||
"hooks/gsd-statusline.js": "4ad7c2f59577c5bc",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "920558a55eb86587",
|
||||
"hooks/gsd-statusline.js": "4ad7c2f59577c5bc",
|
||||
"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": "a84914f7cab74332",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "6ab2905537f885ba",
|
||||
"hooks/gsd-statusline.js": "29cdce15038d1ab1",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "600e9ed0cb6ed7bb",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "db16ded31dd6eecf",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "b3f927ee4b4c4711",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "34d2ae303558b917",
|
||||
"hooks/gsd-statusline.js": "884347ccd6549f29",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "ae0c0e7a934c5deb",
|
||||
".kimi/hooks/gsd-statusline.js": "be35341758d50fa5",
|
||||
".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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "3dd40389fa786dd6",
|
||||
"hooks/gsd-statusline.js": "6fcb59ad86d2d0ea",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "41b0080cf0f3e7e9",
|
||||
"hooks/gsd-statusline.js": "daa1a98fde95ccaf",
|
||||
"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": "6454021dec4d9563",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "5686cf31c721b2b1",
|
||||
"hooks/gsd-statusline.js": "2541196849ec5ffc",
|
||||
"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": "c28c9076058c5e73",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "568e7c6bdd415c1a",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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": "37fb6081fcb18038",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "ef818b1afd7ee8e4",
|
||||
"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",
|
||||
|
||||
196
tests/gsd-statusline-git.property.test.cjs
Normal file
196
tests/gsd-statusline-git.property.test.cjs
Normal file
@@ -0,0 +1,196 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Property-based tests for the statusline git-segment parser (#2163)
|
||||
*
|
||||
* Module: hooks/gsd-statusline.js
|
||||
* Exported: parseGitStatus(text), buildGitSegment(info)
|
||||
*
|
||||
* Properties tested:
|
||||
* (a) parseGitStatus: never throws on any input; returns null or the full
|
||||
* typed shape with non-negative counts (total robustness)
|
||||
* (b) round-trip: synthetic porcelain-v2 output for a generated repo state
|
||||
* parses back to exactly that state
|
||||
* (c) prefix-robustness: unknown-prefix lines (future porcelain extensions)
|
||||
* inserted anywhere leave the parsed result unchanged
|
||||
* (d) no-branch-header: any input lacking '# branch.head' parses to null
|
||||
* (e) buildGitSegment: never throws; '' iff branch absent; ✓ iff clean+synced
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
|
||||
const { parseGitStatus, buildGitSegment } = require('../hooks/gsd-statusline.js');
|
||||
|
||||
const INFO_KEYS = ['branch', 'ahead', 'behind', 'staged', 'unstaged', 'untracked'];
|
||||
const COUNT_KEYS = ['ahead', 'behind', 'staged', 'unstaged', 'untracked'];
|
||||
|
||||
// Branch names from a path-safe alphabet (no whitespace/control chars, which
|
||||
// git itself forbids in ref names).
|
||||
const arbBranch = fc.stringMatching(/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,40}$/);
|
||||
|
||||
// A generated repo state with bounded counts.
|
||||
const arbState = fc.record({
|
||||
branch: arbBranch,
|
||||
ahead: fc.nat({ max: 99 }),
|
||||
behind: fc.nat({ max: 99 }),
|
||||
staged: fc.nat({ max: 20 }),
|
||||
unstagedChanged: fc.nat({ max: 20 }),
|
||||
unmerged: fc.nat({ max: 5 }),
|
||||
untracked: fc.nat({ max: 20 }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Render synthetic porcelain v2 --branch output for a generated state.
|
||||
* Staged entries get XY 'M.', unstaged 'M' entries '.M', unmerged 'u UU'.
|
||||
*/
|
||||
function renderPorcelain(s) {
|
||||
const lines = [
|
||||
'# branch.oid 0123456789abcdef0123456789abcdef01234567',
|
||||
`# branch.head ${s.branch}`,
|
||||
`# branch.upstream origin/${s.branch}`,
|
||||
`# branch.ab +${s.ahead} -${s.behind}`,
|
||||
];
|
||||
for (let i = 0; i < s.staged; i++) {
|
||||
lines.push(`1 M. N... 100644 100644 100644 0123456 0123456 staged-${i}.txt`);
|
||||
}
|
||||
for (let i = 0; i < s.unstagedChanged; i++) {
|
||||
lines.push(`1 .M N... 100644 100644 100644 0123456 0123456 unstaged-${i}.txt`);
|
||||
}
|
||||
for (let i = 0; i < s.unmerged; i++) {
|
||||
lines.push(`u UU N... 100644 100644 100644 100644 0123456 0123456 0123456 conflict-${i}.txt`);
|
||||
}
|
||||
for (let i = 0; i < s.untracked; i++) {
|
||||
lines.push(`? new-${i}.txt`);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
describe('gsd-statusline git segment: parseGitStatus properties', () => {
|
||||
// (a) Total: never throws, result is null or the full typed shape
|
||||
test('property: never throws and returns null or a well-formed info object', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.oneof(
|
||||
fc.constant(null),
|
||||
fc.constant(undefined),
|
||||
fc.constant(42),
|
||||
fc.constant({}),
|
||||
fc.string({ unit: 'binary', maxLength: 300 }),
|
||||
fc.string({ unit: 'grapheme-composite', maxLength: 300 }),
|
||||
// line soup: random lines joined with \n
|
||||
fc.array(fc.string({ maxLength: 60 }), { maxLength: 30 }).map((a) => a.join('\n'))
|
||||
),
|
||||
(input) => {
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = parseGitStatus(input); });
|
||||
if (result !== null) {
|
||||
assert.deepEqual(Object.keys(result).sort(), [...INFO_KEYS].sort());
|
||||
for (const k of COUNT_KEYS) {
|
||||
assert.equal(typeof result[k], 'number');
|
||||
assert.ok(result[k] >= 0, `${k} must be >= 0`);
|
||||
}
|
||||
assert.equal(typeof result.branch, 'string');
|
||||
assert.ok(result.branch.length > 0);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// (b) Round-trip: synthetic porcelain for a state parses back exactly
|
||||
test('property: round-trip through synthetic porcelain v2 output', () => {
|
||||
fc.assert(
|
||||
fc.property(arbState, (s) => {
|
||||
const parsed = parseGitStatus(renderPorcelain(s));
|
||||
assert.ok(parsed, 'well-formed porcelain must parse');
|
||||
assert.equal(parsed.branch, s.branch);
|
||||
assert.equal(parsed.ahead, s.ahead);
|
||||
assert.equal(parsed.behind, s.behind);
|
||||
assert.equal(parsed.staged, s.staged);
|
||||
// unmerged (conflict) entries count as unstaged
|
||||
assert.equal(parsed.unstaged, s.unstagedChanged + s.unmerged);
|
||||
assert.equal(parsed.untracked, s.untracked);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// (c) Prefix-robustness: unknown-prefix lines never change the result
|
||||
test('property: unknown-prefix lines inserted anywhere are ignored', () => {
|
||||
// Lines whose first two chars are none of the recognized prefixes
|
||||
// ('# ', '1 ', '2 ', 'u ', '? ').
|
||||
const arbUnknownLine = fc
|
||||
.string({ maxLength: 50 })
|
||||
.map((s) => `z ${s}`);
|
||||
fc.assert(
|
||||
fc.property(
|
||||
arbState,
|
||||
fc.array(arbUnknownLine, { minLength: 1, maxLength: 10 }),
|
||||
fc.nat({ max: 1000 }),
|
||||
(s, extras, seedPos) => {
|
||||
const baseline = parseGitStatus(renderPorcelain(s));
|
||||
const lines = renderPorcelain(s).split('\n');
|
||||
// deterministic insertion positions derived from seedPos
|
||||
extras.forEach((extra, i) => {
|
||||
const pos = (seedPos + i * 7) % (lines.length + 1);
|
||||
lines.splice(pos, 0, extra);
|
||||
});
|
||||
const withExtras = parseGitStatus(lines.join('\n'));
|
||||
assert.deepEqual(withExtras, baseline);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// (d) No-branch-header: input without '# branch.head' parses to null
|
||||
test('property: input lacking a branch.head header parses to null', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(fc.string({ maxLength: 60 }), { maxLength: 30 }),
|
||||
(rawLines) => {
|
||||
const text = rawLines
|
||||
.filter((l) => !l.startsWith('# branch.head '))
|
||||
.join('\n');
|
||||
assert.equal(parseGitStatus(text), null);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-statusline git segment: buildGitSegment properties', () => {
|
||||
// (e) Never throws; '' iff no branch; clean+synced renders the ✓ marker
|
||||
test('property: never throws and is empty exactly when branch is absent', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.oneof(
|
||||
fc.constant(null),
|
||||
fc.constant(undefined),
|
||||
fc.constant({}),
|
||||
arbState.map((s) => ({
|
||||
branch: s.branch,
|
||||
ahead: s.ahead,
|
||||
behind: s.behind,
|
||||
staged: s.staged,
|
||||
unstaged: s.unstagedChanged + s.unmerged,
|
||||
untracked: s.untracked,
|
||||
}))
|
||||
),
|
||||
(info) => {
|
||||
let seg;
|
||||
assert.doesNotThrow(() => { seg = buildGitSegment(info); });
|
||||
assert.equal(typeof seg, 'string');
|
||||
if (!info || !info.branch) {
|
||||
assert.equal(seg, '');
|
||||
} else {
|
||||
assert.ok(seg.includes(info.branch));
|
||||
const dirty =
|
||||
info.staged || info.unstaged || info.untracked || info.ahead || info.behind;
|
||||
assert.equal(seg.includes('✓'), !dirty);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1363,3 +1363,280 @@ test('config-set statusline.show_context_tokens yes → rejected', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Git segment (statusline.show_git)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
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 { execFileSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const statusline = require('../hooks/gsd-statusline.js');
|
||||
const { parseGitStatus, buildGitSegment, readGitStatus, composeStatusline } = statusline;
|
||||
const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config-schema.cjs');
|
||||
|
||||
describe('config schema: statusline.show_git', () => {
|
||||
test('registers statusline.show_git', () => {
|
||||
assert.ok(
|
||||
VALID_CONFIG_KEYS.has('statusline.show_git'),
|
||||
'statusline.show_git must be in VALID_CONFIG_KEYS',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseGitStatus', () => {
|
||||
test('returns null for non-string / missing branch header', () => {
|
||||
assert.equal(parseGitStatus(null), null);
|
||||
assert.equal(parseGitStatus(undefined), null);
|
||||
assert.equal(parseGitStatus(''), null);
|
||||
assert.equal(parseGitStatus('? some-file\n'), null);
|
||||
});
|
||||
|
||||
test('parses a clean, in-sync branch', () => {
|
||||
const text = [
|
||||
'# branch.oid abc123',
|
||||
'# branch.head main',
|
||||
'# branch.upstream origin/main',
|
||||
'# branch.ab +0 -0',
|
||||
'',
|
||||
].join('\n');
|
||||
assert.deepEqual(parseGitStatus(text), {
|
||||
branch: 'main', ahead: 0, behind: 0, staged: 0, unstaged: 0, untracked: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('counts staged, unstaged, untracked, ahead, behind', () => {
|
||||
const text = [
|
||||
'# branch.oid abc123',
|
||||
'# branch.head feat/x',
|
||||
'# branch.upstream origin/feat/x',
|
||||
'# branch.ab +2 -1',
|
||||
'1 M. N... 100644 100644 100644 aaa bbb staged-only.txt',
|
||||
'1 .M N... 100644 100644 100644 aaa bbb unstaged-only.txt',
|
||||
'1 MM N... 100644 100644 100644 aaa bbb both.txt',
|
||||
'2 R. N... 100644 100644 100644 aaa bbb R100 new.txt\told.txt',
|
||||
'? untracked-1.txt',
|
||||
'? untracked-2.txt',
|
||||
'',
|
||||
].join('\n');
|
||||
assert.deepEqual(parseGitStatus(text), {
|
||||
branch: 'feat/x', ahead: 2, behind: 1, staged: 3, unstaged: 2, untracked: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('counts unmerged (conflict) entries as unstaged', () => {
|
||||
const text = [
|
||||
'# branch.head main',
|
||||
'u UU N... 100644 100644 100644 100644 aaa bbb ccc conflict.txt',
|
||||
'',
|
||||
].join('\n');
|
||||
const info = parseGitStatus(text);
|
||||
assert.equal(info.unstaged, 1);
|
||||
assert.equal(info.staged, 0);
|
||||
});
|
||||
|
||||
test('detached HEAD passes through as "(detached)"', () => {
|
||||
const text = '# branch.head (detached)\n';
|
||||
assert.equal(parseGitStatus(text).branch, '(detached)');
|
||||
});
|
||||
|
||||
test('no upstream (no branch.ab line) leaves ahead/behind at 0', () => {
|
||||
const text = '# branch.head local-only\n? new.txt\n';
|
||||
const info = parseGitStatus(text);
|
||||
assert.deepEqual([info.ahead, info.behind, info.untracked], [0, 0, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildGitSegment', () => {
|
||||
const strip = (s) =>
|
||||
// eslint-disable-next-line no-control-regex -- stripping ANSI SGR sequences to assert on visible text
|
||||
s.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
|
||||
test('returns empty string for null info', () => {
|
||||
assert.equal(buildGitSegment(null), '');
|
||||
assert.equal(buildGitSegment({}), '');
|
||||
});
|
||||
|
||||
test('clean repo renders branch with a check mark', () => {
|
||||
const seg = buildGitSegment({ branch: 'main', ahead: 0, behind: 0, staged: 0, unstaged: 0, untracked: 0 });
|
||||
assert.equal(strip(seg), ' │ main✓');
|
||||
});
|
||||
|
||||
test('dirty repo renders each nonzero marker in order', () => {
|
||||
const seg = buildGitSegment({ branch: 'feat/x', ahead: 2, behind: 1, staged: 3, unstaged: 2, untracked: 4 });
|
||||
assert.equal(strip(seg), ' │ feat/x+3~2?4↑2↓1');
|
||||
});
|
||||
|
||||
test('omits zero markers', () => {
|
||||
const seg = buildGitSegment({ branch: 'main', ahead: 1, behind: 0, staged: 0, unstaged: 0, untracked: 0 });
|
||||
assert.equal(strip(seg), ' │ main↑1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readGitStatus + parseGitStatus against a real repo', () => {
|
||||
function makeGitRepo() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-seg-'));
|
||||
const run = (args) => execFileSync('git', ['-C', dir, ...args], {
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' },
|
||||
});
|
||||
run(['init', '-q', '-b', 'main']);
|
||||
run(['config', 'user.email', 'test@test.invalid']);
|
||||
run(['config', 'user.name', 'Test']);
|
||||
return { dir, run };
|
||||
}
|
||||
|
||||
test('non-repo directory yields null', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-seg-plain-'));
|
||||
try {
|
||||
assert.equal(parseGitStatus(readGitStatus(dir)), null);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// Deterministic IO-failure injection (repo convention, cf. the fs
|
||||
// monkeypatch in ensure-runtime-build.test.cjs): readGitStatus shares the
|
||||
// one cached child_process module object, so replacing execFileSync here
|
||||
// injects the failure without a real hang or oversized repo.
|
||||
test('maxBuffer overflow degrades to null (segment absent)', () => {
|
||||
const childProcess = require('node:child_process');
|
||||
const original = childProcess.execFileSync;
|
||||
childProcess.execFileSync = () => {
|
||||
const err = new RangeError('stdout maxBuffer length exceeded');
|
||||
err.code = 'ERR_CHILD_PROCESS_STDOUT_MAXBUFFER';
|
||||
throw err;
|
||||
};
|
||||
try {
|
||||
assert.equal(readGitStatus('/tmp'), null);
|
||||
} finally {
|
||||
childProcess.execFileSync = original;
|
||||
}
|
||||
});
|
||||
|
||||
test('spawn timeout degrades to null (segment absent)', () => {
|
||||
const childProcess = require('node:child_process');
|
||||
const original = childProcess.execFileSync;
|
||||
childProcess.execFileSync = () => {
|
||||
const err = new Error('spawnSync git ETIMEDOUT');
|
||||
err.code = 'ETIMEDOUT';
|
||||
err.errno = -110;
|
||||
throw err;
|
||||
};
|
||||
try {
|
||||
assert.equal(readGitStatus('/tmp'), null);
|
||||
} finally {
|
||||
childProcess.execFileSync = original;
|
||||
}
|
||||
});
|
||||
|
||||
test('fresh repo with an untracked file is counted', () => {
|
||||
const { dir } = makeGitRepo();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'new.txt'), 'hello');
|
||||
const info = parseGitStatus(readGitStatus(dir));
|
||||
assert.equal(info.branch, 'main');
|
||||
assert.equal(info.untracked, 1);
|
||||
assert.equal(info.staged, 0);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('staged and committed states are reflected', () => {
|
||||
const { dir, run } = makeGitRepo();
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'a.txt'), '1');
|
||||
run(['add', 'a.txt']);
|
||||
let info = parseGitStatus(readGitStatus(dir));
|
||||
assert.equal(info.staged, 1);
|
||||
run(['commit', '-q', '-m', 'init']);
|
||||
info = parseGitStatus(readGitStatus(dir));
|
||||
assert.deepEqual(
|
||||
[info.staged, info.unstaged, info.untracked], [0, 0, 0]);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('composeStatusline gitSuffix placement', () => {
|
||||
test('git segment renders after the directory in end layout', () => {
|
||||
const out = composeStatusline({
|
||||
model: 'Claude', dirname: 'proj',
|
||||
gitSuffix: ' │ main✓', ctx: ' CTX', lastCmdSuffix: ' │ last: /foo',
|
||||
});
|
||||
assert.ok(
|
||||
out.includes('proj\x1b[0m │ main✓ CTX │ last: /foo'),
|
||||
`expected dir → git → ctx → last-cmd order; got: ${out}`,
|
||||
);
|
||||
});
|
||||
test('git segment renders after the directory in front layout', () => {
|
||||
const out = composeStatusline({
|
||||
model: 'Claude', dirname: 'proj',
|
||||
gitSuffix: ' │ main✓', position: 'front',
|
||||
});
|
||||
assert.ok(out.endsWith('proj\x1b[0m │ main✓'), `got: ${out}`);
|
||||
});
|
||||
test('default (no gitSuffix) output is unchanged', () => {
|
||||
const a = composeStatusline({ model: 'Claude', dirname: 'proj' });
|
||||
const b = composeStatusline({ model: 'Claude', dirname: 'proj', gitSuffix: '' });
|
||||
assert.equal(a, b);
|
||||
});
|
||||
});
|
||||
|
||||
describe('show_git e2e through the hook', () => {
|
||||
const hookPath = path.join(__dirname, '..', 'hooks', 'gsd-statusline.js');
|
||||
|
||||
function runHook(dir) {
|
||||
const payload = JSON.stringify({
|
||||
model: { display_name: 'Claude' },
|
||||
workspace: { current_dir: dir },
|
||||
session_id: `test-git-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
});
|
||||
let stdout = '';
|
||||
try {
|
||||
stdout = execFileSync(process.execPath, [hookPath], {
|
||||
input: payload, encoding: 'utf8', timeout: 4000,
|
||||
});
|
||||
} catch (e) {
|
||||
stdout = e.stdout || '';
|
||||
}
|
||||
// eslint-disable-next-line no-control-regex -- stripping ANSI SGR sequences from captured CLI output
|
||||
return stdout.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
}
|
||||
|
||||
test('flag=true renders the branch segment', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-seg-e2e-'));
|
||||
try {
|
||||
execFileSync('git', ['-C', dir, 'init', '-q', '-b', 'main']);
|
||||
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.planning', 'config.json'),
|
||||
JSON.stringify({ statusline: { show_git: true } }),
|
||||
);
|
||||
const out = runHook(dir);
|
||||
assert.ok(out.includes('│ main'), `expected branch segment; got: ${out}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('default (flag absent) has no git segment', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'git-seg-e2e-'));
|
||||
try {
|
||||
execFileSync('git', ['-C', dir, 'init', '-q', '-b', 'main']);
|
||||
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
|
||||
const out = runHook(dir);
|
||||
assert.ok(!out.includes('│ main'), `expected no git segment; got: ${out}`);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user