pi-usereq 0.6.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 +38 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +24470 -4561
- package/pi-usereq/docs/REQUIREMENTS.md +70 -50
- package/pi-usereq/docs/WORKFLOW.md +112 -77
- package/src/core/config.ts +84 -29
- package/src/core/extension-status.ts +49 -177
- package/src/core/pi-notify.ts +473 -313
- package/src/core/settings-menu.ts +21 -1
- package/src/core/token-counter.ts +55 -3
- package/src/index.ts +735 -438
- package/src/resources/images/favicon.svg +21 -0
- package/src/resources/images/pi.dev.png +0 -0
- package/tests/debug-extension-harness.test.ts +6 -6
- package/tests/extension-registration.test.ts +609 -129
|
@@ -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 {
|
|
@@ -730,6 +786,46 @@ test("files-tokens returns token-optimized structured source facts", async () =>
|
|
|
730
786
|
}
|
|
731
787
|
});
|
|
732
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
|
+
|
|
733
829
|
test("files-references returns structured repository, symbol, and Doxygen facts", async () => {
|
|
734
830
|
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
735
831
|
const samplePath = path.join(projectBase, "src", "sample.ts");
|
|
@@ -1339,37 +1435,144 @@ test("configuration menus expose show-config ordering and omit overview or notif
|
|
|
1339
1435
|
piUsereqExtension(pi);
|
|
1340
1436
|
const command = pi.commands.get("pi-usereq");
|
|
1341
1437
|
assert.ok(command);
|
|
1342
|
-
const ctx = createFakeCtx(cwd, { selects: ["show-config", "notifications", "
|
|
1438
|
+
const ctx = createFakeCtx(cwd, { selects: ["show-config", "notifications", "Save and close", "Save and close"] });
|
|
1343
1439
|
|
|
1344
1440
|
await command!.handler("", ctx);
|
|
1345
1441
|
|
|
1346
1442
|
const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
|
|
1347
1443
|
assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
|
|
1348
|
-
"
|
|
1349
|
-
"
|
|
1350
|
-
"
|
|
1351
|
-
"static
|
|
1352
|
-
"
|
|
1353
|
-
"
|
|
1354
|
-
"
|
|
1444
|
+
"Document directory",
|
|
1445
|
+
"Source-code directories",
|
|
1446
|
+
"Unit tests directory",
|
|
1447
|
+
"Language static code checkers",
|
|
1448
|
+
"Enable tools",
|
|
1449
|
+
"Notifications",
|
|
1450
|
+
"Show configuration",
|
|
1355
1451
|
"Reset defaults",
|
|
1356
1452
|
"Save and close",
|
|
1357
1453
|
]);
|
|
1358
|
-
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:/);
|
|
1359
1457
|
assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
|
|
1360
|
-
"
|
|
1361
|
-
"
|
|
1362
|
-
"
|
|
1363
|
-
"
|
|
1458
|
+
"Enable notification",
|
|
1459
|
+
"Notification events",
|
|
1460
|
+
"Notify command",
|
|
1461
|
+
"Enable sound",
|
|
1462
|
+
"Sound events",
|
|
1364
1463
|
"Sound toggle hotkey bind",
|
|
1365
|
-
"
|
|
1366
|
-
"
|
|
1367
|
-
"
|
|
1368
|
-
"
|
|
1369
|
-
"
|
|
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",
|
|
1370
1476
|
]);
|
|
1371
1477
|
});
|
|
1372
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",
|
|
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");
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1373
1576
|
test("configuration menus reuse CLI settings theme semantics", async () => {
|
|
1374
1577
|
const cwd = createTempDir("pi-usereq-menu-theme-");
|
|
1375
1578
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -1451,12 +1654,12 @@ test("session_start applies configured pi-usereq startup tools", async () => {
|
|
|
1451
1654
|
testsDir: "tests",
|
|
1452
1655
|
srcDir: ["src", "foobar"],
|
|
1453
1656
|
contextFilledCells: 0,
|
|
1454
|
-
et: "
|
|
1657
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1455
1658
|
}),
|
|
1456
1659
|
);
|
|
1457
1660
|
});
|
|
1458
1661
|
|
|
1459
|
-
test("session_start renders the
|
|
1662
|
+
test("session_start renders the 0-percent context icon when context usage is empty", async () => {
|
|
1460
1663
|
const cwd = createTempDir("pi-usereq-context-empty-status-");
|
|
1461
1664
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1462
1665
|
const pi = createFakePi();
|
|
@@ -1476,7 +1679,7 @@ test("session_start renders the CLEAR overlay when context usage is empty", asyn
|
|
|
1476
1679
|
srcDir: ["src"],
|
|
1477
1680
|
contextFilledCells: 0,
|
|
1478
1681
|
contextPercent: 0,
|
|
1479
|
-
et: "
|
|
1682
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1480
1683
|
}),
|
|
1481
1684
|
);
|
|
1482
1685
|
});
|
|
@@ -1543,12 +1746,12 @@ test("context hook refreshes context usage and rounds progress cells upward", as
|
|
|
1543
1746
|
srcDir: ["src"],
|
|
1544
1747
|
contextFilledCells: 2,
|
|
1545
1748
|
contextPercent: 19.1,
|
|
1546
|
-
et: "
|
|
1749
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1547
1750
|
}),
|
|
1548
1751
|
);
|
|
1549
1752
|
});
|
|
1550
1753
|
|
|
1551
|
-
test("context hook renders the
|
|
1754
|
+
test("context hook renders the full context icon when usage exceeds ninety percent", async () => {
|
|
1552
1755
|
const cwd = createTempDir("pi-usereq-context-full-status-");
|
|
1553
1756
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1554
1757
|
const contextUsage = { tokens: 910, contextWindow: 1000, percent: 91 };
|
|
@@ -1570,19 +1773,19 @@ test("context hook renders the FULL! overlay when usage exceeds ninety percent",
|
|
|
1570
1773
|
srcDir: ["src"],
|
|
1571
1774
|
contextFilledCells: 10,
|
|
1572
1775
|
contextPercent: 91,
|
|
1573
|
-
et: "
|
|
1776
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1574
1777
|
}),
|
|
1575
1778
|
);
|
|
1576
1779
|
});
|
|
1577
1780
|
|
|
1578
|
-
test("status
|
|
1781
|
+
test("status overflow context icon uses only CLI-supported theme tokens", async () => {
|
|
1579
1782
|
const cwd = createTempDir("pi-usereq-context-theme-contract-");
|
|
1580
1783
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1581
1784
|
const usedColors = new Set<string>();
|
|
1582
1785
|
const pi = createFakePi();
|
|
1583
1786
|
piUsereqExtension(pi);
|
|
1584
1787
|
const ctx = createFakeCtx(cwd, { selects: [] }, {
|
|
1585
|
-
getContextUsage: () => ({ tokens:
|
|
1788
|
+
getContextUsage: () => ({ tokens: 1250, contextWindow: 1000, percent: 125 }),
|
|
1586
1789
|
theme: {
|
|
1587
1790
|
fg(color: string, text: string) {
|
|
1588
1791
|
assert.ok(["accent", "warning", "dim", "error"].includes(color));
|
|
@@ -1611,8 +1814,8 @@ test("status FULL overlay uses only CLI-supported theme tokens", async () => {
|
|
|
1611
1814
|
testsDir: "tests",
|
|
1612
1815
|
srcDir: ["src"],
|
|
1613
1816
|
contextFilledCells: 10,
|
|
1614
|
-
contextPercent:
|
|
1615
|
-
et: "
|
|
1817
|
+
contextPercent: 125,
|
|
1818
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1616
1819
|
}),
|
|
1617
1820
|
);
|
|
1618
1821
|
});
|
|
@@ -1651,7 +1854,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1651
1854
|
testsDir: "tests",
|
|
1652
1855
|
srcDir: ["src"],
|
|
1653
1856
|
contextFilledCells: 0,
|
|
1654
|
-
et: "
|
|
1857
|
+
et: "⏱︎ 0:00 ⚑ --:-- ⌛︎--:--",
|
|
1655
1858
|
}),
|
|
1656
1859
|
);
|
|
1657
1860
|
|
|
@@ -1665,7 +1868,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1665
1868
|
testsDir: "tests",
|
|
1666
1869
|
srcDir: ["src"],
|
|
1667
1870
|
contextFilledCells: 0,
|
|
1668
|
-
et: "
|
|
1871
|
+
et: "⏱︎ 1:01 ⚑ --:-- ⌛︎--:--",
|
|
1669
1872
|
}),
|
|
1670
1873
|
);
|
|
1671
1874
|
|
|
@@ -1686,7 +1889,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1686
1889
|
testsDir: "tests",
|
|
1687
1890
|
srcDir: ["src"],
|
|
1688
1891
|
contextFilledCells: 0,
|
|
1689
|
-
et: "
|
|
1892
|
+
et: "⏱︎ --:-- ⚑ 1:01 ⌛︎1:01",
|
|
1690
1893
|
}),
|
|
1691
1894
|
);
|
|
1692
1895
|
|
|
@@ -1710,7 +1913,7 @@ test("agent timing status updates et while preserving accumulated completed runt
|
|
|
1710
1913
|
testsDir: "tests",
|
|
1711
1914
|
srcDir: ["src"],
|
|
1712
1915
|
contextFilledCells: 0,
|
|
1713
|
-
et: "
|
|
1916
|
+
et: "⏱︎ --:-- ⚑ 1:01 ⌛︎1:01",
|
|
1714
1917
|
}),
|
|
1715
1918
|
);
|
|
1716
1919
|
assert.ok(clearedHandles.length >= 2);
|
|
@@ -1738,12 +1941,23 @@ test("session_start enables default custom tools and default embedded tools only
|
|
|
1738
1941
|
assert.equal(activeTools.has("ls"), false);
|
|
1739
1942
|
});
|
|
1740
1943
|
|
|
1741
|
-
test("default configuration
|
|
1944
|
+
test("default configuration applies the documented notify, sound, and pushover defaults", () => {
|
|
1742
1945
|
const config = getDefaultConfig(createTempDir("pi-usereq-default-notify-"));
|
|
1743
1946
|
|
|
1744
|
-
assert.equal(config["notify-
|
|
1745
|
-
assert.equal(config["notify-
|
|
1746
|
-
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%%");
|
|
1747
1961
|
});
|
|
1748
1962
|
|
|
1749
1963
|
test("configuration menu can enable embedded builtin tools", async () => {
|
|
@@ -1757,7 +1971,7 @@ test("configuration menu can enable embedded builtin tools", async () => {
|
|
|
1757
1971
|
await command!.handler(
|
|
1758
1972
|
"",
|
|
1759
1973
|
createFakeCtx(cwd, {
|
|
1760
|
-
selects: ["
|
|
1974
|
+
selects: ["Enable tools", "Enable tools", "grep", "Save and close", "Save and close"],
|
|
1761
1975
|
}),
|
|
1762
1976
|
);
|
|
1763
1977
|
|
|
@@ -1777,7 +1991,7 @@ test("configuration menu can disable configurable active tools", async () => {
|
|
|
1777
1991
|
await command!.handler(
|
|
1778
1992
|
"",
|
|
1779
1993
|
createFakeCtx(cwd, {
|
|
1780
|
-
selects: ["
|
|
1994
|
+
selects: ["Enable tools", "Disable all configurable tools", "Save and close", "Save and close"],
|
|
1781
1995
|
}),
|
|
1782
1996
|
);
|
|
1783
1997
|
|
|
@@ -1786,7 +2000,7 @@ test("configuration menu can disable configurable active tools", async () => {
|
|
|
1786
2000
|
assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
|
|
1787
2001
|
});
|
|
1788
2002
|
|
|
1789
|
-
test("configuration menu can persist
|
|
2003
|
+
test("configuration menu can persist notify and sound settings", async () => {
|
|
1790
2004
|
const cwd = createTempDir("pi-usereq-menu-notify-");
|
|
1791
2005
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1792
2006
|
const pi = createFakePi();
|
|
@@ -1796,19 +2010,26 @@ test("configuration menu can persist pi-notify settings", async () => {
|
|
|
1796
2010
|
const ctx = createFakeCtx(cwd, {
|
|
1797
2011
|
selects: [
|
|
1798
2012
|
"notifications",
|
|
1799
|
-
"
|
|
1800
|
-
"
|
|
1801
|
-
"
|
|
2013
|
+
"Enable notification",
|
|
2014
|
+
"Notification events",
|
|
2015
|
+
"Prompt failed",
|
|
2016
|
+
"Save and close",
|
|
2017
|
+
"Enable sound",
|
|
1802
2018
|
"high",
|
|
2019
|
+
"Sound events",
|
|
2020
|
+
"Prompt interrupted",
|
|
2021
|
+
"Save and close",
|
|
1803
2022
|
"Sound toggle hotkey bind",
|
|
1804
|
-
"Notify command
|
|
1805
|
-
"
|
|
1806
|
-
"
|
|
1807
|
-
"
|
|
2023
|
+
"Notify command",
|
|
2024
|
+
"Sound command (low vol.)",
|
|
2025
|
+
"Sound command (mid vol.)",
|
|
2026
|
+
"Sound command (high vol.)",
|
|
2027
|
+
"Save and close",
|
|
1808
2028
|
"Save and close",
|
|
1809
2029
|
],
|
|
1810
2030
|
inputs: [
|
|
1811
2031
|
"alt+shift+s",
|
|
2032
|
+
"notify-send -i %%INSTALLATION_PATH%%/resources/images/pi.dev.png -a \"PI-useReq\" \"%%PROMT%% @ %%BASE%% [%%TIME%%]\" \"%%RESULT%%\"",
|
|
1812
2033
|
"echo low %%INSTALLATION_PATH%%",
|
|
1813
2034
|
"echo mid %%INSTALLATION_PATH%%",
|
|
1814
2035
|
"echo high %%INSTALLATION_PATH%%",
|
|
@@ -1818,11 +2039,16 @@ test("configuration menu can persist pi-notify settings", async () => {
|
|
|
1818
2039
|
await command!.handler("", ctx);
|
|
1819
2040
|
|
|
1820
2041
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1821
|
-
assert.equal(config["notify-
|
|
1822
|
-
assert.equal(config["notify-
|
|
1823
|
-
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);
|
|
1824
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);
|
|
1825
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%%\"");
|
|
1826
2052
|
assert.equal(config.PI_NOTIFY_SOUND_LOW_CMD, "echo low %%INSTALLATION_PATH%%");
|
|
1827
2053
|
assert.equal(config.PI_NOTIFY_SOUND_MID_CMD, "echo mid %%INSTALLATION_PATH%%");
|
|
1828
2054
|
assert.equal(config.PI_NOTIFY_SOUND_HIGH_CMD, "echo high %%INSTALLATION_PATH%%");
|
|
@@ -1834,8 +2060,7 @@ test("configuration menu can persist pi-notify settings", async () => {
|
|
|
1834
2060
|
testsDir: "tests",
|
|
1835
2061
|
srcDir: ["src"],
|
|
1836
2062
|
contextFilledCells: 0,
|
|
1837
|
-
et: "
|
|
1838
|
-
beep: "esc",
|
|
2063
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
1839
2064
|
sound: "high",
|
|
1840
2065
|
}),
|
|
1841
2066
|
);
|
|
@@ -1851,17 +2076,22 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
1851
2076
|
const ctx = createFakeCtx(cwd, {
|
|
1852
2077
|
selects: [
|
|
1853
2078
|
"notifications",
|
|
1854
|
-
"
|
|
1855
|
-
"
|
|
1856
|
-
"
|
|
1857
|
-
"
|
|
1858
|
-
"
|
|
1859
|
-
"
|
|
1860
|
-
"
|
|
1861
|
-
"
|
|
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",
|
|
1862
2090
|
"Save and close",
|
|
1863
2091
|
],
|
|
1864
2092
|
inputs: [
|
|
2093
|
+
"%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2094
|
+
"%%RESULT%%\n%%ARGS%%",
|
|
1865
2095
|
"gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
1866
2096
|
"ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
1867
2097
|
],
|
|
@@ -1870,11 +2100,15 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
1870
2100
|
await command!.handler("", ctx);
|
|
1871
2101
|
|
|
1872
2102
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
1873
|
-
assert.equal(config["notify-pushover-
|
|
1874
|
-
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);
|
|
1875
2107
|
assert.equal(config["notify-pushover-user-key"], "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
|
|
1876
2108
|
assert.equal(config["notify-pushover-api-token"], "ah6bf5u2sj63mcvou6qamiabeoubbe");
|
|
1877
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%%");
|
|
1878
2112
|
assert.equal(
|
|
1879
2113
|
ctx.__state.statuses.get("pi-usereq"),
|
|
1880
2114
|
buildExpectedFakeStatusText({
|
|
@@ -1883,13 +2117,13 @@ test("configuration menu can persist pushover settings", async () => {
|
|
|
1883
2117
|
testsDir: "tests",
|
|
1884
2118
|
srcDir: ["src"],
|
|
1885
2119
|
contextFilledCells: 0,
|
|
1886
|
-
et: "
|
|
1887
|
-
|
|
2120
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2121
|
+
sound: "none",
|
|
1888
2122
|
}),
|
|
1889
2123
|
);
|
|
1890
2124
|
});
|
|
1891
2125
|
|
|
1892
|
-
test("
|
|
2126
|
+
test("prompt-end pushover requests honor global enable, event toggles, credentials, priority, and templates", async () => {
|
|
1893
2127
|
const cwd = createTempDir("pi-usereq-pushover-run-");
|
|
1894
2128
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
1895
2129
|
const configPath = getProjectConfigPath(cwd);
|
|
@@ -1932,10 +2166,13 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
1932
2166
|
|
|
1933
2167
|
try {
|
|
1934
2168
|
writeConfig({
|
|
1935
|
-
"notify-pushover-
|
|
2169
|
+
"notify-pushover-enabled": true,
|
|
2170
|
+
"notify-pushover-on-completed": true,
|
|
1936
2171
|
"notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
1937
2172
|
"notify-pushover-api-token": "ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
1938
2173
|
"notify-pushover-priority": 1,
|
|
2174
|
+
"notify-pushover-title": "%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2175
|
+
"notify-pushover-text": "%%RESULT%%\n%%ARGS%%",
|
|
1939
2176
|
});
|
|
1940
2177
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
1941
2178
|
recordedRequests.length = 0;
|
|
@@ -1959,8 +2196,8 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
1959
2196
|
assert.equal(successParams.get("token"), "ah6bf5u2sj63mcvou6qamiabeoubbe");
|
|
1960
2197
|
assert.equal(successParams.get("user"), "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
|
|
1961
2198
|
assert.equal(successParams.get("priority"), "1");
|
|
1962
|
-
assert.equal(successParams.get("message"), "
|
|
1963
|
-
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]`);
|
|
1964
2201
|
|
|
1965
2202
|
recordedRequests.length = 0;
|
|
1966
2203
|
nowMs = 10_000;
|
|
@@ -1995,17 +2232,69 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
1995
2232
|
assert.equal(recordedRequests.length, 0);
|
|
1996
2233
|
|
|
1997
2234
|
writeConfig({
|
|
1998
|
-
"notify-pushover-
|
|
2235
|
+
"notify-pushover-enabled": true,
|
|
2236
|
+
"notify-pushover-on-interrupted": true,
|
|
2237
|
+
"notify-pushover-on-failed": true,
|
|
1999
2238
|
"notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
|
|
2000
|
-
"notify-pushover-api-token": "",
|
|
2239
|
+
"notify-pushover-api-token": "ah6bf5u2sj63mcvou6qamiabeoubbe",
|
|
2001
2240
|
"notify-pushover-priority": 1,
|
|
2241
|
+
"notify-pushover-title": "%%PROMT%% @ %%BASE%% [%%TIME%%]",
|
|
2242
|
+
"notify-pushover-text": "%%RESULT%%\n%%ARGS%%",
|
|
2002
2243
|
});
|
|
2003
2244
|
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
2004
2245
|
recordedRequests.length = 0;
|
|
2005
2246
|
nowMs = 30_000;
|
|
2006
|
-
await pi.commands.get("req-
|
|
2247
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
2007
2248
|
await pi.emit("agent_start", {}, ctx);
|
|
2008
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;
|
|
2009
2298
|
await pi.emit("agent_end", {
|
|
2010
2299
|
messages: [
|
|
2011
2300
|
{
|
|
@@ -2022,7 +2311,7 @@ test("successful req prompt completion sends pushover requests and suppresses in
|
|
|
2022
2311
|
}
|
|
2023
2312
|
});
|
|
2024
2313
|
|
|
2025
|
-
test("pushover global
|
|
2314
|
+
test("pushover global enable controls status and suppresses delivery when disabled", async () => {
|
|
2026
2315
|
const cwd = createTempDir("pi-usereq-pushover-disabled-");
|
|
2027
2316
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2028
2317
|
const pi = createFakePi();
|
|
@@ -2032,13 +2321,9 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2032
2321
|
const ctx = createFakeCtx(cwd, {
|
|
2033
2322
|
selects: [
|
|
2034
2323
|
"notifications",
|
|
2035
|
-
"Pushover
|
|
2036
|
-
"
|
|
2037
|
-
"
|
|
2038
|
-
"User Key/Delivery Group Key",
|
|
2039
|
-
"Token/API Token Key",
|
|
2040
|
-
"Back",
|
|
2041
|
-
"Back",
|
|
2324
|
+
"Pushover User Key/Delivery Group Key",
|
|
2325
|
+
"Pushover Token/API Token Key",
|
|
2326
|
+
"Save and close",
|
|
2042
2327
|
"Save and close",
|
|
2043
2328
|
],
|
|
2044
2329
|
inputs: [
|
|
@@ -2050,8 +2335,8 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2050
2335
|
await command!.handler("", ctx);
|
|
2051
2336
|
|
|
2052
2337
|
const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
|
|
2053
|
-
assert.equal(config["notify-pushover-
|
|
2054
|
-
assert.equal(config["notify-pushover-
|
|
2338
|
+
assert.equal(config["notify-pushover-enabled"], false);
|
|
2339
|
+
assert.equal(config["notify-pushover-on-completed"], true);
|
|
2055
2340
|
assert.equal(
|
|
2056
2341
|
ctx.__state.statuses.get("pi-usereq"),
|
|
2057
2342
|
buildExpectedFakeStatusText({
|
|
@@ -2060,8 +2345,8 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2060
2345
|
testsDir: "tests",
|
|
2061
2346
|
srcDir: ["src"],
|
|
2062
2347
|
contextFilledCells: 0,
|
|
2063
|
-
et: "
|
|
2064
|
-
|
|
2348
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2349
|
+
sound: "none",
|
|
2065
2350
|
}),
|
|
2066
2351
|
);
|
|
2067
2352
|
|
|
@@ -2108,6 +2393,201 @@ test("pushover global disable suppresses delivery without clearing enabled state
|
|
|
2108
2393
|
}
|
|
2109
2394
|
});
|
|
2110
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
|
+
|
|
2111
2591
|
test("sound toggle shortcut cycles persisted pi-notify sound levels", async () => {
|
|
2112
2592
|
const cwd = createTempDir("pi-usereq-shortcut-notify-");
|
|
2113
2593
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
@@ -2137,7 +2617,7 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
|
|
|
2137
2617
|
testsDir: "tests",
|
|
2138
2618
|
srcDir: ["src"],
|
|
2139
2619
|
contextFilledCells: 0,
|
|
2140
|
-
et: "
|
|
2620
|
+
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
2141
2621
|
sound: expectedSound,
|
|
2142
2622
|
}),
|
|
2143
2623
|
);
|
|
@@ -2176,24 +2656,24 @@ test("static-check exposes the supported programming languages", () => {
|
|
|
2176
2656
|
});
|
|
2177
2657
|
});
|
|
2178
2658
|
|
|
2179
|
-
test("configuration menu
|
|
2659
|
+
test("configuration menu omits removed static-check raw-spec and reference actions", async () => {
|
|
2180
2660
|
const cwd = createTempDir("pi-usereq-menu-sc-");
|
|
2181
2661
|
fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
|
|
2182
2662
|
const pi = createFakePi();
|
|
2183
2663
|
piUsereqExtension(pi);
|
|
2184
2664
|
const command = pi.commands.get("pi-usereq");
|
|
2185
2665
|
assert.ok(command);
|
|
2666
|
+
const ctx = createFakeCtx(cwd, {
|
|
2667
|
+
selects: ["static-check"],
|
|
2668
|
+
});
|
|
2186
2669
|
|
|
2187
|
-
await command!.handler(
|
|
2188
|
-
"",
|
|
2189
|
-
createFakeCtx(cwd, {
|
|
2190
|
-
selects: ["static-check", "Add entry from LANG=MODULE[,CMD[,PARAM...]]", "Back", "Save and close"],
|
|
2191
|
-
inputs: ["Python=Command,true"],
|
|
2192
|
-
}),
|
|
2193
|
-
);
|
|
2670
|
+
await command!.handler("", ctx);
|
|
2194
2671
|
|
|
2195
|
-
const
|
|
2196
|
-
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/);
|
|
2197
2677
|
});
|
|
2198
2678
|
|
|
2199
2679
|
test("configuration menu hides removed static-check modules from user-facing actions", async () => {
|
|
@@ -2210,7 +2690,7 @@ test("configuration menu hides removed static-check modules from user-facing act
|
|
|
2210
2690
|
await command!.handler("", ctx);
|
|
2211
2691
|
|
|
2212
2692
|
const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
|
|
2213
|
-
assert.match(renderedStaticCheckMenu, /Add
|
|
2693
|
+
assert.match(renderedStaticCheckMenu, /Add static code checker/);
|
|
2214
2694
|
assert.doesNotMatch(renderedStaticCheckMenu, /Pylance/);
|
|
2215
2695
|
assert.doesNotMatch(renderedStaticCheckMenu, /Ruff/);
|
|
2216
2696
|
assert.doesNotMatch(renderedStaticCheckMenu, /Dummy/);
|
|
@@ -2229,9 +2709,9 @@ test("configuration menu can add guided static-check entries for explicit suppor
|
|
|
2229
2709
|
createFakeCtx(cwd, {
|
|
2230
2710
|
selects: [
|
|
2231
2711
|
"static-check",
|
|
2232
|
-
"Add
|
|
2712
|
+
"Add static code checker",
|
|
2233
2713
|
"Python",
|
|
2234
|
-
"
|
|
2714
|
+
"Save and close",
|
|
2235
2715
|
"Save and close",
|
|
2236
2716
|
],
|
|
2237
2717
|
inputs: ["true", ""],
|