test: fail closed around unsafe or faulty modules - #14
Conversation
The shell already refused an unsafe, incompatible, corrupt, or faulty module, but most of that was only asserted from inside the process. A rejection that holds in a unit test and not through the real seam is not a rejection a user can rely on, so every case now runs the built shell against an isolated store. The faults a conforming module cannot commit — lying about its identity, requiring an unknown capability, putting a damaged frame on the wire — need a module that speaks the contract by hand, so the acceptance harness gains one, steered by a control file beside its executable. Every failing case asserts the same three things: one stable problem code, the exit class automation acts on, and nothing on standard output, so a half-finished exchange can never reach a script as a success. Closes wso2#6
…side Review of the previous commit found the "before launch" and "before invocation" claims rested on the shell's own account of the failure: the absence of an "rpc." problem code. That is a proxy, and it would survive a shell that launched an executable and refused it a moment later. The installed executable now supplies the evidence instead. A launch canary records being run at all, and the fault fixture records the invocation arriving, so both claims are read from the file system. Control cases assert each marker does appear when the exchange gets that far, so an absent one is evidence rather than an untested default. The grace period gains the same treatment: a module that exits only when its protocol input closes proves the shell offered it the chance rather than killing it outright. The termination check now waits for the process to disappear instead of sampling once, which an unreaped child could have failed. Protocol incompatibility joins the other incompatible receipt facts in one table, where it is now also proved to happen before launch.
📝 WalkthroughSummary
WalkthroughAdds black-box acceptance tests for fail-closed module execution. Coverage includes receipt path and metadata validation, executable resolution, runtime identity checks, malformed protocol frames, panic and hanging modules, diagnostic bounding, and structured-output isolation. New fault-injecting and launch-canary executables provide controlled protocol violations and launch markers. Existing status tests now record module PIDs, verify termination, and configure modules through explicit control-file contents. Sequence Diagram(s)sequenceDiagram
participant AcceptanceTest
participant Shell
participant Module
participant Output
AcceptanceTest->>Shell: invoke installed module
Shell->>Module: validate handshake and send invocation
Module-->>Shell: return result or fault
Shell->>Output: emit valid stdout and bounded stderr
Shell-->>AcceptanceTest: return stable exit class
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/acceptance/failclosed_test.go (1)
477-500: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove the restrictive
ShellRangedefault ininstallLaunchCanary.
fixture.Installalready applies a broad default whenShellRangeis empty, so this">=0.1.0 <1.0.0"override can make compatible reference modules reject shell versions outside that range instead of exercising the intended failure scenarios.♻️ Proposed fix
- if install.ShellRange == "" { - install.ShellRange = ">=0.1.0 <1.0.0" - } if len(install.ProtocolVersions) == 0 { install.ProtocolVersions = []int{testProtocolVersionNumber} }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/acceptance/failclosed_test.go` around lines 477 - 500, Remove the explicit “>=0.1.0 <1.0.0” fallback from installLaunchCanary, leaving an empty install.ShellRange for fixture.Install to default. Preserve the existing namespace, version, source path, and protocol-version setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/acceptance/failclosed_test.go`:
- Around line 477-500: Remove the explicit “>=0.1.0 <1.0.0” fallback from
installLaunchCanary, leaving an empty install.ShellRange for fixture.Install to
default. Preserve the existing namespace, version, source path, and
protocol-version setup.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 553542c9-2e06-473a-b408-96b57efb5a6f
📒 Files selected for processing (4)
test/acceptance/failclosed_test.gotest/acceptance/status_test.gotest/acceptance/testdata/faultymodule/main.gotest/acceptance/testdata/launchcanary/main.go
Closes #6. Parent: #3. Follows #5, which built the module contract this
proves the boundaries of.
What this is
The shell already refused a module that was unsafe, incompatible,
corrupt, or faulty. Almost none of that was asserted through the seam a
user actually runs, and a rejection that holds in a unit test and not
through the real process boundary is not a rejection anyone can rely on.
So this is a test change. No shell behaviour needed altering: every
acceptance criterion on #6 was already implemented, and the work was
proving it from outside, against a built shell and an isolated store.
How the faults are produced
Two fixture executables under
test/acceptance/testdata:faultymodulespeaks the module contract by hand. The SDK existsto make a module conform, so a module built on it cannot lie about its
identity, require a capability no shell has heard of, or put a damaged
frame on the wire — which is exactly the set of failures the shell has
to survive. One control file beside its executable selects one fault,
because the shell passes a module no arguments and sanitizes its
environment to nothing.
launchcanaryrecords being run, and does nothing else.Proving what did not happen
"Rejected before launch" and "rejected before invocation" are claims
about absence, so they are not left to the shell's own account of the
failure. The installed executable records how far the exchange got,
beside itself, and the assertions read that. Control cases prove each
marker does appear when the exchange reaches that point, so an absent
one is evidence rather than an untested default.
Coverage
Each acceptance criterion of #6, black-box:
PATHor in the working directoryNotes for review
TestAModuleThatSpeaksAnotherProtocolFailsBeforeInvocationisremoved. The incompatible-receipt table covers it and also proves the
before-launch half its name claimed but never asserted; a comment at
its old site points to the replacement.
the tests, because a
mainpackage cannot be imported. Every fault isselected by content and every marker asserted by name, so a value that
drifts fails its test rather than passing quietly.
invocation deadline. The timing-sensitive tests were run
-count=3without a flake.
Verification
go vet ./...,gofmt -l ., andgo test ./...are clean, includingthe build-boundary tests and the SDK under
GOWORK=off.