test(desktop): add a leak-proof seam for the effective-owner revocation - #12664
Conversation
`effectiveOwnerTransitionInProgress` reads a `private` singleton that no suite can set or clear. When an owner transition is abandoned mid-flight the revocation stays active, `RuntimeOwnerIdentity.currentOwnerId` returns nil for every caller afterwards, and no test in the process can recover — the failure surfaces later, in an unrelated suite, as something else entirely. Two changes, both narrow: `withEffectiveOwnerTransitionForTests` is scoped rather than a `begin()`/`end()` pair, so a test cannot leave the revocation active even if its body throws. It takes the caller's isolation so a `@MainActor` suite can pass an isolated closure without sending it. `resetEffectiveOwnerTransitionForTests` clears a revocation leaked from elsewhere. `KernelTurnRecordedProjectionTests.setUp` now fails with the real cause when it inherits an active revocation, then clears it so the suite still runs. Previously that inheritance could only show up as a confusing downstream assertion. `testEffectiveOwnerTransitionSeamNeverLeaksTheRevocation` covers the return and throw paths of the new seam. This does not close the `KernelTurnRecordedProjectionTests` leg of BasedHardware#12039. It builds the instrument that leg needs. Two hypotheses for that flake are now empirically excluded rather than merely untested: an unresolvable owner does not cause it (`performMainChatHarnessResetTransaction` installs one via `withAutomationOwnerIfMissing`, and the reset completes with `error=nil`, `clearCalls=[9]`), and neither does an active global revocation — the reset completes there too. Whether the harness reset *should* proceed while owner resolution is globally revoked is a real question this raises, and a maintainer call rather than something to silently pin. Verification: 38/38 in the suite; 91/91 with SuggestedTasksStoreTests, RuntimeOwnerIdentityTests and EffectiveOwnerDatabaseBoundaryTests in one process. Failure-Class: none
|
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.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
cubic caught a real defect in the seam this PR adds. `defer { end() }` cleared the
shared boolean unconditionally, so the "leak unrepresentable" claim only held when the
seam was entered from an already-clear state. Two cases broke it:
- A nested scope released its caller's revocation on the way out, leaving the outer body
running unrevoked.
- Entering while a revocation was already leaked into the process cleared that leak — the
precise condition `setUp` exists to report, silently swallowed by the seam built to
expose it.
The seam now captures the entry state and only ends what it started. I did not take the
reference-counting suggestion: the counter would have to live on the singleton that
production's `beginAuthorizationRevocation`/`endAuthorizationRevocation` also drives, so
it would change production transition semantics to fix a test-only seam. Restoring the
entry state fixes the seam's own contract and leaves that alone.
A body that triggers a real owner transition can still have its revocation ended by
production while the body runs. That is inherent to a shared boolean rather than
something this seam can fix, and no test drives it.
Verification: 39/39 in the suite. `testSeamRestoresTheRevocationStateItFound` covers
nesting and inherited-leak entry; reverting to the unconditional `defer` fails both
assertions — "a nested scope must not release its caller's revocation" and "an inherited
revocation must outlive an inner scope".
Failure-Class: none
Release compile failed where debug passed:
RuntimeOwnerIdentity.swift:229:22: error: non-sendable result type 'T'
cannot be sent from nonisolated context in call to parameter 'body'
The seam is `@MainActor` but its `body` was not, so returning a generic `T` across
that boundary needs `T: Sendable` — a requirement release-mode concurrency checking
enforces and the debug build does not. Marking `body` `@MainActor` removes the
crossing entirely rather than constraining `T`, which would have pushed the
requirement onto every future caller for no reason: the only callers are `@MainActor`
XCTestCases already.
Verified with `-strict-concurrency=complete`, which is what surfaces this locally —
a plain debug build does not, which is why the first attempt looked fine here and
failed in CI. Build clean; KernelTurnRecordedProjectionTests 39/39.
Failure-Class: none
kodjima33
left a comment
There was a problem hiding this comment.
Desktop test-tooling seam (not a fix per author's own framing — 'reviewed as tooling, not as a fix'); scores 2/5 on the bug-fix heuristic and doesn't describe end-user behavior change, so it doesn't qualify for owner-override merge. Purely additive test seam, CI green/pending-only, no abuse signals. Approve-only, staying open for a maintainer look at the open question raised (owner-resolution-revoked harness reset) rather than merging speculatively.
|
Thanks @aryanorastar — verified the full diff against the head; this is a clean, purely additive piece of test tooling and the right scope call (build the instrument, assert nothing you aren't sure of).
On the earlier nested-scope finding: the c75efd3 fix (restore the entry state instead of ending unconditionally) addresses it correctly, and declining the refcount was the right call — a counter would have to live on the singleton that production's
Changelog: Both desktop checks are still queued on this head, so treat the above as statically verified; the lane is the confirmation. Leaving the merge call for human maintainer review once those land. by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
…tion BasedHardware#12664 raised an open question and declined to answer it: should a harness reset proceed while owner resolution is globally revoked? It currently does. The premise does not hold. withAutomationOwnerIfMissing asks currentOwnerId(), gets nil -- the revocation working as designed -- concludes an owner is missing, and installs one through performEffectiveOwnerTransition. That transition's endAuthorizationRevocation calls EffectiveOwnerAuthorizationRevocation.end(), clearing the outstanding flag, because the revocation is a plain boolean rather than a counter. The reset therefore runs with owner resolution restored, not revoked. It does not bypass the revocation; it dissolves it through the sanctioned path. Pin the mechanism rather than the outcome. If the revocation ever became a refcount the body would run genuinely revoked, and this assertion catches that as the behavior change it would be. Two supporting facts, verified statically and left as comments rather than tests: EffectiveOwnerTransitionFence.beginTransition admits one transition at a time and queues the rest, so the boolean never nests in production; and performMainChatHarnessResetTransaction is guarded by AppBuild.isNonProduction, so it never runs in production at all. Failure-Class: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed The question: should a harness reset proceed while owner resolution is globally revoked? It currently does. The premise does not hold. The reset never runs under a revocation — it dissolves it first.
Two supporting facts, verified statically and left as comments rather than tests:
So: no defect, nothing to file, and no maintainer decision needed on this point. One test I wrote and then deleted, because it was passing for the wrong reason. I had a second test asserting that The only ways around it are writing a synthetic
|
What
Adds a leak-proof test seam over the process-global effective-owner revocation, and makes
KernelTurnRecordedProjectionTestsreport an inherited revocation as itself.This does not close a leg of #12039. It builds the instrument that leg needs, and records two hypotheses that are now empirically excluded. Flagging that up front so it is reviewed as tooling, not as a fix.
Why
RuntimeOwnerIdentity.effectiveOwnerTransitionInProgressreads aprivatesingleton:Production brackets it correctly —
LocalMutationAuthorizationcallsendAuthorizationRevocation()on both the success andcatchpaths. But an owner transition abandoned mid-flight (a suspended, never-resumed task) leaves it active, and then:currentOwnerIdreturns nil for every caller in the process, andSo one suite's abandoned transition silently changes behavior for everything that runs after it, and surfaces somewhere unrelated.
The seam
withEffectiveOwnerTransitionForTestsis scoped rather than abegin()/end()pair, so a test cannot leave the revocation active even if its body throws — the leak is unrepresentable rather than merely discouraged. It takes the caller's isolation (isolation: isolated (any Actor)? = #isolation) so a@MainActorsuite can pass an isolated closure without sending a non-Sendable value.resetEffectiveOwnerTransitionForTestsclears a revocation leaked from elsewhere.Naming follows the existing
resetForTests()convention (DesktopDiagnosticsManager,PushToTalkManager). I deliberately did not gate these behind#if DEBUG: a release-mode test lane exists, and DEBUG-gating symbols referenced from tests would break it.Leak detection
KernelTurnRecordedProjectionTests.setUpnow fails with the actual cause when it inherits an active revocation, then clears it so the suite still runs. Previously that inheritance could only appear as a confusing downstream assertion in whichever test ran first.What this rules out for #12039
Both were tested, not assumed:
auth_userIdandautomationOwnerOverrideboth cleared,currentOwnerIdreturns nil — and the reset still completes:error=nil,clearCalls=[9].performMainChatHarnessResetTransactioninstalls an owner itself viawithAutomationOwnerIfMissing.That leaves an open question worth a maintainer's eye, which I have not pinned either way: should a harness reset proceed while owner resolution is globally revoked? It currently does. Asserting the present behavior would enshrine something that may be a defect, so this PR only reports it.
Product invariants affected
product/invariants/auth-session.md) —RuntimeOwnerIdentity.swiftsits under this invariant's globs.Neither invariant's behavior changes. The diff to that file is purely additive: two
…ForTestsstatics that wrapbegin()/end()on the existing revocation singleton. No production call site is added or altered,currentOwnerId's guard is untouched, and no signed-in/owner state is written. The seam can only be reached from test code.Verification
KernelTurnRecordedProjectionTests.SuggestedTasksStoreTests,RuntimeOwnerIdentityTestsandEffectiveOwnerDatabaseBoundaryTestsin one process.testEffectiveOwnerTransitionSeamNeverLeaksTheRevocationcovers both the return and throw paths of the seam.One production file changes, and only by addition: two test-only statics. No existing behavior is modified.
Related: #12650 isolates the Suggested-tasks feedback owner (a different leg of #12039).
Failure-Class: none