@oh-my-pi/pi-coding-agent 16.3.14 → 16.4.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 +35 -0
- package/dist/cli.js +3053 -3158
- package/dist/types/advisor/config.d.ts +7 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +28 -15
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +7 -2
- package/dist/types/dap/config.d.ts +13 -1
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +7 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +8 -3
- package/dist/types/modes/components/hook-selector.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/session-context.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/thinking.d.ts +6 -3
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/config.test.ts +84 -0
- package/src/advisor/config.ts +28 -0
- package/src/cli/args.ts +1 -0
- package/src/cli/flag-tables.ts +3 -0
- package/src/config/model-registry.ts +1 -1
- package/src/config/model-resolver.ts +14 -12
- package/src/config/models-config-schema.ts +3 -2
- package/src/config/settings-schema.ts +5 -2
- package/src/dap/config.ts +136 -39
- package/src/dap/defaults.json +1 -1
- package/src/eval/js/shared/runtime.ts +20 -4
- package/src/eval/js/worker-core.ts +9 -2
- package/src/exec/bash-executor.ts +8 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/config.ts +27 -13
- package/src/lsp/index.ts +114 -29
- package/src/main.ts +21 -1
- package/src/mcp/transports/stdio.test.ts +12 -0
- package/src/mcp/transports/stdio.ts +14 -8
- package/src/modes/components/hook-selector.ts +9 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/theme/defaults/dark-poimandres.json +6 -5
- package/src/modes/theme/defaults/light-poimandres.json +6 -5
- package/src/modes/theme/theme-schema.json +6 -2
- package/src/modes/theme/theme.ts +24 -18
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/{explore.md → scout.md} +3 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +5 -3
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/debug.md +4 -4
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/task.md +2 -2
- package/src/sdk.ts +68 -45
- package/src/session/agent-session.ts +215 -64
- package/src/session/session-context.test.ts +83 -0
- package/src/session/session-context.ts +1 -0
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-manager.ts +9 -1
- package/src/system-prompt.test.ts +20 -11
- package/src/task/agents.ts +2 -5
- package/src/task/executor.ts +111 -74
- package/src/thinking.ts +16 -7
- package/src/tools/ask.ts +51 -13
- package/src/tools/browser/cmux/cmux-tab.ts +21 -12
- package/src/tools/debug.ts +41 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/prompts/agents/tester.md +0 -111
package/src/tools/ask.ts
CHANGED
|
@@ -380,6 +380,7 @@ interface AskSingleQuestionOptions {
|
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
interface UIContext {
|
|
383
|
+
timeoutStartsOnPresentation?: boolean;
|
|
383
384
|
select(
|
|
384
385
|
prompt: string,
|
|
385
386
|
options: ExtensionUISelectItem[],
|
|
@@ -389,6 +390,8 @@ interface UIContext {
|
|
|
389
390
|
signal?: AbortSignal;
|
|
390
391
|
outline?: boolean;
|
|
391
392
|
onTimeout?: () => void;
|
|
393
|
+
onTimeoutStart?: () => void;
|
|
394
|
+
onTimeoutReset?: () => void;
|
|
392
395
|
onLeft?: () => void;
|
|
393
396
|
onRight?: () => void;
|
|
394
397
|
helpText?: string;
|
|
@@ -432,12 +435,30 @@ async function askSingleQuestion(
|
|
|
432
435
|
const helpText = navigation
|
|
433
436
|
? "up/down navigate enter select ←/→ question esc cancel"
|
|
434
437
|
: "up/down navigate enter select esc cancel";
|
|
438
|
+
const timeoutMs = typeof timeout === "number" && timeout > 0 ? timeout : undefined;
|
|
439
|
+
const timeoutController = timeoutMs === undefined ? undefined : new AbortController();
|
|
440
|
+
const dialogSignal =
|
|
441
|
+
signal && timeoutController
|
|
442
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
443
|
+
: (timeoutController?.signal ?? signal);
|
|
444
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
445
|
+
let timeoutStartedMs = Date.now();
|
|
446
|
+
const armFallbackTimeout = (durationMs: number) => {
|
|
447
|
+
clearTimeout(timeoutId);
|
|
448
|
+
timeoutStartedMs = Date.now();
|
|
449
|
+
timeoutId = setTimeout(() => {
|
|
450
|
+
timeoutTriggered = true;
|
|
451
|
+
timeoutController?.abort();
|
|
452
|
+
}, durationMs);
|
|
453
|
+
};
|
|
435
454
|
const dialogOptions = {
|
|
436
455
|
initialIndex,
|
|
437
456
|
timeout,
|
|
438
|
-
signal,
|
|
457
|
+
signal: dialogSignal,
|
|
439
458
|
outline: true,
|
|
440
459
|
onTimeout,
|
|
460
|
+
onTimeoutStart: timeoutMs === undefined ? undefined : () => armFallbackTimeout(timeoutMs),
|
|
461
|
+
onTimeoutReset: timeoutMs === undefined ? undefined : () => armFallbackTimeout(timeoutMs),
|
|
441
462
|
helpText,
|
|
442
463
|
selectionMarker: marker?.selectionMarker,
|
|
443
464
|
checkedIndices: marker?.checkedIndices,
|
|
@@ -453,19 +474,32 @@ async function askSingleQuestion(
|
|
|
453
474
|
}
|
|
454
475
|
: undefined,
|
|
455
476
|
};
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
477
|
+
try {
|
|
478
|
+
const runSelect = () => {
|
|
479
|
+
const selection = ui.select(prompt, optionsToShow, dialogOptions);
|
|
480
|
+
if (timeoutMs !== undefined && !ui.timeoutStartsOnPresentation) {
|
|
481
|
+
armFallbackTimeout(timeoutMs);
|
|
482
|
+
}
|
|
483
|
+
return selection;
|
|
484
|
+
};
|
|
485
|
+
const choice = dialogSignal ? await untilAborted(dialogSignal, runSelect) : await runSelect();
|
|
486
|
+
if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
|
|
487
|
+
// Fallback for UI surfaces that enforce `timeout` without invoking
|
|
488
|
+
// `onTimeout`: their auto-cancel resolves right at the deadline. A
|
|
489
|
+
// cancel arriving well past the deadline is a deliberate user Esc on
|
|
490
|
+
// a surface that kept the dialog open — keep treating it as a cancel.
|
|
491
|
+
const elapsed = Date.now() - timeoutStartedMs;
|
|
492
|
+
timeoutTriggered = elapsed >= timeout && elapsed <= timeout + TIMEOUT_DETECTION_TOLERANCE_MS;
|
|
493
|
+
}
|
|
494
|
+
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (timeoutTriggered && error instanceof Error && error.name === "AbortError") {
|
|
497
|
+
return { choice: undefined, timedOut: true, navigation: navigationAction };
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
500
|
+
} finally {
|
|
501
|
+
clearTimeout(timeoutId);
|
|
467
502
|
}
|
|
468
|
-
return { choice, timedOut: timeoutTriggered, navigation: navigationAction };
|
|
469
503
|
};
|
|
470
504
|
|
|
471
505
|
const promptForCustomInput = async (
|
|
@@ -614,6 +648,9 @@ async function askSingleQuestion(
|
|
|
614
648
|
customInput = undefined;
|
|
615
649
|
break;
|
|
616
650
|
}
|
|
651
|
+
if (timedOut && selectedOptions.length === 0 && customInput === undefined) {
|
|
652
|
+
selectedOptions = getAutoSelectionOnTimeout(questionOptions, recommended);
|
|
653
|
+
}
|
|
617
654
|
if (navigation?.allowForward) {
|
|
618
655
|
return { selectedOptions, customInput, timedOut, navigation: "forward" };
|
|
619
656
|
}
|
|
@@ -743,6 +780,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
743
780
|
|
|
744
781
|
const extensionUi = context.ui;
|
|
745
782
|
const ui: UIContext = {
|
|
783
|
+
timeoutStartsOnPresentation: extensionUi.timeoutStartsOnPresentation,
|
|
746
784
|
select: (prompt, options, dialogOptions) => extensionUi.select(prompt, options, dialogOptions),
|
|
747
785
|
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
748
786
|
extensionUi.editor(title, prefill, dialogOptions, editorOptions),
|
|
@@ -1288,20 +1288,13 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1288
1288
|
const screenshots: ScreenshotResult[] = [];
|
|
1289
1289
|
const runId = crypto.randomUUID();
|
|
1290
1290
|
tab.setRunContext({ session: opts.snapshot, displays, screenshots, signal, timeoutMs: opts.timeoutMs });
|
|
1291
|
-
const runtime = tab.ensureRuntime(opts.snapshot);
|
|
1292
|
-
runtime.setCwd(opts.snapshot.cwd);
|
|
1293
|
-
const runTab = bindBrowserRunFacade(tab, signal);
|
|
1294
|
-
runtime.setRunScope({
|
|
1295
|
-
page: bindBrowserRunFacade(tab.page, signal),
|
|
1296
|
-
browser: bindBrowserRunFacade(tab.browser, signal),
|
|
1297
|
-
tab: runTab,
|
|
1298
|
-
assert: (cond: unknown, text?: string): void => {
|
|
1299
|
-
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
1300
|
-
},
|
|
1301
|
-
wait: (ms: number): Promise<void> => waitForBrowserRun(ms, signal),
|
|
1302
|
-
});
|
|
1303
1291
|
|
|
1304
1292
|
const { promise: cancelRejection, reject } = Promise.withResolvers<never>();
|
|
1293
|
+
// If the synchronous setup below throws (same-realm ownership conflict)
|
|
1294
|
+
// while `signal` is already aborted, `Promise.race` never attaches a
|
|
1295
|
+
// handler to this promise; keep its armed rejection from surfacing as an
|
|
1296
|
+
// unhandled rejection — the postmortem-fatal path this run guards against.
|
|
1297
|
+
cancelRejection.catch(() => {});
|
|
1305
1298
|
const onAbort = (): void => {
|
|
1306
1299
|
if (timeoutSignal.aborted) {
|
|
1307
1300
|
reject(new ToolError(`Browser code execution timed out after ${opts.timeoutMs}ms`));
|
|
@@ -1317,6 +1310,22 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1317
1310
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
1318
1311
|
|
|
1319
1312
|
try {
|
|
1313
|
+
const runtime = tab.ensureRuntime(opts.snapshot);
|
|
1314
|
+
// setCwd is non-exclusive; setRunScope/run still assert same-realm ownership.
|
|
1315
|
+
// Keep both inside try so a concurrent in-process eval/browser run surfaces as
|
|
1316
|
+
// a rejected promise the supervisor can report, never an unhandled rejection.
|
|
1317
|
+
runtime.setCwd(opts.snapshot.cwd);
|
|
1318
|
+
const runTab = bindBrowserRunFacade(tab, signal);
|
|
1319
|
+
runtime.setRunScope({
|
|
1320
|
+
page: bindBrowserRunFacade(tab.page, signal),
|
|
1321
|
+
browser: bindBrowserRunFacade(tab.browser, signal),
|
|
1322
|
+
tab: runTab,
|
|
1323
|
+
assert: (cond: unknown, text?: string): void => {
|
|
1324
|
+
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
1325
|
+
},
|
|
1326
|
+
wait: (ms: number): Promise<void> => waitForBrowserRun(ms, signal),
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1320
1329
|
const hooks: RuntimeHooks = {
|
|
1321
1330
|
onText: chunk => {
|
|
1322
1331
|
throwIfAborted(signal);
|
package/src/tools/debug.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type DapThread,
|
|
32
32
|
type DapVariable,
|
|
33
33
|
dapSessionManager,
|
|
34
|
+
getAdapterConfigs,
|
|
34
35
|
getAvailableAdapters,
|
|
35
36
|
type LaunchProgramKind,
|
|
36
37
|
resolveLaunchOverrides,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
formatStatusIcon,
|
|
51
52
|
PREVIEW_LIMITS,
|
|
52
53
|
replaceTabs,
|
|
54
|
+
shortenPath,
|
|
53
55
|
TRUNCATE_LENGTHS,
|
|
54
56
|
truncateToWidth,
|
|
55
57
|
} from "./render-utils";
|
|
@@ -106,9 +108,9 @@ const debugActionSchema = type.enumerated(
|
|
|
106
108
|
);
|
|
107
109
|
const debugSchema = type({
|
|
108
110
|
action: debugActionSchema,
|
|
109
|
-
"program?": type("string").describe("
|
|
111
|
+
"program?": type("string").describe("debug target path; Delve accepts Go package directories"),
|
|
110
112
|
"args?": type("string[]").describe("program arguments"),
|
|
111
|
-
"adapter?": type("string").describe("
|
|
113
|
+
"adapter?": type("string").describe("configured adapter id (gdb, lldb-dap, debugpy, dlv, rdbg, or dap.json entry)"),
|
|
112
114
|
cwd: "string?",
|
|
113
115
|
"file?": type("string").describe("source file"),
|
|
114
116
|
"line?": type("number").describe("source line"),
|
|
@@ -494,7 +496,33 @@ function buildOutcomeText(outcome: DapContinueOutcome, timeoutSec: number, verb:
|
|
|
494
496
|
|
|
495
497
|
function getConfiguredAdapters(cwd: string): string {
|
|
496
498
|
const adapters = getAvailableAdapters(cwd).map(adapter => adapter.name);
|
|
497
|
-
|
|
499
|
+
const names = adapters.length > 0 ? adapters.join(", ") : "none";
|
|
500
|
+
return truncateToWidth(replaceTabs(names), TRUNCATE_LENGTHS.LONG);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const ADAPTER_UNAVAILABLE_MESSAGES: Readonly<Record<string, string>> = {
|
|
504
|
+
debugpy: "adapter 'debugpy' is not available: python not found in PATH",
|
|
505
|
+
dlv: "adapter 'dlv' is not available: install with 'go install github.com/go-delve/delve/cmd/dlv@latest'",
|
|
506
|
+
rdbg: "adapter 'rdbg' is not available: install with 'gem install debug'",
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const ADAPTER_CANONICAL_COMMANDS: Readonly<Record<string, string>> = {
|
|
510
|
+
debugpy: "python",
|
|
511
|
+
dlv: "dlv",
|
|
512
|
+
rdbg: "rdbg",
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
function formatAdapterUnavailable(adapterName: string, command: string, cwd: string): string {
|
|
516
|
+
const displayName = truncateToWidth(replaceTabs(adapterName), TRUNCATE_LENGTHS.SHORT);
|
|
517
|
+
const canonicalCommand = ADAPTER_CANONICAL_COMMANDS[adapterName] ?? adapterName;
|
|
518
|
+
if (command !== canonicalCommand) {
|
|
519
|
+
const displayCommand = truncateToWidth(replaceTabs(shortenPath(command)), TRUNCATE_LENGTHS.CONTENT);
|
|
520
|
+
return `adapter '${displayName}' is not available: configured command '${displayCommand}' did not resolve. Check the DAP adapter config for this workspace.`;
|
|
521
|
+
}
|
|
522
|
+
return (
|
|
523
|
+
ADAPTER_UNAVAILABLE_MESSAGES[adapterName] ??
|
|
524
|
+
`adapter '${displayName}' is not available. Installed adapters: ${getConfiguredAdapters(cwd)}`
|
|
525
|
+
);
|
|
498
526
|
}
|
|
499
527
|
|
|
500
528
|
async function classifyLaunchProgram(program: string): Promise<LaunchProgramKind> {
|
|
@@ -515,7 +543,7 @@ function validateLaunchProgram(
|
|
|
515
543
|
if (programKind !== "directory" || adapter.acceptsDirectoryProgram) return;
|
|
516
544
|
const displayPath = formatPathRelativeToCwd(program, cwd, { trailingSlash: true });
|
|
517
545
|
throw new ToolError(
|
|
518
|
-
`launch program resolves to a directory: ${displayPath}. Pass an executable file path
|
|
546
|
+
`launch program resolves to a directory: ${displayPath}. Pass an executable file path or choose an adapter that supports package directories.`,
|
|
519
547
|
);
|
|
520
548
|
}
|
|
521
549
|
|
|
@@ -711,15 +739,16 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
711
739
|
const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
|
|
712
740
|
const program = resolveToCwd(params.program, commandCwd);
|
|
713
741
|
const programKind = await classifyLaunchProgram(program);
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
742
|
+
const selection = selectLaunchAdapter(program, commandCwd, params.adapter, programKind);
|
|
743
|
+
if (selection.kind === "unavailable") {
|
|
744
|
+
throw new ToolError(formatAdapterUnavailable(selection.adapterName, selection.command, commandCwd));
|
|
745
|
+
}
|
|
746
|
+
if (selection.kind === "none") {
|
|
719
747
|
throw new ToolError(
|
|
720
748
|
`No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
|
|
721
749
|
);
|
|
722
750
|
}
|
|
751
|
+
const { adapter } = selection;
|
|
723
752
|
validateLaunchProgram(program, commandCwd, programKind, adapter);
|
|
724
753
|
const extraLaunchArguments = resolveLaunchOverrides(adapter, program, programKind);
|
|
725
754
|
const snapshot = await dapSessionManager.launch(
|
|
@@ -738,8 +767,9 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
738
767
|
const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
|
|
739
768
|
const adapter = selectAttachAdapter(commandCwd, params.adapter, params.port);
|
|
740
769
|
if (!adapter) {
|
|
741
|
-
if (params.adapter
|
|
742
|
-
|
|
770
|
+
if (params.adapter) {
|
|
771
|
+
const command = getAdapterConfigs(commandCwd)[params.adapter]?.command ?? params.adapter;
|
|
772
|
+
throw new ToolError(formatAdapterUnavailable(params.adapter, command, commandCwd));
|
|
743
773
|
}
|
|
744
774
|
throw new ToolError(
|
|
745
775
|
`No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
|
|
@@ -21,7 +21,7 @@ export interface FileDisplayModeSession {
|
|
|
21
21
|
/**
|
|
22
22
|
* Computes effective line display mode from session settings/env.
|
|
23
23
|
* Hashline mode takes precedence and implies line-addressed output everywhere.
|
|
24
|
-
* Hashlines are suppressed when the edit tool is not available (e.g.
|
|
24
|
+
* Hashlines are suppressed when the edit tool is not available (e.g. scout agents),
|
|
25
25
|
* when the caller signals a `raw` read, and when the source is `immutable`
|
|
26
26
|
* (e.g. internal URLs like artifact://, agent://, memory:// — there is no edit
|
|
27
27
|
* path that could consume the anchors). Raw output is returned as-is.
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Tester
|
|
3
|
-
description: Authoritative test writer. ALWAYS delegate test authoring to this agent — NEVER write tests yourself. Writes high-signal tests defending real contracts (behavior, invariants, edge cases) and refuses worthless tests that assert plumbing or restate the code.
|
|
4
|
-
tools: read, grep, glob, bash, edit, write, lsp, ast_grep, ast_edit
|
|
5
|
-
spawns: explore
|
|
6
|
-
model: pi/task
|
|
7
|
-
thinking-level: high
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
<system-conventions>
|
|
11
|
-
RFC 2119 applies to MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` and `AVOID` MUST be interpreted as aliases for `MUST NOT` and `SHOULD NOT` respectively.
|
|
12
|
-
</system-conventions>
|
|
13
|
-
|
|
14
|
-
You are a staff test engineer with taste. You write tests that earn their place in the suite and you delete — or refuse to write — tests that don't. You have agency: when asked for coverage that proves nothing, you write the test that would actually catch the bug instead.
|
|
15
|
-
|
|
16
|
-
<stakes>
|
|
17
|
-
A test suite is a liability until it pays for itself. Every worthless test is negative value: it costs CI time, blocks honest refactors, and lulls the team into false confidence while the real bug ships. A test's only job is to FAIL when behavior breaks and PASS otherwise. A test that cannot fail for any real defect is noise wearing a green check. You are here because models flood codebases with exactly that noise. You write the opposite.
|
|
18
|
-
</stakes>
|
|
19
|
-
|
|
20
|
-
<critical>
|
|
21
|
-
- The litmus for every test: **name the concrete, externally observable contract it defends** — a behavior, output shape, state transition, error mapping, invariant, or a regression-prone parsing boundary. Cannot name it in one sentence? NEVER write the test.
|
|
22
|
-
- Mutation test in your head: if a plausible bug — a flipped condition, an off-by-one, a wrong return value, a dropped case — would still let the test PASS, the test is worthless. Discard it.
|
|
23
|
-
- You NEVER write tests that assert plumbing or restate the implementation. The forbidden classes are enumerated in `<worthless-tests>` and are hard prohibitions.
|
|
24
|
-
- You MUST match the repo's existing test conventions — framework, file layout, naming, assertion style. A second convention beside an existing one is PROHIBITED.
|
|
25
|
-
- NEVER test defaults (configurations, fallback values, or default environment values). If you are updating/refactoring existing tests that test defaults, you MUST delete those assertions or delete the entire default-testing tests instead.
|
|
26
|
-
- You are explicitly ALLOWED to write **no tests at all** if you were spawned for a stupid reason (meaning: the change is trivial—such as docs, comments, types, exports, or simple config; the behavior is already fully covered; or any tests you would write would be worthless, restate plumbing, or test defaults). If so, state this clearly and exit.
|
|
27
|
-
</critical>
|
|
28
|
-
|
|
29
|
-
<anti-patterns name="worthless-tests">
|
|
30
|
-
NEVER write any of these. Each is a green check that survives real bugs:
|
|
31
|
-
- **Config/setter echo.** Setting a value then asserting it reads back (`set(x, 30); expect(get(x)).toBe(30)`) tests the language's assignment, not your code.
|
|
32
|
-
- **Source-grep.** Reading an implementation/build file and asserting on its TEXT — `expect(src).toContain("newFn()")`, `.toMatch(/import …/)`, `.not.toContain("oldName")`, "comment says X". Tests how code LOOKS, breaks on rename/reflow, passes while behavior is broken. Enforce structural facts with a type test or lint rule; enforce behavior by running the code.
|
|
33
|
-
- **Tautologies.** `expect(true).toBe(true)`, `expect(x).toBe(x)`, asserting a constant equals its literal.
|
|
34
|
-
- **Bare no-throw.** `expect(() => f()).not.toThrow()` with no assertion on the result. "It ran" is not a contract.
|
|
35
|
-
- **Construction smoke.** "Constructs without error", "package boots", "command starts" — unless that wiring genuinely can't be exercised in-process AND a real failure mode hides there.
|
|
36
|
-
- **Mock round-trips.** Asserting a mock was called with the args you just passed it. You tested the mock, not the system.
|
|
37
|
-
- **Existence/shape-only.** Non-empty string, length-grew, "field is defined", "returns an object with key Y" — without asserting the VALUE that matters.
|
|
38
|
-
- **Default values.** NEVER assert that default configurations, fallback properties, or default environment values match specific literals. A harmless change to a default setting must never break the tests. If you are touching or refactoring existing tests that assert defaults, **delete those assertions or the entire test instead**.
|
|
39
|
-
- **Field-wiring.** Asserting an option passed in lands on a property, or that a getter returns the value the constructor stored. Test the downstream BEHAVIOR that depends on it, not the assignment.
|
|
40
|
-
- **Duplicate-layer coverage.** Re-proving through mocks what an integration test already proves. Drop the narrower restatement.
|
|
41
|
-
|
|
42
|
-
When asked for coverage that would only produce the above, you write the test that actually exercises the behavior, and you state in your result why the requested shape was worthless.
|
|
43
|
-
</anti-patterns>
|
|
44
|
-
|
|
45
|
-
<what-to-test>
|
|
46
|
-
Aim every test at something that can actually break:
|
|
47
|
-
- **Behavior & outputs** — given input, the observable result (return value, emitted event, written file, error surfaced).
|
|
48
|
-
- **State transitions** — the legal and illegal moves of a stateful component; one test per invariant or transition, not one per field touched.
|
|
49
|
-
- **Invariants across fields** — relationships that MUST hold (sorted output stays sorted, sum of parts equals total, encode∘decode is identity).
|
|
50
|
-
- **Edge & boundary values** — zero, empty, one, max, negative, off-by-one, overflow, unicode, the value just inside and just outside a limit.
|
|
51
|
-
- **Precedence & resolution** — arg beats env beats default; later override wins; first-match-wins.
|
|
52
|
-
- **Error paths** — trigger the REAL failure (bad input, missing dep, denied permission) and assert the surfaced contract (error type, message mapping, exit code). NEVER instantiate the error class directly or inspect internal metadata.
|
|
53
|
-
- **Regression-prone parsing boundaries** — the exact bytes where a parser/serializer historically broke; pin past regressions with a named case.
|
|
54
|
-
</what-to-test>
|
|
55
|
-
|
|
56
|
-
<techniques>
|
|
57
|
-
Reach for the right shape; do not reinvent what the repo's framework already gives you.
|
|
58
|
-
- **Table-driven tests.** One body, many `{ name, input, expected }` rows covering boundaries and equivalence classes plus error cases. Name every row so a failure points at the case. The default shape for any function with a clear input→output mapping.
|
|
59
|
-
- **Subtests.** Group related cases under one parent with isolated setup and independent failure reporting. Prefer over many tiny near-duplicate test functions.
|
|
60
|
-
- **Property-based tests.** Assert invariants over generated inputs — round-trip identity, idempotence (`f(f(x)) == f(x)`), commutativity, monotonicity, "never panics and output stays well-formed". Catches cases you wouldn't enumerate by hand.
|
|
61
|
-
- **Deterministic randomness.** Seed every generator and PRINT the seed on failure so a red run reproduces exactly. NEVER use an unseeded clock-derived source — flaky tests are worse than no tests.
|
|
62
|
-
- **Fuzz tests.** For parsers, decoders, deserializers, anything eating untrusted bytes: feed mutated/random input, assert no crash and that invariants hold. Seed the corpus from known-tricky inputs and every past regression.
|
|
63
|
-
- **Benchmarks.** ONLY when performance is part of the contract. Measure the operation, not setup; consume the result so it isn't optimized away; compare against a baseline or threshold. A benchmark that asserts nothing is documentation, not a test.
|
|
64
|
-
- **Golden/snapshot.** Only for genuinely stable, human-reviewed output where exact bytes are the contract (codegen, serialized formats). NEVER snapshot volatile or incidental output — it becomes a rubber stamp nobody reads.
|
|
65
|
-
</techniques>
|
|
66
|
-
|
|
67
|
-
<black-box>
|
|
68
|
-
- **Test through the public API**, the way a real consumer calls it. Place tests in an EXTERNAL test package/module (separate namespace, no access to internals) so the compiler forbids reaching past the contract. This is the default and it forces you to test what callers depend on.
|
|
69
|
-
- **Internal (white-box) tests only for private invariants with no observable surface** — e.g. a balancing property of an internal tree, a cache eviction order. Justify each one; if the invariant has an observable effect, test that effect from outside instead.
|
|
70
|
-
- NEVER reach into private state to assert what you could observe through the public surface. Coupling tests to internals is what makes refactors painful and tempts people to delete the suite.
|
|
71
|
-
</black-box>
|
|
72
|
-
|
|
73
|
-
<fakes>
|
|
74
|
-
- **Prefer real implementations.** If the dependency is cheap and deterministic, use the real thing.
|
|
75
|
-
- **Prefer hand-written fakes over mocking frameworks.** A small in-memory implementation of an interface is type-checked, readable, survives refactors, and tests behavior. Mocking frameworks pull you toward asserting call counts and argument sequences — that is plumbing, and it breaks on every harmless internal change.
|
|
76
|
-
- **Mock only true external boundaries** — network, wall clock, filesystem, system randomness, third-party services — and even there a fake beats a mock. Inject the boundary; never patch globals.
|
|
77
|
-
- NEVER use module-registry mocking that leaks across test files. Spy on the imported object and restore in teardown.
|
|
78
|
-
</fakes>
|
|
79
|
-
|
|
80
|
-
<isolation>
|
|
81
|
-
Tests MUST be full-suite safe and order-independent, not merely file-local safe.
|
|
82
|
-
- **No timing dependence.** NEVER `sleep`/`setTimeout`-race to "let it settle". Inject a controllable clock and advance it; wait on a condition, signal, or promise, never a wall-clock duration. Real-time waits are the #1 source of flake.
|
|
83
|
-
- **No environment pollution.** NEVER leak env vars, temp files, global singletons, `process.env`/`process.platform`/`Bun.*` mutations, or monkeypatches past the test. Use per-test setup with restore in teardown. A test that passes alone but poisons a later file is broken.
|
|
84
|
-
- **Deterministic.** No dependence on map/iteration order, filesystem ordering, locale, timezone, or concurrency interleaving unless that ordering IS the contract under test.
|
|
85
|
-
- **Hermetic.** No real network or real time. Each test creates and tears down its own fixtures.
|
|
86
|
-
</isolation>
|
|
87
|
-
|
|
88
|
-
<workflow>
|
|
89
|
-
1. **Study the code under test.** Read exact signatures, return types, and error paths with `lsp`/`read` — NEVER guess an API. Spawn `explore` for unfamiliar areas.
|
|
90
|
-
2. **Study existing tests.** Find the framework, file layout, naming, fake/fixture helpers, and assertion style. You MUST reuse them. `grep`/`glob` for sibling test files.
|
|
91
|
-
3. **Enumerate contracts.** List the observable behaviors, invariants, edge cases, and error mappings worth defending. Drop anything that fails the `<critical>` litmus.
|
|
92
|
-
4. **Pick the shape** per `<techniques>` — table, property, fuzz, benchmark, or a focused unit/integration test.
|
|
93
|
-
5. **Write the tests**, matching repo conventions exactly. Assert semantic content; assert exact bytes ONLY where downstream parses them.
|
|
94
|
-
6. **Run them and verify they have teeth.** Execute the suite with the repo's runner; confirm green. Then confirm each test can FAIL: mentally (or by a throwaway mutation) check that a real defect reddens it. A test you never saw fail is unproven.
|
|
95
|
-
</workflow>
|
|
96
|
-
|
|
97
|
-
<verify>
|
|
98
|
-
- You MUST run the tests you wrote with the project's test command and confirm they pass.
|
|
99
|
-
- You MUST confirm they are not vacuous: a test that passes against broken code is a defect you authored. When cheap, perturb the implementation to watch the test fail, then revert.
|
|
100
|
-
- Run ONLY the tests you added or touched unless asked for the full suite.
|
|
101
|
-
- Report each test by the contract it defends — not "added N tests", but "covers <behavior/invariant/edge>".
|
|
102
|
-
</verify>
|
|
103
|
-
|
|
104
|
-
<critical>
|
|
105
|
-
- A test exists to FAIL on a real bug. No nameable contract, or no plausible bug would redden it → NEVER write it.
|
|
106
|
-
- NEVER assert plumbing, restate the implementation, or grep the source. Test observable behavior through the public surface.
|
|
107
|
-
- No timing races, no environment pollution, deterministic and order-independent — full-suite safe.
|
|
108
|
-
- NEVER test defaults. If updating tests that do, delete them instead.
|
|
109
|
-
- You are explicitly ALLOWED to write **no tests at all** if you were spawned for a stupid reason (trivial changes, already covered, or if any possible test would be worthless/test defaults).
|
|
110
|
-
- You MUST keep going until the tests are written, passing, and proven to have teeth (unless skipped per above).
|
|
111
|
-
</critical>
|