pi-usereq 0.10.0 → 0.11.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.
@@ -406,9 +406,30 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
406
406
  return `<bg-from-fg-${color}>${text}</bg-from-fg-${color}>`;
407
407
  }
408
408
 
409
+ /**
410
+ * @brief Resolves the expected fake branch payload for status assertions.
411
+ * @details Reads `git branch --show-current` from the supplied base path and falls back to `unknown` when the path is outside a repository or HEAD has no branch name. Runtime is dominated by git execution. Side effects include subprocess creation.
412
+ * @param[in] basePath {string | undefined} Base path represented by the fake context.
413
+ * @return {string} Expected branch name or `unknown` when unavailable.
414
+ */
415
+ function buildExpectedFakeBranchValue(basePath: string | undefined): string {
416
+ if (!basePath || !fs.existsSync(basePath)) {
417
+ return "unknown";
418
+ }
419
+ const result = spawnSync("git", ["branch", "--show-current"], {
420
+ cwd: basePath,
421
+ encoding: "utf8",
422
+ });
423
+ if (result.error || result.status !== 0) {
424
+ return "unknown";
425
+ }
426
+ const branchName = result.stdout.trim();
427
+ return branchName === "" ? "unknown" : branchName;
428
+ }
429
+
409
430
  /**
410
431
  * @brief Builds the expected fake context-gauge payload for assertions.
411
- * @details Resolves the documented icon thresholds for `0-90%`, emits the blinking warning full icon for percent values above `90` through `100`, and emits the blinking red overflow icon for percent values above `100`. Runtime is O(1). No external state is mutated.
432
+ * @details Resolves the documented icon thresholds for `0`, `>0-<25`, `>=25-<50`, `>=50-<75`, and `>=75-<90`, emits the non-blinking error full icon for `>=90-<100`, and emits the blinking error full icon for `>=100`. Runtime is O(1). No external state is mutated.
412
433
  * @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-gauge facts.
413
434
  * @return {string} Encoded context-gauge string.
414
435
  */
@@ -418,32 +439,35 @@ function buildExpectedFakeContextBar(options: {
418
439
  }): string {
419
440
  const percent = options.percent;
420
441
  if (percent === undefined || percent === null || percent <= 0) {
421
- return formatFakeThemeForeground("warning", "▕_▏");
442
+ return "▕_▏";
422
443
  }
423
- if (percent > 100) {
444
+ if (percent >= 100) {
424
445
  return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
425
446
  }
426
- if (percent <= 25) {
427
- return formatFakeThemeForeground("warning", "▕▂▏");
447
+ if (percent >= 90) {
448
+ return formatFakeThemeForeground("error", "▕█▏");
428
449
  }
429
- if (percent <= 50) {
430
- return formatFakeThemeForeground("warning", "▕▄▏");
450
+ if (percent < 25) {
451
+ return "▕▂▏";
431
452
  }
432
- if (percent <= 90) {
433
- return formatFakeThemeForeground("warning", "▕▆▏");
453
+ if (percent < 50) {
454
+ return "▕▄▏";
434
455
  }
435
- return formatFakeThemeForeground("warning", "\u001b[5m▕█▏\u001b[25m");
456
+ if (percent < 75) {
457
+ return "▕▆▏";
458
+ }
459
+ return "▕█▏";
436
460
  }
437
461
 
438
462
  /**
439
463
  * @brief Builds the expected fake pi-usereq status-bar string for assertions.
440
- * @details Reconstructs the field order, workflow-state highlighting, separators, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1). No external state is mutated.
441
- * @param[in] options {{ workflowState?: string; basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts where `basePath` carries the rendered `current-path` value.
464
+ * @details Reconstructs the field order, workflow-state highlighting, branch field, icon-based context gauge, consolidated elapsed field, and sound field emitted by the extension using deterministic fake theme markers. Runtime is O(1) plus optional git execution for branch discovery. No external state is mutated.
465
+ * @param[in] options {{ workflowState?: string; basePath?: string; contextFilledCells: number; contextPercent?: number | null; et: string; sound?: string }} Expected status facts for rendered `status`, `branch`, `context`, `elapsed`, and `sound` fields.
442
466
  * @return {string} Encoded status-bar string.
443
467
  */
444
468
  function buildExpectedFakeStatusText(options: {
445
469
  workflowState?: string;
446
- basePath: string;
470
+ basePath?: string;
447
471
  docsDir?: string;
448
472
  testsDir?: string;
449
473
  srcDir?: string[];
@@ -458,13 +482,14 @@ function buildExpectedFakeStatusText(options: {
458
482
  const workflowStateValue = workflowStateText === "error"
459
483
  ? formatFakeThemeForeground("error", "\u001b[5merror\u001b[25m")
460
484
  : formatFakeThemeForeground("warning", workflowStateText);
485
+ const branchValue = buildExpectedFakeBranchValue(options.basePath);
461
486
  const contextBar = buildExpectedFakeContextBar({
462
487
  filledCells: options.contextFilledCells,
463
488
  percent: options.contextPercent,
464
489
  });
465
490
  return [
466
491
  `${formatFakeThemeForeground("accent", "status:")}${workflowStateValue}`,
467
- buildField("current-path", options.basePath),
492
+ buildField("branch", branchValue),
468
493
  `${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
469
494
  buildField("elapsed", options.et),
470
495
  buildField("sound", options.sound ?? "none"),
@@ -472,10 +497,11 @@ function buildExpectedFakeStatusText(options: {
472
497
  }
473
498
 
474
499
  /**
475
- * @brief Formats one expected absolute base path for fake status assertions.
500
+ * @brief Formats one expected absolute base path for prompt and status assertions.
476
501
  * @details Resolves the supplied cwd and normalizes path separators to `/` so
477
- * fake status comparisons stay stable across operating systems. Runtime is O(p)
478
- * in path length. No external state is mutated.
502
+ * prompt-text comparisons and legacy status-call-site inputs stay stable across
503
+ * operating systems. Runtime is O(p) in path length. No external state is
504
+ * mutated.
479
505
  * @param[in] cwd {string} Runtime working directory.
480
506
  * @return {string} Slash-normalized absolute base path.
481
507
  */
@@ -1557,10 +1583,20 @@ test("descendant configuration menus end with Reset defaults only", async () =>
1557
1583
 
1558
1584
  await command!.handler("", ctx);
1559
1585
 
1586
+ const getMenuIndex = (title: string, marker?: string): number => {
1587
+ const index = ctx.__state.selectCalls.findIndex(
1588
+ (entry) => entry.title === title && (marker === undefined || entry.items.includes(marker)),
1589
+ );
1590
+ assert.notEqual(index, -1, `missing menu ${title}`);
1591
+ return index;
1592
+ };
1560
1593
  const getTerminalRows = (title: string, marker?: string): string[] => {
1561
- const call = ctx.__state.selectCalls.find((entry) => entry.title === title && (marker === undefined || entry.items.includes(marker)));
1562
- assert.ok(call, `missing menu ${title}`);
1563
- return call.items.slice(-1);
1594
+ const index = getMenuIndex(title, marker);
1595
+ return ctx.__state.selectCalls[index]!.items.slice(-1);
1596
+ };
1597
+ const getRenderedMenu = (title: string, marker?: string): string => {
1598
+ const index = getMenuIndex(title, marker);
1599
+ return (ctx.__state.customRenderLines[index] ?? []).join("\n");
1564
1600
  };
1565
1601
 
1566
1602
  assert.deepEqual(getTerminalRows("Enable sound", "high"), ["Reset defaults"]);
@@ -1570,6 +1606,11 @@ test("descendant configuration menus end with Reset defaults only", async () =>
1570
1606
  assert.deepEqual(getTerminalRows("static-check language", "Python"), ["Reset defaults"]);
1571
1607
  assert.deepEqual(getTerminalRows("Remove static code checker"), ["Reset defaults"]);
1572
1608
  assert.deepEqual(getTerminalRows("Remove source-code directory", "src"), ["Reset defaults"]);
1609
+ assert.doesNotMatch(getRenderedMenu("Enable sound", "high"), /Reset defaults.*none/);
1610
+ assert.doesNotMatch(getRenderedMenu("Pushover priority", "High"), /Reset defaults.*Normal/);
1611
+ assert.doesNotMatch(getRenderedMenu("Enable tools", "static-check"), /Reset defaults.*defaults/);
1612
+ assert.doesNotMatch(getRenderedMenu("Debug", "Log on status"), /Reset defaults.*running/);
1613
+ assert.doesNotMatch(getRenderedMenu("Remove source-code directory", "src"), /Reset defaults.*src/);
1573
1614
  });
1574
1615
 
1575
1616
  test("debug menu dims locked rows and persists debug settings with focus-preserving re-renders", async () => {
@@ -2238,6 +2279,7 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2238
2279
  };
2239
2280
  await pi.emit("session_start", { reason: "startup" }, ctx);
2240
2281
 
2282
+ const originalBranchName = buildExpectedFakeBranchValue(projectBase);
2241
2283
  await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
2242
2284
  const promptText = String(pi.sentUserMessages[0]?.content ?? "");
2243
2285
  const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
@@ -2256,6 +2298,7 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2256
2298
  assert.equal(process.cwd(), executionBasePath);
2257
2299
  assert.equal(ctx.cwd, executionBasePath);
2258
2300
  assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>running<\/warning>/.test(status)));
2301
+ assert.ok(recordedStatuses.some((status) => status.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", worktreeName)}`)));
2259
2302
 
2260
2303
  await pi.emit("before_agent_start", {}, ctx);
2261
2304
  assert.equal(process.cwd(), executionBasePath);
@@ -2290,7 +2333,10 @@ test("worktree-enabled prompt commands merge successful runs, restore base-path,
2290
2333
  );
2291
2334
  assert.equal(ctx.__state.notifications.filter((entry) => entry.level === "error").length, 0);
2292
2335
  assert.ok(recordedStatuses.some((status) => /<accent>status:<\/accent><warning>merging<\/warning>/.test(status)));
2293
- assert.match(ctx.__state.statuses.get("pi-usereq") ?? "", /<accent>status:<\/accent><warning>idle<\/warning>/);
2336
+ assert.ok(recordedStatuses.some((status) => status.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", originalBranchName)}`)));
2337
+ const finalStatus = ctx.__state.statuses.get("pi-usereq") ?? "";
2338
+ assert.match(finalStatus, /<accent>status:<\/accent><warning>idle<\/warning>/);
2339
+ assert.ok(finalStatus.includes(`${formatFakeThemeForeground("accent", "branch:")}${formatFakeThemeForeground("warning", originalBranchName)}`));
2294
2340
  } finally {
2295
2341
  process.chdir(previousCwd);
2296
2342
  fs.rmSync(projectBase, { recursive: true, force: true });
@@ -4044,7 +4090,47 @@ test("context hook refreshes context usage and rounds progress cells upward", as
4044
4090
  );
4045
4091
  });
4046
4092
 
4047
- test("context hook renders the full context icon when usage exceeds ninety percent", async () => {
4093
+ /**
4094
+ * @brief Verifies context-gauge icon cutovers below the error range.
4095
+ * @details Replays deterministic context percentages at documented threshold boundaries below `90%` so the status bar proves the `▕▂▏`, `▕▄▏`, `▕▆▏`, and `▕█▏` transitions while keeping the default-color contract intact. Runtime is O(n) in case count. Side effects are limited to in-memory status updates.
4096
+ * @return {Promise<void>} Promise resolved after all threshold assertions complete.
4097
+ * @throws {AssertionError} Throws when any threshold renders the wrong icon.
4098
+ * @satisfies TST-096
4099
+ */
4100
+ test("context hook applies quarter-threshold gauge icons below the error range", async () => {
4101
+ const cwd = createTempDir("pi-usereq-context-threshold-status-");
4102
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4103
+ const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
4104
+ const pi = createFakePi();
4105
+ piUsereqExtension(pi);
4106
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
4107
+ getContextUsage: () => ({ ...contextUsage }),
4108
+ });
4109
+
4110
+ await pi.emit("session_start", { reason: "startup" }, ctx);
4111
+
4112
+ for (const percent of [24.9, 25, 49.9, 50, 74.9, 75, 89.9]) {
4113
+ contextUsage.tokens = Math.round(percent * 10);
4114
+ contextUsage.percent = percent;
4115
+ await pi.emit("context", { messages: [] }, ctx);
4116
+
4117
+ assert.equal(
4118
+ ctx.__state.statuses.get("pi-usereq"),
4119
+ buildExpectedFakeStatusText({
4120
+ basePath: buildExpectedFakeBasePath(cwd),
4121
+ docsDir: DEFAULT_DOCS_DIR,
4122
+ testsDir: "tests",
4123
+ srcDir: ["src"],
4124
+ contextFilledCells: Math.ceil(percent / 10),
4125
+ contextPercent: percent,
4126
+ et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4127
+ }),
4128
+ `unexpected context icon at ${percent}%`,
4129
+ );
4130
+ }
4131
+ });
4132
+
4133
+ test("context hook renders the error full context icon when usage exceeds ninety percent", async () => {
4048
4134
  const cwd = createTempDir("pi-usereq-context-full-status-");
4049
4135
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4050
4136
  const contextUsage = { tokens: 910, contextWindow: 1000, percent: 91 };
@@ -4071,14 +4157,14 @@ test("context hook renders the full context icon when usage exceeds ninety perce
4071
4157
  );
4072
4158
  });
4073
4159
 
4074
- test("status overflow context icon uses only CLI-supported theme tokens", async () => {
4160
+ test("status overflow-threshold context icon uses only CLI-supported theme tokens", async () => {
4075
4161
  const cwd = createTempDir("pi-usereq-context-theme-contract-");
4076
4162
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
4077
4163
  const usedColors = new Set<string>();
4078
4164
  const pi = createFakePi();
4079
4165
  piUsereqExtension(pi);
4080
4166
  const ctx = createFakeCtx(cwd, { selects: [] }, {
4081
- getContextUsage: () => ({ tokens: 1250, contextWindow: 1000, percent: 125 }),
4167
+ getContextUsage: () => ({ tokens: 1000, contextWindow: 1000, percent: 100 }),
4082
4168
  theme: {
4083
4169
  fg(color: string, text: string) {
4084
4170
  assert.ok(["accent", "warning", "dim", "error"].includes(color));
@@ -4107,7 +4193,7 @@ test("status overflow context icon uses only CLI-supported theme tokens", async
4107
4193
  testsDir: "tests",
4108
4194
  srcDir: ["src"],
4109
4195
  contextFilledCells: 10,
4110
- contextPercent: 125,
4196
+ contextPercent: 100,
4111
4197
  et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
4112
4198
  }),
4113
4199
  );