pi-usereq 0.33.0 → 0.35.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.
@@ -475,10 +475,39 @@ function buildExpectedFakeContextBar(options: {
475
475
  return formatFakeThemeForeground("warning", "▕█▏");
476
476
  }
477
477
 
478
+ /**
479
+ * @brief Reads the repository package manifest fields used by status-bar tests.
480
+ * @details Loads `../package.json` relative to this test module so status-bar assertions derive the expected runtime extension name and version from repository metadata instead of hard-coded literals. Runtime is O(n) in file size. Side effects are limited to one filesystem read.
481
+ * @return {{ name: string; version: string }} Expected runtime extension package metadata.
482
+ */
483
+ function readExpectedRuntimeExtensionMetadata(): { name: string; version: string } {
484
+ const packageJson = JSON.parse(
485
+ fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
486
+ ) as { name?: unknown; version?: unknown };
487
+ return {
488
+ name: typeof packageJson.name === "string" && packageJson.name.trim() !== ""
489
+ ? packageJson.name.trim()
490
+ : "pi-usereq",
491
+ version: typeof packageJson.version === "string" && packageJson.version.trim() !== ""
492
+ ? packageJson.version.trim()
493
+ : "unknown",
494
+ };
495
+ }
496
+
497
+ /**
498
+ * @brief Builds the expected fake runtime extension identity prefix for status assertions.
499
+ * @details Renders `<extension>:v<version>` using the same accent-label and warning-value markers consumed by the fake status theme so prefix assertions remain aligned with runtime package metadata. Runtime is O(n) in rendered text length. No external state is mutated.
500
+ * @return {string} Encoded extension identity prefix.
501
+ */
502
+ function buildExpectedFakeExtensionIdentityStatusField(): string {
503
+ const metadata = readExpectedRuntimeExtensionMetadata();
504
+ return `${formatFakeThemeForeground("accent", `${metadata.name}:`)}${formatFakeThemeForeground("warning", `v${metadata.version}`)}`;
505
+ }
506
+
478
507
  /**
479
508
  * @brief Builds the expected fake pi-usereq status-bar string for assertions.
480
- * @details Reconstructs the field order, workflow-state highlighting, branch field, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1) plus optional git execution for branch discovery. No external state is mutated.
481
- * @param[in] options {{ workflowState?: string; basePath?: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts for rendered `status`, `branch`, `context`, `elapsed`, and `sound` fields.
509
+ * @details Reconstructs the runtime extension identity prefix, workflow-state highlighting, branch field, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1) plus optional git execution for branch discovery. No external state is mutated.
510
+ * @param[in] options {{ workflowState?: string; basePath?: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts for rendered runtime extension prefix, `status`, `branch`, `context`, `elapsed`, and `sound` fields.
482
511
  * @return {string} Encoded status-bar string.
483
512
  */
484
513
  function buildExpectedFakeStatusText(options: {
@@ -504,6 +533,7 @@ function buildExpectedFakeStatusText(options: {
504
533
  percent: options.contextPercent,
505
534
  });
506
535
  return [
536
+ buildExpectedFakeExtensionIdentityStatusField(),
507
537
  `${formatFakeThemeForeground("accent", "status:")}${workflowStateValue}`,
508
538
  buildField("branch", branchValue),
509
539
  `${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
@@ -842,6 +872,7 @@ test("extension registers prompt and config commands while exposing tool capabil
842
872
  "debug-compress",
843
873
  "debug-references",
844
874
  "debug-static-check",
875
+ "debug-summarize",
845
876
  "debug-tokens",
846
877
  "pi-usereq-show-config",
847
878
  "test-static-check",
@@ -869,7 +900,7 @@ test("extension registers prompt and config commands while exposing tool capabil
869
900
 
870
901
  /**
871
902
  * @brief Verifies debug tool wrapper commands register only when the local debug flag is enabled.
872
- * @details Activates the extension once from a default-disabled fixture and once from a fixture whose local config enables debug tool wrapper commands, then asserts the four debug slash commands appear only in the enabled runtime. Runtime is dominated by fixture setup and extension registration. Side effects are limited to temporary filesystem mutation and process cwd changes.
903
+ * @details Activates the extension once from a default-disabled fixture and once from a fixture whose local config enables debug tool wrapper commands, then asserts the five debug slash commands appear only in the enabled runtime. Runtime is dominated by fixture setup and extension registration. Side effects are limited to temporary filesystem mutation and process cwd changes.
873
904
  * @return {void} No return value.
874
905
  * @throws {AssertionError} Throws when command registration ignores the local debug tool wrapper flag.
875
906
  * @satisfies TST-115
@@ -881,6 +912,7 @@ test("debug tool commands register only when enabled in local config", () => {
881
912
  "debug-compress",
882
913
  "debug-references",
883
914
  "debug-static-check",
915
+ "debug-summarize",
884
916
  "debug-tokens",
885
917
  ];
886
918
 
@@ -1606,7 +1638,7 @@ test("static-check tools return monolithic text payloads with minimal execution
1606
1638
 
1607
1639
  /**
1608
1640
  * @brief Verifies debug tool wrapper commands mirror tool content and reject when disabled.
1609
- * @details Enables debug tool wrapper commands for one fixture project, compares each debug slash-command editor payload against the matching tool `content[0].text`, then disables the flag and verifies the already-registered wrapper rejects further execution. Runtime is dominated by tool execution plus temporary git fixture setup. Side effects are limited to temporary filesystem mutation, editor-text capture, and process cwd changes.
1641
+ * @details Enables debug tool wrapper commands for one fixture project, compares the five debug slash-command editor payloads against the matching tool `content[0].text`, then disables the flag and verifies the already-registered wrappers reject further execution. Runtime is dominated by tool execution plus temporary git fixture setup. Side effects are limited to temporary filesystem mutation, editor-text capture, and process cwd changes.
1610
1642
  * @return {Promise<void>} Promise resolved when the verification completes.
1611
1643
  * @throws {AssertionError} Throws when a debug wrapper output diverges from the wrapped tool text or disabled execution is accepted.
1612
1644
  * @satisfies TST-116
@@ -1643,6 +1675,9 @@ test("debug tool wrapper commands mirror tool content and reject when disabled",
1643
1675
  const staticCheckToolResult = await executeRegisteredTool(pi, "static-check", projectBase) as {
1644
1676
  content?: Array<{ type: string; text?: string }>;
1645
1677
  };
1678
+ const summarizeToolResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
1679
+ content?: Array<{ type: string; text?: string }>;
1680
+ };
1646
1681
  const tokensToolResult = await executeRegisteredTool(pi, "tokens", projectBase, {}) as {
1647
1682
  content?: Array<{ type: string; text?: string }>;
1648
1683
  };
@@ -1659,6 +1694,10 @@ test("debug tool wrapper commands mirror tool content and reject when disabled",
1659
1694
  await pi.commands.get("debug-static-check")!.handler("", staticCheckCtx);
1660
1695
  assert.equal(staticCheckCtx.__state.editorText, staticCheckToolResult.content?.[0]?.text ?? "");
1661
1696
 
1697
+ const summarizeCtx = createFakeCtx(projectBase);
1698
+ await pi.commands.get("debug-summarize")!.handler("", summarizeCtx);
1699
+ assert.equal(summarizeCtx.__state.editorText, summarizeToolResult.content?.[0]?.text ?? "");
1700
+
1662
1701
  const tokensCtx = createFakeCtx(projectBase);
1663
1702
  await pi.commands.get("debug-tokens")!.handler("", tokensCtx);
1664
1703
  assert.equal(tokensCtx.__state.editorText, tokensToolResult.content?.[0]?.text ?? "");
@@ -1666,12 +1705,20 @@ test("debug tool wrapper commands mirror tool content and reject when disabled",
1666
1705
  writeProjectConfigOverrides(projectBase, {
1667
1706
  DEBUG_TOOL_COMMANDS_ENABLED: "disable",
1668
1707
  });
1669
- const disabledCtx = createFakeCtx(projectBase);
1670
- await assert.rejects(
1671
- async () => pi.commands.get("debug-tokens")!.handler("", disabledCtx),
1672
- /disabled by Debug > Enable debug commands for tools/u,
1673
- );
1674
- assert.equal(disabledCtx.__state.editorText, "");
1708
+ for (const commandName of [
1709
+ "debug-compress",
1710
+ "debug-references",
1711
+ "debug-static-check",
1712
+ "debug-summarize",
1713
+ "debug-tokens",
1714
+ ]) {
1715
+ const disabledCtx = createFakeCtx(projectBase);
1716
+ await assert.rejects(
1717
+ async () => pi.commands.get(commandName)!.handler("", disabledCtx),
1718
+ /disabled by Debug > Enable debug commands for tools/u,
1719
+ );
1720
+ assert.equal(disabledCtx.__state.editorText, "");
1721
+ }
1675
1722
  } finally {
1676
1723
  process.chdir(previousCwd);
1677
1724
  fs.rmSync(projectBase, { recursive: true, force: true });
@@ -1968,6 +2015,10 @@ test("debug menu dims locked rows and persists debug settings with focus-preserv
1968
2015
  const debugCalls = ctx.__state.selectCalls.filter((call) => call.title === "Debug");
1969
2016
  assert.match(renderedInitialDebugMenu, /<dim>Log file/);
1970
2017
  assert.match(renderedInitialDebugMenu, /<dim>Enable debug commands for tools/);
2018
+ assert.ok(
2019
+ renderedInitialDebugMenu.indexOf("<dim>Enable debug commands for tools")
2020
+ < renderedInitialDebugMenu.indexOf("<dim>Log file"),
2021
+ );
1971
2022
  assert.match(renderedInitialDebugMenu, /<dim>Log on status/);
1972
2023
  assert.match(renderedInitialDebugMenu, /<dim>Status changes/);
1973
2024
  assert.match(renderedInitialDebugMenu, /<dim>Workflow events/);
@@ -1981,6 +2032,7 @@ test("debug menu dims locked rows and persists debug settings with focus-preserv
1981
2032
  assert.ok(pi.commands.get("debug-compress"));
1982
2033
  assert.ok(pi.commands.get("debug-references"));
1983
2034
  assert.ok(pi.commands.get("debug-static-check"));
2035
+ assert.ok(pi.commands.get("debug-summarize"));
1984
2036
  assert.ok(pi.commands.get("debug-tokens"));
1985
2037
  assert.deepEqual(persistedConfig.DEBUG_ENABLED_TOOLS, ["files-tokens"]);
1986
2038
  assert.deepEqual(persistedConfig.DEBUG_ENABLED_PROMPTS, ["req-analyze"]);
@@ -2023,8 +2075,8 @@ test("debug menu rows derive from canonical tool and prompt inventories", async
2023
2075
 
2024
2076
  assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
2025
2077
  "Debug",
2026
- "Log file",
2027
2078
  "Enable debug commands for tools",
2079
+ "Log file",
2028
2080
  "Log on status",
2029
2081
  "Status changes",
2030
2082
  "Workflow events",
@@ -5003,6 +5055,27 @@ test("session_start applies configured pi-usereq startup tools", async () => {
5003
5055
  );
5004
5056
  });
5005
5057
 
5058
+ /**
5059
+ * @brief Verifies the status bar prepends runtime extension identity before the `status` field.
5060
+ * @details Emits `session_start`, reads the rendered footer text, and asserts the prefix starts with the runtime package name plus `v<version>` before `status:` using the same accent-label and warning-value markers used by other non-error status fields. Runtime is O(1). Side effects are limited to temporary filesystem mutation and in-memory status capture.
5061
+ * @return {Promise<void>} Promise resolved after the status-prefix assertion completes.
5062
+ * @throws {AssertionError} Throws when the footer omits or misorders the runtime extension identity prefix.
5063
+ * @satisfies TST-031, TST-033, TST-049
5064
+ */
5065
+ test("session_start status bar prepends runtime extension identity before status", async () => {
5066
+ const cwd = createTempDir("pi-usereq-extension-identity-status-");
5067
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
5068
+ const pi = createFakePi();
5069
+ piUsereqExtension(pi);
5070
+ const ctx = createFakeCtx(cwd);
5071
+
5072
+ await pi.emit("session_start", { reason: "startup" }, ctx);
5073
+
5074
+ const statusText = ctx.__state.statuses.get("pi-usereq") ?? "";
5075
+ const expectedPrefix = `${buildExpectedFakeExtensionIdentityStatusField()}${formatFakeThemeForeground("dim", " • ")}${formatFakeThemeForeground("accent", "status:")}`;
5076
+ assert.ok(statusText.startsWith(expectedPrefix));
5077
+ });
5078
+
5006
5079
  test("session_start renders the 0-percent context icon when context usage is empty", async () => {
5007
5080
  const cwd = createTempDir("pi-usereq-context-empty-status-");
5008
5081
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1,4 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import test from "node:test";
@@ -16,6 +17,61 @@ function readReleaseWorkflow(): string {
16
17
  );
17
18
  }
18
19
 
20
+ /**
21
+ * @brief Verifies the release workflow YAML parses successfully via Python.
22
+ * @details Resolves a project-local `.venv/bin/python` first, then falls back to `python` and `python3`, executes an inline `yaml.safe_load(...)` check against `.github/workflows/release-npm.yml`, accepts the `PyYAML not available` skip path, and fails on non-zero exit status or `YAML ERROR` output. Runtime is dominated by subprocess probing and one Python execution. Side effects are limited to subprocess creation and filesystem reads performed by the Python snippet.
23
+ * @return {void} No return value.
24
+ * @throws {AssertionError} Throws when no usable Python interpreter exists or the workflow YAML fails to parse.
25
+ * @satisfies TST-039
26
+ */
27
+ function assertReleaseWorkflowYamlSyntax(): void {
28
+ const workflowPath = path.join(
29
+ process.cwd(),
30
+ ".github",
31
+ "workflows",
32
+ "release-npm.yml",
33
+ );
34
+ const pythonCandidates = [
35
+ path.join(process.cwd(), ".venv", "bin", "python"),
36
+ "python",
37
+ "python3",
38
+ ];
39
+ const pythonCommand = pythonCandidates.find((candidate) => {
40
+ if (candidate.includes(path.sep) && !fs.existsSync(candidate)) {
41
+ return false;
42
+ }
43
+ const probe = spawnSync(candidate, ["--version"], { encoding: "utf8" });
44
+ return !probe.error && probe.status === 0;
45
+ });
46
+ assert.ok(pythonCommand, "python executable not found for release workflow validation");
47
+
48
+ const pythonCode = [
49
+ "from pathlib import Path",
50
+ "import sys",
51
+ `p = Path(${JSON.stringify(workflowPath)})`,
52
+ "text = p.read_text()",
53
+ "try:",
54
+ " import yaml",
55
+ "except Exception as error:",
56
+ " print(f'PyYAML not available: {error}')",
57
+ " sys.exit(0)",
58
+ "try:",
59
+ " yaml.safe_load(text)",
60
+ " print('YAML OK')",
61
+ "except Exception as error:",
62
+ " print(f'YAML ERROR: {error}')",
63
+ " sys.exit(1)",
64
+ ].join("\n");
65
+ const result = spawnSync(pythonCommand, ["-c", pythonCode], {
66
+ cwd: process.cwd(),
67
+ encoding: "utf8",
68
+ });
69
+ const output = `${result.stdout}${result.stderr}`;
70
+
71
+ assert.equal(result.status, 0, output || result.error?.message || "unknown python execution failure");
72
+ assert.doesNotMatch(output, /YAML ERROR:/u);
73
+ }
74
+
19
75
  /**
20
76
  * @brief Describes the package manifest fields asserted by npm release tests.
21
77
  * @details Constrains the parsed `package.json` shape to the publication identity and provenance metadata required by the release workflow tests. The interface is compile-time only and introduces no runtime side effects.
@@ -75,6 +131,11 @@ function assertCanonicalPackageProvenanceMetadata(): void {
75
131
  assert.equal(manifest.homepage, "https://github.com/Ogekuri/PI-useReq#readme");
76
132
  }
77
133
 
134
+ test(
135
+ "release workflow remains valid YAML when parsed by Python",
136
+ assertReleaseWorkflowYamlSyntax,
137
+ );
138
+
78
139
  test(
79
140
  "release workflow keeps the current tag trigger and gates release work on origin/master ancestry",
80
141
  () => {