pi-usereq 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +24470 -4590
- package/pi-usereq/docs/REQUIREMENTS.md +89 -68
- package/pi-usereq/docs/WORKFLOW.md +133 -106
- package/src/core/agent-tool-json.ts +42 -193
- package/src/core/compress-payload.ts +7 -15
- package/src/core/config.ts +84 -29
- package/src/core/extension-status.ts +49 -177
- package/src/core/find-payload.ts +21 -44
- package/src/core/pi-notify.ts +473 -313
- package/src/core/reference-payload.ts +6 -14
- package/src/core/settings-menu.ts +21 -1
- package/src/core/token-counter.ts +58 -124
- package/src/index.ts +798 -537
- package/src/resources/images/favicon.svg +21 -0
- package/src/resources/images/pi.dev.png +0 -0
- package/tests/debug-extension-harness.test.ts +17 -23
- package/tests/extension-registration.test.ts +632 -215
|
@@ -2,6 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import piUsereqExtension from "../src/index.ts";
|
|
7
8
|
import {
|
|
@@ -9,8 +10,13 @@ import {
|
|
|
9
10
|
getDefaultConfig,
|
|
10
11
|
getProjectConfigPath,
|
|
11
12
|
} from "../src/core/config.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
13
|
+
import {
|
|
14
|
+
setJsTiktokenModuleLoaderForTests,
|
|
15
|
+
} from "../src/core/token-counter.js";
|
|
16
|
+
import {
|
|
17
|
+
setPiNotifyHttpsRequestForTests,
|
|
18
|
+
setPiNotifySpawnForTests,
|
|
19
|
+
} from "../src/core/pi-notify.js";
|
|
14
20
|
import {
|
|
15
21
|
PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES,
|
|
16
22
|
PI_USEREQ_EMBEDDED_TOOL_NAMES,
|
|
@@ -289,42 +295,43 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
|
|
|
289
295
|
}
|
|
290
296
|
|
|
291
297
|
/**
|
|
292
|
-
* @brief Builds the expected fake context-
|
|
293
|
-
* @details Resolves the
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* @
|
|
298
|
-
* @return {string} Encoded context-bar string.
|
|
298
|
+
* @brief Builds the expected fake context-gauge payload for assertions.
|
|
299
|
+
* @details Resolves the documented icon thresholds for `0-100%` and emits the
|
|
300
|
+
* blinking red overflow icon for percent values above `100`. Runtime is O(1).
|
|
301
|
+
* No external state is mutated.
|
|
302
|
+
* @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
|
|
303
|
+
* @return {string} Encoded context-gauge string.
|
|
299
304
|
*/
|
|
300
305
|
function buildExpectedFakeContextBar(options: {
|
|
301
306
|
filledCells: number;
|
|
302
307
|
percent?: number | null;
|
|
303
308
|
}): string {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
formatFakeThemeForeground("warning", "◀ CLEAR ▶ "),
|
|
308
|
-
);
|
|
309
|
+
const percent = options.percent;
|
|
310
|
+
if (percent === undefined || percent === null || percent <= 0) {
|
|
311
|
+
return formatFakeThemeForeground("warning", "▕_▏");
|
|
309
312
|
}
|
|
310
|
-
if (
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
);
|
|
313
|
+
if (percent > 100) {
|
|
314
|
+
return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
|
|
315
|
+
}
|
|
316
|
+
if (percent <= 25) {
|
|
317
|
+
return formatFakeThemeForeground("warning", "▕▂▏");
|
|
315
318
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
)
|
|
321
|
-
|
|
319
|
+
if (percent <= 50) {
|
|
320
|
+
return formatFakeThemeForeground("warning", "▕▄▏");
|
|
321
|
+
}
|
|
322
|
+
if (percent <= 90) {
|
|
323
|
+
return formatFakeThemeForeground("warning", "▕▆▏");
|
|
324
|
+
}
|
|
325
|
+
return formatFakeThemeForeground("warning", "▕█▏");
|
|
322
326
|
}
|
|
323
327
|
|
|
324
328
|
/**
|
|
325
329
|
* @brief Builds the expected fake pi-usereq status-bar string for assertions.
|
|
326
|
-
* @details Reconstructs the field order, separators, context
|
|
327
|
-
*
|
|
330
|
+
* @details Reconstructs the field order, separators, icon-based context gauge,
|
|
331
|
+
* consolidated elapsed field, and sound field emitted by the extension using
|
|
332
|
+
* deterministic fake theme markers. Runtime is O(1). No external state is
|
|
333
|
+
* mutated.
|
|
334
|
+
* @param[in] options {{ basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts.
|
|
328
335
|
* @return {string} Encoded status-bar string.
|
|
329
336
|
*/
|
|
330
337
|
function buildExpectedFakeStatusText(options: {
|
|
@@ -335,9 +342,7 @@ function buildExpectedFakeStatusText(options: {
|
|
|
335
342
|
contextFilledCells: number;
|
|
336
343
|
contextPercent?: number | null;
|
|
337
344
|
et: string;
|
|
338
|
-
beep?: string;
|
|
339
345
|
sound?: string;
|
|
340
|
-
pushover?: string;
|
|
341
346
|
}): string {
|
|
342
347
|
const buildField = (fieldName: string, value: string): string =>
|
|
343
348
|
`${formatFakeThemeForeground("accent", `${fieldName}:`)}${formatFakeThemeForeground("warning", value)}`;
|
|
@@ -349,9 +354,7 @@ function buildExpectedFakeStatusText(options: {
|
|
|
349
354
|
buildField("base", options.basePath),
|
|
350
355
|
`${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
|
|
351
356
|
buildField("elapsed", options.et),
|
|
352
|
-
buildField("beep", options.beep ?? "end,esc,err"),
|
|
353
357
|
buildField("sound", options.sound ?? "none"),
|
|
354
|
-
buildField("pushover", options.pushover ?? "off"),
|
|
355
358
|
].join(formatFakeThemeForeground("dim", " • "));
|
|
356
359
|
}
|
|
357
360
|
|
|
@@ -367,6 +370,54 @@ function buildExpectedFakeBasePath(cwd: string): string {
|
|
|
367
370
|
return path.resolve(cwd).split(path.sep).join("/");
|
|
368
371
|
}
|
|
369
372
|
|
|
373
|
+
/**
|
|
374
|
+
* @brief Formats one expected home-relative base path for notify-template assertions.
|
|
375
|
+
* @details Resolves the supplied cwd, 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.
|
|
376
|
+
* @param[in] cwd {string} Runtime working directory.
|
|
377
|
+
* @return {string} Home-relative or absolute base path used by notification templates.
|
|
378
|
+
*/
|
|
379
|
+
function buildExpectedNotifyBasePath(cwd: string): string {
|
|
380
|
+
const resolvedPath = path.resolve(cwd);
|
|
381
|
+
const homePath = path.resolve(process.env.HOME ?? os.homedir());
|
|
382
|
+
if (resolvedPath === homePath) {
|
|
383
|
+
return "~";
|
|
384
|
+
}
|
|
385
|
+
const relativePath = path.relative(homePath, resolvedPath);
|
|
386
|
+
if (relativePath !== "" && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
387
|
+
return `~/${relativePath.split(path.sep).join("/")}`;
|
|
388
|
+
}
|
|
389
|
+
return resolvedPath.split(path.sep).join("/");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @brief Formats the expected `Show configuration` path value.
|
|
394
|
+
* @details Resolves the current 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.
|
|
395
|
+
* @param[in] cwd {string} Runtime working directory.
|
|
396
|
+
* @return {string} `~`-relative or absolute config-path display string.
|
|
397
|
+
*/
|
|
398
|
+
function buildExpectedShowConfigPath(cwd: string): string {
|
|
399
|
+
return buildExpectedNotifyBasePath(path.dirname(path.dirname(getProjectConfigPath(cwd)))) === buildExpectedNotifyBasePath(cwd)
|
|
400
|
+
? `${buildExpectedNotifyBasePath(cwd)}/.pi-usereq/config.json`
|
|
401
|
+
: getProjectConfigPath(cwd).split(path.sep).join("/");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @brief Formats the expected `%%RESULT%%` placeholder value for one terminal stop reason.
|
|
406
|
+
* @details Maps the tested assistant stop reasons to the literal runtime result tokens required by the default notify and Pushover templates. Runtime is O(1). No external state is mutated.
|
|
407
|
+
* @param[in] stopReason {"stop" | "aborted" | "error"} Terminal stop reason under test.
|
|
408
|
+
* @return {string} `successed`, `aborted`, or `failed`.
|
|
409
|
+
*/
|
|
410
|
+
function buildExpectedNotifyResult(stopReason: "stop" | "aborted" | "error"): string {
|
|
411
|
+
switch (stopReason) {
|
|
412
|
+
case "aborted":
|
|
413
|
+
return "aborted";
|
|
414
|
+
case "error":
|
|
415
|
+
return "failed";
|
|
416
|
+
default:
|
|
417
|
+
return "successed";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
370
421
|
/**
|
|
371
422
|
* @brief Creates a fake extension command context with scripted UI interactions.
|
|
372
423
|
* @details Materializes deterministic `select`, shared settings-menu `custom`, `input`, `notify`, `setStatus`, `setEditorText`, `waitForIdle`, `newSession`, `sessionManager`, `getContextUsage`, theme-color behaviors, and captured custom-menu render output backed by in-memory state so menu-oriented and prompt-command tests can assert side effects without a real UI. Optional `onNewSession` and theme overrides let tests emulate runtime replacement and strict token validation. Runtime is O(1) plus queued interaction count and delegated new-session setup cost. Side effects are limited to in-memory state mutation.
|
|
@@ -389,7 +440,7 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
389
440
|
editorText: "",
|
|
390
441
|
statuses: new Map<string, string>(),
|
|
391
442
|
notifications: [] as Array<{ message: string; level: string }>,
|
|
392
|
-
selectCalls: [] as Array<{ title: string; items: string[] }>,
|
|
443
|
+
selectCalls: [] as Array<{ title: string; items: string[]; selectedChoiceId?: string }>,
|
|
393
444
|
customRenderLines: [] as string[][],
|
|
394
445
|
waitForIdleCalls: 0,
|
|
395
446
|
newSessions: [] as Array<{ messages: string[] }>,
|
|
@@ -460,6 +511,7 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
460
511
|
__piUsereqSettingsMenu?: {
|
|
461
512
|
title: string;
|
|
462
513
|
choices: Array<{ label: string }>;
|
|
514
|
+
selectedChoiceId?: string;
|
|
463
515
|
selectByLabel: (label: string) => boolean;
|
|
464
516
|
cancel: () => void;
|
|
465
517
|
};
|
|
@@ -470,7 +522,11 @@ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options
|
|
|
470
522
|
state.customRenderLines.push(component.render(120));
|
|
471
523
|
}
|
|
472
524
|
const response = selects.shift();
|
|
473
|
-
state.selectCalls.push({
|
|
525
|
+
state.selectCalls.push({
|
|
526
|
+
title: bridge.title,
|
|
527
|
+
items: bridge.choices.map((choice) => choice.label),
|
|
528
|
+
selectedChoiceId: bridge.selectedChoiceId,
|
|
529
|
+
});
|
|
474
530
|
if (response === undefined) {
|
|
475
531
|
bridge.cancel();
|
|
476
532
|
} else {
|
|
@@ -606,13 +662,13 @@ test("token tools register agent-oriented descriptions and schema details", () =
|
|
|
606
662
|
assert.ok(filesTokens, "missing files-tokens tool");
|
|
607
663
|
assert.ok(tokens, "missing tokens tool");
|
|
608
664
|
|
|
609
|
-
assert.match(filesTokens.description ?? "", /
|
|
665
|
+
assert.match(filesTokens.description ?? "", /token-optimized JSON payload/);
|
|
610
666
|
assert.ok(filesTokens.promptGuidelines?.some((line) => line.includes("Output contract:")));
|
|
611
|
-
assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
667
|
+
assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /token metrics/);
|
|
612
668
|
|
|
613
669
|
assert.match(tokens.description ?? "", /canonical docs/);
|
|
614
|
-
assert.ok(tokens.promptGuidelines?.some((line) => line.includes("
|
|
615
|
-
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
670
|
+
assert.ok(tokens.promptGuidelines?.some((line) => line.includes("registration metadata")));
|
|
671
|
+
assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /token-optimized JSON shape/);
|
|
616
672
|
});
|
|
617
673
|
|
|
618
674
|
test("reference tools register agent-oriented descriptions and schema details", () => {
|
|
@@ -624,7 +680,7 @@ test("reference tools register agent-oriented descriptions and schema details",
|
|
|
624
680
|
assert.ok(filesReferences, "missing files-references tool");
|
|
625
681
|
assert.ok(references, "missing references tool");
|
|
626
682
|
|
|
627
|
-
assert.match(filesReferences.description ?? "", /
|
|
683
|
+
assert.match(filesReferences.description ?? "", /summary, repository, files, and execution sections/);
|
|
628
684
|
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Numeric contract:")));
|
|
629
685
|
assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
|
|
630
686
|
assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /structured Doxygen fields/);
|
|
@@ -644,11 +700,11 @@ test("find tools register agent-oriented descriptions and schema details", () =>
|
|
|
644
700
|
assert.ok(filesFind, "missing files-find tool");
|
|
645
701
|
assert.ok(find, "missing find tool");
|
|
646
702
|
|
|
647
|
-
assert.match(filesFind.description ?? "", /
|
|
703
|
+
assert.match(filesFind.description ?? "", /token-optimized JSON payload/);
|
|
648
704
|
assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Regex rule:")));
|
|
649
705
|
assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Supported tags [Typescript]:")));
|
|
650
706
|
assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Supported tags [Python]:")));
|
|
651
|
-
assert.match(String((filesFind.parameters as { description?: string } | undefined)?.description ?? ""), /
|
|
707
|
+
assert.match(String((filesFind.parameters as { description?: string } | undefined)?.description ?? ""), /Static supported-tag matrices are documented in tool registration metadata/);
|
|
652
708
|
|
|
653
709
|
assert.match(find.description ?? "", /configured project source directories/);
|
|
654
710
|
assert.ok(find.promptGuidelines?.some((line) => line.includes("source_directory_paths")));
|
|
@@ -665,7 +721,7 @@ test("compression tools register agent-oriented descriptions and schema details"
|
|
|
665
721
|
assert.ok(filesCompress, "missing files-compress tool");
|
|
666
722
|
assert.ok(compress, "missing compress tool");
|
|
667
723
|
|
|
668
|
-
assert.match(filesCompress.description ?? "", /
|
|
724
|
+
assert.match(filesCompress.description ?? "", /summary, repository, files, and execution sections/);
|
|
669
725
|
assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Line-number behavior:")));
|
|
670
726
|
assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
|
|
671
727
|
assert.match(String((filesCompress.parameters as { description?: string } | undefined)?.description ?? ""), /structured compressed lines/);
|
|
@@ -676,7 +732,7 @@ test("compression tools register agent-oriented descriptions and schema details"
|
|
|
676
732
|
assert.match(String((compress.parameters as { description?: string } | undefined)?.description ?? ""), /structured compressed lines/);
|
|
677
733
|
});
|
|
678
734
|
|
|
679
|
-
test("files-tokens returns structured source facts
|
|
735
|
+
test("files-tokens returns token-optimized structured source facts", async () => {
|
|
680
736
|
const { projectBase } = initFixtureRepo({
|
|
681
737
|
fixtures: [],
|
|
682
738
|
docs: {
|
|
@@ -693,10 +749,6 @@ test("files-tokens returns structured source facts and separated guidance", asyn
|
|
|
693
749
|
}) as {
|
|
694
750
|
content?: Array<{ type: string; text?: string }>;
|
|
695
751
|
details?: {
|
|
696
|
-
request: {
|
|
697
|
-
base_dir_path: string;
|
|
698
|
-
requested_input_paths: string[];
|
|
699
|
-
};
|
|
700
752
|
summary: {
|
|
701
753
|
counted_file_count: number;
|
|
702
754
|
skipped_file_count: number;
|
|
@@ -709,14 +761,8 @@ test("files-tokens returns structured source facts and separated guidance", asyn
|
|
|
709
761
|
line_count: number;
|
|
710
762
|
byte_count: number;
|
|
711
763
|
primary_heading_text?: string;
|
|
764
|
+
error_message?: string;
|
|
712
765
|
}>;
|
|
713
|
-
guidance: {
|
|
714
|
-
source_observations: {
|
|
715
|
-
skipped_inputs: Array<{ input_path: string; canonical_path: string; reason: string }>;
|
|
716
|
-
};
|
|
717
|
-
derived_recommendations: Array<{ kind: string; ordered_paths: string[] }>;
|
|
718
|
-
actionable_next_steps: Array<{ kind: string; ordered_paths: string[] }>;
|
|
719
|
-
};
|
|
720
766
|
execution: {
|
|
721
767
|
stderr: string;
|
|
722
768
|
};
|
|
@@ -725,8 +771,6 @@ test("files-tokens returns structured source facts and separated guidance", asyn
|
|
|
725
771
|
|
|
726
772
|
const payload = result.details!;
|
|
727
773
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
|
|
728
|
-
assert.equal(payload.request.base_dir_path, projectBase);
|
|
729
|
-
assert.deepEqual(payload.request.requested_input_paths, [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`, `${DEFAULT_DOCS_DIR}/MISSING.md`]);
|
|
730
774
|
assert.equal(payload.summary.counted_file_count, 1);
|
|
731
775
|
assert.equal(payload.summary.skipped_file_count, 1);
|
|
732
776
|
assert.equal(payload.files[0]?.canonical_path, `${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`);
|
|
@@ -734,17 +778,54 @@ test("files-tokens returns structured source facts and separated guidance", asyn
|
|
|
734
778
|
assert.equal(payload.files[0]?.end_line_number, payload.files[0]?.line_count);
|
|
735
779
|
assert.ok((payload.files[0]?.byte_count ?? 0) > 0);
|
|
736
780
|
assert.equal(payload.files[0]?.primary_heading_text, "Requirements");
|
|
737
|
-
assert.
|
|
738
|
-
|
|
739
|
-
]);
|
|
740
|
-
assert.equal(payload.guidance.derived_recommendations[0]?.kind, "prioritize_high_token_paths");
|
|
741
|
-
assert.equal(payload.guidance.actionable_next_steps[0]?.kind, "read_top_token_paths_first");
|
|
781
|
+
assert.equal(payload.files[1]?.canonical_path, `${DEFAULT_DOCS_DIR}/MISSING.md`);
|
|
782
|
+
assert.equal(payload.files[1]?.error_message, "not found");
|
|
742
783
|
assert.match(payload.execution.stderr, new RegExp(`skipped: ${DEFAULT_DOCS_DIR.replace("/", "\\/")}/MISSING\\.md: not found`));
|
|
743
784
|
} finally {
|
|
744
785
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
745
786
|
}
|
|
746
787
|
});
|
|
747
788
|
|
|
789
|
+
test("files-tokens defers js-tiktoken loading until execution and returns structured dependency failures", async () => {
|
|
790
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
791
|
+
const missingModuleError = new Error("Cannot find module 'js-tiktoken'");
|
|
792
|
+
setJsTiktokenModuleLoaderForTests(() => {
|
|
793
|
+
throw missingModuleError;
|
|
794
|
+
});
|
|
795
|
+
try {
|
|
796
|
+
const pi = createFakePi();
|
|
797
|
+
assert.doesNotThrow(() => piUsereqExtension(pi));
|
|
798
|
+
const result = await executeRegisteredTool(pi, "files-tokens", projectBase, {
|
|
799
|
+
files: [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`],
|
|
800
|
+
}) as {
|
|
801
|
+
details?: {
|
|
802
|
+
summary: {
|
|
803
|
+
counted_file_count: number;
|
|
804
|
+
skipped_file_count: number;
|
|
805
|
+
processable_file_count: number;
|
|
806
|
+
};
|
|
807
|
+
files: unknown[];
|
|
808
|
+
execution: {
|
|
809
|
+
code: number;
|
|
810
|
+
stderr: string;
|
|
811
|
+
};
|
|
812
|
+
};
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const payload = result.details!;
|
|
816
|
+
assert.equal(payload.summary.processable_file_count, 0);
|
|
817
|
+
assert.equal(payload.summary.counted_file_count, 0);
|
|
818
|
+
assert.equal(payload.summary.skipped_file_count, 0);
|
|
819
|
+
assert.deepEqual(payload.files, []);
|
|
820
|
+
assert.equal(payload.execution.code, 1);
|
|
821
|
+
assert.match(payload.execution.stderr, /js-tiktoken/);
|
|
822
|
+
assert.match(payload.execution.stderr, /npm ci/);
|
|
823
|
+
} finally {
|
|
824
|
+
setJsTiktokenModuleLoaderForTests(undefined);
|
|
825
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
748
829
|
test("files-references returns structured repository, symbol, and Doxygen facts", async () => {
|
|
749
830
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
750
831
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
@@ -771,9 +852,6 @@ export function buildSample(input: string): string {
|
|
|
771
852
|
}) as {
|
|
772
853
|
content?: Array<{ type: string; text?: string }>;
|
|
773
854
|
details?: {
|
|
774
|
-
request: {
|
|
775
|
-
requested_input_paths: string[];
|
|
776
|
-
};
|
|
777
855
|
summary: {
|
|
778
856
|
analyzed_file_count: number;
|
|
779
857
|
skipped_file_count: number;
|
|
@@ -808,7 +886,6 @@ export function buildSample(input: string): string {
|
|
|
808
886
|
|
|
809
887
|
const payload = result.details!;
|
|
810
888
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
|
|
811
|
-
assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
|
|
812
889
|
assert.equal(payload.summary.analyzed_file_count, 1);
|
|
813
890
|
assert.equal(payload.summary.skipped_file_count, 1);
|
|
814
891
|
assert.deepEqual(payload.repository.file_canonical_paths, ["src/sample.ts"]);
|
|
@@ -912,10 +989,6 @@ export function buildSample(input: string): string {
|
|
|
912
989
|
}) as {
|
|
913
990
|
content?: Array<{ type: string; text?: string }>;
|
|
914
991
|
details?: {
|
|
915
|
-
request: {
|
|
916
|
-
line_number_mode: string;
|
|
917
|
-
requested_input_paths: string[];
|
|
918
|
-
};
|
|
919
992
|
summary: {
|
|
920
993
|
compressed_file_count: number;
|
|
921
994
|
skipped_file_count: number;
|
|
@@ -950,8 +1023,6 @@ export function buildSample(input: string): string {
|
|
|
950
1023
|
|
|
951
1024
|
const payload = result.details!;
|
|
952
1025
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
|
|
953
|
-
assert.equal(payload.request.line_number_mode, "enabled");
|
|
954
|
-
assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
|
|
955
1026
|
assert.equal(payload.summary.compressed_file_count, 1);
|
|
956
1027
|
assert.equal(payload.summary.skipped_file_count, 1);
|
|
957
1028
|
assert.equal(payload.summary.total_symbol_count, 1);
|
|
@@ -999,10 +1070,6 @@ test("compress returns a structured repository-scoped compression payload", asyn
|
|
|
999
1070
|
}) as {
|
|
1000
1071
|
content?: Array<{ type: string; text?: string }>;
|
|
1001
1072
|
details?: {
|
|
1002
|
-
request: {
|
|
1003
|
-
scope: string;
|
|
1004
|
-
line_number_mode: string;
|
|
1005
|
-
};
|
|
1006
1073
|
summary: {
|
|
1007
1074
|
compressed_file_count: number;
|
|
1008
1075
|
total_compressed_line_count: number;
|
|
@@ -1026,8 +1093,6 @@ test("compress returns a structured repository-scoped compression payload", asyn
|
|
|
1026
1093
|
|
|
1027
1094
|
const payload = result.details!;
|
|
1028
1095
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
|
|
1029
|
-
assert.equal(payload.request.scope, "configured-source-directories");
|
|
1030
|
-
assert.equal(payload.request.line_number_mode, "disabled");
|
|
1031
1096
|
assert.equal(payload.summary.compressed_file_count, 2);
|
|
1032
1097
|
assert.ok((payload.summary.total_compressed_line_count ?? 0) >= 2);
|
|
1033
1098
|
assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
|
|
@@ -1071,12 +1136,6 @@ export function buildSample(input: string): string {
|
|
|
1071
1136
|
}) as {
|
|
1072
1137
|
content?: Array<{ type: string; text?: string }>;
|
|
1073
1138
|
details?: {
|
|
1074
|
-
request: {
|
|
1075
|
-
tag_filter_values: string[];
|
|
1076
|
-
tag_filter_status: string;
|
|
1077
|
-
line_number_mode: string;
|
|
1078
|
-
requested_input_paths: string[];
|
|
1079
|
-
};
|
|
1080
1139
|
summary: {
|
|
1081
1140
|
search_status: string;
|
|
1082
1141
|
matched_file_count: number;
|
|
@@ -1084,7 +1143,7 @@ export function buildSample(input: string): string {
|
|
|
1084
1143
|
total_match_count: number;
|
|
1085
1144
|
};
|
|
1086
1145
|
repository: {
|
|
1087
|
-
|
|
1146
|
+
file_canonical_paths: string[];
|
|
1088
1147
|
};
|
|
1089
1148
|
files: Array<{
|
|
1090
1149
|
canonical_path: string;
|
|
@@ -1117,15 +1176,11 @@ export function buildSample(input: string): string {
|
|
|
1117
1176
|
|
|
1118
1177
|
const payload = result.details!;
|
|
1119
1178
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
|
|
1120
|
-
assert.deepEqual(payload.request.tag_filter_values, ["FUNCTION"]);
|
|
1121
|
-
assert.equal(payload.request.tag_filter_status, "valid");
|
|
1122
|
-
assert.equal(payload.request.line_number_mode, "enabled");
|
|
1123
|
-
assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
|
|
1124
1179
|
assert.equal(payload.summary.search_status, "matched");
|
|
1125
1180
|
assert.equal(payload.summary.matched_file_count, 1);
|
|
1126
1181
|
assert.equal(payload.summary.skipped_file_count, 1);
|
|
1127
1182
|
assert.equal(payload.summary.total_match_count, 1);
|
|
1128
|
-
assert.
|
|
1183
|
+
assert.deepEqual(payload.repository.file_canonical_paths, ["src/sample.ts", "src/missing.ts"]);
|
|
1129
1184
|
assert.deepEqual(payload.files.map((file) => file.status), ["matched", "skipped"]);
|
|
1130
1185
|
assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
|
|
1131
1186
|
assert.equal(payload.files[0]?.match_count, 1);
|
|
@@ -1182,11 +1237,6 @@ export function buildAlpha(): number {
|
|
|
1182
1237
|
}) as {
|
|
1183
1238
|
content?: Array<{ type: string; text?: string }>;
|
|
1184
1239
|
details?: {
|
|
1185
|
-
request: {
|
|
1186
|
-
scope: string;
|
|
1187
|
-
regex_status: string;
|
|
1188
|
-
requested_file_count: number;
|
|
1189
|
-
};
|
|
1190
1240
|
summary: {
|
|
1191
1241
|
search_status: string;
|
|
1192
1242
|
matched_file_count: number;
|
|
@@ -1196,7 +1246,6 @@ export function buildAlpha(): number {
|
|
|
1196
1246
|
repository: {
|
|
1197
1247
|
source_directory_paths: string[];
|
|
1198
1248
|
file_canonical_paths: string[];
|
|
1199
|
-
supported_tags_by_language: Record<string, string[]>;
|
|
1200
1249
|
};
|
|
1201
1250
|
files: Array<{
|
|
1202
1251
|
canonical_path: string;
|
|
@@ -1216,16 +1265,12 @@ export function buildAlpha(): number {
|
|
|
1216
1265
|
|
|
1217
1266
|
const payload = result.details!;
|
|
1218
1267
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
|
|
1219
|
-
assert.equal(payload.request.scope, "configured-source-directories");
|
|
1220
|
-
assert.equal(payload.request.regex_status, "valid");
|
|
1221
|
-
assert.equal(payload.request.requested_file_count, 2);
|
|
1222
1268
|
assert.equal(payload.summary.search_status, "matched");
|
|
1223
1269
|
assert.equal(payload.summary.matched_file_count, 1);
|
|
1224
1270
|
assert.equal(payload.summary.no_match_file_count, 1);
|
|
1225
1271
|
assert.equal(payload.summary.total_match_count, 1);
|
|
1226
1272
|
assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
|
|
1227
1273
|
assert.deepEqual(payload.repository.file_canonical_paths, ["src/alpha.ts", "src/nested/beta.ts"]);
|
|
1228
|
-
assert.ok(payload.repository.supported_tags_by_language.typescript?.includes("FUNCTION"));
|
|
1229
1274
|
assert.deepEqual(payload.files.map((file) => file.status), ["matched", "no_match"]);
|
|
1230
1275
|
assert.equal(payload.files[0]?.matches[0]?.symbol_name, "buildAlpha");
|
|
1231
1276
|
assert.match(payload.files[0]?.matches[0]?.code_lines[0]?.display_text ?? "", /^export function buildAlpha/);
|
|
@@ -1257,31 +1302,28 @@ test("path, static-check, git, docs, and worktree tools return structured JSON p
|
|
|
1257
1302
|
content?: Array<{ text?: string }>;
|
|
1258
1303
|
details?: {
|
|
1259
1304
|
result: { path_value: string; path_present: boolean };
|
|
1260
|
-
runtime_paths: { git_path: string };
|
|
1261
1305
|
execution: { code: number };
|
|
1262
1306
|
};
|
|
1263
1307
|
};
|
|
1264
1308
|
assert.deepEqual(JSON.parse(gitPathResult.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(gitPathResult.details)));
|
|
1265
|
-
assert.equal(gitPathResult.details?.result.path_value,
|
|
1309
|
+
assert.equal(gitPathResult.details?.result.path_value, projectBase);
|
|
1266
1310
|
assert.equal(gitPathResult.details?.result.path_present, true);
|
|
1267
1311
|
assert.equal(gitPathResult.details?.execution.code, 0);
|
|
1268
1312
|
|
|
1269
1313
|
const basePathResult = await executeRegisteredTool(pi, "get-base-path", projectBase) as {
|
|
1270
1314
|
details?: {
|
|
1271
1315
|
result: { path_value: string };
|
|
1272
|
-
runtime_paths: { base_path: string };
|
|
1273
1316
|
execution: { code: number };
|
|
1274
1317
|
};
|
|
1275
1318
|
};
|
|
1276
|
-
assert.equal(basePathResult.details?.result.path_value,
|
|
1319
|
+
assert.equal(basePathResult.details?.result.path_value, projectBase);
|
|
1277
1320
|
assert.equal(basePathResult.details?.execution.code, 0);
|
|
1278
1321
|
|
|
1279
1322
|
const gitCheckResult = await executeRegisteredTool(pi, "git-check", projectBase) as {
|
|
1280
|
-
details?: { result: {
|
|
1323
|
+
details?: { result: { git_path_present: boolean; status: string }; execution: { code: number } };
|
|
1281
1324
|
};
|
|
1325
|
+
assert.equal(gitCheckResult.details?.result.git_path_present, true);
|
|
1282
1326
|
assert.equal(gitCheckResult.details?.result.status, "clean");
|
|
1283
|
-
assert.equal(gitCheckResult.details?.result.worktree_status, "clean");
|
|
1284
|
-
assert.equal(gitCheckResult.details?.result.head_status, "valid");
|
|
1285
1327
|
assert.equal(gitCheckResult.details?.execution.code, 0);
|
|
1286
1328
|
|
|
1287
1329
|
const docsCheckResult = await executeRegisteredTool(pi, "docs-check", projectBase) as {
|
|
@@ -1317,43 +1359,34 @@ test("path, static-check, git, docs, and worktree tools return structured JSON p
|
|
|
1317
1359
|
|
|
1318
1360
|
const staticCheckResult = await executeRegisteredTool(pi, "static-check", projectBase) as {
|
|
1319
1361
|
details?: {
|
|
1320
|
-
request: { selection_directory_paths: string[]; excluded_directory_paths: string[] };
|
|
1321
1362
|
summary: { selected_file_count: number };
|
|
1322
1363
|
files: Array<{ canonical_path: string; status: string }>;
|
|
1323
1364
|
execution: { code: number };
|
|
1324
1365
|
};
|
|
1325
1366
|
};
|
|
1326
|
-
assert.deepEqual(staticCheckResult.details?.request.selection_directory_paths, ["src", "tests"]);
|
|
1327
|
-
assert.ok(staticCheckResult.details?.request.excluded_directory_paths.includes("tests/fixtures"));
|
|
1328
1367
|
assert.equal(staticCheckResult.details?.summary.selected_file_count, 1);
|
|
1329
1368
|
assert.equal(staticCheckResult.details?.files[0]?.canonical_path, "src/check.ts");
|
|
1330
1369
|
assert.equal(staticCheckResult.details?.files[0]?.status, "selected");
|
|
1331
1370
|
assert.equal(staticCheckResult.details?.execution.code, 0);
|
|
1332
1371
|
|
|
1333
1372
|
const gitWtNameResult = await executeRegisteredTool(pi, "git-wt-name", projectBase) as {
|
|
1334
|
-
details?: { result: { worktree_name?: string
|
|
1373
|
+
details?: { result: { worktree_name?: string }; execution: { code: number } };
|
|
1335
1374
|
};
|
|
1336
1375
|
const wtName = gitWtNameResult.details?.result.worktree_name ?? "";
|
|
1337
|
-
assert.equal(gitWtNameResult.details?.result.status, "generated");
|
|
1338
|
-
assert.equal(gitWtNameResult.details?.result.format_text, "useReq-<project>-<sanitized-branch>-<YYYYMMDDHHMMSS>");
|
|
1339
1376
|
assert.match(wtName, /^useReq-/);
|
|
1340
1377
|
assert.equal(gitWtNameResult.details?.execution.code, 0);
|
|
1341
1378
|
|
|
1342
1379
|
const gitWtCreateResult = await executeRegisteredTool(pi, "git-wt-create", projectBase, { wtName }) as {
|
|
1343
|
-
details?: { result: {
|
|
1380
|
+
details?: { result: { worktree_name: string; worktree_path: string }; execution: { code: number } };
|
|
1344
1381
|
};
|
|
1345
|
-
assert.equal(gitWtCreateResult.details?.result.status, "created");
|
|
1346
1382
|
assert.equal(gitWtCreateResult.details?.result.worktree_name, wtName);
|
|
1347
|
-
assert.equal(gitWtCreateResult.details?.result.branch_name, wtName);
|
|
1348
1383
|
assert.match(gitWtCreateResult.details?.result.worktree_path ?? "", new RegExp(`${wtName}$`));
|
|
1349
1384
|
assert.equal(gitWtCreateResult.details?.execution.code, 0);
|
|
1350
1385
|
|
|
1351
1386
|
const gitWtDeleteResult = await executeRegisteredTool(pi, "git-wt-delete", projectBase, { wtName }) as {
|
|
1352
|
-
details?: { result: {
|
|
1387
|
+
details?: { result: { worktree_name: string; worktree_path: string }; execution: { code: number } };
|
|
1353
1388
|
};
|
|
1354
|
-
assert.equal(gitWtDeleteResult.details?.result.status, "deleted");
|
|
1355
1389
|
assert.equal(gitWtDeleteResult.details?.result.worktree_name, wtName);
|
|
1356
|
-
assert.equal(gitWtDeleteResult.details?.result.branch_name, wtName);
|
|
1357
1390
|
assert.match(gitWtDeleteResult.details?.result.worktree_path ?? "", new RegExp(`${wtName}$`));
|
|
1358
1391
|
assert.equal(gitWtDeleteResult.details?.execution.code, 0);
|
|
1359
1392
|
} finally {
|
|
@@ -1402,35 +1435,142 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1402
1435
|
piUsereqExtension(pi);
|
|
1403
1436
|
const command = pi.commands.get("pi-usereq");
|
|
1404
1437
|
assert.ok(command);
|
|
1405
|
-
const ctx = createFakeCtx(cwd, { selects: ["show-config", "notifications", "
|
|
1438
|
+
const ctx = createFakeCtx(cwd, { selects: ["show-config", "notifications", "Save and close", "Save and close"] });
|
|
1406
1439
|
|
|
1407
1440
|
await command!.handler("", ctx);
|
|
1408
1441
|
|
|
1409
1442
|
const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
|
|
1410
1443
|
assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
|
|
1411
|
-
"
|
|
1412
|
-
"
|
|
1413
|
-
"
|
|
1414
|
-
"static
|
|
1415
|
-
"
|
|
1416
|
-
"
|
|
1417
|
-
"
|
|
1444
|
+
"Document directory",
|
|
1445
|
+
"Source-code directories",
|
|
1446
|
+
"Unit tests directory",
|
|
1447
|
+
"Language static code checkers",
|
|
1448
|
+
"Enable tools",
|
|
1449
|
+
"Notifications",
|
|
1450
|
+
"Show configuration",
|
|
1418
1451
|
"Reset defaults",
|
|
1419
1452
|
"Save and close",
|
|
1420
1453
|
]);
|
|
1421
|
-
assert.ok(renderedMenu.includes(
|
|
1454
|
+
assert.ok(renderedMenu.includes(buildExpectedShowConfigPath(cwd)));
|
|
1455
|
+
assert.ok(renderedMenu.includes("notification:off • sound:none • pushover:off"));
|
|
1456
|
+
assert.doesNotMatch(renderedMenu, /beep:/);
|
|
1422
1457
|
assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
|
|
1423
|
-
"
|
|
1424
|
-
"
|
|
1425
|
-
"
|
|
1426
|
-
"
|
|
1458
|
+
"Enable notification",
|
|
1459
|
+
"Notification events",
|
|
1460
|
+
"Notify command",
|
|
1461
|
+
"Enable sound",
|
|
1462
|
+
"Sound events",
|
|
1427
1463
|
"Sound toggle hotkey bind",
|
|
1428
|
-
"
|
|
1429
|
-
"
|
|
1430
|
-
"
|
|
1431
|
-
"
|
|
1432
|
-
"
|
|
1464
|
+
"Sound command (low vol.)",
|
|
1465
|
+
"Sound command (mid vol.)",
|
|
1466
|
+
"Sound command (high vol.)",
|
|
1467
|
+
"Enable pushover",
|
|
1468
|
+
"Pushover events",
|
|
1469
|
+
"Pushover priority",
|
|
1470
|
+
"Pushover title",
|
|
1471
|
+
"Pushover text",
|
|
1472
|
+
"Pushover User Key/Delivery Group Key",
|
|
1473
|
+
"Pushover Token/API Token Key",
|
|
1474
|
+
"Reset defaults",
|
|
1475
|
+
"Save and close",
|
|
1476
|
+
]);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
test("notifications menu preserves focus on toggled and edited rows", async () => {
|
|
1480
|
+
const cwd = createTempDir("pi-usereq-menu-focus-");
|
|
1481
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1482
|
+
const pi = createFakePi();
|
|
1483
|
+
piUsereqExtension(pi);
|
|
1484
|
+
const command = pi.commands.get("pi-usereq");
|
|
1485
|
+
assert.ok(command);
|
|
1486
|
+
const ctx = createFakeCtx(cwd, {
|
|
1487
|
+
selects: [
|
|
1488
|
+
"notifications",
|
|
1489
|
+
"Notification events",
|
|
1490
|
+
"Prompt failed",
|
|
1491
|
+
"Save and close",
|
|
1492
|
+
"Notify command",
|
|
1493
|
+
"Save and close",
|
|
1494
|
+
"Save and close",
|
|
1495
|
+
],
|
|
1496
|
+
inputs: ["echo notify"],
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
await command!.handler("", ctx);
|
|
1500
|
+
|
|
1501
|
+
const notificationCalls = ctx.__state.selectCalls.filter((call) => call.title === "Notifications");
|
|
1502
|
+
assert.equal(notificationCalls[0]?.selectedChoiceId, "notify-enabled");
|
|
1503
|
+
assert.equal(notificationCalls[1]?.selectedChoiceId, "notification-events");
|
|
1504
|
+
assert.equal(notificationCalls[2]?.selectedChoiceId, "notify-command");
|
|
1505
|
+
|
|
1506
|
+
const notificationEventCalls = ctx.__state.selectCalls.filter((call) => call.title === "Notification events");
|
|
1507
|
+
assert.deepEqual(notificationEventCalls[0]?.items ?? [], [
|
|
1508
|
+
"Prompt completed",
|
|
1509
|
+
"Prompt interrupted",
|
|
1510
|
+
"Prompt failed",
|
|
1511
|
+
"Reset defaults",
|
|
1512
|
+
"Save and close",
|
|
1433
1513
|
]);
|
|
1514
|
+
assert.equal(notificationEventCalls[0]?.selectedChoiceId, "notify-on-completed");
|
|
1515
|
+
assert.equal(notificationEventCalls[1]?.selectedChoiceId, "notify-on-failed");
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
test("notifications reset defaults preserves non-notification settings", async () => {
|
|
1519
|
+
const cwd = createTempDir("pi-usereq-menu-reset-notifications-");
|
|
1520
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1521
|
+
const pi = createFakePi();
|
|
1522
|
+
piUsereqExtension(pi);
|
|
1523
|
+
const command = pi.commands.get("pi-usereq");
|
|
1524
|
+
assert.ok(command);
|
|
1525
|
+
const ctx = createFakeCtx(cwd, {
|
|
1526
|
+
selects: [
|
|
1527
|
+
"docs-dir",
|
|
1528
|
+
"notifications",
|
|
1529
|
+
"Enable notification",
|
|
1530
|
+
"Enable sound",
|
|
1531
|
+
"high",
|
|
1532
|
+
"Reset defaults",
|
|
1533
|
+
"Save and close",
|
|
1534
|
+
"Save and close",
|
|
1535
|
+
],
|
|
1536
|
+
inputs: ["docs/custom"],
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
await command!.handler("", ctx);
|
|
1540
|
+
|
|
1541
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1542
|
+
assert.equal(config["docs-dir"], "docs/custom");
|
|
1543
|
+
assert.equal(config["notify-enabled"], false);
|
|
1544
|
+
assert.equal(config["notify-sound"], "none");
|
|
1545
|
+
assert.equal(config["notify-pushover-enabled"], false);
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
test("top-level reset defaults restores all configuration values", async () => {
|
|
1549
|
+
const cwd = createTempDir("pi-usereq-menu-reset-all-");
|
|
1550
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1551
|
+
const pi = createFakePi();
|
|
1552
|
+
piUsereqExtension(pi);
|
|
1553
|
+
const command = pi.commands.get("pi-usereq");
|
|
1554
|
+
assert.ok(command);
|
|
1555
|
+
const ctx = createFakeCtx(cwd, {
|
|
1556
|
+
selects: [
|
|
1557
|
+
"docs-dir",
|
|
1558
|
+
"notifications",
|
|
1559
|
+
"Enable notification",
|
|
1560
|
+
"Save and close",
|
|
1561
|
+
"Reset defaults",
|
|
1562
|
+
"Save and close",
|
|
1563
|
+
],
|
|
1564
|
+
inputs: ["docs/custom"],
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
await command!.handler("", ctx);
|
|
1568
|
+
|
|
1569
|
+
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1570
|
+
assert.equal(config["docs-dir"], DEFAULT_DOCS_DIR);
|
|
1571
|
+
assert.deepEqual(config["src-dir"], ["src"]);
|
|
1572
|
+
assert.equal(config["notify-enabled"], false);
|
|
1573
|
+
assert.equal(config["notify-sound"], "none");
|
|
1434
1574
|
});
|
|
1435
1575
|
|
|
1436
1576
|
test("configuration menus reuse CLI settings theme semantics", async () => {
|
|
@@ -1514,12 +1654,12 @@ test("session_start applies configured pi-usereq startup tools", async () => {
|
|
|
1514
1654
|
testsDir: "tests",
|
|
1515
1655
|
srcDir: ["src", "foobar"],
|
|
1516
1656
|
contextFilledCells: 0,
|
|
1517
|
-
et: "
|
|
1657
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1518
1658
|
}),
|
|
1519
1659
|
);
|
|
1520
1660
|
});
|
|
1521
1661
|
|
|
1522
|
-
test("session_start renders the
|
|
1662
|
+
test("session_start renders the 0-percent context icon when context usage is empty", async () => {
|
|
1523
1663
|
const cwd = createTempDir("pi-usereq-context-empty-status-");
|
|
1524
1664
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1525
1665
|
const pi = createFakePi();
|
|
@@ -1539,7 +1679,7 @@ test("session_start renders the CLEAR overlay when context usage is empty", asyn
|
|
|
1539
1679
|
srcDir: ["src"],
|
|
1540
1680
|
contextFilledCells: 0,
|
|
1541
1681
|
contextPercent: 0,
|
|
1542
|
-
et: "
|
|
1682
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1543
1683
|
}),
|
|
1544
1684
|
);
|
|
1545
1685
|
});
|
|
@@ -1606,12 +1746,12 @@ test("context hook refreshes context usage and rounds progress cells upward", as
|
|
|
1606
1746
|
srcDir: ["src"],
|
|
1607
1747
|
contextFilledCells: 2,
|
|
1608
1748
|
contextPercent: 19.1,
|
|
1609
|
-
et: "
|
|
1749
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1610
1750
|
}),
|
|
1611
1751
|
);
|
|
1612
1752
|
});
|
|
1613
1753
|
|
|
1614
|
-
test("context hook renders the
|
|
1754
|
+
test("context hook renders the full context icon when usage exceeds ninety percent", async () => {
|
|
1615
1755
|
const cwd = createTempDir("pi-usereq-context-full-status-");
|
|
1616
1756
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1617
1757
|
const contextUsage = { tokens: 910, contextWindow: 1000, percent: 91 };
|
|
@@ -1633,19 +1773,19 @@ test("context hook renders the FULL! overlay when usage exceeds ninety percent",
|
|
|
1633
1773
|
srcDir: ["src"],
|
|
1634
1774
|
contextFilledCells: 10,
|
|
1635
1775
|
contextPercent: 91,
|
|
1636
|
-
et: "
|
|
1776
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1637
1777
|
}),
|
|
1638
1778
|
);
|
|
1639
1779
|
});
|
|
1640
1780
|
|
|
1641
|
-
test("status
|
|
1781
|
+
test("status overflow context icon uses only CLI-supported theme tokens", async () => {
|
|
1642
1782
|
const cwd = createTempDir("pi-usereq-context-theme-contract-");
|
|
1643
1783
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1644
1784
|
const usedColors = new Set<string>();
|
|
1645
1785
|
const pi = createFakePi();
|
|
1646
1786
|
piUsereqExtension(pi);
|
|
1647
1787
|
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
1648
|
-
getContextUsage: () => ({ tokens:
|
|
1788
|
+
getContextUsage: () => ({ tokens: 1250, contextWindow: 1000, percent: 125 }),
|
|
1649
1789
|
theme: {
|
|
1650
1790
|
fg(color: string, text: string) {
|
|
1651
1791
|
assert.ok(["accent", "warning", "dim", "error"].includes(color));
|
|
@@ -1674,8 +1814,8 @@ test("status FULL overlay uses only CLI-supported theme tokens", async () => {
|
|
|
1674
1814
|
testsDir: "tests",
|
|
1675
1815
|
srcDir: ["src"],
|
|
1676
1816
|
contextFilledCells: 10,
|
|
1677
|
-
contextPercent:
|
|
1678
|
-
et: "
|
|
1817
|
+
contextPercent: 125,
|
|
1818
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1679
1819
|
}),
|
|
1680
1820
|
);
|
|
1681
1821
|
});
|
|
@@ -1714,7 +1854,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1714
1854
|
testsDir: "tests",
|
|
1715
1855
|
srcDir: ["src"],
|
|
1716
1856
|
contextFilledCells: 0,
|
|
1717
|
-
et: "
|
|
1857
|
+
et: "⏱︎ 0:00 ⚑ --:-- ⌛︎--:--",
|
|
1718
1858
|
}),
|
|
1719
1859
|
);
|
|
1720
1860
|
|
|
@@ -1728,7 +1868,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1728
1868
|
testsDir: "tests",
|
|
1729
1869
|
srcDir: ["src"],
|
|
1730
1870
|
contextFilledCells: 0,
|
|
1731
|
-
et: "
|
|
1871
|
+
et: "⏱︎ 1:01 ⚑ --:-- ⌛︎--:--",
|
|
1732
1872
|
}),
|
|
1733
1873
|
);
|
|
1734
1874
|
|
|
@@ -1749,7 +1889,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1749
1889
|
testsDir: "tests",
|
|
1750
1890
|
srcDir: ["src"],
|
|
1751
1891
|
contextFilledCells: 0,
|
|
1752
|
-
et: "
|
|
1892
|
+
et: "⏱︎ --:-- ⚑ 1:01 ⌛︎1:01",
|
|
1753
1893
|
}),
|
|
1754
1894
|
);
|
|
1755
1895
|
|
|
@@ -1773,7 +1913,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1773
1913
|
testsDir: "tests",
|
|
1774
1914
|
srcDir: ["src"],
|
|
1775
1915
|
contextFilledCells: 0,
|
|
1776
|
-
et: "
|
|
1916
|
+
et: "⏱︎ --:-- ⚑ 1:01 ⌛︎1:01",
|
|
1777
1917
|
}),
|
|
1778
1918
|
);
|
|
1779
1919
|
assert.ok(clearedHandles.length >= 2);
|
|
@@ -1801,12 +1941,23 @@ test("session_start enables default custom tools and default embedded tools only
|
|
|
1801
1941
|
assert.equal(activeTools.has("ls"), false);
|
|
1802
1942
|
});
|
|
1803
1943
|
|
|
1804
|
-
test("default configuration
|
|
1944
|
+
test("default configuration applies the documented notify, sound, and pushover defaults", () => {
|
|
1805
1945
|
const config = getDefaultConfig(createTempDir("pi-usereq-default-notify-"));
|
|
1806
1946
|
|
|
1807
|
-
assert.equal(config["notify-
|
|
1808
|
-
assert.equal(config["notify-
|
|
1809
|
-
assert.equal(config["notify-
|
|
1947
|
+
assert.equal(config["notify-enabled"], false);
|
|
1948
|
+
assert.equal(config["notify-on-completed"], true);
|
|
1949
|
+
assert.equal(config["notify-on-interrupted"], false);
|
|
1950
|
+
assert.equal(config["notify-on-failed"], false);
|
|
1951
|
+
assert.equal(config["notify-sound"], "none");
|
|
1952
|
+
assert.equal(config["notify-sound-on-completed"], true);
|
|
1953
|
+
assert.equal(config["notify-sound-on-interrupted"], false);
|
|
1954
|
+
assert.equal(config["notify-sound-on-failed"], false);
|
|
1955
|
+
assert.equal(config["notify-pushover-enabled"], false);
|
|
1956
|
+
assert.equal(config["notify-pushover-on-completed"], true);
|
|
1957
|
+
assert.equal(config["notify-pushover-on-interrupted"], false);
|
|
1958
|
+
assert.equal(config["notify-pushover-on-failed"], false);
|
|
1959
|
+
assert.equal(config.PI_NOTIFY_CMD, 'notify-send -i %%INSTALLATION_PATH%%/resources/images/pi.dev.png -a "PI-useReq" "%%PROMT%% @ %%BASE%% [%%TIME%%]" "%%RESULT%%"');
|
|
1960
|
+
assert.equal(config["notify-pushover-text"], "%%RESULT%%\n%%ARGS%%");
|
|
1810
1961
|
});
|
|
1811
1962
|
|
|
1812
1963
|
test("configuration menu can enable embedded builtin tools", async () => {
|
|
@@ -1820,7 +1971,7 @@ test("configuration menu can enable embedded builtin tools", async () => {
|
|
|
1820
1971
|
await command!.handler(
|
|
1821
1972
|
"",
|
|
1822
1973
|
createFakeCtx(cwd, {
|
|
1823
|
-
selects: ["
|
|
1974
|
+
selects: ["Enable tools", "Enable tools", "grep", "Save and close", "Save and close"],
|
|
1824
1975
|
}),
|
|
1825
1976
|
);
|
|
1826
1977
|
|
|
@@ -1840,7 +1991,7 @@ test("configuration menu can disable configurable active tools", async () => {
|
|
|
1840
1991
|
await command!.handler(
|
|
1841
1992
|
"",
|
|
1842
1993
|
createFakeCtx(cwd, {
|
|
1843
|
-
selects: ["
|
|
1994
|
+
selects: ["Enable tools", "Disable all configurable tools", "Save and close", "Save and close"],
|
|
1844
1995
|
}),
|
|
1845
1996
|
);
|
|
1846
1997
|
|
|
@@ -1849,7 +2000,7 @@ test("configuration menu can disable configurable active tools", async () => {
|
|
|
1849
2000
|
assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
|
|
1850
2001
|
});
|
|
1851
2002
|
|
|
1852
|
-
test("configuration menu can persist
|
|
2003
|
+
test("configuration menu can persist notify and sound settings", async () => {
|
|
1853
2004
|
const cwd = createTempDir("pi-usereq-menu-notify-");
|
|
1854
2005
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1855
2006
|
const pi = createFakePi();
|
|
@@ -1859,19 +2010,26 @@ test("configuration menu can persist pi-notify settings", async () => {
|
|
|
1859
2010
|
const ctx = createFakeCtx(cwd, {
|
|
1860
2011
|
selects: [
|
|
1861
2012
|
"notifications",
|
|
1862
|
-
"
|
|
1863
|
-
"
|
|
1864
|
-
"
|
|
2013
|
+
"Enable notification",
|
|
2014
|
+
"Notification events",
|
|
2015
|
+
"Prompt failed",
|
|
2016
|
+
"Save and close",
|
|
2017
|
+
"Enable sound",
|
|
1865
2018
|
"high",
|
|
2019
|
+
"Sound events",
|
|
2020
|
+
"Prompt interrupted",
|
|
2021
|
+
"Save and close",
|
|
1866
2022
|
"Sound toggle hotkey bind",
|
|
1867
|
-
"Notify command
|
|
1868
|
-
"
|
|
1869
|
-
"
|
|
1870
|
-
"
|
|
2023
|
+
"Notify command",
|
|
2024
|
+
"Sound command (low vol.)",
|
|
2025
|
+
"Sound command (mid vol.)",
|
|
2026
|
+
"Sound command (high vol.)",
|
|
2027
|
+
"Save and close",
|
|
1871
2028
|
"Save and close",
|
|
1872
2029
|
],
|
|
1873
2030
|
inputs: [
|
|
1874
2031
|
"alt+shift+s",
|
|
2032
|
+
"notify-send -i %%INSTALLATION_PATH%%/resources/images/pi.dev.png -a \"PI-useReq\" \"%%PROMT%% @ %%BASE%% [%%TIME%%]\" \"%%RESULT%%\"",
|
|
1875
2033
|
"echo low %%INSTALLATION_PATH%%",
|
|
1876
2034
|
"echo mid %%INSTALLATION_PATH%%",
|
|
1877
2035
|
"echo high %%INSTALLATION_PATH%%",
|
|
@@ -1881,11 +2039,16 @@ test("configuration menu can persist pi-notify settings", async () => {
|
|
|
1881
2039
|
await command!.handler("", ctx);
|
|
1882
2040
|
|
|
1883
2041
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1884
|
-
assert.equal(config["notify-
|
|
1885
|
-
assert.equal(config["notify-
|
|
1886
|
-
assert.equal(config["notify-
|
|
2042
|
+
assert.equal(config["notify-enabled"], true);
|
|
2043
|
+
assert.equal(config["notify-on-completed"], true);
|
|
2044
|
+
assert.equal(config["notify-on-interrupted"], false);
|
|
2045
|
+
assert.equal(config["notify-on-failed"], true);
|
|
1887
2046
|
assert.equal(config["notify-sound"], "high");
|
|
2047
|
+
assert.equal(config["notify-sound-on-completed"], true);
|
|
2048
|
+
assert.equal(config["notify-sound-on-interrupted"], true);
|
|
2049
|
+
assert.equal(config["notify-sound-on-failed"], false);
|
|
1888
2050
|
assert.equal(config["notify-sound-toggle-shortcut"], "alt+shift+s");
|
|
2051
|
+
assert.equal(config.PI_NOTIFY_CMD, "notify-send -i %%INSTALLATION_PATH%%/resources/images/pi.dev.png -a \"PI-useReq\" \"%%PROMT%% @ %%BASE%% [%%TIME%%]\" \"%%RESULT%%\"");
|
|
1889
2052
|
assert.equal(config.PI_NOTIFY_SOUND_LOW_CMD, "echo low %%INSTALLATION_PATH%%");
|
|
1890
2053
|
assert.equal(config.PI_NOTIFY_SOUND_MID_CMD, "echo mid %%INSTALLATION_PATH%%");
|
|
1891
2054
|
assert.equal(config.PI_NOTIFY_SOUND_HIGH_CMD, "echo high %%INSTALLATION_PATH%%");
|
|
@@ -1897,8 +2060,7 @@ test("configuration menu can persist pi-notify settings", async () => {
|
|
|
1897
2060
|
testsDir: "tests",
|
|
1898
2061
|
srcDir: ["src"],
|
|
1899
2062
|
contextFilledCells: 0,
|
|
1900
|
-
et: "
|
|
1901
|
-
beep: "esc",
|
|
2063
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1902
2064
|
sound: "high",
|
|
1903
2065
|
}),
|
|
1904
2066
|
);
|
|
@@ -1914,17 +2076,22 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
1914
2076
|
const ctx = createFakeCtx(cwd, {
|
|
1915
2077
|
selects: [
|
|
1916
2078
|
"notifications",
|
|
1917
|
-
"
|
|
1918
|
-
"
|
|
1919
|
-
"
|
|
1920
|
-
"
|
|
1921
|
-
"
|
|
1922
|
-
"
|
|
1923
|
-
"
|
|
1924
|
-
"
|
|
2079
|
+
"Enable pushover",
|
|
2080
|
+
"Pushover events",
|
|
2081
|
+
"Prompt failed",
|
|
2082
|
+
"Save and close",
|
|
2083
|
+
"Pushover priority",
|
|
2084
|
+
"High",
|
|
2085
|
+
"Pushover title",
|
|
2086
|
+
"Pushover text",
|
|
2087
|
+
"Pushover User Key/Delivery Group Key",
|
|
2088
|
+
"Pushover Token/API Token Key",
|
|
2089
|
+
"Save and close",
|
|
1925
2090
|
"Save and close",
|
|
1926
2091
|
],
|
|
1927
2092
|
inputs: [
|
|
2093
|
+
"%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2094
|
+
"%%RESULT%%\n%%ARGS%%",
|
|
1928
2095
|
"gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
1929
2096
|
"ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
1930
2097
|
],
|
|
@@ -1933,11 +2100,15 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
1933
2100
|
await command!.handler("", ctx);
|
|
1934
2101
|
|
|
1935
2102
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1936
|
-
assert.equal(config["notify-pushover-
|
|
1937
|
-
assert.equal(config["notify-pushover-on-
|
|
2103
|
+
assert.equal(config["notify-pushover-enabled"], true);
|
|
2104
|
+
assert.equal(config["notify-pushover-on-completed"], true);
|
|
2105
|
+
assert.equal(config["notify-pushover-on-interrupted"], false);
|
|
2106
|
+
assert.equal(config["notify-pushover-on-failed"], true);
|
|
1938
2107
|
assert.equal(config["notify-pushover-user-key"], "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
|
|
1939
2108
|
assert.equal(config["notify-pushover-api-token"], "ah6bf5u2sj63mcvou6qamiabeoubbe");
|
|
1940
2109
|
assert.equal(config["notify-pushover-priority"], 1);
|
|
2110
|
+
assert.equal(config["notify-pushover-title"], "%%PROMT%% @ %%BASE%% [%%TIME%%]");
|
|
2111
|
+
assert.equal(config["notify-pushover-text"], "%%RESULT%%\n%%ARGS%%");
|
|
1941
2112
|
assert.equal(
|
|
1942
2113
|
ctx.__state.statuses.get("pi-usereq"),
|
|
1943
2114
|
buildExpectedFakeStatusText({
|
|
@@ -1946,13 +2117,13 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
1946
2117
|
testsDir: "tests",
|
|
1947
2118
|
srcDir: ["src"],
|
|
1948
2119
|
contextFilledCells: 0,
|
|
1949
|
-
et: "
|
|
1950
|
-
|
|
2120
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2121
|
+
sound: "none",
|
|
1951
2122
|
}),
|
|
1952
2123
|
);
|
|
1953
2124
|
});
|
|
1954
2125
|
|
|
1955
|
-
test("
|
|
2126
|
+
test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
|
|
1956
2127
|
const cwd = createTempDir("pi-usereq-pushover-run-");
|
|
1957
2128
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1958
2129
|
const configPath = getProjectConfigPath(cwd);
|
|
@@ -1995,10 +2166,13 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
1995
2166
|
|
|
1996
2167
|
try {
|
|
1997
2168
|
writeConfig({
|
|
1998
|
-
"notify-pushover-
|
|
2169
|
+
"notify-pushover-enabled": true,
|
|
2170
|
+
"notify-pushover-on-completed": true,
|
|
1999
2171
|
"notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
2000
2172
|
"notify-pushover-api-token": "ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
2001
2173
|
"notify-pushover-priority": 1,
|
|
2174
|
+
"notify-pushover-title": "%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2175
|
+
"notify-pushover-text": "%%RESULT%%\n%%ARGS%%",
|
|
2002
2176
|
});
|
|
2003
2177
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2004
2178
|
recordedRequests.length = 0;
|
|
@@ -2022,8 +2196,8 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
2022
2196
|
assert.equal(successParams.get("token"), "ah6bf5u2sj63mcvou6qamiabeoubbe");
|
|
2023
2197
|
assert.equal(successParams.get("user"), "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
|
|
2024
2198
|
assert.equal(successParams.get("priority"), "1");
|
|
2025
|
-
assert.equal(successParams.get("message"), "
|
|
2026
|
-
assert.equal(successParams.get("title"), `analyze @ ${
|
|
2199
|
+
assert.equal(successParams.get("message"), `${buildExpectedNotifyResult("stop")}\nInspect src/index.ts`);
|
|
2200
|
+
assert.equal(successParams.get("title"), `analyze @ ${buildExpectedNotifyBasePath(cwd)} [0:05]`);
|
|
2027
2201
|
|
|
2028
2202
|
recordedRequests.length = 0;
|
|
2029
2203
|
nowMs = 10_000;
|
|
@@ -2058,17 +2232,69 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
2058
2232
|
assert.equal(recordedRequests.length, 0);
|
|
2059
2233
|
|
|
2060
2234
|
writeConfig({
|
|
2061
|
-
"notify-pushover-
|
|
2235
|
+
"notify-pushover-enabled": true,
|
|
2236
|
+
"notify-pushover-on-interrupted": true,
|
|
2237
|
+
"notify-pushover-on-failed": true,
|
|
2062
2238
|
"notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
2063
|
-
"notify-pushover-api-token": "",
|
|
2239
|
+
"notify-pushover-api-token": "ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
2064
2240
|
"notify-pushover-priority": 1,
|
|
2241
|
+
"notify-pushover-title": "%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2242
|
+
"notify-pushover-text": "%%RESULT%%\n%%ARGS%%",
|
|
2065
2243
|
});
|
|
2066
2244
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2067
2245
|
recordedRequests.length = 0;
|
|
2068
2246
|
nowMs = 30_000;
|
|
2069
|
-
await pi.commands.get("req-
|
|
2247
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2070
2248
|
await pi.emit("agent_start", {}, ctx);
|
|
2071
2249
|
nowMs = 32_000;
|
|
2250
|
+
await pi.emit("agent_end", {
|
|
2251
|
+
messages: [
|
|
2252
|
+
{
|
|
2253
|
+
role: "assistant",
|
|
2254
|
+
stopReason: "aborted",
|
|
2255
|
+
content: [],
|
|
2256
|
+
},
|
|
2257
|
+
],
|
|
2258
|
+
}, ctx);
|
|
2259
|
+
assert.equal(recordedRequests.length, 1);
|
|
2260
|
+
const abortedParams = new URLSearchParams(recordedRequests[0]?.body ?? "");
|
|
2261
|
+
assert.equal(abortedParams.get("title"), `change @ ${buildExpectedNotifyBasePath(cwd)} [0:02]`);
|
|
2262
|
+
assert.equal(abortedParams.get("message"), `${buildExpectedNotifyResult("aborted")}\nAdjust docs`);
|
|
2263
|
+
|
|
2264
|
+
recordedRequests.length = 0;
|
|
2265
|
+
nowMs = 40_000;
|
|
2266
|
+
await pi.commands.get("req-write")!.handler("Write docs", ctx);
|
|
2267
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2268
|
+
nowMs = 42_000;
|
|
2269
|
+
await pi.emit("agent_end", {
|
|
2270
|
+
messages: [
|
|
2271
|
+
{
|
|
2272
|
+
role: "assistant",
|
|
2273
|
+
stopReason: "error",
|
|
2274
|
+
content: [],
|
|
2275
|
+
},
|
|
2276
|
+
],
|
|
2277
|
+
}, ctx);
|
|
2278
|
+
assert.equal(recordedRequests.length, 1);
|
|
2279
|
+
const errorParams = new URLSearchParams(recordedRequests[0]?.body ?? "");
|
|
2280
|
+
assert.equal(errorParams.get("title"), `write @ ${buildExpectedNotifyBasePath(cwd)} [0:02]`);
|
|
2281
|
+
assert.equal(errorParams.get("message"), `${buildExpectedNotifyResult("error")}\nWrite docs`);
|
|
2282
|
+
|
|
2283
|
+
writeConfig({
|
|
2284
|
+
"notify-pushover-enabled": true,
|
|
2285
|
+
"notify-pushover-on-completed": true,
|
|
2286
|
+
"notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
2287
|
+
"notify-pushover-api-token": "",
|
|
2288
|
+
"notify-pushover-priority": 1,
|
|
2289
|
+
"notify-pushover-title": "%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2290
|
+
"notify-pushover-text": "%%RESULT%%\n%%ARGS%%",
|
|
2291
|
+
});
|
|
2292
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2293
|
+
recordedRequests.length = 0;
|
|
2294
|
+
nowMs = 50_000;
|
|
2295
|
+
await pi.commands.get("req-check")!.handler("Check docs", ctx);
|
|
2296
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2297
|
+
nowMs = 53_000;
|
|
2072
2298
|
await pi.emit("agent_end", {
|
|
2073
2299
|
messages: [
|
|
2074
2300
|
{
|
|
@@ -2085,7 +2311,7 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
2085
2311
|
}
|
|
2086
2312
|
});
|
|
2087
2313
|
|
|
2088
|
-
test("pushover global
|
|
2314
|
+
test("pushover global enable controls status and suppresses delivery when disabled", async () => {
|
|
2089
2315
|
const cwd = createTempDir("pi-usereq-pushover-disabled-");
|
|
2090
2316
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2091
2317
|
const pi = createFakePi();
|
|
@@ -2095,13 +2321,9 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2095
2321
|
const ctx = createFakeCtx(cwd, {
|
|
2096
2322
|
selects: [
|
|
2097
2323
|
"notifications",
|
|
2098
|
-
"Pushover
|
|
2099
|
-
"
|
|
2100
|
-
"
|
|
2101
|
-
"User Key/Delivery Group Key",
|
|
2102
|
-
"Token/API Token Key",
|
|
2103
|
-
"Back",
|
|
2104
|
-
"Back",
|
|
2324
|
+
"Pushover User Key/Delivery Group Key",
|
|
2325
|
+
"Pushover Token/API Token Key",
|
|
2326
|
+
"Save and close",
|
|
2105
2327
|
"Save and close",
|
|
2106
2328
|
],
|
|
2107
2329
|
inputs: [
|
|
@@ -2113,8 +2335,8 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2113
2335
|
await command!.handler("", ctx);
|
|
2114
2336
|
|
|
2115
2337
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
2116
|
-
assert.equal(config["notify-pushover-
|
|
2117
|
-
assert.equal(config["notify-pushover-
|
|
2338
|
+
assert.equal(config["notify-pushover-enabled"], false);
|
|
2339
|
+
assert.equal(config["notify-pushover-on-completed"], true);
|
|
2118
2340
|
assert.equal(
|
|
2119
2341
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2120
2342
|
buildExpectedFakeStatusText({
|
|
@@ -2123,8 +2345,8 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2123
2345
|
testsDir: "tests",
|
|
2124
2346
|
srcDir: ["src"],
|
|
2125
2347
|
contextFilledCells: 0,
|
|
2126
|
-
et: "
|
|
2127
|
-
|
|
2348
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2349
|
+
sound: "none",
|
|
2128
2350
|
}),
|
|
2129
2351
|
);
|
|
2130
2352
|
|
|
@@ -2171,6 +2393,201 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2171
2393
|
}
|
|
2172
2394
|
});
|
|
2173
2395
|
|
|
2396
|
+
test("command notify routes through PI_NOTIFY_CMD placeholders and per-event toggles", async () => {
|
|
2397
|
+
const cwd = createTempDir("pi-usereq-command-notify-");
|
|
2398
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2399
|
+
const configPath = getProjectConfigPath(cwd);
|
|
2400
|
+
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
2401
|
+
const config = {
|
|
2402
|
+
...getDefaultConfig(cwd),
|
|
2403
|
+
...overrides,
|
|
2404
|
+
};
|
|
2405
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
2406
|
+
};
|
|
2407
|
+
const pi = createFakePi();
|
|
2408
|
+
piUsereqExtension(pi);
|
|
2409
|
+
const ctx = createFakeCtx(cwd);
|
|
2410
|
+
const originalDateNow = Date.now;
|
|
2411
|
+
const recordedCommands: string[] = [];
|
|
2412
|
+
let nowMs = 0;
|
|
2413
|
+
setPiNotifySpawnForTests(((command: string, args?: readonly string[] | undefined, _options?: Record<string, unknown>) => {
|
|
2414
|
+
void command;
|
|
2415
|
+
recordedCommands.push(String(args && args.length > 0 ? args[args.length - 1] : ""));
|
|
2416
|
+
const fakeChild = {
|
|
2417
|
+
once(_eventName: string, _handler: (...handlerArgs: unknown[]) => void) {
|
|
2418
|
+
return fakeChild;
|
|
2419
|
+
},
|
|
2420
|
+
unref() {
|
|
2421
|
+
return undefined;
|
|
2422
|
+
},
|
|
2423
|
+
};
|
|
2424
|
+
return fakeChild as unknown as ReturnType<typeof import("node:child_process").spawn>;
|
|
2425
|
+
}) as typeof import("node:child_process").spawn);
|
|
2426
|
+
Date.now = () => nowMs;
|
|
2427
|
+
|
|
2428
|
+
try {
|
|
2429
|
+
writeConfig({
|
|
2430
|
+
"notify-enabled": true,
|
|
2431
|
+
"notify-on-completed": true,
|
|
2432
|
+
"notify-on-interrupted": false,
|
|
2433
|
+
"notify-on-failed": true,
|
|
2434
|
+
"notify-sound": "none",
|
|
2435
|
+
PI_NOTIFY_CMD: 'notify-send "%%PROMT%% @ %%BASE%% [%%TIME%%]" "%%RESULT%%"',
|
|
2436
|
+
});
|
|
2437
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2438
|
+
recordedCommands.length = 0;
|
|
2439
|
+
nowMs = 0;
|
|
2440
|
+
await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
|
|
2441
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2442
|
+
nowMs = 5_000;
|
|
2443
|
+
await pi.emit("agent_end", {
|
|
2444
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2445
|
+
}, ctx);
|
|
2446
|
+
|
|
2447
|
+
assert.equal(recordedCommands.length, 1);
|
|
2448
|
+
assert.match(
|
|
2449
|
+
recordedCommands[0] ?? "",
|
|
2450
|
+
new RegExp(`analyze @ ${buildExpectedNotifyBasePath(cwd).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\[0:05\\]`),
|
|
2451
|
+
);
|
|
2452
|
+
assert.match(recordedCommands[0] ?? "", new RegExp(buildExpectedNotifyResult("stop")));
|
|
2453
|
+
|
|
2454
|
+
recordedCommands.length = 0;
|
|
2455
|
+
nowMs = 10_000;
|
|
2456
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2457
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2458
|
+
nowMs = 12_000;
|
|
2459
|
+
await pi.emit("agent_end", {
|
|
2460
|
+
messages: [{ role: "assistant", stopReason: "aborted", content: [] }],
|
|
2461
|
+
}, ctx);
|
|
2462
|
+
assert.equal(recordedCommands.length, 0);
|
|
2463
|
+
|
|
2464
|
+
writeConfig({
|
|
2465
|
+
"notify-enabled": true,
|
|
2466
|
+
"notify-on-completed": true,
|
|
2467
|
+
"notify-on-interrupted": true,
|
|
2468
|
+
"notify-on-failed": true,
|
|
2469
|
+
"notify-sound": "none",
|
|
2470
|
+
PI_NOTIFY_CMD: 'notify-send "%%PROMT%% @ %%BASE%% [%%TIME%%]" "%%RESULT%%"',
|
|
2471
|
+
});
|
|
2472
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2473
|
+
recordedCommands.length = 0;
|
|
2474
|
+
nowMs = 20_000;
|
|
2475
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2476
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2477
|
+
nowMs = 22_000;
|
|
2478
|
+
await pi.emit("agent_end", {
|
|
2479
|
+
messages: [{ role: "assistant", stopReason: "aborted", content: [] }],
|
|
2480
|
+
}, ctx);
|
|
2481
|
+
assert.equal(recordedCommands.length, 1);
|
|
2482
|
+
assert.match(
|
|
2483
|
+
recordedCommands[0] ?? "",
|
|
2484
|
+
new RegExp(`change @ ${buildExpectedNotifyBasePath(cwd).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\[0:02\\]`),
|
|
2485
|
+
);
|
|
2486
|
+
assert.match(recordedCommands[0] ?? "", new RegExp(buildExpectedNotifyResult("aborted")));
|
|
2487
|
+
|
|
2488
|
+
recordedCommands.length = 0;
|
|
2489
|
+
nowMs = 30_000;
|
|
2490
|
+
await pi.commands.get("req-write")!.handler("Write docs", ctx);
|
|
2491
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2492
|
+
nowMs = 33_000;
|
|
2493
|
+
await pi.emit("agent_end", {
|
|
2494
|
+
messages: [{ role: "assistant", stopReason: "error", content: [] }],
|
|
2495
|
+
}, ctx);
|
|
2496
|
+
assert.equal(recordedCommands.length, 1);
|
|
2497
|
+
assert.match(
|
|
2498
|
+
recordedCommands[0] ?? "",
|
|
2499
|
+
new RegExp(`write @ ${buildExpectedNotifyBasePath(cwd).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\[0:03\\]`),
|
|
2500
|
+
);
|
|
2501
|
+
assert.match(recordedCommands[0] ?? "", new RegExp(buildExpectedNotifyResult("error")));
|
|
2502
|
+
} finally {
|
|
2503
|
+
setPiNotifySpawnForTests(undefined);
|
|
2504
|
+
Date.now = originalDateNow;
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
test("sound routing honors the selected sound state and per-event toggles", async () => {
|
|
2509
|
+
const cwd = createTempDir("pi-usereq-sound-routing-");
|
|
2510
|
+
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2511
|
+
const configPath = getProjectConfigPath(cwd);
|
|
2512
|
+
const writeConfig = (overrides: Record<string, unknown>) => {
|
|
2513
|
+
const config = {
|
|
2514
|
+
...getDefaultConfig(cwd),
|
|
2515
|
+
...overrides,
|
|
2516
|
+
};
|
|
2517
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
2518
|
+
};
|
|
2519
|
+
const pi = createFakePi();
|
|
2520
|
+
piUsereqExtension(pi);
|
|
2521
|
+
const ctx = createFakeCtx(cwd);
|
|
2522
|
+
const recordedCommands: string[] = [];
|
|
2523
|
+
setPiNotifySpawnForTests(((command: string, args?: readonly string[] | undefined, _options?: Record<string, unknown>) => {
|
|
2524
|
+
void command;
|
|
2525
|
+
recordedCommands.push(String(args && args.length > 0 ? args[args.length - 1] : ""));
|
|
2526
|
+
const fakeChild = {
|
|
2527
|
+
once(_eventName: string, _handler: (...handlerArgs: unknown[]) => void) {
|
|
2528
|
+
return fakeChild;
|
|
2529
|
+
},
|
|
2530
|
+
unref() {
|
|
2531
|
+
return undefined;
|
|
2532
|
+
},
|
|
2533
|
+
};
|
|
2534
|
+
return fakeChild as unknown as ReturnType<typeof import("node:child_process").spawn>;
|
|
2535
|
+
}) as typeof import("node:child_process").spawn);
|
|
2536
|
+
|
|
2537
|
+
try {
|
|
2538
|
+
writeConfig({
|
|
2539
|
+
"notify-enabled": false,
|
|
2540
|
+
"notify-sound": "none",
|
|
2541
|
+
"notify-sound-on-completed": true,
|
|
2542
|
+
PI_NOTIFY_SOUND_HIGH_CMD: "echo high %%INSTALLATION_PATH%%",
|
|
2543
|
+
});
|
|
2544
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2545
|
+
recordedCommands.length = 0;
|
|
2546
|
+
await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
|
|
2547
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2548
|
+
await pi.emit("agent_end", {
|
|
2549
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
2550
|
+
}, ctx);
|
|
2551
|
+
assert.deepEqual(recordedCommands, []);
|
|
2552
|
+
|
|
2553
|
+
writeConfig({
|
|
2554
|
+
"notify-enabled": false,
|
|
2555
|
+
"notify-sound": "high",
|
|
2556
|
+
"notify-sound-on-completed": false,
|
|
2557
|
+
"notify-sound-on-interrupted": true,
|
|
2558
|
+
"notify-sound-on-failed": false,
|
|
2559
|
+
PI_NOTIFY_SOUND_HIGH_CMD: "echo high %%INSTALLATION_PATH%%",
|
|
2560
|
+
});
|
|
2561
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2562
|
+
recordedCommands.length = 0;
|
|
2563
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2564
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2565
|
+
await pi.emit("agent_end", {
|
|
2566
|
+
messages: [{ role: "assistant", stopReason: "aborted", content: [] }],
|
|
2567
|
+
}, ctx);
|
|
2568
|
+
assert.equal(recordedCommands.length, 1);
|
|
2569
|
+
|
|
2570
|
+
writeConfig({
|
|
2571
|
+
"notify-enabled": false,
|
|
2572
|
+
"notify-sound": "high",
|
|
2573
|
+
"notify-sound-on-completed": false,
|
|
2574
|
+
"notify-sound-on-interrupted": false,
|
|
2575
|
+
"notify-sound-on-failed": true,
|
|
2576
|
+
PI_NOTIFY_SOUND_HIGH_CMD: "echo high %%INSTALLATION_PATH%%",
|
|
2577
|
+
});
|
|
2578
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2579
|
+
recordedCommands.length = 0;
|
|
2580
|
+
await pi.commands.get("req-write")!.handler("Write docs", ctx);
|
|
2581
|
+
await pi.emit("agent_start", {}, ctx);
|
|
2582
|
+
await pi.emit("agent_end", {
|
|
2583
|
+
messages: [{ role: "assistant", stopReason: "error", content: [] }],
|
|
2584
|
+
}, ctx);
|
|
2585
|
+
assert.equal(recordedCommands.length, 1);
|
|
2586
|
+
} finally {
|
|
2587
|
+
setPiNotifySpawnForTests(undefined);
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2174
2591
|
test("sound toggle shortcut cycles persisted pi-notify sound levels", async () => {
|
|
2175
2592
|
const cwd = createTempDir("pi-usereq-shortcut-notify-");
|
|
2176
2593
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -2200,7 +2617,7 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
2200
2617
|
testsDir: "tests",
|
|
2201
2618
|
srcDir: ["src"],
|
|
2202
2619
|
contextFilledCells: 0,
|
|
2203
|
-
et: "
|
|
2620
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2204
2621
|
sound: expectedSound,
|
|
2205
2622
|
}),
|
|
2206
2623
|
);
|
|
@@ -2219,11 +2636,11 @@ test("git-path tool derives the repository root at runtime", async () => {
|
|
|
2219
2636
|
piUsereqExtension(pi);
|
|
2220
2637
|
const result = await executeRegisteredTool(pi, "git-path", projectBase) as {
|
|
2221
2638
|
content?: Array<{ text?: string }>;
|
|
2222
|
-
details?: { result: { path_value: string }
|
|
2639
|
+
details?: { result: { path_value: string } };
|
|
2223
2640
|
};
|
|
2224
2641
|
|
|
2225
2642
|
assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(result.details)));
|
|
2226
|
-
assert.equal(result.details?.result.path_value,
|
|
2643
|
+
assert.equal(result.details?.result.path_value, projectBase);
|
|
2227
2644
|
} finally {
|
|
2228
2645
|
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
2229
2646
|
}
|
|
@@ -2239,24 +2656,24 @@ test("static-check exposes the supported programming languages", () => {
|
|
|
2239
2656
|
});
|
|
2240
2657
|
});
|
|
2241
2658
|
|
|
2242
|
-
test("configuration menu
|
|
2659
|
+
test("configuration menu omits removed static-check raw-spec and reference actions", async () => {
|
|
2243
2660
|
const cwd = createTempDir("pi-usereq-menu-sc-");
|
|
2244
2661
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2245
2662
|
const pi = createFakePi();
|
|
2246
2663
|
piUsereqExtension(pi);
|
|
2247
2664
|
const command = pi.commands.get("pi-usereq");
|
|
2248
2665
|
assert.ok(command);
|
|
2666
|
+
const ctx = createFakeCtx(cwd, {
|
|
2667
|
+
selects: ["static-check"],
|
|
2668
|
+
});
|
|
2249
2669
|
|
|
2250
|
-
await command!.handler(
|
|
2251
|
-
"",
|
|
2252
|
-
createFakeCtx(cwd, {
|
|
2253
|
-
selects: ["static-check", "Add entry from LANG=MODULE[,CMD[,PARAM...]]", "Back", "Save and close"],
|
|
2254
|
-
inputs: ["Python=Command,true"],
|
|
2255
|
-
}),
|
|
2256
|
-
);
|
|
2670
|
+
await command!.handler("", ctx);
|
|
2257
2671
|
|
|
2258
|
-
const
|
|
2259
|
-
assert.
|
|
2672
|
+
const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
2673
|
+
assert.match(renderedStaticCheckMenu, /Add static code checker/);
|
|
2674
|
+
assert.match(renderedStaticCheckMenu, /Remove static code checker/);
|
|
2675
|
+
assert.doesNotMatch(renderedStaticCheckMenu, /Add entry from LANG=MODULE\[,CMD\[,PARAM\.\.\.\]\]/);
|
|
2676
|
+
assert.doesNotMatch(renderedStaticCheckMenu, /Show supported languages/);
|
|
2260
2677
|
});
|
|
2261
2678
|
|
|
2262
2679
|
test("configuration menu hides removed static-check modules from user-facing actions", async () => {
|
|
@@ -2273,7 +2690,7 @@ test("configuration menu hides removed static-check modules from user-facing act
|
|
|
2273
2690
|
await command!.handler("", ctx);
|
|
2274
2691
|
|
|
2275
2692
|
const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
2276
|
-
assert.match(renderedStaticCheckMenu, /Add
|
|
2693
|
+
assert.match(renderedStaticCheckMenu, /Add static code checker/);
|
|
2277
2694
|
assert.doesNotMatch(renderedStaticCheckMenu, /Pylance/);
|
|
2278
2695
|
assert.doesNotMatch(renderedStaticCheckMenu, /Ruff/);
|
|
2279
2696
|
assert.doesNotMatch(renderedStaticCheckMenu, /Dummy/);
|
|
@@ -2292,9 +2709,9 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
2292
2709
|
createFakeCtx(cwd, {
|
|
2293
2710
|
selects: [
|
|
2294
2711
|
"static-check",
|
|
2295
|
-
"Add
|
|
2712
|
+
"Add static code checker",
|
|
2296
2713
|
"Python",
|
|
2297
|
-
"
|
|
2714
|
+
"Save and close",
|
|
2298
2715
|
"Save and close",
|
|
2299
2716
|
],
|
|
2300
2717
|
inputs: ["true", ""],
|