pi-usereq 0.4.0 → 0.5.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.
Files changed (60) hide show
  1. package/.gitignore +3 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +1 -1
  4. package/package.json +1 -1
  5. package/{req → pi-usereq}/docs/REFERENCES.md +196 -159
  6. package/{req → pi-usereq}/docs/REQUIREMENTS.md +65 -44
  7. package/{req → pi-usereq}/docs/WORKFLOW.md +66 -73
  8. package/src/core/config.ts +39 -12
  9. package/src/core/extension-status.ts +94 -58
  10. package/src/core/pi-notify.ts +198 -11
  11. package/src/core/runtime-project-paths.ts +3 -32
  12. package/src/core/settings-menu.ts +9 -4
  13. package/src/core/static-check.ts +35 -171
  14. package/src/index.ts +281 -86
  15. package/tests/attended-results-scenarios.ts +3 -13
  16. package/tests/cli-command-option-parity.test.ts +21 -12
  17. package/tests/debug-extension-harness.test.ts +11 -7
  18. package/tests/extension-registration.test.ts +371 -71
  19. package/tests/oracle-project.test.ts +8 -3
  20. package/tests/oracle-standalone.test.ts +7 -13
  21. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_c.c.json +0 -5
  22. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_cpp.cpp.json +0 -5
  23. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_csharp.cs.json +0 -5
  24. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_elixir.ex.json +0 -5
  25. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_go.go.json +0 -5
  26. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_haskell.hs.json +0 -5
  27. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_java.java.json +0 -5
  28. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_javascript.js.json +0 -5
  29. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_kotlin.kt.json +0 -5
  30. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_lua.lua.json +0 -5
  31. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_perl.pl.json +0 -5
  32. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_php.php.json +0 -5
  33. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_python.py.json +0 -5
  34. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_ruby.rb.json +0 -5
  35. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_rust.rs.json +0 -5
  36. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_scala.scala.json +0 -5
  37. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_shell.sh.json +0 -5
  38. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_swift.swift.json +0 -5
  39. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_typescript.ts.json +0 -5
  40. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_zig.zig.json +0 -5
  41. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_c.c.json +0 -5
  42. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_cpp.cpp.json +0 -5
  43. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_csharp.cs.json +0 -5
  44. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_elixir.ex.json +0 -5
  45. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_go.go.json +0 -5
  46. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_haskell.hs.json +0 -5
  47. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_java.java.json +0 -5
  48. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_javascript.js.json +0 -5
  49. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_kotlin.kt.json +0 -5
  50. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_lua.lua.json +0 -5
  51. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_perl.pl.json +0 -5
  52. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_php.php.json +0 -5
  53. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_python.py.json +0 -5
  54. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_ruby.rb.json +0 -5
  55. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_rust.rs.json +0 -5
  56. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_scala.scala.json +0 -5
  57. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_shell.sh.json +0 -5
  58. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_swift.swift.json +0 -5
  59. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_typescript.ts.json +0 -5
  60. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_zig.zig.json +0 -5
@@ -4,7 +4,13 @@ import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import piUsereqExtension from "../src/index.ts";
7
- import { DEFAULT_DOCS_DIR, getProjectConfigPath } from "../src/core/config.js";
7
+ import {
8
+ DEFAULT_DOCS_DIR,
9
+ getDefaultConfig,
10
+ getProjectConfigPath,
11
+ } from "../src/core/config.js";
12
+ import { formatRuntimePathForDisplay } from "../src/core/path-context.js";
13
+ import { setPiNotifyHttpsRequestForTests } from "../src/core/pi-notify.js";
8
14
  import {
9
15
  PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES,
10
16
  PI_USEREQ_EMBEDDED_TOOL_NAMES,
@@ -284,9 +290,9 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
284
290
 
285
291
  /**
286
292
  * @brief Builds the expected fake context-bar payload for assertions.
287
- * @details Resolves the empty-state `CLEAR` overlay for unavailable or
288
- * non-positive percent, resolves the high-water `FULL!` overlay for percent
289
- * above 90, and otherwise reconstructs the 5-cell bar. Runtime is O(1). No
293
+ * @details Resolves the empty-state `◀ CLEAR ▶ ` overlay for unavailable or
294
+ * non-positive percent, resolves the centered ` ◀ FULL ▶ ` overlay for percent
295
+ * above 90, and otherwise reconstructs the 10-cell bar. Runtime is O(1). No
290
296
  * external state is mutated.
291
297
  * @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-bar facts.
292
298
  * @return {string} Encoded context-bar string.
@@ -298,16 +304,16 @@ function buildExpectedFakeContextBar(options: {
298
304
  if (options.percent === undefined || options.percent === null || options.percent <= 0) {
299
305
  return formatFakeThemeBackgroundFromForeground(
300
306
  "accent",
301
- formatFakeThemeForeground("warning", "CLEAR"),
307
+ formatFakeThemeForeground("warning", "◀ CLEAR ▶ "),
302
308
  );
303
309
  }
304
310
  if (options.percent > 90) {
305
311
  return formatFakeThemeBackgroundFromForeground(
306
312
  "warning",
307
- formatFakeThemeForeground("error", "FULL!"),
313
+ formatFakeThemeForeground("error", " ◀ FULL ▶ "),
308
314
  );
309
315
  }
310
- return Array.from({ length: 5 }, (_value, index) =>
316
+ return Array.from({ length: 10 }, (_value, index) =>
311
317
  formatFakeThemeBackgroundFromForeground(
312
318
  "accent",
313
319
  formatFakeThemeForeground(index < options.filledCells ? "warning" : "dim", "▓"),
@@ -317,23 +323,21 @@ function buildExpectedFakeContextBar(options: {
317
323
 
318
324
  /**
319
325
  * @brief Builds the expected fake pi-usereq status-bar string for assertions.
320
- * @details Reconstructs the field order, separators, context bar, and timing fields emitted by the extension using deterministic fake theme markers. Runtime is O(s) in source-path count. No external state is mutated.
321
- * @param[in] options {{ gitPath?: string; basePath?: string; docsDir: string; testsDir: string; srcDir: string[]; toolCount: number; contextFilledCells: number; contextPercent?: number | null; elapsed: string; last: string; beep?: string; sound?: string }} Expected status facts.
326
+ * @details Reconstructs the field order, separators, context bar, consolidated elapsed field, and Pushover enable field emitted by the extension using deterministic fake theme markers. Runtime is O(1). No external state is mutated.
327
+ * @param[in] options {{ basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; beep?: string; sound?: string; pushover?: string }} Expected status facts.
322
328
  * @return {string} Encoded status-bar string.
323
329
  */
324
330
  function buildExpectedFakeStatusText(options: {
325
- gitPath?: string;
326
- basePath?: string;
327
- docsDir: string;
328
- testsDir: string;
329
- srcDir: string[];
330
- toolCount: number;
331
+ basePath: string;
332
+ docsDir?: string;
333
+ testsDir?: string;
334
+ srcDir?: string[];
331
335
  contextFilledCells: number;
332
336
  contextPercent?: number | null;
333
- elapsed: string;
334
- last: string;
337
+ et: string;
335
338
  beep?: string;
336
339
  sound?: string;
340
+ pushover?: string;
337
341
  }): string {
338
342
  const buildField = (fieldName: string, value: string): string =>
339
343
  `${formatFakeThemeForeground("accent", `${fieldName}:`)}${formatFakeThemeForeground("warning", value)}`;
@@ -342,20 +346,27 @@ function buildExpectedFakeStatusText(options: {
342
346
  percent: options.contextPercent,
343
347
  });
344
348
  return [
345
- buildField("git", options.gitPath ?? ""),
346
- buildField("base", options.basePath ?? "."),
347
- buildField("docs", options.docsDir),
348
- buildField("tests", options.testsDir),
349
- buildField("src", options.srcDir.join(",")),
350
- buildField("tools", String(options.toolCount)),
349
+ buildField("base", options.basePath),
351
350
  `${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
352
- buildField("elapsed", options.elapsed),
353
- buildField("last", options.last),
354
- buildField("beep", options.beep ?? "none"),
351
+ buildField("elapsed", options.et),
352
+ buildField("beep", options.beep ?? "end,esc,err"),
355
353
  buildField("sound", options.sound ?? "none"),
354
+ buildField("pushover", options.pushover ?? "off"),
356
355
  ].join(formatFakeThemeForeground("dim", " • "));
357
356
  }
358
357
 
358
+ /**
359
+ * @brief Formats one expected absolute base path for fake status assertions.
360
+ * @details Resolves the supplied cwd and normalizes path separators to `/` so
361
+ * fake status comparisons stay stable across operating systems. Runtime is O(p)
362
+ * in path length. No external state is mutated.
363
+ * @param[in] cwd {string} Runtime working directory.
364
+ * @return {string} Slash-normalized absolute base path.
365
+ */
366
+ function buildExpectedFakeBasePath(cwd: string): string {
367
+ return path.resolve(cwd).split(path.sep).join("/");
368
+ }
369
+
359
370
  /**
360
371
  * @brief Creates a fake extension command context with scripted UI interactions.
361
372
  * @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.
@@ -1383,7 +1394,9 @@ test("configuration menu omits prompt-delivery controls and persisted config omi
1383
1394
  });
1384
1395
 
1385
1396
  test("configuration menus expose show-config ordering and omit overview or notification-reference rows", async () => {
1386
- const cwd = createTempDir("pi-usereq-menu-structure-");
1397
+ const cwd = fs.mkdtempSync(
1398
+ path.join(process.env.HOME ?? process.cwd(), "pi-usereq-menu-structure-"),
1399
+ );
1387
1400
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1388
1401
  const pi = createFakePi();
1389
1402
  piUsereqExtension(pi);
@@ -1393,6 +1406,7 @@ test("configuration menus expose show-config ordering and omit overview or notif
1393
1406
 
1394
1407
  await command!.handler("", ctx);
1395
1408
 
1409
+ const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
1396
1410
  assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
1397
1411
  "docs-dir",
1398
1412
  "tests-dir",
@@ -1400,10 +1414,11 @@ test("configuration menus expose show-config ordering and omit overview or notif
1400
1414
  "static-check",
1401
1415
  "startup tools",
1402
1416
  "notifications",
1403
- "Reset defaults",
1404
1417
  "show-config",
1418
+ "Reset defaults",
1405
1419
  "Save and close",
1406
1420
  ]);
1421
+ assert.ok(renderedMenu.includes(formatRuntimePathForDisplay(getProjectConfigPath(cwd))));
1407
1422
  assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
1408
1423
  "Toggle beep on success",
1409
1424
  "Toggle beep on escape",
@@ -1413,6 +1428,7 @@ test("configuration menus expose show-config ordering and omit overview or notif
1413
1428
  "Notify command (low vol.)",
1414
1429
  "Notify command (mid vol.)",
1415
1430
  "Notify command (high vol.)",
1431
+ "Pushover notifications",
1416
1432
  "Back",
1417
1433
  ]);
1418
1434
  });
@@ -1493,13 +1509,12 @@ test("session_start applies configured pi-usereq startup tools", async () => {
1493
1509
  assert.equal(
1494
1510
  ctx.__state.statuses.get("pi-usereq"),
1495
1511
  buildExpectedFakeStatusText({
1512
+ basePath: buildExpectedFakeBasePath(cwd),
1496
1513
  docsDir: DEFAULT_DOCS_DIR,
1497
1514
  testsDir: "tests",
1498
1515
  srcDir: ["src", "foobar"],
1499
- toolCount: 2,
1500
1516
  contextFilledCells: 0,
1501
- elapsed: "idle",
1502
- last: "N/A",
1517
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1503
1518
  }),
1504
1519
  );
1505
1520
  });
@@ -1518,14 +1533,13 @@ test("session_start renders the CLEAR overlay when context usage is empty", asyn
1518
1533
  assert.equal(
1519
1534
  ctx.__state.statuses.get("pi-usereq"),
1520
1535
  buildExpectedFakeStatusText({
1536
+ basePath: buildExpectedFakeBasePath(cwd),
1521
1537
  docsDir: DEFAULT_DOCS_DIR,
1522
1538
  testsDir: "tests",
1523
1539
  srcDir: ["src"],
1524
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1525
1540
  contextFilledCells: 0,
1526
1541
  contextPercent: 0,
1527
- elapsed: "idle",
1528
- last: "N/A",
1542
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1529
1543
  }),
1530
1544
  );
1531
1545
  });
@@ -1586,14 +1600,13 @@ test("context hook refreshes context usage and rounds progress cells upward", as
1586
1600
  assert.equal(
1587
1601
  ctx.__state.statuses.get("pi-usereq"),
1588
1602
  buildExpectedFakeStatusText({
1603
+ basePath: buildExpectedFakeBasePath(cwd),
1589
1604
  docsDir: DEFAULT_DOCS_DIR,
1590
1605
  testsDir: "tests",
1591
1606
  srcDir: ["src"],
1592
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1593
- contextFilledCells: 1,
1607
+ contextFilledCells: 2,
1594
1608
  contextPercent: 19.1,
1595
- elapsed: "idle",
1596
- last: "N/A",
1609
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1597
1610
  }),
1598
1611
  );
1599
1612
  });
@@ -1614,14 +1627,13 @@ test("context hook renders the FULL! overlay when usage exceeds ninety percent",
1614
1627
  assert.equal(
1615
1628
  ctx.__state.statuses.get("pi-usereq"),
1616
1629
  buildExpectedFakeStatusText({
1630
+ basePath: buildExpectedFakeBasePath(cwd),
1617
1631
  docsDir: DEFAULT_DOCS_DIR,
1618
1632
  testsDir: "tests",
1619
1633
  srcDir: ["src"],
1620
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1621
- contextFilledCells: 5,
1634
+ contextFilledCells: 10,
1622
1635
  contextPercent: 91,
1623
- elapsed: "idle",
1624
- last: "N/A",
1636
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1625
1637
  }),
1626
1638
  );
1627
1639
  });
@@ -1657,19 +1669,18 @@ test("status FULL overlay uses only CLI-supported theme tokens", async () => {
1657
1669
  assert.equal(
1658
1670
  ctx.__state.statuses.get("pi-usereq"),
1659
1671
  buildExpectedFakeStatusText({
1672
+ basePath: buildExpectedFakeBasePath(cwd),
1660
1673
  docsDir: DEFAULT_DOCS_DIR,
1661
1674
  testsDir: "tests",
1662
1675
  srcDir: ["src"],
1663
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1664
- contextFilledCells: 5,
1676
+ contextFilledCells: 10,
1665
1677
  contextPercent: 91,
1666
- elapsed: "idle",
1667
- last: "N/A",
1678
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1668
1679
  }),
1669
1680
  );
1670
1681
  });
1671
1682
 
1672
- test("agent timing status updates elapsed and last while preserving aborted last values", async () => {
1683
+ test("agent timing status updates et while preserving accumulated completed runtime", async () => {
1673
1684
  const cwd = createTempDir("pi-usereq-timing-status-");
1674
1685
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1675
1686
 
@@ -1698,13 +1709,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1698
1709
  assert.equal(
1699
1710
  ctx.__state.statuses.get("pi-usereq"),
1700
1711
  buildExpectedFakeStatusText({
1712
+ basePath: buildExpectedFakeBasePath(cwd),
1701
1713
  docsDir: DEFAULT_DOCS_DIR,
1702
1714
  testsDir: "tests",
1703
1715
  srcDir: ["src"],
1704
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1705
1716
  contextFilledCells: 0,
1706
- elapsed: "0:00",
1707
- last: "N/A",
1717
+ et: " ⏱︎ 0:00 ⚑ --:-- ⌛︎ --:--",
1708
1718
  }),
1709
1719
  );
1710
1720
 
@@ -1713,13 +1723,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1713
1723
  assert.equal(
1714
1724
  ctx.__state.statuses.get("pi-usereq"),
1715
1725
  buildExpectedFakeStatusText({
1726
+ basePath: buildExpectedFakeBasePath(cwd),
1716
1727
  docsDir: DEFAULT_DOCS_DIR,
1717
1728
  testsDir: "tests",
1718
1729
  srcDir: ["src"],
1719
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1720
1730
  contextFilledCells: 0,
1721
- elapsed: "1:01",
1722
- last: "N/A",
1731
+ et: " ⏱︎ 1:01 ⚑ --:-- ⌛︎ --:--",
1723
1732
  }),
1724
1733
  );
1725
1734
 
@@ -1735,13 +1744,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1735
1744
  assert.equal(
1736
1745
  ctx.__state.statuses.get("pi-usereq"),
1737
1746
  buildExpectedFakeStatusText({
1747
+ basePath: buildExpectedFakeBasePath(cwd),
1738
1748
  docsDir: DEFAULT_DOCS_DIR,
1739
1749
  testsDir: "tests",
1740
1750
  srcDir: ["src"],
1741
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1742
1751
  contextFilledCells: 0,
1743
- elapsed: "idle",
1744
- last: "1:01",
1752
+ et: " ⏱︎ --:-- ⚑ 1:01 ⌛︎ 1:01",
1745
1753
  }),
1746
1754
  );
1747
1755
 
@@ -1760,13 +1768,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1760
1768
  assert.equal(
1761
1769
  ctx.__state.statuses.get("pi-usereq"),
1762
1770
  buildExpectedFakeStatusText({
1771
+ basePath: buildExpectedFakeBasePath(cwd),
1763
1772
  docsDir: DEFAULT_DOCS_DIR,
1764
1773
  testsDir: "tests",
1765
1774
  srcDir: ["src"],
1766
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1767
1775
  contextFilledCells: 0,
1768
- elapsed: "idle",
1769
- last: "1:01",
1776
+ et: " ⏱︎ --:-- ⚑ 1:01 ⌛︎ 1:01",
1770
1777
  }),
1771
1778
  );
1772
1779
  assert.ok(clearedHandles.length >= 2);
@@ -1794,6 +1801,14 @@ test("session_start enables default custom tools and default embedded tools only
1794
1801
  assert.equal(activeTools.has("ls"), false);
1795
1802
  });
1796
1803
 
1804
+ test("default configuration enables all pi-notify beep outcomes", () => {
1805
+ const config = getDefaultConfig(createTempDir("pi-usereq-default-notify-"));
1806
+
1807
+ assert.equal(config["notify-beep-on-end"], true);
1808
+ assert.equal(config["notify-beep-on-esc"], true);
1809
+ assert.equal(config["notify-beep-on-error"], true);
1810
+ });
1811
+
1797
1812
  test("configuration menu can enable embedded builtin tools", async () => {
1798
1813
  const cwd = createTempDir("pi-usereq-menu-embedded-");
1799
1814
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1866,9 +1881,9 @@ test("configuration menu can persist pi-notify settings", async () => {
1866
1881
  await command!.handler("", ctx);
1867
1882
 
1868
1883
  const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1869
- assert.equal(config["notify-beep-on-end"], true);
1870
- assert.equal(config["notify-beep-on-error"], true);
1871
- assert.equal(config["notify-beep-on-esc"], false);
1884
+ assert.equal(config["notify-beep-on-end"], false);
1885
+ assert.equal(config["notify-beep-on-error"], false);
1886
+ assert.equal(config["notify-beep-on-esc"], true);
1872
1887
  assert.equal(config["notify-sound"], "high");
1873
1888
  assert.equal(config["notify-sound-toggle-shortcut"], "alt+shift+s");
1874
1889
  assert.equal(config.PI_NOTIFY_SOUND_LOW_CMD, "echo low %%INSTALLATION_PATH%%");
@@ -1877,19 +1892,285 @@ test("configuration menu can persist pi-notify settings", async () => {
1877
1892
  assert.equal(
1878
1893
  ctx.__state.statuses.get("pi-usereq"),
1879
1894
  buildExpectedFakeStatusText({
1895
+ basePath: buildExpectedFakeBasePath(cwd),
1880
1896
  docsDir: DEFAULT_DOCS_DIR,
1881
1897
  testsDir: "tests",
1882
1898
  srcDir: ["src"],
1883
- toolCount: pi.getActiveTools().length,
1884
1899
  contextFilledCells: 0,
1885
- elapsed: "idle",
1886
- last: "N/A",
1887
- beep: "end,err",
1900
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1901
+ beep: "esc",
1888
1902
  sound: "high",
1889
1903
  }),
1890
1904
  );
1891
1905
  });
1892
1906
 
1907
+ test("configuration menu can persist pushover settings", async () => {
1908
+ const cwd = createTempDir("pi-usereq-menu-pushover-");
1909
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1910
+ const pi = createFakePi();
1911
+ piUsereqExtension(pi);
1912
+ const command = pi.commands.get("pi-usereq");
1913
+ assert.ok(command);
1914
+ const ctx = createFakeCtx(cwd, {
1915
+ selects: [
1916
+ "notifications",
1917
+ "Pushover notifications",
1918
+ "Notify on success",
1919
+ "User Key/Delivery Group Key",
1920
+ "Token/API Token Key",
1921
+ "Priority",
1922
+ "1=High Priority",
1923
+ "Back",
1924
+ "Back",
1925
+ "Save and close",
1926
+ ],
1927
+ inputs: [
1928
+ "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
1929
+ "ah6bf5u2sj63mcvou6qamiabeoubbe",
1930
+ ],
1931
+ });
1932
+
1933
+ await command!.handler("", ctx);
1934
+
1935
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1936
+ assert.equal(config["notify-pushover-global-disable"], false);
1937
+ assert.equal(config["notify-pushover-on-success"], true);
1938
+ assert.equal(config["notify-pushover-user-key"], "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
1939
+ assert.equal(config["notify-pushover-api-token"], "ah6bf5u2sj63mcvou6qamiabeoubbe");
1940
+ assert.equal(config["notify-pushover-priority"], 1);
1941
+ assert.equal(
1942
+ ctx.__state.statuses.get("pi-usereq"),
1943
+ buildExpectedFakeStatusText({
1944
+ basePath: buildExpectedFakeBasePath(cwd),
1945
+ docsDir: DEFAULT_DOCS_DIR,
1946
+ testsDir: "tests",
1947
+ srcDir: ["src"],
1948
+ contextFilledCells: 0,
1949
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1950
+ pushover: "on",
1951
+ }),
1952
+ );
1953
+ });
1954
+
1955
+ test("successful req prompt completion sends pushover requests and suppresses invalid scenarios", async () => {
1956
+ const cwd = createTempDir("pi-usereq-pushover-run-");
1957
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1958
+ const configPath = getProjectConfigPath(cwd);
1959
+ const writeConfig = (overrides: Record<string, unknown>) => {
1960
+ const config = {
1961
+ ...getDefaultConfig(cwd),
1962
+ ...overrides,
1963
+ };
1964
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
1965
+ };
1966
+ const pi = createFakePi();
1967
+ piUsereqExtension(pi);
1968
+ const ctx = createFakeCtx(cwd);
1969
+ const originalDateNow = Date.now;
1970
+ const recordedRequests: Array<{ url: string; options: Record<string, unknown>; body: string }> = [];
1971
+ let nowMs = 0;
1972
+ setPiNotifyHttpsRequestForTests(((url: URL, options: Record<string, unknown>, callback?: (response: any) => void) => {
1973
+ const record = { url: url.toString(), options, body: "" };
1974
+ recordedRequests.push(record);
1975
+ const fakeResponse = {
1976
+ on(_eventName: string, _handler: (...args: any[]) => void) {
1977
+ return fakeResponse;
1978
+ },
1979
+ resume() {
1980
+ return undefined;
1981
+ },
1982
+ };
1983
+ callback?.(fakeResponse);
1984
+ const fakeRequest = {
1985
+ on(_eventName: string, _handler: (...args: any[]) => void) {
1986
+ return fakeRequest;
1987
+ },
1988
+ end(body?: string) {
1989
+ record.body = body ?? "";
1990
+ },
1991
+ };
1992
+ return fakeRequest as unknown as ReturnType<typeof import("node:https").request>;
1993
+ }) as typeof import("node:https").request);
1994
+ Date.now = () => nowMs;
1995
+
1996
+ try {
1997
+ writeConfig({
1998
+ "notify-pushover-on-success": true,
1999
+ "notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
2000
+ "notify-pushover-api-token": "ah6bf5u2sj63mcvou6qamiabeoubbe",
2001
+ "notify-pushover-priority": 1,
2002
+ });
2003
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2004
+ recordedRequests.length = 0;
2005
+ nowMs = 0;
2006
+ await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
2007
+ await pi.emit("agent_start", {}, ctx);
2008
+ nowMs = 5_000;
2009
+ await pi.emit("agent_end", {
2010
+ messages: [
2011
+ {
2012
+ role: "assistant",
2013
+ stopReason: "stop",
2014
+ content: [],
2015
+ },
2016
+ ],
2017
+ }, ctx);
2018
+
2019
+ assert.equal(recordedRequests.length, 1);
2020
+ assert.equal(recordedRequests[0]?.url, "https://api.pushover.net/1/messages.json");
2021
+ const successParams = new URLSearchParams(recordedRequests[0]?.body ?? "");
2022
+ assert.equal(successParams.get("token"), "ah6bf5u2sj63mcvou6qamiabeoubbe");
2023
+ assert.equal(successParams.get("user"), "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
2024
+ assert.equal(successParams.get("priority"), "1");
2025
+ assert.equal(successParams.get("message"), "Inspect src/index.ts");
2026
+ assert.equal(successParams.get("title"), `analyze @ ${buildExpectedFakeBasePath(cwd)} [0:05]`);
2027
+
2028
+ recordedRequests.length = 0;
2029
+ nowMs = 10_000;
2030
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2031
+ await pi.emit("agent_start", {}, ctx);
2032
+ nowMs = 14_000;
2033
+ await pi.emit("agent_end", {
2034
+ messages: [
2035
+ {
2036
+ role: "assistant",
2037
+ stopReason: "aborted",
2038
+ content: [],
2039
+ },
2040
+ ],
2041
+ }, ctx);
2042
+ assert.equal(recordedRequests.length, 0);
2043
+
2044
+ recordedRequests.length = 0;
2045
+ nowMs = 20_000;
2046
+ await pi.commands.get("req-create")!.handler("Create docs", ctx);
2047
+ await pi.emit("agent_start", {}, ctx);
2048
+ nowMs = 23_000;
2049
+ await pi.emit("agent_end", {
2050
+ messages: [
2051
+ {
2052
+ role: "assistant",
2053
+ stopReason: "error",
2054
+ content: [],
2055
+ },
2056
+ ],
2057
+ }, ctx);
2058
+ assert.equal(recordedRequests.length, 0);
2059
+
2060
+ writeConfig({
2061
+ "notify-pushover-on-success": true,
2062
+ "notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
2063
+ "notify-pushover-api-token": "",
2064
+ "notify-pushover-priority": 1,
2065
+ });
2066
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2067
+ recordedRequests.length = 0;
2068
+ nowMs = 30_000;
2069
+ await pi.commands.get("req-write")!.handler("Write docs", ctx);
2070
+ await pi.emit("agent_start", {}, ctx);
2071
+ nowMs = 32_000;
2072
+ await pi.emit("agent_end", {
2073
+ messages: [
2074
+ {
2075
+ role: "assistant",
2076
+ stopReason: "stop",
2077
+ content: [],
2078
+ },
2079
+ ],
2080
+ }, ctx);
2081
+ assert.equal(recordedRequests.length, 0);
2082
+ } finally {
2083
+ setPiNotifyHttpsRequestForTests(undefined);
2084
+ Date.now = originalDateNow;
2085
+ }
2086
+ });
2087
+
2088
+ test("pushover global disable suppresses delivery without clearing enabled state", async () => {
2089
+ const cwd = createTempDir("pi-usereq-pushover-disabled-");
2090
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
2091
+ const pi = createFakePi();
2092
+ piUsereqExtension(pi);
2093
+ const command = pi.commands.get("pi-usereq");
2094
+ assert.ok(command);
2095
+ const ctx = createFakeCtx(cwd, {
2096
+ selects: [
2097
+ "notifications",
2098
+ "Pushover notifications",
2099
+ "Notify on success",
2100
+ "Global disable",
2101
+ "User Key/Delivery Group Key",
2102
+ "Token/API Token Key",
2103
+ "Back",
2104
+ "Back",
2105
+ "Save and close",
2106
+ ],
2107
+ inputs: [
2108
+ "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
2109
+ "ah6bf5u2sj63mcvou6qamiabeoubbe",
2110
+ ],
2111
+ });
2112
+
2113
+ await command!.handler("", ctx);
2114
+
2115
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
2116
+ assert.equal(config["notify-pushover-on-success"], true);
2117
+ assert.equal(config["notify-pushover-global-disable"], true);
2118
+ assert.equal(
2119
+ ctx.__state.statuses.get("pi-usereq"),
2120
+ buildExpectedFakeStatusText({
2121
+ basePath: buildExpectedFakeBasePath(cwd),
2122
+ docsDir: DEFAULT_DOCS_DIR,
2123
+ testsDir: "tests",
2124
+ srcDir: ["src"],
2125
+ contextFilledCells: 0,
2126
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
2127
+ pushover: "on",
2128
+ }),
2129
+ );
2130
+
2131
+ const recordedRequests: string[] = [];
2132
+ setPiNotifyHttpsRequestForTests(((url: URL, options: Record<string, unknown>, callback?: (response: any) => void) => {
2133
+ void options;
2134
+ recordedRequests.push(url.toString());
2135
+ const fakeResponse = {
2136
+ on(_eventName: string, _handler: (...args: any[]) => void) {
2137
+ return fakeResponse;
2138
+ },
2139
+ resume() {
2140
+ return undefined;
2141
+ },
2142
+ };
2143
+ callback?.(fakeResponse);
2144
+ const fakeRequest = {
2145
+ on(_eventName: string, _handler: (...args: any[]) => void) {
2146
+ return fakeRequest;
2147
+ },
2148
+ end(_body?: string) {
2149
+ return undefined;
2150
+ },
2151
+ };
2152
+ return fakeRequest as unknown as ReturnType<typeof import("node:https").request>;
2153
+ }) as typeof import("node:https").request);
2154
+
2155
+ try {
2156
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2157
+ await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
2158
+ await pi.emit("agent_start", {}, ctx);
2159
+ await pi.emit("agent_end", {
2160
+ messages: [
2161
+ {
2162
+ role: "assistant",
2163
+ stopReason: "stop",
2164
+ content: [],
2165
+ },
2166
+ ],
2167
+ }, ctx);
2168
+ assert.deepEqual(recordedRequests, []);
2169
+ } finally {
2170
+ setPiNotifyHttpsRequestForTests(undefined);
2171
+ }
2172
+ });
2173
+
1893
2174
  test("sound toggle shortcut cycles persisted pi-notify sound levels", async () => {
1894
2175
  const cwd = createTempDir("pi-usereq-shortcut-notify-");
1895
2176
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1914,13 +2195,12 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
1914
2195
  assert.equal(
1915
2196
  ctx.__state.statuses.get("pi-usereq"),
1916
2197
  buildExpectedFakeStatusText({
2198
+ basePath: buildExpectedFakeBasePath(cwd),
1917
2199
  docsDir: DEFAULT_DOCS_DIR,
1918
2200
  testsDir: "tests",
1919
2201
  srcDir: ["src"],
1920
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1921
2202
  contextFilledCells: 0,
1922
- elapsed: "idle",
1923
- last: "N/A",
2203
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1924
2204
  sound: expectedSound,
1925
2205
  }),
1926
2206
  );
@@ -1979,6 +2259,26 @@ test("configuration menu can add static-check entries from raw specs", async ()
1979
2259
  assert.deepEqual(config["static-check"].Python, [{ module: "Command", cmd: "true" }]);
1980
2260
  });
1981
2261
 
2262
+ test("configuration menu hides removed static-check modules from user-facing actions", async () => {
2263
+ const cwd = createTempDir("pi-usereq-menu-sc-hidden-");
2264
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
2265
+ const pi = createFakePi();
2266
+ piUsereqExtension(pi);
2267
+ const command = pi.commands.get("pi-usereq");
2268
+ assert.ok(command);
2269
+ const ctx = createFakeCtx(cwd, {
2270
+ selects: ["static-check"],
2271
+ });
2272
+
2273
+ await command!.handler("", ctx);
2274
+
2275
+ const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
2276
+ assert.match(renderedStaticCheckMenu, /Add entry for supported language/);
2277
+ assert.doesNotMatch(renderedStaticCheckMenu, /Pylance/);
2278
+ assert.doesNotMatch(renderedStaticCheckMenu, /Ruff/);
2279
+ assert.doesNotMatch(renderedStaticCheckMenu, /Dummy/);
2280
+ });
2281
+
1982
2282
  test("configuration menu can add guided static-check entries for explicit supported languages", async () => {
1983
2283
  const cwd = createTempDir("pi-usereq-menu-sc-guided-");
1984
2284
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1994,13 +2294,13 @@ test("configuration menu can add guided static-check entries for explicit suppor
1994
2294
  "static-check",
1995
2295
  "Add entry for supported language",
1996
2296
  "Python",
1997
- "Ruff",
1998
2297
  "Back",
1999
2298
  "Save and close",
2000
2299
  ],
2300
+ inputs: ["true", ""],
2001
2301
  }),
2002
2302
  );
2003
2303
 
2004
2304
  const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
2005
- assert.deepEqual(config["static-check"].Python, [{ module: "Ruff" }]);
2305
+ assert.deepEqual(config["static-check"].Python, [{ module: "Command", cmd: "true" }]);
2006
2306
  });