mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-13 10:55:59 +00:00
dc2ababf00
* feat(blocking-io): add changed-lines blocking-IO scanner (L1) * feat(blocking-io): add scan-changed CLI wrapper * feat(skill): add blocking-io-guard developer SOP skill * docs(blocking-io): point contributors at the blocking-io-guard skill * style(blocking-io): apply ruff format to scanner and tests * docs(backend): document changed-lines blocking-IO scanner in CLAUDE.md * feat(skill): add post-fix re-scan check and PR batching policy * refactor(skill): fix SOP step ordering, align template with repo conventions - Move re-scan into an explicit 'apply the fix' step (was wedged after anchor generation while telling you to go back before the anchor) - Renumber steps 0-6; drop undefined 'L1' jargon - Mode A: document that the diff is <base>...HEAD (commit first) - Mode B: prefer make detect-blocking-io + findings JSON file - anchor template: module-level pytestmark per tests/blocking_io convention - CLAUDE.md: fix 'git diff --base' phrasing * fix(skill): catch findings introduced without touching the blocking line Review follow-up: changed-line intersection alone misses the case where a new async caller exposes an old sync helper — the static finding sits on the untouched blocking line, so Mode A returned empty and the SOP stopped on a false 'no blocking-IO surface'. Selection is now a union over the changed files: - findings on added lines of git diff <base>...HEAD (kept: a second identical symbol in an already-flagged function collides on the stable key and only this selection sees it); - findings new versus the merge base, matched by (path, function, symbol) — never line numbers. Base sources are materialized via git show <merge-base>:<path>; files absent at base count every head finding as new. SKILL.md now states the residual same-file-only blind spot (cross-file async callers) instead of treating an empty list as proof of zero exposure, and only requires reading sop-skeleton.md when generalizing to another detector domain. * docs(skill): examples teach test-writing, the teeth check defines the rule All examples in the references/template are filesystem-flavored; make explicit that they are instances, not the SOP's boundary — the same rules apply to every detector category (FILE_IO, HTTP, SUBPROCESS, SLEEP) and acceptance is always red/green teeth, never similarity to an example. Neutralize the template's arrange comment accordingly. * fix(blocking-io): harden changed-lines scanner per review - Dedup the union selection by the stable key (path, function, symbol) instead of dict identity, so a future selector returning copied dicts cannot silently empty the result. - parse_changed_lines now handles any unified diff: context lines advance the new-file counter, \-markers and deletions do not, and the counter resets at each +++ header. Previously correct only for --unified=0. - Add blocking_io_static.scan_source (in-memory scan); base-version comparison no longer round-trips through temp files. - Empty Mode A report now prints the same-file-only reachability caveat at the point of use instead of relying on the SOP text alone. * docs(skill): bound best-effort cleanup when the offload sits in finally Lesson from the #3505 review: the SOP routinely drives 'offload the cleanup branch' transformations, and an awaited cleanup in finally can mask or stall the primary exception. One sentence in Step 2 closes that gap at the point where the fix is written.
66 lines
3.1 KiB
Markdown
66 lines
3.1 KiB
Markdown
# Good anchor rules + teeth (blocking-IO fill)
|
|
|
|
Distilled from `backend/docs/BLOCKING_IO_DETECTION.md`. An anchor lives in
|
|
`backend/tests/blocking_io/`; the suite's conftest runs each test under the
|
|
strict Blockbuster gate scoped to `app.*` / `deerflow.*`.
|
|
|
|
The examples in this file and in `templates/` are all filesystem-flavored.
|
|
They demonstrate how to *write* the test, not what the SOP covers: the same
|
|
rules apply to every category the detector reports (FILE_IO, HTTP,
|
|
SUBPROCESS, SLEEP), and the acceptance criterion is always the teeth check
|
|
below — never similarity to an example.
|
|
|
|
## A good anchor
|
|
|
|
- Calls the **real production async entry point** — not a low-level helper,
|
|
unless that helper *is* the entry point production executes.
|
|
- Does **not** bypass the blocking surface with a test-only
|
|
`asyncio.to_thread` / `run_in_executor` wrapper.
|
|
- Uses **real local filesystem** inputs when the bug shape is filesystem IO.
|
|
- Mocks **only** the external dependency boundary (network service, third-party
|
|
saver), never the offload being guarded.
|
|
- Drives the **specific branch** you are protecting (error / cleanup / 404 /
|
|
409), not just the happy path.
|
|
|
|
## Teeth (the acceptance test)
|
|
|
|
An anchor only counts if the gate actually fires when the code blocks:
|
|
|
|
1. Reintroduce the block (revert the offload, or run pre-fix code).
|
|
2. `cd backend && make test-blocking-io` → the anchor **must fail** (RED).
|
|
3. Restore the fix → the anchor **must pass** (GREEN).
|
|
|
|
A green-on-happy-path anchor with no proven red is fake coverage. Don't ship it.
|
|
|
|
## The RULE route (rare; strict admission criteria)
|
|
|
|
Blockbuster's built-in rules cover the common blocking primitives well. The
|
|
two deliberate openings in this SOP are:
|
|
|
|
1. **Coverage opening** (the normal case): the rules already see the
|
|
primitive — you only need an anchor so runtime detection executes the real
|
|
business path and CI prevents regression.
|
|
2. **Rule opening** (rare): you reintroduced a *real* block and the gate
|
|
stayed GREEN — Blockbuster has no rule for that primitive.
|
|
|
|
A project rule lives in `_PROJECT_BLOCKING_RULES` inside
|
|
`backend/tests/support/detectors/blocking_io_runtime.py` and changes detection
|
|
for the **entire** blocking-IO suite — global blast radius. Admission criteria
|
|
for adding one:
|
|
|
|
- You have the **fails-to-fail anchor** as evidence: a good anchor (per the
|
|
rules above) that drives a genuinely blocking path and stays green. No
|
|
evidence, no rule.
|
|
- The primitive is a real blocking call (verified against its implementation
|
|
or docs), not a false positive of the static detector.
|
|
- The rule ships in its **own commit**, naming the primitive, the anchor that
|
|
exposed the gap, and the suite-wide impact. Run the full
|
|
`make test-blocking-io` suite after adding it — a new rule can turn other
|
|
previously-green tests red, and each such red is either a real latent bug
|
|
(fix it) or rule overreach (narrow the rule).
|
|
- If you are not in a position to own that blast radius (e.g. external
|
|
contributor), escalate to a maintainer with the evidence instead.
|
|
|
|
**Never add a runtime rule just because a path is untested** — that case needs
|
|
an anchor, not a rule.
|