@gotgenes/pi-autoformat 0.1.0 → 4.0.3
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/.github/workflows/ci.yml +1 -3
- package/.github/workflows/release-please.yml +29 -0
- package/.markdownlint-cli2.yaml +14 -2
- package/.pi/extensions/pi-autoformat/config.json +3 -6
- package/.pi/prompts/README.md +59 -0
- package/.pi/prompts/plan-issue.md +64 -0
- package/.pi/prompts/retro.md +144 -0
- package/.pi/prompts/ship-issue.md +77 -0
- package/.pi/prompts/tdd-plan.md +67 -0
- package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +39 -0
- package/CHANGELOG.md +365 -0
- package/README.md +42 -109
- package/biome.json +1 -1
- package/docs/assets/logo.png +0 -0
- package/docs/assets/logo.svg +533 -0
- package/docs/configuration.md +358 -38
- package/docs/plans/0001-initial-implementation-plan.md +17 -9
- package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
- package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
- package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
- package/docs/plans/0010-acceptance-test-coverage.md +240 -0
- package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
- package/docs/plans/0013-fallback-chain-step-type.md +280 -0
- package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
- package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
- package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
- package/docs/plans/0022-pi-coding-agent-types.md +201 -0
- package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
- package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
- package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
- package/docs/retro/0013-fallback-chain-step-type.md +67 -0
- package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
- package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
- package/docs/retro/0022-pi-coding-agent-types.md +62 -0
- package/docs/testing.md +95 -0
- package/package.json +30 -11
- package/prek.toml +2 -2
- package/schemas/pi-autoformat.schema.json +145 -21
- package/src/builtin-formatters.ts +205 -0
- package/src/command-probe.ts +66 -0
- package/src/config-loader.ts +829 -90
- package/src/custom-mutation-tools.ts +125 -0
- package/src/extension.ts +469 -82
- package/src/format-scope.ts +118 -0
- package/src/formatter-config.ts +73 -36
- package/src/formatter-executor.ts +230 -34
- package/src/formatter-output-report.ts +149 -0
- package/src/formatter-registry.ts +139 -30
- package/src/index.ts +26 -5
- package/src/prompt-autoformatter.ts +148 -23
- package/src/shell-mutation-detector.ts +572 -0
- package/src/touched-files-queue.ts +72 -11
- package/test/acceptance-event-bus.test.ts +138 -0
- package/test/acceptance.test.ts +69 -0
- package/test/builtin-formatters.test.ts +382 -0
- package/test/command-probe.test.ts +79 -0
- package/test/config-loader.test.ts +640 -21
- package/test/custom-mutation-tools.test.ts +190 -0
- package/test/extension.test.ts +1535 -158
- package/test/fallback-acceptance.test.ts +98 -0
- package/test/fixtures/event-bus-emitter.ts +26 -0
- package/test/fixtures/formatter-recorder.mjs +25 -0
- package/test/format-scope.test.ts +139 -0
- package/test/formatter-config.test.ts +56 -5
- package/test/formatter-executor.test.ts +555 -35
- package/test/formatter-output-report.test.ts +178 -0
- package/test/formatter-registry.test.ts +330 -37
- package/test/helpers/rpc.ts +146 -0
- package/test/prompt-autoformatter.test.ts +315 -22
- package/test/schema.test.ts +149 -0
- package/test/shell-mutation-detector.test.ts +221 -0
- package/test/touched-files-queue.test.ts +40 -1
- package/test/types/theme-stub.test-d.ts +42 -0
package/test/extension.test.ts
CHANGED
|
@@ -1,33 +1,134 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
Theme,
|
|
5
|
+
} from "@mariozechner/pi-coding-agent";
|
|
1
6
|
import { describe, expect, it, vi } from "vitest";
|
|
2
7
|
|
|
3
8
|
import type { LoadConfigResult } from "../src/config-loader.js";
|
|
4
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildSteeringMessageContent,
|
|
11
|
+
createAutoformatExtension,
|
|
12
|
+
createDefaultAutoformatter,
|
|
13
|
+
} from "../src/extension.js";
|
|
5
14
|
import { createFormatterConfig } from "../src/formatter-config.js";
|
|
6
15
|
import type { PromptAutoformatterResult } from "../src/prompt-autoformatter.js";
|
|
7
16
|
|
|
8
|
-
type Handler = (event:
|
|
17
|
+
type Handler = (event: never, ctx: TestContext) => void | Promise<void>;
|
|
9
18
|
|
|
10
19
|
type EventName =
|
|
11
20
|
| "session_start"
|
|
21
|
+
| "tool_call"
|
|
12
22
|
| "tool_result"
|
|
23
|
+
| "turn_end"
|
|
13
24
|
| "agent_end"
|
|
14
25
|
| "session_shutdown";
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Class-based stub that mirrors Pi's real `Theme.fg` `this`-binding
|
|
29
|
+
* requirement. Plain object literals like `{ fg: (n, t) => t }` would
|
|
30
|
+
* not surface the regression fixed in 6a6ec16, so every test ctx must
|
|
31
|
+
* use this stub (or a real Theme instance).
|
|
32
|
+
*/
|
|
33
|
+
class StubTheme {
|
|
34
|
+
private readonly fgColors = new Map<string, string>([
|
|
35
|
+
["success", ""],
|
|
36
|
+
["warning", ""],
|
|
37
|
+
["error", ""],
|
|
38
|
+
["dim", ""],
|
|
39
|
+
["accent", ""],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
fg(color: string, text: string): string {
|
|
43
|
+
if (!this.fgColors.has(color)) {
|
|
44
|
+
throw new Error(`Unknown theme color: ${color}`);
|
|
45
|
+
}
|
|
46
|
+
return text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build a `Theme`-shaped value backed by `StubTheme`. */
|
|
51
|
+
function makeStubTheme(): Theme {
|
|
52
|
+
return new StubTheme() as unknown as Theme;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Narrowed `ExtensionContext` view used by the autoformat extension. The
|
|
57
|
+
* real `ExtensionContext` requires sessionManager/modelRegistry/etc. that
|
|
58
|
+
* the autoformatter never touches, so tests fabricate only the surface
|
|
59
|
+
* exercised here. `ui.theme` is anchored to the real `Theme` type so
|
|
60
|
+
* plain-arrow stubs are rejected at compile time.
|
|
61
|
+
*/
|
|
16
62
|
type TestContext = {
|
|
17
63
|
cwd: string;
|
|
18
64
|
hasUI: boolean;
|
|
19
65
|
ui: {
|
|
20
66
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
67
|
+
setStatus?: (key: string, text: string | undefined) => void;
|
|
68
|
+
theme?: Theme;
|
|
21
69
|
};
|
|
22
70
|
};
|
|
23
71
|
|
|
24
72
|
class TestPi {
|
|
25
73
|
private readonly handlers = new Map<EventName, Handler[]>();
|
|
74
|
+
private readonly busHandlers = new Map<
|
|
75
|
+
string,
|
|
76
|
+
Array<(data: unknown) => void>
|
|
77
|
+
>();
|
|
26
78
|
|
|
27
|
-
|
|
79
|
+
// Cast through unknown: TestPi only models the events we exercise, not
|
|
80
|
+
// ExtensionAPI's full overload set. The boundary cast lives here once
|
|
81
|
+
// and the autoformat-side typing remains anchored to ExtensionAPI.
|
|
82
|
+
readonly on = ((eventName: EventName, handler: Handler): void => {
|
|
28
83
|
const existing = this.handlers.get(eventName) ?? [];
|
|
29
84
|
existing.push(handler);
|
|
30
85
|
this.handlers.set(eventName, existing);
|
|
86
|
+
}) as unknown as ExtensionAPI["on"];
|
|
87
|
+
|
|
88
|
+
readonly events: ExtensionAPI["events"] = {
|
|
89
|
+
emit: (_channel: string, _data: unknown): void => {
|
|
90
|
+
// Tests use `emitBus` for clarity; the EventBus.emit is a no-op in
|
|
91
|
+
// tests because no real bus producers run.
|
|
92
|
+
},
|
|
93
|
+
on: (channel, handler) => {
|
|
94
|
+
const existing = this.busHandlers.get(channel) ?? [];
|
|
95
|
+
existing.push(handler);
|
|
96
|
+
this.busHandlers.set(channel, existing);
|
|
97
|
+
return () => {
|
|
98
|
+
const current = this.busHandlers.get(channel) ?? [];
|
|
99
|
+
this.busHandlers.set(
|
|
100
|
+
channel,
|
|
101
|
+
current.filter((h) => h !== handler),
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
readonly sentMessages: Array<{
|
|
108
|
+
message: Record<string, unknown>;
|
|
109
|
+
options?: Record<string, unknown>;
|
|
110
|
+
}> = [];
|
|
111
|
+
|
|
112
|
+
readonly sendMessage = ((message: unknown, options?: unknown): void => {
|
|
113
|
+
this.sentMessages.push({
|
|
114
|
+
message: message as Record<string, unknown>,
|
|
115
|
+
options: options as Record<string, unknown> | undefined,
|
|
116
|
+
});
|
|
117
|
+
}) as unknown as ExtensionAPI["sendMessage"];
|
|
118
|
+
|
|
119
|
+
/** Cast helper: TestPi satisfies the slice of ExtensionAPI under test. */
|
|
120
|
+
asExtensionAPI(): ExtensionAPI {
|
|
121
|
+
return this as unknown as ExtensionAPI;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
emitBus(channel: string, data: unknown): void {
|
|
125
|
+
for (const handler of this.busHandlers.get(channel) ?? []) {
|
|
126
|
+
handler(data);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
busHandlerCount(channel: string): number {
|
|
131
|
+
return (this.busHandlers.get(channel) ?? []).length;
|
|
31
132
|
}
|
|
32
133
|
|
|
33
134
|
async emit(
|
|
@@ -36,16 +137,14 @@ class TestPi {
|
|
|
36
137
|
ctx: TestContext,
|
|
37
138
|
): Promise<void> {
|
|
38
139
|
for (const handler of this.handlers.get(eventName) ?? []) {
|
|
39
|
-
await handler(event, ctx);
|
|
140
|
+
await handler(event as never, ctx as unknown as ExtensionContext);
|
|
40
141
|
}
|
|
41
142
|
}
|
|
42
143
|
}
|
|
43
144
|
|
|
44
|
-
function createLoadResult(
|
|
45
|
-
formatMode: "tool" | "prompt" | "session",
|
|
46
|
-
): LoadConfigResult {
|
|
145
|
+
function createLoadResult(): LoadConfigResult {
|
|
47
146
|
return {
|
|
48
|
-
config: createFormatterConfig(
|
|
147
|
+
config: createFormatterConfig(),
|
|
49
148
|
globalConfigPath: "/global/config.json",
|
|
50
149
|
projectConfigPath: "/project/config.json",
|
|
51
150
|
issues: [],
|
|
@@ -58,6 +157,8 @@ function createContext(overrides?: Partial<TestContext>): TestContext {
|
|
|
58
157
|
hasUI: true,
|
|
59
158
|
ui: {
|
|
60
159
|
notify: vi.fn(),
|
|
160
|
+
setStatus: vi.fn(),
|
|
161
|
+
theme: makeStubTheme(),
|
|
61
162
|
},
|
|
62
163
|
...overrides,
|
|
63
164
|
};
|
|
@@ -65,48 +166,184 @@ function createContext(overrides?: Partial<TestContext>): TestContext {
|
|
|
65
166
|
|
|
66
167
|
function createFlushResult(): PromptAutoformatterResult {
|
|
67
168
|
return {
|
|
68
|
-
|
|
169
|
+
groups: [
|
|
69
170
|
{
|
|
70
|
-
|
|
71
|
-
|
|
171
|
+
chain: ["prettier"],
|
|
172
|
+
files: ["/repo/src/example.ts"],
|
|
173
|
+
runs: [
|
|
174
|
+
{
|
|
175
|
+
formatterName: "prettier",
|
|
176
|
+
command: ["prettier", "--write", "/repo/src/example.ts"],
|
|
177
|
+
files: ["/repo/src/example.ts"],
|
|
178
|
+
success: true,
|
|
179
|
+
exitCode: 0,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
changedFiles: [],
|
|
72
183
|
},
|
|
73
184
|
],
|
|
74
185
|
};
|
|
75
186
|
}
|
|
76
187
|
|
|
77
188
|
describe("createAutoformatExtension", () => {
|
|
78
|
-
it("
|
|
189
|
+
it("preserves the theme `this` binding when coloring the status line", async () => {
|
|
190
|
+
// Regression: Pi's real Theme.fg is an instance method that reads
|
|
191
|
+
// `this.fgColors`. If our extension destructures the method off the
|
|
192
|
+
// theme object and calls it standalone, `this` is undefined and the
|
|
193
|
+
// call throws "Cannot read properties of undefined (reading
|
|
194
|
+
// 'fgColors')". The module-level `StubTheme` reproduces that shape.
|
|
195
|
+
const pi = new TestPi();
|
|
196
|
+
const notify = vi.fn();
|
|
197
|
+
const setStatus = vi.fn();
|
|
198
|
+
const ctx = createContext({
|
|
199
|
+
ui: { notify, setStatus, theme: makeStubTheme() },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
203
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
204
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
205
|
+
recordToolResult: vi.fn(),
|
|
206
|
+
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await pi.emit("session_start", {}, ctx);
|
|
211
|
+
setStatus.mockClear();
|
|
212
|
+
await pi.emit("agent_end", {}, ctx);
|
|
213
|
+
|
|
214
|
+
// The flush must succeed cleanly: no "Unexpected runtime error" warning,
|
|
215
|
+
// and a normal success status was written.
|
|
216
|
+
const warningCalls = notify.mock.calls.filter((c) => c[1] === "warning");
|
|
217
|
+
expect(warningCalls).toEqual([]);
|
|
218
|
+
expect(setStatus).toHaveBeenCalledWith(
|
|
219
|
+
"autoformat",
|
|
220
|
+
expect.stringContaining("autoformat:"),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("clears the autoformat status on an empty flush in the TUI", async () => {
|
|
225
|
+
const pi = new TestPi();
|
|
226
|
+
const notify = vi.fn();
|
|
227
|
+
const setStatus = vi.fn();
|
|
228
|
+
const ctx = createContext({
|
|
229
|
+
ui: {
|
|
230
|
+
notify,
|
|
231
|
+
setStatus,
|
|
232
|
+
theme: makeStubTheme(),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
237
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
238
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
239
|
+
recordToolResult: vi.fn(),
|
|
240
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await pi.emit("session_start", {}, ctx);
|
|
245
|
+
setStatus.mockClear();
|
|
246
|
+
await pi.emit("agent_end", {}, ctx);
|
|
247
|
+
|
|
248
|
+
expect(setStatus).toHaveBeenCalledWith("autoformat", undefined);
|
|
249
|
+
expect(notify).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("reports interactive success summaries via the footer status", async () => {
|
|
253
|
+
const pi = new TestPi();
|
|
254
|
+
const notify = vi.fn();
|
|
255
|
+
const setStatus = vi.fn();
|
|
256
|
+
const ctx = createContext({
|
|
257
|
+
ui: {
|
|
258
|
+
notify,
|
|
259
|
+
setStatus,
|
|
260
|
+
theme: makeStubTheme(),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
265
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
266
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
267
|
+
recordToolResult: vi.fn(),
|
|
268
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
269
|
+
groups: [
|
|
270
|
+
{
|
|
271
|
+
chain: ["prettier"],
|
|
272
|
+
files: ["/repo/src/example.ts", "/repo/README.md"],
|
|
273
|
+
runs: [
|
|
274
|
+
{
|
|
275
|
+
formatterName: "prettier",
|
|
276
|
+
command: [],
|
|
277
|
+
files: ["/repo/src/example.ts", "/repo/README.md"],
|
|
278
|
+
success: true,
|
|
279
|
+
exitCode: 0,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
}),
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await pi.emit("session_start", {}, ctx);
|
|
289
|
+
setStatus.mockClear();
|
|
290
|
+
await pi.emit("agent_end", {}, ctx);
|
|
291
|
+
|
|
292
|
+
expect(setStatus).toHaveBeenCalledTimes(1);
|
|
293
|
+
const [statusKey, statusText] = setStatus.mock.calls[0];
|
|
294
|
+
expect(statusKey).toBe("autoformat");
|
|
295
|
+
expect(statusText).toContain("autoformat:");
|
|
296
|
+
expect(statusText).toContain("2 files");
|
|
297
|
+
expect(statusText).toContain("prettier");
|
|
298
|
+
expect(notify).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("counts files across multiple groups in the success summary", async () => {
|
|
79
302
|
const pi = new TestPi();
|
|
80
303
|
const notify = vi.fn();
|
|
304
|
+
const setStatus = vi.fn();
|
|
81
305
|
const ctx = createContext({
|
|
82
306
|
ui: {
|
|
83
307
|
notify,
|
|
308
|
+
setStatus,
|
|
309
|
+
theme: makeStubTheme(),
|
|
84
310
|
},
|
|
85
311
|
});
|
|
86
312
|
|
|
87
|
-
createAutoformatExtension(pi, {
|
|
88
|
-
loadConfig: vi.fn().mockReturnValue(createLoadResult(
|
|
313
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
314
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
89
315
|
createAutoformatter: vi.fn().mockReturnValue({
|
|
90
316
|
recordToolResult: vi.fn(),
|
|
91
317
|
flushPrompt: vi.fn().mockResolvedValue({
|
|
92
|
-
|
|
318
|
+
groups: [
|
|
93
319
|
{
|
|
94
|
-
|
|
320
|
+
chain: ["prettier"],
|
|
321
|
+
files: ["/repo/a.ts", "/repo/b.ts"],
|
|
95
322
|
runs: [
|
|
96
323
|
{
|
|
97
324
|
formatterName: "prettier",
|
|
98
325
|
command: [],
|
|
326
|
+
files: ["/repo/a.ts", "/repo/b.ts"],
|
|
99
327
|
success: true,
|
|
100
328
|
exitCode: 0,
|
|
101
329
|
},
|
|
102
330
|
],
|
|
103
331
|
},
|
|
104
332
|
{
|
|
105
|
-
|
|
333
|
+
chain: ["prettier", "markdownlint"],
|
|
334
|
+
files: ["/repo/c.md"],
|
|
106
335
|
runs: [
|
|
107
336
|
{
|
|
108
337
|
formatterName: "prettier",
|
|
109
338
|
command: [],
|
|
339
|
+
files: ["/repo/c.md"],
|
|
340
|
+
success: true,
|
|
341
|
+
exitCode: 0,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
formatterName: "markdownlint",
|
|
345
|
+
command: [],
|
|
346
|
+
files: ["/repo/c.md"],
|
|
110
347
|
success: true,
|
|
111
348
|
exitCode: 0,
|
|
112
349
|
},
|
|
@@ -118,67 +355,107 @@ describe("createAutoformatExtension", () => {
|
|
|
118
355
|
});
|
|
119
356
|
|
|
120
357
|
await pi.emit("session_start", {}, ctx);
|
|
358
|
+
setStatus.mockClear();
|
|
121
359
|
await pi.emit("agent_end", {}, ctx);
|
|
122
360
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
361
|
+
const [, statusText] = setStatus.mock.calls[0];
|
|
362
|
+
expect(statusText).toContain("3 files");
|
|
363
|
+
expect(statusText).toContain("prettier");
|
|
364
|
+
expect(statusText).toContain("markdownlint");
|
|
365
|
+
expect(notify).not.toHaveBeenCalled();
|
|
127
366
|
});
|
|
128
367
|
|
|
129
|
-
it("
|
|
368
|
+
it("reports per-batch failure lines listing each batch's files", async () => {
|
|
130
369
|
const pi = new TestPi();
|
|
131
370
|
const notify = vi.fn();
|
|
371
|
+
const setStatus = vi.fn();
|
|
372
|
+
const fg = vi.fn((_name: string, text: string) => text);
|
|
373
|
+
class SpyTheme {
|
|
374
|
+
fg = fg;
|
|
375
|
+
}
|
|
132
376
|
const ctx = createContext({
|
|
133
377
|
ui: {
|
|
134
378
|
notify,
|
|
379
|
+
setStatus,
|
|
380
|
+
theme: new SpyTheme() as unknown as Theme,
|
|
135
381
|
},
|
|
136
382
|
});
|
|
137
383
|
|
|
138
|
-
createAutoformatExtension(pi, {
|
|
139
|
-
loadConfig: vi.fn().mockReturnValue(
|
|
140
|
-
...createLoadResult("prompt"),
|
|
141
|
-
config: createFormatterConfig({
|
|
142
|
-
formatMode: "prompt",
|
|
143
|
-
hideSummariesInTui: true,
|
|
144
|
-
}),
|
|
145
|
-
}),
|
|
384
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
385
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
146
386
|
createAutoformatter: vi.fn().mockReturnValue({
|
|
147
387
|
recordToolResult: vi.fn(),
|
|
148
|
-
flushPrompt: vi.fn().mockResolvedValue(
|
|
388
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
389
|
+
groups: [
|
|
390
|
+
{
|
|
391
|
+
chain: ["prettier"],
|
|
392
|
+
files: ["/repo/a.ts", "/repo/b.ts"],
|
|
393
|
+
runs: [
|
|
394
|
+
{
|
|
395
|
+
formatterName: "prettier",
|
|
396
|
+
command: [],
|
|
397
|
+
files: ["/repo/a.ts", "/repo/b.ts"],
|
|
398
|
+
success: false,
|
|
399
|
+
exitCode: 2,
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}),
|
|
149
405
|
}),
|
|
150
406
|
});
|
|
151
407
|
|
|
152
408
|
await pi.emit("session_start", {}, ctx);
|
|
153
409
|
await pi.emit("agent_end", {}, ctx);
|
|
154
410
|
|
|
155
|
-
expect(notify).
|
|
411
|
+
expect(notify).toHaveBeenCalledWith(
|
|
412
|
+
"Formatter failures in 1 batch:\nprettier (exit 2): /repo/a.ts, /repo/b.ts",
|
|
413
|
+
"warning",
|
|
414
|
+
);
|
|
415
|
+
const failureStatusCalls = setStatus.mock.calls.filter(
|
|
416
|
+
(c) => c[1] !== undefined,
|
|
417
|
+
);
|
|
418
|
+
expect(failureStatusCalls).toHaveLength(1);
|
|
419
|
+
const [statusKey, statusText] = failureStatusCalls[0];
|
|
420
|
+
expect(statusKey).toBe("autoformat");
|
|
421
|
+
expect(statusText).toContain("1 batch failed");
|
|
422
|
+
expect(statusText).toContain("prettier");
|
|
423
|
+
expect(fg).toHaveBeenCalledWith("error", expect.any(String));
|
|
156
424
|
});
|
|
157
425
|
|
|
158
|
-
it("
|
|
426
|
+
it("shows mixed-result failures with surviving success batches in the status", async () => {
|
|
159
427
|
const pi = new TestPi();
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const ctx = createContext({
|
|
428
|
+
const notify = vi.fn();
|
|
429
|
+
const setStatus = vi.fn();
|
|
430
|
+
const ctx = createContext({
|
|
431
|
+
ui: {
|
|
432
|
+
notify,
|
|
433
|
+
setStatus,
|
|
434
|
+
theme: makeStubTheme(),
|
|
435
|
+
},
|
|
436
|
+
});
|
|
163
437
|
|
|
164
|
-
createAutoformatExtension(pi, {
|
|
165
|
-
loadConfig: vi.fn().mockReturnValue(createLoadResult(
|
|
438
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
439
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
166
440
|
createAutoformatter: vi.fn().mockReturnValue({
|
|
167
441
|
recordToolResult: vi.fn(),
|
|
168
442
|
flushPrompt: vi.fn().mockResolvedValue({
|
|
169
|
-
|
|
443
|
+
groups: [
|
|
170
444
|
{
|
|
171
|
-
|
|
445
|
+
chain: ["prettier", "markdownlint"],
|
|
446
|
+
files: ["/repo/x.md"],
|
|
172
447
|
runs: [
|
|
173
448
|
{
|
|
174
449
|
formatterName: "prettier",
|
|
175
|
-
command: [
|
|
176
|
-
|
|
177
|
-
|
|
450
|
+
command: [],
|
|
451
|
+
files: ["/repo/x.md"],
|
|
452
|
+
success: true,
|
|
453
|
+
exitCode: 0,
|
|
178
454
|
},
|
|
179
455
|
{
|
|
180
|
-
formatterName: "markdownlint
|
|
181
|
-
command: [
|
|
456
|
+
formatterName: "markdownlint",
|
|
457
|
+
command: [],
|
|
458
|
+
files: ["/repo/x.md"],
|
|
182
459
|
success: false,
|
|
183
460
|
exitCode: 1,
|
|
184
461
|
},
|
|
@@ -190,175 +467,1275 @@ describe("createAutoformatExtension", () => {
|
|
|
190
467
|
});
|
|
191
468
|
|
|
192
469
|
await pi.emit("session_start", {}, ctx);
|
|
470
|
+
setStatus.mockClear();
|
|
193
471
|
await pi.emit("agent_end", {}, ctx);
|
|
194
472
|
|
|
195
|
-
expect(
|
|
196
|
-
"
|
|
473
|
+
expect(notify).toHaveBeenCalledWith(
|
|
474
|
+
expect.stringContaining("markdownlint (exit 1): /repo/x.md"),
|
|
475
|
+
"warning",
|
|
197
476
|
);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
477
|
+
const failureStatus = setStatus.mock.calls.find((c) => c[1] !== undefined);
|
|
478
|
+
expect(failureStatus).toBeDefined();
|
|
479
|
+
const text = failureStatus?.[1] as string;
|
|
480
|
+
expect(text).toContain("1 batch failed");
|
|
481
|
+
expect(text).toContain("1 ok");
|
|
202
482
|
});
|
|
203
483
|
|
|
204
|
-
it("
|
|
484
|
+
it("renders fallback context in success summaries when present", async () => {
|
|
205
485
|
const pi = new TestPi();
|
|
206
|
-
const
|
|
207
|
-
const
|
|
486
|
+
const notify = vi.fn();
|
|
487
|
+
const setStatus = vi.fn();
|
|
488
|
+
const ctx = createContext({
|
|
489
|
+
ui: {
|
|
490
|
+
notify,
|
|
491
|
+
setStatus,
|
|
492
|
+
theme: makeStubTheme(),
|
|
493
|
+
},
|
|
494
|
+
});
|
|
208
495
|
|
|
209
|
-
createAutoformatExtension(pi, {
|
|
210
|
-
loadConfig: vi.fn().mockReturnValue(
|
|
211
|
-
...createLoadResult("prompt"),
|
|
212
|
-
issues: [
|
|
213
|
-
{
|
|
214
|
-
path: "formatMode",
|
|
215
|
-
message: "Expected a valid mode.",
|
|
216
|
-
sourcePath: "/repo/.pi/extensions/pi-autoformat/config.json",
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
}),
|
|
496
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
497
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
220
498
|
createAutoformatter: vi.fn().mockReturnValue({
|
|
221
499
|
recordToolResult: vi.fn(),
|
|
222
|
-
flushPrompt: vi.fn().mockResolvedValue({
|
|
500
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
501
|
+
groups: [
|
|
502
|
+
{
|
|
503
|
+
chain: [{ fallback: ["biome", "prettier"] }],
|
|
504
|
+
files: ["/repo/a.ts"],
|
|
505
|
+
runs: [
|
|
506
|
+
{
|
|
507
|
+
formatterName: "prettier",
|
|
508
|
+
command: ["prettier", "--write", "/repo/a.ts"],
|
|
509
|
+
files: ["/repo/a.ts"],
|
|
510
|
+
success: true,
|
|
511
|
+
exitCode: 0,
|
|
512
|
+
fallbackContext: { skipped: ["biome"] },
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
}),
|
|
223
518
|
}),
|
|
224
519
|
});
|
|
225
520
|
|
|
226
521
|
await pi.emit("session_start", {}, ctx);
|
|
522
|
+
setStatus.mockClear();
|
|
523
|
+
await pi.emit("agent_end", {}, ctx);
|
|
227
524
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
525
|
+
const statusTexts = setStatus.mock.calls.map((c) => c[1] as string);
|
|
526
|
+
expect(
|
|
527
|
+
statusTexts.some((m) =>
|
|
528
|
+
/prettier \(fallback after biome unavailable\)/.test(m),
|
|
529
|
+
),
|
|
530
|
+
).toBe(true);
|
|
233
531
|
});
|
|
234
532
|
|
|
235
|
-
it("
|
|
533
|
+
it("renders fallback context in failure summaries when present", async () => {
|
|
236
534
|
const pi = new TestPi();
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
recordToolResult: vi.fn(),
|
|
240
|
-
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
241
|
-
};
|
|
242
|
-
const reportFlushResult = vi.fn();
|
|
535
|
+
const notify = vi.fn();
|
|
536
|
+
const ctx = createContext({ ui: { notify } });
|
|
243
537
|
|
|
244
|
-
createAutoformatExtension(pi, {
|
|
245
|
-
loadConfig: vi.fn().mockReturnValue(createLoadResult(
|
|
246
|
-
createAutoformatter: vi.fn().mockReturnValue(
|
|
247
|
-
|
|
538
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
539
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
540
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
541
|
+
recordToolResult: vi.fn(),
|
|
542
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
543
|
+
groups: [
|
|
544
|
+
{
|
|
545
|
+
chain: [{ fallback: ["biome", "prettier"] }],
|
|
546
|
+
files: ["/repo/a.ts"],
|
|
547
|
+
runs: [
|
|
548
|
+
{
|
|
549
|
+
formatterName: "prettier",
|
|
550
|
+
command: ["prettier", "--write", "/repo/a.ts"],
|
|
551
|
+
files: ["/repo/a.ts"],
|
|
552
|
+
success: false,
|
|
553
|
+
exitCode: 2,
|
|
554
|
+
fallbackContext: { skipped: ["biome"] },
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
}),
|
|
560
|
+
}),
|
|
248
561
|
});
|
|
249
562
|
|
|
250
563
|
await pi.emit("session_start", {}, ctx);
|
|
251
|
-
await pi.emit(
|
|
252
|
-
"tool_result",
|
|
253
|
-
{
|
|
254
|
-
toolName: "write",
|
|
255
|
-
input: { path: "src/example.ts", content: "export {};" },
|
|
256
|
-
isError: false,
|
|
257
|
-
},
|
|
258
|
-
ctx,
|
|
259
|
-
);
|
|
260
564
|
await pi.emit("agent_end", {}, ctx);
|
|
261
565
|
|
|
262
|
-
expect(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
expect(autoformatter.flushPrompt).toHaveBeenCalledTimes(1);
|
|
267
|
-
expect(reportFlushResult).toHaveBeenCalledTimes(1);
|
|
566
|
+
expect(notify).toHaveBeenCalledWith(
|
|
567
|
+
"Formatter failures in 1 batch:\nprettier (fallback after biome unavailable) (exit 2): /repo/a.ts",
|
|
568
|
+
"warning",
|
|
569
|
+
);
|
|
268
570
|
});
|
|
269
571
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
572
|
+
describe("formatterOutput surfacing", () => {
|
|
573
|
+
function makeFailedFlushResult(stdout: string, stderr: string) {
|
|
574
|
+
return {
|
|
575
|
+
groups: [
|
|
576
|
+
{
|
|
577
|
+
chain: ["prettier"],
|
|
578
|
+
files: ["/repo/a.ts"],
|
|
579
|
+
runs: [
|
|
580
|
+
{
|
|
581
|
+
formatterName: "prettier",
|
|
582
|
+
command: ["prettier", "--write", "/repo/a.ts"],
|
|
583
|
+
files: ["/repo/a.ts"],
|
|
584
|
+
success: false,
|
|
585
|
+
exitCode: 2,
|
|
586
|
+
stdout,
|
|
587
|
+
stderr,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
277
594
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
595
|
+
it("omits stdout/stderr by default (onFailure: none)", async () => {
|
|
596
|
+
const pi = new TestPi();
|
|
597
|
+
const notify = vi.fn();
|
|
598
|
+
const ctx = createContext({ ui: { notify } });
|
|
599
|
+
|
|
600
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
601
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
602
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
603
|
+
recordToolResult: vi.fn(),
|
|
604
|
+
flushPrompt: vi
|
|
605
|
+
.fn()
|
|
606
|
+
.mockResolvedValue(makeFailedFlushResult("out", "err")),
|
|
607
|
+
}),
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
await pi.emit("session_start", {}, ctx);
|
|
611
|
+
await pi.emit("agent_end", {}, ctx);
|
|
612
|
+
|
|
613
|
+
expect(notify).toHaveBeenCalledWith(
|
|
614
|
+
"Formatter failures in 1 batch:\nprettier (exit 2): /repo/a.ts",
|
|
615
|
+
"warning",
|
|
616
|
+
);
|
|
282
617
|
});
|
|
283
618
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
input: { path: "src/example.ts", edits: [] },
|
|
289
|
-
isError: false,
|
|
290
|
-
},
|
|
291
|
-
ctx,
|
|
292
|
-
);
|
|
619
|
+
it("appends only stderr under onFailure: stderr", async () => {
|
|
620
|
+
const pi = new TestPi();
|
|
621
|
+
const notify = vi.fn();
|
|
622
|
+
const ctx = createContext({ ui: { notify } });
|
|
293
623
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
624
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
625
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
626
|
+
...createLoadResult(),
|
|
627
|
+
config: createFormatterConfig({
|
|
628
|
+
formatterOutput: { onFailure: "stderr" },
|
|
629
|
+
}),
|
|
630
|
+
}),
|
|
631
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
632
|
+
recordToolResult: vi.fn(),
|
|
633
|
+
flushPrompt: vi
|
|
634
|
+
.fn()
|
|
635
|
+
.mockResolvedValue(
|
|
636
|
+
makeFailedFlushResult("chatty stdout", "boom!\nat foo"),
|
|
637
|
+
),
|
|
638
|
+
}),
|
|
639
|
+
});
|
|
297
640
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const ctx = createContext();
|
|
301
|
-
const autoformatter = {
|
|
302
|
-
recordToolResult: vi.fn(),
|
|
303
|
-
flushPrompt: vi.fn().mockResolvedValue({ files: [] }),
|
|
304
|
-
};
|
|
641
|
+
await pi.emit("session_start", {}, ctx);
|
|
642
|
+
await pi.emit("agent_end", {}, ctx);
|
|
305
643
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
644
|
+
const [message] = notify.mock.calls[0];
|
|
645
|
+
expect(message).toContain("prettier (exit 2): /repo/a.ts");
|
|
646
|
+
expect(message).toContain(" stderr:");
|
|
647
|
+
expect(message).toContain(" boom!");
|
|
648
|
+
expect(message).toContain(" at foo");
|
|
649
|
+
expect(message).not.toContain(" stdout:");
|
|
650
|
+
expect(message).not.toContain("chatty stdout");
|
|
310
651
|
});
|
|
311
652
|
|
|
312
|
-
|
|
313
|
-
|
|
653
|
+
it("appends stdout above stderr under onFailure: both", async () => {
|
|
654
|
+
const pi = new TestPi();
|
|
655
|
+
const notify = vi.fn();
|
|
656
|
+
const ctx = createContext({ ui: { notify } });
|
|
657
|
+
|
|
658
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
659
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
660
|
+
...createLoadResult(),
|
|
661
|
+
config: createFormatterConfig({
|
|
662
|
+
formatterOutput: { onFailure: "both" },
|
|
663
|
+
}),
|
|
664
|
+
}),
|
|
665
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
666
|
+
recordToolResult: vi.fn(),
|
|
667
|
+
flushPrompt: vi
|
|
668
|
+
.fn()
|
|
669
|
+
.mockResolvedValue(makeFailedFlushResult("out line", "err line")),
|
|
670
|
+
}),
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
await pi.emit("session_start", {}, ctx);
|
|
674
|
+
await pi.emit("agent_end", {}, ctx);
|
|
675
|
+
|
|
676
|
+
const [message] = notify.mock.calls[0];
|
|
677
|
+
const stdoutIdx = message.indexOf(" stdout:");
|
|
678
|
+
const stderrIdx = message.indexOf(" stderr:");
|
|
679
|
+
expect(stdoutIdx).toBeGreaterThanOrEqual(0);
|
|
680
|
+
expect(stderrIdx).toBeGreaterThan(stdoutIdx);
|
|
681
|
+
expect(message).toContain(" out line");
|
|
682
|
+
expect(message).toContain(" err line");
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("omits an empty stdout block under onFailure: both", async () => {
|
|
686
|
+
const pi = new TestPi();
|
|
687
|
+
const notify = vi.fn();
|
|
688
|
+
const ctx = createContext({ ui: { notify } });
|
|
689
|
+
|
|
690
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
691
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
692
|
+
...createLoadResult(),
|
|
693
|
+
config: createFormatterConfig({
|
|
694
|
+
formatterOutput: { onFailure: "both" },
|
|
695
|
+
}),
|
|
696
|
+
}),
|
|
697
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
698
|
+
recordToolResult: vi.fn(),
|
|
699
|
+
flushPrompt: vi
|
|
700
|
+
.fn()
|
|
701
|
+
.mockResolvedValue(makeFailedFlushResult("", "only stderr")),
|
|
702
|
+
}),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
await pi.emit("session_start", {}, ctx);
|
|
706
|
+
await pi.emit("agent_end", {}, ctx);
|
|
707
|
+
|
|
708
|
+
const [message] = notify.mock.calls[0];
|
|
709
|
+
expect(message).not.toContain(" stdout:");
|
|
710
|
+
expect(message).toContain(" stderr:");
|
|
711
|
+
expect(message).toContain(" only stderr");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("truncates a multi-kilobyte stderr while preserving the tail", async () => {
|
|
715
|
+
const pi = new TestPi();
|
|
716
|
+
const notify = vi.fn();
|
|
717
|
+
const ctx = createContext({ ui: { notify } });
|
|
718
|
+
|
|
719
|
+
// 200 lines, ~50 bytes each → ~10 KB. Cap well below.
|
|
720
|
+
const longStderr = Array.from(
|
|
721
|
+
{ length: 200 },
|
|
722
|
+
(_, i) =>
|
|
723
|
+
`line${String(i).padStart(3, "0")}: ${"diagnostic-".repeat(3)}`,
|
|
724
|
+
).join("\n");
|
|
725
|
+
|
|
726
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
727
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
728
|
+
...createLoadResult(),
|
|
729
|
+
config: createFormatterConfig({
|
|
730
|
+
formatterOutput: {
|
|
731
|
+
onFailure: "stderr",
|
|
732
|
+
maxBytes: 1024,
|
|
733
|
+
maxLines: 100,
|
|
734
|
+
},
|
|
735
|
+
}),
|
|
736
|
+
}),
|
|
737
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
738
|
+
recordToolResult: vi.fn(),
|
|
739
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
740
|
+
groups: [
|
|
741
|
+
{
|
|
742
|
+
chain: ["prettier"],
|
|
743
|
+
files: ["/repo/a.ts"],
|
|
744
|
+
runs: [
|
|
745
|
+
{
|
|
746
|
+
formatterName: "prettier",
|
|
747
|
+
command: ["prettier", "--write", "/repo/a.ts"],
|
|
748
|
+
files: ["/repo/a.ts"],
|
|
749
|
+
success: false,
|
|
750
|
+
exitCode: 2,
|
|
751
|
+
stderr: longStderr,
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
}),
|
|
757
|
+
}),
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
await pi.emit("session_start", {}, ctx);
|
|
761
|
+
await pi.emit("agent_end", {}, ctx);
|
|
762
|
+
|
|
763
|
+
const [message] = notify.mock.calls[0];
|
|
764
|
+
expect(message).toMatch(/\(truncated, \d+ earlier (bytes|lines)\)/);
|
|
765
|
+
// Tail must survive.
|
|
766
|
+
expect(message).toContain("line199");
|
|
767
|
+
// Head must not.
|
|
768
|
+
expect(message).not.toContain("line000");
|
|
769
|
+
expect(message).not.toContain("line050");
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("never annotates successful runs even under onFailure: both", async () => {
|
|
773
|
+
const pi = new TestPi();
|
|
774
|
+
const notify = vi.fn();
|
|
775
|
+
const setStatus = vi.fn();
|
|
776
|
+
const ctx = createContext({
|
|
777
|
+
ui: {
|
|
778
|
+
notify,
|
|
779
|
+
setStatus,
|
|
780
|
+
theme: makeStubTheme(),
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
785
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
786
|
+
...createLoadResult(),
|
|
787
|
+
config: createFormatterConfig({
|
|
788
|
+
formatterOutput: { onFailure: "both" },
|
|
789
|
+
}),
|
|
790
|
+
}),
|
|
791
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
792
|
+
recordToolResult: vi.fn(),
|
|
793
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
794
|
+
groups: [
|
|
795
|
+
{
|
|
796
|
+
chain: ["prettier"],
|
|
797
|
+
files: ["/repo/a.ts"],
|
|
798
|
+
runs: [
|
|
799
|
+
{
|
|
800
|
+
formatterName: "prettier",
|
|
801
|
+
command: ["prettier", "--write", "/repo/a.ts"],
|
|
802
|
+
files: ["/repo/a.ts"],
|
|
803
|
+
success: true,
|
|
804
|
+
exitCode: 0,
|
|
805
|
+
stdout: "would never be shown",
|
|
806
|
+
stderr: "deprecation notice",
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
},
|
|
810
|
+
],
|
|
811
|
+
}),
|
|
812
|
+
}),
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
await pi.emit("session_start", {}, ctx);
|
|
816
|
+
await pi.emit("agent_end", {}, ctx);
|
|
817
|
+
|
|
818
|
+
// No failure notify; success status only.
|
|
819
|
+
expect(notify).not.toHaveBeenCalled();
|
|
820
|
+
const statusTexts = setStatus.mock.calls
|
|
821
|
+
.map((c) => c[1])
|
|
822
|
+
.filter((t): t is string => typeof t === "string");
|
|
823
|
+
for (const text of statusTexts) {
|
|
824
|
+
expect(text).not.toContain("would never be shown");
|
|
825
|
+
expect(text).not.toContain("deprecation notice");
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("hides interactive success summaries when configured", async () => {
|
|
831
|
+
const pi = new TestPi();
|
|
832
|
+
const notify = vi.fn();
|
|
833
|
+
const setStatus = vi.fn();
|
|
834
|
+
const ctx = createContext({
|
|
835
|
+
ui: {
|
|
836
|
+
notify,
|
|
837
|
+
setStatus,
|
|
838
|
+
theme: makeStubTheme(),
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
843
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
844
|
+
...createLoadResult(),
|
|
845
|
+
config: createFormatterConfig({
|
|
846
|
+
hideSummariesInTui: true,
|
|
847
|
+
}),
|
|
848
|
+
}),
|
|
849
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
850
|
+
recordToolResult: vi.fn(),
|
|
851
|
+
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
852
|
+
}),
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
await pi.emit("session_start", {}, ctx);
|
|
856
|
+
setStatus.mockClear();
|
|
857
|
+
await pi.emit("agent_end", {}, ctx);
|
|
858
|
+
|
|
859
|
+
expect(notify).not.toHaveBeenCalled();
|
|
860
|
+
expect(setStatus).toHaveBeenCalledWith("autoformat", undefined);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("still surfaces failures when hideSummariesInTui is true", async () => {
|
|
864
|
+
const pi = new TestPi();
|
|
865
|
+
const notify = vi.fn();
|
|
866
|
+
const setStatus = vi.fn();
|
|
867
|
+
const ctx = createContext({
|
|
868
|
+
ui: {
|
|
869
|
+
notify,
|
|
870
|
+
setStatus,
|
|
871
|
+
theme: makeStubTheme(),
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
876
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
877
|
+
...createLoadResult(),
|
|
878
|
+
config: createFormatterConfig({
|
|
879
|
+
hideSummariesInTui: true,
|
|
880
|
+
}),
|
|
881
|
+
}),
|
|
882
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
883
|
+
recordToolResult: vi.fn(),
|
|
884
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
885
|
+
groups: [
|
|
886
|
+
{
|
|
887
|
+
chain: ["prettier"],
|
|
888
|
+
files: ["/repo/a.ts"],
|
|
889
|
+
runs: [
|
|
890
|
+
{
|
|
891
|
+
formatterName: "prettier",
|
|
892
|
+
command: [],
|
|
893
|
+
files: ["/repo/a.ts"],
|
|
894
|
+
success: false,
|
|
895
|
+
exitCode: 2,
|
|
896
|
+
},
|
|
897
|
+
],
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
}),
|
|
901
|
+
}),
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
await pi.emit("session_start", {}, ctx);
|
|
905
|
+
setStatus.mockClear();
|
|
906
|
+
await pi.emit("agent_end", {}, ctx);
|
|
907
|
+
|
|
908
|
+
expect(notify).toHaveBeenCalledWith(
|
|
909
|
+
expect.stringContaining("prettier (exit 2): /repo/a.ts"),
|
|
910
|
+
"warning",
|
|
911
|
+
);
|
|
912
|
+
const failureStatus = setStatus.mock.calls.find((c) => c[1] !== undefined);
|
|
913
|
+
expect(failureStatus?.[1]).toContain("1 batch failed");
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it("keeps non-interactive success summaries on console.log without setStatus", async () => {
|
|
917
|
+
const pi = new TestPi();
|
|
918
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
919
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
920
|
+
const setStatus = vi.fn();
|
|
921
|
+
const ctx: TestContext = {
|
|
922
|
+
cwd: "/repo",
|
|
923
|
+
hasUI: false,
|
|
924
|
+
ui: { notify: vi.fn(), setStatus },
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
928
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
929
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
930
|
+
recordToolResult: vi.fn(),
|
|
931
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
932
|
+
groups: [
|
|
933
|
+
{
|
|
934
|
+
chain: ["prettier"],
|
|
935
|
+
files: ["/repo/a.ts"],
|
|
936
|
+
runs: [
|
|
937
|
+
{
|
|
938
|
+
formatterName: "prettier",
|
|
939
|
+
command: [],
|
|
940
|
+
files: ["/repo/a.ts"],
|
|
941
|
+
success: true,
|
|
942
|
+
exitCode: 0,
|
|
943
|
+
},
|
|
944
|
+
],
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
}),
|
|
948
|
+
}),
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
await pi.emit("session_start", {}, ctx);
|
|
952
|
+
await pi.emit("agent_end", {}, ctx);
|
|
953
|
+
|
|
954
|
+
expect(log).toHaveBeenCalledWith(
|
|
955
|
+
"[pi-autoformat] Autoformatted 1 file: /repo/a.ts",
|
|
956
|
+
);
|
|
957
|
+
expect(setStatus).not.toHaveBeenCalled();
|
|
958
|
+
expect(warn).not.toHaveBeenCalled();
|
|
959
|
+
|
|
960
|
+
log.mockRestore();
|
|
961
|
+
warn.mockRestore();
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it("reports non-interactive formatter failures via console warnings", async () => {
|
|
965
|
+
const pi = new TestPi();
|
|
966
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
967
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
968
|
+
const ctx = createContext({ hasUI: false });
|
|
969
|
+
|
|
970
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
971
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
972
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
973
|
+
recordToolResult: vi.fn(),
|
|
974
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
975
|
+
groups: [
|
|
976
|
+
{
|
|
977
|
+
chain: ["prettier", "markdownlint-cli2"],
|
|
978
|
+
files: ["/repo/README.md"],
|
|
979
|
+
runs: [
|
|
980
|
+
{
|
|
981
|
+
formatterName: "prettier",
|
|
982
|
+
command: ["prettier", "--write", "/repo/README.md"],
|
|
983
|
+
files: ["/repo/README.md"],
|
|
984
|
+
success: false,
|
|
985
|
+
exitCode: 2,
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
formatterName: "markdownlint-cli2",
|
|
989
|
+
command: ["markdownlint-cli2", "--fix", "/repo/README.md"],
|
|
990
|
+
files: ["/repo/README.md"],
|
|
991
|
+
success: false,
|
|
992
|
+
exitCode: 1,
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
}),
|
|
998
|
+
}),
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
await pi.emit("session_start", {}, ctx);
|
|
1002
|
+
await pi.emit("agent_end", {}, ctx);
|
|
1003
|
+
|
|
1004
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1005
|
+
"[pi-autoformat] Formatter failures in 2 batches:\nprettier (exit 2): /repo/README.md\nmarkdownlint-cli2 (exit 1): /repo/README.md",
|
|
1006
|
+
);
|
|
1007
|
+
expect(log).not.toHaveBeenCalled();
|
|
1008
|
+
// Non-interactive contexts must never touch setStatus even when present.
|
|
1009
|
+
const setStatus = (ctx.ui as { setStatus?: ReturnType<typeof vi.fn> })
|
|
1010
|
+
.setStatus;
|
|
1011
|
+
expect(setStatus).toBeDefined();
|
|
1012
|
+
expect(setStatus).not.toHaveBeenCalled();
|
|
1013
|
+
|
|
1014
|
+
warn.mockRestore();
|
|
1015
|
+
log.mockRestore();
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
it("reports non-interactive config issues via console warnings", async () => {
|
|
1019
|
+
const pi = new TestPi();
|
|
1020
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1021
|
+
const ctx = createContext({ hasUI: false });
|
|
1022
|
+
|
|
1023
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1024
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
1025
|
+
...createLoadResult(),
|
|
1026
|
+
issues: [
|
|
1027
|
+
{
|
|
1028
|
+
path: "commandTimeoutMs",
|
|
1029
|
+
message: "Expected a positive integer.",
|
|
1030
|
+
sourcePath: "/repo/.pi/extensions/pi-autoformat/config.json",
|
|
1031
|
+
},
|
|
1032
|
+
],
|
|
1033
|
+
}),
|
|
1034
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1035
|
+
recordToolResult: vi.fn(),
|
|
1036
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1037
|
+
}),
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
await pi.emit("session_start", {}, ctx);
|
|
1041
|
+
|
|
1042
|
+
expect(warn).toHaveBeenCalledWith(
|
|
1043
|
+
"[pi-autoformat] Configuration issues detected:\n/repo/.pi/extensions/pi-autoformat/config.json commandTimeoutMs: Expected a positive integer.",
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
warn.mockRestore();
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("clears the autoformat status on session_start and session_shutdown", async () => {
|
|
1050
|
+
const pi = new TestPi();
|
|
1051
|
+
const setStatus = vi.fn();
|
|
1052
|
+
const ctx = createContext({
|
|
1053
|
+
ui: {
|
|
1054
|
+
notify: vi.fn(),
|
|
1055
|
+
setStatus,
|
|
1056
|
+
theme: makeStubTheme(),
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1061
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1062
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1063
|
+
recordToolResult: vi.fn(),
|
|
1064
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1065
|
+
}),
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await pi.emit("session_start", {}, ctx);
|
|
1069
|
+
expect(setStatus).toHaveBeenCalledWith("autoformat", undefined);
|
|
1070
|
+
|
|
1071
|
+
setStatus.mockClear();
|
|
1072
|
+
await pi.emit("session_shutdown", {}, ctx);
|
|
1073
|
+
expect(setStatus).toHaveBeenCalledWith("autoformat", undefined);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it("records successful tool results and flushes at prompt end in prompt mode", async () => {
|
|
1077
|
+
const pi = new TestPi();
|
|
1078
|
+
const ctx = createContext();
|
|
1079
|
+
const autoformatter = {
|
|
1080
|
+
recordToolResult: vi.fn(),
|
|
1081
|
+
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
1082
|
+
};
|
|
1083
|
+
const reportFlushResult = vi.fn();
|
|
1084
|
+
|
|
1085
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1086
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1087
|
+
createAutoformatter: vi.fn().mockReturnValue(autoformatter),
|
|
1088
|
+
reportFlushResult,
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
await pi.emit("session_start", {}, ctx);
|
|
1092
|
+
await pi.emit(
|
|
1093
|
+
"tool_result",
|
|
1094
|
+
{
|
|
1095
|
+
toolName: "write",
|
|
1096
|
+
input: { path: "src/example.ts", content: "export {};" },
|
|
1097
|
+
isError: false,
|
|
1098
|
+
},
|
|
1099
|
+
ctx,
|
|
1100
|
+
);
|
|
1101
|
+
await pi.emit("agent_end", {}, ctx);
|
|
1102
|
+
|
|
1103
|
+
expect(autoformatter.recordToolResult).toHaveBeenCalledWith(
|
|
1104
|
+
"write",
|
|
1105
|
+
{
|
|
1106
|
+
path: "src/example.ts",
|
|
1107
|
+
content: "export {};",
|
|
1108
|
+
},
|
|
1109
|
+
"",
|
|
1110
|
+
);
|
|
1111
|
+
expect(autoformatter.flushPrompt).toHaveBeenCalledTimes(1);
|
|
1112
|
+
expect(reportFlushResult).toHaveBeenCalledTimes(1);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("ignores failed tool results", async () => {
|
|
1116
|
+
const pi = new TestPi();
|
|
1117
|
+
const ctx = createContext();
|
|
1118
|
+
const autoformatter = {
|
|
1119
|
+
recordToolResult: vi.fn(),
|
|
1120
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1124
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1125
|
+
createAutoformatter: vi.fn().mockReturnValue(autoformatter),
|
|
1126
|
+
reportFlushResult: vi.fn(),
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
await pi.emit("session_start", {}, ctx);
|
|
1130
|
+
await pi.emit(
|
|
1131
|
+
"tool_result",
|
|
1132
|
+
{
|
|
1133
|
+
toolName: "write",
|
|
1134
|
+
input: { path: "src/example.ts", content: "" },
|
|
1135
|
+
isError: true,
|
|
1136
|
+
},
|
|
1137
|
+
ctx,
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
expect(autoformatter.recordToolResult).not.toHaveBeenCalled();
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it("forwards bash tool output to the autoformatter", async () => {
|
|
1144
|
+
const pi = new TestPi();
|
|
1145
|
+
const ctx = createContext();
|
|
1146
|
+
const autoformatter = {
|
|
1147
|
+
recordToolResult: vi.fn(),
|
|
1148
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1149
|
+
addTouchedPath: vi.fn(),
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1153
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1154
|
+
createAutoformatter: vi.fn().mockReturnValue(autoformatter),
|
|
1155
|
+
reportFlushResult: vi.fn(),
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
await pi.emit(
|
|
1159
|
+
"tool_result",
|
|
1160
|
+
{
|
|
1161
|
+
toolName: "bash",
|
|
1162
|
+
input: { command: "sed -i 's/a/b/' foo.txt" },
|
|
1163
|
+
isError: false,
|
|
1164
|
+
content: [{ type: "text", text: "some output" }],
|
|
1165
|
+
},
|
|
1166
|
+
ctx,
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
expect(autoformatter.recordToolResult).toHaveBeenCalledWith(
|
|
1170
|
+
"bash",
|
|
1171
|
+
{ command: "sed -i 's/a/b/' foo.txt" },
|
|
1172
|
+
"some output",
|
|
1173
|
+
);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it("reports config issues on session start", async () => {
|
|
1177
|
+
const pi = new TestPi();
|
|
1178
|
+
const ctx = createContext();
|
|
1179
|
+
const reportConfigIssues = vi.fn();
|
|
1180
|
+
|
|
1181
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1182
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
1183
|
+
...createLoadResult(),
|
|
1184
|
+
issues: [
|
|
1185
|
+
{
|
|
1186
|
+
path: "commandTimeoutMs",
|
|
1187
|
+
message: "Expected a positive integer.",
|
|
1188
|
+
sourcePath: "/repo/.pi/extensions/pi-autoformat/config.json",
|
|
1189
|
+
},
|
|
1190
|
+
],
|
|
1191
|
+
}),
|
|
1192
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1193
|
+
recordToolResult: vi.fn(),
|
|
1194
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1195
|
+
}),
|
|
1196
|
+
reportConfigIssues,
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
await pi.emit("session_start", {}, ctx);
|
|
1200
|
+
|
|
1201
|
+
expect(reportConfigIssues).toHaveBeenCalledWith(
|
|
1202
|
+
[
|
|
1203
|
+
{
|
|
1204
|
+
path: "commandTimeoutMs",
|
|
1205
|
+
message: "Expected a positive integer.",
|
|
1206
|
+
sourcePath: "/repo/.pi/extensions/pi-autoformat/config.json",
|
|
1207
|
+
},
|
|
1208
|
+
],
|
|
1209
|
+
{ ctx },
|
|
1210
|
+
);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it("subscribes to the configured EventBus channel and forwards touched paths", async () => {
|
|
1214
|
+
const pi = new TestPi();
|
|
1215
|
+
const ctx = createContext();
|
|
1216
|
+
const addTouchedPath = vi.fn();
|
|
1217
|
+
const autoformatter = {
|
|
1218
|
+
recordToolResult: vi.fn(),
|
|
1219
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1220
|
+
addTouchedPath,
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1224
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1225
|
+
createAutoformatter: vi.fn().mockReturnValue(autoformatter),
|
|
1226
|
+
reportFlushResult: vi.fn(),
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
await pi.emit("session_start", {}, ctx);
|
|
1230
|
+
expect(pi.busHandlerCount("autoformat:touched")).toBe(1);
|
|
1231
|
+
|
|
1232
|
+
pi.emitBus("autoformat:touched", { path: "src/a.ts" });
|
|
1233
|
+
pi.emitBus("autoformat:touched", {
|
|
1234
|
+
paths: ["src/b.ts", "src/c.ts"],
|
|
1235
|
+
});
|
|
1236
|
+
pi.emitBus("autoformat:touched", "not-an-object");
|
|
1237
|
+
|
|
1238
|
+
expect(addTouchedPath.mock.calls.map((c) => c[0])).toEqual([
|
|
1239
|
+
"src/a.ts",
|
|
1240
|
+
"src/b.ts",
|
|
1241
|
+
"src/c.ts",
|
|
1242
|
+
]);
|
|
1243
|
+
|
|
1244
|
+
await pi.emit("session_shutdown", {}, ctx);
|
|
1245
|
+
expect(pi.busHandlerCount("autoformat:touched")).toBe(0);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it("does not subscribe when eventBusMutationChannel.enabled is false", async () => {
|
|
1249
|
+
const pi = new TestPi();
|
|
1250
|
+
const ctx = createContext();
|
|
1251
|
+
|
|
1252
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1253
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
1254
|
+
...createLoadResult(),
|
|
1255
|
+
config: createFormatterConfig({
|
|
1256
|
+
eventBusMutationChannel: { enabled: false },
|
|
1257
|
+
}),
|
|
1258
|
+
}),
|
|
1259
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1260
|
+
recordToolResult: vi.fn(),
|
|
1261
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1262
|
+
addTouchedPath: vi.fn(),
|
|
1263
|
+
}),
|
|
1264
|
+
reportFlushResult: vi.fn(),
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
await pi.emit("session_start", {}, ctx);
|
|
1268
|
+
expect(pi.busHandlerCount("autoformat:touched")).toBe(0);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it("respects a custom EventBus channel name", async () => {
|
|
1272
|
+
const pi = new TestPi();
|
|
1273
|
+
const ctx = createContext();
|
|
1274
|
+
const addTouchedPath = vi.fn();
|
|
1275
|
+
|
|
1276
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1277
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
1278
|
+
...createLoadResult(),
|
|
1279
|
+
config: createFormatterConfig({
|
|
1280
|
+
eventBusMutationChannel: { channel: "my:channel" },
|
|
1281
|
+
}),
|
|
1282
|
+
}),
|
|
1283
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1284
|
+
recordToolResult: vi.fn(),
|
|
1285
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1286
|
+
addTouchedPath,
|
|
1287
|
+
}),
|
|
1288
|
+
reportFlushResult: vi.fn(),
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
await pi.emit("session_start", {}, ctx);
|
|
1292
|
+
pi.emitBus("my:channel", { path: "src/x.ts" });
|
|
1293
|
+
expect(addTouchedPath).toHaveBeenCalledWith("src/x.ts");
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
it("wires customMutationTools into the default autoformatter queue", async () => {
|
|
1297
|
+
const config = createFormatterConfig({
|
|
1298
|
+
customMutationTools: [{ toolName: "my-codegen", pathField: "output" }],
|
|
1299
|
+
formatters: {
|
|
1300
|
+
"echo-fmt": {
|
|
1301
|
+
command: ["true"],
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
chains: {
|
|
1305
|
+
".ts": ["echo-fmt"],
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
const autoformatter = createDefaultAutoformatter("/repo", config);
|
|
1309
|
+
|
|
1310
|
+
autoformatter.recordToolResult(
|
|
1311
|
+
"my-codegen",
|
|
1312
|
+
{ output: "src/generated.ts" },
|
|
1313
|
+
"",
|
|
1314
|
+
);
|
|
1315
|
+
const result = await autoformatter.flushPrompt();
|
|
1316
|
+
|
|
1317
|
+
expect(result.groups.flatMap((g) => g.files)).toEqual([
|
|
1318
|
+
"/repo/src/generated.ts",
|
|
1319
|
+
]);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it("does not send a follow-up message from agent_end", async () => {
|
|
1323
|
+
const pi = new TestPi();
|
|
1324
|
+
const ctx = createContext();
|
|
1325
|
+
|
|
1326
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1327
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1328
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1329
|
+
recordToolResult: vi.fn(),
|
|
1330
|
+
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
1331
|
+
addTouchedPath: vi.fn(),
|
|
1332
|
+
}),
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
await pi.emit("session_start", {}, ctx);
|
|
1336
|
+
await pi.emit("agent_end", {}, ctx);
|
|
1337
|
+
|
|
1338
|
+
// agent_end should NOT send messages (steering is at turn_end only)
|
|
1339
|
+
expect(pi.sentMessages).toHaveLength(0);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it("flushes formatters at turn_end", async () => {
|
|
1343
|
+
const pi = new TestPi();
|
|
1344
|
+
const ctx = createContext();
|
|
1345
|
+
const autoformatter = {
|
|
1346
|
+
recordToolResult: vi.fn(),
|
|
1347
|
+
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
1348
|
+
addTouchedPath: vi.fn(),
|
|
1349
|
+
};
|
|
1350
|
+
const reportFlushResult = vi.fn();
|
|
1351
|
+
|
|
1352
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1353
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1354
|
+
createAutoformatter: vi.fn().mockReturnValue(autoformatter),
|
|
1355
|
+
reportFlushResult,
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
await pi.emit("session_start", {}, ctx);
|
|
1359
|
+
await pi.emit(
|
|
314
1360
|
"tool_result",
|
|
315
1361
|
{
|
|
316
1362
|
toolName: "write",
|
|
317
|
-
input: { path: "src/example.ts", content: "" },
|
|
318
|
-
isError:
|
|
1363
|
+
input: { path: "src/example.ts", content: "export {};" },
|
|
1364
|
+
isError: false,
|
|
319
1365
|
},
|
|
320
1366
|
ctx,
|
|
321
1367
|
);
|
|
322
|
-
await pi.emit("
|
|
1368
|
+
await pi.emit("turn_end", {}, ctx);
|
|
323
1369
|
|
|
324
|
-
expect(autoformatter.recordToolResult).not.toHaveBeenCalled();
|
|
325
1370
|
expect(autoformatter.flushPrompt).toHaveBeenCalledTimes(1);
|
|
1371
|
+
expect(reportFlushResult).toHaveBeenCalledTimes(1);
|
|
326
1372
|
});
|
|
327
1373
|
|
|
328
|
-
it("
|
|
1374
|
+
it("sends a steering message when turn_end flush changes files", async () => {
|
|
329
1375
|
const pi = new TestPi();
|
|
330
1376
|
const ctx = createContext();
|
|
331
|
-
const reportConfigIssues = vi.fn();
|
|
332
1377
|
|
|
333
|
-
createAutoformatExtension(pi, {
|
|
334
|
-
loadConfig: vi.fn().mockReturnValue(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1378
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1379
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1380
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1381
|
+
recordToolResult: vi.fn(),
|
|
1382
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
1383
|
+
groups: [
|
|
1384
|
+
{
|
|
1385
|
+
chain: ["prettier"],
|
|
1386
|
+
files: ["/repo/src/foo.ts"],
|
|
1387
|
+
runs: [
|
|
1388
|
+
{
|
|
1389
|
+
formatterName: "prettier",
|
|
1390
|
+
command: ["prettier", "--write"],
|
|
1391
|
+
files: ["/repo/src/foo.ts"],
|
|
1392
|
+
success: true,
|
|
1393
|
+
exitCode: 0,
|
|
1394
|
+
},
|
|
1395
|
+
],
|
|
1396
|
+
changedFiles: ["/repo/src/foo.ts"],
|
|
1397
|
+
},
|
|
1398
|
+
],
|
|
1399
|
+
}),
|
|
1400
|
+
addTouchedPath: vi.fn(),
|
|
343
1401
|
}),
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
await pi.emit("session_start", {}, ctx);
|
|
1405
|
+
await pi.emit("turn_end", {}, ctx);
|
|
1406
|
+
|
|
1407
|
+
expect(pi.sentMessages).toHaveLength(1);
|
|
1408
|
+
const content = pi.sentMessages[0].message.content as string;
|
|
1409
|
+
expect(content).toContain("[autoformat] Formatted 1 file(s)");
|
|
1410
|
+
expect(content).toContain("/repo/src/foo.ts");
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it("does not send a steering message when turn_end flush has no changes and no failures", async () => {
|
|
1414
|
+
const pi = new TestPi();
|
|
1415
|
+
const ctx = createContext();
|
|
1416
|
+
|
|
1417
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1418
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
344
1419
|
createAutoformatter: vi.fn().mockReturnValue({
|
|
345
1420
|
recordToolResult: vi.fn(),
|
|
346
|
-
flushPrompt: vi.fn().mockResolvedValue({
|
|
1421
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
1422
|
+
groups: [
|
|
1423
|
+
{
|
|
1424
|
+
chain: ["prettier"],
|
|
1425
|
+
files: ["/repo/src/foo.ts"],
|
|
1426
|
+
runs: [
|
|
1427
|
+
{
|
|
1428
|
+
formatterName: "prettier",
|
|
1429
|
+
command: ["prettier", "--write"],
|
|
1430
|
+
files: ["/repo/src/foo.ts"],
|
|
1431
|
+
success: true,
|
|
1432
|
+
exitCode: 0,
|
|
1433
|
+
},
|
|
1434
|
+
],
|
|
1435
|
+
changedFiles: [],
|
|
1436
|
+
},
|
|
1437
|
+
],
|
|
1438
|
+
}),
|
|
1439
|
+
addTouchedPath: vi.fn(),
|
|
347
1440
|
}),
|
|
348
|
-
reportConfigIssues,
|
|
349
1441
|
});
|
|
350
1442
|
|
|
351
1443
|
await pi.emit("session_start", {}, ctx);
|
|
1444
|
+
await pi.emit("turn_end", {}, ctx);
|
|
352
1445
|
|
|
353
|
-
expect(
|
|
354
|
-
|
|
1446
|
+
expect(pi.sentMessages).toHaveLength(0);
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it("sends a steering message with failure details on formatter failure", async () => {
|
|
1450
|
+
const pi = new TestPi();
|
|
1451
|
+
const ctx = createContext();
|
|
1452
|
+
|
|
1453
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1454
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1455
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1456
|
+
recordToolResult: vi.fn(),
|
|
1457
|
+
flushPrompt: vi.fn().mockResolvedValue({
|
|
1458
|
+
groups: [
|
|
1459
|
+
{
|
|
1460
|
+
chain: ["biome"],
|
|
1461
|
+
files: ["/repo/src/broken.ts"],
|
|
1462
|
+
runs: [
|
|
1463
|
+
{
|
|
1464
|
+
formatterName: "biome",
|
|
1465
|
+
command: ["biome", "format", "--write"],
|
|
1466
|
+
files: ["/repo/src/broken.ts"],
|
|
1467
|
+
success: false,
|
|
1468
|
+
exitCode: 1,
|
|
1469
|
+
stderr: "SyntaxError: Unexpected token at line 42",
|
|
1470
|
+
},
|
|
1471
|
+
],
|
|
1472
|
+
changedFiles: [],
|
|
1473
|
+
},
|
|
1474
|
+
],
|
|
1475
|
+
}),
|
|
1476
|
+
addTouchedPath: vi.fn(),
|
|
1477
|
+
}),
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
await pi.emit("session_start", {}, ctx);
|
|
1481
|
+
await pi.emit("turn_end", {}, ctx);
|
|
1482
|
+
|
|
1483
|
+
expect(pi.sentMessages).toHaveLength(1);
|
|
1484
|
+
const content = pi.sentMessages[0].message.content as string;
|
|
1485
|
+
expect(content).toContain("Failures:");
|
|
1486
|
+
expect(content).toContain("biome (exit 1) on /repo/src/broken.ts");
|
|
1487
|
+
expect(content).toContain("SyntaxError: Unexpected token at line 42");
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
it("does not send a steering message on empty flush", async () => {
|
|
1491
|
+
const pi = new TestPi();
|
|
1492
|
+
const ctx = createContext();
|
|
1493
|
+
|
|
1494
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1495
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1496
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1497
|
+
recordToolResult: vi.fn(),
|
|
1498
|
+
flushPrompt: vi.fn().mockResolvedValue({ groups: [] }),
|
|
1499
|
+
addTouchedPath: vi.fn(),
|
|
1500
|
+
}),
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
await pi.emit("session_start", {}, ctx);
|
|
1504
|
+
await pi.emit("turn_end", {}, ctx);
|
|
1505
|
+
|
|
1506
|
+
expect(pi.sentMessages).toHaveLength(0);
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
it("formats EventBus-sourced files at agent_end as safety net", async () => {
|
|
1510
|
+
const pi = new TestPi();
|
|
1511
|
+
const ctx = createContext();
|
|
1512
|
+
const reportFlushResult = vi.fn();
|
|
1513
|
+
let flushCallCount = 0;
|
|
1514
|
+
|
|
1515
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1516
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1517
|
+
createAutoformatter: vi.fn().mockReturnValue({
|
|
1518
|
+
recordToolResult: vi.fn(),
|
|
1519
|
+
flushPrompt: vi.fn().mockImplementation(() => {
|
|
1520
|
+
flushCallCount += 1;
|
|
1521
|
+
// First flush (turn_end) is empty; second (agent_end) has work
|
|
1522
|
+
if (flushCallCount === 1) {
|
|
1523
|
+
return Promise.resolve({ groups: [] });
|
|
1524
|
+
}
|
|
1525
|
+
return Promise.resolve(createFlushResult());
|
|
1526
|
+
}),
|
|
1527
|
+
addTouchedPath: vi.fn(),
|
|
1528
|
+
}),
|
|
1529
|
+
reportFlushResult,
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
await pi.emit("session_start", {}, ctx);
|
|
1533
|
+
// Simulate EventBus file added after turn_end
|
|
1534
|
+
await pi.emit("turn_end", {}, ctx);
|
|
1535
|
+
// Now agent_end flushes the EventBus-sourced file
|
|
1536
|
+
await pi.emit("agent_end", {}, ctx);
|
|
1537
|
+
|
|
1538
|
+
expect(reportFlushResult).toHaveBeenCalledTimes(2);
|
|
1539
|
+
// Second call has groups (from the safety-net flush)
|
|
1540
|
+
const secondResult = reportFlushResult.mock.calls[1][0];
|
|
1541
|
+
expect(secondResult.groups.length).toBeGreaterThan(0);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it("does not re-flush at agent_end after turn_end already flushed", async () => {
|
|
1545
|
+
const pi = new TestPi();
|
|
1546
|
+
const ctx = createContext();
|
|
1547
|
+
const autoformatter = {
|
|
1548
|
+
recordToolResult: vi.fn(),
|
|
1549
|
+
flushPrompt: vi.fn().mockResolvedValue(createFlushResult()),
|
|
1550
|
+
addTouchedPath: vi.fn(),
|
|
1551
|
+
};
|
|
1552
|
+
const reportFlushResult = vi.fn();
|
|
1553
|
+
|
|
1554
|
+
createAutoformatExtension(pi.asExtensionAPI(), {
|
|
1555
|
+
loadConfig: vi.fn().mockReturnValue(createLoadResult()),
|
|
1556
|
+
createAutoformatter: vi.fn().mockReturnValue(autoformatter),
|
|
1557
|
+
reportFlushResult,
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
await pi.emit("session_start", {}, ctx);
|
|
1561
|
+
await pi.emit(
|
|
1562
|
+
"tool_result",
|
|
1563
|
+
{
|
|
1564
|
+
toolName: "write",
|
|
1565
|
+
input: { path: "src/example.ts", content: "export {};" },
|
|
1566
|
+
isError: false,
|
|
1567
|
+
},
|
|
1568
|
+
ctx,
|
|
1569
|
+
);
|
|
1570
|
+
await pi.emit("turn_end", {}, ctx);
|
|
1571
|
+
reportFlushResult.mockClear();
|
|
1572
|
+
autoformatter.flushPrompt.mockClear();
|
|
1573
|
+
|
|
1574
|
+
// agent_end calls flush but queue is already empty
|
|
1575
|
+
autoformatter.flushPrompt.mockResolvedValue({ groups: [] });
|
|
1576
|
+
await pi.emit("agent_end", {}, ctx);
|
|
1577
|
+
|
|
1578
|
+
expect(autoformatter.flushPrompt).toHaveBeenCalledTimes(1);
|
|
1579
|
+
// The second flush should produce an empty result
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
describe("buildSteeringMessageContent", () => {
|
|
1584
|
+
it("returns undefined when no changedFiles and no failures", () => {
|
|
1585
|
+
const result = buildSteeringMessageContent({
|
|
1586
|
+
groups: [
|
|
355
1587
|
{
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1588
|
+
chain: ["prettier"],
|
|
1589
|
+
files: ["/repo/a.ts"],
|
|
1590
|
+
runs: [
|
|
1591
|
+
{
|
|
1592
|
+
formatterName: "prettier",
|
|
1593
|
+
command: ["prettier", "--write"],
|
|
1594
|
+
files: ["/repo/a.ts"],
|
|
1595
|
+
success: true,
|
|
1596
|
+
exitCode: 0,
|
|
1597
|
+
},
|
|
1598
|
+
],
|
|
1599
|
+
changedFiles: [],
|
|
359
1600
|
},
|
|
360
1601
|
],
|
|
361
|
-
|
|
362
|
-
);
|
|
1602
|
+
});
|
|
1603
|
+
expect(result).toBeUndefined();
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
it("returns undefined for empty groups", () => {
|
|
1607
|
+
expect(buildSteeringMessageContent({ groups: [] })).toBeUndefined();
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
it("lists a single changed file", () => {
|
|
1611
|
+
const result = buildSteeringMessageContent({
|
|
1612
|
+
groups: [
|
|
1613
|
+
{
|
|
1614
|
+
chain: ["prettier"],
|
|
1615
|
+
files: ["/repo/src/foo.ts"],
|
|
1616
|
+
runs: [
|
|
1617
|
+
{
|
|
1618
|
+
formatterName: "prettier",
|
|
1619
|
+
command: ["prettier", "--write"],
|
|
1620
|
+
files: ["/repo/src/foo.ts"],
|
|
1621
|
+
success: true,
|
|
1622
|
+
exitCode: 0,
|
|
1623
|
+
},
|
|
1624
|
+
],
|
|
1625
|
+
changedFiles: ["/repo/src/foo.ts"],
|
|
1626
|
+
},
|
|
1627
|
+
],
|
|
1628
|
+
});
|
|
1629
|
+
expect(result).toContain("[autoformat] Formatted 1 file(s)");
|
|
1630
|
+
expect(result).toContain("/repo/src/foo.ts");
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it("lists three changed files", () => {
|
|
1634
|
+
const result = buildSteeringMessageContent({
|
|
1635
|
+
groups: [
|
|
1636
|
+
{
|
|
1637
|
+
chain: ["prettier"],
|
|
1638
|
+
files: ["/repo/a.ts", "/repo/b.ts", "/repo/c.ts"],
|
|
1639
|
+
runs: [
|
|
1640
|
+
{
|
|
1641
|
+
formatterName: "prettier",
|
|
1642
|
+
command: [],
|
|
1643
|
+
files: ["/repo/a.ts", "/repo/b.ts", "/repo/c.ts"],
|
|
1644
|
+
success: true,
|
|
1645
|
+
exitCode: 0,
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
changedFiles: ["/repo/a.ts", "/repo/b.ts", "/repo/c.ts"],
|
|
1649
|
+
},
|
|
1650
|
+
],
|
|
1651
|
+
});
|
|
1652
|
+
expect(result).toContain("3 file(s)");
|
|
1653
|
+
expect(result).toContain("/repo/a.ts");
|
|
1654
|
+
expect(result).toContain("/repo/b.ts");
|
|
1655
|
+
expect(result).toContain("/repo/c.ts");
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
it("truncates file lists beyond 10 files", () => {
|
|
1659
|
+
const changedFiles = Array.from({ length: 11 }, (_, i) => `/repo/f${i}.ts`);
|
|
1660
|
+
const result = buildSteeringMessageContent({
|
|
1661
|
+
groups: [
|
|
1662
|
+
{
|
|
1663
|
+
chain: ["prettier"],
|
|
1664
|
+
files: changedFiles,
|
|
1665
|
+
runs: [
|
|
1666
|
+
{
|
|
1667
|
+
formatterName: "prettier",
|
|
1668
|
+
command: [],
|
|
1669
|
+
files: changedFiles,
|
|
1670
|
+
success: true,
|
|
1671
|
+
exitCode: 0,
|
|
1672
|
+
},
|
|
1673
|
+
],
|
|
1674
|
+
changedFiles,
|
|
1675
|
+
},
|
|
1676
|
+
],
|
|
1677
|
+
});
|
|
1678
|
+
expect(result).toContain("11 file(s)");
|
|
1679
|
+
expect(result).toContain("/repo/f9.ts");
|
|
1680
|
+
expect(result).not.toContain("/repo/f10.ts");
|
|
1681
|
+
expect(result).toContain("and 1 more");
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
it("includes failure details with stderr", () => {
|
|
1685
|
+
const result = buildSteeringMessageContent({
|
|
1686
|
+
groups: [
|
|
1687
|
+
{
|
|
1688
|
+
chain: ["prettier"],
|
|
1689
|
+
files: ["/repo/bad.ts"],
|
|
1690
|
+
runs: [
|
|
1691
|
+
{
|
|
1692
|
+
formatterName: "prettier",
|
|
1693
|
+
command: ["prettier", "--write"],
|
|
1694
|
+
files: ["/repo/bad.ts"],
|
|
1695
|
+
success: false,
|
|
1696
|
+
exitCode: 2,
|
|
1697
|
+
stderr: "SyntaxError: Unexpected token at line 42",
|
|
1698
|
+
},
|
|
1699
|
+
],
|
|
1700
|
+
changedFiles: [],
|
|
1701
|
+
},
|
|
1702
|
+
],
|
|
1703
|
+
});
|
|
1704
|
+
expect(result).toContain("Failures:");
|
|
1705
|
+
expect(result).toContain("prettier (exit 2) on /repo/bad.ts");
|
|
1706
|
+
expect(result).toContain("SyntaxError: Unexpected token at line 42");
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
it("includes both changed files and failures", () => {
|
|
1710
|
+
const result = buildSteeringMessageContent({
|
|
1711
|
+
groups: [
|
|
1712
|
+
{
|
|
1713
|
+
chain: ["prettier"],
|
|
1714
|
+
files: ["/repo/ok.ts", "/repo/bad.ts"],
|
|
1715
|
+
runs: [
|
|
1716
|
+
{
|
|
1717
|
+
formatterName: "prettier",
|
|
1718
|
+
command: ["prettier", "--write"],
|
|
1719
|
+
files: ["/repo/ok.ts"],
|
|
1720
|
+
success: true,
|
|
1721
|
+
exitCode: 0,
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
formatterName: "prettier",
|
|
1725
|
+
command: ["prettier", "--write"],
|
|
1726
|
+
files: ["/repo/bad.ts"],
|
|
1727
|
+
success: false,
|
|
1728
|
+
exitCode: 2,
|
|
1729
|
+
stderr: "SyntaxError",
|
|
1730
|
+
},
|
|
1731
|
+
],
|
|
1732
|
+
changedFiles: ["/repo/ok.ts"],
|
|
1733
|
+
},
|
|
1734
|
+
],
|
|
1735
|
+
});
|
|
1736
|
+
expect(result).toContain("[autoformat] Formatted 1 file(s)");
|
|
1737
|
+
expect(result).toContain("/repo/ok.ts");
|
|
1738
|
+
expect(result).toContain("Failures:");
|
|
1739
|
+
expect(result).toContain("prettier (exit 2)");
|
|
363
1740
|
});
|
|
364
1741
|
});
|