pi-usereq 0.11.0 → 0.12.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/CHANGELOG.md +48 -0
- package/README.md +6 -6
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +818 -691
- package/pi-usereq/docs/REQUIREMENTS.md +131 -77
- package/pi-usereq/docs/WORKFLOW.md +185 -51
- package/scripts/lib/extension-debug-harness.ts +2 -2
- package/scripts/tool-args-to-params.ts +2 -2
- package/src/cli.ts +12 -12
- package/src/core/extension-status.ts +69 -12
- package/src/core/pi-notify.ts +5 -5
- package/src/core/pi-usereq-tools.ts +4 -2
- package/src/core/prompt-command-catalog.ts +4 -5
- package/src/core/prompt-command-runtime.ts +183 -44
- package/src/core/prompts.ts +0 -2
- package/src/core/req-references-command.ts +175 -0
- package/src/core/req-reset-command.ts +323 -0
- package/src/core/resources.ts +6 -23
- package/src/core/settings-menu.ts +85 -28
- package/src/core/tool-runner.ts +26 -6
- package/src/index.ts +523 -85
- package/tests/attended-results-scenarios.ts +5 -5
- package/tests/cli-command-option-parity.test.ts +25 -25
- package/tests/debug-extension-harness.test.ts +1 -1
- package/tests/extension-registration.test.ts +1029 -82
- package/tests/oracle-project.test.ts +4 -4
- package/tests/oracle-standalone.test.ts +5 -5
- package/src/core/reference-payload.ts +0 -752
- package/src/resources/prompts/references.md +0 -64
- /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
- /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
|
@@ -5,6 +5,13 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import piUsereqExtension from "../src/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
REQ_REFERENCES_COMMAND_DESCRIPTION,
|
|
10
|
+
REQ_REFERENCES_COMMIT_MESSAGE,
|
|
11
|
+
} from "../src/core/req-references-command.js";
|
|
12
|
+
import {
|
|
13
|
+
REQ_RESET_COMMAND_DESCRIPTION,
|
|
14
|
+
} from "../src/core/req-reset-command.js";
|
|
8
15
|
import {
|
|
9
16
|
createStaticCheckLanguageConfig,
|
|
10
17
|
DEFAULT_DOCS_DIR,
|
|
@@ -45,9 +52,11 @@ import {
|
|
|
45
52
|
} from "../src/core/pi-usereq-tools.js";
|
|
46
53
|
import {
|
|
47
54
|
createPiUsereqStatusController,
|
|
55
|
+
renderPiUsereqStatus,
|
|
48
56
|
setPiUsereqStatusConfig,
|
|
49
57
|
setPiUsereqWorkflowState,
|
|
50
58
|
} from "../src/core/extension-status.js";
|
|
59
|
+
import { showPiUsereqSettingsMenu } from "../src/core/settings-menu.js";
|
|
51
60
|
import { getSupportedStaticCheckLanguageSupport } from "../src/core/static-check.js";
|
|
52
61
|
import { createTempDir, initFixtureRepo } from "./helpers.js";
|
|
53
62
|
|
|
@@ -429,7 +438,7 @@ function buildExpectedFakeBranchValue(basePath: string | undefined): string {
|
|
|
429
438
|
|
|
430
439
|
/**
|
|
431
440
|
* @brief Builds the expected fake context-gauge payload for assertions.
|
|
432
|
-
* @details Resolves the documented icon thresholds for `0`, `>0-<25`, `>=25-<50`, `>=50-<75`, and `>=75-<90`, emits the non-blinking error full icon for `>=90-<100`, and emits the blinking error full icon for `>=100`. Runtime is O(1). No external state is mutated.
|
|
441
|
+
* @details Resolves the documented icon thresholds for `0`, `>0-<25`, `>=25-<50`, `>=50-<75`, and `>=75-<90`, renders those non-error bands with the same `warning` token used by the `status` value, emits the non-blinking error full icon for `>=90-<100`, and emits the blinking error full icon for `>=100`. Runtime is O(1). No external state is mutated.
|
|
433
442
|
* @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
|
|
434
443
|
* @return {string} Encoded context-gauge string.
|
|
435
444
|
*/
|
|
@@ -439,7 +448,7 @@ function buildExpectedFakeContextBar(options: {
|
|
|
439
448
|
}): string {
|
|
440
449
|
const percent = options.percent;
|
|
441
450
|
if (percent === undefined || percent === null || percent <= 0) {
|
|
442
|
-
return "▕_▏";
|
|
451
|
+
return formatFakeThemeForeground("warning", "▕_▏");
|
|
443
452
|
}
|
|
444
453
|
if (percent >= 100) {
|
|
445
454
|
return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
|
|
@@ -448,15 +457,15 @@ function buildExpectedFakeContextBar(options: {
|
|
|
448
457
|
return formatFakeThemeForeground("error", "▕█▏");
|
|
449
458
|
}
|
|
450
459
|
if (percent < 25) {
|
|
451
|
-
return "▕▂▏";
|
|
460
|
+
return formatFakeThemeForeground("warning", "▕▂▏");
|
|
452
461
|
}
|
|
453
462
|
if (percent < 50) {
|
|
454
|
-
return "▕▄▏";
|
|
463
|
+
return formatFakeThemeForeground("warning", "▕▄▏");
|
|
455
464
|
}
|
|
456
465
|
if (percent < 75) {
|
|
457
|
-
return "▕▆▏";
|
|
466
|
+
return formatFakeThemeForeground("warning", "▕▆▏");
|
|
458
467
|
}
|
|
459
|
-
return "▕█▏";
|
|
468
|
+
return formatFakeThemeForeground("warning", "▕█▏");
|
|
460
469
|
}
|
|
461
470
|
|
|
462
471
|
/**
|
|
@@ -785,6 +794,7 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
785
794
|
"req-recreate",
|
|
786
795
|
"req-refactor",
|
|
787
796
|
"req-references",
|
|
797
|
+
"req-reset",
|
|
788
798
|
"req-renumber",
|
|
789
799
|
"req-workflow",
|
|
790
800
|
"req-write",
|
|
@@ -795,9 +805,10 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
795
805
|
|
|
796
806
|
for (const name of [
|
|
797
807
|
"files-tokens",
|
|
798
|
-
"files-
|
|
808
|
+
"files-summarize",
|
|
799
809
|
"files-compress",
|
|
800
810
|
"files-search",
|
|
811
|
+
"summarize",
|
|
801
812
|
"references",
|
|
802
813
|
"compress",
|
|
803
814
|
"search",
|
|
@@ -813,9 +824,10 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
813
824
|
const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
|
|
814
825
|
for (const name of [
|
|
815
826
|
"files-tokens",
|
|
816
|
-
"files-
|
|
827
|
+
"files-summarize",
|
|
817
828
|
"files-compress",
|
|
818
829
|
"files-search",
|
|
830
|
+
"summarize",
|
|
819
831
|
"references",
|
|
820
832
|
"compress",
|
|
821
833
|
"search",
|
|
@@ -827,6 +839,46 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
827
839
|
}
|
|
828
840
|
});
|
|
829
841
|
|
|
842
|
+
/**
|
|
843
|
+
* @brief Verifies `req-references` registration is independent from bundled prompt inventory.
|
|
844
|
+
* @details Confirms `references` is absent from `PROMPT_COMMAND_NAMES` while extension activation still registers `req-references` with the fixed dedicated description. Runtime is O(1). Side effects are limited to extension registration.
|
|
845
|
+
* @return {void} No return value.
|
|
846
|
+
* @throws {AssertionError} Throws when `references` remains in the bundled prompt inventory or the command description diverges.
|
|
847
|
+
* @satisfies TST-102
|
|
848
|
+
*/
|
|
849
|
+
test("req-references registers outside the bundled prompt inventory and keeps its dedicated description", () => {
|
|
850
|
+
const bundledPromptNames = PROMPT_COMMAND_NAMES as readonly string[];
|
|
851
|
+
assert.equal(bundledPromptNames.includes("references"), false);
|
|
852
|
+
|
|
853
|
+
const pi = createFakePi();
|
|
854
|
+
piUsereqExtension(pi);
|
|
855
|
+
|
|
856
|
+
assert.equal(
|
|
857
|
+
pi.commands.get("req-references")?.description,
|
|
858
|
+
REQ_REFERENCES_COMMAND_DESCRIPTION,
|
|
859
|
+
);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* @brief Verifies `req-reset` registration is independent from bundled prompt inventory.
|
|
864
|
+
* @details Confirms `reset` is absent from `PROMPT_COMMAND_NAMES` while extension activation still registers `req-reset` with the fixed dedicated description. Runtime is O(1). Side effects are limited to extension registration.
|
|
865
|
+
* @return {void} No return value.
|
|
866
|
+
* @throws {AssertionError} Throws when `reset` remains in the bundled prompt inventory or the command description diverges.
|
|
867
|
+
* @satisfies TST-105
|
|
868
|
+
*/
|
|
869
|
+
test("req-reset registers outside the bundled prompt inventory and keeps its dedicated description", () => {
|
|
870
|
+
const bundledPromptNames = PROMPT_COMMAND_NAMES as readonly string[];
|
|
871
|
+
assert.equal(bundledPromptNames.includes("reset"), false);
|
|
872
|
+
|
|
873
|
+
const pi = createFakePi();
|
|
874
|
+
piUsereqExtension(pi);
|
|
875
|
+
|
|
876
|
+
assert.equal(
|
|
877
|
+
pi.commands.get("req-reset")?.description,
|
|
878
|
+
REQ_RESET_COMMAND_DESCRIPTION,
|
|
879
|
+
);
|
|
880
|
+
});
|
|
881
|
+
|
|
830
882
|
test("token tools register agent-oriented descriptions and schema details", () => {
|
|
831
883
|
const pi = createFakePi();
|
|
832
884
|
piUsereqExtension(pi);
|
|
@@ -845,24 +897,37 @@ test("token tools register agent-oriented descriptions and schema details", () =
|
|
|
845
897
|
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
|
|
846
898
|
});
|
|
847
899
|
|
|
848
|
-
test("
|
|
900
|
+
test("summary tools register agent-oriented descriptions and schema details", () => {
|
|
901
|
+
const pi = createFakePi();
|
|
902
|
+
piUsereqExtension(pi);
|
|
903
|
+
|
|
904
|
+
const filesSummarize = pi.tools.find((tool: RegisteredTool) => tool.name === "files-summarize");
|
|
905
|
+
const summarize = pi.tools.find((tool: RegisteredTool) => tool.name === "summarize");
|
|
906
|
+
assert.ok(filesSummarize, "missing files-summarize tool");
|
|
907
|
+
assert.ok(summarize, "missing summarize tool");
|
|
908
|
+
|
|
909
|
+
assert.match(filesSummarize.description ?? "", /monolithic summary markdown report/);
|
|
910
|
+
assert.ok(filesSummarize.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
911
|
+
assert.ok(filesSummarize.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
912
|
+
assert.match(String((filesSummarize.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
913
|
+
|
|
914
|
+
assert.match(summarize.description ?? "", /configured project source directories/);
|
|
915
|
+
assert.ok(summarize.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
|
|
916
|
+
assert.ok(summarize.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
|
|
917
|
+
assert.match(String((summarize.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test("references tool registers agent-oriented descriptions and schema details", () => {
|
|
849
921
|
const pi = createFakePi();
|
|
850
922
|
piUsereqExtension(pi);
|
|
851
923
|
|
|
852
|
-
const filesReferences = pi.tools.find((tool: RegisteredTool) => tool.name === "files-references");
|
|
853
924
|
const references = pi.tools.find((tool: RegisteredTool) => tool.name === "references");
|
|
854
|
-
assert.ok(filesReferences, "missing files-references tool");
|
|
855
925
|
assert.ok(references, "missing references tool");
|
|
856
926
|
|
|
857
|
-
assert.match(
|
|
858
|
-
assert.ok(
|
|
859
|
-
assert.ok(
|
|
860
|
-
assert.match(String((
|
|
861
|
-
|
|
862
|
-
assert.match(references.description ?? "", /configured project source directories/);
|
|
863
|
-
assert.ok(references.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
|
|
864
|
-
assert.ok(references.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
|
|
865
|
-
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
927
|
+
assert.match(references.description ?? "", /configured project source directories and configured docs-dir/);
|
|
928
|
+
assert.ok(references.promptGuidelines?.some((line) => line.includes("overwrite `<docs-dir>/REFERENCES.md`")));
|
|
929
|
+
assert.ok(references.promptGuidelines?.some((line) => line.includes("generated markdown is never returned")));
|
|
930
|
+
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /content\[0\]\.text returns only `success` or `error: <diagnostic>`/);
|
|
866
931
|
});
|
|
867
932
|
|
|
868
933
|
test("search tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -913,9 +978,10 @@ test("agent tools define custom renderResult with compact and expanded structure
|
|
|
913
978
|
piUsereqExtension(pi);
|
|
914
979
|
const toolNames = [
|
|
915
980
|
"files-tokens",
|
|
916
|
-
"files-
|
|
981
|
+
"files-summarize",
|
|
917
982
|
"files-compress",
|
|
918
983
|
"files-search",
|
|
984
|
+
"summarize",
|
|
919
985
|
"references",
|
|
920
986
|
"compress",
|
|
921
987
|
"search",
|
|
@@ -963,7 +1029,7 @@ test("files-tokens returns monolithic token summary text with execution diagnost
|
|
|
963
1029
|
docs: {
|
|
964
1030
|
"REQUIREMENTS.md": "---\ntitle: Requirements\n---\n# Requirements\nAlpha\nBeta\n",
|
|
965
1031
|
"WORKFLOW.md": "# Workflow\nStep one\n",
|
|
966
|
-
"REFERENCES.md": "#
|
|
1032
|
+
"REFERENCES.md": "# Summarize\nEntry one\n",
|
|
967
1033
|
},
|
|
968
1034
|
});
|
|
969
1035
|
try {
|
|
@@ -1011,7 +1077,7 @@ test("files-tokens defers js-tiktoken loading until execution and returns monoli
|
|
|
1011
1077
|
}
|
|
1012
1078
|
});
|
|
1013
1079
|
|
|
1014
|
-
test("files-
|
|
1080
|
+
test("files-summarize returns monolithic summary markdown", async () => {
|
|
1015
1081
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1016
1082
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
1017
1083
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -1032,7 +1098,7 @@ export function buildSample(input: string): string {
|
|
|
1032
1098
|
try {
|
|
1033
1099
|
const pi = createFakePi();
|
|
1034
1100
|
piUsereqExtension(pi);
|
|
1035
|
-
const result = await executeRegisteredTool(pi, "files-
|
|
1101
|
+
const result = await executeRegisteredTool(pi, "files-summarize", projectBase, {
|
|
1036
1102
|
files: ["src/sample.ts", "src/missing.ts"],
|
|
1037
1103
|
}) as {
|
|
1038
1104
|
content?: Array<{ type: string; text?: string }>;
|
|
@@ -1051,7 +1117,7 @@ export function buildSample(input: string): string {
|
|
|
1051
1117
|
}
|
|
1052
1118
|
});
|
|
1053
1119
|
|
|
1054
|
-
test("
|
|
1120
|
+
test("summarize returns monolithic project summary markdown", async () => {
|
|
1055
1121
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1056
1122
|
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
1057
1123
|
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
@@ -1059,7 +1125,7 @@ test("references returns monolithic project references markdown", async () => {
|
|
|
1059
1125
|
try {
|
|
1060
1126
|
const pi = createFakePi();
|
|
1061
1127
|
piUsereqExtension(pi);
|
|
1062
|
-
const result = await executeRegisteredTool(pi, "
|
|
1128
|
+
const result = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
1063
1129
|
content?: Array<{ type: string; text?: string }>;
|
|
1064
1130
|
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1065
1131
|
};
|
|
@@ -1077,6 +1143,85 @@ test("references returns monolithic project references markdown", async () => {
|
|
|
1077
1143
|
}
|
|
1078
1144
|
});
|
|
1079
1145
|
|
|
1146
|
+
test("references overwrites the configured REFERENCES.md and returns status-only success", async () => {
|
|
1147
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1148
|
+
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
1149
|
+
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
1150
|
+
fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
|
|
1151
|
+
const docsDir = path.join(projectBase, "docs", "custom");
|
|
1152
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
1153
|
+
fs.writeFileSync(path.join(docsDir, "REFERENCES.md"), "stale\n", "utf8");
|
|
1154
|
+
const configPath = getProjectConfigPath(projectBase);
|
|
1155
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
1156
|
+
config["docs-dir"] = "docs/custom";
|
|
1157
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
const pi = createFakePi();
|
|
1161
|
+
piUsereqExtension(pi);
|
|
1162
|
+
const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
|
|
1163
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1164
|
+
details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
|
|
1165
|
+
};
|
|
1166
|
+
const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
1167
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
const referencesPath = path.join(docsDir, "REFERENCES.md");
|
|
1171
|
+
const writtenText = fs.readFileSync(referencesPath, "utf8");
|
|
1172
|
+
assert.equal(referencesResult.content?.[0]?.text ?? "", "success");
|
|
1173
|
+
assert.equal(writtenText.trimEnd(), summarizeResult.content?.[0]?.text ?? "");
|
|
1174
|
+
assert.doesNotMatch(writtenText, /^stale$/m);
|
|
1175
|
+
assert.equal(referencesResult.details?.execution.code, 0);
|
|
1176
|
+
assert.deepEqual(referencesResult.details?.execution.stderr_lines, undefined);
|
|
1177
|
+
assert.deepEqual(referencesResult.details?.execution.stdout_lines, undefined);
|
|
1178
|
+
} finally {
|
|
1179
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("references returns status-only errors for source-discovery and write failures", async () => {
|
|
1184
|
+
const sourceFailure = initFixtureRepo({ fixtures: [] });
|
|
1185
|
+
try {
|
|
1186
|
+
const pi = createFakePi();
|
|
1187
|
+
piUsereqExtension(pi);
|
|
1188
|
+
const result = await executeRegisteredTool(pi, "references", sourceFailure.projectBase, {}) as {
|
|
1189
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1190
|
+
details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
assert.equal(result.content?.[0]?.text ?? "", "error: no source files found in configured directories.");
|
|
1194
|
+
assert.equal(result.details?.execution.code, 1);
|
|
1195
|
+
assert.deepEqual(result.details?.execution.stdout_lines, undefined);
|
|
1196
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => line.includes("no source files found in configured directories.")));
|
|
1197
|
+
} finally {
|
|
1198
|
+
fs.rmSync(sourceFailure.projectBase, { recursive: true, force: true });
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const writeFailure = initFixtureRepo({ fixtures: [] });
|
|
1202
|
+
fs.writeFileSync(path.join(writeFailure.projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
1203
|
+
const writeFailureConfigPath = getProjectConfigPath(writeFailure.projectBase);
|
|
1204
|
+
const writeFailureConfig = JSON.parse(fs.readFileSync(writeFailureConfigPath, "utf8")) as Record<string, unknown>;
|
|
1205
|
+
writeFailureConfig["docs-dir"] = "docs/missing/custom";
|
|
1206
|
+
fs.writeFileSync(writeFailureConfigPath, `${JSON.stringify(writeFailureConfig, null, 2)}\n`, "utf8");
|
|
1207
|
+
|
|
1208
|
+
try {
|
|
1209
|
+
const pi = createFakePi();
|
|
1210
|
+
piUsereqExtension(pi);
|
|
1211
|
+
const result = await executeRegisteredTool(pi, "references", writeFailure.projectBase, {}) as {
|
|
1212
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1213
|
+
details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
assert.match(result.content?.[0]?.text ?? "", /^error: ENOENT:/);
|
|
1217
|
+
assert.equal(result.details?.execution.code, 1);
|
|
1218
|
+
assert.deepEqual(result.details?.execution.stdout_lines, undefined);
|
|
1219
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => /^error: ENOENT:/.test(line)));
|
|
1220
|
+
} finally {
|
|
1221
|
+
fs.rmSync(writeFailure.projectBase, { recursive: true, force: true });
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1080
1225
|
test("files-compress returns monolithic compression markdown", async () => {
|
|
1081
1226
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1082
1227
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
@@ -1264,21 +1409,21 @@ func (s *Server) Start(ctx context.Context) error {
|
|
|
1264
1409
|
const pi = createFakePi();
|
|
1265
1410
|
piUsereqExtension(pi);
|
|
1266
1411
|
|
|
1267
|
-
const
|
|
1412
|
+
const filesSummarizeResult = await executeRegisteredTool(pi, "files-summarize", projectBase, {
|
|
1268
1413
|
files: ["src/sample.go"],
|
|
1269
1414
|
}) as {
|
|
1270
1415
|
content?: Array<{ type: string; text?: string }>;
|
|
1271
1416
|
details?: { execution: { code: number } };
|
|
1272
1417
|
};
|
|
1273
|
-
assert.match(
|
|
1274
|
-
assert.equal(
|
|
1418
|
+
assert.match(filesSummarizeResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1419
|
+
assert.equal(filesSummarizeResult.details?.execution.code, 0);
|
|
1275
1420
|
|
|
1276
|
-
const
|
|
1421
|
+
const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
1277
1422
|
content?: Array<{ type: string; text?: string }>;
|
|
1278
1423
|
details?: { execution: { code: number } };
|
|
1279
1424
|
};
|
|
1280
|
-
assert.match(
|
|
1281
|
-
assert.equal(
|
|
1425
|
+
assert.match(summarizeResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1426
|
+
assert.equal(summarizeResult.details?.execution.code, 0);
|
|
1282
1427
|
|
|
1283
1428
|
const filesCompressResult = await executeRegisteredTool(pi, "files-compress", projectBase, {
|
|
1284
1429
|
files: ["src/sample.go"],
|
|
@@ -1501,12 +1646,12 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1501
1646
|
assert.ok(renderedMenu.includes("disable"));
|
|
1502
1647
|
assert.doesNotMatch(renderedMenu, /beep:/);
|
|
1503
1648
|
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
1504
|
-
assert.match(renderedNotificationsMenu, /<dim>
|
|
1649
|
+
assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
|
|
1505
1650
|
assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
|
|
1506
1651
|
"Enable notification",
|
|
1507
1652
|
"Notification events",
|
|
1508
1653
|
"Notify command",
|
|
1509
|
-
"Enable sound",
|
|
1654
|
+
"Enable sound (boot value)",
|
|
1510
1655
|
"Sound events",
|
|
1511
1656
|
"Sound toggle hotkey bind",
|
|
1512
1657
|
"Sound command (low vol.)",
|
|
@@ -1556,7 +1701,7 @@ test("descendant configuration menus end with Reset defaults only", async () =>
|
|
|
1556
1701
|
const ctx = createFakeCtx(cwd, {
|
|
1557
1702
|
selects: [
|
|
1558
1703
|
"notifications",
|
|
1559
|
-
"Enable sound",
|
|
1704
|
+
"Enable sound (boot value)",
|
|
1560
1705
|
"Save and close",
|
|
1561
1706
|
"Pushover priority",
|
|
1562
1707
|
"Save and close",
|
|
@@ -1599,14 +1744,14 @@ test("descendant configuration menus end with Reset defaults only", async () =>
|
|
|
1599
1744
|
return (ctx.__state.customRenderLines[index] ?? []).join("\n");
|
|
1600
1745
|
};
|
|
1601
1746
|
|
|
1602
|
-
assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
|
|
1747
|
+
assert.deepEqual(getTerminalRows("Enable sound (boot value)", "high"), ["Reset defaults"]);
|
|
1603
1748
|
assert.deepEqual(getTerminalRows("Pushover priority", "High"), ["Reset defaults"]);
|
|
1604
1749
|
assert.deepEqual(getTerminalRows("Enable tools", "static-check"), ["Reset defaults"]);
|
|
1605
1750
|
assert.deepEqual(getTerminalRows("Debug", "Log on status"), ["Reset defaults"]);
|
|
1606
1751
|
assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
|
|
1607
1752
|
assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
|
|
1608
1753
|
assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
|
|
1609
|
-
assert.doesNotMatch(getRenderedMenu("Enable sound", "high"), /Reset defaults.*none/);
|
|
1754
|
+
assert.doesNotMatch(getRenderedMenu("Enable sound (boot value)", "high"), /Reset defaults.*none/);
|
|
1610
1755
|
assert.doesNotMatch(getRenderedMenu("Pushover priority", "High"), /Reset defaults.*Normal/);
|
|
1611
1756
|
assert.doesNotMatch(getRenderedMenu("Enable tools", "static-check"), /Reset defaults.*defaults/);
|
|
1612
1757
|
assert.doesNotMatch(getRenderedMenu("Debug", "Log on status"), /Reset defaults.*running/);
|
|
@@ -1917,6 +2062,101 @@ test("notifications menu preserves focus on toggled and edited rows", async () =
|
|
|
1917
2062
|
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1918
2063
|
});
|
|
1919
2064
|
|
|
2065
|
+
/**
|
|
2066
|
+
* @brief Verifies inline toggle rows stay open and preserve selection for confirm keys.
|
|
2067
|
+
* @details Builds one shared settings-menu instance with a primary inline toggle and a dependent secondary row whose disabled styling changes after each toggle. The test proves `space` and `enter` must mutate the selected row in place, rebuild dependent rows without closing the menu, and preserve focus on the toggled row until explicit cancel.
|
|
2068
|
+
* @return {Promise<void>} Promise resolved after the raw-input interaction sequence completes.
|
|
2069
|
+
* @throws {AssertionError} Throws when the menu closes early, loses focus, or fails to rebuild dependent row styling after a toggle.
|
|
2070
|
+
* @satisfies REQ-192, TST-052
|
|
2071
|
+
*/
|
|
2072
|
+
test("shared settings menu keeps inline toggles focused and open for space and enter", async () => {
|
|
2073
|
+
let primaryEnabled = true;
|
|
2074
|
+
let component:
|
|
2075
|
+
| {
|
|
2076
|
+
handleInput?: (data: string) => void;
|
|
2077
|
+
render: (width: number) => string[];
|
|
2078
|
+
}
|
|
2079
|
+
| undefined;
|
|
2080
|
+
let resolved = false;
|
|
2081
|
+
const changes: string[] = [];
|
|
2082
|
+
const theme = {
|
|
2083
|
+
fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
|
|
2084
|
+
bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
|
|
2085
|
+
bold: (text: string) => `<bold>${text}</bold>`,
|
|
2086
|
+
};
|
|
2087
|
+
const ctx = {
|
|
2088
|
+
ui: {
|
|
2089
|
+
async custom<T>(factory: (tui: { requestRender: () => void }, customTheme: typeof theme, keybindings: Record<string, never>, done: (value?: T) => void) => unknown) {
|
|
2090
|
+
let resolveResult!: (value: T | undefined) => void;
|
|
2091
|
+
const resultPromise = new Promise<T | undefined>((resolve) => {
|
|
2092
|
+
resolveResult = (value?: T) => {
|
|
2093
|
+
resolved = true;
|
|
2094
|
+
resolve(value);
|
|
2095
|
+
};
|
|
2096
|
+
});
|
|
2097
|
+
component = factory(
|
|
2098
|
+
{ requestRender: () => undefined },
|
|
2099
|
+
theme,
|
|
2100
|
+
{},
|
|
2101
|
+
(value?: T) => resolveResult(value),
|
|
2102
|
+
) as typeof component;
|
|
2103
|
+
return resultPromise;
|
|
2104
|
+
},
|
|
2105
|
+
},
|
|
2106
|
+
} as any;
|
|
2107
|
+
const buildChoices = () => [
|
|
2108
|
+
{
|
|
2109
|
+
id: "primary",
|
|
2110
|
+
label: "Primary",
|
|
2111
|
+
value: primaryEnabled ? "enable" : "disable",
|
|
2112
|
+
values: ["enable", "disable"],
|
|
2113
|
+
description: "Toggle the primary row in place.",
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
id: "secondary",
|
|
2117
|
+
label: "Secondary",
|
|
2118
|
+
labelTone: primaryEnabled ? undefined : "dim",
|
|
2119
|
+
value: primaryEnabled ? "editable" : "locked",
|
|
2120
|
+
valueTone: primaryEnabled ? undefined : "dim",
|
|
2121
|
+
disabled: !primaryEnabled,
|
|
2122
|
+
description: "Dependent row that must rebuild without closing the menu.",
|
|
2123
|
+
},
|
|
2124
|
+
] as any;
|
|
2125
|
+
|
|
2126
|
+
const resultPromise = showPiUsereqSettingsMenu(
|
|
2127
|
+
ctx,
|
|
2128
|
+
"Toggle menu",
|
|
2129
|
+
buildChoices(),
|
|
2130
|
+
{
|
|
2131
|
+
getChoices: buildChoices,
|
|
2132
|
+
onChange: (choiceId: string, newValue: string) => {
|
|
2133
|
+
if (choiceId === "primary") {
|
|
2134
|
+
primaryEnabled = newValue === "enable";
|
|
2135
|
+
changes.push(newValue);
|
|
2136
|
+
}
|
|
2137
|
+
},
|
|
2138
|
+
} as any,
|
|
2139
|
+
);
|
|
2140
|
+
|
|
2141
|
+
assert.ok(component);
|
|
2142
|
+
assert.doesNotMatch(component.render(120).join("\n"), /<dim>Secondary/);
|
|
2143
|
+
|
|
2144
|
+
component.handleInput?.(" ");
|
|
2145
|
+
await Promise.resolve();
|
|
2146
|
+
assert.deepEqual(changes, ["disable"]);
|
|
2147
|
+
assert.equal(resolved, false);
|
|
2148
|
+
assert.match(component.render(120).join("\n"), /<dim>Secondary/);
|
|
2149
|
+
|
|
2150
|
+
component.handleInput?.("\r");
|
|
2151
|
+
await Promise.resolve();
|
|
2152
|
+
assert.deepEqual(changes, ["disable", "enable"]);
|
|
2153
|
+
assert.equal(resolved, false);
|
|
2154
|
+
assert.doesNotMatch(component.render(120).join("\n"), /<dim>Secondary/);
|
|
2155
|
+
|
|
2156
|
+
component.handleInput?.("\x1b");
|
|
2157
|
+
assert.equal(await resultPromise, undefined);
|
|
2158
|
+
});
|
|
2159
|
+
|
|
1920
2160
|
test("notifications reset defaults preserves non-notification settings", async () => {
|
|
1921
2161
|
const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
|
|
1922
2162
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1929,7 +2169,7 @@ test("notifications reset defaults preserves non-notification settings", async (
|
|
|
1929
2169
|
"docs-dir",
|
|
1930
2170
|
"notifications",
|
|
1931
2171
|
"Enable notification",
|
|
1932
|
-
"Enable sound",
|
|
2172
|
+
"Enable sound (boot value)",
|
|
1933
2173
|
"high",
|
|
1934
2174
|
"Reset defaults",
|
|
1935
2175
|
"Approve reset",
|
|
@@ -2033,6 +2273,376 @@ test("prompt commands use the current session by default", async () => {
|
|
|
2033
2273
|
}
|
|
2034
2274
|
});
|
|
2035
2275
|
|
|
2276
|
+
/**
|
|
2277
|
+
* @brief Verifies successful `req-references` completion uses the specialized direct commit workflow.
|
|
2278
|
+
* @details Confirms the command regenerates configured `REFERENCES.md`, creates no worktree, sends no LLM user message, stages only the target file, commits the fixed message, and restores workflow state `idle`. Runtime is dominated by temporary git execution and reference generation. Side effects are limited to temporary repository mutation and command-handler notifications.
|
|
2279
|
+
* @return {Promise<void>} Promise resolved after success-path assertions complete.
|
|
2280
|
+
* @throws {AssertionError} Throws when `req-references` creates a worktree, starts a session handoff, skips the fixed commit, or leaves the repository dirty.
|
|
2281
|
+
* @satisfies TST-103
|
|
2282
|
+
*/
|
|
2283
|
+
test("req-references updates REFERENCES.md with a direct commit workflow and no LLM session", async () => {
|
|
2284
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2285
|
+
const previousCwd = process.cwd();
|
|
2286
|
+
try {
|
|
2287
|
+
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
2288
|
+
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
2289
|
+
fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
|
|
2290
|
+
const docsDir = path.join(projectBase, "docs", "custom");
|
|
2291
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
2292
|
+
fs.writeFileSync(path.join(docsDir, "REFERENCES.md"), "stale\n", "utf8");
|
|
2293
|
+
writeProjectConfigOverrides(projectBase, { "docs-dir": "docs/custom" });
|
|
2294
|
+
assert.equal(spawnSync("git", ["add", "src/alpha.ts", "src/nested/beta.ts", "docs/custom/REFERENCES.md"], {
|
|
2295
|
+
cwd: projectBase,
|
|
2296
|
+
encoding: "utf8",
|
|
2297
|
+
}).status, 0);
|
|
2298
|
+
const fixtureCommit = spawnSync("git", ["commit", "-m", "fixture sources"], {
|
|
2299
|
+
cwd: projectBase,
|
|
2300
|
+
encoding: "utf8",
|
|
2301
|
+
});
|
|
2302
|
+
assert.equal(fixtureCommit.status, 0, fixtureCommit.stderr);
|
|
2303
|
+
|
|
2304
|
+
const worktreeParentPath = path.dirname(projectBase);
|
|
2305
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2306
|
+
const pi = createFakePi();
|
|
2307
|
+
piUsereqExtension(pi);
|
|
2308
|
+
const ctx = createFakeCtx(projectBase);
|
|
2309
|
+
const recordedStatuses: string[] = [];
|
|
2310
|
+
let switchSessionCalls = 0;
|
|
2311
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2312
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
2313
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2314
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2315
|
+
recordedStatuses.push(value);
|
|
2316
|
+
}
|
|
2317
|
+
originalSetStatus(key, value);
|
|
2318
|
+
};
|
|
2319
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
2320
|
+
switchSessionCalls += 1;
|
|
2321
|
+
return originalSwitchSession(sessionPath);
|
|
2322
|
+
};
|
|
2323
|
+
|
|
2324
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2325
|
+
await pi.commands.get("req-references")!.handler("", ctx);
|
|
2326
|
+
|
|
2327
|
+
const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
2328
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
2329
|
+
};
|
|
2330
|
+
const referencesPath = path.join(docsDir, "REFERENCES.md");
|
|
2331
|
+
const writtenText = fs.readFileSync(referencesPath, "utf8");
|
|
2332
|
+
const lastCommitMessage = spawnSync("git", ["log", "-1", "--pretty=%s"], {
|
|
2333
|
+
cwd: projectBase,
|
|
2334
|
+
encoding: "utf8",
|
|
2335
|
+
}).stdout.trim();
|
|
2336
|
+
const committedPaths = spawnSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"], {
|
|
2337
|
+
cwd: projectBase,
|
|
2338
|
+
encoding: "utf8",
|
|
2339
|
+
}).stdout.trim().split(/\r?\n/).filter((line) => line !== "");
|
|
2340
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2341
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2342
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2343
|
+
const finalStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2344
|
+
|
|
2345
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2346
|
+
assert.equal(switchSessionCalls, 0);
|
|
2347
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2348
|
+
assert.equal(writtenText.trimEnd(), summarizeResult.content?.[0]?.text ?? "");
|
|
2349
|
+
assert.doesNotMatch(writtenText, /^stale$/m);
|
|
2350
|
+
assert.equal(lastCommitMessage, REQ_REFERENCES_COMMIT_MESSAGE);
|
|
2351
|
+
assert.deepEqual(committedPaths, ["docs/custom/REFERENCES.md"]);
|
|
2352
|
+
assert.equal(
|
|
2353
|
+
spawnSync("git", ["status", "--porcelain"], { cwd: projectBase, encoding: "utf8" }).stdout.trim(),
|
|
2354
|
+
"",
|
|
2355
|
+
);
|
|
2356
|
+
assert.ok(ctx.__state.notifications.some((entry) => /SUCCESS: Updated .*REFERENCES\.md/.test(entry.message)));
|
|
2357
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
|
|
2358
|
+
assert.ok(recordedStatuses.every((status) => !/<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
|
|
2359
|
+
assert.equal(
|
|
2360
|
+
finalStatus,
|
|
2361
|
+
buildExpectedFakeStatusText({
|
|
2362
|
+
workflowState: "idle",
|
|
2363
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2364
|
+
docsDir: "docs/custom",
|
|
2365
|
+
testsDir: "tests",
|
|
2366
|
+
srcDir: ["src"],
|
|
2367
|
+
contextFilledCells: 0,
|
|
2368
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2369
|
+
}),
|
|
2370
|
+
);
|
|
2371
|
+
} finally {
|
|
2372
|
+
process.chdir(previousCwd);
|
|
2373
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
/**
|
|
2378
|
+
* @brief Verifies failing `req-references` runs report errors without session handoff.
|
|
2379
|
+
* @details Covers both git-preflight failure and reference-generation failure, proving the command creates no worktree, sends no LLM message, transitions workflow state to `error`, and preserves the previous git commit when execution aborts. Runtime is dominated by temporary repository setup plus failing command invocations. Side effects are limited to temporary filesystem and git mutation inside fixture repositories.
|
|
2380
|
+
* @return {Promise<void>} Promise resolved after failure-path assertions complete.
|
|
2381
|
+
* @throws {AssertionError} Throws when `req-references` hides failures, mutates history, or attempts prompt-session orchestration.
|
|
2382
|
+
* @satisfies TST-104
|
|
2383
|
+
*/
|
|
2384
|
+
test("req-references surfaces git-validation and reference-generation failures without session handoff", async () => {
|
|
2385
|
+
const gitFailure = initFixtureRepo({ fixtures: [] });
|
|
2386
|
+
const previousCwd = process.cwd();
|
|
2387
|
+
try {
|
|
2388
|
+
fs.writeFileSync(path.join(gitFailure.projectBase, "DIRTY.txt"), "dirty\n", "utf8");
|
|
2389
|
+
const worktreeParentPath = path.dirname(gitFailure.projectBase);
|
|
2390
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2391
|
+
const pi = createFakePi();
|
|
2392
|
+
piUsereqExtension(pi);
|
|
2393
|
+
const ctx = createFakeCtx(gitFailure.projectBase);
|
|
2394
|
+
|
|
2395
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2396
|
+
await assert.rejects(
|
|
2397
|
+
pi.commands.get("req-references")!.handler("", ctx),
|
|
2398
|
+
/ERROR: Git status unclear!/,
|
|
2399
|
+
);
|
|
2400
|
+
|
|
2401
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2402
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2403
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2404
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2405
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2406
|
+
assert.ok(ctx.__state.notifications.some((entry) => /ERROR: Git status unclear!/.test(entry.message)));
|
|
2407
|
+
assert.equal(
|
|
2408
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2409
|
+
buildExpectedFakeStatusText({
|
|
2410
|
+
workflowState: "error",
|
|
2411
|
+
basePath: buildExpectedFakeBasePath(gitFailure.projectBase),
|
|
2412
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2413
|
+
testsDir: "tests",
|
|
2414
|
+
srcDir: ["src"],
|
|
2415
|
+
contextFilledCells: 0,
|
|
2416
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2417
|
+
}),
|
|
2418
|
+
);
|
|
2419
|
+
assert.equal(
|
|
2420
|
+
spawnSync("git", ["log", "-1", "--pretty=%s"], {
|
|
2421
|
+
cwd: gitFailure.projectBase,
|
|
2422
|
+
encoding: "utf8",
|
|
2423
|
+
}).stdout.trim(),
|
|
2424
|
+
"init",
|
|
2425
|
+
);
|
|
2426
|
+
} finally {
|
|
2427
|
+
process.chdir(previousCwd);
|
|
2428
|
+
fs.rmSync(gitFailure.projectBase, { recursive: true, force: true });
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
const sourceFailure = initFixtureRepo({ fixtures: [] });
|
|
2432
|
+
try {
|
|
2433
|
+
const worktreeParentPath = path.dirname(sourceFailure.projectBase);
|
|
2434
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2435
|
+
const pi = createFakePi();
|
|
2436
|
+
piUsereqExtension(pi);
|
|
2437
|
+
const ctx = createFakeCtx(sourceFailure.projectBase);
|
|
2438
|
+
const recordedStatuses: string[] = [];
|
|
2439
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2440
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2441
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2442
|
+
recordedStatuses.push(value);
|
|
2443
|
+
}
|
|
2444
|
+
originalSetStatus(key, value);
|
|
2445
|
+
};
|
|
2446
|
+
|
|
2447
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2448
|
+
await assert.rejects(
|
|
2449
|
+
pi.commands.get("req-references")!.handler("", ctx),
|
|
2450
|
+
/no source files found in configured directories/,
|
|
2451
|
+
);
|
|
2452
|
+
|
|
2453
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2454
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2455
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2456
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2457
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2458
|
+
assert.ok(ctx.__state.notifications.some((entry) => /no source files found in configured directories/.test(entry.message)));
|
|
2459
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
|
|
2460
|
+
assert.equal(
|
|
2461
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2462
|
+
buildExpectedFakeStatusText({
|
|
2463
|
+
workflowState: "error",
|
|
2464
|
+
basePath: buildExpectedFakeBasePath(sourceFailure.projectBase),
|
|
2465
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2466
|
+
testsDir: "tests",
|
|
2467
|
+
srcDir: ["src"],
|
|
2468
|
+
contextFilledCells: 0,
|
|
2469
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2470
|
+
}),
|
|
2471
|
+
);
|
|
2472
|
+
assert.equal(
|
|
2473
|
+
spawnSync("git", ["log", "-1", "--pretty=%s"], {
|
|
2474
|
+
cwd: sourceFailure.projectBase,
|
|
2475
|
+
encoding: "utf8",
|
|
2476
|
+
}).stdout.trim(),
|
|
2477
|
+
"init",
|
|
2478
|
+
);
|
|
2479
|
+
} finally {
|
|
2480
|
+
process.chdir(previousCwd);
|
|
2481
|
+
fs.rmSync(sourceFailure.projectBase, { recursive: true, force: true });
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* @brief Verifies successful `req-reset` recovers from a non-`idle` worktree-backed prompt state.
|
|
2487
|
+
* @details Starts one worktree-backed `req-change` run until the fake command context remains attached to the execution worktree with workflow state `running`, appends one unique execution-session record, invokes `req-reset` from that non-`idle` state, and verifies transcript preservation, base-path restoration, generated worktree plus branch deletion, unchanged LLM prompt count, success notification, and final workflow state `idle`. Runtime is dominated by temporary git worktree setup plus session-file inspection. Side effects are limited to temporary repository mutation and fake UI notifications.
|
|
2488
|
+
* @return {Promise<void>} Promise resolved after recovery-path assertions complete.
|
|
2489
|
+
* @throws {AssertionError} Throws when `req-reset` rejects the non-`idle` state, loses the execution transcript, leaves generated cleanup artifacts behind, or dispatches an additional LLM prompt.
|
|
2490
|
+
* @satisfies TST-106
|
|
2491
|
+
*/
|
|
2492
|
+
test("req-reset accepts non-idle workflow state, preserves transcript, and removes generated worktrees", async () => {
|
|
2493
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2494
|
+
const previousCwd = process.cwd();
|
|
2495
|
+
try {
|
|
2496
|
+
const pi = createFakePi();
|
|
2497
|
+
piUsereqExtension(pi);
|
|
2498
|
+
const ctx = createFakeCtx(projectBase);
|
|
2499
|
+
|
|
2500
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2501
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2502
|
+
assert.notEqual(originalSessionFile, "");
|
|
2503
|
+
|
|
2504
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2505
|
+
|
|
2506
|
+
const sentMessageCountBeforeReset = pi.sentUserMessages.length;
|
|
2507
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2508
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2509
|
+
assert.ok(worktreeMatch, promptText);
|
|
2510
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2511
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2512
|
+
const executionSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2513
|
+
const appendedEntryId = "req-reset-transcript-entry";
|
|
2514
|
+
|
|
2515
|
+
assert.notEqual(executionSessionFile, "");
|
|
2516
|
+
assert.notEqual(executionSessionFile, originalSessionFile);
|
|
2517
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2518
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2519
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2520
|
+
assert.match(
|
|
2521
|
+
ctx.__state.statuses.get("pi-usereq") ?? "",
|
|
2522
|
+
/<accent>status:<\/accent><warning>running<\/warning>/,
|
|
2523
|
+
);
|
|
2524
|
+
|
|
2525
|
+
fs.appendFileSync(
|
|
2526
|
+
executionSessionFile,
|
|
2527
|
+
`${JSON.stringify({
|
|
2528
|
+
type: "custom",
|
|
2529
|
+
id: appendedEntryId,
|
|
2530
|
+
parentId: null,
|
|
2531
|
+
timestamp: new Date(0).toISOString(),
|
|
2532
|
+
customType: "req-reset-test",
|
|
2533
|
+
data: { recovered: true },
|
|
2534
|
+
})}\n`,
|
|
2535
|
+
"utf8",
|
|
2536
|
+
);
|
|
2537
|
+
|
|
2538
|
+
await pi.commands.get("req-reset")!.handler("", ctx);
|
|
2539
|
+
|
|
2540
|
+
assert.equal(pi.sentUserMessages.length, sentMessageCountBeforeReset);
|
|
2541
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2542
|
+
assert.equal(process.cwd(), projectBase);
|
|
2543
|
+
assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
|
|
2544
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2545
|
+
assert.equal(
|
|
2546
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2547
|
+
cwd: projectBase,
|
|
2548
|
+
encoding: "utf8",
|
|
2549
|
+
}).status,
|
|
2550
|
+
1,
|
|
2551
|
+
);
|
|
2552
|
+
assert.match(fs.readFileSync(originalSessionFile, "utf8"), new RegExp(appendedEntryId));
|
|
2553
|
+
assert.ok(
|
|
2554
|
+
ctx.__state.notifications.some((entry) => /SUCCESS: req-reset/.test(entry.message)),
|
|
2555
|
+
);
|
|
2556
|
+
assert.equal(
|
|
2557
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2558
|
+
buildExpectedFakeStatusText({
|
|
2559
|
+
workflowState: "idle",
|
|
2560
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2561
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2562
|
+
testsDir: "tests",
|
|
2563
|
+
srcDir: ["src"],
|
|
2564
|
+
contextFilledCells: 0,
|
|
2565
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2566
|
+
}),
|
|
2567
|
+
);
|
|
2568
|
+
} finally {
|
|
2569
|
+
process.chdir(previousCwd);
|
|
2570
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
/**
|
|
2575
|
+
* @brief Verifies failing `req-reset` surfaces restoration errors without starting new worktree or LLM execution.
|
|
2576
|
+
* @details Starts one worktree-backed `req-change` run until the fake command context remains attached to the execution worktree, forces the subsequent base-path restoration switch to return `cancelled`, invokes `req-reset` from that non-`idle` state, and verifies the command reports the restoration failure, leaves the generated worktree plus branch in place for manual recovery, sends no additional LLM prompt, and transitions workflow state to `error`. Runtime is dominated by temporary git worktree setup. Side effects are limited to temporary repository mutation and fake UI notifications.
|
|
2577
|
+
* @return {Promise<void>} Promise resolved after failure-path assertions complete.
|
|
2578
|
+
* @throws {AssertionError} Throws when `req-reset` hides restoration failures, creates new cleanup artifacts, or clears the generated worktree plus branch despite the failed restore.
|
|
2579
|
+
* @satisfies TST-107
|
|
2580
|
+
*/
|
|
2581
|
+
test("req-reset surfaces restoration failures and transitions workflow state to error", async () => {
|
|
2582
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2583
|
+
const previousCwd = process.cwd();
|
|
2584
|
+
try {
|
|
2585
|
+
const pi = createFakePi();
|
|
2586
|
+
piUsereqExtension(pi);
|
|
2587
|
+
const ctx = createFakeCtx(projectBase);
|
|
2588
|
+
|
|
2589
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2590
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2591
|
+
assert.notEqual(originalSessionFile, "");
|
|
2592
|
+
|
|
2593
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2594
|
+
|
|
2595
|
+
const sentMessageCountBeforeReset = pi.sentUserMessages.length;
|
|
2596
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2597
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2598
|
+
assert.ok(worktreeMatch, promptText);
|
|
2599
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2600
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2601
|
+
|
|
2602
|
+
assert.match(
|
|
2603
|
+
ctx.__state.statuses.get("pi-usereq") ?? "",
|
|
2604
|
+
/<accent>status:<\/accent><warning>running<\/warning>/,
|
|
2605
|
+
);
|
|
2606
|
+
|
|
2607
|
+
ctx.switchSession = async (_sessionPath: string) => ({ cancelled: true });
|
|
2608
|
+
|
|
2609
|
+
await assert.rejects(
|
|
2610
|
+
pi.commands.get("req-reset")!.handler("", ctx),
|
|
2611
|
+
/Session switch cancelled/,
|
|
2612
|
+
);
|
|
2613
|
+
|
|
2614
|
+
assert.equal(pi.sentUserMessages.length, sentMessageCountBeforeReset);
|
|
2615
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2616
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2617
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2618
|
+
assert.equal(
|
|
2619
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2620
|
+
cwd: projectBase,
|
|
2621
|
+
encoding: "utf8",
|
|
2622
|
+
}).status,
|
|
2623
|
+
0,
|
|
2624
|
+
);
|
|
2625
|
+
assert.ok(
|
|
2626
|
+
ctx.__state.notifications.some((entry) => /Session switch cancelled/.test(entry.message)),
|
|
2627
|
+
);
|
|
2628
|
+
assert.equal(
|
|
2629
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2630
|
+
buildExpectedFakeStatusText({
|
|
2631
|
+
workflowState: "error",
|
|
2632
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2633
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2634
|
+
testsDir: "tests",
|
|
2635
|
+
srcDir: ["src"],
|
|
2636
|
+
contextFilledCells: 0,
|
|
2637
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2638
|
+
}),
|
|
2639
|
+
);
|
|
2640
|
+
} finally {
|
|
2641
|
+
process.chdir(previousCwd);
|
|
2642
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
|
|
2036
2646
|
test("prompt commands skip worktree creation when auto git commit is disabled", async () => {
|
|
2037
2647
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2038
2648
|
try {
|
|
@@ -2060,35 +2670,102 @@ test("prompt commands skip worktree creation when auto git commit is disabled",
|
|
|
2060
2670
|
}
|
|
2061
2671
|
});
|
|
2062
2672
|
|
|
2063
|
-
test("prompt commands capture checking before handoff and running at prompt injection", async () => {
|
|
2673
|
+
test("prompt commands capture checking before handoff and running at prompt injection", async () => {
|
|
2674
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2675
|
+
const previousCwd = process.cwd();
|
|
2676
|
+
let checkingStatus = "";
|
|
2677
|
+
let handoffStatus = "";
|
|
2678
|
+
try {
|
|
2679
|
+
const pi = createFakePi();
|
|
2680
|
+
piUsereqExtension(pi);
|
|
2681
|
+
const ctx = createFakeCtx(projectBase);
|
|
2682
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2683
|
+
const originalSendUserMessage = pi.sendUserMessage.bind(pi);
|
|
2684
|
+
pi.sendUserMessage = (content: unknown, options?: unknown) => {
|
|
2685
|
+
handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2686
|
+
originalSendUserMessage(content, options);
|
|
2687
|
+
};
|
|
2688
|
+
setPromptCommandPostCreateHookForTests(() => {
|
|
2689
|
+
checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2690
|
+
});
|
|
2691
|
+
|
|
2692
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2693
|
+
|
|
2694
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2695
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2696
|
+
assert.ok(worktreeMatch, promptText);
|
|
2697
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2698
|
+
assert.equal(
|
|
2699
|
+
checkingStatus,
|
|
2700
|
+
buildExpectedFakeStatusText({
|
|
2701
|
+
workflowState: "checking",
|
|
2702
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2703
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2704
|
+
testsDir: "tests",
|
|
2705
|
+
srcDir: ["src"],
|
|
2706
|
+
contextFilledCells: 0,
|
|
2707
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2708
|
+
}),
|
|
2709
|
+
);
|
|
2710
|
+
assert.equal(
|
|
2711
|
+
handoffStatus,
|
|
2712
|
+
buildExpectedFakeStatusText({
|
|
2713
|
+
workflowState: "checking",
|
|
2714
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2715
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2716
|
+
testsDir: "tests",
|
|
2717
|
+
srcDir: ["src"],
|
|
2718
|
+
contextFilledCells: 0,
|
|
2719
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2720
|
+
}),
|
|
2721
|
+
);
|
|
2722
|
+
assert.equal(
|
|
2723
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2724
|
+
buildExpectedFakeStatusText({
|
|
2725
|
+
workflowState: "running",
|
|
2726
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2727
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2728
|
+
testsDir: "tests",
|
|
2729
|
+
srcDir: ["src"],
|
|
2730
|
+
contextFilledCells: 0,
|
|
2731
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2732
|
+
}),
|
|
2733
|
+
);
|
|
2734
|
+
|
|
2735
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2736
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2737
|
+
await pi.emit("agent_end", {
|
|
2738
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2739
|
+
}, ctx);
|
|
2740
|
+
} finally {
|
|
2741
|
+
setPromptCommandPostCreateHookForTests(undefined);
|
|
2742
|
+
process.chdir(previousCwd);
|
|
2743
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
/**
|
|
2748
|
+
* @brief Verifies every req command except `req-reset` rejects non-`idle` workflow state and transitions to `error`.
|
|
2749
|
+
* @details Starts one no-worktree `req-change` run until workflow state becomes `running`, invokes `req-analyze` and `req-references` while orchestration remains active, and verifies both handlers reject with deterministic diagnostics, emit error notifications, send no additional LLM prompt, and move the shared workflow state to `error` before `agent_end` restores `idle`. Runtime is dominated by temporary project setup plus fake lifecycle dispatch. Side effects are limited to temporary filesystem mutation and fake UI notifications.
|
|
2750
|
+
* @return {Promise<void>} Promise resolved after rejection-path assertions complete.
|
|
2751
|
+
* @throws {AssertionError} Throws when a busy req command starts new orchestration, skips the error transition, or prevents the active no-worktree prompt from returning to `idle`.
|
|
2752
|
+
* @satisfies TST-068
|
|
2753
|
+
*/
|
|
2754
|
+
test("req commands except req-reset reject non-idle workflow state and transition to error", async () => {
|
|
2064
2755
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2065
2756
|
const previousCwd = process.cwd();
|
|
2066
|
-
let checkingStatus = "";
|
|
2067
|
-
let handoffStatus = "";
|
|
2068
2757
|
try {
|
|
2758
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2069
2759
|
const pi = createFakePi();
|
|
2070
2760
|
piUsereqExtension(pi);
|
|
2071
2761
|
const ctx = createFakeCtx(projectBase);
|
|
2072
2762
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2073
|
-
const originalSendUserMessage = pi.sendUserMessage.bind(pi);
|
|
2074
|
-
pi.sendUserMessage = (content: unknown, options?: unknown) => {
|
|
2075
|
-
handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2076
|
-
originalSendUserMessage(content, options);
|
|
2077
|
-
};
|
|
2078
|
-
setPromptCommandPostCreateHookForTests(() => {
|
|
2079
|
-
checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2080
|
-
});
|
|
2081
2763
|
|
|
2082
2764
|
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2083
|
-
|
|
2084
|
-
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2085
|
-
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2086
|
-
assert.ok(worktreeMatch, promptText);
|
|
2087
|
-
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2088
2765
|
assert.equal(
|
|
2089
|
-
|
|
2766
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2090
2767
|
buildExpectedFakeStatusText({
|
|
2091
|
-
workflowState: "
|
|
2768
|
+
workflowState: "running",
|
|
2092
2769
|
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2093
2770
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2094
2771
|
testsDir: "tests",
|
|
@@ -2097,11 +2774,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
|
|
|
2097
2774
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2098
2775
|
}),
|
|
2099
2776
|
);
|
|
2777
|
+
|
|
2778
|
+
await assert.rejects(
|
|
2779
|
+
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2780
|
+
/expected idle/,
|
|
2781
|
+
);
|
|
2782
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2100
2783
|
assert.equal(
|
|
2101
|
-
|
|
2784
|
+
ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
|
|
2785
|
+
1,
|
|
2786
|
+
);
|
|
2787
|
+
assert.equal(
|
|
2788
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2102
2789
|
buildExpectedFakeStatusText({
|
|
2103
|
-
workflowState: "
|
|
2104
|
-
basePath: buildExpectedFakeBasePath(
|
|
2790
|
+
workflowState: "error",
|
|
2791
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2105
2792
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2106
2793
|
testsDir: "tests",
|
|
2107
2794
|
srcDir: ["src"],
|
|
@@ -2109,11 +2796,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
|
|
|
2109
2796
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2110
2797
|
}),
|
|
2111
2798
|
);
|
|
2799
|
+
|
|
2800
|
+
await assert.rejects(
|
|
2801
|
+
pi.commands.get("req-references")!.handler("", ctx),
|
|
2802
|
+
/expected idle/,
|
|
2803
|
+
);
|
|
2804
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2805
|
+
assert.equal(
|
|
2806
|
+
ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
|
|
2807
|
+
2,
|
|
2808
|
+
);
|
|
2112
2809
|
assert.equal(
|
|
2113
2810
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2114
2811
|
buildExpectedFakeStatusText({
|
|
2115
|
-
workflowState: "
|
|
2116
|
-
basePath: buildExpectedFakeBasePath(
|
|
2812
|
+
workflowState: "error",
|
|
2813
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2117
2814
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2118
2815
|
testsDir: "tests",
|
|
2119
2816
|
srcDir: ["src"],
|
|
@@ -2122,33 +2819,64 @@ test("prompt commands capture checking before handoff and running at prompt inje
|
|
|
2122
2819
|
}),
|
|
2123
2820
|
);
|
|
2124
2821
|
|
|
2125
|
-
await pi.emit("before_agent_start", {}, ctx);
|
|
2126
2822
|
await pi.emit("agent_start", {}, ctx);
|
|
2127
2823
|
await pi.emit("agent_end", {
|
|
2128
2824
|
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2129
2825
|
}, ctx);
|
|
2826
|
+
assert.equal(
|
|
2827
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2828
|
+
buildExpectedFakeStatusText({
|
|
2829
|
+
workflowState: "idle",
|
|
2830
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2831
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2832
|
+
testsDir: "tests",
|
|
2833
|
+
srcDir: ["src"],
|
|
2834
|
+
contextFilledCells: 0,
|
|
2835
|
+
et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
|
|
2836
|
+
}),
|
|
2837
|
+
);
|
|
2130
2838
|
} finally {
|
|
2131
|
-
setPromptCommandPostCreateHookForTests(undefined);
|
|
2132
2839
|
process.chdir(previousCwd);
|
|
2133
2840
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2134
2841
|
}
|
|
2135
2842
|
});
|
|
2136
2843
|
|
|
2137
|
-
|
|
2844
|
+
/**
|
|
2845
|
+
* @brief Verifies a worktree-backed prompt still finalizes after a busy req-command rejection moves workflow state to `error`.
|
|
2846
|
+
* @details Starts one worktree-backed `req-change` run until the fake command context remains attached to the execution worktree with workflow state `running`, invokes `req-analyze` while the active run is still in progress so the busy-command guard forces workflow state to `error`, then emits the matched lifecycle completion and verifies `base-path` restoration, worktree plus branch deletion, unchanged prompt count, and final workflow state `idle`. Runtime is dominated by temporary git worktree setup plus fake lifecycle dispatch. Side effects are limited to temporary repository mutation and fake UI notifications.
|
|
2847
|
+
* @return {Promise<void>} Promise resolved after finalization-path assertions complete.
|
|
2848
|
+
* @throws {AssertionError} Throws when busy-command rejection orphaned the successful worktree-backed run or prevented cleanup.
|
|
2849
|
+
* @satisfies TST-068, TST-082
|
|
2850
|
+
*/
|
|
2851
|
+
test("worktree-backed prompt finalization still succeeds after a busy-command rejection", async () => {
|
|
2138
2852
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2853
|
+
const previousCwd = process.cwd();
|
|
2139
2854
|
try {
|
|
2140
|
-
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2141
2855
|
const pi = createFakePi();
|
|
2142
2856
|
piUsereqExtension(pi);
|
|
2143
2857
|
const ctx = createFakeCtx(projectBase);
|
|
2858
|
+
|
|
2144
2859
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2860
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2861
|
+
assert.notEqual(originalSessionFile, "");
|
|
2145
2862
|
|
|
2146
2863
|
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2864
|
+
|
|
2865
|
+
const sentMessageCount = pi.sentUserMessages.length;
|
|
2866
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2867
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2868
|
+
assert.ok(worktreeMatch, promptText);
|
|
2869
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2870
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2871
|
+
|
|
2872
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2873
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2874
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2147
2875
|
assert.equal(
|
|
2148
2876
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2149
2877
|
buildExpectedFakeStatusText({
|
|
2150
2878
|
workflowState: "running",
|
|
2151
|
-
basePath: buildExpectedFakeBasePath(
|
|
2879
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2152
2880
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2153
2881
|
testsDir: "tests",
|
|
2154
2882
|
srcDir: ["src"],
|
|
@@ -2161,13 +2889,16 @@ test("prompt commands reject non-idle workflow state before starting a new orche
|
|
|
2161
2889
|
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2162
2890
|
/expected idle/,
|
|
2163
2891
|
);
|
|
2164
|
-
assert.equal(pi.sentUserMessages.length,
|
|
2165
|
-
assert.
|
|
2892
|
+
assert.equal(pi.sentUserMessages.length, sentMessageCount);
|
|
2893
|
+
assert.equal(
|
|
2894
|
+
ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
|
|
2895
|
+
1,
|
|
2896
|
+
);
|
|
2166
2897
|
assert.equal(
|
|
2167
2898
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2168
2899
|
buildExpectedFakeStatusText({
|
|
2169
|
-
workflowState: "
|
|
2170
|
-
basePath: buildExpectedFakeBasePath(
|
|
2900
|
+
workflowState: "error",
|
|
2901
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2171
2902
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2172
2903
|
testsDir: "tests",
|
|
2173
2904
|
srcDir: ["src"],
|
|
@@ -2176,10 +2907,30 @@ test("prompt commands reject non-idle workflow state before starting a new orche
|
|
|
2176
2907
|
}),
|
|
2177
2908
|
);
|
|
2178
2909
|
|
|
2910
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2179
2911
|
await pi.emit("agent_start", {}, ctx);
|
|
2180
2912
|
await pi.emit("agent_end", {
|
|
2181
2913
|
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2182
2914
|
}, ctx);
|
|
2915
|
+
|
|
2916
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2917
|
+
assert.equal(process.cwd(), projectBase);
|
|
2918
|
+
assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
|
|
2919
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2920
|
+
assert.notEqual(
|
|
2921
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2922
|
+
cwd: projectBase,
|
|
2923
|
+
encoding: "utf8",
|
|
2924
|
+
}).status,
|
|
2925
|
+
0,
|
|
2926
|
+
);
|
|
2927
|
+
assert.equal(
|
|
2928
|
+
spawnSync("git", ["status", "--porcelain"], {
|
|
2929
|
+
cwd: projectBase,
|
|
2930
|
+
encoding: "utf8",
|
|
2931
|
+
}).stdout.trim(),
|
|
2932
|
+
"",
|
|
2933
|
+
);
|
|
2183
2934
|
assert.equal(
|
|
2184
2935
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2185
2936
|
buildExpectedFakeStatusText({
|
|
@@ -2193,6 +2944,7 @@ test("prompt commands reject non-idle workflow state before starting a new orche
|
|
|
2193
2944
|
}),
|
|
2194
2945
|
);
|
|
2195
2946
|
} finally {
|
|
2947
|
+
process.chdir(previousCwd);
|
|
2196
2948
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2197
2949
|
}
|
|
2198
2950
|
});
|
|
@@ -2343,6 +3095,110 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
|
|
|
2343
3095
|
}
|
|
2344
3096
|
});
|
|
2345
3097
|
|
|
3098
|
+
/**
|
|
3099
|
+
* @brief Verifies successful closure stashes tracked `base-path` changes before merge and restores them afterward.
|
|
3100
|
+
* @details Creates tracked `base-path` files, dirties one in the index and one in the worktree after worktree handoff, commits a worktree change, and proves prompt finalization runs `git stash`/merge/`git stash pop` semantics by merging successfully, restoring the local `base-path` changes, deleting the worktree, and surfacing a warning-only notification about the dirty restored base path.
|
|
3101
|
+
* @return {Promise<void>} Promise resolved after stash-assisted merge assertions complete.
|
|
3102
|
+
* @throws {AssertionError} Throws when finalization drops local `base-path` changes, skips the merge, or reports an error instead of a warning.
|
|
3103
|
+
* @satisfies REQ-208, REQ-291, REQ-292
|
|
3104
|
+
*/
|
|
3105
|
+
test("worktree-backed successful closure stashes tracked base-path changes, merges, restores changes, and warns about dirty base-path", async () => {
|
|
3106
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3107
|
+
const previousCwd = process.cwd();
|
|
3108
|
+
try {
|
|
3109
|
+
const stagedBasePath = path.join(projectBase, "staged-base.txt");
|
|
3110
|
+
const worktreeBasePath = path.join(projectBase, "worktree-base.txt");
|
|
3111
|
+
fs.writeFileSync(stagedBasePath, "staged base clean\n", "utf8");
|
|
3112
|
+
fs.writeFileSync(worktreeBasePath, "worktree base clean\n", "utf8");
|
|
3113
|
+
assert.equal(
|
|
3114
|
+
spawnSync("git", ["add", "staged-base.txt", "worktree-base.txt"], {
|
|
3115
|
+
cwd: projectBase,
|
|
3116
|
+
encoding: "utf8",
|
|
3117
|
+
}).status,
|
|
3118
|
+
0,
|
|
3119
|
+
);
|
|
3120
|
+
const baseCommit = spawnSync("git", ["commit", "-m", "tracked base files"], {
|
|
3121
|
+
cwd: projectBase,
|
|
3122
|
+
encoding: "utf8",
|
|
3123
|
+
});
|
|
3124
|
+
assert.equal(baseCommit.status, 0, baseCommit.stderr);
|
|
3125
|
+
|
|
3126
|
+
const pi = createFakePi();
|
|
3127
|
+
piUsereqExtension(pi);
|
|
3128
|
+
const ctx = createFakeCtx(projectBase);
|
|
3129
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3130
|
+
|
|
3131
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3132
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3133
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3134
|
+
assert.ok(worktreeMatch, promptText);
|
|
3135
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3136
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3137
|
+
|
|
3138
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
3139
|
+
await pi.emit("agent_start", {}, ctx);
|
|
3140
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3141
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "stash-assisted.ts"), "export const STASH_ASSISTED = 1;\n", "utf8");
|
|
3142
|
+
assert.equal(
|
|
3143
|
+
spawnSync("git", ["add", "src/stash-assisted.ts"], {
|
|
3144
|
+
cwd: executionBasePath,
|
|
3145
|
+
encoding: "utf8",
|
|
3146
|
+
}).status,
|
|
3147
|
+
0,
|
|
3148
|
+
);
|
|
3149
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "stash assisted change"], {
|
|
3150
|
+
cwd: executionBasePath,
|
|
3151
|
+
encoding: "utf8",
|
|
3152
|
+
});
|
|
3153
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3154
|
+
|
|
3155
|
+
fs.writeFileSync(stagedBasePath, "staged base dirty\n", "utf8");
|
|
3156
|
+
assert.equal(
|
|
3157
|
+
spawnSync("git", ["add", "staged-base.txt"], {
|
|
3158
|
+
cwd: projectBase,
|
|
3159
|
+
encoding: "utf8",
|
|
3160
|
+
}).status,
|
|
3161
|
+
0,
|
|
3162
|
+
);
|
|
3163
|
+
fs.writeFileSync(worktreeBasePath, "worktree base dirty\n", "utf8");
|
|
3164
|
+
|
|
3165
|
+
await pi.emit("agent_end", {
|
|
3166
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3167
|
+
}, ctx);
|
|
3168
|
+
|
|
3169
|
+
const statusResult = spawnSync("git", ["status", "--porcelain"], {
|
|
3170
|
+
cwd: projectBase,
|
|
3171
|
+
encoding: "utf8",
|
|
3172
|
+
});
|
|
3173
|
+
assert.equal(statusResult.status, 0, statusResult.stderr);
|
|
3174
|
+
assert.equal(process.cwd(), projectBase);
|
|
3175
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3176
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "stash-assisted.ts")));
|
|
3177
|
+
assert.equal(fs.readFileSync(stagedBasePath, "utf8"), "staged base dirty\n");
|
|
3178
|
+
assert.equal(fs.readFileSync(worktreeBasePath, "utf8"), "worktree base dirty\n");
|
|
3179
|
+
assert.match(statusResult.stdout, /staged-base\.txt/);
|
|
3180
|
+
assert.match(statusResult.stdout, /worktree-base\.txt/);
|
|
3181
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3182
|
+
assert.notEqual(
|
|
3183
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3184
|
+
cwd: projectBase,
|
|
3185
|
+
encoding: "utf8",
|
|
3186
|
+
}).status,
|
|
3187
|
+
0,
|
|
3188
|
+
);
|
|
3189
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3190
|
+
assert.ok(
|
|
3191
|
+
ctx.__state.notifications.some(
|
|
3192
|
+
(entry) => entry.level === "info"
|
|
3193
|
+
&& /WARNING: Restored base-path changes after merge; base-path is not clean\./.test(entry.message),
|
|
3194
|
+
),
|
|
3195
|
+
);
|
|
3196
|
+
} finally {
|
|
3197
|
+
process.chdir(previousCwd);
|
|
3198
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
|
|
2346
3202
|
/**
|
|
2347
3203
|
* @brief Verifies prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands.
|
|
2348
3204
|
* @details Simulates the `/tmp/debug.json` follow-up failure mode: one successful worktree-backed `req-change` run restores `base-path` and deletes its worktree, then a later `req-change` invocation receives a stale command context whose `cwd` and session getters still point at the removed execution worktree. The test proves command bootstrap MUST discard deleted-worktree cwd/session residue, prepare a fresh worktree with a new identifier, and complete the second successful run without reusing the removed execution path.
|
|
@@ -3996,6 +4852,58 @@ test("session_start renders the 0-percent context icon when context usage is emp
|
|
|
3996
4852
|
);
|
|
3997
4853
|
});
|
|
3998
4854
|
|
|
4855
|
+
/**
|
|
4856
|
+
* @brief Verifies direct status renders refresh context usage before drawing the footer.
|
|
4857
|
+
* @details Exercises `renderPiUsereqStatus(...)` without going through an intercepted lifecycle hook, then mutates the fake `getContextUsage()` result to simulate a post-compaction context reset. The test proves direct rerender call sites must fetch fresh context usage so the `context:` gauge updates immediately instead of reusing stale controller state.
|
|
4858
|
+
* @return {void} No return value.
|
|
4859
|
+
* @throws {AssertionError} Throws when a direct render ignores the live `getContextUsage()` snapshot or fails to clear the gauge after context reset.
|
|
4860
|
+
* @satisfies REQ-118, REQ-119, REQ-122, REQ-284
|
|
4861
|
+
*/
|
|
4862
|
+
test("direct status rerenders refresh the context gauge after context reset", () => {
|
|
4863
|
+
const cwd = createTempDir("pi-usereq-context-direct-render-");
|
|
4864
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4865
|
+
const controller = createPiUsereqStatusController();
|
|
4866
|
+
setPiUsereqStatusConfig(controller, getDefaultConfig(cwd));
|
|
4867
|
+
const contextUsage: { tokens: number | null; contextWindow: number; percent: number | null } = {
|
|
4868
|
+
tokens: 910,
|
|
4869
|
+
contextWindow: 1000,
|
|
4870
|
+
percent: 91,
|
|
4871
|
+
};
|
|
4872
|
+
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
4873
|
+
getContextUsage: () => ({ ...contextUsage }),
|
|
4874
|
+
});
|
|
4875
|
+
|
|
4876
|
+
renderPiUsereqStatus(controller, ctx);
|
|
4877
|
+
assert.equal(
|
|
4878
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
4879
|
+
buildExpectedFakeStatusText({
|
|
4880
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4881
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4882
|
+
testsDir: "tests",
|
|
4883
|
+
srcDir: ["src"],
|
|
4884
|
+
contextFilledCells: 10,
|
|
4885
|
+
contextPercent: 91,
|
|
4886
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4887
|
+
}),
|
|
4888
|
+
);
|
|
4889
|
+
|
|
4890
|
+
contextUsage.tokens = null;
|
|
4891
|
+
contextUsage.percent = null;
|
|
4892
|
+
renderPiUsereqStatus(controller, ctx);
|
|
4893
|
+
assert.equal(
|
|
4894
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
4895
|
+
buildExpectedFakeStatusText({
|
|
4896
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4897
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4898
|
+
testsDir: "tests",
|
|
4899
|
+
srcDir: ["src"],
|
|
4900
|
+
contextFilledCells: 0,
|
|
4901
|
+
contextPercent: null,
|
|
4902
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4903
|
+
}),
|
|
4904
|
+
);
|
|
4905
|
+
});
|
|
4906
|
+
|
|
3999
4907
|
/**
|
|
4000
4908
|
* @brief Verifies stale replacement contexts do not break workflow-state rendering.
|
|
4001
4909
|
* @details Simulates the guarded pi runtime error thrown when an extension context is accessed after session replacement or reload. The test proves workflow-state updates must preserve internal state while suppressing stale-context render failures instead of surfacing extension errors.
|
|
@@ -4092,12 +5000,12 @@ test("context hook refreshes context usage and rounds progress cells upward", as
|
|
|
4092
5000
|
|
|
4093
5001
|
/**
|
|
4094
5002
|
* @brief Verifies context-gauge icon cutovers below the error range.
|
|
4095
|
-
* @details Replays deterministic context percentages at documented threshold boundaries below `90%` so the status bar proves the `▕▂▏`, `▕▄▏`, `▕▆▏`, and `▕█▏` transitions while
|
|
5003
|
+
* @details Replays deterministic context percentages at documented threshold boundaries below `90%` so the status bar proves the `▕▂▏`, `▕▄▏`, `▕▆▏`, and `▕█▏` transitions while rendering every non-error band with the same warning-token color used by the `status` value. Runtime is O(n) in case count. Side effects are limited to in-memory status updates.
|
|
4096
5004
|
* @return {Promise<void>} Promise resolved after all threshold assertions complete.
|
|
4097
|
-
* @throws {AssertionError} Throws when any threshold renders the wrong icon.
|
|
4098
|
-
* @satisfies TST-096
|
|
5005
|
+
* @throws {AssertionError} Throws when any threshold renders the wrong icon or low-band color.
|
|
5006
|
+
* @satisfies REQ-122, REQ-126, TST-096
|
|
4099
5007
|
*/
|
|
4100
|
-
test("context hook applies quarter-threshold gauge icons below the error range", async () => {
|
|
5008
|
+
test("context hook applies warning-colored quarter-threshold gauge icons below the error range", async () => {
|
|
4101
5009
|
const cwd = createTempDir("pi-usereq-context-threshold-status-");
|
|
4102
5010
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4103
5011
|
const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
|
|
@@ -4476,11 +5384,12 @@ test("configuration menu orders tool toggles and can enable embedded builtin too
|
|
|
4476
5384
|
"references",
|
|
4477
5385
|
"search",
|
|
4478
5386
|
"static-check",
|
|
5387
|
+
"summarize",
|
|
4479
5388
|
"tokens",
|
|
4480
5389
|
"files-compress",
|
|
4481
|
-
"files-references",
|
|
4482
5390
|
"files-search",
|
|
4483
5391
|
"files-static-check",
|
|
5392
|
+
"files-summarize",
|
|
4484
5393
|
"files-tokens",
|
|
4485
5394
|
"bash",
|
|
4486
5395
|
"edit",
|
|
@@ -4515,7 +5424,7 @@ test("configuration menu can disable configurable active tools", async () => {
|
|
|
4515
5424
|
assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
|
|
4516
5425
|
});
|
|
4517
5426
|
|
|
4518
|
-
test("configuration menu can persist notify and sound settings", async () => {
|
|
5427
|
+
test("configuration menu can persist notify and boot-sound settings without changing runtime sound", async () => {
|
|
4519
5428
|
const cwd = createTempDir("pi-usereq-menu-notify-");
|
|
4520
5429
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4521
5430
|
const pi = createFakePi();
|
|
@@ -4529,7 +5438,7 @@ test("configuration menu can persist notify and sound settings", async () => {
|
|
|
4529
5438
|
"Notification events",
|
|
4530
5439
|
"Prompt failed",
|
|
4531
5440
|
"Save and close",
|
|
4532
|
-
"Enable sound",
|
|
5441
|
+
"Enable sound (boot value)",
|
|
4533
5442
|
"high",
|
|
4534
5443
|
"Sound events",
|
|
4535
5444
|
"Prompt interrupted",
|
|
@@ -4551,6 +5460,7 @@ test("configuration menu can persist notify and sound settings", async () => {
|
|
|
4551
5460
|
],
|
|
4552
5461
|
});
|
|
4553
5462
|
|
|
5463
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
4554
5464
|
await command!.handler("", ctx);
|
|
4555
5465
|
|
|
4556
5466
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
@@ -4576,7 +5486,7 @@ test("configuration menu can persist notify and sound settings", async () => {
|
|
|
4576
5486
|
srcDir: ["src"],
|
|
4577
5487
|
contextFilledCells: 0,
|
|
4578
5488
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4579
|
-
sound: "
|
|
5489
|
+
sound: "none",
|
|
4580
5490
|
}),
|
|
4581
5491
|
);
|
|
4582
5492
|
});
|
|
@@ -4662,7 +5572,7 @@ test("notifications menu escapes pushover text and locks pushover enablement unt
|
|
|
4662
5572
|
const renderedNotificationsMenu = (ctx.__state.customRenderLines[2] ?? []).join("\n");
|
|
4663
5573
|
assert.match(renderedNotificationsMenu, /first line\\nsecond line/);
|
|
4664
5574
|
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
4665
|
-
assert.match(renderedNotificationsMenu, /<dim>
|
|
5575
|
+
assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
|
|
4666
5576
|
});
|
|
4667
5577
|
|
|
4668
5578
|
test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
|
|
@@ -5125,9 +6035,11 @@ test("sound routing honors the selected sound state and per-event toggles", asyn
|
|
|
5125
6035
|
}
|
|
5126
6036
|
});
|
|
5127
6037
|
|
|
5128
|
-
test("sound toggle shortcut cycles
|
|
6038
|
+
test("sound toggle shortcut cycles active runtime pi-notify sound levels without persisting boot config", async () => {
|
|
5129
6039
|
const cwd = createTempDir("pi-usereq-shortcut-notify-");
|
|
5130
6040
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
6041
|
+
const persistedConfigText = `${JSON.stringify(getDefaultConfig(cwd), null, 2)}\n`;
|
|
6042
|
+
fs.writeFileSync(getProjectConfigPath(cwd), persistedConfigText, "utf8");
|
|
5131
6043
|
const previousCwd = process.cwd();
|
|
5132
6044
|
const pi = createFakePi();
|
|
5133
6045
|
process.chdir(cwd);
|
|
@@ -5144,8 +6056,7 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
5144
6056
|
|
|
5145
6057
|
for (const expectedSound of ["low", "mid", "high", "none"]) {
|
|
5146
6058
|
await shortcut!.handler(ctx);
|
|
5147
|
-
|
|
5148
|
-
assert.equal(config["notify-sound"], expectedSound);
|
|
6059
|
+
assert.equal(fs.readFileSync(getProjectConfigPath(cwd), "utf8"), persistedConfigText);
|
|
5149
6060
|
assert.equal(
|
|
5150
6061
|
ctx.__state.statuses.get("pi-usereq"),
|
|
5151
6062
|
buildExpectedFakeStatusText({
|
|
@@ -5161,6 +6072,42 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
5161
6072
|
}
|
|
5162
6073
|
});
|
|
5163
6074
|
|
|
6075
|
+
test("session_start loads the active runtime sound from persisted boot config", async () => {
|
|
6076
|
+
const cwd = createTempDir("pi-usereq-session-start-sound-");
|
|
6077
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
6078
|
+
const persistedConfig = getDefaultConfig(cwd);
|
|
6079
|
+
persistedConfig["notify-sound"] = "mid";
|
|
6080
|
+
fs.writeFileSync(
|
|
6081
|
+
getProjectConfigPath(cwd),
|
|
6082
|
+
`${JSON.stringify(persistedConfig, null, 2)}\n`,
|
|
6083
|
+
"utf8",
|
|
6084
|
+
);
|
|
6085
|
+
const previousCwd = process.cwd();
|
|
6086
|
+
const pi = createFakePi();
|
|
6087
|
+
process.chdir(cwd);
|
|
6088
|
+
try {
|
|
6089
|
+
piUsereqExtension(pi);
|
|
6090
|
+
} finally {
|
|
6091
|
+
process.chdir(previousCwd);
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
const ctx = createFakeCtx(cwd);
|
|
6095
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
6096
|
+
|
|
6097
|
+
assert.equal(
|
|
6098
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
6099
|
+
buildExpectedFakeStatusText({
|
|
6100
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
6101
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
6102
|
+
testsDir: "tests",
|
|
6103
|
+
srcDir: ["src"],
|
|
6104
|
+
contextFilledCells: 0,
|
|
6105
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
6106
|
+
sound: "mid",
|
|
6107
|
+
}),
|
|
6108
|
+
);
|
|
6109
|
+
});
|
|
6110
|
+
|
|
5164
6111
|
test("static-check exposes the supported programming languages", () => {
|
|
5165
6112
|
const supported = getSupportedStaticCheckLanguageSupport();
|
|
5166
6113
|
assert.equal(supported.length, 20);
|