mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
cdd48d916d
* docs: single data-flow map + DF-1..DF-3 coverage lint (#758) Add docs/DATA_FLOWS.md — one row per network touchpoint (trigger, payload class, recipient, credentials, off switch) and one row per local store (path, content, TTL, deletion), with an explicit scope statement (the Claude session itself is platform-governed; nothing publishes autonomously). Covers the four gate resolvers, the standalone Chinese-literature resolver (NOT in the gate), the consent-bound claim-standing discovery adapters, both cross-model transports (API and citation-only Codex subscription), the SessionStart update check, the manual smoke tests, and the v3.9.4 timeline bootstrap — the last one surfaced by the new lint itself on first run (it was absent from the #758 issue enumeration). Inbound links from README, SECURITY.md (in-scope exfiltration anchor), and THIRD_PARTY.md (core-suite vs third-party contrast). Lint (same-PR drift-point discipline): scripts/check_data_flows.py — DF-1 every non-test scripts/*.py importing a network module (AST scan, so no-call guards naming urllib.request in strings do not count) must be named on the map; DF-2 same for curl-invoking shell scripts; DF-3 README/SECURITY/THIRD_PARTY keep a rendered resolving inbound link (fences + HTML comments stripped with the semantics converged in the PR #768 review; consolidation into a shared helper is follow-up). 12 mutation tests; wired into spec-consistency.yml + pytest manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * refactor: apply /simplify pass (4-agent, deduped) (#758) Doc: the four gate resolvers collapse into a 4-column sub-table under one shared trigger/payload/off-switch lead (the wide table kept only heterogeneous touchpoints); the exhaustiveness sentence is bounded to what DF-1/DF-2 actually detect (direct imports + curl; spawned-CLI and session-tooling paths held by review); "Nothing here publishes" now inherits POSITIONING.md and its not-a-runtime-guarantee qualifier; the subscription-free note is trimmed to its rationale; Related gains the SETUP bullet as the tunables authority. Coverage: docs/SETUP.md becomes the fourth DF-3-pinned inbound surface (pointer added in the cache section); the four translated READMEs mirror the README pointer; docs/DATA_FLOWS.md registers into check_spec_consistency.py relative-link validation. Lint: DF-1 module vocabulary rebuilt as the network subset of the no-call envelope FORBIDDEN_IMPORTS (deviations documented: dotted urllib.request/http.client instead of bare urllib/http; ssl excluded); scan is now recursive into scripts/ subpackages. Tunable constants in verification_cache.py gain update-both comments. Tests: the three hollow assert-baseline tests become real mutations (name-based test exemption, uncomment-curl, from-urllib idiom); recursive-scan and SETUP-surface tests added (17 total). Skipped with reason: endpoint-hostname lint (near-zero event rate, composed-URL false-fire risk); row-id shrink constant (review-owned per degradation-registry precedent); markdown-helper consolidation with check_control_availability.py (whichever PR merges second extracts the shared module — recorded in both PR bodies). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R1 findings — 8 P2 + 4 P3 (#758) Doc accuracy (6): Chinese-literature row rewritten (callable client, no CLI; PubMed path sends the required NCBI contact email + bibliographic search coordinates); codex-transport payload names citation_context (can contain unpublished manuscript text); update check documented as one curl transfer per 24 h with redirects and the ARS_UPDATE_CHECK_REMOTE_URL override; retraction-status SQLite cache added to local stores (caller-supplied path, 30-day stale threshold, no auto-expiry); discovery adapters credentials corrected (fixed User-Agent, resolver env keys not consumed); resolver payload narrowed to identifiers + title query strings; update-check state content corrected (state label + two version strings). Lint mis-pass/mis-fire (4): DF-1/DF-2 coverage now requires the full repo-relative path (basename-substring collision closed); DF-2 is recursive over scripts/ and hooks/, recognizes path-qualified curl, and masks quoted spans before the comment strip; DF-3 strips inline code spans before link extraction (a backticked link does not render). Five mutation tests added (22 total). The code-span rule is a divergence from check_control_availability.py to be carried over at the declared helper consolidation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R2 findings — 4 P2 + 1 P3 (#758) - DF-2 scans command-substitution bodies BEFORE quote masking, so resp="$(curl ...)" — a real network call inside double quotes — fires (mutation test added; suite now genuinely 22, correcting the prior commit message which said 22 when 21 were collected). - Map gains the Codex audit wrapper row (scripts/run_codex_audit.sh: human/CI/hook-invoked only, sends deliverable + supporting file contents through the local Codex CLI login). - Update check re-bounded: at most one SUCCESSFUL check per 24 h; a failed attempt writes no state and may retry next session. - Cache TTL wording corrected: expiry is a cache miss, not deletion; expired rows persist until invalidated or the file is deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: full comment lines execute nothing — DF-2 substitution scan (#758) The R2 command-substitution scan ran before any comment handling, so a full comment line containing $(curl ...) false-fired — surfaced by the codex R3 pass (timed out mid-review, but its transcript had already demonstrated the false fire). Comment-only lines are now skipped before the substitution scan; a $(curl) inside a trailing inline comment remains a documented accepted edge. Mutation test added (23 total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R3 findings — command-position curl + image links + retention wording (#758) - DF-2 rebuilt around COMMAND POSITION: curl counts only as the first non-assignment token of a segment (pipes/separators/substitution openers), so `command -v curl` preflights and `echo curl` no longer false-fire; VAR=x curl still fires; wrapper-prefixed invocations (sudo/timeout) are documented accepted edges. - DF-3 link grammar excludes image syntax —  renders no anchor and cannot keep the acceptance criterion green. - Cache retention wording includes the overwrite path: expired rows persist until overwritten by re-verification, invalidated, or the file is deleted. - Three mutation tests added (26 total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R4 finding — curl behind shell control words (#758) The command-position head-token scan now skips shell control words (if/elif/while/until/then/else/do/!/time/exec) before naming the head, so `if curl …; then` and `while ! curl …; do` fire while `if true; then` stays quiet. Two mutation tests (28 total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R5 finding — option tokens after control words (#758) `time -p curl …` / option-bearing exec forms: the head scan now skips `-`-prefixed option tokens alongside assignments and control words, so the option cannot shadow the command head. Mutation test added (29 total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R6 finding — harness spawned-CLI paths scoped out (#758) Three maintainer-only measurement scripts reach the network through locally authenticated CLIs (dispatch_e4_panel via claude -p, run_review_criteria_constructive_value via Codex, check_ranking_lift via gh api). They are not user-facing feature paths, so instead of diluting the touchpoint tables they are now an explicit named scope exclusion — the exhaustiveness claim no longer silently spans them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc * fix: close codex R7 finding — boundary count wording (#758) "Two boundaries" became three after the R6 harness exclusion; the count is removed rather than maintained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EosnA4RdUYgbF2KmZ1DTmc --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
383 lines
12 KiB
Python
383 lines
12 KiB
Python
"""Mutation tests for check_data_flows.py (#758).
|
|
|
|
Two layers:
|
|
- the real tree must pass (baseline), and
|
|
- a synthetic fixture repo exercises each invariant's fire/no-fire edge
|
|
without depending on the real script inventory.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from check_data_flows import (
|
|
DOC_RELPATH,
|
|
run_all_checks,
|
|
)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _write(root: Path, rel: str, content: str) -> None:
|
|
path = root / rel
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
@pytest.fixture()
|
|
def fixture_repo(tmp_path: Path) -> Path:
|
|
"""Minimal repo: one documented network script, one non-network script,
|
|
one network-importing test file (exempt), one documented curl shell
|
|
script, and the three inbound-link surfaces."""
|
|
_write(
|
|
tmp_path,
|
|
"docs/DATA_FLOWS.md",
|
|
"# Data Flows\n\n"
|
|
"| Touchpoint |\n|---|\n"
|
|
"| `scripts/net_client.py` row |\n"
|
|
"| `scripts/fetch_thing.sh` row |\n",
|
|
)
|
|
_write(
|
|
tmp_path,
|
|
"scripts/net_client.py",
|
|
"import urllib.request\n\n\ndef go(url):\n"
|
|
" return urllib.request.urlopen(url)\n",
|
|
)
|
|
_write(
|
|
tmp_path,
|
|
"scripts/pure_local.py",
|
|
"import json\nimport urllib.parse\n\nVALUE = urllib.parse.quote('x')\n",
|
|
)
|
|
_write(
|
|
tmp_path,
|
|
"scripts/test_net_helper.py",
|
|
"import urllib.request\n",
|
|
)
|
|
_write(
|
|
tmp_path,
|
|
"scripts/fetch_thing.sh",
|
|
"#!/bin/sh\ncurl -fsSL https://example.invalid/x\n",
|
|
)
|
|
_write(
|
|
tmp_path,
|
|
"scripts/no_net.sh",
|
|
"#!/bin/sh\n# curl is only mentioned in this comment\necho ok\n",
|
|
)
|
|
link = "[docs/DATA_FLOWS.md](docs/DATA_FLOWS.md)"
|
|
_write(tmp_path, "README.md", f"# Fixture\n\nSee {link}.\n")
|
|
_write(tmp_path, "SECURITY.md", f"# Security\n\nMap: {link}.\n")
|
|
_write(tmp_path, "THIRD_PARTY.md", f"# Third party\n\nMap: {link}.\n")
|
|
_write(
|
|
tmp_path,
|
|
"docs/SETUP.md",
|
|
"# Setup\n\nSee [DATA_FLOWS.md](DATA_FLOWS.md).\n",
|
|
)
|
|
return tmp_path
|
|
|
|
|
|
def test_real_tree_passes() -> None:
|
|
assert run_all_checks(REPO_ROOT) == []
|
|
|
|
|
|
def test_fixture_baseline_passes(fixture_repo: Path) -> None:
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_missing_doc_is_single_fatal_error(fixture_repo: Path) -> None:
|
|
(fixture_repo / DOC_RELPATH).unlink()
|
|
errors = run_all_checks(fixture_repo)
|
|
assert len(errors) == 1
|
|
assert "missing" in errors[0]
|
|
|
|
|
|
def test_df1_unnamed_network_script_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/new_resolver.py",
|
|
"from urllib import request\n\n\ndef go(url):\n"
|
|
" return request.urlopen(url)\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-1" in e and "new_resolver.py" in e for e in errors)
|
|
|
|
|
|
def test_df1_string_mention_without_import_not_flagged(
|
|
fixture_repo: Path,
|
|
) -> None:
|
|
# A no-call guard that merely NAMES the module in a forbidden list must
|
|
# not be treated as a network touchpoint (AST, not grep).
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/no_call_guard.py",
|
|
'FORBIDDEN = {"urllib.request", "http.client"}\n',
|
|
)
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_df1_exemption_is_name_based(fixture_repo: Path) -> None:
|
|
# The same network-importing body fires under a non-test_ name and stays
|
|
# quiet under the test_ name — proving the exemption is name-based, not
|
|
# content-based.
|
|
body = (fixture_repo / "scripts/test_net_helper.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert run_all_checks(fixture_repo) == [] # test_ name: exempt
|
|
_write(fixture_repo, "scripts/net_helper.py", body)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-1" in e and "net_helper.py" in e for e in errors)
|
|
|
|
|
|
def test_df1_recursive_scan_reaches_subdirectories(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/verification_gate/sub_resolver.py",
|
|
"import urllib.request\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-1" in e and "sub_resolver.py" in e for e in errors)
|
|
|
|
|
|
def test_df1_from_urllib_parse_only_not_flagged(fixture_repo: Path) -> None:
|
|
# pure_local.py imports urllib.parse only; adding the same idiom via
|
|
# `from urllib import parse` must also stay quiet.
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/parse_only.py",
|
|
"from urllib import parse\nVALUE = parse.quote('x')\n",
|
|
)
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_df1_from_urllib_import_request_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/idiom_resolver.py",
|
|
"from urllib import request\n\n\ndef go(url):\n"
|
|
" return request.urlopen(url)\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-1" in e and "idiom_resolver.py" in e for e in errors)
|
|
|
|
|
|
def test_df2_unnamed_curl_script_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/new_fetch.sh",
|
|
"#!/bin/sh\ncurl -s https://example.invalid/y\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "new_fetch.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_commented_curl_not_flagged_until_uncommented(
|
|
fixture_repo: Path,
|
|
) -> None:
|
|
# no_net.sh mentions curl only in a comment (quiet); uncommenting the
|
|
# same invocation fires — proving the comment-stripping is load-bearing.
|
|
assert run_all_checks(fixture_repo) == []
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/no_net.sh",
|
|
"#!/bin/sh\ncurl -s https://example.invalid/z\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "no_net.sh" in e for e in errors)
|
|
|
|
|
|
def test_df3_removed_readme_link_fires(fixture_repo: Path) -> None:
|
|
_write(fixture_repo, "README.md", "# Fixture\n\nNo link here.\n")
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "README.md" in e for e in errors)
|
|
|
|
|
|
def test_df3_label_keeps_name_but_target_moves_fires(
|
|
fixture_repo: Path,
|
|
) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"SECURITY.md",
|
|
"# Security\n\n[docs/DATA_FLOWS.md](docs/OTHER.md)\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "SECURITY.md" in e for e in errors)
|
|
|
|
|
|
def test_df1_basename_substring_collision_fires(fixture_repo: Path) -> None:
|
|
# The doc names scripts/net_client.py; a NEW scripts/client.py must not
|
|
# ride on that substring — coverage is full-relative-path based (codex
|
|
# R1 finding).
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/client.py",
|
|
"import urllib.request\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-1" in e and "scripts/client.py" in e for e in errors)
|
|
|
|
|
|
def test_df2_nested_shell_script_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/tools/fetch_nested.sh",
|
|
"#!/bin/sh\ncurl -s https://example.invalid/n\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "fetch_nested.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_path_qualified_curl_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/abs_fetch.sh",
|
|
"#!/bin/sh\n/usr/bin/curl -s https://example.invalid/a\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "abs_fetch.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_quoted_instructional_curl_not_flagged(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/instruct.sh",
|
|
'#!/bin/sh\necho "to fetch manually, run curl -s URL"\n',
|
|
)
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_df2_curl_inside_command_substitution_fires(
|
|
fixture_repo: Path,
|
|
) -> None:
|
|
# `resp="$(curl ...)"` executes curl even though the whole expression
|
|
# sits inside double quotes — quote masking must not hide it (codex R2
|
|
# finding).
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/subst_fetch.sh",
|
|
'#!/bin/sh\nresp="$(curl -fsSL https://example.invalid/s)"\n',
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "subst_fetch.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_commented_command_substitution_not_flagged(
|
|
fixture_repo: Path,
|
|
) -> None:
|
|
# A full comment line containing $(curl ...) executes nothing and must
|
|
# not fire.
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/doc_example.sh",
|
|
'#!/bin/sh\n# example: resp="$(curl -fsSL URL)"\necho ok\n',
|
|
)
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_df2_argument_position_curl_not_flagged(fixture_repo: Path) -> None:
|
|
# `command -v curl` (a preflight) and `echo curl` mention curl only in
|
|
# argument position — no curl process runs (codex R3 finding).
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/preflight.sh",
|
|
"#!/bin/sh\nif ! command -v curl >/dev/null; then\n"
|
|
" echo curl missing\nfi\n",
|
|
)
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_df2_env_prefixed_curl_fires(fixture_repo: Path) -> None:
|
|
# `VAR=x curl ...` is still a curl invocation in command position.
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/env_fetch.sh",
|
|
"#!/bin/sh\nLC_ALL=C curl -s https://example.invalid/e\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "env_fetch.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_curl_behind_control_keywords_fires(fixture_repo: Path) -> None:
|
|
# `if curl …; then` / `while ! curl …; do` keep curl in command
|
|
# position behind shell control words (codex R4 finding).
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/cond_fetch.sh",
|
|
"#!/bin/sh\nif curl -s https://example.invalid/c; then\n"
|
|
" echo ok\nfi\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "cond_fetch.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_option_bearing_control_word_fires(fixture_repo: Path) -> None:
|
|
# `time -p curl …` — the option token must not shadow the command head
|
|
# (codex R5 finding).
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/timed_fetch.sh",
|
|
"#!/bin/sh\ntime -p curl -s https://example.invalid/t\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-2" in e and "timed_fetch.sh" in e for e in errors)
|
|
|
|
|
|
def test_df2_control_keywords_alone_not_flagged(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"scripts/plain_cond.sh",
|
|
"#!/bin/sh\nif true; then\n echo ok\nfi\n",
|
|
)
|
|
assert run_all_checks(fixture_repo) == []
|
|
|
|
|
|
def test_df3_image_syntax_does_not_satisfy(fixture_repo: Path) -> None:
|
|
# `` renders an image, not an anchor — a
|
|
# one-character typo must not keep DF-3 green (codex R3 finding).
|
|
_write(
|
|
fixture_repo,
|
|
"THIRD_PARTY.md",
|
|
"# Third party\n\n\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "THIRD_PARTY.md" in e for e in errors)
|
|
|
|
|
|
def test_df3_link_inside_code_span_fires(fixture_repo: Path) -> None:
|
|
# A link inside inline backticks renders literally, not as a link
|
|
# (codex R1 finding).
|
|
_write(
|
|
fixture_repo,
|
|
"SECURITY.md",
|
|
"# Security\n\n`[map](docs/DATA_FLOWS.md)`\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "SECURITY.md" in e for e in errors)
|
|
|
|
|
|
def test_df3_setup_link_removed_fires(fixture_repo: Path) -> None:
|
|
_write(fixture_repo, "docs/SETUP.md", "# Setup\n\nNo link.\n")
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "SETUP.md" in e for e in errors)
|
|
|
|
|
|
def test_df3_link_inside_fence_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"THIRD_PARTY.md",
|
|
"# Third party\n\n```text\n"
|
|
"[docs/DATA_FLOWS.md](docs/DATA_FLOWS.md)\n```\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "THIRD_PARTY.md" in e for e in errors)
|
|
|
|
|
|
def test_df3_commented_out_link_fires(fixture_repo: Path) -> None:
|
|
_write(
|
|
fixture_repo,
|
|
"THIRD_PARTY.md",
|
|
"# Third party\n\n"
|
|
"<!-- [docs/DATA_FLOWS.md](docs/DATA_FLOWS.md) -->\n",
|
|
)
|
|
errors = run_all_checks(fixture_repo)
|
|
assert any("DF-3" in e and "THIRD_PARTY.md" in e for e in errors)
|