Skip to content

Fix main's CI: a Windows-only path bug in my own gate, and folded_health's shared-registry flake - #517

Open
emooreatx wants to merge 1 commit into
mainfrom
fix/main-ci-windows-gate-and-folded-health
Open

Fix main's CI: a Windows-only path bug in my own gate, and folded_health's shared-registry flake#517
emooreatx wants to merge 1 commit into
mainfrom
fix/main-ci-windows-gate-and-folded-health

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

main went red at the 0.5.196 merge after eleven consecutive green runs. Two independent failures, one platform each.

This PR carries ci:full deliberately. PRs run ubuntu-only, so the Windows half of this fix cannot be validated by a default PR run — which is exactly why neither failure was catchable before the merge.

Windows — my gate, broken by a path separator

The scrape gate I added in 0.5.196 to prove the node alias literal exists exactly once filtered its own file with a display() string prefix:

.filter(|h| !h.starts_with("src/key_convention.rs"))   // renders src\key_convention.rs on Windows

So on Windows it excluded nothing and the gate failed on the very definition it exists to protect. It now keeps PathBufs and filters with Path::ends_with, which matches whole components and is separator-agnostic. Mutation-verified after the fix: reintroducing a second literal in node_key.rs still reds it.

Ubuntu — one leaked warning, four failures

folded_health shares a process-global degradation registry. Three of the four failures assert degraded_mode == false; the fourth indexes warnings[0], which any node-tier entry displaces. One leaked warning explains all four — and none of the four is the test that leaked it.

I could not reproduce it locally: 10 clean runs unloaded, 8 more pinned to two cores under competing CPU load. So rather than ship a guess, three changes that each stand on their own:

1. tests/common/RaisedWarning — an RAII guard that clears on drop, including while unwinding. The old shape raised, then asserted, bound a loopback port, and made two HTTP round-trips before clearing:

raise("test.forced_node_fault");                   // 575
assert_eq!(baseline, ("degraded", true), ...);     // can panic
spawn_brain(...).await; get_health(...).await;     // can panic
clear("test.forced_node_fault");                   // 593 — skipped on unwind

Correctness no longer depends on the body of a test completing, which is the one thing a test cannot promise. The guard also clears before raising — the defensive shape health_reports_degradation and retention_loop already use and folded_health did not, which is why a leak here was not self-healing.

2. The warnings[0] lookup finds its entry BY CODE. Index zero assumed the brain's warning was first; any node-tier warning ahead of it silently retargeted that assertion at a different entry.

3. The fixture guard PRINTS the registry when it trips. The bare assert!(!baseline.1, "fixture: this case needs an undegraded node") named nothing, so the cause had to be reconstructed from four line numbers in a CI log. Only tests write to that registry, so its contents are the diagnosis:

fixture: `a_brain_warning_alone_degrades_the_folded_pair` needs an undegraded node,
but the process-global degradation registry already says "degraded". Only tests
write to it, so one of them raised without clearing. Standing warnings: [
    Warning { code: "leaked.from_somewhere_else", severity: "critical", ... },
]

Verified by simulating a leak. A recurrence now identifies its own cause.

What I deliberately did not do

health_reports_degradation and retention_loop keep their manual raise/clear pairs. Both already clear defensively before raising, so a leak there self-heals on the next case — the hazard is real but bounded, and churning two passing files to fix a bug they do not have is not obviously right. Happy to convert them if you'd rather have one pattern.

Honest limit

The RAII guard removes a real leak path, but CI showed no panicking test, so I cannot claim it is the cause of that specific run. Changes 2 and 3 are what make the next occurrence cheap: it will either not happen, or it will name itself.

Local

476 lib tests, all 15 gate binaries, and the three degradation-touching suites green. clippy --all-targets -- -D warnings and fmt --check clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_017gxnxsTWS8FkN3CT2JEz86

…egistry flake

main's CI went red at the 0.5.196 merge after eleven consecutive green runs. Two
independent failures, one platform each, and PRs run ubuntu-only so neither could
have been caught before merging.

WINDOWS — MY GATE, BROKEN BY A PATH SEPARATOR. The scrape gate I added to prove
the node alias literal exists once filtered its own file with
`starts_with("src/key_convention.rs")` over a `display()` string. Windows renders
that `src\key_convention.rs`, so the filter excluded nothing and the gate failed
on the definition it exists to protect. It now keeps `PathBuf`s and filters with
`Path::ends_with`, which matches whole components and is separator-agnostic.
Mutation-verified after the fix: reintroducing a second literal still reds it.

UBUNTU — ONE LEAKED WARNING, FOUR FAILURES. `folded_health` shares a
process-global degradation registry. Three of the four failures assert
`degraded_mode == false`; the fourth indexes `warnings[0]`, which any node-tier
entry displaces. One leaked warning explains all four, and none of them is the
test that leaked it.

Three changes, because I could not reproduce the leak locally (10 clean runs
unloaded, 8 more pinned to two cores under competing load) and did not want to
ship a guess:

1. `tests/common/RaisedWarning` — an RAII guard that clears on drop, including
   while unwinding. The old shape raised, then asserted, bound a loopback port
   and made two HTTP round-trips, and only then cleared: anything in that window
   that panicked leaked a `critical` into every later case. Correctness no longer
   depends on the body of a test completing, which is the one thing a test cannot
   promise. It also clears BEFORE raising, the defensive shape
   `health_reports_degradation` and `retention_loop` already use and
   `folded_health` did not — which is why a leak there was not self-healing.
2. The `warnings[0]` lookup finds its entry BY CODE. Index zero assumed the
   brain's warning was first; a node-tier warning ahead of it silently retargeted
   the assertion at a different entry.
3. The fixture guard now PRINTS the registry when it trips. The bare
   `assert!(!baseline.1)` named nothing, so the cause had to be reconstructed
   from four line numbers in a CI log. Only tests write to that registry, so its
   contents are the diagnosis. Verified: a simulated leak now reports the exact
   code and message that caused it.

I have not converted `health_reports_degradation` or `retention_loop` to the
guard. Both already clear defensively before raising, so a leak there
self-heals on the next case — the hazard is real but bounded, and churning two
passing files to fix a bug they do not have is not obviously right.

Local: 476 lib tests, all 15 gate binaries, and the three degradation-touching
suites green; clippy -D warnings and fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gxnxsTWS8FkN3CT2JEz86
@emooreatx emooreatx added the ci:full Run the full CI matrix (macOS + Windows) on this PR label Aug 31, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f69e374dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/key_convention.rs
let foreign: Vec<String> = hits
.iter()
.filter(|h| !h.starts_with("src/key_convention.rs"))
.filter(|p| !p.ends_with("key_convention.rs"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict the exclusion to the defining path

Path::ends_with("key_convention.rs") excludes every source file with that basename, not only src/key_convention.rs. If a nested module such as src/foo/key_convention.rs reintroduces the protected literal, this exact-once gate silently passes. Compare against the complete relative path using Path components so the check remains separator-independent without broadening the exclusion.

Useful? React with 👍 / 👎.

emooreatx added a commit that referenced this pull request Sep 5, 2026
…not "foreign" on Windows (#540)

main's `clippy + test (windows-latest)` has been red on every run since e46c45f (0.5.196, 2026-08-30) — the commit that added `key_convention::tests::the_node_alias_literal_exists_exactly_once_in_the_tree`. The test exempts its own file by `starts_with("src/key_convention.rs")`; on Windows `path.display()` yields `src\key_convention.rs`, so the file's own two sites read as foreign and the assertion fired:

```
Found: ["src\\key_convention.rs:118", "src\\key_convention.rs:2…"]
```

Ubuntu and macOS pass, and the PR lane is ubuntu-only, so nothing ever surfaced this before a merge. Same class as #517: a path bug in the gate itself.

**Change:** the scanned path is normalised to forward slashes before the exemption is applied. Behaviour on Linux/macOS is unchanged (7/7 `key_convention` tests pass locally).

Labelled `ci:full` so the Windows and macOS lanes run on this PR rather than after the merge. Because `cargo test` stops at the first failing binary, the Windows lane has not run the integration tests since 2026-08-30 — anything else Windows-only will surface on this run.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01QVZ3v83uX8noLxXr7d7pu9

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVZ3v83uX8noLxXr7d7pu9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Run the full CI matrix (macOS + Windows) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant