@zephyrdeng/pi-review 0.7.0 → 0.8.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/README.md +51 -7
- package/README.zh-CN.md +45 -5
- package/dist/args.d.ts +4 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +82 -5
- package/dist/args.js.map +1 -1
- package/dist/args.test.js +72 -1
- package/dist/args.test.js.map +1 -1
- package/dist/child-process.d.ts +6 -0
- package/dist/child-process.d.ts.map +1 -1
- package/dist/child-process.js +48 -0
- package/dist/child-process.js.map +1 -1
- package/dist/child-process.test.js +55 -1
- package/dist/child-process.test.js.map +1 -1
- package/dist/cli-exit.test.js +14 -5
- package/dist/cli-exit.test.js.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/install-args.test.js +1 -1
- package/dist/install-args.test.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +2 -3
- package/dist/install.js.map +1 -1
- package/dist/json-events.d.ts +34 -2
- package/dist/json-events.d.ts.map +1 -1
- package/dist/json-events.js +156 -49
- package/dist/json-events.js.map +1 -1
- package/dist/json-events.test.js +176 -4
- package/dist/json-events.test.js.map +1 -1
- package/dist/loop.d.ts +28 -2
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +45 -3
- package/dist/loop.js.map +1 -1
- package/dist/loop.test.js +24 -1
- package/dist/loop.test.js.map +1 -1
- package/dist/matcher.test.js +1 -1
- package/dist/matcher.test.js.map +1 -1
- package/dist/meta-footer.d.ts +4 -8
- package/dist/meta-footer.d.ts.map +1 -1
- package/dist/meta-footer.js +19 -5
- package/dist/meta-footer.js.map +1 -1
- package/dist/meta-footer.test.js +11 -4
- package/dist/meta-footer.test.js.map +1 -1
- package/dist/panel-aggregate.test.js +1 -1
- package/dist/panel-aggregate.test.js.map +1 -1
- package/dist/panel-cancellation.test.d.ts +2 -0
- package/dist/panel-cancellation.test.d.ts.map +1 -0
- package/dist/panel-cancellation.test.js +55 -0
- package/dist/panel-cancellation.test.js.map +1 -0
- package/dist/panel-cli-ui.test.d.ts +2 -0
- package/dist/panel-cli-ui.test.d.ts.map +1 -0
- package/dist/panel-cli-ui.test.js +91 -0
- package/dist/panel-cli-ui.test.js.map +1 -0
- package/dist/panel-cli.test.js +76 -27
- package/dist/panel-cli.test.js.map +1 -1
- package/dist/panel-config.d.ts +38 -0
- package/dist/panel-config.d.ts.map +1 -1
- package/dist/panel-config.js +137 -4
- package/dist/panel-config.js.map +1 -1
- package/dist/panel-config.test.js +84 -4
- package/dist/panel-config.test.js.map +1 -1
- package/dist/panel-loop.test.js +1 -1
- package/dist/panel-loop.test.js.map +1 -1
- package/dist/panel-tools.test.d.ts +2 -0
- package/dist/panel-tools.test.d.ts.map +1 -0
- package/dist/panel-tools.test.js +12 -0
- package/dist/panel-tools.test.js.map +1 -0
- package/dist/panel-ui.d.ts +20 -0
- package/dist/panel-ui.d.ts.map +1 -0
- package/dist/panel-ui.js +84 -0
- package/dist/panel-ui.js.map +1 -0
- package/dist/panel-ui.test.d.ts +2 -0
- package/dist/panel-ui.test.d.ts.map +1 -0
- package/dist/panel-ui.test.js +122 -0
- package/dist/panel-ui.test.js.map +1 -0
- package/dist/panel-usage.d.ts +6 -0
- package/dist/panel-usage.d.ts.map +1 -0
- package/dist/panel-usage.js +21 -0
- package/dist/panel-usage.js.map +1 -0
- package/dist/panel-view.d.ts +42 -0
- package/dist/panel-view.d.ts.map +1 -0
- package/dist/panel-view.js +124 -0
- package/dist/panel-view.js.map +1 -0
- package/dist/panel-view.test.d.ts +2 -0
- package/dist/panel-view.test.d.ts.map +1 -0
- package/dist/panel-view.test.js +112 -0
- package/dist/panel-view.test.js.map +1 -0
- package/dist/panel.d.ts +22 -2
- package/dist/panel.d.ts.map +1 -1
- package/dist/panel.js +184 -60
- package/dist/panel.js.map +1 -1
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +34 -3
- package/dist/prompt.js.map +1 -1
- package/dist/prompt.test.js +21 -2
- package/dist/prompt.test.js.map +1 -1
- package/dist/review-events.d.ts +70 -0
- package/dist/review-events.d.ts.map +1 -0
- package/dist/review-events.js +52 -0
- package/dist/review-events.js.map +1 -0
- package/dist/review-events.test.d.ts +2 -0
- package/dist/review-events.test.d.ts.map +1 -0
- package/dist/review-events.test.js +47 -0
- package/dist/review-events.test.js.map +1 -0
- package/dist/review-result.test.js +1 -1
- package/dist/review-result.test.js.map +1 -1
- package/dist/review.js +3 -3
- package/dist/review.js.map +1 -1
- package/dist/review.meta.test.js +1 -1
- package/dist/review.meta.test.js.map +1 -1
- package/dist/skill-protocol.test.js +1 -1
- package/dist/skill-protocol.test.js.map +1 -1
- package/dist/skill-update.test.d.ts +2 -0
- package/dist/skill-update.test.d.ts.map +1 -0
- package/dist/skill-update.test.js +23 -0
- package/dist/skill-update.test.js.map +1 -0
- package/dist/skill.d.ts +22 -0
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +109 -22
- package/dist/skill.js.map +1 -1
- package/dist/types.d.ts +28 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-client.d.ts +13 -0
- package/dist/ui-client.d.ts.map +1 -0
- package/dist/ui-client.js +161 -0
- package/dist/ui-client.js.map +1 -0
- package/dist/ui-client.test.d.ts +2 -0
- package/dist/ui-client.test.d.ts.map +1 -0
- package/dist/ui-client.test.js +48 -0
- package/dist/ui-client.test.js.map +1 -0
- package/dist/ui-html.d.ts +7 -0
- package/dist/ui-html.d.ts.map +1 -0
- package/dist/ui-html.js +52 -0
- package/dist/ui-html.js.map +1 -0
- package/dist/ui-html.test.d.ts +2 -0
- package/dist/ui-html.test.d.ts.map +1 -0
- package/dist/ui-html.test.js +26 -0
- package/dist/ui-html.test.js.map +1 -0
- package/dist/ui-server-main.d.ts +3 -0
- package/dist/ui-server-main.d.ts.map +1 -0
- package/dist/ui-server-main.js +128 -0
- package/dist/ui-server-main.js.map +1 -0
- package/dist/ui-server-main.test.d.ts +2 -0
- package/dist/ui-server-main.test.d.ts.map +1 -0
- package/dist/ui-server-main.test.js +123 -0
- package/dist/ui-server-main.test.js.map +1 -0
- package/dist/ui-server.d.ts +42 -0
- package/dist/ui-server.d.ts.map +1 -0
- package/dist/ui-server.js +233 -0
- package/dist/ui-server.js.map +1 -0
- package/dist/ui-server.test.d.ts +2 -0
- package/dist/ui-server.test.d.ts.map +1 -0
- package/dist/ui-server.test.js +236 -0
- package/dist/ui-server.test.js.map +1 -0
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +48 -19
- package/dist/update.js.map +1 -1
- package/extensions/panel-tool.test.ts +388 -0
- package/extensions/panel-tool.ts +390 -0
- package/extensions/review.test.ts +79 -0
- package/extensions/review.ts +191 -45
- package/extensions/rv-completions.test.ts +177 -18
- package/extensions/rv-completions.ts +490 -60
- package/extensions/rv-interactive.test.ts +562 -0
- package/extensions/rv-interactive.ts +616 -0
- package/extensions/rv-locale.test.ts +12 -4
- package/extensions/rv-locale.ts +44 -11
- package/extensions/rv-model-picker.test.ts +172 -0
- package/extensions/rv-model-picker.ts +313 -0
- package/extensions/rv-model-priorities.test.ts +29 -9
- package/extensions/rv-model-priorities.ts +23 -8
- package/extensions/rv-prompts.test.ts +281 -2
- package/extensions/rv-prompts.ts +480 -32
- package/extensions/rv-resolve.test.ts +189 -0
- package/extensions/rv-resolve.ts +313 -0
- package/extensions/rv-semantic.test.ts +1 -1
- package/extensions/rv-semantic.ts +38 -4
- package/package.json +18 -6
- package/resources/review-presets.json +2 -2
- package/resources/rv-model-priorities.json +18 -9
- package/skills/pi-review/SKILL.md +84 -60
- package/skills/pi-review/references/codex-tools.md +18 -11
- package/skills/pi-review/references/model-selection.md +49 -28
- package/dist/extensions/review.d.ts +0 -2
- package/dist/extensions/review.js +0 -22
- package/dist/extensions/rv-prompts.d.ts +0 -19
- package/dist/extensions/rv-prompts.js +0 -132
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import type { ModelInfo } from "./rv-completions.js";
|
|
4
|
+
import {
|
|
5
|
+
runRvInteractiveWizard,
|
|
6
|
+
shouldRunInteractiveWizard,
|
|
7
|
+
stripInteractiveToken,
|
|
8
|
+
type InteractiveUi,
|
|
9
|
+
} from "./rv-interactive.js";
|
|
10
|
+
import type { RvParsed } from "./rv-prompts.js";
|
|
11
|
+
|
|
12
|
+
const models: ModelInfo[] = [
|
|
13
|
+
{
|
|
14
|
+
provider: "openai-codex",
|
|
15
|
+
id: "gpt-5.6-sol",
|
|
16
|
+
label: "openai-codex/gpt-5.6-sol",
|
|
17
|
+
name: "sol",
|
|
18
|
+
reasoning: true,
|
|
19
|
+
contextWindow: 200000,
|
|
20
|
+
thinkingLevels: ["high", "xhigh"],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
provider: "anthropic",
|
|
24
|
+
id: "claude-sonnet-4-5",
|
|
25
|
+
label: "anthropic/claude-sonnet-4-5",
|
|
26
|
+
name: "sonnet",
|
|
27
|
+
reasoning: true,
|
|
28
|
+
contextWindow: 200000,
|
|
29
|
+
thinkingLevels: ["high", "xhigh"],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// 12 models across 3 providers (5/4/3) — above the MODEL_PICKER_THRESHOLD of 8,
|
|
34
|
+
// so the wizard offers provider browse / search / ranked list / skip.
|
|
35
|
+
// code-profile presets match claude-sonnet-4-5 and gpt-5.6-luna (ranked first).
|
|
36
|
+
const manyModels: ModelInfo[] = [
|
|
37
|
+
// anthropic (5)
|
|
38
|
+
{ provider: "anthropic", id: "claude-sonnet-4-5", label: "anthropic/claude-sonnet-4-5", name: "sonnet", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
39
|
+
{ provider: "anthropic", id: "claude-opus-4", label: "anthropic/claude-opus-4", name: "opus", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
40
|
+
{ provider: "anthropic", id: "claude-haiku-5", label: "anthropic/claude-haiku-5", name: "haiku", reasoning: false, contextWindow: 200000, thinkingLevels: [] },
|
|
41
|
+
{ provider: "anthropic", id: "claude-fable-5", label: "anthropic/claude-fable-5", name: "fable", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
42
|
+
{ provider: "anthropic", id: "claude-solomon", label: "anthropic/claude-solomon", name: "solomon", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
43
|
+
// openai (4)
|
|
44
|
+
{ provider: "openai", id: "gpt-5.6-sol", label: "openai/gpt-5.6-sol", name: "sol", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
45
|
+
{ provider: "openai", id: "gpt-5.6-luna", label: "openai/gpt-5.6-luna", name: "luna", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
46
|
+
{ provider: "openai", id: "gpt-5.5", label: "openai/gpt-5.5", name: "gpt55", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
47
|
+
{ provider: "openai", id: "o3", label: "openai/o3", name: "o3", reasoning: true, contextWindow: 200000, thinkingLevels: ["high"] },
|
|
48
|
+
// google (3)
|
|
49
|
+
{ provider: "google", id: "gemini-3-pro", label: "google/gemini-3-pro", name: "pro", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "xhigh"] },
|
|
50
|
+
{ provider: "google", id: "gemini-3-flash", label: "google/gemini-3-flash", name: "flash", reasoning: true, contextWindow: 200000, thinkingLevels: ["high", "medium"] },
|
|
51
|
+
{ provider: "google", id: "gemma-3", label: "google/gemma-3", name: "gemma", reasoning: false, contextWindow: 200000, thinkingLevels: [] },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function seed(over: Partial<RvParsed> = {}): RvParsed {
|
|
55
|
+
return {
|
|
56
|
+
strategy: "loop",
|
|
57
|
+
mode: "code",
|
|
58
|
+
keepSession: false,
|
|
59
|
+
noStream: false,
|
|
60
|
+
modelsOnly: false,
|
|
61
|
+
target: "",
|
|
62
|
+
...over,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scriptedUi(answers: {
|
|
67
|
+
selects?: string[];
|
|
68
|
+
inputs?: string[];
|
|
69
|
+
confirms?: boolean[];
|
|
70
|
+
customModelPickerResults?: (string | "__skip__" | undefined)[];
|
|
71
|
+
}): InteractiveUi & { log: string[] } {
|
|
72
|
+
const selects = [...(answers.selects ?? [])];
|
|
73
|
+
const inputs = [...(answers.inputs ?? [])];
|
|
74
|
+
const confirms = [...(answers.confirms ?? [])];
|
|
75
|
+
const pickerResults = [...(answers.customModelPickerResults ?? [])];
|
|
76
|
+
const log: string[] = [];
|
|
77
|
+
return {
|
|
78
|
+
log,
|
|
79
|
+
async select(title, options) {
|
|
80
|
+
log.push(`select:${title} :: ${options.slice(0, 3).join(" | ")}`);
|
|
81
|
+
return selects.shift() ?? options[0];
|
|
82
|
+
},
|
|
83
|
+
async input(title) {
|
|
84
|
+
log.push(`input:${title}`);
|
|
85
|
+
return inputs.shift() ?? "@src";
|
|
86
|
+
},
|
|
87
|
+
async confirm(title, message) {
|
|
88
|
+
log.push(`confirm:${title}`);
|
|
89
|
+
log.push(message.split("\n")[0] ?? "");
|
|
90
|
+
return confirms.shift() ?? true;
|
|
91
|
+
},
|
|
92
|
+
notify(message) {
|
|
93
|
+
log.push(`notify:${message}`);
|
|
94
|
+
},
|
|
95
|
+
...(answers.customModelPickerResults
|
|
96
|
+
? {
|
|
97
|
+
async customModelPicker(input: { title: string; ranked: unknown[]; allowSkip: boolean }) {
|
|
98
|
+
log.push(`customModelPicker:${input.title} :: ranked=${input.ranked.length} skip=${input.allowSkip}`);
|
|
99
|
+
return pickerResults.shift();
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test("shouldRunInteractiveWizard triggers on empty or --interactive", () => {
|
|
107
|
+
assert.equal(shouldRunInteractiveWizard("", seed()), true);
|
|
108
|
+
assert.equal(shouldRunInteractiveWizard("interactive", seed()), true);
|
|
109
|
+
assert.equal(shouldRunInteractiveWizard("--interactive @src", seed({ target: "@src" })), true);
|
|
110
|
+
assert.equal(shouldRunInteractiveWizard("-i @src", seed({ target: "@src" })), true);
|
|
111
|
+
assert.equal(shouldRunInteractiveWizard("@src -i", seed({ target: "@src" })), true);
|
|
112
|
+
assert.equal(shouldRunInteractiveWizard("--reviewers 3 @src", seed({ target: "@src", reviewers: 3 })), false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("stripInteractiveToken removes the trigger token", () => {
|
|
116
|
+
assert.equal(stripInteractiveToken("interactive @src"), "@src");
|
|
117
|
+
assert.equal(stripInteractiveToken("--interactive"), "");
|
|
118
|
+
assert.equal(stripInteractiveToken("@src --interactive"), "@src");
|
|
119
|
+
assert.equal(stripInteractiveToken("-i @src"), "@src");
|
|
120
|
+
assert.equal(stripInteractiveToken("@src -i"), "@src");
|
|
121
|
+
assert.equal(stripInteractiveToken("--reviewers 3 -i @src"), "--reviewers 3 @src");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("wizard chooses mode before asking for a missing natural-language target", async () => {
|
|
125
|
+
const ui = scriptedUi({
|
|
126
|
+
selects: ["plan review (plan)"],
|
|
127
|
+
inputs: ["review the auth design"],
|
|
128
|
+
confirms: [true],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
132
|
+
strategy: "panel",
|
|
133
|
+
seed: seed({ strategy: "panel", reviewers: 1, model: "openai-codex/gpt-5.6-sol" }),
|
|
134
|
+
models,
|
|
135
|
+
locale: "en",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.ok(result);
|
|
139
|
+
assert.equal(result!.mode, "plan");
|
|
140
|
+
assert.equal(result!.target, "review the auth design");
|
|
141
|
+
const modeStep = ui.log.findIndex((entry) => entry === "select:Review mode :: code review (code) | plan review (plan) | challenge review (challenge)");
|
|
142
|
+
const targetStep = ui.log.findIndex((entry) => entry === "input:Review target (natural language or @path)");
|
|
143
|
+
assert.ok(modeStep >= 0 && targetStep >= 0 && modeStep < targetStep, ui.log.join("\n"));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("wizard assigns per-reviewer models through select dialogs", async () => {
|
|
147
|
+
const ui = scriptedUi({
|
|
148
|
+
inputs: ["@src"],
|
|
149
|
+
selects: [
|
|
150
|
+
"code review (code)",
|
|
151
|
+
"Single gate (fix then re-run /rv-loop) · recommended",
|
|
152
|
+
"1 · host fix point (recommended)",
|
|
153
|
+
"Custom reviewer count 2–8 (r1..rN, pick models)",
|
|
154
|
+
"3",
|
|
155
|
+
"Pick separately for each reviewer",
|
|
156
|
+
"openai-codex/gpt-5.6-sol",
|
|
157
|
+
"xhigh",
|
|
158
|
+
"anthropic/claude-sonnet-4-5",
|
|
159
|
+
"high",
|
|
160
|
+
"openai-codex/gpt-5.6-sol",
|
|
161
|
+
"xhigh",
|
|
162
|
+
"quorum · at least min-agree agree (default)",
|
|
163
|
+
"2",
|
|
164
|
+
],
|
|
165
|
+
confirms: [true, true],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
169
|
+
strategy: "loop",
|
|
170
|
+
seed: seed(),
|
|
171
|
+
models,
|
|
172
|
+
locale: "en",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
assert.ok(result);
|
|
176
|
+
assert.equal(result!.target, "@src");
|
|
177
|
+
assert.equal(result!.reviewers, 3);
|
|
178
|
+
assert.equal(result!.maxRounds, 1); // wizard always asks; first option kept
|
|
179
|
+
assert.equal(result!.until, undefined);
|
|
180
|
+
assert.equal(result!.consensus, "quorum");
|
|
181
|
+
assert.equal(result!.minAgree, 2);
|
|
182
|
+
assert.deepEqual(result!.reviewerModels, [
|
|
183
|
+
"r1=openai-codex/gpt-5.6-sol:xhigh",
|
|
184
|
+
"r2=anthropic/claude-sonnet-4-5:high",
|
|
185
|
+
"r3=openai-codex/gpt-5.6-sol:xhigh",
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("wizard keeps an explicit single-reviewer non-panel choice", async () => {
|
|
190
|
+
const ui = scriptedUi({
|
|
191
|
+
inputs: ["@src"],
|
|
192
|
+
selects: [
|
|
193
|
+
"code review (code)",
|
|
194
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
195
|
+
"Skip (Pi default)",
|
|
196
|
+
],
|
|
197
|
+
confirms: [true],
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
201
|
+
strategy: "panel",
|
|
202
|
+
seed: seed({ strategy: "panel" }),
|
|
203
|
+
models,
|
|
204
|
+
locale: "en",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
assert.ok(result);
|
|
208
|
+
assert.equal(result!.reviewers, 1);
|
|
209
|
+
assert.equal(result!.panel, undefined);
|
|
210
|
+
assert.equal(result!.consensus, undefined);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("wizard can use code-experts preset and shared model", async () => {
|
|
214
|
+
const ui = scriptedUi({
|
|
215
|
+
inputs: ["review auth"],
|
|
216
|
+
selects: [
|
|
217
|
+
"code review (code)",
|
|
218
|
+
"Single gate (fix then re-run /rv-loop) · recommended",
|
|
219
|
+
"1 · host fix point (recommended)",
|
|
220
|
+
"Preset code-experts (correctness/security/testing)",
|
|
221
|
+
"Same model for all reviewers",
|
|
222
|
+
"openai-codex/gpt-5.6-sol",
|
|
223
|
+
"xhigh",
|
|
224
|
+
"quorum · at least min-agree agree (default)",
|
|
225
|
+
"2",
|
|
226
|
+
],
|
|
227
|
+
confirms: [true, true],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
231
|
+
strategy: "loop",
|
|
232
|
+
seed: seed(),
|
|
233
|
+
models,
|
|
234
|
+
locale: "en",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
assert.ok(result);
|
|
238
|
+
assert.equal(result!.panel, "code-experts");
|
|
239
|
+
assert.deepEqual(result!.reviewerModels, [
|
|
240
|
+
"correctness=openai-codex/gpt-5.6-sol:xhigh",
|
|
241
|
+
"security=openai-codex/gpt-5.6-sol:xhigh",
|
|
242
|
+
"testing=openai-codex/gpt-5.6-sol:xhigh",
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("wizard can select until-clean with a hard budget", async () => {
|
|
247
|
+
const ui = scriptedUi({
|
|
248
|
+
inputs: ["@src"],
|
|
249
|
+
selects: [
|
|
250
|
+
"code review (code)",
|
|
251
|
+
"until clean · review→fix→re-review until clean (hard budget)",
|
|
252
|
+
"10 · default",
|
|
253
|
+
"Preset code-experts (correctness/security/testing)",
|
|
254
|
+
"Same model for all reviewers",
|
|
255
|
+
"openai-codex/gpt-5.6-sol",
|
|
256
|
+
"xhigh",
|
|
257
|
+
"quorum · at least min-agree agree (default)",
|
|
258
|
+
"2",
|
|
259
|
+
],
|
|
260
|
+
confirms: [true, true],
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
264
|
+
strategy: "loop",
|
|
265
|
+
seed: seed(),
|
|
266
|
+
models,
|
|
267
|
+
locale: "en",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
assert.ok(result);
|
|
271
|
+
assert.equal(result!.until, "clean");
|
|
272
|
+
assert.equal(result!.maxRounds, 10);
|
|
273
|
+
assert.equal(result!.panel, "code-experts");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// --- Model picker with a large catalog (> MODEL_PICKER_THRESHOLD) ---
|
|
277
|
+
// These exercise the provider browse / search / ranked-list / skip chooser that
|
|
278
|
+
// replaces the old flat `labels.slice(0, 40)` list. With <= 8 models the wizard
|
|
279
|
+
// still uses the flat list (covered by the tests above with the 2-model fixture).
|
|
280
|
+
|
|
281
|
+
test("wizard model picker browses by provider for a large catalog", async () => {
|
|
282
|
+
const ui = scriptedUi({
|
|
283
|
+
inputs: ["@src"],
|
|
284
|
+
selects: [
|
|
285
|
+
"code review (code)",
|
|
286
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
287
|
+
"Browse by provider (arrows switch provider, enter drills in)",
|
|
288
|
+
"▸ anthropic (5)",
|
|
289
|
+
"anthropic/claude-sonnet-4-5",
|
|
290
|
+
"xhigh",
|
|
291
|
+
],
|
|
292
|
+
confirms: [true],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
296
|
+
strategy: "panel",
|
|
297
|
+
seed: seed({ strategy: "panel" }),
|
|
298
|
+
models: manyModels,
|
|
299
|
+
locale: "en",
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
assert.ok(result);
|
|
303
|
+
assert.equal(result!.reviewers, 1);
|
|
304
|
+
assert.equal(result!.panel, undefined);
|
|
305
|
+
assert.equal(result!.model, "anthropic/claude-sonnet-4-5");
|
|
306
|
+
assert.equal(result!.thinking, "xhigh");
|
|
307
|
+
// The provider picker was actually visited.
|
|
308
|
+
assert.ok(ui.log.some((e) => e.startsWith("select:Model · pick provider")), ui.log.join("\n"));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("wizard model picker can switch providers via the back entry", async () => {
|
|
312
|
+
const ui = scriptedUi({
|
|
313
|
+
inputs: ["@src"],
|
|
314
|
+
selects: [
|
|
315
|
+
"code review (code)",
|
|
316
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
317
|
+
"Browse by provider (arrows switch provider, enter drills in)",
|
|
318
|
+
"▸ anthropic (5)",
|
|
319
|
+
"← Switch provider",
|
|
320
|
+
" openai (4)",
|
|
321
|
+
"openai/gpt-5.6-luna",
|
|
322
|
+
"xhigh",
|
|
323
|
+
],
|
|
324
|
+
confirms: [true],
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
328
|
+
strategy: "panel",
|
|
329
|
+
seed: seed({ strategy: "panel" }),
|
|
330
|
+
models: manyModels,
|
|
331
|
+
locale: "en",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
assert.ok(result);
|
|
335
|
+
assert.equal(result!.model, "openai/gpt-5.6-luna");
|
|
336
|
+
assert.equal(result!.thinking, "xhigh");
|
|
337
|
+
// Provider list was shown twice (initial + after switching back).
|
|
338
|
+
const providerPicks = ui.log.filter((e) => e.startsWith("select:Model · pick provider"));
|
|
339
|
+
assert.equal(providerPicks.length, 2, ui.log.join("\n"));
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("wizard model picker searches by keyword", async () => {
|
|
343
|
+
const ui = scriptedUi({
|
|
344
|
+
inputs: ["@src", "claude"],
|
|
345
|
+
selects: [
|
|
346
|
+
"code review (code)",
|
|
347
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
348
|
+
"Search models (type a provider or model keyword)",
|
|
349
|
+
"anthropic/claude-opus-4",
|
|
350
|
+
"high",
|
|
351
|
+
],
|
|
352
|
+
confirms: [true],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
356
|
+
strategy: "panel",
|
|
357
|
+
seed: seed({ strategy: "panel" }),
|
|
358
|
+
models: manyModels,
|
|
359
|
+
locale: "en",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
assert.ok(result);
|
|
363
|
+
assert.equal(result!.model, "anthropic/claude-opus-4");
|
|
364
|
+
assert.equal(result!.thinking, "high");
|
|
365
|
+
assert.ok(ui.log.some((e) => e.startsWith("input:Model · search")), ui.log.join("\n"));
|
|
366
|
+
assert.ok(ui.log.some((e) => e.startsWith("select:Model · results (5)")), ui.log.join("\n"));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("wizard model picker re-prompts when a search has no matches", async () => {
|
|
370
|
+
const ui = scriptedUi({
|
|
371
|
+
inputs: ["@src", "zzz", "claude"],
|
|
372
|
+
selects: [
|
|
373
|
+
"code review (code)",
|
|
374
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
375
|
+
"Search models (type a provider or model keyword)",
|
|
376
|
+
"anthropic/claude-sonnet-4-5",
|
|
377
|
+
"xhigh",
|
|
378
|
+
],
|
|
379
|
+
confirms: [true],
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
383
|
+
strategy: "panel",
|
|
384
|
+
seed: seed({ strategy: "panel" }),
|
|
385
|
+
models: manyModels,
|
|
386
|
+
locale: "en",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
assert.ok(result);
|
|
390
|
+
assert.equal(result!.model, "anthropic/claude-sonnet-4-5");
|
|
391
|
+
assert.ok(ui.log.some((e) => e.includes(`No models match "zzz"`)), ui.log.join("\n"));
|
|
392
|
+
// Search input was called twice (first miss + retry).
|
|
393
|
+
const searchInputs = ui.log.filter((e) => e.startsWith("input:Model · search"));
|
|
394
|
+
assert.equal(searchInputs.length, 2, ui.log.join("\n"));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("wizard model picker can use the full ranked list", async () => {
|
|
398
|
+
const ui = scriptedUi({
|
|
399
|
+
inputs: ["@src"],
|
|
400
|
+
selects: [
|
|
401
|
+
"code review (code)",
|
|
402
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
403
|
+
"Ranked list (all models)",
|
|
404
|
+
"openai/gpt-5.6-luna",
|
|
405
|
+
"xhigh",
|
|
406
|
+
],
|
|
407
|
+
confirms: [true],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
411
|
+
strategy: "panel",
|
|
412
|
+
seed: seed({ strategy: "panel" }),
|
|
413
|
+
models: manyModels,
|
|
414
|
+
locale: "en",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
assert.ok(result);
|
|
418
|
+
assert.equal(result!.model, "openai/gpt-5.6-luna");
|
|
419
|
+
assert.equal(result!.thinking, "xhigh");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("wizard model picker supports skip for a large catalog", async () => {
|
|
423
|
+
const ui = scriptedUi({
|
|
424
|
+
inputs: ["@src"],
|
|
425
|
+
selects: [
|
|
426
|
+
"code review (code)",
|
|
427
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
428
|
+
"Skip (Pi default)",
|
|
429
|
+
],
|
|
430
|
+
confirms: [true],
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
434
|
+
strategy: "panel",
|
|
435
|
+
seed: seed({ strategy: "panel" }),
|
|
436
|
+
models: manyModels,
|
|
437
|
+
locale: "en",
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
assert.ok(result);
|
|
441
|
+
assert.equal(result!.model, undefined);
|
|
442
|
+
assert.equal(result!.thinking, undefined);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// --- Inline searchable model picker (custom component, TUI path) ---
|
|
446
|
+
// When the host provides `customModelPicker`, the wizard uses the inline
|
|
447
|
+
// searchable picker for every catalog size (it supersedes the select-based
|
|
448
|
+
// flow). These tests stub `customModelPicker` and verify the wizard maps its
|
|
449
|
+
// result to model / skip / cancel correctly.
|
|
450
|
+
|
|
451
|
+
test("inline picker: chosen label sets model + thinking", async () => {
|
|
452
|
+
const ui = scriptedUi({
|
|
453
|
+
inputs: ["@src"],
|
|
454
|
+
selects: [
|
|
455
|
+
"code review (code)",
|
|
456
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
457
|
+
// thinking select still happens after the picker resolves to a label
|
|
458
|
+
"xhigh",
|
|
459
|
+
],
|
|
460
|
+
confirms: [true],
|
|
461
|
+
customModelPickerResults: ["anthropic/claude-sonnet-4-5"],
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
465
|
+
strategy: "panel",
|
|
466
|
+
seed: seed({ strategy: "panel" }),
|
|
467
|
+
models: manyModels,
|
|
468
|
+
locale: "en",
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
assert.ok(result);
|
|
472
|
+
assert.equal(result!.model, "anthropic/claude-sonnet-4-5");
|
|
473
|
+
assert.equal(result!.thinking, "xhigh");
|
|
474
|
+
assert.ok(ui.log.some((e) => e.startsWith("customModelPicker:Model ")), ui.log.join("\n"));
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("inline picker: skip sentinel leaves model unset", async () => {
|
|
478
|
+
const ui = scriptedUi({
|
|
479
|
+
inputs: ["@src"],
|
|
480
|
+
selects: [
|
|
481
|
+
"code review (code)",
|
|
482
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
483
|
+
],
|
|
484
|
+
confirms: [true],
|
|
485
|
+
customModelPickerResults: ["__skip__"],
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
489
|
+
strategy: "panel",
|
|
490
|
+
seed: seed({ strategy: "panel" }),
|
|
491
|
+
models: manyModels,
|
|
492
|
+
locale: "en",
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
assert.ok(result);
|
|
496
|
+
assert.equal(result!.model, undefined);
|
|
497
|
+
assert.equal(result!.thinking, undefined);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("inline picker: cancel (undefined) cancels the whole wizard", async () => {
|
|
501
|
+
const ui = scriptedUi({
|
|
502
|
+
inputs: ["@src"],
|
|
503
|
+
selects: [
|
|
504
|
+
"code review (code)",
|
|
505
|
+
"Single reviewer non-panel (no consensus; shell single review)",
|
|
506
|
+
],
|
|
507
|
+
confirms: [true],
|
|
508
|
+
customModelPickerResults: [undefined],
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const result = await runRvInteractiveWizard(ui, {
|
|
512
|
+
strategy: "panel",
|
|
513
|
+
seed: seed({ strategy: "panel" }),
|
|
514
|
+
models: manyModels,
|
|
515
|
+
locale: "en",
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
assert.equal(result, undefined);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("inline picker: per-reviewer models resolve through repeated picker calls", async () => {
|
|
522
|
+
const uiPanel = scriptedUi({
|
|
523
|
+
inputs: ["@src"],
|
|
524
|
+
selects: [
|
|
525
|
+
"code review (code)",
|
|
526
|
+
"Single gate (fix then re-run /rv-loop) · recommended",
|
|
527
|
+
"1 · host fix point (recommended)",
|
|
528
|
+
"Custom reviewer count 2–8 (r1..rN, pick models)",
|
|
529
|
+
"3",
|
|
530
|
+
"Pick separately for each reviewer",
|
|
531
|
+
// thinking selects for r1, r2, r3
|
|
532
|
+
"xhigh",
|
|
533
|
+
"high",
|
|
534
|
+
"xhigh",
|
|
535
|
+
"quorum · at least min-agree agree (default)",
|
|
536
|
+
"2",
|
|
537
|
+
],
|
|
538
|
+
confirms: [true, true],
|
|
539
|
+
customModelPickerResults: [
|
|
540
|
+
"openai/gpt-5.6-luna",
|
|
541
|
+
"anthropic/claude-opus-4",
|
|
542
|
+
"google/gemini-3-pro",
|
|
543
|
+
],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const result = await runRvInteractiveWizard(uiPanel, {
|
|
547
|
+
strategy: "loop",
|
|
548
|
+
seed: seed(),
|
|
549
|
+
models: manyModels,
|
|
550
|
+
locale: "en",
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
assert.ok(result);
|
|
554
|
+
assert.deepEqual(result!.reviewerModels, [
|
|
555
|
+
"r1=openai/gpt-5.6-luna:xhigh",
|
|
556
|
+
"r2=anthropic/claude-opus-4:high",
|
|
557
|
+
"r3=google/gemini-3-pro:xhigh",
|
|
558
|
+
]);
|
|
559
|
+
// The picker was invoked once per reviewer.
|
|
560
|
+
const pickerCalls = uiPanel.log.filter((e) => e.startsWith("customModelPicker:"));
|
|
561
|
+
assert.equal(pickerCalls.length, 3, uiPanel.log.join("\n"));
|
|
562
|
+
});
|