pi-lens 3.2.0 → 3.3.1

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -10
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/format-service.test.js +245 -0
  5. package/clients/__tests__/formatters.test.js +271 -0
  6. package/clients/agent-behavior-client.test.js +94 -0
  7. package/clients/biome-client.test.js +144 -0
  8. package/clients/cache-manager.test.js +197 -0
  9. package/clients/complexity-client.test.js +234 -0
  10. package/clients/dependency-checker.test.js +60 -0
  11. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  12. package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
  13. package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
  14. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  15. package/clients/dispatch/dispatcher.format.test.js +46 -0
  16. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  17. package/clients/dispatch/dispatcher.test.js +116 -0
  18. package/clients/dispatch/runners/architect.test.js +138 -0
  19. package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
  20. package/clients/dispatch/runners/lsp.js +42 -5
  21. package/clients/dispatch/runners/oxlint.test.js +230 -0
  22. package/clients/dispatch/runners/pyright.test.js +98 -0
  23. package/clients/dispatch/runners/python-slop.test.js +203 -0
  24. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  25. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  26. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  27. package/clients/dispatch/utils/format-utils.js +1 -6
  28. package/clients/dispatch/utils/format-utils.ts +1 -6
  29. package/clients/dogfood.test.js +201 -0
  30. package/clients/file-kinds.test.js +169 -0
  31. package/clients/formatters.js +1 -1
  32. package/clients/go-client.test.js +127 -0
  33. package/clients/jscpd-client.test.js +127 -0
  34. package/clients/knip-client.test.js +112 -0
  35. package/clients/lsp/__tests__/client.test.js +310 -0
  36. package/clients/lsp/__tests__/client.test.ts +1 -46
  37. package/clients/lsp/__tests__/config.test.js +167 -0
  38. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  39. package/clients/lsp/__tests__/integration.test.js +127 -0
  40. package/clients/lsp/__tests__/launch.test.js +313 -0
  41. package/clients/lsp/__tests__/server.test.js +259 -0
  42. package/clients/lsp/__tests__/service.test.js +435 -0
  43. package/clients/lsp/client.js +32 -44
  44. package/clients/lsp/client.ts +36 -45
  45. package/clients/lsp/launch.js +11 -6
  46. package/clients/lsp/launch.ts +11 -6
  47. package/clients/lsp/server.js +27 -2
  48. package/clients/metrics-client.test.js +141 -0
  49. package/clients/ruff-client.test.js +132 -0
  50. package/clients/rust-client.test.js +108 -0
  51. package/clients/sanitize.test.js +177 -0
  52. package/clients/secrets-scanner.test.js +100 -0
  53. package/clients/test-runner-client.test.js +192 -0
  54. package/clients/todo-scanner.test.js +301 -0
  55. package/clients/type-coverage-client.test.js +105 -0
  56. package/clients/typescript-client.codefix.test.js +157 -0
  57. package/clients/typescript-client.test.js +105 -0
  58. package/commands/rate.test.js +119 -0
  59. package/index.ts +66 -72
  60. package/package.json +1 -1
  61. package/clients/bus/bus.js +0 -191
  62. package/clients/bus/bus.ts +0 -251
  63. package/clients/bus/events.js +0 -214
  64. package/clients/bus/events.ts +0 -279
  65. package/clients/bus/index.js +0 -8
  66. package/clients/bus/index.ts +0 -9
  67. package/clients/bus/integration.js +0 -158
  68. package/clients/bus/integration.ts +0 -227
  69. package/clients/dispatch/bus-dispatcher.js +0 -178
  70. package/clients/dispatch/bus-dispatcher.ts +0 -258
  71. package/clients/services/__tests__/effect-integration.test.ts +0 -111
  72. package/clients/services/effect-integration.js +0 -198
  73. package/clients/services/effect-integration.ts +0 -276
  74. package/clients/services/index.js +0 -7
  75. package/clients/services/index.ts +0 -8
  76. package/clients/services/runner-service.js +0 -134
  77. package/clients/services/runner-service.ts +0 -225
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tests for sanitize.ts
3
+ * Tool output sanitization
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { extractErrorMessage, normalizeWhitespace, sanitizeBiomeOutput, sanitizeLine, sanitizeOutput, sanitizeRuffOutput, sanitizeToolOutput, stripAnsi, truncateMessage, } from "./sanitize.js";
7
+ describe("stripAnsi", () => {
8
+ it("should remove ANSI color codes", () => {
9
+ expect(stripAnsi("\x1b[31mError\x1b[0m")).toBe("Error");
10
+ expect(stripAnsi("\x1b[1;32mSuccess\x1b[0m")).toBe("Success");
11
+ });
12
+ it("should remove extended ANSI codes", () => {
13
+ expect(stripAnsi("\x1b[1;2;3mText\x1b[0m")).toBe("Text");
14
+ });
15
+ it("should return original string if no ANSI codes", () => {
16
+ expect(stripAnsi("Plain text")).toBe("Plain text");
17
+ });
18
+ });
19
+ describe("normalizeWhitespace", () => {
20
+ it("should collapse multiple spaces", () => {
21
+ expect(normalizeWhitespace("Hello World")).toBe("Hello World");
22
+ });
23
+ it("should trim lines", () => {
24
+ expect(normalizeWhitespace(" Hello \n World ")).toBe("Hello\nWorld");
25
+ });
26
+ it("should remove empty lines", () => {
27
+ expect(normalizeWhitespace("Hello\n\n\nWorld")).toBe("Hello\nWorld");
28
+ });
29
+ });
30
+ describe("sanitizeLine", () => {
31
+ it("should remove common error prefixes", () => {
32
+ expect(sanitizeLine("[error] Something went wrong")).toBe("Something went wrong");
33
+ expect(sanitizeLine("error: Something went wrong")).toBe("Something went wrong");
34
+ });
35
+ it("should remove check/cross marks", () => {
36
+ expect(sanitizeLine("× Error message")).toBe("Error message");
37
+ expect(sanitizeLine("✓ Success message")).toBe("Success message");
38
+ });
39
+ it("should normalize whitespace", () => {
40
+ expect(sanitizeLine(" Error message ")).toBe("Error message");
41
+ });
42
+ });
43
+ describe("sanitizeOutput", () => {
44
+ it("should return empty string for empty input", () => {
45
+ expect(sanitizeOutput("")).toBe("");
46
+ expect(sanitizeOutput(" ")).toBe("");
47
+ });
48
+ it("should filter out detail lines", () => {
49
+ const output = "Error message\n at line 10\n at function foo";
50
+ const result = sanitizeOutput(output);
51
+ expect(result).toBe("Error message");
52
+ });
53
+ it("should remove ANSI codes and normalize", () => {
54
+ const output = "\x1b[31mError\x1b[0m\n\x1b[32mSuccess\x1b[0m";
55
+ const result = sanitizeOutput(output);
56
+ expect(result).toBe("Error\nSuccess");
57
+ });
58
+ });
59
+ describe("extractErrorMessage", () => {
60
+ it("should return undefined for empty input", () => {
61
+ expect(extractErrorMessage("")).toBeUndefined();
62
+ expect(extractErrorMessage(" ")).toBeUndefined();
63
+ });
64
+ it("should return first error line containing error indicator", () => {
65
+ // Note: extractErrorMessage sanitizes lines first, then checks isErrorLine
66
+ // Since "error:" prefix is removed by sanitizeLine, we need to use a keyword
67
+ // that remains in the sanitized line, like "failed" or "syntax"
68
+ const output = "Some info\nOperation failed: disk full\nMore info";
69
+ const result = extractErrorMessage(output);
70
+ expect(result).toBe("Operation failed: disk full");
71
+ });
72
+ it("should fall back to first non-empty line", () => {
73
+ const output = "info line\nanother info\nand more";
74
+ expect(extractErrorMessage(output)).toBe("info line");
75
+ });
76
+ it("should find error by keyword", () => {
77
+ const result = extractErrorMessage("warning: low severity\nfailed: operation");
78
+ // extractErrorMessage returns the first line with error indicators
79
+ // "failed:" contains "failed" which matches ERROR_INDICATORS
80
+ expect(result).toBe("failed: operation");
81
+ });
82
+ });
83
+ describe("truncateMessage", () => {
84
+ it("should not truncate short messages", () => {
85
+ const msg = "Short message";
86
+ expect(truncateMessage(msg, 100)).toBe(msg);
87
+ });
88
+ it("should truncate long messages", () => {
89
+ const msg = "A".repeat(200);
90
+ const result = truncateMessage(msg, 50);
91
+ expect(result).toBe(`${"A".repeat(49)}…`);
92
+ });
93
+ it("should use default max length of 140", () => {
94
+ const msg = "A".repeat(200);
95
+ const result = truncateMessage(msg);
96
+ expect(result.length).toBe(140);
97
+ });
98
+ });
99
+ describe("sanitizeToolOutput", () => {
100
+ it("should return empty result for empty input", () => {
101
+ const result = sanitizeToolOutput("");
102
+ expect(result.summary).toBeUndefined();
103
+ expect(result.details).toBeUndefined();
104
+ expect(result.truncated).toBe(false);
105
+ });
106
+ it("should extract summary and details", () => {
107
+ const output = "error: something failed\nline 2\nline 3\nline 4";
108
+ const result = sanitizeToolOutput(output);
109
+ // sanitizeLine removes "error:" prefix
110
+ expect(result.summary).toContain("something failed");
111
+ expect(result.details).toBeDefined();
112
+ });
113
+ it("should mark as truncated when too many lines", () => {
114
+ const lines = [];
115
+ for (let i = 0; i < 30; i++) {
116
+ lines.push(`error line ${i}`);
117
+ }
118
+ const result = sanitizeToolOutput(lines.join("\n"));
119
+ expect(result.truncated).toBe(true);
120
+ expect(result.details).toContain("more lines");
121
+ });
122
+ it("should respect max summary length", () => {
123
+ const output = `error: ${"x".repeat(200)}`;
124
+ const result = sanitizeToolOutput(output, 50);
125
+ expect(result.summary?.length).toBeLessThanOrEqual(50);
126
+ });
127
+ });
128
+ describe("sanitizeBiomeOutput", () => {
129
+ it("should parse JSON diagnostics", () => {
130
+ const json = JSON.stringify({
131
+ diagnostics: [
132
+ {
133
+ location: {
134
+ path: "file.ts",
135
+ span: { start: { line: 10, column: 0 } },
136
+ },
137
+ message: "Unexpected token",
138
+ },
139
+ ],
140
+ });
141
+ const result = sanitizeBiomeOutput(json);
142
+ expect(result).toContain("file.ts");
143
+ expect(result).toContain("11"); // line + 1
144
+ expect(result).toContain("Unexpected token");
145
+ });
146
+ it("should handle text output with errors", () => {
147
+ const output = "error: something went wrong\ninfo: some info";
148
+ const result = sanitizeBiomeOutput(output);
149
+ expect(result).toContain("error");
150
+ });
151
+ it("should return empty for empty input", () => {
152
+ expect(sanitizeBiomeOutput("")).toBe("");
153
+ });
154
+ });
155
+ describe("sanitizeRuffOutput", () => {
156
+ it("should parse JSON diagnostics", () => {
157
+ const json = JSON.stringify([
158
+ {
159
+ location: { row: 10, column: 5 },
160
+ code: "E501",
161
+ message: "Line too long",
162
+ },
163
+ ]);
164
+ const result = sanitizeRuffOutput(json);
165
+ expect(result).toContain("10:5");
166
+ expect(result).toContain("[E501]");
167
+ expect(result).toContain("Line too long");
168
+ });
169
+ it("should handle text output", () => {
170
+ const output = "file.py:10:5 [E501] Line too long";
171
+ const result = sanitizeRuffOutput(output);
172
+ expect(result).toContain("E501");
173
+ });
174
+ it("should return empty for empty input", () => {
175
+ expect(sanitizeRuffOutput("")).toBe("");
176
+ });
177
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatSecrets, scanForSecrets } from "./secrets-scanner.js";
3
+ describe("scanForSecrets", () => {
4
+ it("should detect Stripe/OpenAI keys (sk-*)", () => {
5
+ const content = `const apiKey = "sk-live-1234567890abcdefghij";`;
6
+ const findings = scanForSecrets(content);
7
+ expect(findings.length).toBe(1);
8
+ expect(findings[0].message).toContain("Stripe or OpenAI");
9
+ });
10
+ it("should detect GitHub personal tokens (ghp_*)", () => {
11
+ const content = `token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";`;
12
+ const findings = scanForSecrets(content);
13
+ expect(findings.length).toBe(1);
14
+ expect(findings[0].message).toContain("GitHub personal");
15
+ });
16
+ it("should detect AWS access keys (AKIA*)", () => {
17
+ const content = `const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";`;
18
+ const findings = scanForSecrets(content);
19
+ expect(findings.length).toBe(1);
20
+ expect(findings[0].message).toContain("AWS access key");
21
+ });
22
+ it("should detect private key material", () => {
23
+ const content = `-----BEGIN RSA PRIVATE KEY-----
24
+ MIIEpAIBAAKCAQEA...`;
25
+ const findings = scanForSecrets(content);
26
+ expect(findings.length).toBe(1);
27
+ expect(findings[0].message).toContain("Private key");
28
+ });
29
+ it("should detect hardcoded passwords", () => {
30
+ const content = `const config = { password: "hunter2" };`;
31
+ const findings = scanForSecrets(content);
32
+ expect(findings.length).toBe(1);
33
+ expect(findings[0].message).toContain("password");
34
+ });
35
+ it("should detect secrets in .env format", () => {
36
+ const content = `API_KEY=sk-live-1234567890abcdefghij
37
+ DATABASE_URL=postgres://localhost`;
38
+ const findings = scanForSecrets(content);
39
+ expect(findings.length).toBe(1);
40
+ // sk-* pattern catches this first (more specific)
41
+ expect(findings[0].message).toContain("Stripe or OpenAI");
42
+ });
43
+ it("should NOT flag safe content", () => {
44
+ const content = `
45
+ const name = "test";
46
+ const url = "https://example.com";
47
+ const port = 3000;
48
+ const message = "Hello world";
49
+ `;
50
+ const findings = scanForSecrets(content);
51
+ expect(findings.length).toBe(0);
52
+ });
53
+ it("should NOT flag env var references", () => {
54
+ const content = `const key = process.env.API_KEY;`;
55
+ const findings = scanForSecrets(content);
56
+ expect(findings.length).toBe(0);
57
+ });
58
+ it("should detect multiple secrets", () => {
59
+ const content = `
60
+ const sk = "sk-live-1234567890abcdefghij";
61
+ const gh = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";
62
+ `;
63
+ const findings = scanForSecrets(content);
64
+ expect(findings.length).toBe(2);
65
+ });
66
+ it("should report correct line numbers", () => {
67
+ const content = `line 1
68
+ line 2
69
+ const secret = "sk-live-1234567890abcdefghij";
70
+ line 4`;
71
+ const findings = scanForSecrets(content);
72
+ expect(findings.length).toBe(1);
73
+ expect(findings[0].line).toBe(3);
74
+ });
75
+ });
76
+ describe("formatSecrets", () => {
77
+ it("should format findings for terminal output", () => {
78
+ const findings = [
79
+ { line: 5, message: "Possible Stripe or OpenAI API key (sk-*)" },
80
+ ];
81
+ const output = formatSecrets(findings, "src/config.ts");
82
+ expect(output).toContain("STOP");
83
+ expect(output).toContain("1 potential secret(s)");
84
+ expect(output).toContain("L5");
85
+ expect(output).toContain("src/config.ts");
86
+ });
87
+ it("should return empty string for no findings", () => {
88
+ const output = formatSecrets([], "src/config.ts");
89
+ expect(output).toBe("");
90
+ });
91
+ it("should truncate at 5 findings", () => {
92
+ const findings = Array.from({ length: 10 }, (_, i) => ({
93
+ line: i + 1,
94
+ message: "Test secret",
95
+ }));
96
+ const output = formatSecrets(findings, "src/config.ts");
97
+ expect(output).toContain("10 potential secret(s)");
98
+ expect(output).toContain("... and 5 more");
99
+ });
100
+ });
@@ -0,0 +1,192 @@
1
+ import * as path from "node:path";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { TestRunnerClient } from "./test-runner-client.js";
4
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
5
+ describe("TestRunnerClient", () => {
6
+ let client;
7
+ let tmpDir;
8
+ let cleanup;
9
+ beforeEach(() => {
10
+ client = new TestRunnerClient();
11
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-test-runner-"));
12
+ });
13
+ afterEach(() => {
14
+ cleanup();
15
+ });
16
+ afterEach(() => {
17
+ cleanup();
18
+ });
19
+ describe("detectRunner", () => {
20
+ it("should detect vitest from config file", () => {
21
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
22
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
23
+ const result = client.detectRunner(tmpDir);
24
+ expect(result).not.toBeNull();
25
+ expect(result?.runner).toBe("vitest");
26
+ });
27
+ it("should detect jest from config file", () => {
28
+ createTempFile(tmpDir, "jest.config.js", "module.exports = {}");
29
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
30
+ const result = client.detectRunner(tmpDir);
31
+ expect(result).not.toBeNull();
32
+ expect(result?.runner).toBe("jest");
33
+ });
34
+ it("should detect pytest from config file", () => {
35
+ createTempFile(tmpDir, "pytest.ini", "[tool:pytest]");
36
+ createTempFile(tmpDir, "src/app.py", "x = 1");
37
+ const result = client.detectRunner(tmpDir);
38
+ expect(result).not.toBeNull();
39
+ expect(result?.runner).toBe("pytest");
40
+ });
41
+ it("should detect runner from node_modules", () => {
42
+ // Create a node_modules/vitest to simulate installed package
43
+ createTempFile(tmpDir, "node_modules/vitest/package.json", "{}");
44
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
45
+ const result = client.detectRunner(tmpDir);
46
+ // Should detect vitest from node_modules
47
+ expect(result).not.toBeNull();
48
+ });
49
+ it("should prefer vitest over jest when both exist", () => {
50
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
51
+ createTempFile(tmpDir, "jest.config.js", "module.exports = {}");
52
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
53
+ const result = client.detectRunner(tmpDir);
54
+ expect(result?.runner).toBe("vitest");
55
+ });
56
+ });
57
+ describe("findTestFile", () => {
58
+ it("should find test file with .test.ts suffix", () => {
59
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
60
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
61
+ createTempFile(tmpDir, "src/app.test.ts", "describe('app', () => {});");
62
+ const result = client.findTestFile(path.join(tmpDir, "src/app.ts"), tmpDir);
63
+ expect(result).not.toBeNull();
64
+ expect(result?.testFile).toContain("app.test.ts");
65
+ expect(result?.runner).toBe("vitest");
66
+ });
67
+ it("should find test file with .spec.ts suffix", () => {
68
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
69
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
70
+ createTempFile(tmpDir, "src/app.spec.ts", "describe('app', () => {});");
71
+ const result = client.findTestFile(path.join(tmpDir, "src/app.ts"), tmpDir);
72
+ expect(result).not.toBeNull();
73
+ expect(result?.testFile).toContain("app.spec.ts");
74
+ });
75
+ it("should find test file in __tests__ directory", () => {
76
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
77
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
78
+ createTempFile(tmpDir, "src/__tests__/app.test.ts", "describe('app', () => {});");
79
+ const result = client.findTestFile(path.join(tmpDir, "src/app.ts"), tmpDir);
80
+ expect(result).not.toBeNull();
81
+ expect(result?.testFile).toContain("__tests__");
82
+ });
83
+ it("should find test file in top-level tests/ directory", () => {
84
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
85
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
86
+ createTempFile(tmpDir, "tests/app.test.ts", "describe('app', () => {});");
87
+ const result = client.findTestFile(path.join(tmpDir, "src/app.ts"), tmpDir);
88
+ expect(result).not.toBeNull();
89
+ expect(result?.testFile).toContain(path.join("tests", "app.test.ts"));
90
+ });
91
+ it("should find pytest test file with test_ prefix", () => {
92
+ createTempFile(tmpDir, "pytest.ini", "[tool:pytest]");
93
+ createTempFile(tmpDir, "src/app.py", "x = 1");
94
+ createTempFile(tmpDir, "tests/test_app.py", "def test_app(): pass");
95
+ const result = client.findTestFile(path.join(tmpDir, "src/app.py"), tmpDir);
96
+ expect(result).not.toBeNull();
97
+ expect(result?.testFile).toContain("test_app.py");
98
+ expect(result?.runner).toBe("pytest");
99
+ });
100
+ it("should return null when no test file found", () => {
101
+ createTempFile(tmpDir, "vitest.config.ts", "export default {}");
102
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
103
+ const result = client.findTestFile(path.join(tmpDir, "src/app.ts"), tmpDir);
104
+ expect(result).toBeNull();
105
+ });
106
+ it("should find test file even without config (if runner installed)", () => {
107
+ // Simulate vitest installed in node_modules
108
+ createTempFile(tmpDir, "node_modules/vitest/package.json", "{}");
109
+ createTempFile(tmpDir, "src/app.ts", "export const app = {};");
110
+ createTempFile(tmpDir, "src/app.test.ts", "describe('app', () => {});");
111
+ const result = client.findTestFile(path.join(tmpDir, "src/app.ts"), tmpDir);
112
+ // Should find the test file since vitest is "installed"
113
+ expect(result).not.toBeNull();
114
+ });
115
+ });
116
+ describe("formatResult", () => {
117
+ it("should format passing tests", () => {
118
+ const result = {
119
+ file: "/test/app.test.ts",
120
+ sourceFile: "/test/app.ts",
121
+ runner: "vitest",
122
+ passed: 5,
123
+ failed: 0,
124
+ skipped: 0,
125
+ failures: [],
126
+ duration: 420,
127
+ };
128
+ const formatted = client.formatResult(result);
129
+ expect(formatted).toContain("✓");
130
+ expect(formatted).toContain("5/5 passed");
131
+ expect(formatted).toContain("0.42s");
132
+ });
133
+ it("should format failing tests", () => {
134
+ const result = {
135
+ file: "/test/app.test.ts",
136
+ sourceFile: "/test/app.ts",
137
+ runner: "vitest",
138
+ passed: 3,
139
+ failed: 2,
140
+ skipped: 0,
141
+ failures: [
142
+ {
143
+ name: "should add",
144
+ message: "expected 4, got 3",
145
+ location: "app.test.ts:10",
146
+ },
147
+ {
148
+ name: "should subtract",
149
+ message: "expected 1, got 2",
150
+ location: "app.test.ts:20",
151
+ },
152
+ ],
153
+ duration: 420,
154
+ };
155
+ const formatted = client.formatResult(result);
156
+ expect(formatted).toContain("✗");
157
+ expect(formatted).toContain("2/5 failed");
158
+ expect(formatted).toContain("should add");
159
+ expect(formatted).toContain("should subtract");
160
+ });
161
+ it("should format runner errors", () => {
162
+ const result = {
163
+ file: "/test/app.test.ts",
164
+ sourceFile: "/test/app.ts",
165
+ runner: "vitest",
166
+ passed: 0,
167
+ failed: 0,
168
+ skipped: 0,
169
+ failures: [],
170
+ duration: 0,
171
+ error: "Test file not found",
172
+ };
173
+ const formatted = client.formatResult(result);
174
+ expect(formatted).toContain("⚠");
175
+ expect(formatted).toContain("Could not run tests");
176
+ });
177
+ it("should return empty string for no tests", () => {
178
+ const result = {
179
+ file: "/test/app.test.ts",
180
+ sourceFile: "/test/app.ts",
181
+ runner: "vitest",
182
+ passed: 0,
183
+ failed: 0,
184
+ skipped: 0,
185
+ failures: [],
186
+ duration: 0,
187
+ };
188
+ const formatted = client.formatResult(result);
189
+ expect(formatted).toBe("");
190
+ });
191
+ });
192
+ });