Skip to content
Merged
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
172 changes: 160 additions & 12 deletions packages/cli/src/commands/analyze.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { mkdtemp, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { upsertRegistry } from "../registry.js";
import {
checkFastPath,
detectCoverageReport,
isWorkingTreeDirty,
resolveCoverageEnabled,
resolveMaxSummariesCap,
resolveSbomEnabled,
resolveScanEnabled,
resolveSummariesEnabled,
} from "./analyze.js";

Expand Down Expand Up @@ -148,37 +152,66 @@ test("resolveMaxSummariesCap: seed of 5 yields a cap of 0 under the 10% rule", a
});

// ---------------------------------------------------------------------------
// resolveSummariesEnabled — env kill-switch + P04 default-on contract.
// resolveSummariesEnabled — fast-default contract: LLM summaries are opt-in.
// `codehub analyze` runs tree-sitter + SCIP + cochange phases only by default,
// so a fresh invocation never spends on Bedrock or blocks on a network hop.
// ---------------------------------------------------------------------------

test("resolveSummariesEnabled: default-on when both env and flag are absent (P04)", () => {
assert.equal(resolveSummariesEnabled(undefined, {}), true);
test("resolveSummariesEnabled: default-off when both env and flag are absent", () => {
assert.equal(resolveSummariesEnabled(undefined, {}), false);
});

test("resolveSummariesEnabled: explicit --summaries keeps it on", () => {
test("resolveSummariesEnabled: explicit --summaries opts in", () => {
assert.equal(resolveSummariesEnabled(true, {}), true);
});

test("resolveSummariesEnabled: explicit --no-summaries turns it off", () => {
test("resolveSummariesEnabled: explicit --no-summaries stays off", () => {
assert.equal(resolveSummariesEnabled(false, {}), false);
});

test("resolveSummariesEnabled: CODEHUB_BEDROCK_SUMMARIES=1 opts in (env-only)", () => {
// Operators can enable summaries for a whole CI job without editing every
// invocation. Only the literal "1" triggers — anything else is treated as
// absent, mirroring the kill-switch semantics below.
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_SUMMARIES: "1" }), true);
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_SUMMARIES: "0" }), false);
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_SUMMARIES: "" }), false);
});

test("resolveSummariesEnabled: CODEHUB_BEDROCK_DISABLED=1 kills the phase", () => {
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_DISABLED: "1" }), false);
});

test("resolveSummariesEnabled: env kill-switch wins over --summaries=true", () => {
test("resolveSummariesEnabled: kill-switch wins over --summaries=true", () => {
// Operator passed --summaries explicitly but the env var forces off.
// Required so CI / restricted environments can lock out Bedrock without
// auditing every invocation site.
assert.equal(resolveSummariesEnabled(true, { CODEHUB_BEDROCK_DISABLED: "1" }), false);
});

test("resolveSummariesEnabled: CODEHUB_BEDROCK_DISABLED=0 does not kill the phase", () => {
// Only the literal "1" triggers the kill-switch — anything else is a
// no-op. This keeps operator intent unambiguous.
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_DISABLED: "0" }), true);
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_DISABLED: "" }), true);
test("resolveSummariesEnabled: kill-switch wins over CODEHUB_BEDROCK_SUMMARIES=1", () => {
// Both env vars set → disable wins. This lets a CI environment pin the
// opt-in globally while still allowing per-job kill-switch overrides.
assert.equal(
resolveSummariesEnabled(undefined, {
CODEHUB_BEDROCK_SUMMARIES: "1",
CODEHUB_BEDROCK_DISABLED: "1",
}),
false,
);
});

test("resolveSummariesEnabled: --no-summaries wins over CODEHUB_BEDROCK_SUMMARIES=1", () => {
// Explicit CLI false beats env opt-in. Matches how --no-flag usually
// wins against ambient config everywhere else in the CLI.
assert.equal(resolveSummariesEnabled(false, { CODEHUB_BEDROCK_SUMMARIES: "1" }), false);
});

test("resolveSummariesEnabled: CODEHUB_BEDROCK_DISABLED=0 does not enable the phase", () => {
// Only the literal "1" on the opt-in var flips this; anything else leaves
// summaries in their (fast, off) default.
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_DISABLED: "0" }), false);
assert.equal(resolveSummariesEnabled(undefined, { CODEHUB_BEDROCK_DISABLED: "" }), false);
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -245,3 +278,118 @@ test("isWorkingTreeDirty: returns false when the git binary is unavailable", asy
else process.env["PATH"] = originalPath;
}
});

// ---------------------------------------------------------------------------
// resolveSbomEnabled — default on, --no-sbom opts out.
// ---------------------------------------------------------------------------

test("resolveSbomEnabled: default-on when flag is absent", () => {
assert.equal(resolveSbomEnabled(undefined), true);
});

test("resolveSbomEnabled: explicit true keeps it on", () => {
assert.equal(resolveSbomEnabled(true), true);
});

test("resolveSbomEnabled: explicit false turns it off (--no-sbom)", () => {
assert.equal(resolveSbomEnabled(false), false);
});

// ---------------------------------------------------------------------------
// resolveScanEnabled — default on, --no-scan opts out.
// ---------------------------------------------------------------------------

test("resolveScanEnabled: default-on when flag is absent", () => {
assert.equal(resolveScanEnabled(undefined), true);
});

test("resolveScanEnabled: explicit true keeps it on", () => {
assert.equal(resolveScanEnabled(true), true);
});

test("resolveScanEnabled: explicit false turns it off (--no-scan)", () => {
assert.equal(resolveScanEnabled(false), false);
});

// ---------------------------------------------------------------------------
// detectCoverageReport + resolveCoverageEnabled — auto-detect semantics.
// ---------------------------------------------------------------------------

test("detectCoverageReport: returns undefined when no report exists", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-none-"));
assert.equal(await detectCoverageReport(dir), undefined);
});

test("detectCoverageReport: finds coverage/lcov.info", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-lcov-"));
await mkdir(join(dir, "coverage"), { recursive: true });
await writeFile(join(dir, "coverage", "lcov.info"), "TN:\n");
assert.equal(await detectCoverageReport(dir), "coverage/lcov.info");
});

test("detectCoverageReport: finds top-level lcov.info", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-lcov2-"));
await writeFile(join(dir, "lcov.info"), "TN:\n");
assert.equal(await detectCoverageReport(dir), "lcov.info");
});

test("detectCoverageReport: finds coverage.xml (cobertura)", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-xml-"));
await writeFile(join(dir, "coverage.xml"), "<coverage/>\n");
assert.equal(await detectCoverageReport(dir), "coverage.xml");
});

test("detectCoverageReport: finds jacoco xml at the Gradle path", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-jacoco-"));
await mkdir(join(dir, "build", "reports", "jacoco", "test"), { recursive: true });
await writeFile(join(dir, "build", "reports", "jacoco", "test", "jacocoTestReport.xml"), "<r/>");
assert.equal(await detectCoverageReport(dir), "build/reports/jacoco/test/jacocoTestReport.xml");
});

test("detectCoverageReport: finds coverage.json (coverage.py)", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-json-"));
await writeFile(join(dir, "coverage.json"), "{}\n");
assert.equal(await detectCoverageReport(dir), "coverage.json");
});

test("detectCoverageReport: prefers coverage/lcov.info over top-level lcov.info", async () => {
// Probe order matches the phase's `CANDIDATES` array so the analyze
// wrapper and the phase agree on which report is the "one" when a
// repo has both.
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-both-"));
await mkdir(join(dir, "coverage"), { recursive: true });
await writeFile(join(dir, "coverage", "lcov.info"), "TN:\n");
await writeFile(join(dir, "lcov.info"), "TN:\n");
assert.equal(await detectCoverageReport(dir), "coverage/lcov.info");
});

test("resolveCoverageEnabled: explicit true short-circuits detection", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-force-on-"));
// No report on disk; explicit true still returns true so the phase
// runs and the operator sees the "no report found" warning.
assert.equal(await resolveCoverageEnabled(true, dir), true);
});

test("resolveCoverageEnabled: explicit false short-circuits detection", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-force-off-"));
await writeFile(join(dir, "lcov.info"), "TN:\n");
// Report IS on disk; explicit false still returns false so the phase
// is a silent no-op.
assert.equal(await resolveCoverageEnabled(false, dir), false);
});

test("resolveCoverageEnabled: undefined + no report → undefined (silent)", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-auto-none-"));
// No flag, no report → plumb `undefined` through so the phase is a
// silent no-op. Critically, this does NOT return `false` — that would
// still be equivalent behavior from the phase's perspective, but
// `undefined` is the documented "auto" sentinel and round-trips
// through `pipelineOptions` as omitted-key.
assert.equal(await resolveCoverageEnabled(undefined, dir), undefined);
});

test("resolveCoverageEnabled: undefined + report found → true (auto-on)", async () => {
const dir = await mkdtemp(join(tmpdir(), "och-analyze-cov-auto-on-"));
await writeFile(join(dir, "lcov.info"), "TN:\n");
assert.equal(await resolveCoverageEnabled(undefined, dir), true);
});
Loading
Loading