|
| 1 | +import { execFile } from "node:child_process"; |
1 | 2 | import { |
2 | 3 | chmodSync, |
3 | 4 | mkdirSync, |
|
6 | 7 | writeFileSync, |
7 | 8 | } from "node:fs"; |
8 | 9 | import { tmpdir } from "node:os"; |
9 | | -import { join } from "node:path"; |
| 10 | +import { join, resolve } from "node:path"; |
| 11 | +import { promisify } from "node:util"; |
10 | 12 |
|
11 | 13 | import { afterEach, describe, expect, it, vi } from "vitest"; |
12 | 14 |
|
@@ -79,6 +81,41 @@ function makeMixedProject(options?: { |
79 | 81 | /** The committed config `makeMixedProject` writes, as `check` would resolve it. */ |
80 | 82 | const sgConfigPaths = [".taskless/sg/sgconfig.yml"]; |
81 | 83 |
|
| 84 | +const execFileAsync = promisify(execFile); |
| 85 | +const binPath = resolve(import.meta.dirname, "../dist/index.js"); |
| 86 | + |
| 87 | +/** Run the built CLI, tolerating a non-zero exit. */ |
| 88 | +async function runCli( |
| 89 | + args: string[] |
| 90 | +): Promise<{ stdout: string; exitCode: number }> { |
| 91 | + try { |
| 92 | + const { stdout } = await execFileAsync("node", [binPath, ...args]); |
| 93 | + return { stdout, exitCode: 0 }; |
| 94 | + } catch (error) { |
| 95 | + const execError = error as { stdout: string; code: number }; |
| 96 | + return { stdout: execError.stdout ?? "", exitCode: execError.code }; |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +/** The `--json` line, ignoring any preceding migration notice. */ |
| 101 | +function parseJson(stdout: string): { |
| 102 | + success: boolean; |
| 103 | + results: unknown[]; |
| 104 | + failures?: string[]; |
| 105 | + notices?: string[]; |
| 106 | +} { |
| 107 | + const line = stdout |
| 108 | + .trim() |
| 109 | + .split("\n") |
| 110 | + .findLast((entry) => entry.trim().startsWith("{")); |
| 111 | + return JSON.parse(line ?? "{}") as { |
| 112 | + success: boolean; |
| 113 | + results: unknown[]; |
| 114 | + failures?: string[]; |
| 115 | + notices?: string[]; |
| 116 | + }; |
| 117 | +} |
| 118 | + |
82 | 119 | describe("hasValeRules", () => { |
83 | 120 | it("is false for a scaffolded-but-empty rules directory", async () => { |
84 | 121 | // The common state after `taskless init`. Spawning Vale per check to |
@@ -295,3 +332,38 @@ describe("runEngines when Vale is unavailable", () => { |
295 | 332 | } |
296 | 333 | ); |
297 | 334 | }); |
| 335 | + |
| 336 | +describe("an engine failure under --json", () => { |
| 337 | + it("reaches the machine envelope, not only the suppressed warning", async () => { |
| 338 | + // `warn()` is a no-op under `--json`, so without a field for it the |
| 339 | + // consumer sees `{"success":false,"results":[]}` and cannot tell a broken |
| 340 | + // engine from a clean run. A CI script reading that treats a dead engine |
| 341 | + // as a pass. |
| 342 | + const cwd = makeMixedProject({ valeRules: false }); |
| 343 | + // An unparseable rule file: ast-grep exits non-zero and the engine fails |
| 344 | + // with no findings, the exact shape that used to read as clean. |
| 345 | + writeFileSync( |
| 346 | + join(cwd, ".taskless", "sg", "rules", "broken.yml"), |
| 347 | + "id: broken\nlanguage: javascript\nrule:\n bogusKey: nope\n" |
| 348 | + ); |
| 349 | + |
| 350 | + const { stdout, exitCode } = await runCli(["check", "-d", cwd, "--json"]); |
| 351 | + const output = parseJson(stdout); |
| 352 | + |
| 353 | + expect(exitCode).toBe(1); |
| 354 | + expect(output.success).toBe(false); |
| 355 | + expect(output.results).toEqual([]); |
| 356 | + expect(output.failures).toHaveLength(1); |
| 357 | + expect(output.failures?.[0]).toContain("sg engine failed"); |
| 358 | + }); |
| 359 | + |
| 360 | + it("omits both fields when nothing failed, as `skipped` does", async () => { |
| 361 | + const cwd = makeMixedProject({ valeRules: false }); |
| 362 | + const { stdout } = await runCli(["check", "-d", cwd, "doc.md", "--json"]); |
| 363 | + const output = parseJson(stdout); |
| 364 | + |
| 365 | + expect(output.success).toBe(true); |
| 366 | + expect(output.failures).toBeUndefined(); |
| 367 | + expect(output.notices).toBeUndefined(); |
| 368 | + }); |
| 369 | +}); |
0 commit comments