pi-usereq 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +9 -3
- package/package.json +7 -4
- package/pi-usereq/docs/REFERENCES.md +303 -286
- package/pi-usereq/docs/REQUIREMENTS.md +18 -5
- package/pi-usereq/docs/WORKFLOW.md +47 -13
- package/scripts/install-static-checkers.ts +125 -0
- package/src/core/config.ts +5 -5
- package/src/core/prompt-command-runtime.ts +6 -6
- package/src/core/prompts.ts +6 -6
- package/src/core/req-references-command.ts +3 -3
- package/src/core/req-reset-command.ts +3 -3
- package/src/core/runtime-project-paths.ts +3 -3
- package/src/core/static-check.ts +61 -4
- package/src/index.ts +32 -9
- package/tests/extension-registration.test.ts +291 -1
- package/tests/prompt-command-summary.test.ts +36 -0
- package/tests/static-check-bundled.test.ts +100 -0
package/src/core/static-check.ts
CHANGED
|
@@ -9,8 +9,9 @@ import path from "node:path";
|
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
11
|
import fg from "fast-glob";
|
|
12
|
-
import type { StaticCheckEntry } from "./config.js";
|
|
12
|
+
import type { StaticCheckEntry, UseReqConfig } from "./config.js";
|
|
13
13
|
import { ReqError } from "./errors.js";
|
|
14
|
+
import { getInstallationPath } from "./path-context.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @brief Maps user-facing language aliases to canonical static-check language names.
|
|
@@ -264,7 +265,7 @@ export function validateStaticCheckEntry(entry: StaticCheckEntry): void {
|
|
|
264
265
|
if (!cmd) {
|
|
265
266
|
throw new ReqError("Error: Command module requires a cmd argument in --enable-static-check. Format: LANG=Command,CMD[,PARAM...]", 1);
|
|
266
267
|
}
|
|
267
|
-
if (!
|
|
268
|
+
if (!resolveCheckerExecutable(cmd)) {
|
|
268
269
|
throw new ReqError(`Error: --enable-static-check Command cmd '${cmd}' is not an executable program on this system.`, 1);
|
|
269
270
|
}
|
|
270
271
|
}
|
|
@@ -398,7 +399,7 @@ export class StaticCheckCommand extends StaticCheckBase {
|
|
|
398
399
|
* @throws {ReqError} Throws when the executable cannot be found on PATH.
|
|
399
400
|
*/
|
|
400
401
|
constructor(cmd: string, inputs: string[], extraArgs?: string[], failOnly = false) {
|
|
401
|
-
if (!
|
|
402
|
+
if (!resolveCheckerExecutable(cmd)) {
|
|
402
403
|
throw new ReqError(`Error: external command '${cmd}' not found on PATH.`, 1);
|
|
403
404
|
}
|
|
404
405
|
super(inputs, extraArgs, failOnly);
|
|
@@ -460,7 +461,7 @@ function isExecutableFile(candidate: string): boolean {
|
|
|
460
461
|
* @param[in] cmd {string} Executable name to locate.
|
|
461
462
|
* @return {string | undefined} Absolute executable path, or `undefined` when not found.
|
|
462
463
|
*/
|
|
463
|
-
function findExecutable(cmd: string): string | undefined {
|
|
464
|
+
export function findExecutable(cmd: string): string | undefined {
|
|
464
465
|
if (cmd.includes(path.sep)) {
|
|
465
466
|
return isExecutableFile(cmd) ? path.resolve(cmd) : undefined;
|
|
466
467
|
}
|
|
@@ -473,6 +474,59 @@ function findExecutable(cmd: string): string | undefined {
|
|
|
473
474
|
return undefined;
|
|
474
475
|
}
|
|
475
476
|
|
|
477
|
+
/**
|
|
478
|
+
* @brief Resolves one checker executable across bundled `node_modules/.bin` locations and PATH.
|
|
479
|
+
* @details Probes the installation-owned `node_modules/.bin` directory, the project-scope parent `node_modules/.bin` directory used by `--prefix` layouts, and finally the system PATH scan, returning the first executable match. Runtime is O(p) in PATH entry count plus bounded filesystem metadata checks. Side effects are limited to filesystem reads.
|
|
480
|
+
* @param[in] cmd {string} Executable name or relative path to resolve.
|
|
481
|
+
* @return {string | undefined} Absolute executable path, or `undefined` when not found in any probed location.
|
|
482
|
+
* @satisfies REQ-023, REQ-037, DES-018
|
|
483
|
+
*/
|
|
484
|
+
export function resolveCheckerExecutable(cmd: string): string | undefined {
|
|
485
|
+
if (cmd.includes(path.sep)) {
|
|
486
|
+
return isExecutableFile(cmd) ? path.resolve(cmd) : undefined;
|
|
487
|
+
}
|
|
488
|
+
const installationPath = getInstallationPath();
|
|
489
|
+
const bundledBinCandidates = [
|
|
490
|
+
path.join(installationPath, "..", "node_modules", ".bin", cmd),
|
|
491
|
+
path.join(installationPath, "..", "..", "node_modules", ".bin", cmd),
|
|
492
|
+
];
|
|
493
|
+
for (const candidate of bundledBinCandidates) {
|
|
494
|
+
if (isExecutableFile(candidate)) {
|
|
495
|
+
return candidate;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return findExecutable(cmd);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @brief Determines which enabled static-check executables are missing from the runtime environment.
|
|
503
|
+
* @details Iterates every enabled language checker entry in the effective config, resolves each `Command` entry executable through `resolveCheckerExecutable`, and collects the missing command names so the caller can surface one consolidated warning. Runtime is O(l * c * p) where l is language count, c is checker count, and p is PATH entry count. Side effects are limited to filesystem reads.
|
|
504
|
+
* @param[in] config {UseReqConfig} Effective merged configuration.
|
|
505
|
+
* @return {string[]} Unique command names that are enabled but unresolvable.
|
|
506
|
+
* @satisfies REQ-341, REQ-343, REQ-344
|
|
507
|
+
*/
|
|
508
|
+
export function checkDefaultCheckersAvailability(config: UseReqConfig): string[] {
|
|
509
|
+
const missing = new Set<string>();
|
|
510
|
+
for (const languageConfig of Object.values(config["static-check"] ?? {})) {
|
|
511
|
+
if (languageConfig.enabled !== "enable") {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
for (const entry of languageConfig.checkers) {
|
|
515
|
+
if (String(entry.module ?? "").trim().toLowerCase() !== "command") {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const cmd = typeof entry.cmd === "string" ? entry.cmd.trim() : "";
|
|
519
|
+
if (!cmd) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (!resolveCheckerExecutable(cmd)) {
|
|
523
|
+
missing.add(cmd);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return [...missing];
|
|
528
|
+
}
|
|
529
|
+
|
|
476
530
|
/**
|
|
477
531
|
* @brief Dispatches one configured static checker for a single file.
|
|
478
532
|
* @details Selects the debug `Dummy` or user-facing `Command` implementation by module name, normalizes parameter arrays, and runs exactly one checker instance against the target file. Runtime is dominated by the selected checker. Side effects include console output and possible process spawning.
|
|
@@ -527,6 +581,9 @@ export function runStaticCheck(argv: string[]): number {
|
|
|
527
581
|
throw new ReqError("Error: --test-static-check command requires a <cmd> argument.", 1);
|
|
528
582
|
}
|
|
529
583
|
const [cmd, ...files] = rest;
|
|
584
|
+
if (!resolveCheckerExecutable(cmd!)) {
|
|
585
|
+
throw new ReqError(`Error: external command '${cmd}' not found on PATH.`, 1);
|
|
586
|
+
}
|
|
530
587
|
return new StaticCheckCommand(cmd!, files).run();
|
|
531
588
|
}
|
|
532
589
|
default:
|
package/src/index.ts
CHANGED
|
@@ -172,6 +172,7 @@ import {
|
|
|
172
172
|
} from "./core/tool-runner.js";
|
|
173
173
|
import { LANGUAGE_TAGS } from "./core/find-constructs.js";
|
|
174
174
|
import {
|
|
175
|
+
checkDefaultCheckersAvailability,
|
|
175
176
|
getSupportedStaticCheckLanguageSupport,
|
|
176
177
|
} from "./core/static-check.js";
|
|
177
178
|
import { makeRelativeIfContainsProject, shellSplit } from "./core/utils.js";
|
|
@@ -910,7 +911,7 @@ function executeStatusTool(operation: () => ToolResult): ReturnType<typeof build
|
|
|
910
911
|
|
|
911
912
|
/**
|
|
912
913
|
* @brief Starts delivery of one rendered prompt into the current active session.
|
|
913
|
-
* @details Prefers the replacement-session `sendMessage(...)` helper exposed by `withSession(...)` callbacks after session replacement so post-switch prompt delivery never reuses stale pre-switch session-bound extension objects. Delivers the rendered prompt as a `display:false` custom message with `triggerTurn:true` so the full content reaches the LLM agent without appearing on screen, and emits a `display:true` command invocation summary so the TUI shows only the compact summary. Returns the underlying hidden-delivery promise without awaiting
|
|
914
|
+
* @details Prefers the replacement-session `sendMessage(...)` helper exposed by `withSession(...)` callbacks after session replacement so post-switch prompt delivery never reuses stale pre-switch session-bound extension objects. Delivers the rendered prompt as a `display:false` custom message with `triggerTurn:true` so the full content reaches the LLM agent without appearing on screen, and emits a `display:true` command invocation summary so the TUI shows only the compact summary. Awaits the non-critical summary delivery and suppresses its failures before dispatching the authoritative hidden prompt turn so a rejecting summary never surfaces as an unhandled rejection that could terminate the process before prompt-end closure runs the worktree merge. Returns the underlying hidden-delivery promise without awaiting the full agent turn so callers can record the `running` workflow transition as soon as prompt handoff is accepted; the returned promise resolves after the hidden prompt message is accepted, which on runtimes whose async replacement-session helpers resolve only after `agent_end` lets prompt-end closure finalize the worktree merge during the awaited delivery. When pi later invalidates that replacement-session context during successful prompt-end restoration, the helper suppresses the documented stale-extension-context rejection because the prompt was already accepted and late rethrow would surface a false orchestration failure. Falls back to `sendUserMessage(...)` only for non-replacement flows or runtimes that do not expose `sendMessage`. Runtime is O(n) in prompt length. Side effects are limited to hidden prompt delivery plus on-screen summary display.
|
|
914
915
|
* @param[in] pi {ExtensionAPI} Handler-scoped extension API instance retained as the fallback dispatcher.
|
|
915
916
|
* @param[in] content {string} Rendered prompt markdown.
|
|
916
917
|
* @param[in] summary {string} Command invocation summary text displayed on screen.
|
|
@@ -942,9 +943,16 @@ function deliverPromptCommand(
|
|
|
942
943
|
display: false,
|
|
943
944
|
};
|
|
944
945
|
if (typeof replacementContext?.sendMessage === "function") {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
946
|
+
// The on-screen summary is non-critical; await it and suppress its delivery failures so a
|
|
947
|
+
// rejecting summary never surfaces as an unhandled rejection that could terminate the
|
|
948
|
+
// process before the authoritative hidden prompt turn drives prompt-end closure.
|
|
949
|
+
const summaryDelivery = Promise.resolve(
|
|
950
|
+
replacementContext.sendMessage(summaryMessage),
|
|
951
|
+
).catch(() => {
|
|
952
|
+
// Intentionally suppressed: the hidden prompt message below is the authoritative turn trigger.
|
|
953
|
+
});
|
|
954
|
+
return summaryDelivery.then(() =>
|
|
955
|
+
replacementContext!.sendMessage!(hiddenPromptMessage, { triggerTurn: true }),
|
|
948
956
|
).catch((error) => {
|
|
949
957
|
if (isStaleExtensionContextError(error)) {
|
|
950
958
|
return;
|
|
@@ -953,9 +961,17 @@ function deliverPromptCommand(
|
|
|
953
961
|
});
|
|
954
962
|
}
|
|
955
963
|
if (typeof pi.sendMessage === "function") {
|
|
956
|
-
pi.sendMessage
|
|
957
|
-
|
|
958
|
-
|
|
964
|
+
// `pi.sendMessage` is a fire-and-forget dispatcher in the pi runtime; chain both deliveries
|
|
965
|
+
// through one suppressed summary promise so a rejecting summary cannot terminate the process
|
|
966
|
+
// before the hidden prompt turn drives prompt-end closure.
|
|
967
|
+
const fallbackSummary = Promise.resolve(
|
|
968
|
+
pi.sendMessage(summaryMessage),
|
|
969
|
+
).catch(() => {
|
|
970
|
+
// Intentionally suppressed: the hidden prompt message below is the authoritative turn trigger.
|
|
971
|
+
});
|
|
972
|
+
return fallbackSummary.then(() => {
|
|
973
|
+
pi.sendMessage(hiddenPromptMessage, { triggerTurn: true });
|
|
974
|
+
});
|
|
959
975
|
}
|
|
960
976
|
if (typeof replacementContext?.sendUserMessage === "function") {
|
|
961
977
|
return Promise.resolve(replacementContext.sendUserMessage(content)).catch((error) => {
|
|
@@ -1072,7 +1088,7 @@ function logPromptWorkflowEvent(
|
|
|
1072
1088
|
* @param[in] projectBase {string} Absolute original project base path.
|
|
1073
1089
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
1074
1090
|
* @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
|
|
1075
|
-
* @param[in] nextState {
|
|
1091
|
+
* @param[in] nextState {import("./core/extension-status.js").PiUsereqWorkflowState} Next workflow state.
|
|
1076
1092
|
* @return {void} No return value.
|
|
1077
1093
|
*/
|
|
1078
1094
|
function transitionPromptWorkflowState(
|
|
@@ -1081,7 +1097,7 @@ function transitionPromptWorkflowState(
|
|
|
1081
1097
|
projectBase: string,
|
|
1082
1098
|
config: UseReqConfig,
|
|
1083
1099
|
promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
|
|
1084
|
-
nextState:
|
|
1100
|
+
nextState: import("./core/extension-status.js").PiUsereqWorkflowState,
|
|
1085
1101
|
): void {
|
|
1086
1102
|
const previousState = statusController.state.workflowState;
|
|
1087
1103
|
setPiUsereqWorkflowState(statusController, nextState, ctx);
|
|
@@ -1267,6 +1283,13 @@ async function handleExtensionStatusEvent(
|
|
|
1267
1283
|
const config = loadProjectConfig(startupCwd);
|
|
1268
1284
|
applyConfiguredPiUsereqTools(pi, config);
|
|
1269
1285
|
setPiUsereqStatusConfig(statusController, config);
|
|
1286
|
+
const missingCheckers = checkDefaultCheckersAvailability(config);
|
|
1287
|
+
if (missingCheckers.length > 0) {
|
|
1288
|
+
ctx.ui.notify(
|
|
1289
|
+
`Missing static checkers: ${missingCheckers.join(", ")}\nInstall: run 'npm install' or install each missing checker via your system package manager.`,
|
|
1290
|
+
"warning",
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1270
1293
|
}
|
|
1271
1294
|
if (hookName === "before_agent_start") {
|
|
1272
1295
|
const requestForActivation = activePromptRequest ?? pendingPromptRequest;
|
|
@@ -3780,6 +3780,296 @@ test("worktree-backed prompt commands use replacement-session callbacks for prom
|
|
|
3780
3780
|
}
|
|
3781
3781
|
});
|
|
3782
3782
|
|
|
3783
|
+
/**
|
|
3784
|
+
* @brief Verifies worktree-backed prompt closure still merges and deletes the worktree when prompt delivery uses the production `sendMessage` custom-message path.
|
|
3785
|
+
* @details Simulates the pi runtime `sendMessage(message, { triggerTurn: true })` contract documented for `ReplacedSessionContext`: a `triggerTurn` custom message runs the agent loop which emits `agent_start` then `agent_end` (without a separate `before_agent_start`), and the `sendMessage` promise resolves only after the agent turn completes. The test proves that when the replacement session exposes `sendMessage`, prompt delivery must still drive the prompt-end closure so the worktree branch is merged into `base-path`, the worktree plus branch are deleted, and the workflow state returns to `idle`. Runtime is dominated by temporary git worktree setup and teardown. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
3786
|
+
* @return {Promise<void>} Promise resolved after `sendMessage`-driven prompt delivery, merge handling, and restored-base assertions complete.
|
|
3787
|
+
* @throws {AssertionError} Throws when `sendMessage`-driven prompt delivery leaves the worktree unmerged, the worktree or branch retained, or the workflow state not `idle`.
|
|
3788
|
+
* @satisfies REQ-068, REQ-208, REQ-228, REQ-230, REQ-258, REQ-282, DES-016
|
|
3789
|
+
*/
|
|
3790
|
+
test("worktree-backed prompt commands merge and delete the worktree when prompt delivery uses sendMessage triggerTurn", async () => {
|
|
3791
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3792
|
+
const previousCwd = process.cwd();
|
|
3793
|
+
try {
|
|
3794
|
+
const pi = createFakePi();
|
|
3795
|
+
piUsereqExtension(pi);
|
|
3796
|
+
const ctx = createFakeCtx(projectBase);
|
|
3797
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3798
|
+
|
|
3799
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
3800
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3801
|
+
const sentMessages: Array<{ message: unknown; options?: unknown }> = [];
|
|
3802
|
+
let activeSessionCtx: any = ctx;
|
|
3803
|
+
ctx.switchSession = async (
|
|
3804
|
+
sessionPath: string,
|
|
3805
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
3806
|
+
) => {
|
|
3807
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3808
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
3809
|
+
const replacementCtx = {
|
|
3810
|
+
...ctx,
|
|
3811
|
+
cwd: replacementCwd,
|
|
3812
|
+
sessionManager: {
|
|
3813
|
+
...ctx.sessionManager,
|
|
3814
|
+
getCwd: () => replacementCwd,
|
|
3815
|
+
getSessionFile: () => sessionPath,
|
|
3816
|
+
},
|
|
3817
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
3818
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
3819
|
+
},
|
|
3820
|
+
async sendMessage(message: unknown, sendOptions?: unknown) {
|
|
3821
|
+
sentMessages.push({ message, options: sendOptions });
|
|
3822
|
+
if ((sendOptions as { triggerTurn?: boolean } | undefined)?.triggerTurn === true) {
|
|
3823
|
+
await Promise.resolve();
|
|
3824
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
3825
|
+
await pi.emit("agent_end", {
|
|
3826
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3827
|
+
}, activeSessionCtx);
|
|
3828
|
+
}
|
|
3829
|
+
},
|
|
3830
|
+
};
|
|
3831
|
+
activeSessionCtx = replacementCtx;
|
|
3832
|
+
if (options?.withSession) {
|
|
3833
|
+
process.chdir(replacementCwd);
|
|
3834
|
+
await options.withSession(replacementCtx);
|
|
3835
|
+
ctx.cwd = projectBase;
|
|
3836
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3837
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
3838
|
+
return result;
|
|
3839
|
+
}
|
|
3840
|
+
ctx.cwd = projectBase;
|
|
3841
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3842
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
3843
|
+
process.chdir(projectBase);
|
|
3844
|
+
return result;
|
|
3845
|
+
};
|
|
3846
|
+
|
|
3847
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3848
|
+
|
|
3849
|
+
assert.ok(
|
|
3850
|
+
sentMessages.some((entry) => (entry.options as { triggerTurn?: boolean } | undefined)?.triggerTurn === true),
|
|
3851
|
+
"prompt delivery must use sendMessage with triggerTurn",
|
|
3852
|
+
);
|
|
3853
|
+
const promptMessage = sentMessages.find((entry) =>
|
|
3854
|
+
typeof (entry.message as { content?: unknown } | undefined)?.content === "string"
|
|
3855
|
+
&& String((entry.message as { content?: unknown }).content).includes("created worktree-dir"));
|
|
3856
|
+
const promptText = String((promptMessage?.message as { content?: string } | undefined)?.content ?? "");
|
|
3857
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3858
|
+
assert.ok(worktreeMatch, promptText);
|
|
3859
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
3860
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3861
|
+
|
|
3862
|
+
assert.equal(process.cwd(), projectBase);
|
|
3863
|
+
assert.equal(fs.existsSync(executionBasePath), false, "worktree directory must be deleted after successful closure");
|
|
3864
|
+
assert.notEqual(
|
|
3865
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
3866
|
+
cwd: projectBase,
|
|
3867
|
+
encoding: "utf8",
|
|
3868
|
+
}).status,
|
|
3869
|
+
0,
|
|
3870
|
+
"worktree branch must be deleted after successful closure",
|
|
3871
|
+
);
|
|
3872
|
+
const logResult = spawnSync("git", ["log", "--oneline", "-1"], { cwd: projectBase, encoding: "utf8" });
|
|
3873
|
+
assert.match(logResult.stdout, /init/, "base-path must remain on the original head after a no-commit ff-only merge");
|
|
3874
|
+
} finally {
|
|
3875
|
+
process.chdir(previousCwd);
|
|
3876
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
|
|
3880
|
+
/**
|
|
3881
|
+
* @brief Verifies a rejecting on-screen summary delivery never surfaces as an unhandled rejection and never interrupts the worktree merge.
|
|
3882
|
+
* @details Simulates the pi runtime `sendMessage` contract where the non-critical `display:true` summary delivery rejects (for example because the session is torn down mid-emit during prompt-end closure) while the authoritative `display:false` hidden prompt message with `triggerTurn:true` still drives the agent turn. The test proves `deliverPromptCommand` must await and suppress summary delivery failures so the rejection cannot terminate the process (Node default `--unhandled-rejections=throw`) before prompt-end closure runs the worktree merge, branch deletion, and worktree deletion. Runtime is dominated by temporary git worktree setup and teardown. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
3883
|
+
* @return {Promise<void>} Promise resolved after summary-rejection-tolerant prompt delivery, merge handling, and restored-base assertions complete.
|
|
3884
|
+
* @throws {AssertionError} Throws when a rejecting summary surfaces as an unhandled rejection or leaves the worktree unmerged.
|
|
3885
|
+
* @satisfies REQ-068, REQ-208, REQ-228, REQ-230, REQ-258, REQ-282, REQ-334, DES-016
|
|
3886
|
+
*/
|
|
3887
|
+
test("worktree-backed prompt commands tolerate a rejecting summary sendMessage without interrupting the merge", async () => {
|
|
3888
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3889
|
+
const previousCwd = process.cwd();
|
|
3890
|
+
let unhandledRejections: unknown[] = [];
|
|
3891
|
+
const rejectionHandler = (reason: unknown): void => { unhandledRejections.push(reason); };
|
|
3892
|
+
process.on("unhandledRejection", rejectionHandler);
|
|
3893
|
+
try {
|
|
3894
|
+
const pi = createFakePi();
|
|
3895
|
+
piUsereqExtension(pi);
|
|
3896
|
+
const ctx = createFakeCtx(projectBase);
|
|
3897
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
3898
|
+
|
|
3899
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
3900
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
3901
|
+
const sentMessages: Array<{ message: unknown; options?: unknown }> = [];
|
|
3902
|
+
let activeSessionCtx: any = ctx;
|
|
3903
|
+
ctx.switchSession = async (
|
|
3904
|
+
sessionPath: string,
|
|
3905
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
3906
|
+
) => {
|
|
3907
|
+
const result = await originalSwitchSession(sessionPath);
|
|
3908
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
3909
|
+
const replacementCtx = {
|
|
3910
|
+
...ctx,
|
|
3911
|
+
cwd: replacementCwd,
|
|
3912
|
+
sessionManager: {
|
|
3913
|
+
...ctx.sessionManager,
|
|
3914
|
+
getCwd: () => replacementCwd,
|
|
3915
|
+
getSessionFile: () => sessionPath,
|
|
3916
|
+
},
|
|
3917
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
3918
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
3919
|
+
},
|
|
3920
|
+
async sendMessage(message: unknown, sendOptions?: unknown) {
|
|
3921
|
+
const isTrigger = (sendOptions as { triggerTurn?: boolean } | undefined)?.triggerTurn === true;
|
|
3922
|
+
sentMessages.push({ message, options: sendOptions });
|
|
3923
|
+
if (!isTrigger) {
|
|
3924
|
+
throw new Error("summary sendMessage rejected");
|
|
3925
|
+
}
|
|
3926
|
+
await Promise.resolve();
|
|
3927
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
3928
|
+
await pi.emit("agent_end", {
|
|
3929
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3930
|
+
}, activeSessionCtx);
|
|
3931
|
+
},
|
|
3932
|
+
};
|
|
3933
|
+
activeSessionCtx = replacementCtx;
|
|
3934
|
+
if (options?.withSession) {
|
|
3935
|
+
process.chdir(replacementCwd);
|
|
3936
|
+
await options.withSession(replacementCtx);
|
|
3937
|
+
ctx.cwd = projectBase;
|
|
3938
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3939
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
3940
|
+
return result;
|
|
3941
|
+
}
|
|
3942
|
+
ctx.cwd = projectBase;
|
|
3943
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
3944
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
3945
|
+
process.chdir(projectBase);
|
|
3946
|
+
return result;
|
|
3947
|
+
};
|
|
3948
|
+
|
|
3949
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
3950
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
3951
|
+
|
|
3952
|
+
assert.ok(
|
|
3953
|
+
sentMessages.some((entry) => (entry.options as { triggerTurn?: boolean } | undefined)?.triggerTurn === true),
|
|
3954
|
+
"prompt delivery must still dispatch the hidden prompt message with triggerTurn",
|
|
3955
|
+
);
|
|
3956
|
+
assert.equal(process.cwd(), projectBase, "base-path must be restored after successful closure");
|
|
3957
|
+
assert.equal(unhandledRejections.length, 0, "rejecting summary delivery must not surface as an unhandled rejection");
|
|
3958
|
+
const promptMessage = sentMessages.find((entry) =>
|
|
3959
|
+
typeof (entry.message as { content?: unknown } | undefined)?.content === "string"
|
|
3960
|
+
&& String((entry.message as { content?: unknown }).content).includes("created worktree-dir"));
|
|
3961
|
+
const promptText = String((promptMessage?.message as { content?: string } | undefined)?.content ?? "");
|
|
3962
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
3963
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
3964
|
+
assert.ok(worktreeMatch, promptText);
|
|
3965
|
+
assert.equal(fs.existsSync(executionBasePath), false, "worktree directory must be deleted after successful closure");
|
|
3966
|
+
} finally {
|
|
3967
|
+
process.off("unhandledRejection", rejectionHandler);
|
|
3968
|
+
process.chdir(previousCwd);
|
|
3969
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
/**
|
|
3974
|
+
* @brief Verifies worktree-backed prompt closure still merges and deletes the worktree when prompt delivery falls back to `pi.sendMessage` because the replacement session omits `sendMessage`.
|
|
3975
|
+
* @details Simulates the pi runtime where `ctx.switchSession(...)` exposes a replacement context without `sendMessage` while the top-level `pi.sendMessage` is available as a fire-and-forget custom-message dispatcher. The test proves that even in this fallback path, prompt delivery must drive the prompt-end closure so the worktree branch is merged into `base-path`, the worktree plus branch are deleted, and the workflow state returns to `idle`. Runtime is dominated by temporary git worktree setup and teardown. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
3976
|
+
* @return {Promise<void>} Promise resolved after `sendMessage`-fallback prompt delivery, merge handling, and restored-base assertions complete.
|
|
3977
|
+
* @throws {AssertionError} Throws when `pi.sendMessage`-fallback prompt delivery leaves the worktree unmerged, the worktree or branch retained, or the workflow state not `idle`.
|
|
3978
|
+
* @satisfies REQ-068, REQ-208, REQ-228, REQ-230, REQ-258, REQ-282, DES-016
|
|
3979
|
+
*/
|
|
3980
|
+
test("worktree-backed prompt commands merge and delete the worktree when prompt delivery falls back to pi.sendMessage", async () => {
|
|
3981
|
+
const { projectBase } = initFixtureRepo({ fixtures: [] });
|
|
3982
|
+
const previousCwd = process.cwd();
|
|
3983
|
+
try {
|
|
3984
|
+
const pi = createFakePi();
|
|
3985
|
+
const sentMessages: Array<{ message: unknown; options?: unknown }> = [];
|
|
3986
|
+
(pi as any).sendMessage = (message: unknown, options?: unknown) => {
|
|
3987
|
+
sentMessages.push({ message, options });
|
|
3988
|
+
// Simulate the real SDK fire-and-forget dispatcher: trigger the agent turn asynchronously.
|
|
3989
|
+
if ((options as { triggerTurn?: boolean } | undefined)?.triggerTurn === true) {
|
|
3990
|
+
Promise.resolve().then(async () => {
|
|
3991
|
+
await pi.emit("agent_start", {}, activeSessionCtx);
|
|
3992
|
+
await pi.emit("agent_end", {
|
|
3993
|
+
messages: [{ role: "assistant", stopReason: "stop", content: [] }],
|
|
3994
|
+
}, activeSessionCtx);
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
};
|
|
3998
|
+
piUsereqExtension(pi);
|
|
3999
|
+
const ctx = createFakeCtx(projectBase);
|
|
4000
|
+
await pi.emit("session_start", { reason: "startup" }, ctx);
|
|
4001
|
+
|
|
4002
|
+
const originalSessionFile = ctx.sessionManager.getSessionFile();
|
|
4003
|
+
const originalSwitchSession = ctx.switchSession.bind(ctx);
|
|
4004
|
+
let activeSessionCtx: any = ctx;
|
|
4005
|
+
ctx.switchSession = async (
|
|
4006
|
+
sessionPath: string,
|
|
4007
|
+
options?: { withSession?: (replacementCtx: any) => Promise<void> },
|
|
4008
|
+
) => {
|
|
4009
|
+
const result = await originalSwitchSession(sessionPath);
|
|
4010
|
+
const replacementCwd = readFakeSessionFileCwd(sessionPath, projectBase);
|
|
4011
|
+
const replacementCtx = {
|
|
4012
|
+
...ctx,
|
|
4013
|
+
cwd: replacementCwd,
|
|
4014
|
+
sessionManager: {
|
|
4015
|
+
...ctx.sessionManager,
|
|
4016
|
+
getCwd: () => replacementCwd,
|
|
4017
|
+
getSessionFile: () => sessionPath,
|
|
4018
|
+
},
|
|
4019
|
+
async sendUserMessage(content: unknown, sendOptions?: unknown) {
|
|
4020
|
+
pi.sentUserMessages.push({ content, options: sendOptions });
|
|
4021
|
+
},
|
|
4022
|
+
};
|
|
4023
|
+
activeSessionCtx = replacementCtx;
|
|
4024
|
+
if (options?.withSession) {
|
|
4025
|
+
process.chdir(replacementCwd);
|
|
4026
|
+
await options.withSession(replacementCtx);
|
|
4027
|
+
ctx.cwd = projectBase;
|
|
4028
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
4029
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
4030
|
+
return result;
|
|
4031
|
+
}
|
|
4032
|
+
ctx.cwd = projectBase;
|
|
4033
|
+
ctx.sessionManager.getSessionFile = () => originalSessionFile;
|
|
4034
|
+
ctx.sessionManager.getCwd = () => projectBase;
|
|
4035
|
+
process.chdir(projectBase);
|
|
4036
|
+
return result;
|
|
4037
|
+
};
|
|
4038
|
+
|
|
4039
|
+
await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
|
|
4040
|
+
// The real SDK `pi.sendMessage` dispatcher is fire-and-forget; let the simulated agent turn drain.
|
|
4041
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
4042
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
4043
|
+
|
|
4044
|
+
assert.ok(
|
|
4045
|
+
sentMessages.some((entry) => (entry.options as { triggerTurn?: boolean } | undefined)?.triggerTurn === true),
|
|
4046
|
+
"prompt delivery must use pi.sendMessage with triggerTurn",
|
|
4047
|
+
);
|
|
4048
|
+
const promptMessage = sentMessages.find((entry) =>
|
|
4049
|
+
typeof (entry.message as { content?: unknown } | undefined)?.content === "string"
|
|
4050
|
+
&& String((entry.message as { content?: unknown }).content).includes("created worktree-dir"));
|
|
4051
|
+
const promptText = String((promptMessage?.message as { content?: string } | undefined)?.content ?? "");
|
|
4052
|
+
const worktreeMatch = promptText.match(/created worktree-dir `([^`]+)` and prepared context-path `([^`]+)`\./);
|
|
4053
|
+
assert.ok(worktreeMatch, promptText);
|
|
4054
|
+
const worktreeName = worktreeMatch?.[1] ?? "";
|
|
4055
|
+
const executionBasePath = worktreeMatch?.[2] ?? "";
|
|
4056
|
+
|
|
4057
|
+
assert.equal(process.cwd(), projectBase);
|
|
4058
|
+
assert.equal(fs.existsSync(executionBasePath), false, "worktree directory must be deleted after successful closure");
|
|
4059
|
+
assert.notEqual(
|
|
4060
|
+
spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${worktreeName}`], {
|
|
4061
|
+
cwd: projectBase,
|
|
4062
|
+
encoding: "utf8",
|
|
4063
|
+
}).status,
|
|
4064
|
+
0,
|
|
4065
|
+
"worktree branch must be deleted after successful closure",
|
|
4066
|
+
);
|
|
4067
|
+
} finally {
|
|
4068
|
+
process.chdir(previousCwd);
|
|
4069
|
+
fs.rmSync(projectBase, { recursive: true, force: true });
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
|
|
3783
4073
|
/**
|
|
3784
4074
|
* @brief Verifies worktree-backed prompt orchestration against the pi runtime session-switch contract.
|
|
3785
4075
|
* @details Simulates the current pi runtime behavior discovered in `/tmp/pi-mono`: `ctx.switchSession(...)` exposes the replacement session through `withSession(...)` and replacement-session contexts while the host process cwd stays anchored to the original project directory until the extension updates it explicitly. The test proves prompt delivery and prompt-end closure must synchronize `process.cwd()` with the active worktree session during execution and restore `base-path` plus remove the worktree after successful merge even when the pi host leaves cwd unchanged. Runtime is dominated by temporary git worktree setup and teardown. Side effects are limited to temporary repository mutation and temporary session-file writes.
|
|
@@ -5595,7 +5885,7 @@ test("default configuration applies the documented static-check, debug, notify,
|
|
|
5595
5885
|
{ module: "Command", cmd: "node", params: ["--check"] },
|
|
5596
5886
|
], "enable"));
|
|
5597
5887
|
assert.deepEqual(config["static-check"].TypeScript, createStaticCheckLanguageConfig([
|
|
5598
|
-
{ module: "Command", cmd: "
|
|
5888
|
+
{ module: "Command", cmd: "eslint", params: [] },
|
|
5599
5889
|
], "enable"));
|
|
5600
5890
|
assert.deepEqual(config["static-check"].Ruby, createStaticCheckLanguageConfig([], "disable"));
|
|
5601
5891
|
assert.equal(config.DEBUG_ENABLED, "disable");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_STATIC_CHECK_LANGUAGES,
|
|
5
|
+
getDefaultConfig,
|
|
6
|
+
} from "../src/core/config.js";
|
|
7
|
+
import { renderPromptCommandSummary } from "../src/core/prompts.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @brief Builds a configuration with no enabled context files, no enabled static-check languages, and no enabled tools.
|
|
11
|
+
* @details Clones the documented default configuration, disables every context-file flag, forces every canonical static-check language to `disable`, and empties the enabled-tool list so the command invocation summary is forced to render the `none` placeholder for all three empty-category fields. Runtime is O(l) in supported language count. No external state is mutated.
|
|
12
|
+
* @param[in] projectBase {string} Absolute project root path used to seed the default configuration.
|
|
13
|
+
* @return {ReturnType<typeof getDefaultConfig>} Configuration object with all three summary categories empty.
|
|
14
|
+
*/
|
|
15
|
+
function buildEmptySummaryConfig(projectBase: string): ReturnType<typeof getDefaultConfig> {
|
|
16
|
+
const config = getDefaultConfig(projectBase);
|
|
17
|
+
config["context-files-requirements"] = false;
|
|
18
|
+
config["context-files-references"] = false;
|
|
19
|
+
config["context-files-workflow"] = false;
|
|
20
|
+
for (const language of DEFAULT_STATIC_CHECK_LANGUAGES) {
|
|
21
|
+
config["static-check"][language] = { ...config["static-check"][language], enabled: "disable" };
|
|
22
|
+
}
|
|
23
|
+
config["enabled-tools"] = [];
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("renderPromptCommandSummary renders none for empty context files, static code checks, and enabled tools", () => {
|
|
28
|
+
const projectBase = process.cwd();
|
|
29
|
+
const config = buildEmptySummaryConfig(projectBase);
|
|
30
|
+
const summary = renderPromptCommandSummary("analyze", "Inspect runtime behavior", config);
|
|
31
|
+
assert.match(summary, /Command: ANALYZE/);
|
|
32
|
+
assert.match(summary, /User's Request: Inspect runtime behavior/);
|
|
33
|
+
assert.match(summary, /- context files: none/);
|
|
34
|
+
assert.match(summary, /- static code checks: none/);
|
|
35
|
+
assert.match(summary, /- enabled tools: none/);
|
|
36
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* @brief Verifies bundled static-checker resolution, postinstall exit-code invariants, and session_start missing-checker notification.
|
|
4
|
+
* @details Exercises `resolveCheckerExecutable` bundled-bin precedence, the `scripts/install-static-checkers.ts` exit-code-0 invariant, and the `checkDefaultCheckersAvailability` helper used during `session_start`. Runtime is dominated by filesystem metadata checks and optional child-process execution. Side effects are limited to temporary filesystem reads and bounded writes under the installation-owned `node_modules/.bin` with cleanup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import {
|
|
15
|
+
resolveCheckerExecutable,
|
|
16
|
+
checkDefaultCheckersAvailability,
|
|
17
|
+
} from "../src/core/static-check.js";
|
|
18
|
+
import { getInstallationPath } from "../src/core/path-context.js";
|
|
19
|
+
import { getDefaultConfig } from "../src/core/config.js";
|
|
20
|
+
import { main as installStaticCheckersMain } from "../scripts/install-static-checkers.ts";
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
24
|
+
const TSX_LOADER = require.resolve("tsx", { paths: [ROOT] });
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @brief Resolves the bundled `node_modules/.bin` directory probed by `resolveCheckerExecutable`.
|
|
28
|
+
* @details Computes the first bundled-bin candidate directory derived from the runtime installation path so tests can create and clean up temporary fake executables at the exact probed location. Runtime is O(1). No side effects occur.
|
|
29
|
+
* @return {string} Absolute bundled `node_modules/.bin` directory path.
|
|
30
|
+
*/
|
|
31
|
+
function getBundledBinDir(): string {
|
|
32
|
+
return path.join(getInstallationPath(), "..", "node_modules", ".bin");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("resolveCheckerExecutable probes bundled node_modules/.bin before PATH scan", () => {
|
|
36
|
+
const binDir = getBundledBinDir();
|
|
37
|
+
const fakeName = "pi-usereq-fake-checker";
|
|
38
|
+
const fakeBin = path.join(binDir, fakeName);
|
|
39
|
+
const dirExisted = fs.existsSync(binDir);
|
|
40
|
+
try {
|
|
41
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
42
|
+
fs.writeFileSync(fakeBin, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
43
|
+
const resolved = resolveCheckerExecutable(fakeName);
|
|
44
|
+
assert.ok(resolved, "expected bundled-bin candidate to resolve");
|
|
45
|
+
assert.equal(resolved, fakeBin);
|
|
46
|
+
} finally {
|
|
47
|
+
if (fs.existsSync(fakeBin)) {
|
|
48
|
+
fs.rmSync(fakeBin, { force: true });
|
|
49
|
+
}
|
|
50
|
+
if (!dirExisted && fs.existsSync(binDir)) {
|
|
51
|
+
fs.rmSync(binDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("resolveCheckerExecutable falls back to PATH scan for system executables", () => {
|
|
57
|
+
const resolved = resolveCheckerExecutable("node");
|
|
58
|
+
assert.ok(resolved, "expected 'node' to resolve via PATH scan");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("resolveCheckerExecutable returns undefined for non-existent commands", () => {
|
|
62
|
+
const resolved = resolveCheckerExecutable("pi-usereq-definitely-not-a-real-binary-xyz");
|
|
63
|
+
assert.equal(resolved, undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("scripts/install-static-checkers.ts always returns exit code 0", () => {
|
|
67
|
+
const exitCode = installStaticCheckersMain([]);
|
|
68
|
+
assert.equal(exitCode, 0);
|
|
69
|
+
const exitCodeWithArgs = installStaticCheckersMain(["--ignored"]);
|
|
70
|
+
assert.equal(exitCodeWithArgs, 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("scripts/install-static-checkers.ts exits 0 when invoked as a subprocess", () => {
|
|
74
|
+
const result = spawnSync(
|
|
75
|
+
"node",
|
|
76
|
+
["--import", TSX_LOADER, path.join(ROOT, "scripts", "install-static-checkers.ts")],
|
|
77
|
+
{ encoding: "utf8" },
|
|
78
|
+
);
|
|
79
|
+
assert.equal(result.status, 0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("checkDefaultCheckersAvailability returns missing enabled checkers for default config", () => {
|
|
83
|
+
const config = getDefaultConfig(process.cwd());
|
|
84
|
+
const missing = checkDefaultCheckersAvailability(config);
|
|
85
|
+
assert.ok(Array.isArray(missing), "expected an array of missing checker names");
|
|
86
|
+
for (const checker of ["cppcheck", "clang-format"]) {
|
|
87
|
+
if (!resolveCheckerExecutable(checker)) {
|
|
88
|
+
assert.ok(missing.includes(checker), `expected missing checker '${checker}' to be reported`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("checkDefaultCheckersAvailability returns empty array when all checkers disabled", () => {
|
|
94
|
+
const config = getDefaultConfig(process.cwd());
|
|
95
|
+
for (const languageConfig of Object.values(config["static-check"])) {
|
|
96
|
+
languageConfig.enabled = "disable";
|
|
97
|
+
}
|
|
98
|
+
const missing = checkDefaultCheckersAvailability(config);
|
|
99
|
+
assert.equal(missing.length, 0);
|
|
100
|
+
});
|