@duckmind/dm-darwin-x64 0.33.1 → 0.33.4
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/dm +0 -0
- package/extensions/.dm-extensions.json +1 -34
- package/extensions/greedysearch-dm/bin/cdp.mjs +1 -1
- package/package.json +1 -1
- package/theme/theme-alps.json +68 -77
- package/extensions/dm-ask/CHANGELOG.md +0 -149
- package/extensions/dm-ask/LICENSE +0 -21
- package/extensions/dm-ask/README.md +0 -94
- package/extensions/dm-ask/ask-ui.ts +0 -238
- package/extensions/dm-ask/ask.ts +0 -348
- package/extensions/dm-ask/btw-ui.test.ts +0 -270
- package/extensions/dm-ask/btw.command.test.ts +0 -152
- package/extensions/dm-ask/btw.test.ts +0 -340
- package/extensions/dm-ask/docs/cover.png +0 -0
- package/extensions/dm-ask/docs/cover.svg +0 -70
- package/extensions/dm-ask/docs/overlay.jpg +0 -0
- package/extensions/dm-ask/docs/vertical-cover.png +0 -0
- package/extensions/dm-ask/docs/vertical-cover.svg +0 -147
- package/extensions/dm-ask/index.ts +0 -17
- package/extensions/dm-ask/package.json +0 -44
- package/extensions/dm-ask/prompts/ask-system.txt +0 -9
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { type TUI, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import { makeTui } from "@juicesharp/rpiv-test-utils";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import type { AskTurn } from "./ask.js";
|
|
6
|
-
import { AskOverlayController, showAskOverlay } from "./ask-ui.js";
|
|
7
|
-
|
|
8
|
-
const identityTheme = {
|
|
9
|
-
fg: (_c: string, s: string) => s,
|
|
10
|
-
bg: (_c: string, s: string) => s,
|
|
11
|
-
bold: (s: string) => s,
|
|
12
|
-
strikethrough: (s: string) => s,
|
|
13
|
-
} as unknown as Theme;
|
|
14
|
-
|
|
15
|
-
function makeTurn(q: string, a = "ans"): AskTurn {
|
|
16
|
-
return {
|
|
17
|
-
userMessage: { role: "user", content: q, timestamp: 0 },
|
|
18
|
-
assistantMessage: {
|
|
19
|
-
role: "assistant",
|
|
20
|
-
content: [{ type: "text", text: a }],
|
|
21
|
-
api: "anthropic" as never,
|
|
22
|
-
provider: "anthropic" as never,
|
|
23
|
-
model: "m",
|
|
24
|
-
usage: {} as never,
|
|
25
|
-
stopReason: "done" as never,
|
|
26
|
-
timestamp: 0,
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeController(opts: { question?: string; history?: AskTurn[]; tui?: TUI; rows?: number } = {}) {
|
|
32
|
-
const tui = opts.tui ?? (makeTui() as unknown as TUI);
|
|
33
|
-
(tui as unknown as { terminal: { rows: number } }).terminal = { rows: opts.rows ?? 24 };
|
|
34
|
-
const done = vi.fn();
|
|
35
|
-
const controller = new AbortController();
|
|
36
|
-
const onClearHistory = vi.fn();
|
|
37
|
-
const ctl = new AskOverlayController(
|
|
38
|
-
opts.question ?? "what?",
|
|
39
|
-
opts.history ?? [],
|
|
40
|
-
identityTheme,
|
|
41
|
-
tui,
|
|
42
|
-
done,
|
|
43
|
-
controller,
|
|
44
|
-
onClearHistory,
|
|
45
|
-
);
|
|
46
|
-
return { ctl, tui, done, controller, onClearHistory };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
vi.restoreAllMocks();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("AskOverlayController — initial (pending) render", () => {
|
|
54
|
-
it("contains the banner, echo line, pending glyph, and dismiss footer", () => {
|
|
55
|
-
const { ctl } = makeController({ question: "hello?" });
|
|
56
|
-
const out = ctl.render(80).join("\n");
|
|
57
|
-
expect(out).toContain("/ask hello?");
|
|
58
|
-
expect(out).toContain("…"); // PENDING_GLYPH
|
|
59
|
-
expect(out).toContain("Esc to dismiss");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("does NOT show 'scroll' or 'clear' hints when pending + no history", () => {
|
|
63
|
-
const { ctl } = makeController({ question: "q" });
|
|
64
|
-
const out = ctl.render(80).join("\n");
|
|
65
|
-
expect(out).not.toContain("↑/↓ to scroll");
|
|
66
|
-
expect(out).not.toContain("x to clear history");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("shows 'x to clear history' hint when history is non-empty", () => {
|
|
70
|
-
const { ctl } = makeController({ history: [makeTurn("prev")] });
|
|
71
|
-
const out = ctl.render(80).join("\n");
|
|
72
|
-
expect(out).toContain("x to clear history");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("AskOverlayController — setAnswer", () => {
|
|
77
|
-
it("replaces pending glyph with the answer text", () => {
|
|
78
|
-
const { ctl, tui } = makeController();
|
|
79
|
-
ctl.setAnswer("forty-two");
|
|
80
|
-
const out = ctl.render(80).join("\n");
|
|
81
|
-
expect(out).toContain("forty-two");
|
|
82
|
-
expect(out).not.toContain("…");
|
|
83
|
-
expect(tui.requestRender).toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("enables the 'scroll' footer hint once the mode is no longer pending", () => {
|
|
87
|
-
const { ctl } = makeController();
|
|
88
|
-
ctl.setAnswer("a");
|
|
89
|
-
expect(ctl.render(80).join("\n")).toContain("↑/↓ to scroll");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("wraps multi-line answers into the answer body", () => {
|
|
93
|
-
const { ctl } = makeController();
|
|
94
|
-
ctl.setAnswer("line1\nline2\nline3");
|
|
95
|
-
const out = ctl.render(80);
|
|
96
|
-
expect(out.some((l) => l.includes("line1"))).toBe(true);
|
|
97
|
-
expect(out.some((l) => l.includes("line2"))).toBe(true);
|
|
98
|
-
expect(out.some((l) => l.includes("line3"))).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("AskOverlayController — setError", () => {
|
|
103
|
-
it("renders the error message in the answer slot", () => {
|
|
104
|
-
const { ctl } = makeController();
|
|
105
|
-
ctl.setError("boom: nope");
|
|
106
|
-
const out = ctl.render(80).join("\n");
|
|
107
|
-
expect(out).toContain("boom: nope");
|
|
108
|
-
expect(out).not.toContain("…");
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("AskOverlayController — handleInput", () => {
|
|
113
|
-
it("Esc aborts the controller and resolves done()", () => {
|
|
114
|
-
const { ctl, controller, done } = makeController();
|
|
115
|
-
ctl.handleInput("\u001b");
|
|
116
|
-
expect(controller.signal.aborted).toBe(true);
|
|
117
|
-
expect(done).toHaveBeenCalled();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("'x' clears in-memory history and invokes onClearHistory", () => {
|
|
121
|
-
const { ctl, onClearHistory, tui } = makeController({ history: [makeTurn("a"), makeTurn("b")] });
|
|
122
|
-
ctl.handleInput("x");
|
|
123
|
-
expect(onClearHistory).toHaveBeenCalledTimes(1);
|
|
124
|
-
const out = ctl.render(80).join("\n");
|
|
125
|
-
expect(out).not.toContain("/ask a");
|
|
126
|
-
expect(out).not.toContain("/ask b");
|
|
127
|
-
expect(out).not.toContain("x to clear history");
|
|
128
|
-
expect(tui.requestRender).toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("unknown keys do not abort or clear", () => {
|
|
132
|
-
const { ctl, controller, done, onClearHistory } = makeController();
|
|
133
|
-
ctl.handleInput("z");
|
|
134
|
-
expect(controller.signal.aborted).toBe(false);
|
|
135
|
-
expect(done).not.toHaveBeenCalled();
|
|
136
|
-
expect(onClearHistory).not.toHaveBeenCalled();
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe("AskOverlayController — scroll + clipping", () => {
|
|
141
|
-
it("render() returns all natural lines when within maxRows", () => {
|
|
142
|
-
const { ctl } = makeController({ rows: 100 });
|
|
143
|
-
ctl.setAnswer("answer-body");
|
|
144
|
-
const lines = ctl.render(80);
|
|
145
|
-
// banner + blank + 0 history + echo + blank + 1 answer + blank + footer = 7
|
|
146
|
-
expect(lines.length).toBe(7);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("clips top when content overflows terminal height; scroll↑ reveals older history", () => {
|
|
150
|
-
// Use distinct non-overlapping markers so substring matches are unambiguous.
|
|
151
|
-
const history: AskTurn[] = Array.from({ length: 20 }, (_, i) => makeTurn(`mark-${i + 1}-end`));
|
|
152
|
-
const { ctl } = makeController({ history, rows: 10 });
|
|
153
|
-
ctl.setAnswer("A");
|
|
154
|
-
const base = ctl.render(80);
|
|
155
|
-
const maxRows = Math.floor(10 * 0.85); // 8
|
|
156
|
-
expect(base.length).toBe(maxRows);
|
|
157
|
-
// Bottom-anchored: footer + answer visible; earliest history hidden
|
|
158
|
-
expect(base.join("\n")).not.toContain("mark-1-end");
|
|
159
|
-
expect(base.join("\n")).toContain("mark-20-end");
|
|
160
|
-
expect(base.join("\n")).toContain("Esc to dismiss");
|
|
161
|
-
// Scroll up reveals older history at the top.
|
|
162
|
-
ctl.handleInput("\u001b[A");
|
|
163
|
-
const scrolled = ctl.render(80);
|
|
164
|
-
expect(scrolled.length).toBe(maxRows);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("scroll↓ at bottom stays clamped (no throw, still renders maxRows)", () => {
|
|
168
|
-
const history: AskTurn[] = Array.from({ length: 20 }, (_, i) => makeTurn(`mark-${i + 1}-end`));
|
|
169
|
-
const { ctl } = makeController({ history, rows: 10 });
|
|
170
|
-
ctl.setAnswer("A");
|
|
171
|
-
ctl.handleInput("\u001b[B"); // down
|
|
172
|
-
const out = ctl.render(80);
|
|
173
|
-
const maxRows = Math.floor(10 * 0.85);
|
|
174
|
-
expect(out.length).toBe(maxRows);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("invalidate() is a callable no-op", () => {
|
|
178
|
-
const { ctl } = makeController();
|
|
179
|
-
expect(() => ctl.invalidate()).not.toThrow();
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("AskOverlayController — banner + echo formatting", () => {
|
|
184
|
-
it("banner is padded to full visible width", () => {
|
|
185
|
-
const { ctl } = makeController({ question: "q" });
|
|
186
|
-
const banner = ctl.render(40)[0];
|
|
187
|
-
expect(visibleWidth(banner)).toBe(40);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("truncates long questions in the banner with ellipsis", () => {
|
|
191
|
-
const long = "a".repeat(200);
|
|
192
|
-
const { ctl } = makeController({ question: long });
|
|
193
|
-
const banner = ctl.render(40)[0];
|
|
194
|
-
expect(visibleWidth(banner)).toBe(40);
|
|
195
|
-
expect(banner).toContain("…");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("history echo uses '/ask ' prefix and trims whitespace", () => {
|
|
199
|
-
const { ctl } = makeController({ history: [makeTurn(" multi\nline q ")] });
|
|
200
|
-
const out = ctl.render(80).join("\n");
|
|
201
|
-
expect(out).toContain("/ask multi line q");
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe("showAskOverlay — factory wiring", () => {
|
|
206
|
-
it("invokes ctx.ui.custom with overlay options and resolves controllerReady with the AskOverlayController", async () => {
|
|
207
|
-
const requestRender = vi.fn();
|
|
208
|
-
const tui = { requestRender, terminal: { rows: 24 } } as unknown as TUI;
|
|
209
|
-
const custom = vi.fn((factory: unknown, opts: unknown) => {
|
|
210
|
-
const f = factory as (
|
|
211
|
-
tui: TUI,
|
|
212
|
-
theme: Theme,
|
|
213
|
-
kb: undefined,
|
|
214
|
-
done: (v: undefined) => void,
|
|
215
|
-
) => AskOverlayController;
|
|
216
|
-
const ctl = f(tui, identityTheme, undefined, () => {});
|
|
217
|
-
// Keep `opts` addressable for the assertion below.
|
|
218
|
-
(custom as unknown as { lastOpts: unknown }).lastOpts = opts;
|
|
219
|
-
return new Promise<void>(() => {
|
|
220
|
-
// keep pending so we can inspect the controller
|
|
221
|
-
void ctl;
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
const ctx = { ui: { custom } } as never;
|
|
225
|
-
|
|
226
|
-
const { controllerReady } = showAskOverlay({
|
|
227
|
-
ctx,
|
|
228
|
-
question: "q",
|
|
229
|
-
history: [],
|
|
230
|
-
controller: new AbortController(),
|
|
231
|
-
onClearHistory: vi.fn(),
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
const ctl = await controllerReady;
|
|
235
|
-
expect(ctl).toBeInstanceOf(AskOverlayController);
|
|
236
|
-
expect(custom).toHaveBeenCalledTimes(1);
|
|
237
|
-
const opts = (custom as unknown as { lastOpts: { overlay: boolean; overlayOptions: unknown } }).lastOpts;
|
|
238
|
-
expect(opts).toMatchObject({ overlay: true });
|
|
239
|
-
expect(opts.overlayOptions).toMatchObject({ anchor: "bottom-center" });
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("controller returned by the factory is the same one exposed via controllerReady", async () => {
|
|
243
|
-
let factoryCtl: AskOverlayController | undefined;
|
|
244
|
-
const custom = vi.fn((factory: unknown) => {
|
|
245
|
-
const f = factory as (
|
|
246
|
-
tui: TUI,
|
|
247
|
-
theme: Theme,
|
|
248
|
-
kb: undefined,
|
|
249
|
-
done: (v: undefined) => void,
|
|
250
|
-
) => AskOverlayController;
|
|
251
|
-
factoryCtl = f(
|
|
252
|
-
{ requestRender: vi.fn(), terminal: { rows: 24 } } as unknown as TUI,
|
|
253
|
-
identityTheme,
|
|
254
|
-
undefined,
|
|
255
|
-
() => {},
|
|
256
|
-
);
|
|
257
|
-
return new Promise<void>(() => {});
|
|
258
|
-
});
|
|
259
|
-
const ctx = { ui: { custom } } as never;
|
|
260
|
-
const { controllerReady } = showAskOverlay({
|
|
261
|
-
ctx,
|
|
262
|
-
question: "q",
|
|
263
|
-
history: [],
|
|
264
|
-
controller: new AbortController(),
|
|
265
|
-
onClearHistory: vi.fn(),
|
|
266
|
-
});
|
|
267
|
-
const ctl = await controllerReady;
|
|
268
|
-
expect(ctl).toBe(factoryCtl);
|
|
269
|
-
});
|
|
270
|
-
});
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
-
import { createMockCtx, createMockDM } from "@juicesharp/rpiv-test-utils";
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
vi.mock("./ask-ui.js", () => ({
|
|
6
|
-
showAskOverlay: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
vi.mock("@mariozechner/pi-ai", async (importOriginal) => {
|
|
10
|
-
const actual = await importOriginal<typeof import("@mariozechner/pi-ai")>();
|
|
11
|
-
return {
|
|
12
|
-
...actual,
|
|
13
|
-
completeSimple: vi.fn(),
|
|
14
|
-
getSupportedThinkingLevels: vi.fn(() => ["off", "minimal", "low", "medium", "high"]),
|
|
15
|
-
};
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
import { completeSimple } from "@mariozechner/pi-ai";
|
|
19
|
-
import { ASK_COMMAND_NAME, ASK_STATE_KEY, registerAskCommand } from "./ask.js";
|
|
20
|
-
import { showAskOverlay } from "./ask-ui.js";
|
|
21
|
-
|
|
22
|
-
const model = { provider: "a", id: "m" } as unknown as Model<Api>;
|
|
23
|
-
|
|
24
|
-
type OverlayCtl = { setAnswer: ReturnType<typeof vi.fn>; setError: ReturnType<typeof vi.fn> };
|
|
25
|
-
|
|
26
|
-
function stubOverlay(): OverlayCtl {
|
|
27
|
-
const ctl: OverlayCtl = { setAnswer: vi.fn(), setError: vi.fn() };
|
|
28
|
-
vi.mocked(showAskOverlay).mockReturnValueOnce({
|
|
29
|
-
overlayPromise: Promise.resolve(),
|
|
30
|
-
controllerReady: Promise.resolve(ctl as never),
|
|
31
|
-
} as never);
|
|
32
|
-
return ctl;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function doneResponse(text: string) {
|
|
36
|
-
return {
|
|
37
|
-
role: "assistant",
|
|
38
|
-
content: [{ type: "text", text }],
|
|
39
|
-
timestamp: Date.now(),
|
|
40
|
-
stopReason: "done",
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
vi.mocked(showAskOverlay).mockReset();
|
|
46
|
-
vi.mocked(completeSimple).mockReset();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
delete (globalThis as Record<symbol, unknown>)[ASK_STATE_KEY];
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
function register() {
|
|
54
|
-
const { DM, captured } = createMockDM();
|
|
55
|
-
registerAskCommand(pi);
|
|
56
|
-
return captured.commands.get(ASK_COMMAND_NAME)!;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe("/ask — early-return branches", () => {
|
|
60
|
-
it("!hasUI notifies error and skips overlay", async () => {
|
|
61
|
-
const cmd = register();
|
|
62
|
-
const ctx = createMockCtx({ hasUI: false, model });
|
|
63
|
-
await cmd.handler("anything", ctx as never);
|
|
64
|
-
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("interactive"), "error");
|
|
65
|
-
expect(showAskOverlay).not.toHaveBeenCalled();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("empty question emits usage warning", async () => {
|
|
69
|
-
const cmd = register();
|
|
70
|
-
const ctx = createMockCtx({ hasUI: true, model });
|
|
71
|
-
await cmd.handler(" ", ctx as never);
|
|
72
|
-
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Usage"), "warning");
|
|
73
|
-
expect(showAskOverlay).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("missing model notifies error", async () => {
|
|
77
|
-
const cmd = register();
|
|
78
|
-
const ctx = createMockCtx({ hasUI: true });
|
|
79
|
-
await cmd.handler("hello?", ctx as never);
|
|
80
|
-
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("active model"), "error");
|
|
81
|
-
expect(showAskOverlay).not.toHaveBeenCalled();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("/ask — happy path", () => {
|
|
86
|
-
it("invokes overlay, awaits executor, pipes answer to setAnswer", async () => {
|
|
87
|
-
const ctl = stubOverlay();
|
|
88
|
-
vi.mocked(completeSimple).mockResolvedValueOnce(doneResponse("42") as never);
|
|
89
|
-
const cmd = register();
|
|
90
|
-
const ctx = createMockCtx({ hasUI: true, model });
|
|
91
|
-
await cmd.handler("what is 6 times 7?", ctx as never);
|
|
92
|
-
expect(showAskOverlay).toHaveBeenCalledTimes(1);
|
|
93
|
-
const params = vi.mocked(showAskOverlay).mock.calls[0][0];
|
|
94
|
-
expect(params.question).toBe("what is 6 times 7?");
|
|
95
|
-
expect(params.history).toEqual([]);
|
|
96
|
-
expect(ctl.setAnswer).toHaveBeenCalledWith("42");
|
|
97
|
-
expect(ctl.setError).not.toHaveBeenCalled();
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("/ask — aborted", () => {
|
|
102
|
-
it("does not touch the overlay controller", async () => {
|
|
103
|
-
const ctl = stubOverlay();
|
|
104
|
-
vi.mocked(completeSimple).mockResolvedValueOnce({
|
|
105
|
-
role: "assistant",
|
|
106
|
-
content: [],
|
|
107
|
-
timestamp: Date.now(),
|
|
108
|
-
stopReason: "aborted",
|
|
109
|
-
} as never);
|
|
110
|
-
const cmd = register();
|
|
111
|
-
const ctx = createMockCtx({ hasUI: true, model });
|
|
112
|
-
await cmd.handler("q", ctx as never);
|
|
113
|
-
expect(ctl.setAnswer).not.toHaveBeenCalled();
|
|
114
|
-
expect(ctl.setError).not.toHaveBeenCalled();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe("/ask — executor failure", () => {
|
|
119
|
-
it("pipes error into setError", async () => {
|
|
120
|
-
const ctl = stubOverlay();
|
|
121
|
-
vi.mocked(completeSimple).mockResolvedValueOnce({
|
|
122
|
-
role: "assistant",
|
|
123
|
-
content: [],
|
|
124
|
-
timestamp: Date.now(),
|
|
125
|
-
stopReason: "error",
|
|
126
|
-
errorMessage: "upstream 502",
|
|
127
|
-
} as never);
|
|
128
|
-
const cmd = register();
|
|
129
|
-
const ctx = createMockCtx({ hasUI: true, model });
|
|
130
|
-
await cmd.handler("q", ctx as never);
|
|
131
|
-
expect(ctl.setError).toHaveBeenCalledWith(expect.stringContaining("upstream 502"));
|
|
132
|
-
expect(ctl.setAnswer).not.toHaveBeenCalled();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("/ask — cross-session hint is rendered after turns accumulate", () => {
|
|
137
|
-
it("second invocation's systemPrompt contains the recent-questions section", async () => {
|
|
138
|
-
stubOverlay();
|
|
139
|
-
vi.mocked(completeSimple).mockResolvedValueOnce(doneResponse("ans1") as never);
|
|
140
|
-
const cmd = register();
|
|
141
|
-
const ctx = createMockCtx({ hasUI: true, model });
|
|
142
|
-
await cmd.handler("first question", ctx as never);
|
|
143
|
-
|
|
144
|
-
stubOverlay();
|
|
145
|
-
vi.mocked(completeSimple).mockResolvedValueOnce(doneResponse("ans2") as never);
|
|
146
|
-
await cmd.handler("second question", ctx as never);
|
|
147
|
-
|
|
148
|
-
const secondSystemPrompt = vi.mocked(completeSimple).mock.calls[1][1].systemPrompt ?? "";
|
|
149
|
-
expect(secondSystemPrompt).toContain("Recent /ask questions across sessions");
|
|
150
|
-
expect(secondSystemPrompt).toContain("first question");
|
|
151
|
-
});
|
|
152
|
-
});
|