diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index 09c990d7..58ef3531 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -3649,14 +3649,15 @@ Loads every ADR with `rules: true` in its frontmatter, executes the companion `. ## Options -| Option | Description | -| -------------- | -------------------------------------------------------------------- | -| `--staged` | Only check git-staged files (useful for pre-commit hooks) | -| `--base [ref]` | Compare changed files against a base ref (auto-detects when omitted) | -| `--json` | Machine-readable JSON output | -| `--ci` | GitHub Actions annotation format | -| `--adr ` | Only check rules from a specific ADR | -| `--verbose` | Show passing rules and timing info | +| Option | Description | +| -------------------- | --------------------------------------------------------------------------------- | +| `--staged` | Only check git-staged files (useful for pre-commit hooks) | +| `--base [ref]` | Compare changed files against a base ref (auto-detects when omitted) | +| `--json` | Machine-readable JSON output | +| `--ci` | GitHub Actions annotation format | +| `--adr ` | Only check rules from a specific ADR | +| `--verbose` | Show passing rules and timing info | +| `--max-warnings ` | Fail (exit 1) when the warning count exceeds `n`. Use `0` to fail on any warning. | ## Arguments @@ -3666,11 +3667,11 @@ Loads every ADR with `rules: true` in its frontmatter, executes the companion `. ## Exit codes -| Code | Meaning | -| ---- | -------------------------------------------------------------------- | -| 0 | All rules pass. No violations found. | -| 1 | One or more violations detected. | -| 2 | Rule execution error (e.g., malformed rule, security scanner block). | +| Code | Meaning | +| ---- | ----------------------------------------------------------------------- | +| 0 | All rules pass. No violations found. | +| 1 | One or more violations detected, or warnings exceeded `--max-warnings`. | +| 2 | Rule execution error (e.g., malformed rule, security scanner block). | ## Examples @@ -3698,6 +3699,12 @@ Check a single ADR: archgate check --adr ARCH-001 ``` +Treat any warning as a failure (useful in CI): + +```bash +archgate check --max-warnings 0 +``` + Check specific files (only matching ADRs run): ```bash @@ -3754,6 +3761,8 @@ During execution, `archgate check` emits warnings for common misconfigurations t These warnings appear in the standard output and do not affect the exit code. They also appear in JSON output when `--json` is used (as violations with `"severity": "warning"`). +By default, warnings (both diagnostics and rule-reported `"severity": "warning"` violations) never change the exit code. Pass `--max-warnings ` to make `archgate check` exit 1 when the total warning count exceeds `n`. `--max-warnings 0` fails on any warning. When the threshold is exceeded the JSON output's `warningsExceeded` field is `true` and `pass` is `false`. + ## JSON output format When `--json` is used, the output is a single JSON object: @@ -3768,6 +3777,7 @@ When `--json` is used, the output is a single JSON object: "errors": 1, "infos": 0, "ruleErrors": 0, + "warningsExceeded": false, "truncated": false, "results": [ { diff --git a/docs/src/content/docs/nb/reference/cli/check.mdx b/docs/src/content/docs/nb/reference/cli/check.mdx index cddbabf9..a5eeb27a 100644 --- a/docs/src/content/docs/nb/reference/cli/check.mdx +++ b/docs/src/content/docs/nb/reference/cli/check.mdx @@ -13,14 +13,15 @@ Laster inn hver ADR med `rules: true` i frontmatteren, kjører den tilhørende ` ## Valg -| Valg | Beskrivelse | -| -------------- | ---------------------------------------------------------------------------- | -| `--staged` | Sjekk kun git-stagede filer (nyttig for pre-commit-hooks) | -| `--base [ref]` | Sammenlign endrede filer mot en basisreferanse (autodetekteres hvis utelatt) | -| `--json` | Maskinlesbar JSON-utdata | -| `--ci` | GitHub Actions-annotasjonsformat | -| `--adr ` | Sjekk kun regler fra en bestemt ADR | -| `--verbose` | Vis beståtte regler og tidsinformasjon | +| Valg | Beskrivelse | +| -------------------- | -------------------------------------------------------------------------------------------------------- | +| `--staged` | Sjekk kun git-stagede filer (nyttig for pre-commit-hooks) | +| `--base [ref]` | Sammenlign endrede filer mot en basisreferanse (autodetekteres hvis utelatt) | +| `--json` | Maskinlesbar JSON-utdata | +| `--ci` | GitHub Actions-annotasjonsformat | +| `--adr ` | Sjekk kun regler fra en bestemt ADR | +| `--verbose` | Vis beståtte regler og tidsinformasjon | +| `--max-warnings ` | Mislykkes (kode 1) når antallet advarsler overskrider `n`. Bruk `0` for å mislykkes ved enhver advarsel. | ## Argumenter @@ -33,7 +34,7 @@ Laster inn hver ADR med `rules: true` i frontmatteren, kjører den tilhørende ` | Kode | Betydning | | ---- | -------------------------------------------------------------------------------- | | 0 | Alle regler bestått. Ingen brudd funnet. | -| 1 | Ett eller flere brudd oppdaget. | +| 1 | Ett eller flere brudd oppdaget, eller advarsler overskred `--max-warnings`. | | 2 | Feil ved regelkjøring (f.eks. feilformatert regel, sikkerhetsskannerblokkering). | ## Eksempler @@ -62,6 +63,12 @@ Sjekk en enkelt ADR: archgate check --adr ARCH-001 ``` +Behandle enhver advarsel som en feil (nyttig i CI): + +```bash +archgate check --max-warnings 0 +``` + Sjekk bestemte filer (bare matchende ADR-er kjøres): ```bash @@ -118,6 +125,8 @@ Under kjøring sender `archgate check` advarsler for vanlige feilkonfigurasjoner Disse advarslene vises i standardutdataene og påvirker ikke avslutningskoden. De vises også i JSON-utdata når `--json` brukes (som brudd med `"severity": "warning"`). +Som standard endrer advarsler (både diagnostikk og regelrapporterte brudd med `"severity": "warning"`) aldri avslutningskoden. Send `--max-warnings ` for å få `archgate check` til å avslutte med kode 1 når det totale antallet advarsler overskrider `n`. `--max-warnings 0` mislykkes ved enhver advarsel. Når terskelen overskrides, er feltet `warningsExceeded` i JSON-utdataene `true` og `pass` er `false`. + ## JSON-utdataformat Når `--json` brukes, er utdataene et enkelt JSON-objekt: @@ -132,6 +141,7 @@ Når `--json` brukes, er utdataene et enkelt JSON-objekt: "errors": 1, "infos": 0, "ruleErrors": 0, + "warningsExceeded": false, "truncated": false, "results": [ { diff --git a/docs/src/content/docs/pt-br/reference/cli/check.mdx b/docs/src/content/docs/pt-br/reference/cli/check.mdx index 47053c0c..d28f816e 100644 --- a/docs/src/content/docs/pt-br/reference/cli/check.mdx +++ b/docs/src/content/docs/pt-br/reference/cli/check.mdx @@ -13,14 +13,15 @@ Carrega cada ADR com `rules: true` no frontmatter, executa o arquivo `.rules.ts` ## Opções -| Opção | Descrição | -| -------------- | ----------------------------------------------------------------------------- | -| `--staged` | Verificar apenas arquivos no git stage (útil para hooks de pre-commit) | -| `--base [ref]` | Comparar arquivos alterados contra uma ref base (auto-detecta quando omitido) | -| `--json` | Saída JSON legível por máquina | -| `--ci` | Formato de anotação do GitHub Actions | -| `--adr ` | Verificar apenas regras de um ADR específico | -| `--verbose` | Mostrar regras aprovadas e informações de tempo | +| Opção | Descrição | +| -------------------- | -------------------------------------------------------------------------------------------------- | +| `--staged` | Verificar apenas arquivos no git stage (útil para hooks de pre-commit) | +| `--base [ref]` | Comparar arquivos alterados contra uma ref base (auto-detecta quando omitido) | +| `--json` | Saída JSON legível por máquina | +| `--ci` | Formato de anotação do GitHub Actions | +| `--adr ` | Verificar apenas regras de um ADR específico | +| `--verbose` | Mostrar regras aprovadas e informações de tempo | +| `--max-warnings ` | Falhar (código 1) quando a contagem de avisos exceder `n`. Use `0` para falhar com qualquer aviso. | ## Argumentos @@ -33,7 +34,7 @@ Carrega cada ADR com `rules: true` no frontmatter, executa o arquivo `.rules.ts` | Código | Significado | | ------ | ------------------------------------------------------------------------------------ | | 0 | Todas as regras passaram. Nenhuma violação encontrada. | -| 1 | Uma ou mais violações detectadas. | +| 1 | Uma ou mais violações detectadas, ou avisos excederam `--max-warnings`. | | 2 | Erro na execução de regra (ex.: regra malformada, bloqueio do scanner de segurança). | ## Exemplos @@ -62,6 +63,12 @@ Verificar um único ADR: archgate check --adr ARCH-001 ``` +Tratar qualquer aviso como falha (útil em CI): + +```bash +archgate check --max-warnings 0 +``` + Verificar arquivos específicos (apenas ADRs correspondentes são executados): ```bash @@ -98,6 +105,8 @@ Durante a execução, `archgate check` emite avisos para configurações incorre Esses avisos aparecem na saída padrão e não afetam o código de saída. Eles também aparecem na saída JSON quando `--json` é usado (como violações com `"severity": "warning"`). +Por padrão, avisos (tanto diagnósticos quanto violações de regra com `"severity": "warning"`) nunca alteram o código de saída. Passe `--max-warnings ` para fazer `archgate check` sair com código 1 quando a contagem total de avisos exceder `n`. `--max-warnings 0` falha com qualquer aviso. Quando o limite é excedido, o campo `warningsExceeded` da saída JSON é `true` e `pass` é `false`. + ## Formato da saída JSON Quando `--json` é usado, a saída é um único objeto JSON: @@ -112,6 +121,7 @@ Quando `--json` é usado, a saída é um único objeto JSON: "errors": 1, "infos": 0, "ruleErrors": 0, + "warningsExceeded": false, "truncated": false, "results": [ { diff --git a/docs/src/content/docs/reference/cli/check.mdx b/docs/src/content/docs/reference/cli/check.mdx index 4f47d83f..d96324eb 100644 --- a/docs/src/content/docs/reference/cli/check.mdx +++ b/docs/src/content/docs/reference/cli/check.mdx @@ -13,14 +13,15 @@ Loads every ADR with `rules: true` in its frontmatter, executes the companion `. ## Options -| Option | Description | -| -------------- | -------------------------------------------------------------------- | -| `--staged` | Only check git-staged files (useful for pre-commit hooks) | -| `--base [ref]` | Compare changed files against a base ref (auto-detects when omitted) | -| `--json` | Machine-readable JSON output | -| `--ci` | GitHub Actions annotation format | -| `--adr ` | Only check rules from a specific ADR | -| `--verbose` | Show passing rules and timing info | +| Option | Description | +| -------------------- | --------------------------------------------------------------------------------- | +| `--staged` | Only check git-staged files (useful for pre-commit hooks) | +| `--base [ref]` | Compare changed files against a base ref (auto-detects when omitted) | +| `--json` | Machine-readable JSON output | +| `--ci` | GitHub Actions annotation format | +| `--adr ` | Only check rules from a specific ADR | +| `--verbose` | Show passing rules and timing info | +| `--max-warnings ` | Fail (exit 1) when the warning count exceeds `n`. Use `0` to fail on any warning. | ## Arguments @@ -30,11 +31,11 @@ Loads every ADR with `rules: true` in its frontmatter, executes the companion `. ## Exit codes -| Code | Meaning | -| ---- | -------------------------------------------------------------------- | -| 0 | All rules pass. No violations found. | -| 1 | One or more violations detected. | -| 2 | Rule execution error (e.g., malformed rule, security scanner block). | +| Code | Meaning | +| ---- | ----------------------------------------------------------------------- | +| 0 | All rules pass. No violations found. | +| 1 | One or more violations detected, or warnings exceeded `--max-warnings`. | +| 2 | Rule execution error (e.g., malformed rule, security scanner block). | ## Examples @@ -62,6 +63,12 @@ Check a single ADR: archgate check --adr ARCH-001 ``` +Treat any warning as a failure (useful in CI): + +```bash +archgate check --max-warnings 0 +``` + Check specific files (only matching ADRs run): ```bash @@ -118,6 +125,8 @@ During execution, `archgate check` emits warnings for common misconfigurations t These warnings appear in the standard output and do not affect the exit code. They also appear in JSON output when `--json` is used (as violations with `"severity": "warning"`). +By default, warnings (both diagnostics and rule-reported `"severity": "warning"` violations) never change the exit code. Pass `--max-warnings ` to make `archgate check` exit 1 when the total warning count exceeds `n`. `--max-warnings 0` fails on any warning. When the threshold is exceeded the JSON output's `warningsExceeded` field is `true` and `pass` is `false`. + ## JSON output format When `--json` is used, the output is a single JSON object: @@ -132,6 +141,7 @@ When `--json` is used, the output is a single JSON object: "errors": 1, "infos": 0, "ruleErrors": 0, + "warningsExceeded": false, "truncated": false, "results": [ { diff --git a/src/commands/check.ts b/src/commands/check.ts index d68ec68c..d2c1092e 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Archgate import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { resolveBaseRef } from "../engine/git-files"; import { loadRuleAdrs } from "../engine/loader"; @@ -19,6 +20,11 @@ import { findProjectRoot } from "../helpers/paths"; import { getConfiguredBaseBranch } from "../helpers/project-config"; import { trackCheckResult } from "../helpers/telemetry"; +const maxWarningsOption = new Option( + "--max-warnings ", + "Fail (exit 1) when the number of warnings exceeds this threshold (0 = fail on any warning)" +).argParser((val) => parseInt(val, 10)); + export function registerCheckCommand(program: Command) { program .command("check") @@ -32,6 +38,7 @@ export function registerCheckCommand(program: Command) { ) .option("--adr ", "Only check rules from a specific ADR") .option("--verbose", "Show passing rules and timing info") + .addOption(maxWarningsOption) .argument("[files...]", "Only check rules relevant to these files") .action(async (files, opts) => { const projectRoot = findProjectRoot(); @@ -43,6 +50,16 @@ export function registerCheckCommand(program: Command) { return; } + const maxWarnings = opts.maxWarnings; + if ( + maxWarnings !== undefined && + (Number.isNaN(maxWarnings) || maxWarnings < 0) + ) { + logError("--max-warnings must be a non-negative integer"); + await exitWith(1); + return; + } + let loadResults; const loadStart = performance.now(); try { @@ -72,6 +89,7 @@ export function registerCheckCommand(program: Command) { errors: 0, infos: 0, ruleErrors: 0, + warningsExceeded: false, results: [], durationMs: 0, }, @@ -122,7 +140,7 @@ export function registerCheckCommand(program: Command) { // Build the summary once and share it with the reporters, telemetry, // and exit-code resolver. Previously each of those built its own // summary — 3 walks over the same result set. - const summary = buildSummary(result); + const summary = buildSummary(result, { maxWarnings }); if (opts.ci) { reportCI(result, summary); @@ -146,6 +164,7 @@ export function registerCheckCommand(program: Command) { used_base: Boolean(resolvedBase), used_file_filter: filterFiles.length > 0, used_adr_filter: Boolean(opts.adr), + used_max_warnings: maxWarnings !== undefined, files_scanned: filterFiles.length, load_duration_ms: loadDurationMs, check_duration_ms: Math.round(result.totalDurationMs), diff --git a/src/engine/context.ts b/src/engine/context.ts index 234c0337..0b8d10dc 100644 --- a/src/engine/context.ts +++ b/src/engine/context.ts @@ -191,6 +191,7 @@ const EMPTY_SUMMARY: ReportSummary = { errors: 0, infos: 0, ruleErrors: 0, + warningsExceeded: false, truncated: false, results: [], durationMs: 0, diff --git a/src/engine/reporter.ts b/src/engine/reporter.ts index d7b4f249..747e96b7 100644 --- a/src/engine/reporter.ts +++ b/src/engine/reporter.ts @@ -15,6 +15,8 @@ export interface ReportSummary { errors: number; infos: number; ruleErrors: number; + /** True when a `maxWarnings` threshold was set and the warning count exceeded it. */ + warningsExceeded: boolean; truncated: boolean; results: Array<{ adrId: string; @@ -41,6 +43,12 @@ export interface ReportSummary { interface BuildSummaryOptions { /** Maximum violations per rule. When exceeded, only the first N are kept. Omit or 0 for unlimited. */ maxViolationsPerRule?: number; + /** + * Maximum number of warnings tolerated before the check is considered failed. + * When the total warning count exceeds this threshold, `pass` becomes false and + * `warningsExceeded` is set. Omit for no limit (warnings never affect `pass`). + */ + maxWarnings?: number; } export function buildSummary( @@ -105,8 +113,11 @@ export function buildSummary( }; }); + const warningsExceeded = + options?.maxWarnings !== undefined && warnings > options.maxWarnings; + return { - pass: failed === 0 && ruleErrors === 0, + pass: failed === 0 && ruleErrors === 0 && !warningsExceeded, total: result.results.length, passed, failed, @@ -114,6 +125,7 @@ export function buildSummary( errors, infos, ruleErrors, + warningsExceeded, truncated: anyTruncated, results, durationMs: result.totalDurationMs, @@ -197,6 +209,15 @@ export function reportConsole( console.log(` ${status} - ${parts.join(", ")} ${durationStr}`); + if (summary.warningsExceeded) { + console.log( + styleText( + "yellow", + ` ${summary.warnings} warning(s) exceeded the configured --max-warnings threshold` + ) + ); + } + if (verbose) { const timeDetails = summary.results .map((r) => ` ${r.adrId}/${r.ruleId}: ${r.durationMs.toFixed(0)}ms`) @@ -269,6 +290,7 @@ export function getExitCode( if (summary) { if (summary.ruleErrors > 0) return 2; if (summary.failed > 0) return 1; + if (summary.warningsExceeded) return 1; return 0; } const hasErrors = result.results.some((r) => r.error); diff --git a/src/helpers/telemetry.ts b/src/helpers/telemetry.ts index 6ce044f5..2483f2a4 100644 --- a/src/helpers/telemetry.ts +++ b/src/helpers/telemetry.ts @@ -330,6 +330,7 @@ export function trackCheckResult(properties: { used_base: boolean; used_file_filter: boolean; used_adr_filter: boolean; + used_max_warnings: boolean; files_scanned?: number; load_duration_ms?: number; check_duration_ms?: number; diff --git a/tests/commands/check-action.test.ts b/tests/commands/check-action.test.ts index 94d0dd7a..6bbef236 100644 --- a/tests/commands/check-action.test.ts +++ b/tests/commands/check-action.test.ts @@ -47,6 +47,7 @@ const MOCK_SUMMARY: ReportSummary = { errors: 0, infos: 0, ruleErrors: 0, + warningsExceeded: false, truncated: false, results: [ { diff --git a/tests/engine/reporter.test.ts b/tests/engine/reporter.test.ts index d953345b..2a0c9caf 100644 --- a/tests/engine/reporter.test.ts +++ b/tests/engine/reporter.test.ts @@ -73,6 +73,37 @@ describe("reporter", () => { const result = makeResult({ error: "kaboom" }); expect(getExitCode(result)).toBe(2); }); + + test("returns 1 when warnings exceed maxWarnings threshold", () => { + const result = makeResult({ + violations: [ + { ruleId: "r", adrId: "a", message: "meh", severity: "warning" }, + ], + }); + const summary = buildSummary(result, { maxWarnings: 0 }); + expect(getExitCode(result, summary)).toBe(1); + }); + + test("returns 0 when warnings are within maxWarnings threshold", () => { + const result = makeResult({ + violations: [ + { ruleId: "r", adrId: "a", message: "meh", severity: "warning" }, + ], + }); + const summary = buildSummary(result, { maxWarnings: 1 }); + expect(getExitCode(result, summary)).toBe(0); + }); + + test("rule errors still take precedence over warning threshold", () => { + const result = makeResult({ + error: "kaboom", + violations: [ + { ruleId: "r", adrId: "a", message: "meh", severity: "warning" }, + ], + }); + const summary = buildSummary(result, { maxWarnings: 0 }); + expect(getExitCode(result, summary)).toBe(2); + }); }); describe("reportJSON", () => { @@ -268,5 +299,39 @@ describe("reporter", () => { expect(summary.results[0].violations).toHaveLength(100); expect(summary.truncated).toBe(false); }); + + test("warningsExceeded is false and pass stays true without maxWarnings", () => { + const violations = [ + { ruleId: "r", adrId: "a", message: "w", severity: "warning" as const }, + ]; + const summary = buildSummary(makeResult({ violations })); + expect(summary.warnings).toBe(1); + expect(summary.warningsExceeded).toBe(false); + expect(summary.pass).toBe(true); + }); + + test("warningsExceeded flips pass to false when warnings exceed maxWarnings", () => { + const violations = [ + { ruleId: "r", adrId: "a", message: "w", severity: "warning" as const }, + ]; + const summary = buildSummary(makeResult({ violations }), { + maxWarnings: 0, + }); + expect(summary.warningsExceeded).toBe(true); + expect(summary.pass).toBe(false); + // No error-severity violations, so failed count stays 0 + expect(summary.failed).toBe(0); + }); + + test("warnings at exactly maxWarnings do not exceed the threshold", () => { + const violations = [ + { ruleId: "r", adrId: "a", message: "w", severity: "warning" as const }, + ]; + const summary = buildSummary(makeResult({ violations }), { + maxWarnings: 1, + }); + expect(summary.warningsExceeded).toBe(false); + expect(summary.pass).toBe(true); + }); }); }); diff --git a/tests/helpers/telemetry.test.ts b/tests/helpers/telemetry.test.ts index c711c00e..c4833c69 100644 --- a/tests/helpers/telemetry.test.ts +++ b/tests/helpers/telemetry.test.ts @@ -152,11 +152,12 @@ describe("telemetry", () => { }); describe("trackCheckResult", () => { - test("captures check_completed event without throwing", async () => { + test("captures check_completed event for required and optional payloads", async () => { const { initTelemetry, trackCheckResult } = await import("../../src/helpers/telemetry"); await initTelemetry(); + // Required-only payload expect(() => trackCheckResult({ total_rules: 5, @@ -171,15 +172,10 @@ describe("telemetry", () => { used_base: false, used_file_filter: false, used_adr_filter: false, + used_max_warnings: false, }) ).not.toThrow(); - }); - - test("accepts optional fields (files_scanned, durations)", async () => { - const { initTelemetry, trackCheckResult } = - await import("../../src/helpers/telemetry"); - - await initTelemetry(); + // Payload including optional fields (files_scanned, durations) expect(() => trackCheckResult({ total_rules: 10, @@ -194,6 +190,7 @@ describe("telemetry", () => { used_base: true, used_file_filter: true, used_adr_filter: true, + used_max_warnings: true, files_scanned: 42, load_duration_ms: 15, check_duration_ms: 200, diff --git a/tests/integration/check-max-warnings.test.ts b/tests/integration/check-max-warnings.test.ts new file mode 100644 index 00000000..3666e38e --- /dev/null +++ b/tests/integration/check-max-warnings.test.ts @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Archgate +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { safeRmSync } from "../test-utils"; +import { + runCli, + createTempProject, + scaffoldProject, + writeAdr, + writeRules, + makeAdr, +} from "./cli-harness"; + +const WARNING_RULE = `export default { + rules: { + "soft-rule": { + description: "Emits a warning", + async check(ctx) { + for (const file of ctx.scopedFiles) { + ctx.report.warning({ message: "soft warning", file }); + } + }, + }, + }, +};`; + +describe("check --max-warnings integration", () => { + let dir: string; + + beforeEach(() => { + dir = createTempProject(); + }); + + afterEach(() => { + safeRmSync(dir); + }); + + function writeWarningAdr(id: string): void { + scaffoldProject(dir); + mkdirSync(join(dir, "src"), { recursive: true }); + writeFileSync(join(dir, "src", "warn.ts"), "const x = 1;\n"); + writeAdr( + dir, + `${id}.md`, + makeAdr({ id, title: "Warns", rules: true, files: ["src/**/*.ts"] }) + ); + writeRules(dir, `${id}.rules.ts`, WARNING_RULE); + } + + test("warnings do not affect exit code without the flag", async () => { + writeWarningAdr("WARN-001"); + const { exitCode, stdout } = await runCli(["check", "--json"], dir); + expect(exitCode).toBe(0); + const json = JSON.parse(stdout); + expect(json.pass).toBe(true); + expect(json.warnings).toBeGreaterThan(0); + expect(json.warningsExceeded).toBe(false); + }); + + test("--max-warnings 0 → exit 1 when a warning is reported", async () => { + writeWarningAdr("WARN-002"); + const { exitCode, stdout } = await runCli( + ["check", "--max-warnings", "0", "--json"], + dir + ); + expect(exitCode).toBe(1); + const json = JSON.parse(stdout); + expect(json.pass).toBe(false); + expect(json.warningsExceeded).toBe(true); + }); + + test("--max-warnings tolerates warnings up to the threshold", async () => { + writeWarningAdr("WARN-003"); + const { exitCode, stdout } = await runCli( + ["check", "--max-warnings", "5", "--json"], + dir + ); + expect(exitCode).toBe(0); + const json = JSON.parse(stdout); + expect(json.pass).toBe(true); + expect(json.warningsExceeded).toBe(false); + }); + + test("--max-warnings rejects a non-numeric value", async () => { + scaffoldProject(dir); + const { exitCode, stderr } = await runCli( + ["check", "--max-warnings", "abc"], + dir + ); + expect(exitCode).toBe(1); + expect(stderr.toLowerCase()).toContain("non-negative"); + }); +});