|
| 1 | +import { execFileSync } from "node:child_process"; |
1 | 2 | import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; |
2 | 3 | import { tmpdir } from "node:os"; |
3 | 4 | import { join } from "node:path"; |
4 | 5 | import { describe, expect, it } from "vitest"; |
5 | | -import { formatLintReport, readManifestTextForLint } from "../../scripts/loopover-config-lint"; |
| 6 | +import { formatLintJson, formatLintReport, readManifestTextForLint } from "../../scripts/loopover-config-lint"; |
6 | 7 | import { lintManifestText } from "../../src/selfhost/config-lint"; |
7 | 8 | import { MAX_FOCUS_MANIFEST_BYTES } from "../../src/signals/focus-manifest"; |
8 | 9 |
|
@@ -95,3 +96,75 @@ describe("readManifestTextForLint (#2923 regression)", () => { |
95 | 96 | }); |
96 | 97 | }); |
97 | 98 | }); |
| 99 | + |
| 100 | +describe("formatLintJson (#5931)", () => { |
| 101 | + it("serializes a clean manifest to JSON carrying path + the full SelfHostConfigLintResult", () => { |
| 102 | + const result = lintManifestText("wantedPaths:\n - src/\n"); |
| 103 | + const parsed = JSON.parse(formatLintJson(".loopover.yml", result)); |
| 104 | + expect(parsed).toEqual({ |
| 105 | + path: ".loopover.yml", |
| 106 | + ok: true, |
| 107 | + warnings: [], |
| 108 | + recognizedFields: ["wantedPaths"], |
| 109 | + summary: "Manifest parsed 1 recognized field.", |
| 110 | + }); |
| 111 | + }); |
| 112 | + |
| 113 | + it("serializes a manifest with an unknown top-level field, exposing warnings without echoing the raw value", () => { |
| 114 | + const result = lintManifestText("unknownSecretKey: super-secret-value\n"); |
| 115 | + const json = formatLintJson("private-config.yml", result); |
| 116 | + const parsed = JSON.parse(json); |
| 117 | + expect(parsed.path).toBe("private-config.yml"); |
| 118 | + expect(parsed.ok).toBe(false); |
| 119 | + expect(parsed.recognizedFields).toEqual([]); |
| 120 | + expect(parsed.warnings).toContain("Manifest contains unknown top-level field: unknownSecretKey."); |
| 121 | + expect(typeof parsed.summary).toBe("string"); |
| 122 | + // Same secret-redaction contract as the text report (#2906): the raw value never appears in the output. |
| 123 | + expect(json).not.toContain("super-secret-value"); |
| 124 | + }); |
| 125 | +}); |
| 126 | + |
| 127 | +// #5931: a real CLI-invocation test so a future edit can't silently break `--json` main() wiring (the flag/path |
| 128 | +// parsing + text-vs-json switch live in main()'s v8-ignored I/O block, so only a subprocess exercises them). |
| 129 | +describe("selfhost:config-lint --json CLI (#5931)", () => { |
| 130 | + const TSX_BIN = join(process.cwd(), "node_modules", ".bin", "tsx"); |
| 131 | + function runJson(manifestPath: string): { code: number; parsed: { path: string; ok: boolean; warnings: string[]; recognizedFields: string[]; summary: string } } { |
| 132 | + try { |
| 133 | + const out = execFileSync(TSX_BIN, ["scripts/loopover-config-lint.ts", manifestPath, "--json"], { encoding: "utf8" }); |
| 134 | + return { code: 0, parsed: JSON.parse(out) }; |
| 135 | + } catch (error) { |
| 136 | + // A failing manifest exits 1; execFileSync throws but still captures the JSON it printed to stdout. |
| 137 | + const e = error as { status?: number; stdout?: string }; |
| 138 | + return { code: e.status ?? 1, parsed: JSON.parse(e.stdout ?? "{}") }; |
| 139 | + } |
| 140 | + } |
| 141 | + function withTempManifest(contents: string, run: (path: string) => void): void { |
| 142 | + const dir = mkdtempSync(join(tmpdir(), "loopover-config-lint-json-")); |
| 143 | + try { |
| 144 | + const path = join(dir, "manifest.yml"); |
| 145 | + writeFileSync(path, contents); |
| 146 | + run(path); |
| 147 | + } finally { |
| 148 | + rmSync(dir, { recursive: true, force: true }); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + it("prints valid JSON with ok/warnings/recognizedFields/summary and exits 0 for a clean manifest", () => { |
| 153 | + withTempManifest("wantedPaths:\n - src/\n", (path) => { |
| 154 | + const { code, parsed } = runJson(path); |
| 155 | + expect(code).toBe(0); |
| 156 | + expect(parsed).toMatchObject({ path, ok: true, warnings: [], recognizedFields: ["wantedPaths"] }); |
| 157 | + expect(typeof parsed.summary).toBe("string"); |
| 158 | + }); |
| 159 | + }); |
| 160 | + |
| 161 | + it("prints valid JSON with warnings and exits 1 for a manifest with an unknown top-level field", () => { |
| 162 | + withTempManifest("unknownSecretKey: super-secret-value\n", (path) => { |
| 163 | + const { code, parsed } = runJson(path); |
| 164 | + expect(code).toBe(1); |
| 165 | + expect(parsed.ok).toBe(false); |
| 166 | + expect(parsed.warnings).toContain("Manifest contains unknown top-level field: unknownSecretKey."); |
| 167 | + expect(parsed.recognizedFields).toEqual([]); |
| 168 | + }); |
| 169 | + }); |
| 170 | +}); |
0 commit comments