Repository navigation
fix: the Unavailable arm gets a control that can fail (Copilot review on #3912) - #3916
Conversation
…#3912) The review is right, and it is the review this change of all changes had to take: the wedged-owner arm had NO control. Absent and Compiled/Script were pinned; a regression mapping `NodeReadStatus.Unavailable` back to `Compiled` would have left the suite green — which is exactly the defect #3888 is about, reproduced in the fix's own test suite. It cannot be pinned against a live mesh: `GetMeshNodeOutcome`'s budget is 15 s and the outcome depends on an owner that will not answer, so a monolith test cannot arrange it, and the xunit methodTimeout is 30 s. So the mapping is EXTRACTED — `MeshNodeLanguageService.NothingWasChecked(NodeReadOutcome)`, the whole decision rather than a fragment, the same idiom as `CanReuseWorkspace` three methods below — and driven directly: * NoReadThatProducedNoNodeMayReadAsCompiled — over `Enum.GetValues<NodeReadStatus>()`, so the invariant is "NO status may read as Compiled", including one added later. A per-value test would go silent on a new status. * AnOwnerThatDidNotAnswerIsUnavailable_AndTheReasonSurvives — the arm itself, plus that the cause reaches DescribeProblem. * ADeleteInFlightReadsAbsent_NotUnavailable — the other direction, so the mapping cannot pass by answering Unavailable to everything. Falsified on purpose: with the `_` arm returning `Compiled(empty)`, the two new negative controls go red — Did not expect value to be Compiled because a read that produced no node compiled nothing, so status Present must not render as a verdict about the proposed source. Expected value to be Unavailable because a hub that did not answer is not a hub that answered 'fine' — this is the case that makes the pre-flight a gate rather than a nicety, but found Compiled. — and 7 of 9 stay green. Restored: 9/9. The review's second half was also a real correction to the COMMENT. A delivery-level RLS denial does NOT reach this switch: `GetMeshNodeOutcome` rides a raw GetDataRequest whose NACK surfaces as OnError (DeliveryFailureException) and is caught at the tool boundary (#2554). What DOES reach it is a validator HIDING the node — a filtered node is invisible to the reader by contract — which is how #3888's live case, a NodeType in a partition the caller held no grant on, landed on Absent. Both routes now say so where they are. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S25PJZbVeCbkWhCfMub9P7
There was a problem hiding this comment.
🟢 Approval recommended
Final comments are minor nits only, with no approval-blocking issues.
Pull request overview
Adds fail-closed handling and deterministic tests for unavailable node reads.
Changes:
- Extracts and tests no-node outcome mapping.
- Adds coverage for unavailable and delete-in-progress reads.
- Updates language-service documentation.
File summaries
| File | Summary |
|---|---|
test/MeshWeaver.Compiler.Pipeline.Test/SpeculativeCheckCannotAnswerGreenForAnUncheckedNodeTest.cs |
Adds deterministic fail-closed mapping tests. |
src/MeshWeaver.Documentation/Data/Architecture/LanguageServices.md |
Documents the control strategy. Nit (1 vote): reference CheckSpeculativeOutcome rather than CheckSpeculative. |
src/MeshWeaver.Compiler.Pipeline/MeshNodeLanguageService.cs |
Extracts the outcome mapping. Nit (1 vote): clarify that unauthorized delivery failures remain a separate tool-boundary obligation. |
Review details
Suppressed comments (2)
src/MeshWeaver.Compiler.Pipeline/MeshNodeLanguageService.cs:260
- This comment overstates the current behavior: issue #2554 reports that the LSP tool wrappers let an Unauthorized
DeliveryFailureExceptionescape as an unhandled MCP error, and the dependent plugin changes are not part of this core PR.GetMeshNodeOutcomedoes surface the denial viaOnError, but this code does not convert it to{ok:false}; please describe that as a separate tool-boundary obligation (or point to the change that actually implements it) so the source does not claim permission denials are already structured refusals.
// 🚨 A DELIVERY-level RLS denial does NOT arrive here: GetMeshNodeOutcome rides a raw
// GetDataRequest whose NACK surfaces as OnError (DeliveryFailureException), which the
// tool boundary converts into the same {ok:false} shape (#2554). Two different routes,
// both refusing; neither may read as clean.
src/MeshWeaver.Documentation/Data/Architecture/LanguageServices.md:120
- Now that this table labels the first column
Method, the row below namesCheckSpeculative, but that overload is intentionally still the editor's silent list API. The fail-closed verdict surface added for #3888 isCheckSpeculativeOutcome; leaving this row as-is contradicts the explanation at lines 131–136 and directs readers to the wrong method.
| Method | Fixed in | The symptom |
|---|---|---|
| `GetDiagnostics` | #1592 / #1618 | `lsp_diagnostics_for_node @Edu/DefinitelyNotARealNodeType` → `{"ok":true,"diagnostics":[]}`; the mandated pre-prod sweep reported all-green over stale paths having verified nothing |
| `CheckSpeculative` | #3888 | `lsp_check_node` on a NodeType in a partition the caller cannot read → `{"ok":true,"diagnostics":[]}` — **including for `proposedCode` that is not C# at all**; the `/code` edit loop's pre-flight blessed every proposed edit against every path it could not reach |
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 1)496 tests 496 ✅ 36s ⏱️ Results for commit c2a277e. |
Test Results (shard 3)389 tests 389 ✅ 56s ⏱️ Results for commit c2a277e. |
Test Results (shard 0)254 tests 254 ✅ 2m 17s ⏱️ Results for commit c2a277e. |
Test Results (shard 2)1 229 tests 1 227 ✅ 3m 39s ⏱️ Results for commit c2a277e. |
Test Results (shard 4)1 888 tests 1 888 ✅ 3m 21s ⏱️ Results for commit c2a277e. |
Test Results (shard 5) 6 files 6 suites 4m 30s ⏱️ Results for commit c2a277e. |
Test Results 17 files 17 suites 15m 21s ⏱️ Results for commit c2a277e. |
Follow-up to #3912 (Refs #3888), acting on the Copilot review it received. The branch was already in
the merge queue when the review landed, and a queued branch cannot be pushed to — so this is the
same change set's second commit, carried on its own PR rather than dequeued out from under the group
behind it.
The finding, which is the right one to catch on this change of all changes
Correct. #3912 fixed an instrument that could not fail, and shipped one arm of its own fix with no
control on it. A wedged owner is the case that made the outcome type worth having in the first
place (#1592: memex-cloud went 2 h 20 m with per-node hubs not answering, and every diagnostics probe
in that window would have read clean), and it was the one arm nothing could have caught going back.
Why no live-mesh test could reach it
GetMeshNodeOutcome's budget is 15 s andUnavailabledepends on an owner that will not answer —not something a
MonolithMeshTestBasecan arrange on demand, and the suite'smethodTimeoutis30 s. An arm no control can reach is precisely where a regression puts
Compiledback unnoticed.So the mapping is extracted —
MeshNodeLanguageService.NothingWasChecked(NodeReadOutcome), theWHOLE decision rather than a fragment, the same idiom as
CanReuseWorkspacethree methods below —and driven directly.
The controls
NoReadThatProducedNoNodeMayReadAsCompiledEnum.GetValues<NodeReadStatus>(): no status may render asCompiledorIsClean, including one added laterAnOwnerThatDidNotAnswerIsUnavailable_AndTheReasonSurvivesDescribeProblem— a refusal naming neither path nor reason is only a differently-shaped dead endADeleteInFlightReadsAbsent_NotUnavailableUnavailableto everythingEnumerating the enum rather than testing per value is deliberate: a per-value test goes silent on
a new
NodeReadStatus, which is the same failure shape as the bug — the next status arrives as a redinstead of as a hole.
Falsified on purpose. With the
_arm returningCompiled(empty), the two new negative controlsgo red and 7 of 9 stay green:
Restored:
Passed! - Failed: 0, Passed: 9, Total: 9.The review's second half was a correction to the COMMENT, and it was also right
#3912's comment said the
Unavailablearm covers "a delivery-level denial". It does not. Adelivery-level RLS denial rides a raw
GetDataRequestwhose NACK surfaces asOnError(
DeliveryFailureException) and is caught at the tool boundary (#2554) — it never reaches thisswitch. What does reach it via access control is a read validator hiding the node: a filtered
node is invisible to the reader by contract, which lands on
Absent, and that is how #3888's livecase — a NodeType in a partition the caller held no grant on — got there. Both routes now say so
where they are, so the next reader is not sent looking for a denial in the wrong arm.
Local verification
dotnet build src/MeshWeaver.Compiler.Pipeline -c Release -warnaserror→0 Error(s) 0 Warning(s)dotnet build test/MeshWeaver.Compiler.Pipeline.Test -c Release -warnaserror→0 Error(s) 0 Warning(s)SpeculativeCheckCannotAnswerGreenForAnUncheckedNodeTest→ 9 passed (2 red under the sabotage above)MeshWeaver.Documentation.Testlink/embed/topic/timeout guards → 18 passedDoc/Architecture/LanguageServicesrecords the general point: an arm no live test can reach is notan excuse, it is an instruction to extract the decision and assert the invariant over the enum
rather than over the values you happened to think of.
🤖 Generated with Claude Code
https://claude.ai/code/session_01S25PJZbVeCbkWhCfMub9P7