Skip to content
Open
Show file tree
Hide file tree
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
4 changes: 3 additions & 1 deletion tests/cli/scan-fix-verify-exitcode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ describe("cve-lite [path] --fix exit codes", () => {
lockfileVersion: 3,
packages: {
"": { name: "x" },
"node_modules/lodash": { version: "4.17.21" },
// Fixture-only name so no advisory ever matches it; the exit-0 assertion
// below must not depend on the synced advisory DB state (issue #726).
"node_modules/cve-lite-fixture-safe-pkg": { version: "1.0.0" },
},
}));
}
Expand Down
8 changes: 4 additions & 4 deletions tests/e2e/commands-and-exit-codes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCli, mkProject, rmProject, npmProject } from "./harness.js";
import { runCli, mkProject, rmProject, npmProject, SAFE_PKG } from "./harness.js";

// Track temp dirs created without mkProject (install-skill cwd, temp HOME) so
// afterEach can sweep them.
Expand Down Expand Up @@ -68,7 +68,7 @@ describe("commands + meta", () => {

it("--ratchet --check-overrides keeps override hygiene out of the baseline and notes the boundary", () => {
const dir = mkProject(
npmProject({ name: "ratchet-app", overrides: { gone: "1.0.0" } }, { lodash: "4.17.21" }),
npmProject({ name: "ratchet-app", overrides: { gone: "1.0.0" } }, { [SAFE_PKG]: "1.0.0" }),
);
try {
const r = runCli([dir, "--offline", "--ratchet", "--check-overrides"]);
Expand Down Expand Up @@ -157,7 +157,7 @@ describe("commands + meta", () => {
});

it("overrides <clean project> --json exits 0 with a findings array", () => {
const dir = mkProject(npmProject({ name: "x" }, { lodash: "4.17.21" }));
const dir = mkProject(npmProject({ name: "x" }, { [SAFE_PKG]: "1.0.0" }));
try {
const r = runCli(["overrides", dir, "--json"]);
expect(r.status).toBe(0);
Expand Down Expand Up @@ -227,7 +227,7 @@ describe("commands + meta", () => {

describe("exit codes", () => {
it("0: clean overrides run", () => {
const dir = mkProject(npmProject({ name: "x" }, { lodash: "4.17.21" }));
const dir = mkProject(npmProject({ name: "x" }, { [SAFE_PKG]: "1.0.0" }));
try {
const r = runCli(["overrides", dir, "--json"]);
expect(r.status).toBe(0);
Expand Down
48 changes: 29 additions & 19 deletions tests/e2e/fix-and-auditlog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
rmProject,
npmProject,
installedManifest,
SAFE_PKG,
SAFE_PLATFORM_PARENT,
SAFE_PLATFORM_BINARY,
} from "./harness.js";

/** Read the project package.json back off disk after the CLI mutated it. */
Expand All @@ -38,34 +41,41 @@ function readNdjson(path: string): Array<Record<string, unknown>> {

/**
* Project that triggers OA006 (coupled platform binary) in isolation. The override
* pins @esbuild/linux-x64 to 0.25.0, but its installed parent esbuild@0.25.12
* exact-pins the binary to 0.25.12. node_modules exists (esbuild is installed, so the
* detector is not pre-skipped) but the optional platform binary itself is NOT
* materialized - the common wrong-platform case - so there is no on-disk proof the
* override took. OA006 fires on the latent coupling. Per issue #37 it suppresses only
* when a materialized copy actually satisfies the override; with the binary absent,
* OA004 (needs a surpassing installed version) and OA008 (needs a below-floor copy)
* both stay silent, isolating the tier-2 (proposed) relocate fix.
* pins the platform binary to 0.0.1, but its installed parent exact-pins the binary
* to 0.0.2. node_modules exists (the parent is installed, so the detector is not
* pre-skipped) but the optional platform binary itself is NOT materialized - the
* common wrong-platform case - so there is no on-disk proof the override took. OA006
* fires on the latent coupling. Per issue #37 it suppresses only when a materialized
* copy actually satisfies the override; with the binary absent, OA004 (needs a
* surpassing installed version) and OA008 (needs a below-floor copy) both stay
* silent, isolating the tier-2 (proposed) relocate fix.
*
* The parent/binary use fixture-only names so the CVE scan stays clean regardless of
* the advisory DB state (issue #726). looksLikePlatformBinary() keys off the "linux"
* token, not the package identity, so the synthetic binary still drives OA006.
*/
function makeOa006Project(): string {
const dir = mkProject(
npmProject(
{ name: "oa6", overrides: { "@esbuild/linux-x64": "0.25.0" } },
{ esbuild: "0.25.12", "@esbuild/linux-x64": "0.25.0" }
{ name: "oa6", overrides: { [SAFE_PLATFORM_BINARY]: "0.0.1" } },
{ [SAFE_PLATFORM_PARENT]: "0.0.2", [SAFE_PLATFORM_BINARY]: "0.0.1" }
)
);
installedManifest(dir, "esbuild", {
name: "esbuild",
version: "0.25.12",
optionalDependencies: { "@esbuild/linux-x64": "0.25.12" },
installedManifest(dir, SAFE_PLATFORM_PARENT, {
name: SAFE_PLATFORM_PARENT,
version: "0.0.2",
optionalDependencies: { [SAFE_PLATFORM_BINARY]: "0.0.2" },
});
return dir;
}

/** Project with a single orphan override (OA001): `gone` is not in the lockfile. */
/** Project with a single orphan override (OA001): `gone` is not in the lockfile.
* The resolved dependency is a fixture-only name so the CVE scan stays clean
* regardless of the advisory DB state (issue #726); OA001 still fires because
* `gone` is absent from the tree, which is the only thing this fixture asserts. */
function makeOrphanProject(): string {
return mkProject(
npmProject({ name: "x", overrides: { gone: "1.0.0" } }, { lodash: "4.17.21" })
npmProject({ name: "x", overrides: { gone: "1.0.0" } }, { [SAFE_PKG]: "1.0.0" })
);
}

Expand Down Expand Up @@ -104,11 +114,11 @@ describe("e2e: overrides --fix tiering", () => {
const fix = runCli(["overrides", dir, "--fix"]);
expect(fix.status).toBe(0);

// The override must still be present and unrelocated: no /dependencies/esbuild
// floor was written, and the binary override value is unchanged.
// The override must still be present and unrelocated: no /dependencies floor
// was written, and the binary override value is unchanged.
const pkg = readPkg(dir);
const overrides = pkg.overrides as Record<string, unknown>;
expect(overrides["@esbuild/linux-x64"]).toBe("0.25.0");
expect(overrides[SAFE_PLATFORM_BINARY]).toBe("0.0.1");
expect(pkg.dependencies).toBeUndefined();

// And OA006 still fires on a re-run (the proposed fix was not consumed).
Expand Down
11 changes: 11 additions & 0 deletions tests/e2e/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import { fileURLToPath } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
export const CLI = join(here, "..", "..", "dist", "index.js");

// Fixture package names that no advisory will ever match, so a fixture that
// asserts a clean (exit 0) scan stays clean regardless of the synced advisory
// DB state. Real package names (lodash, express, ...) flip e2e exit-code
// assertions whenever a new CVE lands for the pinned version - see issue #726.
export const SAFE_PKG = "cve-lite-fixture-safe-pkg";
// A platform-binary pair for OA006 fixtures: the override fights the parent's
// exact pin on a native binary. looksLikePlatformBinary() keys off the "linux"
// token, not the package identity, so a fixture-only name triggers OA006 too.
export const SAFE_PLATFORM_PARENT = "cve-lite-fixture-native";
export const SAFE_PLATFORM_BINARY = "@cve-lite-fixture/linux-x64";

export interface CliResult {
stdout: string;
stderr: string;
Expand Down
15 changes: 9 additions & 6 deletions tests/e2e/validation-and-outputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { runCli, mkProject, rmProject, npmProject } from "./harness.js";
import { runCli, mkProject, rmProject, npmProject, SAFE_PKG } from "./harness.js";

const CI_ENV = { CI: "1" } as const;

Expand All @@ -26,7 +26,7 @@ const CI_ENV = { CI: "1" } as const;
function orphanOverrideProject(): string {
const files = npmProject(
{ name: "outproj", version: "1.0.0", overrides: { lodash: "4.17.21" } },
{ express: "4.18.2" },
{ [SAFE_PKG]: "1.0.0" },
);
return mkProject(files);
}
Expand All @@ -38,7 +38,7 @@ function findByExt(dir: string, ext: string): string[] {
describe("validation conflicts", () => {
let proj: string;
beforeAll(() => {
proj = mkProject(npmProject({ name: "vproj" }, { express: "4.18.2" }));
proj = mkProject(npmProject({ name: "vproj" }, { [SAFE_PKG]: "1.0.0" }));
});
afterAll(() => rmProject(proj));

Expand Down Expand Up @@ -71,14 +71,17 @@ describe("validation conflicts", () => {
});

describe("output channels", () => {
// Terminal default: human-readable scan summary on stdout, exit 0.
// Terminal default: human-readable scan output on stdout, exit 0. The fixture
// resolves a CVE-free name so the clean exit does not depend on the advisory DB
// state (issue #726); the assertions key off markers the terminal writer always
// prints, not the "Summary" block that only renders when findings exist.
it("terminal default (--offline) prints human-readable content to stdout", () => {
const proj = mkProject(npmProject({ name: "termproj" }, { express: "4.18.2" }));
const proj = mkProject(npmProject({ name: "termproj" }, { [SAFE_PKG]: "1.0.0" }));
try {
const r = runCli([proj, "--offline"], { env: CI_ENV });
expect(r.status).toBe(0);
expect(r.stdout).toContain("CVE Lite CLI");
expect(r.stdout).toContain("Summary");
expect(r.stdout).toContain("Scan complete");
// Not JSON: stdout should not parse as a JSON document.
expect(() => JSON.parse(r.stdout)).toThrow();
} finally {
Expand Down