Pin default-terminal handler absence per signal kind - #696
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates the event-feed conformance JSON schema to pin “default-terminal” expectations per signal (buffer_overflow vs feed_gap) instead of treating them as a combined case.
Changes:
- Split the previous combined default-terminal rule into two per-signal rules (buffer_overflow and feed_gap).
- Tighten the “no disposition configured” condition to mean “signalDisposition exists but the specific signal key is absent” (or signalDisposition missing).
- Replace
enumreason matching with per-ruleconstreason matching.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The whole-object pin skipped whenever config.signalDisposition was present at all, so a buffer_overflow terminal that configured only a feedGap key (or vice versa) escaped the empty-invocations requirement and could claim a phantom handler invocation. Replace allOf[1] with two per-signal pins, each keyed on the terminal reason const and the absence of that signal's own disposition key. Proof: a fixture-21 clone with a feedGap-only disposition and a phantom bufferOverflow invocation validates against the old schema (exit 0) and is rejected by the new one (exit 1), mirror-image probe likewise; make event-feed-fixtures-check green over all 22 fixtures.
61602f7 to
06c1a1e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
conformance/event-feed/schema.json:155
- This feed-gap branch similarly rejects a valid run that invokes an accepting
bufferOverflowhandler before later terminating on an unhandled feed gap, because the shared exact invocation record would correctly be non-empty. The per-signal pin should prohibit onlyfeedGapinvocation entries rather than forcing all signal-handler history to be empty.
"const": "feed_gap"
conformance/event-feed/schema.json:115
- This over-constrains mixed-signal scenarios.
handlerInvocations.exactis the ordered record for both signal kinds, so a valid run could first invoke an acceptingfeedGaphandler and later terminate on an unhandled buffer overflow. Requiring the entire record to equal[]rejects that run even though no overflow handler was invoked. Keep the record required, but forbid only entries whosekindisbufferOverflow.
This issue also appears on line 155 of the same file.
"exact": {
"const": []
}
The const-[] form said more than the contract does: it pinned the WHOLE
record empty, so a legitimate cross-signal run — an accepted overflow
followed by an unhandled 410 ending feed_gap default-terminal, record
[{bufferOverflow, accept}] — would be schema-rejected. The per-signal
contract is that an unregistered handler cannot be invoked; say exactly
that with a not-contains on the terminal signal's kind.
Proof: both phantom probes still rejected (per-kind), the valid
cross-signal control accepted by this form and rejected by the const-[]
form; make event-feed-fixtures-check green over all 22 fixtures.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (2)
conformance/event-feed/schema.json:152
- The mirrored
feedGapkey-absence branch also lacks an automated regression case; fixture 23 omitssignalDispositionentirely and therefore does not distinguish this condition from the old one. Please gate a negative case with onlybufferOverflowconfigured and a phantomfeedGapinvocation so both per-signal pins are continuously verified.
"not": {
"required": [
"signalDisposition"
],
"properties": {
"signalDisposition": {
"required": [
"feedGap"
]
}
}
conformance/event-feed/schema.json:82
- The new key-specific
bufferOverflowabsence branch is not exercised by the committed suite: fixture 21 has nosignalDispositionat all, so it would also pass with the previous whole-object condition. Please add an automated negative regression case with onlyfeedGapconfigured and a phantombufferOverflowinvocation; the current manual probe will not protect this fix from regression.
This issue also appears on line 142 of the same file.
"not": {
"required": [
"signalDisposition"
],
"properties": {
"signalDisposition": {
"required": [
"bufferOverflow"
]
}
}
The proof probes lived only in the PR body, so nothing in CI would catch a future schema edit quietly re-widening the default-terminal pins — and this schema gets edited again at PR 4 and PR-T. Commit the two phantom probes under conformance/event-feed/invalid-fixtures/ and teach event-feed-fixtures-check to assert each is REJECTED. Each file is schema-valid except for exactly one violation, so its rejection exercises that pin and nothing else; harnesses never glob this directory. Test the test: the same loop run against the pre-fix schema reports the phantom fixtures as wrongly validated and exits 1.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Makefile:569
- This accepts every nonzero validator exit as proof that the intended per-signal pin fired. A malformed fixture—or a future, unrelated schema constraint that rejects it—would keep this gate green even if the per-signal pin were removed, so the negative fixtures do not actually isolate the regression as the README claims. Require a corresponding positive control (for example, the same fixture with only the phantom invocation removed) to validate before asserting that its negative twin fails.
if uvx --from 'check-jsonschema==$(CHECK_JSONSCHEMA_VERSION)' check-jsonschema \
--schemafile conformance/event-feed/schema.json "$$f" >/dev/null 2>&1; then \
echo "ERROR: $$f validated, but this directory holds shapes the schema must reject"; \
exit 1; \
else \
echo "rejected (as required): $$f"; \
Rejected alone doesn't prove the right pin fired: a future unrelated schema constraint could reject the invalid fixture while the pin under test is removed, and the gate would stay green. Each invalid fixture now requires controls/<same name>.json — identical except the one violation is removed — which the gate asserts VALIDATES before asserting the invalid file is rejected. A missing or failing control fails the gate. Test the test, both directions: a schema mutated with an unrelated always-reject constraint fails at the control check; the pre-fix schema (pin absent) fails at the reject check.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Makefile:565
- Pairing files by basename does not enforce the claimed minimal delta, so control acceptance alone cannot isolate rejection to the pin. The committed pairs already differ in
nameanddescriptionas well as the invocation record, and future edits could add another invalid-only constraint while this gate still passes after the per-signal pin is removed. Make each pair identical outside a declared mutation and have the gate verify that relationship (or derive the invalid case from the control) before using it as proof.
@for f in conformance/event-feed/invalid-fixtures/*.json; do \
c="conformance/event-feed/invalid-fixtures/controls/$$(basename $$f)"; \
test -f "$$c" || { \
Two review rounds converged on the same root cause: a committed invalid fixture is a drift-prone artifact. Any nonzero validator exit counted as rejection (so a malformed file would false-green the gate), and basename pairing could not enforce the minimal delta between a fixture and its control. Remove the artifact: each pin-probes/*.json declares a control scenario and one mutation, and scripts/check-event-feed-pin-probes.py validates the control (must pass), derives the mutant in-process, and validates it (must fail). The accepted/rejected delta is the declared mutation by construction; a parse failure is a hard error, not a rejection; the control-first ordering means a mutant rejection can only be a schema rejection. Path-typo and value-equals-control probes fail as vacuous. Test the test: pin-less schema fails at the mutant check, always-reject schema fails at the control check, malformed probe / typo path / vacuous mutation each fail with their own error.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
conformance/event-feed/pin-probes/phantom-gap-invocation.json:124
- The permissive half of the new per-kind rule is not regression-tested: both committed controls use
exact: [], so restoring the earlier over-strictconst: []still accepts both controls and rejects both mutants. This probe already configures and emitsbufferOverflow; record{bufferOverflow, accept}in the control and retain it while adding the phantom{feedGap, accept}in the mutant. That also makes the control consistent with the registered-handler contract and ensures an over-strict schema fails at the control check.
"handlerInvocations": {
"exact": []
The controls asserted exact: [], so a regression back to the over-strict whole-record-empty pin would have passed both probes — the permissive half of the per-kind rule was unguarded. Each control now records the LEGAL other-kind invocation (its disposition key is configured), which an over-strict pin rejects at the control check; each mutant retains that record and adds the phantom invocation of the unhandled kind. The mutant branch also stops reading any nonzero exit as rejection: check-jsonschema exits nonzero for tool, schema-parse, and reference errors too, and a transient failure on that one invocation would have printed pin-verified. The script now requires the validator's JSON output to show a genuine instance-validation failure (status fail, errors non-empty, no parse errors); anything else fails the gate. Test the test: pin-less schema fails at the mutant check, over-strict const-[] schema fails at the control check, always-reject schema fails at the control check, and the discriminator unit cases (tool-error text, parse_errors, empty errors, success, genuine rejection) all behave.
Absorbs the one #645 review finding that merged unaddressed (the resolved-without-reply P2 on
conformance/event-feed/schema.json, thread): the default-terminal pin checked whetherconfig.signalDispositionwas present at all, not whether the disposition for the signal that actually terminated the feed was configured. Abuffer_overflowterminal configuring only afeedGapkey therefore escaped the empty-invocations requirement and could claim a phantom handler invocation — the pin went vacuous exactly when a cross-signal scenario would need it.Change
allOf[1](one whole-object pin) becomes two per-signal pins. Each applies whenfinally.error.reasonis constbuffer_overflow/feed_gapandconfig.signalDispositionlacks that signal's own key (absent entirely, or present without the key — thenot-wrapped composition covers both), and then requiresfinally.handlerInvocationswhoseexactcontains zero invocations of that kind (anot/containsonkind, per commit 2:const []said more than the contract does — it would have rejected a legitimate future cross-signal run such as an accepted overflow followed by an unhandledfeed_gap, which Copilot's suppressed comment correctly flagged). The configured⇒record-required pin and the forbidden-side pin are unchanged.Proof
Probe: a fixture-21 clone with
signalDisposition: {"feedGap": "accept"}and a phantom{bufferOverflow, accept}invocation; mirror probe swaps the roles (feed_gapterminal,bufferOverflow-only key, phantom{feedGap, accept}).A third probe, the valid cross-signal control, is the legitimate shape of the same scenario:
bufferOverflowhandler configured and invoked (accept), run endsfeed_gapdefault-terminal, record[{bufferOverflow, accept}].ee28e96c…)A_REAL_EXIT=0const []form)B_REAL_EXIT=1C_REAL_EXIT=0D_REAL_EXIT=1acebead2…, per-kind form)should not be valid under contains kind: bufferOverflow)E_REAL_EXIT=1F_REAL_EXIT=1G_REAL_EXIT=0H_REAL_EXIT=1make event-feed-fixtures-checkgreen on this head (metaschema + all 22 fixtures),GATE_REAL_EXIT=0. No scenario-fixture changes: today's suite has no cross-signal scenario, so every existing fixture satisfies the tighter pins as-is — the change only closes the door PR 4's future fixtures could otherwise walk through.Committed regression gate (commits 3–5): pin probes with derived mutants
Copilot's second review round made a fair point: the probes above were PR-body evidence only, so a later schema edit (PR 4, PR-T) could quietly re-widen the pins with CI staying green. The first committed shape (invalid fixtures asserted rejected, then + paired positive controls) drew two more correct findings — any nonzero validator exit counted as rejection (a malformed file would false-green the gate), and basename pairing couldn't enforce the minimal delta between fixture and control. Rather than patch the artifact a third time, commit 5 removes it: the invalid case is derived, not committed.
Each
conformance/event-feed/pin-probes/*.jsondeclares acontrolscenario and onemutation(path + value).scripts/check-event-feed-pin-probes.py(run bymake event-feed-fixtures-check) validates the control — must pass — then applies the mutation in-process and validates the mutant — must fail. The isolation claim is now true by construction: the accepted/rejected delta is exactly the declared mutation; a malformed probe is a hard parse error, never a "rejection"; the mutant derives from a control that just parsed and validated, so it cannot be malformed; the control-first ordering means a nonzero on the mutant can only be a schema rejection; and a mutation that misses the control or equals its value fails as vacuous.Round 5 (commit 6) tightened two things inside that shape, both from review findings: each control now records the legal other-kind invocation (so the probes guard both directions — a regression back to the over-strict
const []fails at the control check, and a pin removal fails at the mutant check), and the mutant's rejection must prove itself via the validator's structured JSON output (status: fail, non-emptyerrors, emptyparse_errors) — a transient tool failure or schema-parse error on that invocation fails the gate instead of printing pin-verified.Test-the-test, legs L–V across the rounds: gate green on the committed schema; the pre-fix (pin-less) schema fails at the mutant check; the over-strict
const []schema fails at the control check; an unrelated always-reject schema fails at the control check; a malformed probe, a typo'd mutation path, and a vacuous mutation each fail with their own error; and the rejection discriminator's unit cases (tool-error text, parse_errors, empty errors, success, genuine rejection) all behave. The mechanism is closed-form — further hardening rounds get triaged against whether they name a failure derivation-plus-structured-rejection misses.