diff --git a/.github/workflows/docusaurus-publish.yml b/.github/workflows/docusaurus-publish.yml
index 0472d10..bdf1d7f 100644
--- a/.github/workflows/docusaurus-publish.yml
+++ b/.github/workflows/docusaurus-publish.yml
@@ -73,20 +73,8 @@ jobs:
PR_BRANCH: maintenance/dependency-audit
PR_TITLE: 'Docusaurus maintenance: security audit and dependency upgrades'
VALIDATION_PASSED: ${{ inputs.validation-passed }}
- PR_FOOTER: 'The audit workflow performs the frozen install and production
- build;
-
- review their results above before merging. Failed candidates are drafts
-
- and fail this workflow. Successful reruns leave existing draft status
-
- for human review. Normal PR checks remain enabled. Post-merge CI and deployment
-
- use the normal workflows. Nothing is merged automatically.
-
-
- Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{
- github.run_id }}
+ PR_FOOTER: '[Workflow run](${{ github.server_url }}/${{ github.repository
+ }}/actions/runs/${{ github.run_id }})
'
uses: $/docusaurus/publish
diff --git a/docusaurus/legacy-regressions.json b/docusaurus/legacy-regressions.json
index dcd70c3..0e49c14 100644
--- a/docusaurus/legacy-regressions.json
+++ b/docusaurus/legacy-regressions.json
@@ -2,7 +2,7 @@
"tests/test_dependency_audit.py": {
"source_sha256": "54f474d342ff76ce731cf25a20742697248028b8b4821cf5be3621a41fcca4e7",
"transferred_path": "docusaurus/tests/test_dependency_audit.py",
- "transferred_sha256": "07404b1fe4d46b052a1bb79395652a86c98b8fad3e64e7ff1309813131879322"
+ "transferred_sha256": "a5a2c76b7c35719059e2f613a2cc6740913b7dfaa3402a7e36a65287a4664755"
},
"tests/test_dependency_audit_invariants.py": {
"source_sha256": "229cdaa093eac2696febd15e72786886b6c2619ef27e79b4153d4f39f07b3b27",
diff --git a/docusaurus/runtime/templates/dependency_audit.py b/docusaurus/runtime/templates/dependency_audit.py
index 786952c..58a3d7a 100644
--- a/docusaurus/runtime/templates/dependency_audit.py
+++ b/docusaurus/runtime/templates/dependency_audit.py
@@ -1099,7 +1099,13 @@ def format_audit(data: dict | str, lock: dict) -> str:
if isinstance(via, dict):
findings.append(
f" - [{via['title']}]({via['url']}); "
- f"{via['severity']}; affected range `{via['range']}`."
+ f"{via['severity']}; "
+ + (
+ f"locked `{name}@{versions(lock, via)}`; "
+ if source == "pnpm"
+ else ""
+ )
+ + f"affected range `{via['range']}`."
+ (f" {via['fixStatus']}" if via.get("fixStatus") else "")
)
else:
@@ -1161,6 +1167,119 @@ def format_remaining_audit(data: dict | str, lock: dict) -> str:
)
+def format_audit_summary(before: dict | str, after: dict | str, source: str) -> str:
+ if isinstance(after, str):
+ return f"**Security:** {after}"
+ counts = after["metadata"]["vulnerabilities"]
+ total = sum(counts.get(level, 0) for level in SEVERITY_ORDER)
+ label = "registry advisories" if source == "pnpm" else "affected package entries"
+ if total == 1:
+ label = "registry advisory" if source == "pnpm" else "affected package entry"
+ remaining = ", ".join(
+ f"{counts[level]} {level}" for level in SEVERITY_ORDER if counts.get(level)
+ )
+
+ def entries(data):
+ if isinstance(data, str):
+ return []
+ return [
+ (detail["url"] if source == "pnpm" else name, name, detail)
+ for name, vulnerability in data.get("vulnerabilities", {}).items()
+ for detail in (
+ [via for via in vulnerability.get("via", []) if isinstance(via, dict)]
+ if source == "pnpm"
+ else [vulnerability]
+ )
+ ]
+
+ old, current = entries(before), entries(after)
+ old_ids, current_ids = {row[0] for row in old}, {row[0] for row in current}
+ comparable = (
+ isinstance(before, dict)
+ and (
+ bool(old)
+ or not any(
+ before["metadata"]["vulnerabilities"].get(level, 0)
+ for level in SEVERITY_ORDER
+ )
+ )
+ and (bool(current) or total == 0)
+ )
+ resolved_ids = old_ids - current_ids if comparable else set()
+ new_ids = current_ids - old_ids if comparable else set()
+
+ def names(rows, selected):
+ packages = sorted({name for identity, name, _ in rows if identity in selected})
+ return ", ".join(f"`{name}`" for name in packages[:5]) + (
+ f", and {len(packages) - 5} more packages" if len(packages) > 5 else ""
+ )
+
+ resolved = (
+ f"{len(resolved_ids)} ({names(old, resolved_ids)})"
+ if resolved_ids
+ else "none"
+ if comparable
+ else "unconfirmed; initial or detailed audit evidence unavailable"
+ )
+ summary = f"**Resolved:** {resolved}. **Remaining:** {total} {label}" + (
+ f" ({remaining})." if remaining else "."
+ )
+ if new_ids:
+ summary += f" **New:** {len(new_ids)} ({names(current, new_ids)})."
+
+ def reason(detail):
+ status = detail.get("fixStatus", "")
+ if "Blocked by retained override" in status:
+ return "A retained override blocks the published patch."
+ if "no single patch satisfies all" in status:
+ parents = sorted(set(re.findall(r"`([^`]+)` requires `[^`]+`", status)))
+ if len(parents) == 1:
+ return f"Blocked by the `{parents[0].rsplit('@', 1)[0]}` version constraint."
+ return "No published patch fits all checked parent constraints."
+ if status.startswith("No published stable patch"):
+ return "No published patch matches the fix range."
+ if status.startswith("No patched version declared"):
+ return "The registry supplies no patched version."
+ if "could not be fully verified" in status:
+ return "Patch compatibility still needs verification."
+ if status.startswith("Published patch:"):
+ return "A published patch is available; selection needs review."
+ candidate = detail.get("fixAvailable")
+ if isinstance(candidate, dict) and candidate.get("isSemVerMajor"):
+ return "The offered fix requires a major upgrade."
+ if source == "npm" and not candidate:
+ return "No fix offered by npm."
+ return "Fix availability or resolution needs manual review."
+
+ groups: dict = {}
+ for identity, name, detail in current:
+ groups.setdefault(name, {})[identity] = detail
+ if groups:
+ summary += "\n\n| Still open | Why |\n| --- | --- |\n"
+ ordered = sorted(
+ groups,
+ key=lambda name: (
+ min(
+ SEVERITY_ORDER.get(detail["severity"], 5)
+ for detail in groups[name].values()
+ ),
+ name,
+ ),
+ )
+ for name in ordered[:5]:
+ details = list(groups[name].values())
+ severities = ", ".join(
+ f"{sum(detail['severity'] == level for detail in details)} {level}"
+ for level in SEVERITY_ORDER
+ if any(detail["severity"] == level for detail in details)
+ )
+ reasons = " ".join(dict.fromkeys(reason(detail) for detail in details))
+ summary += f"| `{name}` ({severities}) | {reasons} |\n"
+ if len(ordered) > 5:
+ summary += f"\n{len(ordered) - 5} more affected packages are listed in the audit details."
+ return summary
+
+
def eligible_version(directory, name, latest, snapshot, notes, deadline):
"""Select a stable release within the deployment's publication-age policy."""
result = run(directory, "view", name, "time", "--json", deadline=deadline)
@@ -1507,60 +1626,67 @@ def prepare(directory: Path, *, validation: dict | None = None) -> str:
"parent packages. These are not counts of distinct advisories or "
"demonstrated exploit paths."
)
- return (
- "# Documentation dependency audit\n\n"
- f"Project: `{directory}`; package manager: `{package_manager}`\n\n"
- f"Run: {datetime.now(timezone.utc).isoformat()}\n\n"
- "This is a best-effort upgrade drive. Remaining vulnerabilities are accepted "
- "between maintenance reviews; this report does not certify that the project is "
- "free of vulnerabilities. Review compatibility and validation before merging.\n\n"
- "## Security findings and fixes\n\n"
- "### Changes to affected dependencies\n\n"
- + (
- "Initial audit evidence was unavailable; any retained security-fix "
- "changes are unconfirmed and cannot be attributed to affected "
- "dependencies. The final audit below describes only the final "
- "dependency state.\n\n"
- if not initial_audit_available
- else ""
+ change_sections = []
+ if security_changes:
+ change_sections.append(
+ "## Security findings and fixes\n\n" + "\n".join(security_changes)
)
- + (
- "\n".join(security_changes)
- or (
- "Affected dependency changes cannot be confirmed because initial "
- "audit evidence was unavailable."
- if not initial_audit_available
- else "No upgrades to affected dependencies resolved."
- )
+ other_changes = [line for name, line in changes if name not in affected]
+ if other_changes:
+ change_sections.append(
+ "## Other stable dependency upgrades\n\n" + "\n".join(other_changes)
)
- + "\n\n"
- f"{count_explanation} Fix candidates can require manual migrations.\n\n"
- "## Other stable dependency upgrades\n\n"
- + (
- "\n".join(line for name, line in changes if name not in affected)
- or "No other direct dependency upgrades resolved."
+ if locked_updates:
+ change_sections.append(
+ "## Locked dependency changes\n\n" + "\n".join(locked_updates)
)
- + "\n\n"
- "## Locked dependency changes\n\n"
- + ("\n".join(locked_updates) or "No locked dependency version changes.")
- + "\n\n"
- f"## Remaining advisories\n\n{format_remaining_audit(after, after_lock)}\n\n"
+ report = (
+ f"`{directory}` ยท `{package_manager}`\n\n"
+ f"- **Changes:** {len(changes)} direct / {len(locked_updates)} locked dependencies.\n"
+ f"- {format_audit_summary(before, after, package_manager)}\n\n"
f"## Production build\n\n{build_status}\n\n"
- "## Remaining work\n\n"
- "Review retained overrides and peer constraints, transitive dependency fixes, "
- "and any major-version migration requirements. Build and installation logs "
- "are available in the workflow run.\n\n"
+ )
+ if not initial_audit_available:
+ report += "Initial audit evidence was unavailable; retained security-fix changes are unconfirmed.\n\n"
+ if change_sections:
+ report += (
+ "\nDependency changes
\n\n"
+ + "\n\n".join(change_sections)
+ + "\n\n \n\n"
+ )
+ before_audit = format_audit(before, before_lock)
+ after_audit = format_audit(after, after_lock)
+ # Keep the complete final findings once, including npm propagated packages.
+ remaining = (
+ (
+ "Registry advisory counts: "
+ if package_manager == "pnpm"
+ else "Affected package entries: "
+ )
+ + after_audit
+ if isinstance(after, dict) and after.get("vulnerabilities")
+ else format_remaining_audit(after, after_lock)
+ )
+ report += (
+ f"\nFull before/after {package_manager} audit
\n\n"
+ f"## Remaining advisories\n\n{remaining}\n\n"
+ (
- "\n".join(f"- {note}" for note in notes)
- or "No additional tool errors recorded."
+ "Before and after findings are unchanged.\n\n"
+ if before_audit == after_audit
+ else f"### Before\n\n{before_audit}\n\n"
+ )
+ + " \n\n"
+ "\nAudit evidence
\n\n"
+ f"Run: {datetime.now(timezone.utc).isoformat()}\n\n"
+ f"{count_explanation}\n\n"
+ + (
+ "\n".join(f"- {note}" for note in dict.fromkeys(notes)) + "\n\n"
+ if notes
+ else ""
)
- + "\n"
- f"\n\nFull before/after {package_manager} audit
\n\n"
- f"### Before\n\n{format_audit(before, before_lock)}\n\n"
- f"### After\n\n{format_audit(after, after_lock)}\n\n"
- " \n"
- f"\n\nCandidate dependency files (SHA-256)
\n\n{fingerprints}\n\n \n"
+ + f"### Candidate dependency files (SHA-256)\n\n{fingerprints}\n\n \n"
)
+ return report
def main() -> None:
diff --git a/docusaurus/tests/test_dependency_audit.py b/docusaurus/tests/test_dependency_audit.py
index 7f5d7b4..b82ed8e 100644
--- a/docusaurus/tests/test_dependency_audit.py
+++ b/docusaurus/tests/test_dependency_audit.py
@@ -96,7 +96,7 @@ def test_upgrade_keeps_manual_constraints_and_reports_unfixed_findings(
assert "high: 1, total: 1" in report
assert "npm fix candidate not offered by npm" in report
assert "## Production build\n\nPassed." in report
- assert "does not certify" in report
+ assert "**Remaining:** 1 affected package entry (1 high)" in report
assert "--force" not in [arg for command in commands for arg in command]
@@ -110,7 +110,7 @@ def test_failed_resolution_restores_original_files(npm_project, monkeypatch):
for name, content in original.items():
assert (npm_project / name).read_bytes() == content
assert "from the initial security fix attempt" in report
- assert "No other direct dependency upgrades resolved" in report
+ assert "**Changes:** 0 direct / 0 locked dependencies." in report
assert sum(command[:2] == ["audit", "fix"] for command in commands) == 1
@@ -408,7 +408,7 @@ def test_shared_time_budget_stops_commands_and_writes_partial_report(
assert commands == [["audit", "--json", "--audit", "--package-lock-only"]]
assert "Could not look up renderer" in report
assert "Not run: npm frozen install failed (exit 124)" in report
- assert "### After\n\nAudit unavailable (exit 124)." in report
+ assert "**Security:** Audit unavailable (exit 124)." in report
assert "time budget was exhausted" in report
@@ -486,7 +486,7 @@ def test_cli_entrypoint_prepares_selected_project(npm_project, monkeypatch):
["dependency-audit", "--directory", ".", "--report", str(report)],
)
runpy.run_path(audit_module["__file__"], run_name="__main__")
- assert "Security findings and fixes" in report.read_text()
+ assert "**Remaining:**" in report.read_text()
assert not (npm_project / "DEPENDENCY-AUDIT.md").exists()
manifest = json.loads((npm_project / "package.json").read_text())
assert manifest["dependencies"]["renderer"] == "2.0.0"
diff --git a/docusaurus/tests/test_docusaurus_standard.py b/docusaurus/tests/test_docusaurus_standard.py
index eae314d..0addaea 100644
--- a/docusaurus/tests/test_docusaurus_standard.py
+++ b/docusaurus/tests/test_docusaurus_standard.py
@@ -110,6 +110,96 @@ def invoke(command, **kwargs):
}
+@pytest.mark.parametrize("changed", [False, True], ids=["unchanged", "changed"])
+def test_pnpm_report_keeps_advisory_versions_in_complete_audit(
+ pnpm_project, monkeypatch, changed
+):
+ before_versions = ("1.0.0", "2.0.0")
+ after_versions = ("1.1.0", "2.1.0") if changed else before_versions
+ lockfile = pnpm_project / "pnpm-lock.yaml"
+ lock = yaml.safe_load(lockfile.read_bytes())
+ lock["packages"] = {
+ f"renderer@{version}": {} for version in (*before_versions, *after_versions)
+ }
+ lockfile.write_text(yaml.safe_dump(lock))
+
+ def findings(versions):
+ details = [
+ ("First advisory", "first", versions[0]),
+ ("Second advisory", "second", versions[1]),
+ ]
+ advisories = {
+ key: {
+ "module_name": "renderer",
+ "severity": "high",
+ "title": title,
+ "url": f"https://github.com/advisories/{key}",
+ "vulnerable_versions": "*",
+ "patched_versions": "<0.0.0",
+ "findings": [{"version": version}],
+ }
+ for title, key, version in details
+ }
+ return {
+ "metadata": {"vulnerabilities": {"high": 2, "total": 2}},
+ "advisories": advisories,
+ "package_manager": "pnpm",
+ "vulnerabilities": {
+ "renderer": {
+ "severity": "high",
+ "range": "*",
+ "nodes": [f"renderer@{version}" for _, _, version in details],
+ "via": [
+ {
+ "title": title,
+ "url": f"https://github.com/advisories/{key}",
+ "severity": "high",
+ "range": "*",
+ "nodes": [f"renderer@{version}"],
+ "patchedRange": "<0.0.0",
+ }
+ for title, key, version in details
+ ],
+ "fixAvailable": False,
+ }
+ },
+ }
+
+ audit_results = iter(
+ [findings(before_versions), findings(after_versions), findings(after_versions)]
+ )
+
+ def fake_audit(*args, **kwargs):
+ return next(audit_results)
+
+ module_globals = prepare.__globals__
+ monkeypatch.setitem(module_globals, "audit", fake_audit)
+ monkeypatch.setitem(
+ module_globals,
+ "fix_security",
+ lambda *args, **kwargs: subprocess.CompletedProcess([], 0, "", ""),
+ )
+ monkeypatch.setitem(
+ module_globals, "describe_pnpm_fixes", lambda *args, **kwargs: None
+ )
+
+ def fake_run(directory, *args, **kwargs):
+ stdout = '"1.0.0"' if args[:1] == ("view",) else ""
+ return subprocess.CompletedProcess(args, 0, stdout, "")
+
+ monkeypatch.setitem(module_globals, "run", fake_run)
+ report = prepare(pnpm_project)
+
+ for title, before_version, after_version in (
+ ("First advisory", before_versions[0], after_versions[0]),
+ ("Second advisory", before_versions[1], after_versions[1]),
+ ):
+ lines = [line for line in report.splitlines() if f"[{title}]" in line]
+ assert lines
+ assert any(f"locked `renderer@{before_version}`" in line for line in lines)
+ assert any(f"locked `renderer@{after_version}`" in line for line in lines)
+
+
def fake_npm(monkeypatch, *, install=0, build=0, audit_error=False):
def invoke(command, *, cwd, **kwargs):
args = command[4:]
@@ -445,7 +535,7 @@ def invoke(command, *, cwd, **kwargs):
return subprocess.CompletedProcess(command, 0, stdout, "")
monkeypatch.setattr(subprocess, "run", invoke)
- summary = prepare(project).split("")[0]
+ summary = prepare(project).split("Full before/after")[0]
if changed:
old = ", ".join(sorted({"1.0.0", initial_patched}))
@@ -456,7 +546,7 @@ def invoke(command, *, cwd, **kwargs):
assert "No upgrades to affected dependencies resolved." not in summary
else:
assert "- leaf:" not in summary
- assert "No upgrades to affected dependencies resolved." in summary
+ assert "## Security findings and fixes" not in summary
@pytest.mark.parametrize("package_manager", ["npm", "pnpm"])
@@ -540,7 +630,7 @@ def invoke(command, *, cwd, **kwargs):
return subprocess.CompletedProcess(command, 0, stdout, "")
monkeypatch.setattr(subprocess, "run", invoke)
- summary = prepare(project).split("")[0]
+ summary = prepare(project).split("Full before/after")[0]
if changed:
assert (
"leaf: dependency paths changed; installed versions `1.0.0, 2.0.0`"
@@ -550,7 +640,7 @@ def invoke(command, *, cwd, **kwargs):
assert "No upgrades to affected dependencies resolved." not in summary
else:
assert "- leaf:" not in summary
- assert "No upgrades to affected dependencies resolved." in summary
+ assert "## Security findings and fixes" not in summary
@pytest.mark.parametrize("package_manager", ["npm", "pnpm"])
@@ -659,12 +749,11 @@ def invoke(command, *, cwd, **kwargs):
return subprocess.CompletedProcess(command, 0, stdout, "")
monkeypatch.setattr(subprocess, "run", invoke)
- summary = prepare(project).split("")[0]
- security, other = summary.split("## Other stable dependency upgrades")
- other = other.split("## Locked dependency changes")[0]
+ summary = prepare(project).split("Full before/after")[0]
+ security = summary.split("## Locked dependency changes")[0]
final = "3.0.0" if patched else "2.0.0"
assert f"- renderer: `1.0.0` โ `{final}`;" in security
- assert "- renderer:" not in other
+ assert "## Other stable dependency upgrades" not in summary
assert (
"still reported" in security
if not patched
@@ -720,7 +809,7 @@ def invoke(command, *, cwd, **kwargs):
return subprocess.CompletedProcess(command, 0, stdout, "")
monkeypatch.setattr(subprocess, "run", invoke)
- summary = prepare(pnpm_project).split("")[0]
+ summary = prepare(pnpm_project).split("Full before/after")[0]
result = json.loads(manifest_path.read_text())
assert result["dependencies"]["renderer"] == "2.0.0"
assert result[section]["second"] == "2.0.0"
@@ -763,16 +852,148 @@ def test_summary_precedes_collapsed_full_audit(npm_project, monkeypatch):
report = prepare(npm_project)
summary, details = report.split("", 1)
- assert "renderer: `1.0.0` โ `2.0.0`" in summary
- assert "## Remaining advisories" in summary
+ assert "**Changes:** 1 direct / 0 locked dependencies." in summary
+ assert "**Resolved:** none. **Remaining:** 0 affected package entries." in summary
assert "## Production build\n\nPassed." in summary
- assert summary.index("Security findings") < summary.index("Other stable")
+ assert len(summary) < 600
+ assert "renderer: `1.0.0` โ `2.0.0`" in details
+ assert "No upgrades to affected dependencies" not in report
+ assert "No additional tool errors recorded" not in report
+ assert "This is a best-effort" not in report
assert "### Before" not in summary and "### After" not in summary
- assert "### Before" in details and "### After" in details
+ assert "Before and after findings are unchanged." in details
assert details.endswith(" \n")
assert not (npm_project / "dependency-audit.md").exists()
+@pytest.mark.parametrize(
+ "package_manager, label",
+ [("pnpm", "registry advisories"), ("npm", "affected package entries")],
+)
+def test_compact_security_summary_keeps_severity_and_count_meaning(
+ package_manager, label
+):
+ summarize = audit_module["format_audit_summary"]
+ before = {"metadata": {"vulnerabilities": {"critical": 3, "high": 2, "total": 5}}}
+ after = {
+ "metadata": {
+ "vulnerabilities": {"critical": 2, "high": 1, "moderate": 1, "total": 4}
+ }
+ }
+ result = summarize(before, after, package_manager)
+ assert f"**Remaining:** 4 {label} (2 critical, 1 high, 1 moderate)." in result
+ assert "**Resolved:** unconfirmed" in result
+ assert "Audit unavailable" in summarize(
+ before, "Audit unavailable (exit 1).", package_manager
+ )
+
+
+def test_unchanged_findings_are_rendered_once(npm_project, monkeypatch):
+ fake_npm(monkeypatch)
+ report = prepare(npm_project)
+ assert report.count("No known vulnerabilities reported by npm.") == 1
+ assert report.count("") == report.count(" ")
+
+
+@pytest.fixture
+def security_findings():
+ def make(rows):
+ data = {"metadata": {"vulnerabilities": {}}, "vulnerabilities": {}}
+ counts = data["metadata"]["vulnerabilities"]
+ for name, identity, severity, status in rows:
+ counts[severity] = counts.get(severity, 0) + 1
+ package = data["vulnerabilities"].setdefault(name, {"via": []})
+ package["via"].append(
+ {
+ "url": f"https://github.com/advisories/{identity}",
+ "severity": severity,
+ "fixStatus": status,
+ }
+ )
+ return data
+
+ return make
+
+
+def test_security_outcomes_distinguish_resolved_and_new_with_equal_counts(
+ security_findings,
+):
+ before = security_findings([("old-package", "old", "high", "")])
+ after = security_findings([("new-package", "new", "high", "")])
+ result = audit_module["format_audit_summary"](before, after, "pnpm")
+ assert "**Resolved:** 1 (`old-package`)" in result
+ assert "**New:** 1 (`new-package`)" in result
+ assert "**Remaining:** 1 registry advisory" in result
+
+
+def test_security_outcomes_keep_shared_advisory_open_after_path_change(
+ security_findings,
+):
+ before = security_findings([("old-path", "same", "critical", "")])
+ after = security_findings([("new-path", "same", "critical", "")])
+ result = audit_module["format_audit_summary"](before, after, "pnpm")
+ assert "**Resolved:** none." in result
+ assert "**New:**" not in result
+
+
+def test_security_outcomes_report_partial_resolution_for_one_package(security_findings):
+ first = ("leaf", "first", "high", "")
+ second = ("leaf", "second", "high", "")
+ result = audit_module["format_audit_summary"](
+ security_findings([first, second]), security_findings([first]), "pnpm"
+ )
+ assert "**Resolved:** 1 (`leaf`)" in result
+ assert "**Remaining:** 1 registry advisory" in result
+ assert "| `leaf` (1 high) |" in result
+
+
+def test_security_outcomes_keep_missing_initial_evidence_unconfirmed(security_findings):
+ result = audit_module["format_audit_summary"](
+ "Audit unavailable", security_findings([]), "pnpm"
+ )
+ assert "**Resolved:** unconfirmed" in result
+ assert "**Resolved:** none" not in result
+
+
+def test_security_outcomes_summarize_concrete_blockers(security_findings):
+ constraints = "Published patches exist, but no single patch satisfies all checked parent constraints. Blocked by existing parent constraints: `@docusaurus/core@3.10.2` requires `tinypool ^1.0.2`."
+ before = security_findings(
+ [
+ ("tinypool", "first", "critical", constraints),
+ ("tinypool", "second", "critical", constraints),
+ (
+ "braces",
+ "third",
+ "high",
+ "No published stable patch matches registry range `>=3.0.4`.",
+ ),
+ ]
+ )
+ result = audit_module["format_audit_summary"](before, before, "pnpm")
+ assert (
+ "| `tinypool` (2 critical) | Blocked by the `@docusaurus/core` version constraint. |"
+ in result
+ )
+ assert "No published patch matches the fix range." in result
+ assert result.count("| `tinypool`") == 1
+
+
+def test_security_outcomes_bound_the_visible_package_list(security_findings):
+ data = security_findings([(f"package-{i}", str(i), "high", "") for i in range(7)])
+ result = audit_module["format_audit_summary"](data, data, "pnpm")
+ assert "**Remaining:** 7 registry advisories" in result
+ assert "2 more affected packages are listed in the audit details." in result
+ assert result.count("| `package-") == 5
+
+
+def test_publisher_footer_is_only_a_run_link():
+ workflow = load_workflow()
+ footer = workflow["jobs"]["pull-request"]["steps"][-1]["env"]["PR_FOOTER"]
+ assert footer.strip() == (
+ "[Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
+ )
+
+
@pytest.mark.parametrize("install, build", [(1, 0), (0, 1)])
def test_summary_keeps_failed_validation_visible(
npm_project, monkeypatch, install, build
@@ -827,7 +1048,8 @@ def invoke(command, *, cwd, **kwargs):
return subprocess.CompletedProcess(command, 0, "", "")
monkeypatch.setattr(subprocess, "run", invoke)
- summary = prepare(pnpm_project).split("")[0]
+ report = prepare(pnpm_project)
+ summary = report.split("")[0]
assert "Initial audit evidence was unavailable" in summary
assert "retained security-fix changes are unconfirmed" in summary
@@ -835,7 +1057,7 @@ def invoke(command, *, cwd, **kwargs):
assert "No additional tool errors recorded." not in summary
assert (
"## Remaining advisories\n\nNo known vulnerabilities reported by pnpm."
- in summary
+ in report
)