Repository navigation
Slice 126: a retraction, and nothing about an approval is discarded in silence - #101
Merged
Merged
Conversation
… consumed
Three failing tests, committed before any fix, by name:
AC1: an allow clicked on the requested broadcast, with no wait for
approval_wait, consumes the row and runs the call
-> consumed_at is nil after status "allowed", decision "once"
AC3/AC4: the chain reads ask then allow, both on the same scope, and neither
is a timeout
-> the chain carries ["ask"] and nothing else. The owner answered and
the chain does not carry the answer.
AC5: at the instant the requested broadcast is observable, the session can
already accept a decision
-> decided straight through the gate, no LiveView involved at all, and
the grant is still not applied
The difference from every approval test that passes is one line those tests have
and a browser does not:
collect(id, &match?({:state, :approval_wait}, &1))
The card is rendered from the gate's :requested broadcast, which is emitted from
inside the tool task before the Session has returned from start_tools. So the
suite has only ever exercised the ordering that works, and the synchronisation
it relies on is not something any real caller can perform.
AC5 is the one that rules out "the LiveView should wait": it decides through
Permissions.decide_request/3 with no view in the loop and fails the same way.
Note on reading the failing output: these runs also log a DBConnection ownership
crash from the tool task. That is a consequence, not the defect. The assertions
on status and decision pass before consumed_at fails, so the decision is
recorded and never consumed; the ownership report is in-flight work colliding
with sandbox teardown after the assertion has already failed. The existing
approval_live_test.exs produces none of it and stays green.
Signed-off-by: Ayla Croft <aylacroft@proton.me>
… in silence)
This slice is a retraction. The defect it was opened for does not exist.
The approval handshake is correct. A decision arriving while the Session is in
tool_wait is postponed and redelivered on the next state change, exactly as the
comment in session.ex claims, and a decision made straight through
Permissions.decide_request/3, with no LiveView in the loop, is applied. The
owner's invariant already held.
Both reproductions I reported are withdrawn.
The suite one was my own test bug. It ended with
collect(id, &match?({:state, :idle}, &1), 15_000)
and collect/3 reads the mailbox, where an {:state, :idle} broadcast from before
the message was sent was already waiting. It returned at once and every
assertion ran mid-turn. The tell was in the output all along: "Finished in 0.2
seconds" for a test nominally waiting fifteen.
The live one was scripts/dev_demo_approval.sh, which I wrote the previous day
for the README assets. It stripped the sandbox pool from Trinity.Repo and did
nothing for Trinity.Repo.Receipts, so outside the suite that pool sat in auto
mode and, as config/test.exs warns in its own comment about the other repo, the
first long-lived process to query held the connection until it exited.
Effects.Runner.execute/3 receipts a decision before it dispatches, which is what
makes "nothing fails open" true, so every Receipts.append/2 queued behind that
holder and the tool task blocked after the approval row existed and the card was
painted but before it could answer the Session. Hence consumed_at nil, a chain
carrying the ask and not the allow, and a visible "error: the tool timed out".
Every symptom was real and the cause was mine. The intermittency was the sandbox
connection sometimes being free.
What lands:
- scripts/dev_demo_approval.sh: the sandbox pool stripped from both repos, the
receipts database pointed at its own file rather than the test suite's, and no
apostrophes inside the -e block. That last one is not cosmetic: a single
apostrophe terminates the quoted string and the script dies with no output.
Browser drives go from stalling in tool_wait at 14s and counting to reaching
approval_wait at t=0s and completing 5 of 5, with the chain reading ask,
allow, effect admit, effect done.
- One change under lib/, and only one, because the instruction was explicit that
no product bug be invented to fill the slice. The catch-all for
{:approval, _, _} discarded any unmatched approval event without a word, and a
trace showed :requested discarded in tool_wait on every single turn. A
:requested stays routine and is deliberately not given a second path into the
approval flow. Anything else is an answer with no held call to apply it to,
which is the one shape that can lose a decision, so it is logged and receipted
as outcome=dropped. The receipt is written off the session's task supervisor,
never inline, because blocking the Session on a receipt write is the precise
shape of the stall this slice was opened to explain. Red first: AC6 fails
against the old clause at 3/4 and passes at 4/4.
- Three LiveView tests as regression cover. They pass against unmodified lib/
and PROOF.md says so rather than dressing them as a fix. They exist because
every approval test in the tree synchronises on {:state, :approval_wait}
before clicking, which no real caller can do, so the ordering a browser
actually produces had never been exercised.
Signed-off-by: Ayla Croft <aylacroft@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This slice is a retraction. The defect it was opened for does not exist. The scope here is a correction, not a handshake rewrite, and deliberately does not invent a product bug to fill itself.
What I got wrong
The approval handshake is correct. A decision arriving while the Session is in
tool_waitis postponed and redelivered on the next state change, exactly as the comment insession.exclaims. A decision made straight throughPermissions.decide_request/3, with no LiveView in the loop, is applied. The invariant already held.Both reproductions are withdrawn.
The suite one was my own test bug. It ended with
collect(id, &match?({:state, :idle}, &1), 15_000).collect/3reads the mailbox, and an{:state, :idle}broadcast from before the message was sent was already sitting in it, so it returned at once and every assertion ran mid-turn. The tell was in the output all along:Finished in 0.2 secondsfor a test nominally waiting fifteen.The live one was
scripts/dev_demo_approval.sh, which I wrote the previous day for the README assets. The trace is what settles it:tools_donenever arrives. The Session is not losing the decision; it postpones it correctly and is still waiting on a tool task that never answers. So the question was never "why is the decision lost" but "why does the tool task never return".The script stripped
pool: Ecto.Adapters.SQL.SandboxfromTrinity.Repoand did nothing forTrinity.Repo.Receipts. Outside the suite that pool sits in auto mode and, asconfig/test.exswarns in its own comment about the other repo, "the first long-lived process to query holds a connection until it exits."Effects.Runner.execute/3receipts a decision before it dispatches, which is what makes "nothing fails open" true, so everyReceipts.append/2queued behind that holder and the tool task blocked after the approval row existed and the card was painted but before it could answer the Session.Hence
consumed_at: nil, a chain carrying the ask and not the allow, and a visibleerror: the tool timed out. Every symptom I reported was real. The cause was mine. The intermittency was the sandbox connection sometimes happening to be free.What lands
The harness fix, which is the actual defect
tool_waitat 14s and countingapproval_waitat t=0sThree changes:
:pooldeleted from both repos, receipts pointed at their own database rather than the test suite's, and no apostrophes inside the-eblock. That last is not cosmetic: one apostrophe terminates the single-quoted string and the script dies producing no output at all.One change under
lib/, red firstThe catch-all
handle_event(:info, {:approval, _, _}, _state, _data)discarded any unmatched approval event without a word. A trace shows:requesteddiscarded intool_waiton every single turn.:requestedstays routine and is deliberately not given a second path into the approval flow. Which calls are held is learned fromtools_done, in one place; learning it twice is how a call gets held twice. Logged at debug, dropped.outcome=dropped, naming the state it arrived in.The receipt is written off the session's task supervisor, never inline, because blocking the Session on a
Receipts.append/2call is the precise shape of the stall this slice was opened to explain.Red first, against the old clause:
With the clause in place:
Result: 4 passed.Three LiveView tests as regression cover, and they are NOT red-then-green
Said plainly because it would be easy to present them as a fix. All three pass against unmodified
lib/. There is no application change behind them. PROOF.md states this in those words.They are worth keeping because every approval test in the tree synchronises on
{:state, :approval_wait}before clicking, which no real caller can do: the card renders from the gate's:requestedbroadcast, emitted from inside the tool task before the Session has returned fromstart_tools. The ordering a browser actually produces had never been exercised. It is now, waiting on the painted card and a real deadline, never oncollect(:idle)orcollect(:approval_wait).AC5 is the one that proves the invariant already held: it decides through the gate with no view in the loop at the first observable instant, and it passes.
A flake I introduced and removed: 3 failures in 10 on checking for the card before the LiveView had re-rendered, because the test subscribes to the same topic and can win that race. 12 of 12 after waiting for the card.
For the reviewer
Read the retraction in NOTES.md first. The main output of this slice is that I was wrong, and you should be able to check that rather than take it. The only
lib/change is the discard clause; worth confirming that logging and receipting a dropped decision is what you want rather than raising or refusing.