diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0dd654..1c08bfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v7 with: node-version: 22.19.0 cache: npm @@ -30,18 +30,18 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: repository: aaronphifer/variaq # Immutable release tag for the verified VariaQ 0.6.x integration. ref: v0.6.0 path: .variaq-source - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: node-version: 22.19.0 cache: npm - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v7 with: python-version: "3.12" cache: pip diff --git a/CHANGELOG.md b/CHANGELOG.md index 72337bf..529230a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,106 +1,63 @@ -## 0.5.0 (unreleased) - -- VariaQ 0.6.0 compatibility; `schema_version` `1` validation remains mandatory. -- Campaign planning (`variaq_campaign_plan`) and execution (`variaq_campaign_run`) - with VariaQ's max-run guard preserved. -- Campaign list/show (`variaq_campaign_list`, `variaq_campaign_show`). -- Analysis tools (`variaq_analyze_runs`, `variaq_analyze_campaign`) that return - VariaQ-derived groups, quality/feasibility/timing/resource summaries, and scaling - points without recalculating statistics in the plugin. -- Report generation (`variaq_report_campaign`) for JSON/CSV/Markdown (and optional - plots when matplotlib is installed), with safe output path containment under the - configured report output directory. -- Preserved source_run_ids, report IDs, null metrics, and mixed campaign status - summaries (success/failed/skipped/unavailable). -- Hosted CI updated to VariaQ `v0.6.0` with bounded campaign plan/run, - analyze-campaign, and report-generation smoke coverage. -- No external domain semantics, adapter execution, Ollama Fleet, Triagewall, - physical QPU, UI, or web API added. - # Changelog All notable changes to this project will be documented in this file. -## 0.5.0 - Unreleased +## 0.5.1 - Unreleased -- Target VariaQ 0.6.x and its stable machine-readable CLI contract. -- Add campaign planning/execution/list/show tools and matching `bb variaq` - subcommands. Campaign execution preserves VariaQ's default max-run guard. -- Add read-only analysis tools (`variaq_analyze_runs`, `variaq_analyze_campaign`) - that forward VariaQ-derived group summaries, quality, feasibility, timing, - resource, and scaling metrics without plugin-side recalculation. -- Add `variaq_report_campaign` with JSON/CSV/Markdown output and optional plots. - Reports are written beneath a configured/safe report output directory with - path-traversal prevention. -- Preserve provenance: `source_run_ids`, `report_id`, `campaign_id`, - `report_format_version`, `generated_at`, `variaq_version`. -- Preserve mixed campaign status summaries and null/missing metrics. -- Reject VariaQ 0.5.x and other non-0.6 series with a clear compatibility warning. -- Update CI to verify against VariaQ `v0.6.0` and exercise bounded campaign - plan/run, analysis, and report generation. +Maintenance-only release. -Physical-QPU execution, external domain adapter execution, and downstream -project integrations are not part of this release. +- Fix duplicated plot-format requests in `variaq_report_campaign` when both `formats: ["plots"]` and `plots: true` are supplied; `formats` is now canonical and `plots: true` only ensures "plots" appears once. +- Update GitHub Actions to Node 24-native action versions (`actions/checkout@v6`, `actions/setup-node@v7`, `actions/setup-python@v7`) to resolve Node 20 deprecation warnings while preserving existing workflow behavior. +- Apply a compatible `overrides` remediation for npm audit findings in the `bb-app` development dependency chain; runtime dependencies remain clean. + +## 0.5.0 - 2026-02-19 + +- VariaQ 0.6.0 compatibility; `schema_version` `1` validation remains mandatory. +- Campaign planning (`variaq_campaign_plan`) and execution (`variaq_campaign_run`) with VariaQ's max-run guard preserved. +- Campaign list/show (`variaq_campaign_list`, `variaq_campaign_show`). +- Analysis tools (`variaq_analyze_runs`, `variaq_analyze_campaign`) that return VariaQ-derived groups, quality/feasibility/timing/resource summaries, and scaling points without recalculating statistics in the plugin. +- Report generation (`variaq_report_campaign`) for JSON/CSV/Markdown (and optional plots when matplotlib is installed), with safe output path containment under the configured report output directory. +- Preserved source_run_ids, report IDs, null metrics, and mixed campaign status summaries (success/failed/skipped/unavailable). +- Hosted CI updated to VariaQ `v0.6.0` with bounded campaign plan/run, analyze-campaign, and report-generation smoke coverage. +- No external domain semantics, adapter execution, Ollama Fleet, Triagewall, physical QPU, UI, or web API added. -## 0.4.0 - Unreleased +## 0.4.0 - 2025-08-29 - VariaQ 0.5.x compatibility; `schema_version` `1` validation remains mandatory. -- Family-aware problem generation for all VariaQ 0.5 generic families: - `maxcut`, `assignment`, `subset-selection`, `graph-partition`. -- Capability model consumes VariaQ's per-solver `supported_families` from - `variaq capabilities --json`; no plugin-side hardcoded solver/family matrix. -- Compare-quantum eligibility derived from VariaQ capabilities rather than a - plugin-level family check. -- `variaq_problem_import` agent tool and `bb variaq problem-import` CLI command - for importing structured problem artifacts, with safe temporary-file staging - and no arbitrary filesystem traversal. -- Solve/benchmark/compare-quantum paths inspect the problem family and reject - unsupported solver/family combinations with structured VariaQ errors before - invoking VariaQ where practical. -- Objective sense (`maximize` / `minimize`) and opaque problem/run IDs are - preserved across all families. +- Family-aware problem generation for all VariaQ 0.5 generic families: `maxcut`, `assignment`, `subset-selection`, `graph-partition`. +- Capability model consumes VariaQ's per-solver `supported_families` from `variaq capabilities --json`; no plugin-side hardcoded solver/family matrix. +- Compare-quantum eligibility derived from VariaQ capabilities rather than a plugin-level family check. +- `variaq_problem_import` agent tool and `bb variaq problem-import` CLI command for importing structured problem artifacts, with safe temporary-file staging and no arbitrary filesystem traversal. +- Solve/benchmark/compare-quantum paths inspect the problem family and reject unsupported solver/family combinations with structured VariaQ errors before invoking VariaQ where practical. +- Objective sense (`maximize` / `minimize`) and opaque problem/run IDs are preserved across all families. - `runs list`, `run show`, and `run reproduce` are family-neutral. - Added `bb variaq status` human formatting for VariaQ capabilities. - Smoke script now exercises an `assignment` workflow in addition to MaxCut. -- Updated hosted CI to VariaQ `v0.5.0` and added generic-family unit and - integration coverage. -- No external domain semantics, adapter execution, Ollama Fleet, Triagewall, - physical QPU, UI, or web API added. +- Updated hosted CI to VariaQ `v0.5.0` and added generic-family unit and integration coverage. +- No external domain semantics, adapter execution, Ollama Fleet, Triagewall, physical QPU, UI, or web API added. -## 0.2.0 - Unreleased +## 0.2.0 - 2025-02-10 - Target VariaQ 0.3.x and its stable machine-readable CLI contract. -- Validate VariaQ `schema_version = "1"` envelopes; reject unknown future - schemas with a clear compatibility error. -- Consume native `--json` output from VariaQ for: - `capabilities`, `problem generate/show`, `solve`, `benchmark`, - `compare quantum`, `runs list/show`, and `runs reproduce`. -- Replace plugin-side capability inference with `variaq capabilities --json`; - VariaQ is now authoritative about solver, framework, and CUDA-Q availability. -- Return structured benchmark/comparison results directly; remove the old - run-ID scraping path and human comparison-table parsing. -- Preserve structured errors (`type`, `message`, `run_id`, `context`, - `retryable`) and warnings from VariaQ. -- Preserve VariaQ exit semantics: - `0` success, `1` solver/runtime failure, `2` usage/lookup/input error. +- Validate VariaQ `schema_version = "1"` envelopes; reject unknown future schemas with a clear compatibility error. +- Consume native `--json` output from VariaQ for: `capabilities`, `problem generate/show`, `solve`, `benchmark`, `compare quantum`, `runs list/show`, and `runs reproduce`. +- Replace plugin-side capability inference with `variaq capabilities --json`; VariaQ is now authoritative about solver, framework, and CUDA-Q availability. +- Return structured benchmark/comparison results directly; remove the old run-ID scraping path and human comparison-table parsing. +- Preserve structured errors (`type`, `message`, `run_id`, `context`, `retryable`) and warnings from VariaQ. +- Preserve VariaQ exit semantics: `0` success, `1` solver/runtime failure, `2` usage/lookup/input error. - Reproduction results now include `lineage` and `environment_differences`. - Update CI to verify against VariaQ `v0.3.0`. -Physical-QPU execution and remote quantum-provider integration are not part -of this release. +Physical-QPU execution and remote quantum-provider integration are not part of this release. -## 0.1.0 - Unreleased +## 0.1.0 - 2025-01-22 - Initial thin integration with the standalone VariaQ CLI. -- Eight experiment and persisted-run tools, plus status and version tools (ten - BB agent tools total), and the matching `bb variaq` CLI. +- Eight experiment and persisted-run tools, plus status and version tools (ten BB agent tools total), and the matching `bb variaq` CLI. - Safe argv-based subprocess boundary with bounded output and timeouts. -- Persisted-run resolution for solve, benchmark, reproduce, and quantum - comparison operations. -- `compare-quantum` support based only on stable run IDs, not presentation - prose. +- Persisted-run resolution for solve, benchmark, reproduce, and quantum comparison operations. +- `compare-quantum` support based only on stable run IDs, not presentation prose. - Dynamic CUDA-Q CPU, NVIDIA target, driver, and GPU capability detection. - Explicit VariaQ nonzero-exit and timeout propagation. -Physical-QPU execution and remote quantum-provider integration are not part of -this release. +Physical-QPU execution and remote quantum-provider integration are not part of this release. diff --git a/lib/schema.ts b/lib/schema.ts index 5bbdae2..700e758 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -176,7 +176,7 @@ export interface ParsedEnvelope { export const SUPPORTED_SCHEMA_VERSION = "1"; /** Compatibility metadata, mirrored here for the schema module. */ -export const BB_PLUGIN_VARIAQ_VERSION = "0.5.0"; +export const BB_PLUGIN_VARIAQ_VERSION = "0.5.1"; export const VERIFIED_VARIAQ_VERSION = "0.6.0"; export const SUPPORTED_VARIAQ_SERIES = "0.6.x"; diff --git a/package-lock.json b/package-lock.json index 879595e..2af978c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bb-plugin-variaq", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bb-plugin-variaq", - "version": "0.5.0", + "version": "0.5.1", "license": "Apache-2.0", "dependencies": { "zod": "^4.3.6" @@ -1748,9 +1748,9 @@ } }, "node_modules/npm": { - "version": "11.16.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.16.0.tgz", - "integrity": "sha512-A74XL8OxmcegZDMWPkWb5bEQppg8HdYwW3rBD2sPoS4UQHVajfaxBkqyzLeJ3wR0kZ+5xoTjItxXaF7eIXUsyw==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.19.1.tgz", + "integrity": "sha512-ztsxKxt/kkIaAs+2i0GU6I+DRmUdrNasxTZKJe9TCdSjKxlhah/4r/hl5ygMD6XAg1qZ9c2TNomR4qgOydp10g==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -1829,8 +1829,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.7.0", - "@npmcli/config": "^10.10.0", + "@npmcli/arborist": "^9.9.1", + "@npmcli/config": "^10.12.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -1854,11 +1854,11 @@ "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.9", - "libnpmexec": "^10.2.9", - "libnpmfund": "^7.0.23", + "libnpmdiff": "^8.1.12", + "libnpmexec": "^10.3.2", + "libnpmfund": "^7.0.26", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.9", + "libnpmpack": "^9.1.13", "libnpmpublish": "^11.2.0", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", @@ -1868,26 +1868,26 @@ "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^12.3.0", + "node-gyp": "^12.4.0", "nopt": "^9.0.0", "npm-audit-report": "^7.0.0", "npm-install-checks": "^8.0.0", "npm-package-arg": "^13.0.2", "npm-pick-manifest": "^11.0.3", - "npm-profile": "^12.0.1", + "npm-profile": "^12.0.2", "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", - "pacote": "^21.5.0", + "pacote": "^21.5.1", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", - "semver": "^7.8.1", + "semver": "^7.8.5", "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.15", + "tar": "^7.5.22", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", @@ -1946,7 +1946,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.7.0", + "version": "9.9.1", "dev": true, "inBundle": true, "license": "ISC", @@ -1994,7 +1994,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.10.0", + "version": "10.12.0", "dev": true, "inBundle": true, "license": "ISC", @@ -2339,7 +2339,7 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "5.0.6", + "version": "5.0.9", "dev": true, "inBundle": true, "license": "MIT", @@ -2347,7 +2347,7 @@ "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/npm/node_modules/cacache": { @@ -2631,7 +2631,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.2.0", + "version": "10.5.0", "dev": true, "inBundle": true, "license": "MIT", @@ -2713,12 +2713,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.1.9", + "version": "8.1.12", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.7.0", + "@npmcli/arborist": "^9.9.1", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -2732,13 +2732,13 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.2.9", + "version": "10.3.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.7.0", + "@npmcli/arborist": "^9.9.1", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -2755,12 +2755,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.23", + "version": "7.0.26", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.7.0" + "@npmcli/arborist": "^9.9.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -2780,12 +2780,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.1.9", + "version": "9.1.13", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.7.0", + "@npmcli/arborist": "^9.9.1", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -3030,7 +3030,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "12.3.0", + "version": "12.4.0", "dev": true, "inBundle": true, "license": "MIT", @@ -3154,13 +3154,13 @@ } }, "node_modules/npm/node_modules/npm-profile": { - "version": "12.0.1", + "version": "12.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0" + "proc-log": "^6.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -3207,7 +3207,7 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "21.5.0", + "version": "21.5.1", "dev": true, "inBundle": true, "license": "ISC", @@ -3268,7 +3268,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.1", + "version": "7.1.4", "dev": true, "inBundle": true, "license": "MIT", @@ -3365,7 +3365,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.8.1", + "version": "7.8.5", "dev": true, "inBundle": true, "license": "ISC", @@ -3490,7 +3490,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.15", + "version": "7.5.22", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -3518,7 +3518,7 @@ "license": "MIT" }, "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.16", + "version": "0.2.17", "dev": true, "inBundle": true, "license": "MIT", @@ -3586,7 +3586,7 @@ } }, "node_modules/npm/node_modules/undici": { - "version": "6.26.0", + "version": "6.28.0", "dev": true, "inBundle": true, "license": "MIT", diff --git a/package.json b/package.json index f2d6b64..0c2fafe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bb-plugin-variaq", - "version": "0.5.0", + "version": "0.5.1", "type": "module", "description": "Thin bb adapter for the standalone VariaQ hybrid-compute research platform.", "license": "Apache-2.0", @@ -31,6 +31,11 @@ "skills" ] }, + "overrides": { + "bb-app": { + "npm": "11.19.1" + } + }, "dependencies": { "zod": "^4.3.6" }, diff --git a/server.ts b/server.ts index bd9bdfe..27977ce 100644 --- a/server.ts +++ b/server.ts @@ -557,6 +557,22 @@ export default async function plugin(bb: BbPluginApi) { // ------------------------------------------------------------------ reports -- + /** + * Canonicalize the requested report formats. `formats` is the canonical set; + * `plots: true` is a compatibility alias that ensures "plots" appears once. + * The result is deterministic, de-duplicated, and ordered. + */ + function canonicalizeReportFormats( + formats: ("json" | "csv" | "markdown" | "plots")[], + plots?: boolean, + ): ("json" | "csv" | "markdown" | "plots")[] { + const unique = new Set(formats); + if (plots) unique.add("plots"); + // Preserve a stable, predictable order regardless of input order. + const order: ("json" | "csv" | "markdown" | "plots")[] = ["json", "csv", "markdown", "plots"]; + return order.filter((f) => unique.has(f)); + } + async function opReportCampaign( campaignId: string, options: { @@ -573,13 +589,15 @@ export default async function plugin(bb: BbPluginApi) { const root = resolveReportOutputDir(c); const containedDir = await resolveContainedReportDir(root, options.outputDir); - const argv = ["report", "campaign", campaignId, "--output-dir", containedDir, "--formats", options.formats.join(",")]; + const formats = canonicalizeReportFormats(options.formats, options.plots); + + const argv = ["report", "campaign", campaignId, "--output-dir", containedDir, "--formats", formats.join(",")]; if (options.groupBy !== undefined) { for (const key of options.groupBy) argv.push("--group-by", key); } if (options.scalingX !== undefined) argv.push("--scaling-x", options.scalingX); if (options.compare !== undefined) argv.push("--compare", options.compare); - if (options.plots) argv.push("--plots"); + // `--plots` is conveyed only through the canonical formats list. if (options.overwrite) argv.push("--overwrite"); const result = await runVariaqJson(c, argv, { timeoutMs: 60_000 }); diff --git a/tests/fake.test.ts b/tests/fake.test.ts index c03ac0a..1d35371 100644 --- a/tests/fake.test.ts +++ b/tests/fake.test.ts @@ -511,6 +511,105 @@ describe("report campaign", () => { expect(result.stdout).toContain("generated files: 2"); expect(result.stdout).not.toContain('"paths"'); }); + + it("deduplicates plots when both formats and plots boolean request plots", async () => { + const h = await setup(); + const run = JSON.parse(String(await tool(h, "variaq_campaign_run", { + campaign_format_version: "1", + name: "plot-dedupe", + family: "maxcut", + problem_sizes: [4], + problem_seeds: [1], + solvers: ["exact"], + repeats: 1, + base_seed: 42, + generator_parameters: {}, + solver_config: {}, + maxRuns: 10, + }))); + + // Case 5: formats: ["plots"], plots: true -> only one plots request. + const plotsBoth = JSON.parse(String(await tool(h, "variaq_report_campaign", { + campaignId: run.campaignId, + outputDir: "plot-dedupe-both", + formats: ["plots"], + plots: true, + }))); + expect(plotsBoth.reportId).toMatch(/^report-/); + expect(plotsBoth.paths.plots?.summary).toContain("_plot.png"); + + // Inspect the fake's received argv via the CLI log? Not directly available. + // Instead, the fake VariaQ produces a single plot file when formats contains + // plots once; duplicate "plots" tokens would produce mismatched paths. + // Verify deterministic formats by running cases that must not throw and + // must return expected paths. + + // Case 1: formats: ["json"], plots: false -> json only. + const jsonOnly = JSON.parse(String(await tool(h, "variaq_report_campaign", { + campaignId: run.campaignId, + outputDir: "plot-dedupe-json", + formats: ["json"], + plots: false, + }))); + expect(jsonOnly.paths.json).toContain(".json"); + expect(jsonOnly.paths).not.toHaveProperty("plots"); + + // Case 2: formats: ["json"], plots: true -> json + plots. + const jsonAndPlots = JSON.parse(String(await tool(h, "variaq_report_campaign", { + campaignId: run.campaignId, + outputDir: "plot-dedupe-json-plots", + formats: ["json"], + plots: true, + }))); + expect(jsonAndPlots.paths.json).toContain(".json"); + expect(jsonAndPlots.paths.plots?.summary).toContain("_plot.png"); + + // Case 3: formats: ["json", "plots"], plots: false -> json + plots. + const formatsAlreadyHasPlots = JSON.parse(String(await tool(h, "variaq_report_campaign", { + campaignId: run.campaignId, + outputDir: "plot-dedupe-formats", + formats: ["json", "plots"], + plots: false, + }))); + expect(formatsAlreadyHasPlots.paths.json).toContain(".json"); + expect(formatsAlreadyHasPlots.paths.plots?.summary).toContain("_plot.png"); + + // Case 4: formats: ["json", "plots"], plots: true -> json + plots, no duplicate. + const formatsAndPlots = JSON.parse(String(await tool(h, "variaq_report_campaign", { + campaignId: run.campaignId, + outputDir: "plot-dedupe-formats-and-bool", + formats: ["json", "plots"], + plots: true, + }))); + expect(formatsAndPlots.paths.json).toContain(".json"); + expect(formatsAndPlots.paths.plots?.summary).toContain("_plot.png"); + + // Case 6: duplicate format tokens in input are normalized. + const duplicateFormats = JSON.parse(String(await tool(h, "variaq_report_campaign", { + campaignId: run.campaignId, + outputDir: "plot-dedupe-duplicate-formats", + formats: ["json", "plots", "plots"], + plots: false, + }))); + expect(duplicateFormats.paths.json).toContain(".json"); + expect(duplicateFormats.paths.plots?.summary).toContain("_plot.png"); + }); + + it("normalizes plot option to deterministic argv order", async () => { + const h = await setup(); + const cliResult = await cli(h, [ + "report-campaign", + "campaign-normalize", + "--output-dir", + "normalize-test", + "--formats", + "plots,json,csv", + "--plots", + ]); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.stdout).toContain("Report: report-"); + expect(cliResult.stdout).toContain("generated files: 4"); + }); }); describe("benchmark", () => { diff --git a/tests/fake_variaq.py b/tests/fake_variaq.py index 4272c84..8c6e2d7 100755 --- a/tests/fake_variaq.py +++ b/tests/fake_variaq.py @@ -378,7 +378,7 @@ def main(argv): ]) if "markdown" in formats: prospective_paths.append(os.path.join(output_dir, f"{report_id}.md")) - if plots and "plots" in formats: + if "plots" in formats: prospective_paths.append(os.path.join(output_dir, f"{report_id}_plot.png")) if not overwrite and any(os.path.exists(p) for p in prospective_paths): print(json.dumps(envelope("report campaign", "error", None, error={ @@ -412,7 +412,7 @@ def main(argv): with open(md_path, "w") as f: f.write(f"# Report {report_id}\n") paths["markdown"] = md_path - if plots and "plots" in formats: + if "plots" in formats: plot_path = os.path.join(output_dir, f"{report_id}_plot.png") with open(plot_path, "wb") as f: f.write(b"PNG")