Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions tests/output.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { jest } from "@jest/globals";
import {
getPrimaryParent,
getRecommendedAction,
logInfo,
logWarn,
printCacheSummary,
serializeFinding,
sortFindingsForOutput,
summarizeNextAction,
summarizeRisk,
} from "../src/output/formatters.js";
import {
printActionSummary,
printCompactOutput,
printFinalStatus,
printSummary,
printTable,
} from "../src/output/printers.js";
import { stripAnsi } from "../src/utils/chalk.js";
import type { Finding, OsvVuln, ScanInput } from "../src/types.js";

function createFinding(overrides?: Partial<Finding>): Finding {
const vuln: OsvVuln = {
id: "OSV-123",
aliases: ["CVE-2026-0001"],
summary: "Prototype pollution",
severity: [{ score: "9.8" }],
};

return {
pkg: {
name: "lodash",
version: "4.17.20",
ecosystem: "npm",
paths: [["project", "app", "lodash"]],
},
vulnerabilities: [vuln],
severity: "critical",
cveAliases: ["CVE-2026-0001"],
dependencyPaths: [["project", "app", "lodash"]],
relationship: "transitive",
firstFixedVersion: "4.17.21",
recommendedParentUpgrade: {
package: "app",
currentVersion: "1.0.0",
targetVersion: "1.1.0",
viaPath: ["project", "app", "lodash"],
vulnerablePackage: "lodash",
confidence: "exact-direct-child",
reason: "app@1.1.0 no longer allows lodash@4.17.20",
},
...overrides,
};
}

function createScanInput(mode: ScanInput["mode"] = "resolved-lockfile"): ScanInput {
return {
mode,
source: mode === "manifest-fallback" ? "package-json" : "package-lock",
filePath: "/tmp/package-lock.json",
packages: [],
notes: [],
warnings: [],
skippedDependencies: [],
};
}

function captureLogs(run: () => void): string[] {
const logs: string[] = [];
const spy = jest.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
logs.push(args.map(arg => String(arg)).join(" "));
});

try {
run();
} finally {
spy.mockRestore();
}

return logs.map(line => stripAnsi(line));
}

describe("output formatters", () => {
it("derives the primary parent and recommendation text from findings", () => {
const finding = createFinding();

expect(getPrimaryParent(finding)).toBe("app");
expect(getRecommendedAction(finding)).toContain("Upgrade app from 1.0.0 to 1.1.0");
expect(summarizeRisk(finding)).toContain("specific parent upgrade target");
expect(summarizeNextAction(finding)).toBe("Upgrade app 1.0.0 -> 1.1.0.");
});

it("serializes findings with inferred vulnerability severity", () => {
const finding = createFinding();
const serialized = serializeFinding(finding);

expect(serialized).toMatchObject({
package: "lodash",
version: "4.17.20",
severity: "critical",
relationship: "transitive",
firstFixedVersion: "4.17.21",
primaryParent: "app",
cves: ["CVE-2026-0001"],
});
expect(serialized.vulnerabilities[0]).toMatchObject({
id: "OSV-123",
severity: "critical",
});
});

it("sorts findings by severity and then package name", () => {
const findings = [
createFinding({ pkg: { name: "zlib", version: "1.0.0", ecosystem: "npm" }, severity: "medium" }),
createFinding({ pkg: { name: "axios", version: "1.0.0", ecosystem: "npm" }, severity: "medium" }),
createFinding({ pkg: { name: "chalk", version: "1.0.0", ecosystem: "npm" }, severity: "high" }),
];

const sorted = sortFindingsForOutput(findings);
expect(sorted.map(item => `${item.severity}:${item.pkg.name}`)).toEqual([
"high:chalk",
"medium:axios",
"medium:zlib",
]);
});

it("prints cache and info/warn lines when output is not json", () => {
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cve-lite-output-cache-"));
fs.writeFileSync(
path.join(cacheDir, "osv-vulns.json"),
JSON.stringify({
version: 2,
createdAt: "2026-01-01T00:00:00.000Z",
entries: { "OSV-123": { id: "OSV-123" }, "OSV-404": null },
queryEntries: { "npm:lodash@4.17.20": ["OSV-123"] },
}),
"utf8",
);

try {
const lines = captureLogs(() => {
printCacheSummary(cacheDir);
logInfo("hello");
logWarn("careful");
});

expect(lines[0]).toContain("Cache: 1 package match record, 1 advisory detail record, 1 empty lookup");
expect(lines[1]).toBe("hello");
expect(lines[2]).toBe("careful");
} finally {
fs.rmSync(cacheDir, { recursive: true, force: true });
}
});
});

describe("output printers", () => {
it("prints an empty summary for clean manifest fallback scans", () => {
const lines = captureLogs(() => {
printSummary([], 2, createScanInput("manifest-fallback"));
});

expect(lines).toEqual([
"✓ No known OSV matches found for manifest fallback packages (2 exact direct dependencies checked)",
]);
});

it("prints a finding summary and action summary for vulnerable packages", () => {
const findings = [
createFinding(),
createFinding({
pkg: { name: "minimist", version: "0.0.8", ecosystem: "npm", paths: [["project", "minimist"]] },
relationship: "direct",
dependencyPaths: [["project", "minimist"]],
severity: "high",
firstFixedVersion: "1.2.8",
recommendedParentUpgrade: undefined,
vulnerabilities: [{ id: "OSV-456", severity: [{ score: "7.5" }] }],
}),
];

const lines = captureLogs(() => {
printSummary(findings, 25, createScanInput());
printActionSummary(findings);
});

expect(lines[0]).toContain("✗ Found 2 package(s) with known OSV matches from package-lock");
expect(lines.join("\n")).toContain("Quick take");
expect(lines.join("\n")).toContain("1 vulnerable package look directly fixable in this project.");
expect(lines.join("\n")).toContain("1 issue come through other dependencies.");
});

it("prints a table and final status for findings", () => {
const findings = [createFinding()];

const lines = captureLogs(() => {
printTable(findings, "high");
printFinalStatus(findings);
});

expect(lines.join("\n")).toContain("Showing high+ findings in the main table. Use --all to show everything.");
expect(lines.join("\n")).toContain("Package");
expect(lines.join("\n")).toContain("lodash");
expect(lines.join("\n")).toContain("OSV-123");
expect(lines.join("\n")).toContain("✖ Scan complete. 1 issue found (1 critical, 0 high).");
});

it("prints compact output for urgent findings and a clean final line for empty scans", () => {
const linesWithFinding = captureLogs(() => {
printCompactOutput([createFinding()]);
});
const emptyLines = captureLogs(() => {
printCompactOutput([]);
});

expect(linesWithFinding.join("\n")).toContain("📦 Vulnerabilities found");
expect(linesWithFinding.join("\n")).toContain("🚀 Top Priority Fix");
expect(linesWithFinding.join("\n")).toContain("Upgrade app 1.0.0 → 1.1.0");
expect(emptyLines).toContain("✔ Scan complete. No known vulnerabilities found.");
});
});
Loading