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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +365 -335
- package/pi-usereq/docs/REQUIREMENTS.md +25 -22
- package/pi-usereq/docs/WORKFLOW.md +57 -28
- package/src/core/debug-runtime.ts +2 -2
- package/src/core/extension-status.ts +35 -30
- package/src/core/prompt-command-runtime.ts +168 -2
- package/src/core/runtime-project-paths.ts +21 -1
- package/src/index.ts +10 -22
- package/tests/debug-extension-harness.test.ts +9 -3
- package/tests/extension-registration.test.ts +111 -25
|
@@ -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
|
|
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
|
|
442
|
+
return "▕_▏";
|
|
422
443
|
}
|
|
423
|
-
if (percent
|
|
444
|
+
if (percent >= 100) {
|
|
424
445
|
return formatFakeThemeForeground("error", "\u001b[5m▕█▏\u001b[25m");
|
|
425
446
|
}
|
|
426
|
-
if (percent
|
|
427
|
-
return formatFakeThemeForeground("
|
|
447
|
+
if (percent >= 90) {
|
|
448
|
+
return formatFakeThemeForeground("error", "▕█▏");
|
|
428
449
|
}
|
|
429
|
-
if (percent
|
|
430
|
-
return
|
|
450
|
+
if (percent < 25) {
|
|
451
|
+
return "▕▂▏";
|
|
431
452
|
}
|
|
432
|
-
if (percent
|
|
433
|
-
return
|
|
453
|
+
if (percent < 50) {
|
|
454
|
+
return "▕▄▏";
|
|
434
455
|
}
|
|
435
|
-
|
|
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,
|
|
441
|
-
* @param[in] options {{ workflowState?: string; basePath
|
|
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
|
|
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("
|
|
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
|
|
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
|
-
*
|
|
478
|
-
* in path length. No external state is
|
|
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
|
|
1562
|
-
|
|
1563
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
4196
|
+
contextPercent: 100,
|
|
4111
4197
|
et: "⏱︎ --:-- ⚑ --:-- ⌛︎--:--",
|
|
4112
4198
|
}),
|
|
4113
4199
|
);
|