Skip to content

Commit 5dcd805

Browse files
authored
Merge pull request #60 from sonukapoor/codex/issue-59-output-tests
test: add output formatter and printer tests
2 parents 4fa5cd8 + 212189f commit 5dcd805

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

tests/output.test.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { jest } from "@jest/globals";
5+
import {
6+
getPrimaryParent,
7+
getRecommendedAction,
8+
logInfo,
9+
logWarn,
10+
printCacheSummary,
11+
serializeFinding,
12+
sortFindingsForOutput,
13+
summarizeNextAction,
14+
summarizeRisk,
15+
} from "../src/output/formatters.js";
16+
import {
17+
printActionSummary,
18+
printCompactOutput,
19+
printFinalStatus,
20+
printSummary,
21+
printTable,
22+
} from "../src/output/printers.js";
23+
import { stripAnsi } from "../src/utils/chalk.js";
24+
import type { Finding, OsvVuln, ScanInput } from "../src/types.js";
25+
26+
function createFinding(overrides?: Partial<Finding>): Finding {
27+
const vuln: OsvVuln = {
28+
id: "OSV-123",
29+
aliases: ["CVE-2026-0001"],
30+
summary: "Prototype pollution",
31+
severity: [{ score: "9.8" }],
32+
};
33+
34+
return {
35+
pkg: {
36+
name: "lodash",
37+
version: "4.17.20",
38+
ecosystem: "npm",
39+
paths: [["project", "app", "lodash"]],
40+
},
41+
vulnerabilities: [vuln],
42+
severity: "critical",
43+
cveAliases: ["CVE-2026-0001"],
44+
dependencyPaths: [["project", "app", "lodash"]],
45+
relationship: "transitive",
46+
firstFixedVersion: "4.17.21",
47+
recommendedParentUpgrade: {
48+
package: "app",
49+
currentVersion: "1.0.0",
50+
targetVersion: "1.1.0",
51+
viaPath: ["project", "app", "lodash"],
52+
vulnerablePackage: "lodash",
53+
confidence: "exact-direct-child",
54+
reason: "app@1.1.0 no longer allows lodash@4.17.20",
55+
},
56+
...overrides,
57+
};
58+
}
59+
60+
function createScanInput(mode: ScanInput["mode"] = "resolved-lockfile"): ScanInput {
61+
return {
62+
mode,
63+
source: mode === "manifest-fallback" ? "package-json" : "package-lock",
64+
filePath: "/tmp/package-lock.json",
65+
packages: [],
66+
notes: [],
67+
warnings: [],
68+
skippedDependencies: [],
69+
};
70+
}
71+
72+
function captureLogs(run: () => void): string[] {
73+
const logs: string[] = [];
74+
const spy = jest.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
75+
logs.push(args.map(arg => String(arg)).join(" "));
76+
});
77+
78+
try {
79+
run();
80+
} finally {
81+
spy.mockRestore();
82+
}
83+
84+
return logs.map(line => stripAnsi(line));
85+
}
86+
87+
describe("output formatters", () => {
88+
it("derives the primary parent and recommendation text from findings", () => {
89+
const finding = createFinding();
90+
91+
expect(getPrimaryParent(finding)).toBe("app");
92+
expect(getRecommendedAction(finding)).toContain("Upgrade app from 1.0.0 to 1.1.0");
93+
expect(summarizeRisk(finding)).toContain("specific parent upgrade target");
94+
expect(summarizeNextAction(finding)).toBe("Upgrade app 1.0.0 -> 1.1.0.");
95+
});
96+
97+
it("serializes findings with inferred vulnerability severity", () => {
98+
const finding = createFinding();
99+
const serialized = serializeFinding(finding);
100+
101+
expect(serialized).toMatchObject({
102+
package: "lodash",
103+
version: "4.17.20",
104+
severity: "critical",
105+
relationship: "transitive",
106+
firstFixedVersion: "4.17.21",
107+
primaryParent: "app",
108+
cves: ["CVE-2026-0001"],
109+
});
110+
expect(serialized.vulnerabilities[0]).toMatchObject({
111+
id: "OSV-123",
112+
severity: "critical",
113+
});
114+
});
115+
116+
it("sorts findings by severity and then package name", () => {
117+
const findings = [
118+
createFinding({ pkg: { name: "zlib", version: "1.0.0", ecosystem: "npm" }, severity: "medium" }),
119+
createFinding({ pkg: { name: "axios", version: "1.0.0", ecosystem: "npm" }, severity: "medium" }),
120+
createFinding({ pkg: { name: "chalk", version: "1.0.0", ecosystem: "npm" }, severity: "high" }),
121+
];
122+
123+
const sorted = sortFindingsForOutput(findings);
124+
expect(sorted.map(item => `${item.severity}:${item.pkg.name}`)).toEqual([
125+
"high:chalk",
126+
"medium:axios",
127+
"medium:zlib",
128+
]);
129+
});
130+
131+
it("prints cache and info/warn lines when output is not json", () => {
132+
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cve-lite-output-cache-"));
133+
fs.writeFileSync(
134+
path.join(cacheDir, "osv-vulns.json"),
135+
JSON.stringify({
136+
version: 2,
137+
createdAt: "2026-01-01T00:00:00.000Z",
138+
entries: { "OSV-123": { id: "OSV-123" }, "OSV-404": null },
139+
queryEntries: { "npm:lodash@4.17.20": ["OSV-123"] },
140+
}),
141+
"utf8",
142+
);
143+
144+
try {
145+
const lines = captureLogs(() => {
146+
printCacheSummary(cacheDir);
147+
logInfo("hello");
148+
logWarn("careful");
149+
});
150+
151+
expect(lines[0]).toContain("Cache: 1 package match record, 1 advisory detail record, 1 empty lookup");
152+
expect(lines[1]).toBe("hello");
153+
expect(lines[2]).toBe("careful");
154+
} finally {
155+
fs.rmSync(cacheDir, { recursive: true, force: true });
156+
}
157+
});
158+
});
159+
160+
describe("output printers", () => {
161+
it("prints an empty summary for clean manifest fallback scans", () => {
162+
const lines = captureLogs(() => {
163+
printSummary([], 2, createScanInput("manifest-fallback"));
164+
});
165+
166+
expect(lines).toEqual([
167+
"✓ No known OSV matches found for manifest fallback packages (2 exact direct dependencies checked)",
168+
]);
169+
});
170+
171+
it("prints a finding summary and action summary for vulnerable packages", () => {
172+
const findings = [
173+
createFinding(),
174+
createFinding({
175+
pkg: { name: "minimist", version: "0.0.8", ecosystem: "npm", paths: [["project", "minimist"]] },
176+
relationship: "direct",
177+
dependencyPaths: [["project", "minimist"]],
178+
severity: "high",
179+
firstFixedVersion: "1.2.8",
180+
recommendedParentUpgrade: undefined,
181+
vulnerabilities: [{ id: "OSV-456", severity: [{ score: "7.5" }] }],
182+
}),
183+
];
184+
185+
const lines = captureLogs(() => {
186+
printSummary(findings, 25, createScanInput());
187+
printActionSummary(findings);
188+
});
189+
190+
expect(lines[0]).toContain("✗ Found 2 package(s) with known OSV matches from package-lock");
191+
expect(lines.join("\n")).toContain("Quick take");
192+
expect(lines.join("\n")).toContain("1 vulnerable package look directly fixable in this project.");
193+
expect(lines.join("\n")).toContain("1 issue come through other dependencies.");
194+
});
195+
196+
it("prints a table and final status for findings", () => {
197+
const findings = [createFinding()];
198+
199+
const lines = captureLogs(() => {
200+
printTable(findings, "high");
201+
printFinalStatus(findings);
202+
});
203+
204+
expect(lines.join("\n")).toContain("Showing high+ findings in the main table. Use --all to show everything.");
205+
expect(lines.join("\n")).toContain("Package");
206+
expect(lines.join("\n")).toContain("lodash");
207+
expect(lines.join("\n")).toContain("OSV-123");
208+
expect(lines.join("\n")).toContain("✖ Scan complete. 1 issue found (1 critical, 0 high).");
209+
});
210+
211+
it("prints compact output for urgent findings and a clean final line for empty scans", () => {
212+
const linesWithFinding = captureLogs(() => {
213+
printCompactOutput([createFinding()]);
214+
});
215+
const emptyLines = captureLogs(() => {
216+
printCompactOutput([]);
217+
});
218+
219+
expect(linesWithFinding.join("\n")).toContain("📦 Vulnerabilities found");
220+
expect(linesWithFinding.join("\n")).toContain("🚀 Top Priority Fix");
221+
expect(linesWithFinding.join("\n")).toContain("Upgrade app 1.0.0 → 1.1.0");
222+
expect(emptyLines).toContain("✔ Scan complete. No known vulnerabilities found.");
223+
});
224+
});

0 commit comments

Comments
 (0)