pi-usereq 0.11.0 → 0.13.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 +59 -0
- package/README.md +6 -6
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +1135 -834
- package/pi-usereq/docs/REQUIREMENTS.md +177 -110
- package/pi-usereq/docs/WORKFLOW.md +244 -79
- 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/config.ts +541 -180
- package/src/core/extension-status.ts +69 -12
- package/src/core/path-context.ts +19 -4
- 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 +601 -116
- package/tests/attended-results-scenarios.ts +15 -9
- package/tests/cli-command-option-parity.test.ts +53 -35
- package/tests/debug-extension-harness.test.ts +8 -10
- package/tests/extension-registration.test.ts +1204 -205
- package/tests/helpers.ts +29 -6
- 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,10 +5,18 @@ 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,
|
|
11
18
|
getDefaultConfig,
|
|
19
|
+
getGlobalConfigPath,
|
|
12
20
|
getProjectConfigPath,
|
|
13
21
|
loadConfig,
|
|
14
22
|
} from "../src/core/config.js";
|
|
@@ -45,11 +53,19 @@ import {
|
|
|
45
53
|
} from "../src/core/pi-usereq-tools.js";
|
|
46
54
|
import {
|
|
47
55
|
createPiUsereqStatusController,
|
|
56
|
+
renderPiUsereqStatus,
|
|
48
57
|
setPiUsereqStatusConfig,
|
|
49
58
|
setPiUsereqWorkflowState,
|
|
50
59
|
} from "../src/core/extension-status.js";
|
|
60
|
+
import { showPiUsereqSettingsMenu } from "../src/core/settings-menu.js";
|
|
51
61
|
import { getSupportedStaticCheckLanguageSupport } from "../src/core/static-check.js";
|
|
52
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
createTempDir,
|
|
64
|
+
initFixtureRepo,
|
|
65
|
+
readGlobalConfigJson,
|
|
66
|
+
readProjectConfigJson,
|
|
67
|
+
saveFixtureConfigs,
|
|
68
|
+
} from "./helpers.js";
|
|
53
69
|
|
|
54
70
|
/**
|
|
55
71
|
* @brief Describes one fake extension command registration captured during tests.
|
|
@@ -82,17 +98,16 @@ type RegisteredTool = {
|
|
|
82
98
|
type RegisteredShortcut = { description?: string; handler: (ctx: any) => Promise<void> | void };
|
|
83
99
|
|
|
84
100
|
/**
|
|
85
|
-
* @brief Applies targeted
|
|
86
|
-
* @details Loads the
|
|
101
|
+
* @brief Applies targeted effective-config overrides inside one test project.
|
|
102
|
+
* @details Loads the merged local/global config, merges the supplied top-level overrides, rewrites the split Node config files plus the oracle mirror, and commits the repository-local config artifacts when the fixture is a git repo. Runtime is O(n) in config size. Side effects include filesystem reads, file overwrite, and optional git commits.
|
|
87
103
|
* @param[in] projectBase {string} Fixture project root.
|
|
88
104
|
* @param[in] overrides {Record<string, unknown>} Top-level config overrides.
|
|
89
105
|
* @return {void} No return value.
|
|
90
106
|
*/
|
|
91
107
|
function writeProjectConfigOverrides(projectBase: string, overrides: Record<string, unknown>): void {
|
|
92
|
-
const
|
|
93
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
108
|
+
const config = loadConfig(projectBase);
|
|
94
109
|
Object.assign(config, overrides);
|
|
95
|
-
|
|
110
|
+
saveFixtureConfigs(projectBase, config);
|
|
96
111
|
if (fs.existsSync(path.join(projectBase, ".git"))) {
|
|
97
112
|
const gitEnv = {
|
|
98
113
|
...process.env,
|
|
@@ -429,7 +444,7 @@ function buildExpectedFakeBranchValue(basePath: string | undefined): string {
|
|
|
429
444
|
|
|
430
445
|
/**
|
|
431
446
|
* @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.
|
|
447
|
+
* @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
448
|
* @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
|
|
434
449
|
* @return {string} Encoded context-gauge string.
|
|
435
450
|
*/
|
|
@@ -439,7 +454,7 @@ function buildExpectedFakeContextBar(options: {
|
|
|
439
454
|
}): string {
|
|
440
455
|
const percent = options.percent;
|
|
441
456
|
if (percent === undefined || percent === null || percent <= 0) {
|
|
442
|
-
return "▕_▏";
|
|
457
|
+
return formatFakeThemeForeground("warning", "▕_▏");
|
|
443
458
|
}
|
|
444
459
|
if (percent >= 100) {
|
|
445
460
|
return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
|
|
@@ -448,15 +463,15 @@ function buildExpectedFakeContextBar(options: {
|
|
|
448
463
|
return formatFakeThemeForeground("error", "▕█▏");
|
|
449
464
|
}
|
|
450
465
|
if (percent < 25) {
|
|
451
|
-
return "▕▂▏";
|
|
466
|
+
return formatFakeThemeForeground("warning", "▕▂▏");
|
|
452
467
|
}
|
|
453
468
|
if (percent < 50) {
|
|
454
|
-
return "▕▄▏";
|
|
469
|
+
return formatFakeThemeForeground("warning", "▕▄▏");
|
|
455
470
|
}
|
|
456
471
|
if (percent < 75) {
|
|
457
|
-
return "▕▆▏";
|
|
472
|
+
return formatFakeThemeForeground("warning", "▕▆▏");
|
|
458
473
|
}
|
|
459
|
-
return "▕█▏";
|
|
474
|
+
return formatFakeThemeForeground("warning", "▕█▏");
|
|
460
475
|
}
|
|
461
476
|
|
|
462
477
|
/**
|
|
@@ -510,13 +525,13 @@ function buildExpectedFakeBasePath(cwd: string): string {
|
|
|
510
525
|
}
|
|
511
526
|
|
|
512
527
|
/**
|
|
513
|
-
* @brief Formats one expected home-relative
|
|
514
|
-
* @details Resolves the supplied
|
|
515
|
-
* @param[in]
|
|
516
|
-
* @return {string} Home-relative or absolute
|
|
528
|
+
* @brief Formats one expected home-relative display path.
|
|
529
|
+
* @details Resolves the supplied path, emits `~` or `~/...` when the path is inside the current user home directory, and otherwise returns the slash-normalized absolute path. Runtime is O(p) in path length. No external state is mutated.
|
|
530
|
+
* @param[in] targetPath {string} Absolute or relative path candidate.
|
|
531
|
+
* @return {string} Home-relative or absolute display path.
|
|
517
532
|
*/
|
|
518
|
-
function
|
|
519
|
-
const resolvedPath = path.resolve(
|
|
533
|
+
function buildExpectedDisplayPath(targetPath: string): string {
|
|
534
|
+
const resolvedPath = path.resolve(targetPath);
|
|
520
535
|
const homePath = path.resolve(process.env.HOME ?? os.homedir());
|
|
521
536
|
if (resolvedPath === homePath) {
|
|
522
537
|
return "~";
|
|
@@ -529,15 +544,32 @@ function buildExpectedNotifyBasePath(cwd: string): string {
|
|
|
529
544
|
}
|
|
530
545
|
|
|
531
546
|
/**
|
|
532
|
-
* @brief Formats
|
|
533
|
-
* @details
|
|
547
|
+
* @brief Formats one expected home-relative base path for notify-template assertions.
|
|
548
|
+
* @details Delegates to the generic display-path formatter so notify-template assertions match the runtime `~`-relative path contract. Runtime is O(p) in path length. No external state is mutated.
|
|
549
|
+
* @param[in] cwd {string} Runtime working directory.
|
|
550
|
+
* @return {string} Home-relative or absolute base path used by notification templates.
|
|
551
|
+
*/
|
|
552
|
+
function buildExpectedNotifyBasePath(cwd: string): string {
|
|
553
|
+
return buildExpectedDisplayPath(cwd);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* @brief Formats the expected `Show local configuration` path value.
|
|
558
|
+
* @details Resolves the current local project config path and rewrites a leading home-directory prefix to `~` so top-level menu assertions match the documented display-only substitution without changing stored paths. Runtime is O(p) in path length. No external state is mutated.
|
|
534
559
|
* @param[in] cwd {string} Runtime working directory.
|
|
535
|
-
* @return {string} `~`-relative or absolute config-path display string.
|
|
560
|
+
* @return {string} `~`-relative or absolute local config-path display string.
|
|
561
|
+
*/
|
|
562
|
+
function buildExpectedShowLocalConfigPath(cwd: string): string {
|
|
563
|
+
return buildExpectedDisplayPath(getProjectConfigPath(cwd));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* @brief Formats the expected `Show global configuration` path value.
|
|
568
|
+
* @details Resolves the current global config path and rewrites a leading home-directory prefix to `~` so top-level menu assertions match the documented display-only substitution without changing stored paths. Runtime is O(p) in path length. No external state is mutated.
|
|
569
|
+
* @return {string} `~`-relative or absolute global config-path display string.
|
|
536
570
|
*/
|
|
537
|
-
function
|
|
538
|
-
return
|
|
539
|
-
? `${buildExpectedNotifyBasePath(cwd)}/.pi-usereq.json`
|
|
540
|
-
: getProjectConfigPath(cwd).split(path.sep).join("/");
|
|
571
|
+
function buildExpectedShowGlobalConfigPath(): string {
|
|
572
|
+
return buildExpectedDisplayPath(getGlobalConfigPath());
|
|
541
573
|
}
|
|
542
574
|
|
|
543
575
|
/**
|
|
@@ -785,6 +817,7 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
785
817
|
"req-recreate",
|
|
786
818
|
"req-refactor",
|
|
787
819
|
"req-references",
|
|
820
|
+
"req-reset",
|
|
788
821
|
"req-renumber",
|
|
789
822
|
"req-workflow",
|
|
790
823
|
"req-write",
|
|
@@ -795,9 +828,10 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
795
828
|
|
|
796
829
|
for (const name of [
|
|
797
830
|
"files-tokens",
|
|
798
|
-
"files-
|
|
831
|
+
"files-summarize",
|
|
799
832
|
"files-compress",
|
|
800
833
|
"files-search",
|
|
834
|
+
"summarize",
|
|
801
835
|
"references",
|
|
802
836
|
"compress",
|
|
803
837
|
"search",
|
|
@@ -813,9 +847,10 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
813
847
|
const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
|
|
814
848
|
for (const name of [
|
|
815
849
|
"files-tokens",
|
|
816
|
-
"files-
|
|
850
|
+
"files-summarize",
|
|
817
851
|
"files-compress",
|
|
818
852
|
"files-search",
|
|
853
|
+
"summarize",
|
|
819
854
|
"references",
|
|
820
855
|
"compress",
|
|
821
856
|
"search",
|
|
@@ -827,6 +862,46 @@ test("extension registers prompt and config commands while exposing tool capabil
|
|
|
827
862
|
}
|
|
828
863
|
});
|
|
829
864
|
|
|
865
|
+
/**
|
|
866
|
+
* @brief Verifies `req-references` registration is independent from bundled prompt inventory.
|
|
867
|
+
* @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.
|
|
868
|
+
* @return {void} No return value.
|
|
869
|
+
* @throws {AssertionError} Throws when `references` remains in the bundled prompt inventory or the command description diverges.
|
|
870
|
+
* @satisfies TST-102
|
|
871
|
+
*/
|
|
872
|
+
test("req-references registers outside the bundled prompt inventory and keeps its dedicated description", () => {
|
|
873
|
+
const bundledPromptNames = PROMPT_COMMAND_NAMES as readonly string[];
|
|
874
|
+
assert.equal(bundledPromptNames.includes("references"), false);
|
|
875
|
+
|
|
876
|
+
const pi = createFakePi();
|
|
877
|
+
piUsereqExtension(pi);
|
|
878
|
+
|
|
879
|
+
assert.equal(
|
|
880
|
+
pi.commands.get("req-references")?.description,
|
|
881
|
+
REQ_REFERENCES_COMMAND_DESCRIPTION,
|
|
882
|
+
);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* @brief Verifies `req-reset` registration is independent from bundled prompt inventory.
|
|
887
|
+
* @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.
|
|
888
|
+
* @return {void} No return value.
|
|
889
|
+
* @throws {AssertionError} Throws when `reset` remains in the bundled prompt inventory or the command description diverges.
|
|
890
|
+
* @satisfies TST-105
|
|
891
|
+
*/
|
|
892
|
+
test("req-reset registers outside the bundled prompt inventory and keeps its dedicated description", () => {
|
|
893
|
+
const bundledPromptNames = PROMPT_COMMAND_NAMES as readonly string[];
|
|
894
|
+
assert.equal(bundledPromptNames.includes("reset"), false);
|
|
895
|
+
|
|
896
|
+
const pi = createFakePi();
|
|
897
|
+
piUsereqExtension(pi);
|
|
898
|
+
|
|
899
|
+
assert.equal(
|
|
900
|
+
pi.commands.get("req-reset")?.description,
|
|
901
|
+
REQ_RESET_COMMAND_DESCRIPTION,
|
|
902
|
+
);
|
|
903
|
+
});
|
|
904
|
+
|
|
830
905
|
test("token tools register agent-oriented descriptions and schema details", () => {
|
|
831
906
|
const pi = createFakePi();
|
|
832
907
|
piUsereqExtension(pi);
|
|
@@ -845,24 +920,37 @@ test("token tools register agent-oriented descriptions and schema details", () =
|
|
|
845
920
|
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic pack-summary text/);
|
|
846
921
|
});
|
|
847
922
|
|
|
848
|
-
test("
|
|
923
|
+
test("summary tools register agent-oriented descriptions and schema details", () => {
|
|
924
|
+
const pi = createFakePi();
|
|
925
|
+
piUsereqExtension(pi);
|
|
926
|
+
|
|
927
|
+
const filesSummarize = pi.tools.find((tool: RegisteredTool) => tool.name === "files-summarize");
|
|
928
|
+
const summarize = pi.tools.find((tool: RegisteredTool) => tool.name === "summarize");
|
|
929
|
+
assert.ok(filesSummarize, "missing files-summarize tool");
|
|
930
|
+
assert.ok(summarize, "missing summarize tool");
|
|
931
|
+
|
|
932
|
+
assert.match(filesSummarize.description ?? "", /monolithic summary markdown report/);
|
|
933
|
+
assert.ok(filesSummarize.promptGuidelines?.some((line) => line.includes("monolithic markdown")));
|
|
934
|
+
assert.ok(filesSummarize.promptGuidelines?.some((line) => line.includes("Formatting contract:")));
|
|
935
|
+
assert.match(String((filesSummarize.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
936
|
+
|
|
937
|
+
assert.match(summarize.description ?? "", /configured project source directories/);
|
|
938
|
+
assert.ok(summarize.promptGuidelines?.some((line) => line.includes("file-structure markdown block")));
|
|
939
|
+
assert.ok(summarize.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
|
|
940
|
+
assert.match(String((summarize.parameters as { description?: string } | undefined)?.description ?? ""), /monolithic markdown/);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("references tool registers agent-oriented descriptions and schema details", () => {
|
|
849
944
|
const pi = createFakePi();
|
|
850
945
|
piUsereqExtension(pi);
|
|
851
946
|
|
|
852
|
-
const filesReferences = pi.tools.find((tool: RegisteredTool) => tool.name === "files-references");
|
|
853
947
|
const references = pi.tools.find((tool: RegisteredTool) => tool.name === "references");
|
|
854
|
-
assert.ok(filesReferences, "missing files-references tool");
|
|
855
948
|
assert.ok(references, "missing references tool");
|
|
856
949
|
|
|
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/);
|
|
950
|
+
assert.match(references.description ?? "", /configured project source directories and configured docs-dir/);
|
|
951
|
+
assert.ok(references.promptGuidelines?.some((line) => line.includes("overwrite `<docs-dir>/REFERENCES.md`")));
|
|
952
|
+
assert.ok(references.promptGuidelines?.some((line) => line.includes("generated markdown is never returned")));
|
|
953
|
+
assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /content\[0\]\.text returns only `success` or `error: <diagnostic>`/);
|
|
866
954
|
});
|
|
867
955
|
|
|
868
956
|
test("search tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -913,9 +1001,10 @@ test("agent tools define custom renderResult with compact and expanded structure
|
|
|
913
1001
|
piUsereqExtension(pi);
|
|
914
1002
|
const toolNames = [
|
|
915
1003
|
"files-tokens",
|
|
916
|
-
"files-
|
|
1004
|
+
"files-summarize",
|
|
917
1005
|
"files-compress",
|
|
918
1006
|
"files-search",
|
|
1007
|
+
"summarize",
|
|
919
1008
|
"references",
|
|
920
1009
|
"compress",
|
|
921
1010
|
"search",
|
|
@@ -963,7 +1052,7 @@ test("files-tokens returns monolithic token summary text with execution diagnost
|
|
|
963
1052
|
docs: {
|
|
964
1053
|
"REQUIREMENTS.md": "---\ntitle: Requirements\n---\n# Requirements\nAlpha\nBeta\n",
|
|
965
1054
|
"WORKFLOW.md": "# Workflow\nStep one\n",
|
|
966
|
-
"REFERENCES.md": "#
|
|
1055
|
+
"REFERENCES.md": "# Summarize\nEntry one\n",
|
|
967
1056
|
},
|
|
968
1057
|
});
|
|
969
1058
|
try {
|
|
@@ -1011,7 +1100,7 @@ test("files-tokens defers js-tiktoken loading until execution and returns monoli
|
|
|
1011
1100
|
}
|
|
1012
1101
|
});
|
|
1013
1102
|
|
|
1014
|
-
test("files-
|
|
1103
|
+
test("files-summarize returns monolithic summary markdown", async () => {
|
|
1015
1104
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1016
1105
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
1017
1106
|
fs.writeFileSync(samplePath, `/**
|
|
@@ -1032,7 +1121,7 @@ export function buildSample(input: string): string {
|
|
|
1032
1121
|
try {
|
|
1033
1122
|
const pi = createFakePi();
|
|
1034
1123
|
piUsereqExtension(pi);
|
|
1035
|
-
const result = await executeRegisteredTool(pi, "files-
|
|
1124
|
+
const result = await executeRegisteredTool(pi, "files-summarize", projectBase, {
|
|
1036
1125
|
files: ["src/sample.ts", "src/missing.ts"],
|
|
1037
1126
|
}) as {
|
|
1038
1127
|
content?: Array<{ type: string; text?: string }>;
|
|
@@ -1051,7 +1140,7 @@ export function buildSample(input: string): string {
|
|
|
1051
1140
|
}
|
|
1052
1141
|
});
|
|
1053
1142
|
|
|
1054
|
-
test("
|
|
1143
|
+
test("summarize returns monolithic project summary markdown", async () => {
|
|
1055
1144
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1056
1145
|
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
1057
1146
|
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
@@ -1059,7 +1148,7 @@ test("references returns monolithic project references markdown", async () => {
|
|
|
1059
1148
|
try {
|
|
1060
1149
|
const pi = createFakePi();
|
|
1061
1150
|
piUsereqExtension(pi);
|
|
1062
|
-
const result = await executeRegisteredTool(pi, "
|
|
1151
|
+
const result = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
1063
1152
|
content?: Array<{ type: string; text?: string }>;
|
|
1064
1153
|
details?: { execution: { code: number; stderr_lines?: string[] } };
|
|
1065
1154
|
};
|
|
@@ -1077,6 +1166,85 @@ test("references returns monolithic project references markdown", async () => {
|
|
|
1077
1166
|
}
|
|
1078
1167
|
});
|
|
1079
1168
|
|
|
1169
|
+
test("references overwrites the configured REFERENCES.md and returns status-only success", async () => {
|
|
1170
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1171
|
+
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
1172
|
+
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
1173
|
+
fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
|
|
1174
|
+
const docsDir = path.join(projectBase, "docs", "custom");
|
|
1175
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
1176
|
+
fs.writeFileSync(path.join(docsDir, "REFERENCES.md"), "stale\n", "utf8");
|
|
1177
|
+
const configPath = getProjectConfigPath(projectBase);
|
|
1178
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
1179
|
+
config["docs-dir"] = "docs/custom";
|
|
1180
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
1181
|
+
|
|
1182
|
+
try {
|
|
1183
|
+
const pi = createFakePi();
|
|
1184
|
+
piUsereqExtension(pi);
|
|
1185
|
+
const referencesResult = await executeRegisteredTool(pi, "references", projectBase, {}) as {
|
|
1186
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1187
|
+
details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
|
|
1188
|
+
};
|
|
1189
|
+
const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
1190
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const referencesPath = path.join(docsDir, "REFERENCES.md");
|
|
1194
|
+
const writtenText = fs.readFileSync(referencesPath, "utf8");
|
|
1195
|
+
assert.equal(referencesResult.content?.[0]?.text ?? "", "success");
|
|
1196
|
+
assert.equal(writtenText.trimEnd(), summarizeResult.content?.[0]?.text ?? "");
|
|
1197
|
+
assert.doesNotMatch(writtenText, /^stale$/m);
|
|
1198
|
+
assert.equal(referencesResult.details?.execution.code, 0);
|
|
1199
|
+
assert.deepEqual(referencesResult.details?.execution.stderr_lines, undefined);
|
|
1200
|
+
assert.deepEqual(referencesResult.details?.execution.stdout_lines, undefined);
|
|
1201
|
+
} finally {
|
|
1202
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
test("references returns status-only errors for source-discovery and write failures", async () => {
|
|
1207
|
+
const sourceFailure = initFixtureRepo({ fixtures: [] });
|
|
1208
|
+
try {
|
|
1209
|
+
const pi = createFakePi();
|
|
1210
|
+
piUsereqExtension(pi);
|
|
1211
|
+
const result = await executeRegisteredTool(pi, "references", sourceFailure.projectBase, {}) as {
|
|
1212
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1213
|
+
details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
assert.equal(result.content?.[0]?.text ?? "", "error: no source files found in configured directories.");
|
|
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) => line.includes("no source files found in configured directories.")));
|
|
1220
|
+
} finally {
|
|
1221
|
+
fs.rmSync(sourceFailure.projectBase, { recursive: true, force: true });
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const writeFailure = initFixtureRepo({ fixtures: [] });
|
|
1225
|
+
fs.writeFileSync(path.join(writeFailure.projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
1226
|
+
const writeFailureConfigPath = getProjectConfigPath(writeFailure.projectBase);
|
|
1227
|
+
const writeFailureConfig = JSON.parse(fs.readFileSync(writeFailureConfigPath, "utf8")) as Record<string, unknown>;
|
|
1228
|
+
writeFailureConfig["docs-dir"] = "docs/missing/custom";
|
|
1229
|
+
fs.writeFileSync(writeFailureConfigPath, `${JSON.stringify(writeFailureConfig, null, 2)}\n`, "utf8");
|
|
1230
|
+
|
|
1231
|
+
try {
|
|
1232
|
+
const pi = createFakePi();
|
|
1233
|
+
piUsereqExtension(pi);
|
|
1234
|
+
const result = await executeRegisteredTool(pi, "references", writeFailure.projectBase, {}) as {
|
|
1235
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1236
|
+
details?: { execution: { code: number; stderr_lines?: string[]; stdout_lines?: string[] } };
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
assert.match(result.content?.[0]?.text ?? "", /^error: ENOENT:/);
|
|
1240
|
+
assert.equal(result.details?.execution.code, 1);
|
|
1241
|
+
assert.deepEqual(result.details?.execution.stdout_lines, undefined);
|
|
1242
|
+
assert.ok(result.details?.execution.stderr_lines?.some((line) => /^error: ENOENT:/.test(line)));
|
|
1243
|
+
} finally {
|
|
1244
|
+
fs.rmSync(writeFailure.projectBase, { recursive: true, force: true });
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1080
1248
|
test("files-compress returns monolithic compression markdown", async () => {
|
|
1081
1249
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
1082
1250
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
@@ -1264,21 +1432,21 @@ func (s *Server) Start(ctx context.Context) error {
|
|
|
1264
1432
|
const pi = createFakePi();
|
|
1265
1433
|
piUsereqExtension(pi);
|
|
1266
1434
|
|
|
1267
|
-
const
|
|
1435
|
+
const filesSummarizeResult = await executeRegisteredTool(pi, "files-summarize", projectBase, {
|
|
1268
1436
|
files: ["src/sample.go"],
|
|
1269
1437
|
}) as {
|
|
1270
1438
|
content?: Array<{ type: string; text?: string }>;
|
|
1271
1439
|
details?: { execution: { code: number } };
|
|
1272
1440
|
};
|
|
1273
|
-
assert.match(
|
|
1274
|
-
assert.equal(
|
|
1441
|
+
assert.match(filesSummarizeResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1442
|
+
assert.equal(filesSummarizeResult.details?.execution.code, 0);
|
|
1275
1443
|
|
|
1276
|
-
const
|
|
1444
|
+
const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
1277
1445
|
content?: Array<{ type: string; text?: string }>;
|
|
1278
1446
|
details?: { execution: { code: number } };
|
|
1279
1447
|
};
|
|
1280
|
-
assert.match(
|
|
1281
|
-
assert.equal(
|
|
1448
|
+
assert.match(summarizeResult.content?.[0]?.text ?? "", /\tvar buffer bytes\.Buffer/);
|
|
1449
|
+
assert.equal(summarizeResult.details?.execution.code, 0);
|
|
1282
1450
|
|
|
1283
1451
|
const filesCompressResult = await executeRegisteredTool(pi, "files-compress", projectBase, {
|
|
1284
1452
|
files: ["src/sample.go"],
|
|
@@ -1360,19 +1528,15 @@ test("static-check tools return monolithic text payloads with minimal execution
|
|
|
1360
1528
|
assert.equal(staticCheckResult.details?.execution.code, 0);
|
|
1361
1529
|
assert.deepEqual(staticCheckResult.details?.execution.stderr_lines, undefined);
|
|
1362
1530
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
},
|
|
1373
|
-
}, null, 2)}\n`,
|
|
1374
|
-
"utf8",
|
|
1375
|
-
);
|
|
1531
|
+
saveFixtureConfigs(projectBase, {
|
|
1532
|
+
...getDefaultConfig(projectBase),
|
|
1533
|
+
"docs-dir": DEFAULT_DOCS_DIR,
|
|
1534
|
+
"tests-dir": "tests",
|
|
1535
|
+
"src-dir": ["src"],
|
|
1536
|
+
"static-check": {
|
|
1537
|
+
TypeScript: createStaticCheckLanguageConfig([{ module: "Dummy" }], "disable"),
|
|
1538
|
+
},
|
|
1539
|
+
});
|
|
1376
1540
|
const disabledFilesStaticCheckResult = await executeRegisteredTool(pi, "files-static-check", projectBase, {
|
|
1377
1541
|
files: ["src/check.ts"],
|
|
1378
1542
|
}) as {
|
|
@@ -1396,7 +1560,7 @@ test("configuration menu saves updated docs-dir in project config", async () =>
|
|
|
1396
1560
|
assert.ok(command);
|
|
1397
1561
|
|
|
1398
1562
|
await command!.handler("", createFakeCtx(cwd, { selects: ["docs-dir", "Save and close"], inputs: ["docs/custom"] }));
|
|
1399
|
-
const config =
|
|
1563
|
+
const config = readProjectConfigJson(cwd);
|
|
1400
1564
|
assert.equal(config["docs-dir"], "docs/custom");
|
|
1401
1565
|
});
|
|
1402
1566
|
|
|
@@ -1421,7 +1585,7 @@ test("configuration menu persists trailing-slash-free relative directory values"
|
|
|
1421
1585
|
|
|
1422
1586
|
await command!.handler("", ctx);
|
|
1423
1587
|
|
|
1424
|
-
const config =
|
|
1588
|
+
const config = readProjectConfigJson(cwd);
|
|
1425
1589
|
assert.equal(config["docs-dir"], "docs/custom");
|
|
1426
1590
|
assert.equal(config["tests-dir"], "tests/custom");
|
|
1427
1591
|
assert.deepEqual(config["src-dir"], ["src", "src/custom"]);
|
|
@@ -1441,12 +1605,14 @@ test("configuration menu forces worktree rows off and dim when auto git commit i
|
|
|
1441
1605
|
|
|
1442
1606
|
await command!.handler("", ctx);
|
|
1443
1607
|
|
|
1444
|
-
const
|
|
1608
|
+
const localConfig = readProjectConfigJson(cwd);
|
|
1609
|
+
const globalConfig = readGlobalConfigJson();
|
|
1445
1610
|
const defaultConfig = getDefaultConfig(cwd);
|
|
1446
1611
|
const renderedLockedMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1447
|
-
assert.equal(
|
|
1448
|
-
assert.equal(
|
|
1449
|
-
assert.equal(
|
|
1612
|
+
assert.equal(globalConfig.AUTO_GIT_COMMIT, "disable");
|
|
1613
|
+
assert.equal(globalConfig.GIT_WORKTREE_ENABLED, "disable");
|
|
1614
|
+
assert.equal(globalConfig.GIT_WORKTREE_PREFIX, defaultConfig.GIT_WORKTREE_PREFIX);
|
|
1615
|
+
assert.equal(Object.hasOwn(localConfig, "AUTO_GIT_COMMIT"), false);
|
|
1450
1616
|
assert.match(renderedLockedMenu, /<dim>Git worktree/);
|
|
1451
1617
|
assert.match(renderedLockedMenu, /<dim>Worktree prefix/);
|
|
1452
1618
|
});
|
|
@@ -1467,7 +1633,7 @@ test("configuration menu omits prompt-delivery controls and persisted config omi
|
|
|
1467
1633
|
assert.equal(fs.existsSync(getProjectConfigPath(cwd)), false);
|
|
1468
1634
|
});
|
|
1469
1635
|
|
|
1470
|
-
test("configuration menus expose
|
|
1636
|
+
test("configuration menus expose local/global config ordering and omit overview or notification-reference rows", async () => {
|
|
1471
1637
|
const cwd = fs.mkdtempSync(
|
|
1472
1638
|
path.join(process.env.HOME ?? process.cwd(), "pi-usereq-menu-structure-"),
|
|
1473
1639
|
);
|
|
@@ -1493,20 +1659,28 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1493
1659
|
"Enable tools",
|
|
1494
1660
|
"Notifications",
|
|
1495
1661
|
"Debug",
|
|
1496
|
-
"Show configuration",
|
|
1662
|
+
"Show local configuration",
|
|
1663
|
+
"Show global configuration",
|
|
1497
1664
|
"Reset defaults",
|
|
1498
1665
|
]);
|
|
1499
|
-
assert.
|
|
1666
|
+
assert.match(
|
|
1667
|
+
renderedMenu,
|
|
1668
|
+
new RegExp(`<dim>${buildExpectedShowLocalConfigPath(cwd).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}</dim>`),
|
|
1669
|
+
);
|
|
1670
|
+
assert.match(
|
|
1671
|
+
renderedMenu,
|
|
1672
|
+
new RegExp(`<dim>${buildExpectedShowGlobalConfigPath().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}</dim>`),
|
|
1673
|
+
);
|
|
1500
1674
|
assert.ok(renderedMenu.includes("notification:off • sound:none • pushover:off"));
|
|
1501
1675
|
assert.ok(renderedMenu.includes("disable"));
|
|
1502
1676
|
assert.doesNotMatch(renderedMenu, /beep:/);
|
|
1503
1677
|
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
1504
|
-
assert.match(renderedNotificationsMenu, /<dim>
|
|
1678
|
+
assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
|
|
1505
1679
|
assert.deepEqual(ctx.__state.selectCalls[1]?.items ?? [], [
|
|
1506
1680
|
"Enable notification",
|
|
1507
1681
|
"Notification events",
|
|
1508
1682
|
"Notify command",
|
|
1509
|
-
"Enable sound",
|
|
1683
|
+
"Enable sound (boot value)",
|
|
1510
1684
|
"Sound events",
|
|
1511
1685
|
"Sound toggle hotkey bind",
|
|
1512
1686
|
"Sound command (low vol.)",
|
|
@@ -1523,15 +1697,15 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1523
1697
|
]);
|
|
1524
1698
|
});
|
|
1525
1699
|
|
|
1526
|
-
test("show configuration saves pending config, closes the menu, and writes the persisted
|
|
1527
|
-
const cwd = createTempDir("pi-usereq-menu-show-config-");
|
|
1700
|
+
test("show local configuration saves pending config, closes the menu, and writes the persisted local config file text into the editor", async () => {
|
|
1701
|
+
const cwd = createTempDir("pi-usereq-menu-show-local-config-");
|
|
1528
1702
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1529
1703
|
const pi = createFakePi();
|
|
1530
1704
|
piUsereqExtension(pi);
|
|
1531
1705
|
const command = pi.commands.get("pi-usereq");
|
|
1532
1706
|
assert.ok(command);
|
|
1533
1707
|
const ctx = createFakeCtx(cwd, {
|
|
1534
|
-
selects: ["docs-dir", "show-config"],
|
|
1708
|
+
selects: ["docs-dir", "show-local-config"],
|
|
1535
1709
|
inputs: ["docs/custom"],
|
|
1536
1710
|
});
|
|
1537
1711
|
|
|
@@ -1546,6 +1720,28 @@ test("show configuration saves pending config, closes the menu, and writes the p
|
|
|
1546
1720
|
);
|
|
1547
1721
|
});
|
|
1548
1722
|
|
|
1723
|
+
test("show global configuration saves pending config, closes the menu, and writes the persisted global config file text into the editor", async () => {
|
|
1724
|
+
const cwd = createTempDir("pi-usereq-menu-show-global-config-");
|
|
1725
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1726
|
+
const pi = createFakePi();
|
|
1727
|
+
piUsereqExtension(pi);
|
|
1728
|
+
const command = pi.commands.get("pi-usereq");
|
|
1729
|
+
assert.ok(command);
|
|
1730
|
+
const ctx = createFakeCtx(cwd, {
|
|
1731
|
+
selects: ["Auto git commit", "show-global-config"],
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
await command!.handler("", ctx);
|
|
1735
|
+
|
|
1736
|
+
const persistedText = fs.readFileSync(getGlobalConfigPath(), "utf8");
|
|
1737
|
+
assert.equal(ctx.__state.editorText, persistedText);
|
|
1738
|
+
assert.match(ctx.__state.editorText, /"AUTO_GIT_COMMIT": "disable"/);
|
|
1739
|
+
assert.equal(
|
|
1740
|
+
ctx.__state.selectCalls.filter((call) => call.title === "pi-usereq").length,
|
|
1741
|
+
2,
|
|
1742
|
+
);
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1549
1745
|
test("descendant configuration menus end with Reset defaults only", async () => {
|
|
1550
1746
|
const cwd = createTempDir("pi-usereq-menu-terminal-rows-");
|
|
1551
1747
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1556,7 +1752,7 @@ test("descendant configuration menus end with Reset defaults only", async () =>
|
|
|
1556
1752
|
const ctx = createFakeCtx(cwd, {
|
|
1557
1753
|
selects: [
|
|
1558
1754
|
"notifications",
|
|
1559
|
-
"Enable sound",
|
|
1755
|
+
"Enable sound (boot value)",
|
|
1560
1756
|
"Save and close",
|
|
1561
1757
|
"Pushover priority",
|
|
1562
1758
|
"Save and close",
|
|
@@ -1599,14 +1795,14 @@ test("descendant configuration menus end with Reset defaults only", async () =>
|
|
|
1599
1795
|
return (ctx.__state.customRenderLines[index] ?? []).join("\n");
|
|
1600
1796
|
};
|
|
1601
1797
|
|
|
1602
|
-
assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
|
|
1798
|
+
assert.deepEqual(getTerminalRows("Enable sound (boot value)", "high"), ["Reset defaults"]);
|
|
1603
1799
|
assert.deepEqual(getTerminalRows("Pushover priority", "High"), ["Reset defaults"]);
|
|
1604
1800
|
assert.deepEqual(getTerminalRows("Enable tools", "static-check"), ["Reset defaults"]);
|
|
1605
1801
|
assert.deepEqual(getTerminalRows("Debug", "Log on status"), ["Reset defaults"]);
|
|
1606
1802
|
assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
|
|
1607
1803
|
assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
|
|
1608
1804
|
assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
|
|
1609
|
-
assert.doesNotMatch(getRenderedMenu("Enable sound", "high"), /Reset defaults.*none/);
|
|
1805
|
+
assert.doesNotMatch(getRenderedMenu("Enable sound (boot value)", "high"), /Reset defaults.*none/);
|
|
1610
1806
|
assert.doesNotMatch(getRenderedMenu("Pushover priority", "High"), /Reset defaults.*Normal/);
|
|
1611
1807
|
assert.doesNotMatch(getRenderedMenu("Enable tools", "static-check"), /Reset defaults.*defaults/);
|
|
1612
1808
|
assert.doesNotMatch(getRenderedMenu("Debug", "Log on status"), /Reset defaults.*running/);
|
|
@@ -1640,7 +1836,7 @@ test("debug menu dims locked rows and persists debug settings with focus-preserv
|
|
|
1640
1836
|
await command!.handler("", ctx);
|
|
1641
1837
|
|
|
1642
1838
|
const renderedInitialDebugMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
1643
|
-
const persistedConfig =
|
|
1839
|
+
const persistedConfig = readProjectConfigJson(cwd);
|
|
1644
1840
|
const debugCalls = ctx.__state.selectCalls.filter((call) => call.title === "Debug");
|
|
1645
1841
|
assert.match(renderedInitialDebugMenu, /<dim>Log file/);
|
|
1646
1842
|
assert.match(renderedInitialDebugMenu, /<dim>Log on status/);
|
|
@@ -1666,7 +1862,7 @@ test("debug menu dims locked rows and persists debug settings with focus-preserv
|
|
|
1666
1862
|
selects: ["Debug", "Reset defaults", "Approve reset", "Save and close"],
|
|
1667
1863
|
});
|
|
1668
1864
|
await command!.handler("", resetCtx);
|
|
1669
|
-
const resetConfig =
|
|
1865
|
+
const resetConfig = readProjectConfigJson(cwd);
|
|
1670
1866
|
assert.equal(resetConfig.DEBUG_ENABLED, "disable");
|
|
1671
1867
|
assert.equal(resetConfig.DEBUG_LOG_FILE, DEFAULT_DEBUG_LOG_FILE);
|
|
1672
1868
|
assert.equal(resetConfig.DEBUG_STATUS_CHANGES, "disable");
|
|
@@ -1711,17 +1907,13 @@ test("debug menu rows derive from canonical tool and prompt inventories", async
|
|
|
1711
1907
|
test("tool debug logging honors global enablement and workflow-status filters", async () => {
|
|
1712
1908
|
const idleBase = createTempDir("pi-usereq-tool-debug-idle-");
|
|
1713
1909
|
fs.mkdirSync(path.dirname(getProjectConfigPath(idleBase)), { recursive: true });
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1722
|
-
}, null, 2)}\n`,
|
|
1723
|
-
"utf8",
|
|
1724
|
-
);
|
|
1910
|
+
saveFixtureConfigs(idleBase, {
|
|
1911
|
+
...getDefaultConfig(idleBase),
|
|
1912
|
+
DEBUG_ENABLED: "enable",
|
|
1913
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1914
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1915
|
+
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1916
|
+
});
|
|
1725
1917
|
const idlePi = createFakePi();
|
|
1726
1918
|
piUsereqExtension(idlePi);
|
|
1727
1919
|
const idleCtx = createFakeCtx(idleBase);
|
|
@@ -1782,17 +1974,13 @@ test("tool debug logging honors global enablement and workflow-status filters",
|
|
|
1782
1974
|
|
|
1783
1975
|
const disabledBase = createTempDir("pi-usereq-tool-debug-disabled-");
|
|
1784
1976
|
fs.mkdirSync(path.dirname(getProjectConfigPath(disabledBase)), { recursive: true });
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1793
|
-
}, null, 2)}\n`,
|
|
1794
|
-
"utf8",
|
|
1795
|
-
);
|
|
1977
|
+
saveFixtureConfigs(disabledBase, {
|
|
1978
|
+
...getDefaultConfig(disabledBase),
|
|
1979
|
+
DEBUG_ENABLED: "disable",
|
|
1980
|
+
DEBUG_LOG_FILE: "tool-debug.json",
|
|
1981
|
+
DEBUG_LOG_ON_STATUS: "any",
|
|
1982
|
+
DEBUG_ENABLED_TOOLS: ["bash"],
|
|
1983
|
+
});
|
|
1796
1984
|
const disabledPi = createFakePi();
|
|
1797
1985
|
piUsereqExtension(disabledPi);
|
|
1798
1986
|
const disabledCtx = createFakeCtx(disabledBase);
|
|
@@ -1917,6 +2105,101 @@ test("notifications menu preserves focus on toggled and edited rows", async () =
|
|
|
1917
2105
|
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1918
2106
|
});
|
|
1919
2107
|
|
|
2108
|
+
/**
|
|
2109
|
+
* @brief Verifies inline toggle rows stay open and preserve selection for confirm keys.
|
|
2110
|
+
* @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.
|
|
2111
|
+
* @return {Promise<void>} Promise resolved after the raw-input interaction sequence completes.
|
|
2112
|
+
* @throws {AssertionError} Throws when the menu closes early, loses focus, or fails to rebuild dependent row styling after a toggle.
|
|
2113
|
+
* @satisfies REQ-192, TST-052
|
|
2114
|
+
*/
|
|
2115
|
+
test("shared settings menu keeps inline toggles focused and open for space and enter", async () => {
|
|
2116
|
+
let primaryEnabled = true;
|
|
2117
|
+
let component:
|
|
2118
|
+
| {
|
|
2119
|
+
handleInput?: (data: string) => void;
|
|
2120
|
+
render: (width: number) => string[];
|
|
2121
|
+
}
|
|
2122
|
+
| undefined;
|
|
2123
|
+
let resolved = false;
|
|
2124
|
+
const changes: string[] = [];
|
|
2125
|
+
const theme = {
|
|
2126
|
+
fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
|
|
2127
|
+
bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
|
|
2128
|
+
bold: (text: string) => `<bold>${text}</bold>`,
|
|
2129
|
+
};
|
|
2130
|
+
const ctx = {
|
|
2131
|
+
ui: {
|
|
2132
|
+
async custom<T>(factory: (tui: { requestRender: () => void }, customTheme: typeof theme, keybindings: Record<string, never>, done: (value?: T) => void) => unknown) {
|
|
2133
|
+
let resolveResult!: (value: T | undefined) => void;
|
|
2134
|
+
const resultPromise = new Promise<T | undefined>((resolve) => {
|
|
2135
|
+
resolveResult = (value?: T) => {
|
|
2136
|
+
resolved = true;
|
|
2137
|
+
resolve(value);
|
|
2138
|
+
};
|
|
2139
|
+
});
|
|
2140
|
+
component = factory(
|
|
2141
|
+
{ requestRender: () => undefined },
|
|
2142
|
+
theme,
|
|
2143
|
+
{},
|
|
2144
|
+
(value?: T) => resolveResult(value),
|
|
2145
|
+
) as typeof component;
|
|
2146
|
+
return resultPromise;
|
|
2147
|
+
},
|
|
2148
|
+
},
|
|
2149
|
+
} as any;
|
|
2150
|
+
const buildChoices = () => [
|
|
2151
|
+
{
|
|
2152
|
+
id: "primary",
|
|
2153
|
+
label: "Primary",
|
|
2154
|
+
value: primaryEnabled ? "enable" : "disable",
|
|
2155
|
+
values: ["enable", "disable"],
|
|
2156
|
+
description: "Toggle the primary row in place.",
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
id: "secondary",
|
|
2160
|
+
label: "Secondary",
|
|
2161
|
+
labelTone: primaryEnabled ? undefined : "dim",
|
|
2162
|
+
value: primaryEnabled ? "editable" : "locked",
|
|
2163
|
+
valueTone: primaryEnabled ? undefined : "dim",
|
|
2164
|
+
disabled: !primaryEnabled,
|
|
2165
|
+
description: "Dependent row that must rebuild without closing the menu.",
|
|
2166
|
+
},
|
|
2167
|
+
] as any;
|
|
2168
|
+
|
|
2169
|
+
const resultPromise = showPiUsereqSettingsMenu(
|
|
2170
|
+
ctx,
|
|
2171
|
+
"Toggle menu",
|
|
2172
|
+
buildChoices(),
|
|
2173
|
+
{
|
|
2174
|
+
getChoices: buildChoices,
|
|
2175
|
+
onChange: (choiceId: string, newValue: string) => {
|
|
2176
|
+
if (choiceId === "primary") {
|
|
2177
|
+
primaryEnabled = newValue === "enable";
|
|
2178
|
+
changes.push(newValue);
|
|
2179
|
+
}
|
|
2180
|
+
},
|
|
2181
|
+
} as any,
|
|
2182
|
+
);
|
|
2183
|
+
|
|
2184
|
+
assert.ok(component);
|
|
2185
|
+
assert.doesNotMatch(component.render(120).join("\n"), /<dim>Secondary/);
|
|
2186
|
+
|
|
2187
|
+
component.handleInput?.(" ");
|
|
2188
|
+
await Promise.resolve();
|
|
2189
|
+
assert.deepEqual(changes, ["disable"]);
|
|
2190
|
+
assert.equal(resolved, false);
|
|
2191
|
+
assert.match(component.render(120).join("\n"), /<dim>Secondary/);
|
|
2192
|
+
|
|
2193
|
+
component.handleInput?.("\r");
|
|
2194
|
+
await Promise.resolve();
|
|
2195
|
+
assert.deepEqual(changes, ["disable", "enable"]);
|
|
2196
|
+
assert.equal(resolved, false);
|
|
2197
|
+
assert.doesNotMatch(component.render(120).join("\n"), /<dim>Secondary/);
|
|
2198
|
+
|
|
2199
|
+
component.handleInput?.("\x1b");
|
|
2200
|
+
assert.equal(await resultPromise, undefined);
|
|
2201
|
+
});
|
|
2202
|
+
|
|
1920
2203
|
test("notifications reset defaults preserves non-notification settings", async () => {
|
|
1921
2204
|
const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
|
|
1922
2205
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1929,7 +2212,7 @@ test("notifications reset defaults preserves non-notification settings", async (
|
|
|
1929
2212
|
"docs-dir",
|
|
1930
2213
|
"notifications",
|
|
1931
2214
|
"Enable notification",
|
|
1932
|
-
"Enable sound",
|
|
2215
|
+
"Enable sound (boot value)",
|
|
1933
2216
|
"high",
|
|
1934
2217
|
"Reset defaults",
|
|
1935
2218
|
"Approve reset",
|
|
@@ -1941,11 +2224,12 @@ test("notifications reset defaults preserves non-notification settings", async (
|
|
|
1941
2224
|
|
|
1942
2225
|
await command!.handler("", ctx);
|
|
1943
2226
|
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
assert.equal(
|
|
1947
|
-
assert.equal(
|
|
1948
|
-
assert.equal(
|
|
2227
|
+
const localConfig = readProjectConfigJson(cwd);
|
|
2228
|
+
const globalConfig = readGlobalConfigJson();
|
|
2229
|
+
assert.equal(localConfig["docs-dir"], "docs/custom");
|
|
2230
|
+
assert.equal(globalConfig["notify-enabled"], false);
|
|
2231
|
+
assert.equal(globalConfig["notify-sound"], "none");
|
|
2232
|
+
assert.equal(globalConfig["notify-pushover-enabled"], false);
|
|
1949
2233
|
});
|
|
1950
2234
|
|
|
1951
2235
|
test("top-level reset defaults restores all configuration values", async () => {
|
|
@@ -1970,11 +2254,12 @@ test("top-level reset defaults restores all configuration values", async () => {
|
|
|
1970
2254
|
|
|
1971
2255
|
await command!.handler("", ctx);
|
|
1972
2256
|
|
|
1973
|
-
const
|
|
1974
|
-
|
|
1975
|
-
assert.
|
|
1976
|
-
assert.
|
|
1977
|
-
assert.equal(
|
|
2257
|
+
const localConfig = readProjectConfigJson(cwd);
|
|
2258
|
+
const globalConfig = readGlobalConfigJson();
|
|
2259
|
+
assert.equal(localConfig["docs-dir"], DEFAULT_DOCS_DIR);
|
|
2260
|
+
assert.deepEqual(localConfig["src-dir"], ["src"]);
|
|
2261
|
+
assert.equal(globalConfig["notify-enabled"], false);
|
|
2262
|
+
assert.equal(globalConfig["notify-sound"], "none");
|
|
1978
2263
|
});
|
|
1979
2264
|
|
|
1980
2265
|
test("configuration menus reuse CLI settings theme semantics", async () => {
|
|
@@ -2033,62 +2318,499 @@ test("prompt commands use the current session by default", async () => {
|
|
|
2033
2318
|
}
|
|
2034
2319
|
});
|
|
2035
2320
|
|
|
2036
|
-
|
|
2321
|
+
/**
|
|
2322
|
+
* @brief Verifies successful `req-references` completion uses the specialized direct commit workflow.
|
|
2323
|
+
* @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.
|
|
2324
|
+
* @return {Promise<void>} Promise resolved after success-path assertions complete.
|
|
2325
|
+
* @throws {AssertionError} Throws when `req-references` creates a worktree, starts a session handoff, skips the fixed commit, or leaves the repository dirty.
|
|
2326
|
+
* @satisfies TST-103
|
|
2327
|
+
*/
|
|
2328
|
+
test("req-references updates REFERENCES.md with a direct commit workflow and no LLM session", async () => {
|
|
2037
2329
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2330
|
+
const previousCwd = process.cwd();
|
|
2038
2331
|
try {
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2332
|
+
fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
|
|
2333
|
+
fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
|
|
2334
|
+
fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
|
|
2335
|
+
const docsDir = path.join(projectBase, "docs", "custom");
|
|
2336
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
2337
|
+
fs.writeFileSync(path.join(docsDir, "REFERENCES.md"), "stale\n", "utf8");
|
|
2338
|
+
writeProjectConfigOverrides(projectBase, { "docs-dir": "docs/custom" });
|
|
2339
|
+
assert.equal(spawnSync("git", ["add", "src/alpha.ts", "src/nested/beta.ts", "docs/custom/REFERENCES.md"], {
|
|
2340
|
+
cwd: projectBase,
|
|
2341
|
+
encoding: "utf8",
|
|
2342
|
+
}).status, 0);
|
|
2343
|
+
const fixtureCommit = spawnSync("git", ["commit", "-m", "fixture sources"], {
|
|
2344
|
+
cwd: projectBase,
|
|
2345
|
+
encoding: "utf8",
|
|
2042
2346
|
});
|
|
2347
|
+
assert.equal(fixtureCommit.status, 0, fixtureCommit.stderr);
|
|
2348
|
+
|
|
2349
|
+
const worktreeParentPath = path.dirname(projectBase);
|
|
2350
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2043
2351
|
const pi = createFakePi();
|
|
2044
2352
|
piUsereqExtension(pi);
|
|
2045
|
-
const command = pi.commands.get("req-change");
|
|
2046
|
-
assert.ok(command);
|
|
2047
2353
|
const ctx = createFakeCtx(projectBase);
|
|
2354
|
+
const recordedStatuses: string[] = [];
|
|
2355
|
+
let switchSessionCalls = 0;
|
|
2356
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2357
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
2358
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2359
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2360
|
+
recordedStatuses.push(value);
|
|
2361
|
+
}
|
|
2362
|
+
originalSetStatus(key, value);
|
|
2363
|
+
};
|
|
2364
|
+
ctx.switchSession = async (sessionPath: string) => {
|
|
2365
|
+
switchSessionCalls += 1;
|
|
2366
|
+
return originalSwitchSession(sessionPath);
|
|
2367
|
+
};
|
|
2048
2368
|
|
|
2049
|
-
await
|
|
2369
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2370
|
+
await pi.commands.get("req-references")!.handler("", ctx);
|
|
2050
2371
|
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2372
|
+
const summarizeResult = await executeRegisteredTool(pi, "summarize", projectBase, {}) as {
|
|
2373
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
2374
|
+
};
|
|
2375
|
+
const referencesPath = path.join(docsDir, "REFERENCES.md");
|
|
2376
|
+
const writtenText = fs.readFileSync(referencesPath, "utf8");
|
|
2377
|
+
const lastCommitMessage = spawnSync("git", ["log", "-1", "--pretty=%s"], {
|
|
2378
|
+
cwd: projectBase,
|
|
2379
|
+
encoding: "utf8",
|
|
2380
|
+
}).stdout.trim();
|
|
2381
|
+
const committedPaths = spawnSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD"], {
|
|
2382
|
+
cwd: projectBase,
|
|
2383
|
+
encoding: "utf8",
|
|
2384
|
+
}).stdout.trim().split(/\r?\n/).filter((line) => line !== "");
|
|
2385
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2386
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2387
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2388
|
+
const finalStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2389
|
+
|
|
2390
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2391
|
+
assert.equal(switchSessionCalls, 0);
|
|
2392
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2393
|
+
assert.equal(writtenText.trimEnd(), summarizeResult.content?.[0]?.text ?? "");
|
|
2394
|
+
assert.doesNotMatch(writtenText, /^stale$/m);
|
|
2395
|
+
assert.equal(lastCommitMessage, REQ_REFERENCES_COMMIT_MESSAGE);
|
|
2396
|
+
assert.deepEqual(committedPaths, ["docs/custom/REFERENCES.md"]);
|
|
2397
|
+
assert.equal(
|
|
2398
|
+
spawnSync("git", ["status", "--porcelain"], { cwd: projectBase, encoding: "utf8" }).stdout.trim(),
|
|
2399
|
+
"",
|
|
2400
|
+
);
|
|
2401
|
+
assert.ok(ctx.__state.notifications.some((entry) => /SUCCESS: Updated .*REFERENCES\.md/.test(entry.message)));
|
|
2402
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
|
|
2403
|
+
assert.ok(recordedStatuses.every((status) => !/<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
|
|
2404
|
+
assert.equal(
|
|
2405
|
+
finalStatus,
|
|
2406
|
+
buildExpectedFakeStatusText({
|
|
2407
|
+
workflowState: "idle",
|
|
2408
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2409
|
+
docsDir: "docs/custom",
|
|
2410
|
+
testsDir: "tests",
|
|
2411
|
+
srcDir: ["src"],
|
|
2412
|
+
contextFilledCells: 0,
|
|
2413
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2414
|
+
}),
|
|
2057
2415
|
);
|
|
2058
2416
|
} finally {
|
|
2417
|
+
process.chdir(previousCwd);
|
|
2059
2418
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2060
2419
|
}
|
|
2061
2420
|
});
|
|
2062
2421
|
|
|
2063
|
-
|
|
2422
|
+
/**
|
|
2423
|
+
* @brief Verifies failing `req-references` runs report errors without session handoff.
|
|
2424
|
+
* @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.
|
|
2425
|
+
* @return {Promise<void>} Promise resolved after failure-path assertions complete.
|
|
2426
|
+
* @throws {AssertionError} Throws when `req-references` hides failures, mutates history, or attempts prompt-session orchestration.
|
|
2427
|
+
* @satisfies TST-104
|
|
2428
|
+
*/
|
|
2429
|
+
test("req-references surfaces git-validation and reference-generation failures without session handoff", async () => {
|
|
2430
|
+
const gitFailure = initFixtureRepo({ fixtures: [] });
|
|
2431
|
+
const previousCwd = process.cwd();
|
|
2432
|
+
try {
|
|
2433
|
+
fs.writeFileSync(path.join(gitFailure.projectBase, "DIRTY.txt"), "dirty\n", "utf8");
|
|
2434
|
+
const worktreeParentPath = path.dirname(gitFailure.projectBase);
|
|
2435
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2436
|
+
const pi = createFakePi();
|
|
2437
|
+
piUsereqExtension(pi);
|
|
2438
|
+
const ctx = createFakeCtx(gitFailure.projectBase);
|
|
2439
|
+
|
|
2440
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2441
|
+
await assert.rejects(
|
|
2442
|
+
pi.commands.get("req-references")!.handler("", ctx),
|
|
2443
|
+
/ERROR: Git status unclear!/,
|
|
2444
|
+
);
|
|
2445
|
+
|
|
2446
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2447
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2448
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2449
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2450
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2451
|
+
assert.ok(ctx.__state.notifications.some((entry) => /ERROR: Git status unclear!/.test(entry.message)));
|
|
2452
|
+
assert.equal(
|
|
2453
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2454
|
+
buildExpectedFakeStatusText({
|
|
2455
|
+
workflowState: "error",
|
|
2456
|
+
basePath: buildExpectedFakeBasePath(gitFailure.projectBase),
|
|
2457
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2458
|
+
testsDir: "tests",
|
|
2459
|
+
srcDir: ["src"],
|
|
2460
|
+
contextFilledCells: 0,
|
|
2461
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2462
|
+
}),
|
|
2463
|
+
);
|
|
2464
|
+
assert.equal(
|
|
2465
|
+
spawnSync("git", ["log", "-1", "--pretty=%s"], {
|
|
2466
|
+
cwd: gitFailure.projectBase,
|
|
2467
|
+
encoding: "utf8",
|
|
2468
|
+
}).stdout.trim(),
|
|
2469
|
+
"init",
|
|
2470
|
+
);
|
|
2471
|
+
} finally {
|
|
2472
|
+
process.chdir(previousCwd);
|
|
2473
|
+
fs.rmSync(gitFailure.projectBase, { recursive: true, force: true });
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
const sourceFailure = initFixtureRepo({ fixtures: [] });
|
|
2477
|
+
try {
|
|
2478
|
+
const worktreeParentPath = path.dirname(sourceFailure.projectBase);
|
|
2479
|
+
const initialEntries = new Set(fs.readdirSync(worktreeParentPath));
|
|
2480
|
+
const pi = createFakePi();
|
|
2481
|
+
piUsereqExtension(pi);
|
|
2482
|
+
const ctx = createFakeCtx(sourceFailure.projectBase);
|
|
2483
|
+
const recordedStatuses: string[] = [];
|
|
2484
|
+
const originalSetStatus = ctx.ui.setStatus.bind(ctx.ui);
|
|
2485
|
+
ctx.ui.setStatus = (key: string, value?: string) => {
|
|
2486
|
+
if (key === "pi-usereq" && value !== undefined) {
|
|
2487
|
+
recordedStatuses.push(value);
|
|
2488
|
+
}
|
|
2489
|
+
originalSetStatus(key, value);
|
|
2490
|
+
};
|
|
2491
|
+
|
|
2492
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2493
|
+
await assert.rejects(
|
|
2494
|
+
pi.commands.get("req-references")!.handler("", ctx),
|
|
2495
|
+
/no source files found in configured directories/,
|
|
2496
|
+
);
|
|
2497
|
+
|
|
2498
|
+
const createdWorktreeEntries = fs.readdirSync(worktreeParentPath)
|
|
2499
|
+
.filter((entry) => !initialEntries.has(entry))
|
|
2500
|
+
.filter((entry) => /^PI-useReq-/.test(entry));
|
|
2501
|
+
assert.equal(pi.sentUserMessages.length, 0);
|
|
2502
|
+
assert.deepEqual(createdWorktreeEntries, []);
|
|
2503
|
+
assert.ok(ctx.__state.notifications.some((entry) => /no source files found in configured directories/.test(entry.message)));
|
|
2504
|
+
assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
|
|
2505
|
+
assert.equal(
|
|
2506
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2507
|
+
buildExpectedFakeStatusText({
|
|
2508
|
+
workflowState: "error",
|
|
2509
|
+
basePath: buildExpectedFakeBasePath(sourceFailure.projectBase),
|
|
2510
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2511
|
+
testsDir: "tests",
|
|
2512
|
+
srcDir: ["src"],
|
|
2513
|
+
contextFilledCells: 0,
|
|
2514
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2515
|
+
}),
|
|
2516
|
+
);
|
|
2517
|
+
assert.equal(
|
|
2518
|
+
spawnSync("git", ["log", "-1", "--pretty=%s"], {
|
|
2519
|
+
cwd: sourceFailure.projectBase,
|
|
2520
|
+
encoding: "utf8",
|
|
2521
|
+
}).stdout.trim(),
|
|
2522
|
+
"init",
|
|
2523
|
+
);
|
|
2524
|
+
} finally {
|
|
2525
|
+
process.chdir(previousCwd);
|
|
2526
|
+
fs.rmSync(sourceFailure.projectBase, { recursive: true, force: true });
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
/**
|
|
2531
|
+
* @brief Verifies successful `req-reset` recovers from a non-`idle` worktree-backed prompt state.
|
|
2532
|
+
* @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.
|
|
2533
|
+
* @return {Promise<void>} Promise resolved after recovery-path assertions complete.
|
|
2534
|
+
* @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.
|
|
2535
|
+
* @satisfies TST-106
|
|
2536
|
+
*/
|
|
2537
|
+
test("req-reset accepts non-idle workflow state, preserves transcript, and removes generated worktrees", async () => {
|
|
2538
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2539
|
+
const previousCwd = process.cwd();
|
|
2540
|
+
try {
|
|
2541
|
+
const pi = createFakePi();
|
|
2542
|
+
piUsereqExtension(pi);
|
|
2543
|
+
const ctx = createFakeCtx(projectBase);
|
|
2544
|
+
|
|
2545
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2546
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2547
|
+
assert.notEqual(originalSessionFile, "");
|
|
2548
|
+
|
|
2549
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2550
|
+
|
|
2551
|
+
const sentMessageCountBeforeReset = pi.sentUserMessages.length;
|
|
2552
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2553
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2554
|
+
assert.ok(worktreeMatch, promptText);
|
|
2555
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2556
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2557
|
+
const executionSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2558
|
+
const appendedEntryId = "req-reset-transcript-entry";
|
|
2559
|
+
|
|
2560
|
+
assert.notEqual(executionSessionFile, "");
|
|
2561
|
+
assert.notEqual(executionSessionFile, originalSessionFile);
|
|
2562
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2563
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2564
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2565
|
+
assert.match(
|
|
2566
|
+
ctx.__state.statuses.get("pi-usereq") ?? "",
|
|
2567
|
+
/<accent>status:<\/accent><warning>running<\/warning>/,
|
|
2568
|
+
);
|
|
2569
|
+
|
|
2570
|
+
fs.appendFileSync(
|
|
2571
|
+
executionSessionFile,
|
|
2572
|
+
`${JSON.stringify({
|
|
2573
|
+
type: "custom",
|
|
2574
|
+
id: appendedEntryId,
|
|
2575
|
+
parentId: null,
|
|
2576
|
+
timestamp: new Date(0).toISOString(),
|
|
2577
|
+
customType: "req-reset-test",
|
|
2578
|
+
data: { recovered: true },
|
|
2579
|
+
})}\n`,
|
|
2580
|
+
"utf8",
|
|
2581
|
+
);
|
|
2582
|
+
|
|
2583
|
+
await pi.commands.get("req-reset")!.handler("", ctx);
|
|
2584
|
+
|
|
2585
|
+
assert.equal(pi.sentUserMessages.length, sentMessageCountBeforeReset);
|
|
2586
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2587
|
+
assert.equal(process.cwd(), projectBase);
|
|
2588
|
+
assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
|
|
2589
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2590
|
+
assert.equal(
|
|
2591
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2592
|
+
cwd: projectBase,
|
|
2593
|
+
encoding: "utf8",
|
|
2594
|
+
}).status,
|
|
2595
|
+
1,
|
|
2596
|
+
);
|
|
2597
|
+
assert.match(fs.readFileSync(originalSessionFile, "utf8"), new RegExp(appendedEntryId));
|
|
2598
|
+
assert.ok(
|
|
2599
|
+
ctx.__state.notifications.some((entry) => /SUCCESS: req-reset/.test(entry.message)),
|
|
2600
|
+
);
|
|
2601
|
+
assert.equal(
|
|
2602
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2603
|
+
buildExpectedFakeStatusText({
|
|
2604
|
+
workflowState: "idle",
|
|
2605
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2606
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2607
|
+
testsDir: "tests",
|
|
2608
|
+
srcDir: ["src"],
|
|
2609
|
+
contextFilledCells: 0,
|
|
2610
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2611
|
+
}),
|
|
2612
|
+
);
|
|
2613
|
+
} finally {
|
|
2614
|
+
process.chdir(previousCwd);
|
|
2615
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2616
|
+
}
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* @brief Verifies failing `req-reset` surfaces restoration errors without starting new worktree or LLM execution.
|
|
2621
|
+
* @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.
|
|
2622
|
+
* @return {Promise<void>} Promise resolved after failure-path assertions complete.
|
|
2623
|
+
* @throws {AssertionError} Throws when `req-reset` hides restoration failures, creates new cleanup artifacts, or clears the generated worktree plus branch despite the failed restore.
|
|
2624
|
+
* @satisfies TST-107
|
|
2625
|
+
*/
|
|
2626
|
+
test("req-reset surfaces restoration failures and transitions workflow state to error", async () => {
|
|
2627
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2628
|
+
const previousCwd = process.cwd();
|
|
2629
|
+
try {
|
|
2630
|
+
const pi = createFakePi();
|
|
2631
|
+
piUsereqExtension(pi);
|
|
2632
|
+
const ctx = createFakeCtx(projectBase);
|
|
2633
|
+
|
|
2634
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2635
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2636
|
+
assert.notEqual(originalSessionFile, "");
|
|
2637
|
+
|
|
2638
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2639
|
+
|
|
2640
|
+
const sentMessageCountBeforeReset = pi.sentUserMessages.length;
|
|
2641
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2642
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2643
|
+
assert.ok(worktreeMatch, promptText);
|
|
2644
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2645
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2646
|
+
|
|
2647
|
+
assert.match(
|
|
2648
|
+
ctx.__state.statuses.get("pi-usereq") ?? "",
|
|
2649
|
+
/<accent>status:<\/accent><warning>running<\/warning>/,
|
|
2650
|
+
);
|
|
2651
|
+
|
|
2652
|
+
ctx.switchSession = async (_sessionPath: string) => ({ cancelled: true });
|
|
2653
|
+
|
|
2654
|
+
await assert.rejects(
|
|
2655
|
+
pi.commands.get("req-reset")!.handler("", ctx),
|
|
2656
|
+
/Session switch cancelled/,
|
|
2657
|
+
);
|
|
2658
|
+
|
|
2659
|
+
assert.equal(pi.sentUserMessages.length, sentMessageCountBeforeReset);
|
|
2660
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2661
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2662
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2663
|
+
assert.equal(
|
|
2664
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2665
|
+
cwd: projectBase,
|
|
2666
|
+
encoding: "utf8",
|
|
2667
|
+
}).status,
|
|
2668
|
+
0,
|
|
2669
|
+
);
|
|
2670
|
+
assert.ok(
|
|
2671
|
+
ctx.__state.notifications.some((entry) => /Session switch cancelled/.test(entry.message)),
|
|
2672
|
+
);
|
|
2673
|
+
assert.equal(
|
|
2674
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2675
|
+
buildExpectedFakeStatusText({
|
|
2676
|
+
workflowState: "error",
|
|
2677
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2678
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2679
|
+
testsDir: "tests",
|
|
2680
|
+
srcDir: ["src"],
|
|
2681
|
+
contextFilledCells: 0,
|
|
2682
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2683
|
+
}),
|
|
2684
|
+
);
|
|
2685
|
+
} finally {
|
|
2686
|
+
process.chdir(previousCwd);
|
|
2687
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
test("prompt commands skip worktree creation when auto git commit is disabled", async () => {
|
|
2692
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2693
|
+
try {
|
|
2694
|
+
writeProjectConfigOverrides(projectBase, {
|
|
2695
|
+
AUTO_GIT_COMMIT: "disable",
|
|
2696
|
+
GIT_WORKTREE_ENABLED: "enable",
|
|
2697
|
+
});
|
|
2698
|
+
const pi = createFakePi();
|
|
2699
|
+
piUsereqExtension(pi);
|
|
2700
|
+
const command = pi.commands.get("req-change");
|
|
2701
|
+
assert.ok(command);
|
|
2702
|
+
const ctx = createFakeCtx(projectBase);
|
|
2703
|
+
|
|
2704
|
+
await command!.handler("Adjust docs", ctx);
|
|
2705
|
+
|
|
2706
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2707
|
+
const expectedBasePath = buildExpectedFakeBasePath(projectBase).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2708
|
+
assert.doesNotMatch(promptText, /created worktree/);
|
|
2709
|
+
assert.match(
|
|
2710
|
+
promptText,
|
|
2711
|
+
new RegExp(`worktree orchestration is disabled and context-path remains \`${expectedBasePath}\``),
|
|
2712
|
+
);
|
|
2713
|
+
} finally {
|
|
2714
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2717
|
+
|
|
2718
|
+
test("prompt commands capture checking before handoff and running at prompt injection", async () => {
|
|
2719
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2720
|
+
const previousCwd = process.cwd();
|
|
2721
|
+
let checkingStatus = "";
|
|
2722
|
+
let handoffStatus = "";
|
|
2723
|
+
try {
|
|
2724
|
+
const pi = createFakePi();
|
|
2725
|
+
piUsereqExtension(pi);
|
|
2726
|
+
const ctx = createFakeCtx(projectBase);
|
|
2727
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2728
|
+
const originalSendUserMessage = pi.sendUserMessage.bind(pi);
|
|
2729
|
+
pi.sendUserMessage = (content: unknown, options?: unknown) => {
|
|
2730
|
+
handoffStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2731
|
+
originalSendUserMessage(content, options);
|
|
2732
|
+
};
|
|
2733
|
+
setPromptCommandPostCreateHookForTests(() => {
|
|
2734
|
+
checkingStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
|
|
2735
|
+
});
|
|
2736
|
+
|
|
2737
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2738
|
+
|
|
2739
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2740
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2741
|
+
assert.ok(worktreeMatch, promptText);
|
|
2742
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2743
|
+
assert.equal(
|
|
2744
|
+
checkingStatus,
|
|
2745
|
+
buildExpectedFakeStatusText({
|
|
2746
|
+
workflowState: "checking",
|
|
2747
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2748
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2749
|
+
testsDir: "tests",
|
|
2750
|
+
srcDir: ["src"],
|
|
2751
|
+
contextFilledCells: 0,
|
|
2752
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2753
|
+
}),
|
|
2754
|
+
);
|
|
2755
|
+
assert.equal(
|
|
2756
|
+
handoffStatus,
|
|
2757
|
+
buildExpectedFakeStatusText({
|
|
2758
|
+
workflowState: "checking",
|
|
2759
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2760
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2761
|
+
testsDir: "tests",
|
|
2762
|
+
srcDir: ["src"],
|
|
2763
|
+
contextFilledCells: 0,
|
|
2764
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2765
|
+
}),
|
|
2766
|
+
);
|
|
2767
|
+
assert.equal(
|
|
2768
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2769
|
+
buildExpectedFakeStatusText({
|
|
2770
|
+
workflowState: "running",
|
|
2771
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2772
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2773
|
+
testsDir: "tests",
|
|
2774
|
+
srcDir: ["src"],
|
|
2775
|
+
contextFilledCells: 0,
|
|
2776
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2777
|
+
}),
|
|
2778
|
+
);
|
|
2779
|
+
|
|
2780
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2781
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2782
|
+
await pi.emit("agent_end", {
|
|
2783
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2784
|
+
}, ctx);
|
|
2785
|
+
} finally {
|
|
2786
|
+
setPromptCommandPostCreateHookForTests(undefined);
|
|
2787
|
+
process.chdir(previousCwd);
|
|
2788
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
/**
|
|
2793
|
+
* @brief Verifies every req command except `req-reset` rejects non-`idle` workflow state and transitions to `error`.
|
|
2794
|
+
* @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.
|
|
2795
|
+
* @return {Promise<void>} Promise resolved after rejection-path assertions complete.
|
|
2796
|
+
* @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`.
|
|
2797
|
+
* @satisfies TST-068
|
|
2798
|
+
*/
|
|
2799
|
+
test("req commands except req-reset reject non-idle workflow state and transition to error", async () => {
|
|
2064
2800
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2065
2801
|
const previousCwd = process.cwd();
|
|
2066
|
-
let checkingStatus = "";
|
|
2067
|
-
let handoffStatus = "";
|
|
2068
2802
|
try {
|
|
2803
|
+
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2069
2804
|
const pi = createFakePi();
|
|
2070
2805
|
piUsereqExtension(pi);
|
|
2071
2806
|
const ctx = createFakeCtx(projectBase);
|
|
2072
2807
|
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
2808
|
|
|
2082
2809
|
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
2810
|
assert.equal(
|
|
2089
|
-
|
|
2811
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2090
2812
|
buildExpectedFakeStatusText({
|
|
2091
|
-
workflowState: "
|
|
2813
|
+
workflowState: "running",
|
|
2092
2814
|
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2093
2815
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2094
2816
|
testsDir: "tests",
|
|
@@ -2097,11 +2819,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
|
|
|
2097
2819
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2098
2820
|
}),
|
|
2099
2821
|
);
|
|
2822
|
+
|
|
2823
|
+
await assert.rejects(
|
|
2824
|
+
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2825
|
+
/expected idle/,
|
|
2826
|
+
);
|
|
2827
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2100
2828
|
assert.equal(
|
|
2101
|
-
|
|
2829
|
+
ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
|
|
2830
|
+
1,
|
|
2831
|
+
);
|
|
2832
|
+
assert.equal(
|
|
2833
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2102
2834
|
buildExpectedFakeStatusText({
|
|
2103
|
-
workflowState: "
|
|
2104
|
-
basePath: buildExpectedFakeBasePath(
|
|
2835
|
+
workflowState: "error",
|
|
2836
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2105
2837
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2106
2838
|
testsDir: "tests",
|
|
2107
2839
|
srcDir: ["src"],
|
|
@@ -2109,11 +2841,21 @@ test("prompt commands capture checking before handoff and running at prompt inje
|
|
|
2109
2841
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2110
2842
|
}),
|
|
2111
2843
|
);
|
|
2844
|
+
|
|
2845
|
+
await assert.rejects(
|
|
2846
|
+
pi.commands.get("req-references")!.handler("", ctx),
|
|
2847
|
+
/expected idle/,
|
|
2848
|
+
);
|
|
2849
|
+
assert.equal(pi.sentUserMessages.length, 1);
|
|
2850
|
+
assert.equal(
|
|
2851
|
+
ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
|
|
2852
|
+
2,
|
|
2853
|
+
);
|
|
2112
2854
|
assert.equal(
|
|
2113
2855
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2114
2856
|
buildExpectedFakeStatusText({
|
|
2115
|
-
workflowState: "
|
|
2116
|
-
basePath: buildExpectedFakeBasePath(
|
|
2857
|
+
workflowState: "error",
|
|
2858
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2117
2859
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2118
2860
|
testsDir: "tests",
|
|
2119
2861
|
srcDir: ["src"],
|
|
@@ -2122,33 +2864,64 @@ test("prompt commands capture checking before handoff and running at prompt inje
|
|
|
2122
2864
|
}),
|
|
2123
2865
|
);
|
|
2124
2866
|
|
|
2125
|
-
await pi.emit("before_agent_start", {}, ctx);
|
|
2126
2867
|
await pi.emit("agent_start", {}, ctx);
|
|
2127
2868
|
await pi.emit("agent_end", {
|
|
2128
2869
|
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2129
2870
|
}, ctx);
|
|
2871
|
+
assert.equal(
|
|
2872
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
2873
|
+
buildExpectedFakeStatusText({
|
|
2874
|
+
workflowState: "idle",
|
|
2875
|
+
basePath: buildExpectedFakeBasePath(projectBase),
|
|
2876
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
2877
|
+
testsDir: "tests",
|
|
2878
|
+
srcDir: ["src"],
|
|
2879
|
+
contextFilledCells: 0,
|
|
2880
|
+
et: "⏱︎ --:-- ⚑ 0:00 ⌛︎0:00",
|
|
2881
|
+
}),
|
|
2882
|
+
);
|
|
2130
2883
|
} finally {
|
|
2131
|
-
setPromptCommandPostCreateHookForTests(undefined);
|
|
2132
2884
|
process.chdir(previousCwd);
|
|
2133
2885
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2134
2886
|
}
|
|
2135
2887
|
});
|
|
2136
2888
|
|
|
2137
|
-
|
|
2889
|
+
/**
|
|
2890
|
+
* @brief Verifies a worktree-backed prompt still finalizes after a busy req-command rejection moves workflow state to `error`.
|
|
2891
|
+
* @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.
|
|
2892
|
+
* @return {Promise<void>} Promise resolved after finalization-path assertions complete.
|
|
2893
|
+
* @throws {AssertionError} Throws when busy-command rejection orphaned the successful worktree-backed run or prevented cleanup.
|
|
2894
|
+
* @satisfies TST-068, TST-082
|
|
2895
|
+
*/
|
|
2896
|
+
test("worktree-backed prompt finalization still succeeds after a busy-command rejection", async () => {
|
|
2138
2897
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
2898
|
+
const previousCwd = process.cwd();
|
|
2139
2899
|
try {
|
|
2140
|
-
writeProjectConfigOverrides(projectBase, { GIT_WORKTREE_ENABLED: "disable" });
|
|
2141
2900
|
const pi = createFakePi();
|
|
2142
2901
|
piUsereqExtension(pi);
|
|
2143
2902
|
const ctx = createFakeCtx(projectBase);
|
|
2903
|
+
|
|
2144
2904
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2905
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile() ?? "";
|
|
2906
|
+
assert.notEqual(originalSessionFile, "");
|
|
2145
2907
|
|
|
2146
2908
|
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2909
|
+
|
|
2910
|
+
const sentMessageCount = pi.sentUserMessages.length;
|
|
2911
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
2912
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
2913
|
+
assert.ok(worktreeMatch, promptText);
|
|
2914
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
2915
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
2916
|
+
|
|
2917
|
+
assert.equal(ctx.cwd, executionBasePath);
|
|
2918
|
+
assert.equal(process.cwd(), executionBasePath);
|
|
2919
|
+
assert.equal(fs.existsSync(executionBasePath), true);
|
|
2147
2920
|
assert.equal(
|
|
2148
2921
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2149
2922
|
buildExpectedFakeStatusText({
|
|
2150
2923
|
workflowState: "running",
|
|
2151
|
-
basePath: buildExpectedFakeBasePath(
|
|
2924
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2152
2925
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2153
2926
|
testsDir: "tests",
|
|
2154
2927
|
srcDir: ["src"],
|
|
@@ -2161,13 +2934,16 @@ test("prompt commands reject non-idle workflow state before starting a new orche
|
|
|
2161
2934
|
pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx),
|
|
2162
2935
|
/expected idle/,
|
|
2163
2936
|
);
|
|
2164
|
-
assert.equal(pi.sentUserMessages.length,
|
|
2165
|
-
assert.
|
|
2937
|
+
assert.equal(pi.sentUserMessages.length, sentMessageCount);
|
|
2938
|
+
assert.equal(
|
|
2939
|
+
ctx.__state.notifications.filter((entry) => /expected idle/.test(entry.message)).length,
|
|
2940
|
+
1,
|
|
2941
|
+
);
|
|
2166
2942
|
assert.equal(
|
|
2167
2943
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2168
2944
|
buildExpectedFakeStatusText({
|
|
2169
|
-
workflowState: "
|
|
2170
|
-
basePath: buildExpectedFakeBasePath(
|
|
2945
|
+
workflowState: "error",
|
|
2946
|
+
basePath: buildExpectedFakeBasePath(executionBasePath),
|
|
2171
2947
|
docsDir: DEFAULT_DOCS_DIR,
|
|
2172
2948
|
testsDir: "tests",
|
|
2173
2949
|
srcDir: ["src"],
|
|
@@ -2176,10 +2952,30 @@ test("prompt commands reject non-idle workflow state before starting a new orche
|
|
|
2176
2952
|
}),
|
|
2177
2953
|
);
|
|
2178
2954
|
|
|
2955
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
2179
2956
|
await pi.emit("agent_start", {}, ctx);
|
|
2180
2957
|
await pi.emit("agent_end", {
|
|
2181
2958
|
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2182
2959
|
}, ctx);
|
|
2960
|
+
|
|
2961
|
+
assert.equal(ctx.cwd, projectBase);
|
|
2962
|
+
assert.equal(process.cwd(), projectBase);
|
|
2963
|
+
assert.equal(ctx.sessionManager.getSessionFile(), originalSessionFile);
|
|
2964
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
2965
|
+
assert.notEqual(
|
|
2966
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
2967
|
+
cwd: projectBase,
|
|
2968
|
+
encoding: "utf8",
|
|
2969
|
+
}).status,
|
|
2970
|
+
0,
|
|
2971
|
+
);
|
|
2972
|
+
assert.equal(
|
|
2973
|
+
spawnSync("git", ["status", "--porcelain"], {
|
|
2974
|
+
cwd: projectBase,
|
|
2975
|
+
encoding: "utf8",
|
|
2976
|
+
}).stdout.trim(),
|
|
2977
|
+
"",
|
|
2978
|
+
);
|
|
2183
2979
|
assert.equal(
|
|
2184
2980
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2185
2981
|
buildExpectedFakeStatusText({
|
|
@@ -2193,6 +2989,7 @@ test("prompt commands reject non-idle workflow state before starting a new orche
|
|
|
2193
2989
|
}),
|
|
2194
2990
|
);
|
|
2195
2991
|
} finally {
|
|
2992
|
+
process.chdir(previousCwd);
|
|
2196
2993
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2197
2994
|
}
|
|
2198
2995
|
});
|
|
@@ -2343,6 +3140,110 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
|
|
|
2343
3140
|
}
|
|
2344
3141
|
});
|
|
2345
3142
|
|
|
3143
|
+
/**
|
|
3144
|
+
* @brief Verifies successful closure stashes tracked `base-path` changes before merge and restores them afterward.
|
|
3145
|
+
* @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.
|
|
3146
|
+
* @return {Promise<void>} Promise resolved after stash-assisted merge assertions complete.
|
|
3147
|
+
* @throws {AssertionError} Throws when finalization drops local `base-path` changes, skips the merge, or reports an error instead of a warning.
|
|
3148
|
+
* @satisfies REQ-208, REQ-291, REQ-292
|
|
3149
|
+
*/
|
|
3150
|
+
test("worktree-backed successful closure stashes tracked base-path changes, merges, restores changes, and warns about dirty base-path", async () => {
|
|
3151
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3152
|
+
const previousCwd = process.cwd();
|
|
3153
|
+
try {
|
|
3154
|
+
const stagedBasePath = path.join(projectBase, "staged-base.txt");
|
|
3155
|
+
const worktreeBasePath = path.join(projectBase, "worktree-base.txt");
|
|
3156
|
+
fs.writeFileSync(stagedBasePath, "staged base clean\n", "utf8");
|
|
3157
|
+
fs.writeFileSync(worktreeBasePath, "worktree base clean\n", "utf8");
|
|
3158
|
+
assert.equal(
|
|
3159
|
+
spawnSync("git", ["add", "staged-base.txt", "worktree-base.txt"], {
|
|
3160
|
+
cwd: projectBase,
|
|
3161
|
+
encoding: "utf8",
|
|
3162
|
+
}).status,
|
|
3163
|
+
0,
|
|
3164
|
+
);
|
|
3165
|
+
const baseCommit = spawnSync("git", ["commit", "-m", "tracked base files"], {
|
|
3166
|
+
cwd: projectBase,
|
|
3167
|
+
encoding: "utf8",
|
|
3168
|
+
});
|
|
3169
|
+
assert.equal(baseCommit.status, 0, baseCommit.stderr);
|
|
3170
|
+
|
|
3171
|
+
const pi = createFakePi();
|
|
3172
|
+
piUsereqExtension(pi);
|
|
3173
|
+
const ctx = createFakeCtx(projectBase);
|
|
3174
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3175
|
+
|
|
3176
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3177
|
+
const promptText = String(pi.sentUserMessages[0]?.content ?? "");
|
|
3178
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3179
|
+
assert.ok(worktreeMatch, promptText);
|
|
3180
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3181
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3182
|
+
|
|
3183
|
+
await pi.emit("before_agent_start", {}, ctx);
|
|
3184
|
+
await pi.emit("agent_start", {}, ctx);
|
|
3185
|
+
fs.mkdirSync(path.join(executionBasePath, "src"), { recursive: true });
|
|
3186
|
+
fs.writeFileSync(path.join(executionBasePath, "src", "stash-assisted.ts"), "export const STASH_ASSISTED = 1;\n", "utf8");
|
|
3187
|
+
assert.equal(
|
|
3188
|
+
spawnSync("git", ["add", "src/stash-assisted.ts"], {
|
|
3189
|
+
cwd: executionBasePath,
|
|
3190
|
+
encoding: "utf8",
|
|
3191
|
+
}).status,
|
|
3192
|
+
0,
|
|
3193
|
+
);
|
|
3194
|
+
const worktreeCommit = spawnSync("git", ["commit", "-m", "stash assisted change"], {
|
|
3195
|
+
cwd: executionBasePath,
|
|
3196
|
+
encoding: "utf8",
|
|
3197
|
+
});
|
|
3198
|
+
assert.equal(worktreeCommit.status, 0, worktreeCommit.stderr);
|
|
3199
|
+
|
|
3200
|
+
fs.writeFileSync(stagedBasePath, "staged base dirty\n", "utf8");
|
|
3201
|
+
assert.equal(
|
|
3202
|
+
spawnSync("git", ["add", "staged-base.txt"], {
|
|
3203
|
+
cwd: projectBase,
|
|
3204
|
+
encoding: "utf8",
|
|
3205
|
+
}).status,
|
|
3206
|
+
0,
|
|
3207
|
+
);
|
|
3208
|
+
fs.writeFileSync(worktreeBasePath, "worktree base dirty\n", "utf8");
|
|
3209
|
+
|
|
3210
|
+
await pi.emit("agent_end", {
|
|
3211
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3212
|
+
}, ctx);
|
|
3213
|
+
|
|
3214
|
+
const statusResult = spawnSync("git", ["status", "--porcelain"], {
|
|
3215
|
+
cwd: projectBase,
|
|
3216
|
+
encoding: "utf8",
|
|
3217
|
+
});
|
|
3218
|
+
assert.equal(statusResult.status, 0, statusResult.stderr);
|
|
3219
|
+
assert.equal(process.cwd(), projectBase);
|
|
3220
|
+
assert.equal(ctx.cwd, projectBase);
|
|
3221
|
+
assert.ok(fs.existsSync(path.join(projectBase, "src", "stash-assisted.ts")));
|
|
3222
|
+
assert.equal(fs.readFileSync(stagedBasePath, "utf8"), "staged base dirty\n");
|
|
3223
|
+
assert.equal(fs.readFileSync(worktreeBasePath, "utf8"), "worktree base dirty\n");
|
|
3224
|
+
assert.match(statusResult.stdout, /staged-base\.txt/);
|
|
3225
|
+
assert.match(statusResult.stdout, /worktree-base\.txt/);
|
|
3226
|
+
assert.equal(fs.existsSync(executionBasePath), false);
|
|
3227
|
+
assert.notEqual(
|
|
3228
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3229
|
+
cwd: projectBase,
|
|
3230
|
+
encoding: "utf8",
|
|
3231
|
+
}).status,
|
|
3232
|
+
0,
|
|
3233
|
+
);
|
|
3234
|
+
assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
|
|
3235
|
+
assert.ok(
|
|
3236
|
+
ctx.__state.notifications.some(
|
|
3237
|
+
(entry) => entry.level === "info"
|
|
3238
|
+
&& /WARNING: Restored base-path changes after merge; base-path is not clean\./.test(entry.message),
|
|
3239
|
+
),
|
|
3240
|
+
);
|
|
3241
|
+
} finally {
|
|
3242
|
+
process.chdir(previousCwd);
|
|
3243
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
|
|
2346
3247
|
/**
|
|
2347
3248
|
* @brief Verifies prompt-command bootstrap ignores stale deleted-worktree session metadata on later commands.
|
|
2348
3249
|
* @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.
|
|
@@ -3939,17 +4840,11 @@ test("prompt commands remain functional when custom tool registrations are remov
|
|
|
3939
4840
|
test("session_start applies configured pi-usereq startup tools", async () => {
|
|
3940
4841
|
const cwd = createTempDir("pi-usereq-tools-startup-");
|
|
3941
4842
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
"src-dir": ["src", "foobar"],
|
|
3948
|
-
"static-check": {},
|
|
3949
|
-
"enabled-tools": ["files-tokens", "static-check"],
|
|
3950
|
-
}, null, 2)}\n`,
|
|
3951
|
-
"utf8",
|
|
3952
|
-
);
|
|
4843
|
+
saveFixtureConfigs(cwd, {
|
|
4844
|
+
...getDefaultConfig(cwd),
|
|
4845
|
+
"src-dir": ["src", "foobar"],
|
|
4846
|
+
"enabled-tools": ["files-tokens", "static-check"],
|
|
4847
|
+
});
|
|
3953
4848
|
|
|
3954
4849
|
const pi = createFakePi();
|
|
3955
4850
|
piUsereqExtension(pi);
|
|
@@ -3996,6 +4891,58 @@ test("session_start renders the 0-percent context icon when context usage is emp
|
|
|
3996
4891
|
);
|
|
3997
4892
|
});
|
|
3998
4893
|
|
|
4894
|
+
/**
|
|
4895
|
+
* @brief Verifies direct status renders refresh context usage before drawing the footer.
|
|
4896
|
+
* @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.
|
|
4897
|
+
* @return {void} No return value.
|
|
4898
|
+
* @throws {AssertionError} Throws when a direct render ignores the live `getContextUsage()` snapshot or fails to clear the gauge after context reset.
|
|
4899
|
+
* @satisfies REQ-118, REQ-119, REQ-122, REQ-284
|
|
4900
|
+
*/
|
|
4901
|
+
test("direct status rerenders refresh the context gauge after context reset", () => {
|
|
4902
|
+
const cwd = createTempDir("pi-usereq-context-direct-render-");
|
|
4903
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4904
|
+
const controller = createPiUsereqStatusController();
|
|
4905
|
+
setPiUsereqStatusConfig(controller, getDefaultConfig(cwd));
|
|
4906
|
+
const contextUsage: { tokens: number | null; contextWindow: number; percent: number | null } = {
|
|
4907
|
+
tokens: 910,
|
|
4908
|
+
contextWindow: 1000,
|
|
4909
|
+
percent: 91,
|
|
4910
|
+
};
|
|
4911
|
+
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
4912
|
+
getContextUsage: () => ({ ...contextUsage }),
|
|
4913
|
+
});
|
|
4914
|
+
|
|
4915
|
+
renderPiUsereqStatus(controller, ctx);
|
|
4916
|
+
assert.equal(
|
|
4917
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
4918
|
+
buildExpectedFakeStatusText({
|
|
4919
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4920
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4921
|
+
testsDir: "tests",
|
|
4922
|
+
srcDir: ["src"],
|
|
4923
|
+
contextFilledCells: 10,
|
|
4924
|
+
contextPercent: 91,
|
|
4925
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4926
|
+
}),
|
|
4927
|
+
);
|
|
4928
|
+
|
|
4929
|
+
contextUsage.tokens = null;
|
|
4930
|
+
contextUsage.percent = null;
|
|
4931
|
+
renderPiUsereqStatus(controller, ctx);
|
|
4932
|
+
assert.equal(
|
|
4933
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
4934
|
+
buildExpectedFakeStatusText({
|
|
4935
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
4936
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
4937
|
+
testsDir: "tests",
|
|
4938
|
+
srcDir: ["src"],
|
|
4939
|
+
contextFilledCells: 0,
|
|
4940
|
+
contextPercent: null,
|
|
4941
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4942
|
+
}),
|
|
4943
|
+
);
|
|
4944
|
+
});
|
|
4945
|
+
|
|
3999
4946
|
/**
|
|
4000
4947
|
* @brief Verifies stale replacement contexts do not break workflow-state rendering.
|
|
4001
4948
|
* @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.
|
|
@@ -4007,7 +4954,7 @@ test("stale replacement contexts do not break workflow-state rendering", () => {
|
|
|
4007
4954
|
const cwd = createTempDir("pi-usereq-stale-render-");
|
|
4008
4955
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4009
4956
|
const controller = createPiUsereqStatusController();
|
|
4010
|
-
setPiUsereqStatusConfig(controller, getDefaultConfig());
|
|
4957
|
+
setPiUsereqStatusConfig(controller, getDefaultConfig(cwd));
|
|
4011
4958
|
const staleCtx = createFakeCtx(cwd);
|
|
4012
4959
|
Object.defineProperty(staleCtx, "ui", {
|
|
4013
4960
|
configurable: true,
|
|
@@ -4092,12 +5039,12 @@ test("context hook refreshes context usage and rounds progress cells upward", as
|
|
|
4092
5039
|
|
|
4093
5040
|
/**
|
|
4094
5041
|
* @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
|
|
5042
|
+
* @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
5043
|
* @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
|
|
5044
|
+
* @throws {AssertionError} Throws when any threshold renders the wrong icon or low-band color.
|
|
5045
|
+
* @satisfies REQ-122, REQ-126, TST-096
|
|
4099
5046
|
*/
|
|
4100
|
-
test("context hook applies quarter-threshold gauge icons below the error range", async () => {
|
|
5047
|
+
test("context hook applies warning-colored quarter-threshold gauge icons below the error range", async () => {
|
|
4101
5048
|
const cwd = createTempDir("pi-usereq-context-threshold-status-");
|
|
4102
5049
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4103
5050
|
const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
|
|
@@ -4388,7 +5335,9 @@ test("session_start enables default custom tools and default embedded tools only
|
|
|
4388
5335
|
});
|
|
4389
5336
|
|
|
4390
5337
|
test("default configuration applies the documented static-check, debug, notify, sound, and pushover defaults", () => {
|
|
4391
|
-
const
|
|
5338
|
+
const cwd = createTempDir("pi-usereq-default-notify-");
|
|
5339
|
+
const config = getDefaultConfig(cwd);
|
|
5340
|
+
saveFixtureConfigs(cwd, config);
|
|
4392
5341
|
|
|
4393
5342
|
assert.equal(config.AUTO_GIT_COMMIT, "enable");
|
|
4394
5343
|
assert.deepEqual(Object.keys(config["static-check"]).length, 20);
|
|
@@ -4455,6 +5404,21 @@ test("default configuration applies the documented static-check, debug, notify,
|
|
|
4455
5404
|
assert.equal(config["notify-pushover-on-failed"], false);
|
|
4456
5405
|
assert.equal(config.PI_NOTIFY_CMD, 'notify-send -i %%INSTALLATION_PATH%%/resources/images/pi.dev.png -a "PI-useReq" "%%PROMT%% @ %%BASE%% [%%TIME%%]" "%%RESULT%%"');
|
|
4457
5406
|
assert.equal(config["notify-pushover-text"], "%%RESULT%%\n%%ARGS%%");
|
|
5407
|
+
|
|
5408
|
+
const localConfig = readProjectConfigJson(cwd);
|
|
5409
|
+
const globalConfig = readGlobalConfigJson();
|
|
5410
|
+
assert.equal((localConfig["static-check"] as Record<string, any>).Python.enabled, "enable");
|
|
5411
|
+
assert.equal((localConfig["static-check"] as Record<string, any>).Ruby.enabled, "disable");
|
|
5412
|
+
assert.deepEqual((globalConfig["static-check"] as Record<string, any>).Python.checkers, [
|
|
5413
|
+
{ module: "Command", cmd: "pyright", params: ["--outputjson"] },
|
|
5414
|
+
{ module: "Command", cmd: "ruff", params: ["check"] },
|
|
5415
|
+
]);
|
|
5416
|
+
assert.deepEqual((globalConfig["static-check"] as Record<string, any>).Ruby.checkers, []);
|
|
5417
|
+
assert.equal(globalConfig.AUTO_GIT_COMMIT, "enable");
|
|
5418
|
+
assert.equal(globalConfig.GIT_WORKTREE_ENABLED, "enable");
|
|
5419
|
+
assert.equal(globalConfig["notify-enabled"], false);
|
|
5420
|
+
assert.equal(Object.hasOwn(localConfig, "notify-enabled"), false);
|
|
5421
|
+
assert.equal(Object.hasOwn(localConfig, "enabled-tools"), false);
|
|
4458
5422
|
});
|
|
4459
5423
|
|
|
4460
5424
|
test("configuration menu orders tool toggles and can enable embedded builtin tools", async () => {
|
|
@@ -4470,17 +5434,18 @@ test("configuration menu orders tool toggles and can enable embedded builtin too
|
|
|
4470
5434
|
|
|
4471
5435
|
await command!.handler("", ctx);
|
|
4472
5436
|
|
|
4473
|
-
const config =
|
|
5437
|
+
const config = readGlobalConfigJson();
|
|
4474
5438
|
assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
|
|
4475
5439
|
"compress",
|
|
4476
5440
|
"references",
|
|
4477
5441
|
"search",
|
|
4478
5442
|
"static-check",
|
|
5443
|
+
"summarize",
|
|
4479
5444
|
"tokens",
|
|
4480
5445
|
"files-compress",
|
|
4481
|
-
"files-references",
|
|
4482
5446
|
"files-search",
|
|
4483
5447
|
"files-static-check",
|
|
5448
|
+
"files-summarize",
|
|
4484
5449
|
"files-tokens",
|
|
4485
5450
|
"bash",
|
|
4486
5451
|
"edit",
|
|
@@ -4510,12 +5475,12 @@ test("configuration menu can disable configurable active tools", async () => {
|
|
|
4510
5475
|
}),
|
|
4511
5476
|
);
|
|
4512
5477
|
|
|
4513
|
-
const config =
|
|
5478
|
+
const config = readGlobalConfigJson();
|
|
4514
5479
|
assert.deepEqual(config["enabled-tools"], []);
|
|
4515
5480
|
assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
|
|
4516
5481
|
});
|
|
4517
5482
|
|
|
4518
|
-
test("configuration menu can persist notify and sound settings", async () => {
|
|
5483
|
+
test("configuration menu can persist notify and boot-sound settings without changing runtime sound", async () => {
|
|
4519
5484
|
const cwd = createTempDir("pi-usereq-menu-notify-");
|
|
4520
5485
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4521
5486
|
const pi = createFakePi();
|
|
@@ -4529,7 +5494,7 @@ test("configuration menu can persist notify and sound settings", async () => {
|
|
|
4529
5494
|
"Notification events",
|
|
4530
5495
|
"Prompt failed",
|
|
4531
5496
|
"Save and close",
|
|
4532
|
-
"Enable sound",
|
|
5497
|
+
"Enable sound (boot value)",
|
|
4533
5498
|
"high",
|
|
4534
5499
|
"Sound events",
|
|
4535
5500
|
"Prompt interrupted",
|
|
@@ -4551,9 +5516,10 @@ test("configuration menu can persist notify and sound settings", async () => {
|
|
|
4551
5516
|
],
|
|
4552
5517
|
});
|
|
4553
5518
|
|
|
5519
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
4554
5520
|
await command!.handler("", ctx);
|
|
4555
5521
|
|
|
4556
|
-
const config =
|
|
5522
|
+
const config = readGlobalConfigJson();
|
|
4557
5523
|
assert.equal(config["notify-enabled"], true);
|
|
4558
5524
|
assert.equal(config["notify-on-completed"], true);
|
|
4559
5525
|
assert.equal(config["notify-on-interrupted"], false);
|
|
@@ -4576,7 +5542,7 @@ test("configuration menu can persist notify and sound settings", async () => {
|
|
|
4576
5542
|
srcDir: ["src"],
|
|
4577
5543
|
contextFilledCells: 0,
|
|
4578
5544
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4579
|
-
sound: "
|
|
5545
|
+
sound: "none",
|
|
4580
5546
|
}),
|
|
4581
5547
|
);
|
|
4582
5548
|
});
|
|
@@ -4614,7 +5580,7 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
4614
5580
|
|
|
4615
5581
|
await command!.handler("", ctx);
|
|
4616
5582
|
|
|
4617
|
-
const config =
|
|
5583
|
+
const config = readGlobalConfigJson();
|
|
4618
5584
|
assert.equal(config["notify-pushover-enabled"], true);
|
|
4619
5585
|
assert.equal(config["notify-pushover-on-completed"], true);
|
|
4620
5586
|
assert.equal(config["notify-pushover-on-interrupted"], false);
|
|
@@ -4641,14 +5607,10 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
4641
5607
|
test("notifications menu escapes pushover text and locks pushover enablement until credentials exist", async () => {
|
|
4642
5608
|
const cwd = createTempDir("pi-usereq-menu-pushover-display-");
|
|
4643
5609
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
"notify-pushover-text": "first line\nsecond line",
|
|
4649
|
-
}, null, 2)}\n`,
|
|
4650
|
-
"utf8",
|
|
4651
|
-
);
|
|
5610
|
+
saveFixtureConfigs(cwd, {
|
|
5611
|
+
...getDefaultConfig(cwd),
|
|
5612
|
+
"notify-pushover-text": "first line\nsecond line",
|
|
5613
|
+
});
|
|
4652
5614
|
const pi = createFakePi();
|
|
4653
5615
|
piUsereqExtension(pi);
|
|
4654
5616
|
const command = pi.commands.get("pi-usereq");
|
|
@@ -4662,7 +5624,7 @@ test("notifications menu escapes pushover text and locks pushover enablement unt
|
|
|
4662
5624
|
const renderedNotificationsMenu = (ctx.__state.customRenderLines[2] ?? []).join("\n");
|
|
4663
5625
|
assert.match(renderedNotificationsMenu, /first line\\nsecond line/);
|
|
4664
5626
|
assert.match(renderedNotificationsMenu, /<dim>Enable pushover/);
|
|
4665
|
-
assert.match(renderedNotificationsMenu, /<dim>
|
|
5627
|
+
assert.match(renderedNotificationsMenu, /<dim>configure user\/token keys first<\/dim>/);
|
|
4666
5628
|
});
|
|
4667
5629
|
|
|
4668
5630
|
test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
|
|
@@ -4874,7 +5836,7 @@ test("pushover global enable controls status and suppresses delivery when disabl
|
|
|
4874
5836
|
await command!.handler("", ctx);
|
|
4875
5837
|
writeProjectConfigOverrides(cwd, { GIT_WORKTREE_ENABLED: "disable" });
|
|
4876
5838
|
|
|
4877
|
-
const config =
|
|
5839
|
+
const config = readGlobalConfigJson();
|
|
4878
5840
|
assert.equal(config["notify-pushover-enabled"], false);
|
|
4879
5841
|
assert.equal(config["notify-pushover-on-completed"], true);
|
|
4880
5842
|
assert.equal(
|
|
@@ -5125,9 +6087,12 @@ test("sound routing honors the selected sound state and per-event toggles", asyn
|
|
|
5125
6087
|
}
|
|
5126
6088
|
});
|
|
5127
6089
|
|
|
5128
|
-
test("sound toggle shortcut cycles
|
|
6090
|
+
test("sound toggle shortcut cycles active runtime pi-notify sound levels without persisting boot config", async () => {
|
|
5129
6091
|
const cwd = createTempDir("pi-usereq-shortcut-notify-");
|
|
5130
6092
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
6093
|
+
const persistedConfig = getDefaultConfig(cwd);
|
|
6094
|
+
saveFixtureConfigs(cwd, persistedConfig);
|
|
6095
|
+
const persistedGlobalConfigText = fs.readFileSync(getGlobalConfigPath(), "utf8");
|
|
5131
6096
|
const previousCwd = process.cwd();
|
|
5132
6097
|
const pi = createFakePi();
|
|
5133
6098
|
process.chdir(cwd);
|
|
@@ -5144,8 +6109,7 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
5144
6109
|
|
|
5145
6110
|
for (const expectedSound of ["low", "mid", "high", "none"]) {
|
|
5146
6111
|
await shortcut!.handler(ctx);
|
|
5147
|
-
|
|
5148
|
-
assert.equal(config["notify-sound"], expectedSound);
|
|
6112
|
+
assert.equal(fs.readFileSync(getGlobalConfigPath(), "utf8"), persistedGlobalConfigText);
|
|
5149
6113
|
assert.equal(
|
|
5150
6114
|
ctx.__state.statuses.get("pi-usereq"),
|
|
5151
6115
|
buildExpectedFakeStatusText({
|
|
@@ -5161,6 +6125,38 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
5161
6125
|
}
|
|
5162
6126
|
});
|
|
5163
6127
|
|
|
6128
|
+
test("session_start loads the active runtime sound from persisted boot config", async () => {
|
|
6129
|
+
const cwd = createTempDir("pi-usereq-session-start-sound-");
|
|
6130
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
6131
|
+
const persistedConfig = getDefaultConfig(cwd);
|
|
6132
|
+
persistedConfig["notify-sound"] = "mid";
|
|
6133
|
+
saveFixtureConfigs(cwd, persistedConfig);
|
|
6134
|
+
const previousCwd = process.cwd();
|
|
6135
|
+
const pi = createFakePi();
|
|
6136
|
+
process.chdir(cwd);
|
|
6137
|
+
try {
|
|
6138
|
+
piUsereqExtension(pi);
|
|
6139
|
+
} finally {
|
|
6140
|
+
process.chdir(previousCwd);
|
|
6141
|
+
}
|
|
6142
|
+
|
|
6143
|
+
const ctx = createFakeCtx(cwd);
|
|
6144
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
6145
|
+
|
|
6146
|
+
assert.equal(
|
|
6147
|
+
ctx.__state.statuses.get("pi-usereq"),
|
|
6148
|
+
buildExpectedFakeStatusText({
|
|
6149
|
+
basePath: buildExpectedFakeBasePath(cwd),
|
|
6150
|
+
docsDir: DEFAULT_DOCS_DIR,
|
|
6151
|
+
testsDir: "tests",
|
|
6152
|
+
srcDir: ["src"],
|
|
6153
|
+
contextFilledCells: 0,
|
|
6154
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
6155
|
+
sound: "mid",
|
|
6156
|
+
}),
|
|
6157
|
+
);
|
|
6158
|
+
});
|
|
6159
|
+
|
|
5164
6160
|
test("static-check exposes the supported programming languages", () => {
|
|
5165
6161
|
const supported = getSupportedStaticCheckLanguageSupport();
|
|
5166
6162
|
assert.equal(supported.length, 20);
|
|
@@ -5233,9 +6229,10 @@ test("configuration menu can toggle per-language static-check enablement", async
|
|
|
5233
6229
|
}),
|
|
5234
6230
|
);
|
|
5235
6231
|
|
|
5236
|
-
const
|
|
5237
|
-
|
|
5238
|
-
assert.
|
|
6232
|
+
const localConfig = readProjectConfigJson(cwd);
|
|
6233
|
+
const globalConfig = readGlobalConfigJson();
|
|
6234
|
+
assert.equal((localConfig["static-check"] as Record<string, any>).Python.enabled, "disable");
|
|
6235
|
+
assert.deepEqual((globalConfig["static-check"] as Record<string, any>).Python.checkers, [
|
|
5239
6236
|
{ module: "Command", cmd: "pyright", params: ["--outputjson"] },
|
|
5240
6237
|
{ module: "Command", cmd: "ruff", params: ["check"] },
|
|
5241
6238
|
]);
|
|
@@ -5263,8 +6260,10 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
5263
6260
|
}),
|
|
5264
6261
|
);
|
|
5265
6262
|
|
|
5266
|
-
const
|
|
5267
|
-
|
|
6263
|
+
const localConfig = readProjectConfigJson(cwd);
|
|
6264
|
+
const globalConfig = readGlobalConfigJson();
|
|
6265
|
+
assert.equal((localConfig["static-check"] as Record<string, any>).Ruby.enabled, "enable");
|
|
6266
|
+
assert.deepEqual((globalConfig["static-check"] as Record<string, any>).Ruby.checkers, [
|
|
5268
6267
|
{ module: "Command", cmd: "true" },
|
|
5269
|
-
]
|
|
6268
|
+
]);
|
|
5270
6269
|
});
|