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.
- package/.g.conf +0 -4
- package/.github/workflows/release-npm.yml +4 -2
- package/CHANGELOG.md +21 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +370 -350
- package/pi-usereq/docs/REQUIREMENTS.md +12 -12
- package/pi-usereq/docs/WORKFLOW.md +32 -18
- package/src/core/extension-status.ts +19 -3
- package/src/core/path-context.ts +59 -0
- package/src/index.ts +20 -14
- package/tests/extension-registration.test.ts +84 -11
- package/tests/release-workflow.test.ts +61 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
() => {
|