From 8407ea0a6f0ab8bd60151f8d966cd40bb9f43f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 22:43:20 +0200 Subject: [PATCH 1/4] refactor: complete explicit lifetime fixture migration --- .../app-log-session-resource.test.ts | 2 +- src/daemon/__tests__/back-runtime.test.ts | 2 +- .../__tests__/capture-disclosure.test.ts | 12 ++--- src/daemon/__tests__/daemon-policy.test.ts | 2 +- .../__tests__/daemon-runtime-app-log.test.ts | 2 +- src/daemon/__tests__/focus-runtime.test.ts | 2 +- src/daemon/__tests__/generic-settle.test.ts | 2 +- .../__tests__/human-control-router-fixture.ts | 2 +- src/daemon/__tests__/is-runtime.test.ts | 29 +++++----- src/daemon/__tests__/lease-lifecycle.test.ts | 4 +- .../open-device-contention-wait.test.ts | 13 +++-- .../__tests__/orientation-runtime.test.ts | 2 +- .../perf-capture-session-resource.test.ts | 2 +- ...eplay-divergence-android-occlusion.test.ts | 2 +- ...n-replay-divergence-capture-policy.test.ts | 4 +- ...on-replay-divergence-chrome-filter.test.ts | 17 ++++-- ...sion-replay-divergence-observation.test.ts | 5 +- .../session-replay-divergence-overlay.test.ts | 10 ++-- ...sion-replay-divergence-publication.test.ts | 2 +- ...sion-replay-divergence-suggestions.test.ts | 5 +- ...on-replay-maestro-error-projection.test.ts | 2 +- .../session-replay-maestro-failure.test.ts | 8 +-- ...n-replay-maestro-remote-evalscript.test.ts | 2 +- ...ion-replay-maestro-session-address.test.ts | 2 +- ...on-replay-runtime-maestro-dispatch.test.ts | 4 +- ...ay-runtime-maestro-shell-artifacts.test.ts | 2 +- .../session-replay-repair-acceptance.test.ts | 7 ++- .../session-replay-repair-empty-tail.test.ts | 30 ++++++++--- .../session-replay-repair-loop.test.ts | 13 +++-- ...ion-replay-repair-record-exclusion.test.ts | 5 +- ...-repair-transaction-close-ordering.test.ts | 4 ++ ...on-replay-repair-transaction-force.test.ts | 3 ++ ...sion-replay-repair-transaction.fixtures.ts | 7 ++- .../session-replay-repair-transaction.test.ts | 5 +- .../session-replay-repair.fixtures.ts | 19 ++++--- .../replay-runtime/application.test.ts | 6 +-- ...ion-replay-runtime-artifact-ledger.test.ts | 10 +++- .../session-replay-runtime-binding.test.ts | 2 +- ...on-replay-runtime-failure-response.test.ts | 22 ++++++-- .../session-replay-runtime-failure.test.ts | 33 ++++++++---- ...ession-replay-runtime-keep-session.test.ts | 18 +++---- .../session-replay-runtime-plan.test.ts | 28 +++++----- .../session-replay-runtime.fixtures.ts | 4 +- .../session-replay-runtime.test.ts | 45 ++++++++-------- .../session-replay-script-source.test.ts | 10 ++-- .../replay-suite/session-test-runner.test.ts | 8 +-- .../session-test-suite-command-video.test.ts | 4 +- .../replay-suite/session-test-suite.test.ts | 6 +-- ...sion-replay-dispatch-selector-miss.test.ts | 5 +- .../session-replay-scenario.fixtures.ts | 2 +- .../session-replay-selector-routes.test.ts | 2 +- .../request-dispatch-disclosure.test.ts | 2 +- ...quest-execution-scope-lease-expiry.test.ts | 2 +- ...t-execution-scope-open-device-wait.test.ts | 17 +++--- .../__tests__/request-execution-scope.test.ts | 38 ++++++------- ...quest-handler-chain-provider-scope.test.ts | 2 +- .../__tests__/request-handler-chain.test.ts | 4 +- .../request-router-android-modal.test.ts | 6 +-- .../request-router-android-perf.test.ts | 2 +- ...est-router-android-snapshot-helper.test.ts | 2 +- .../request-router-artifacts-web.test.ts | 2 +- .../__tests__/request-router-cost.test.ts | 10 ++-- .../__tests__/request-router-events.test.ts | 4 +- .../request-router-lock-policy.test.ts | 10 ++-- .../__tests__/request-router-open.test.ts | 6 +-- ...request-router-record-runtime-lock.test.ts | 2 +- .../request-router-recording-health.test.ts | 4 +- .../request-router-replay-scope.test.ts | 6 +-- .../request-router-response-level.test.ts | 2 +- .../request-router-screenshot.test.ts | 6 +-- .../request-router-typed-error.test.ts | 10 ++-- .../request-runtime-binding-router.test.ts | 2 +- .../request-save-script-transports.test.ts | 2 +- .../__tests__/runtime-binding-conformance.ts | 2 +- .../selector-capture-binding.test.ts | 4 +- .../selector-capture-runtime.test.ts | 6 +-- .../__tests__/selector-recording.test.ts | 10 ++-- .../__tests__/session-idle-activity.test.ts | 2 +- .../__tests__/session-idle-expiry-harness.ts | 4 +- src/daemon/__tests__/session-routing.test.ts | 54 ++++++++++--------- src/daemon/__tests__/session-store.test.ts | 6 +-- .../snapshot-command-runtime.test.ts | 2 +- ...shot-custom-actions-platform-guard.test.ts | 2 +- .../__tests__/snapshot-diff-runtime.test.ts | 6 +-- .../__tests__/snapshot-quality-latch.test.ts | 2 +- .../snapshot-runtime-binding.test.ts | 2 +- .../snapshot-runtime-disclosure.test.ts | 2 +- .../sparse-fallback-screenshot.test.ts | 5 +- .../__tests__/system-button-runtime.test.ts | 2 +- .../__tests__/tv-remote-runtime.test.ts | 2 +- src/daemon/__tests__/viewport-runtime.test.ts | 2 +- src/daemon/__tests__/wait-runtime.test.ts | 2 +- .../handlers/__tests__/install-source.test.ts | 16 +++--- .../handlers/__tests__/react-native.test.ts | 28 +++++----- .../record-runtime-stop-recovery.test.ts | 18 +++---- .../__tests__/record-runtime.fixtures.ts | 2 +- .../__tests__/session-appstate-input.test.ts | 14 ++--- .../__tests__/session-boot-shutdown.test.ts | 4 +- .../__tests__/session-clipboard.test.ts | 2 +- .../__tests__/session-device-claims.test.ts | 7 +-- .../session-doctor-app-runtime.test.ts | 4 +- .../__tests__/session-doctor-options.test.ts | 6 +-- .../__tests__/session-doctor-warmup.test.ts | 2 +- .../session-install-source-ref-frame.test.ts | 6 +-- .../__tests__/session-prepare.test.ts | 2 +- .../__tests__/session-push-ref-frame.test.ts | 4 +- .../handlers/__tests__/session-push.test.ts | 10 ++-- .../__tests__/session-reinstall.test.ts | 10 ++-- .../__tests__/session-relaunch-close.test.ts | 44 +++++++++------ .../__tests__/session-relaunch-guards.test.ts | 6 +-- .../__tests__/session-runtime-command.test.ts | 8 +-- .../session-script-publication.test.ts | 21 ++++---- .../session-selector-dispatch.test.ts | 7 +-- .../handlers/__tests__/snapshot-alert.test.ts | 6 +-- .../snapshot-handler-freshness.test.ts | 18 +++---- .../__tests__/snapshot-handler-wait.test.ts | 6 +-- .../__tests__/snapshot-handler.test.ts | 18 +++---- .../__tests__/snapshot-scoped-refs.test.ts | 6 +-- .../snapshot-settings-handler.test.ts | 22 ++++---- .../handlers/__tests__/trace-runtime.test.ts | 2 +- .../__tests__/wait-landmark-recording.test.ts | 2 +- .../__tests__/find-production-route.test.ts | 3 +- .../internal/__tests__/find-read-only.test.ts | 2 +- .../__tests__/find-single-bind.test.ts | 2 +- .../find-target-activation-disclosure.test.ts | 2 +- .../__tests__/find-touch-runtime-fixture.ts | 2 +- .../interaction-capture-disclosure.test.ts | 6 +-- .../__tests__/interaction-common.test.ts | 8 +-- .../interaction-dispatch-disclosure.test.ts | 8 +-- .../interaction-gesture-drag.test.ts | 4 +- .../__tests__/interaction-get.test.ts | 44 +++++++++------ .../interaction-ios-tap-outcome.test.ts | 38 ++++++------- ...nteraction-settle-private-ax-route.test.ts | 2 +- .../__tests__/interaction-settle.test.ts | 3 +- .../interaction-target-evidence.test.ts | 20 +++---- ...nteraction-touch-android-freshness.test.ts | 8 +-- ...nteraction-touch-android-readiness.test.ts | 8 +-- ...ction-touch-direct-ios-eligibility.test.ts | 17 ++++-- .../interaction-touch-direct-ios.test.ts | 9 ++-- .../__tests__/interaction-touch-fill.test.ts | 12 ++--- .../interaction-touch-payload.test.ts | 14 ++--- .../interaction-touch-press-admission.test.ts | 27 +++++----- .../__tests__/interaction-touch-press.test.ts | 21 ++++---- .../interaction-touch-response.test.ts | 12 ++--- .../interaction-touch-runtime.test.ts | 10 ++-- .../__tests__/interaction-touch.test.ts | 16 +++--- ...interaction-type-android-readiness.test.ts | 2 +- src/daemon/selector-runtime-backend.test.ts | 4 +- src/daemon/server/daemon-idle-reap.test.ts | 24 +++++---- .../daemon-runtime-device-claims.test.ts | 2 +- .../daemon-runtime-recording-teardown.test.ts | 4 +- .../server/daemon-runtime-web-cleanup.test.ts | 2 +- .../daemon-runtime-web-close-teardown.test.ts | 6 +-- ...mon-session-idle-expiry-scheduling.test.ts | 16 +++--- .../server/daemon-session-idle-expiry.test.ts | 12 +++-- ...on-capabilities-install-projection.test.ts | 2 +- .../__tests__/session-capabilities.test.ts | 16 +++--- ...session-close-cwd-scoped-resources.test.ts | 16 ++++-- .../session-close-error-precedence.test.ts | 10 ++-- .../session-close-lifecycle-runtime.test.ts | 14 ++--- .../session-close-resource-cleanup.test.ts | 8 +-- .../session-close-save-script.test.ts | 4 +- .../session-close-shutdown-platform.test.ts | 14 ++--- .../session-devices-batch-runtime.test.ts | 2 +- .../session-inventory-apps-runtime.test.ts | 2 +- .../session-inventory-harmonyos.test.ts | 2 +- .../session-inventory-scoped-paths.test.ts | 18 +++---- .../session-open-execution-runtime.test.ts | 2 +- .../__tests__/session-open-existing.test.ts | 18 +++---- .../session-open-url-prewarm.test.ts | 18 +++---- .../session-teardown-resources.test.ts | 16 +++--- .../internal/__tests__/session-audio.test.ts | 20 +++---- .../internal/__tests__/session-logs.test.ts | 8 ++- .../session-network-runtime-parity.test.ts | 8 +-- .../__tests__/session-network.test.ts | 2 +- .../__tests__/session-observability.test.ts | 5 +- .../__tests__/session-perf-runtime.test.ts | 5 +- src/daemon/session-store.ts | 28 +--------- .../nightly/concurrency-torture/bindings.ts | 2 + .../nightly/concurrency-torture/harness.ts | 8 ++- .../android-tv-remote.test.ts | 2 +- .../apple-app-log-runtime-provider.test.ts | 2 +- .../integration/provider-scenarios/harness.ts | 4 +- .../provider-scenarios/ios-fold.test.ts | 4 +- .../ios-wait-selector.test.ts | 2 +- .../network-runtime-provider.test.ts | 2 +- ...ale-provider-runtime-admission.fixtures.ts | 2 +- .../provider-scenarios/vega-tv-remote.test.ts | 2 +- 188 files changed, 880 insertions(+), 756 deletions(-) diff --git a/src/daemon/__tests__/app-log-session-resource.test.ts b/src/daemon/__tests__/app-log-session-resource.test.ts index 63936d7390..0b8f2881f5 100644 --- a/src/daemon/__tests__/app-log-session-resource.test.ts +++ b/src/daemon/__tests__/app-log-session-resource.test.ts @@ -490,7 +490,7 @@ function makeContext( createdAt: Date.now(), actions: [], }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const resourcePath = appLogResourceStore.resolvePath(sessionStore.resolveSessionDir(sessionName)); return { admissionLedger: createAppLogAdmissionLedger(), diff --git a/src/daemon/__tests__/back-runtime.test.ts b/src/daemon/__tests__/back-runtime.test.ts index e2946824c3..55dd261633 100644 --- a/src/daemon/__tests__/back-runtime.test.ts +++ b/src/daemon/__tests__/back-runtime.test.ts @@ -138,7 +138,7 @@ test('request router joins back admission to execution, recording, and ref inval const sessionStore = makeSessionStore('agent-device-back-generic-'); const session = makeSession('back-runtime', { device: appleDevice }); activateCompleteRefFrame(session); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const handler = createRequestHandler({ logPath: '/tmp/daemon.log', token: 't', diff --git a/src/daemon/__tests__/capture-disclosure.test.ts b/src/daemon/__tests__/capture-disclosure.test.ts index e848ded858..59459f360f 100644 --- a/src/daemon/__tests__/capture-disclosure.test.ts +++ b/src/daemon/__tests__/capture-disclosure.test.ts @@ -75,7 +75,7 @@ beforeEach(() => { test('mutating find on a system-surface capture discloses the occlusion on the found outcome', async () => { const sessionStore = makeSessionStore(); const session = makeAndroidSession('default'); - sessionStore.set('default', session); + sessionStore.publish('default', session); const response = await handleFindCommands({ req: { @@ -102,7 +102,7 @@ test('mutating find on a system-surface capture discloses the occlusion on the f test('read-only find exists on a system-surface capture discloses the occlusion', async () => { const sessionStore = makeSessionStore(); const session = makeAndroidSession('default'); - sessionStore.set('default', session); + sessionStore.publish('default', session); const response = await dispatchFindReadOnlyViaRuntime({ req: { @@ -128,7 +128,7 @@ test('read-only find exists on a system-surface capture discloses the occlusion' test('wait timeout for app text hidden behind a system surface discloses the occlusion', async () => { const sessionStore = makeSessionStore(); const session = makeAndroidSession('default'); - sessionStore.set('default', session); + sessionStore.publish('default', session); const response = await dispatchWaitViaRuntime({ req: { @@ -304,7 +304,7 @@ function serveSheetCapture(): void { test('mutating find on an in-place system surface discloses it on the found outcome', async () => { serveSheetCapture(); const sessionStore = makeSessionStore(); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const response = await handleFindCommands({ req: { @@ -331,7 +331,7 @@ test('mutating find on an in-place system surface discloses it on the found outc test('mutating find that misses on an in-place system surface still discloses it', async () => { serveSheetCapture(); const sessionStore = makeSessionStore(); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const response = await handleFindCommands({ req: { @@ -378,7 +378,7 @@ test('the shared disclosure helper reports an iOS system surface on both outcome */ test('wait timeout whose polls required a foreground repair discloses the repair', async () => { const sessionStore = makeSessionStore(); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); legacyDispatchCapture.mockResolvedValue({ backend: 'xctest', truncated: false, diff --git a/src/daemon/__tests__/daemon-policy.test.ts b/src/daemon/__tests__/daemon-policy.test.ts index 679f45fca9..489eaf5745 100644 --- a/src/daemon/__tests__/daemon-policy.test.ts +++ b/src/daemon/__tests__/daemon-policy.test.ts @@ -40,7 +40,7 @@ function makeHandler( options: { inventory?: readonly DeviceInfo[]; providerInventory?: readonly DeviceInfo[] } = {}, ) { const sessionStore = makeSessionStore('agent-device-daemon-policy-'); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const bind = vi.fn(lifecycleDeviceRuntimeGateway.bind); const inspectFacts = vi.fn(lifecycleDeviceRuntimeGateway.inspectFacts); const handler = createRequestHandler({ diff --git a/src/daemon/__tests__/daemon-runtime-app-log.test.ts b/src/daemon/__tests__/daemon-runtime-app-log.test.ts index 6fee6e32a4..08c46cb91b 100644 --- a/src/daemon/__tests__/daemon-runtime-app-log.test.ts +++ b/src/daemon/__tests__/daemon-runtime-app-log.test.ts @@ -123,7 +123,7 @@ test('daemon shutdown settles fenced app-log cleanup before finalization can rel forceCleanup, }); session.appLog = { handle, envelope }; - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const resourcePath = appLogResourceStore.resolvePath( sessionStore.resolveSessionDir(session.name), ); diff --git a/src/daemon/__tests__/focus-runtime.test.ts b/src/daemon/__tests__/focus-runtime.test.ts index 1d7f736966..7e4ea2f7fa 100644 --- a/src/daemon/__tests__/focus-runtime.test.ts +++ b/src/daemon/__tests__/focus-runtime.test.ts @@ -189,7 +189,7 @@ test('request router joins focus admission to execution, recording, and ref inva const sessionStore = makeSessionStore('agent-device-focus-generic-'); const session = makeSession('focus-runtime', { device: appleDevice }); activateCompleteRefFrame(session); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const handler = createRequestHandler({ logPath: '/tmp/daemon.log', token: 't', diff --git a/src/daemon/__tests__/generic-settle.test.ts b/src/daemon/__tests__/generic-settle.test.ts index afddd3d5f5..38469c4ac8 100644 --- a/src/daemon/__tests__/generic-settle.test.ts +++ b/src/daemon/__tests__/generic-settle.test.ts @@ -140,7 +140,7 @@ function seedSession( const session = makeIosSession(sessionName); setSessionSnapshot(session, buildSnapshotState(snapshotPayload(baseline), {})); activateCompleteRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); return session; } diff --git a/src/daemon/__tests__/human-control-router-fixture.ts b/src/daemon/__tests__/human-control-router-fixture.ts index 8cbc7ee6d4..ddecdf3307 100644 --- a/src/daemon/__tests__/human-control-router-fixture.ts +++ b/src/daemon/__tests__/human-control-router-fixture.ts @@ -13,7 +13,7 @@ export function createHumanControlHarness() { const lease = registry.allocateLease(HUMAN_CONTROL_LEASE_REQUEST); const sessionStore = makeSessionStore('agent-device-human-control-'); const sessionName = tenantScopedSessionName(lease.tenantId, 'takeover-test'); - sessionStore.set( + sessionStore.publish( sessionName, makeIosAppSession(sessionName, { lease: buildSessionLeaseFromRequest(humanControlRequest(lease), lease), diff --git a/src/daemon/__tests__/is-runtime.test.ts b/src/daemon/__tests__/is-runtime.test.ts index 32c84ba5b3..68fb57ee65 100644 --- a/src/daemon/__tests__/is-runtime.test.ts +++ b/src/daemon/__tests__/is-runtime.test.ts @@ -74,7 +74,10 @@ function isRequest( test('an admitted is inspects once, binds once, and answers through the bound capture', async () => { const fixture = selectorCaptureFixture({ snapshot: () => buttonSnapshot() }); const sessionStore = makeSessionStore(); - sessionStore.set('is-bound', makeAndroidSession('is-bound', { appBundleId: 'com.example.app' })); + sessionStore.publish( + 'is-bound', + makeAndroidSession('is-bound', { appBundleId: 'com.example.app' }), + ); const response = await dispatchIsViaRuntime({ req: isRequest('is-bound', ['visible', 'id=auth_continue']), @@ -95,7 +98,7 @@ test('is absent uses the bound readAny capture for selector-first input without snapshot: () => ({ nodes: [], backend: 'xctest', producer: 'apple-runner' }), }); const sessionStore = makeSessionStore(); - sessionStore.set('is-absent', makeIosAppSession('is-absent')); + sessionStore.publish('is-absent', makeIosAppSession('is-absent')); const response = await dispatchIsViaRuntime({ req: isRequest('is-absent', ['label="Removed row"', 'absent']), @@ -133,7 +136,7 @@ test('is absent bypasses a cached no-match snapshot before evaluating the bound }), }); const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( 'is-absent-fresh', makeIosAppSession('is-absent-fresh', { snapshot: cachedAbsent }), ); @@ -168,7 +171,7 @@ test('is absent fails closed for a quality-less legacy iOS root-only capture', a }), }); const sessionStore = makeSessionStore(); - sessionStore.set('is-legacy-sparse', makeIosAppSession('is-legacy-sparse')); + sessionStore.publish('is-legacy-sparse', makeIosAppSession('is-legacy-sparse')); const response = await dispatchIsViaRuntime({ req: isRequest('is-legacy-sparse', ['absent', 'label="Removed row"']), @@ -200,7 +203,7 @@ test('is absent fails closed for a quality-less legacy iOS root-only capture', a test('is absent rejects depth and scope before binding with typed invalid arguments', async () => { const fixture = selectorCaptureFixture(); const sessionStore = makeSessionStore(); - sessionStore.set('is-absent-flags', makeIosAppSession('is-absent-flags')); + sessionStore.publish('is-absent-flags', makeIosAppSession('is-absent-flags')); for (const [flag, value] of [ ['snapshotScope', 'Login'], @@ -233,7 +236,7 @@ test('an unavailable capture fact refuses before any bind', async () => { // The watchOS sentinel shape: capability-supported today, no snapshot backend at the owner. const fixture = selectorCaptureFixture({ capture: unavailableCapture }); const sessionStore = makeSessionStore(); - sessionStore.set('is-refused', makeAndroidSession('is-refused', { appBundleId: 'com.a' })); + sessionStore.publish('is-refused', makeAndroidSession('is-refused', { appBundleId: 'com.a' })); const response = await dispatchIsViaRuntime({ req: isRequest('is-refused', ['visible', 'id=auth_continue']), @@ -262,7 +265,7 @@ test('an iOS session with no tracked app is refused with the open hint, not answ snapshot: () => buttonSnapshot(), }); const sessionStore = makeSessionStore(); - sessionStore.set('is-no-app', makeIosSession('is-no-app')); + sessionStore.publish('is-no-app', makeIosSession('is-no-app')); const response = await withTestDeviceInventory( {}, @@ -291,7 +294,7 @@ test('an iOS session WITH a tracked app still answers, so the refusal is the pla snapshot: () => buttonSnapshot(), }); const sessionStore = makeSessionStore(); - sessionStore.set('is-with-app', makeIosAppSession('is-with-app')); + sessionStore.publish('is-with-app', makeIosAppSession('is-with-app')); const response = await dispatchIsViaRuntime({ req: isRequest('is-with-app', ['visible', 'label=Continue']), @@ -308,7 +311,7 @@ test('an iOS session WITH a tracked app still answers, so the refusal is the pla test('an Android session with no tracked app proceeds, because the owner advertises the without-active-app capture', async () => { const fixture = selectorCaptureFixture({ snapshot: () => buttonSnapshot() }); const sessionStore = makeSessionStore(); - sessionStore.set('is-android-no-app', makeAndroidSession('is-android-no-app')); + sessionStore.publish('is-android-no-app', makeAndroidSession('is-android-no-app')); const response = await dispatchIsViaRuntime({ req: isRequest('is-android-no-app', ['visible', 'id=auth_continue']), @@ -329,7 +332,7 @@ test('an Android session with no tracked app proceeds, because the owner adverti test('a refused request reaches the device by no route at all', async () => { const fixture = selectorCaptureFixture({ capture: unavailableCapture }); const sessionStore = makeSessionStore(); - sessionStore.set('is-direct-refused', makeIosAppSession('is-direct-refused')); + sessionStore.publish('is-direct-refused', makeIosAppSession('is-direct-refused')); mockRunAppleRunnerCommand.mockResolvedValue({ found: true, nodes: [ @@ -389,7 +392,7 @@ test('a failing predicate answers COMMAND_FAILED from the bound capture', async }), }); const sessionStore = makeSessionStore(); - sessionStore.set('is-direct-false', makeIosAppSession('is-direct-false')); + sessionStore.publish('is-direct-false', makeIosAppSession('is-direct-false')); mockRunAppleRunnerCommand.mockResolvedValue({ found: true, text: 'Apple Account', @@ -444,7 +447,7 @@ test('a miss on a surface that never settled carries the unsettled fact, and the const sessionStore = makeSessionStore(); const session = makeIosAppSession('is-unsettled'); markDeferredInteractionOutcome({ session, command: 'scroll', positionals: [], flags: {} }); - sessionStore.set('is-unsettled', session); + sessionStore.publish('is-unsettled', session); const isVisible = () => dispatchIsViaRuntime({ req: isRequest('is-unsettled', ['visible', 'id=target']), @@ -486,7 +489,7 @@ test('a read after a scroll that moved nothing carries the no-effect outcome, an const sessionStore = makeSessionStore(); const session = makeIosAppSession('is-no-effect', { snapshot: makeSnapshotState([row]) }); markDeferredInteractionOutcome({ session, command: 'scroll', positionals: ['down'], flags: {} }); - sessionStore.set('is-no-effect', session); + sessionStore.publish('is-no-effect', session); const isVisible = () => dispatchIsViaRuntime({ req: isRequest('is-no-effect', ['visible', 'id=row']), diff --git a/src/daemon/__tests__/lease-lifecycle.test.ts b/src/daemon/__tests__/lease-lifecycle.test.ts index 0ef074372c..365db14d47 100644 --- a/src/daemon/__tests__/lease-lifecycle.test.ts +++ b/src/daemon/__tests__/lease-lifecycle.test.ts @@ -22,7 +22,7 @@ test('admitRequestLeaseForLockedScope heartbeats and stores admitted lease on th deviceKey: 'ios:SIM-001', clientId: 'client-a', }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -71,7 +71,7 @@ test('cleanupExpiredLeasedSession consumes expired lease and deletes the session expiresAt: lease.expiresAt, }, }); - sessionStore.set('default', session); + sessionStore.publish('default', session); now = 1_011; const teardownSession = vi.fn(async () => {}); diff --git a/src/daemon/__tests__/open-device-contention-wait.test.ts b/src/daemon/__tests__/open-device-contention-wait.test.ts index 5b3749c60d..9c0bc8ac5d 100644 --- a/src/daemon/__tests__/open-device-contention-wait.test.ts +++ b/src/daemon/__tests__/open-device-contention-wait.test.ts @@ -56,7 +56,7 @@ function session(address: string): SessionState { function storeWithHolder(): SessionStore { const store = makeSessionStore('agent-device-open-wait-'); - store.set(HOLDER_ADDRESS, session(HOLDER_ADDRESS)); + store.publish(HOLDER_ADDRESS, session(HOLDER_ADDRESS)); return store; } @@ -161,7 +161,7 @@ test('only a fresh open with a budget and a resolved device gets a wait', () => ).toBeUndefined(); // An open onto a session that already exists is bound to a device nobody else is refused for. - sessionStore.set(OPENER_ADDRESS, session(OPENER_ADDRESS)); + sessionStore.publish(OPENER_ADDRESS, session(OPENER_ADDRESS)); expect( beginWait({ req: budget, @@ -193,7 +193,8 @@ test('a device that frees up ends the wait without claiming a spent budget', asy vi.useFakeTimers(); const req = openRequest({ waitMs: 30_000 }); const store = storeWithHolder(); - const release = setTimeout(() => store.delete(HOLDER_ADDRESS), 600); + const holder = store.lookup(HOLDER_ADDRESS)!; + const release = setTimeout(() => store.retire(holder), 600); await waitUntil( () => @@ -223,10 +224,12 @@ test('an open yields the device lock to a session that took the device after the })!; // A competing open puts its session on the device in the window between this open's look at the // free store and its first pass under the locks, and hands it back 300ms later. - setTimeout(() => store.delete(HOLDER_ADDRESS), 300); const trace = lockTrace({ onAcquire: (pass) => { - if (pass === 1) store.set(HOLDER_ADDRESS, session(HOLDER_ADDRESS)); + if (pass === 1) { + const holder = store.publish(HOLDER_ADDRESS, session(HOLDER_ADDRESS)); + setTimeout(() => store.retire(holder), 300); + } }, }); diff --git a/src/daemon/__tests__/orientation-runtime.test.ts b/src/daemon/__tests__/orientation-runtime.test.ts index 65be8bce62..218fca919f 100644 --- a/src/daemon/__tests__/orientation-runtime.test.ts +++ b/src/daemon/__tests__/orientation-runtime.test.ts @@ -195,7 +195,7 @@ test('request router joins orientation admission to execution and ref invalidati const sessionStore = makeSessionStore('agent-device-orientation-generic-'); const session = makeSession('orientation-runtime', { device: testDevice }); activateCompleteRefFrame(session); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const handler = createRequestHandler({ logPath: '/tmp/daemon.log', token: 't', diff --git a/src/daemon/__tests__/perf-capture-session-resource.test.ts b/src/daemon/__tests__/perf-capture-session-resource.test.ts index 7441e03774..de425c50ac 100644 --- a/src/daemon/__tests__/perf-capture-session-resource.test.ts +++ b/src/daemon/__tests__/perf-capture-session-resource.test.ts @@ -52,7 +52,7 @@ test('a perf stop whose pull failed re-collects the device-side trace the first createdAt: 1, actions: [], }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const capture = { action: 'start' as const, kind: 'perfetto' as const, diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts index 6f03e1c8ff..0b35a54061 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts @@ -52,7 +52,7 @@ test.each([ const root = mkdtempForTestSync('agent-device-replay-divergence-qsshade-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set( + sessionStore.publish( sessionName, makeAndroidSession(sessionName, { appBundleId: 'com.google.android.deskclock' }), ); diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts index 8bb184e656..fd0ed718de 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts @@ -61,7 +61,7 @@ test('buildReplayFailureDivergence: routes through the freshness-retry wrapper a baselineCount: 20, routeComparable: false, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); // Capture 1: stale, near-empty dump (a single bare view — no hittable/label/ // id) → `sharp-drop` vs the 20-node baseline → triggers a retry. @@ -149,7 +149,7 @@ test('buildReplayFailureDivergence: divergence capture drops the action snapshot const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; - sessionStore.set(sessionName, makeAndroidSession(sessionName, { appBundleId })); + sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); mockDispatchCommand.mockReset(); mockDispatchCommand.mockResolvedValue({ diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts index 96b8ce8153..df49dfb223 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts @@ -101,7 +101,10 @@ test('buildReplayFailureDivergence excludes keyboard chrome from screen.refs and const root = mkdtempForTestSync('agent-device-replay-divergence-keyboard-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockDispatchCommand.mockResolvedValue({ nodes: keyboardSwampedNodes('Push Article'), @@ -209,7 +212,10 @@ test('buildReplayFailureDivergence drops unlabeled non-interactive structural no const root = mkdtempForTestSync('agent-device-replay-divergence-structural-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockDispatchCommand.mockResolvedValue({ nodes: structuralNoiseNodes(), @@ -321,7 +327,10 @@ test('buildReplayFailureDivergence keeps an app inputAccessoryView control in sc const root = mkdtempForTestSync('agent-device-replay-divergence-accessory-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockDispatchCommand.mockResolvedValue({ nodes: keyboardWithAccessoryNodes(), @@ -373,7 +382,7 @@ test('buildReplayFailureDivergence excludes Android status-bar/IME chrome from s const root = mkdtempForTestSync('agent-device-replay-divergence-android-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set( + sessionStore.publish( sessionName, makeAndroidSession(sessionName, { appBundleId: 'com.callstack.agentdevicelab' }), ); diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-observation.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-observation.test.ts index 8ace66f2be..43126cf961 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-observation.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-observation.test.ts @@ -50,7 +50,10 @@ test('captureDivergenceObservation retryLaunchRace: the 12s deadline bounds retr const root = mkdtempForTestSync('agent-device-replay-divergence-deadline-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockDispatchCommand.mockImplementation(async () => { vi.setSystemTime(new Date(Date.now() + 5_000)); diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts index b72ebe7b4d..29d55a2f24 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts @@ -49,7 +49,7 @@ test('buildReplayFailureDivergence: a system-overlay window survives into screen const root = mkdtempForTestSync('agent-device-replay-divergence-overlay-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set( + sessionStore.publish( sessionName, makeAndroidSession(sessionName, { appBundleId: 'com.callstack.agentdevicelab' }), ); @@ -163,7 +163,7 @@ test('buildReplayFailureDivergence: a fully-captured overlay dismiss-target enum const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; - sessionStore.set(sessionName, makeAndroidSession(sessionName, { appBundleId })); + sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); mockDispatchCommand.mockResolvedValue({ nodes: appSwampWithTrailingOverlay(appBundleId), @@ -259,7 +259,7 @@ test('buildReplayFailureDivergence: when a system overlay mass-covers the app, t const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; - sessionStore.set(sessionName, makeAndroidSession(sessionName, { appBundleId })); + sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); mockDispatchCommand.mockResolvedValue({ nodes: overlayMassCoveringApp(appBundleId), @@ -334,7 +334,7 @@ test('buildReplayFailureDivergence: a mass-covered app with no actionable overla const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; - sessionStore.set(sessionName, makeAndroidSession(sessionName, { appBundleId })); + sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); mockDispatchCommand.mockResolvedValue({ nodes: bareScrimMassCoveringApp(appBundleId), @@ -384,7 +384,7 @@ test('buildReplayFailureDivergence: the partial ref frame authorizes exactly the const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; - sessionStore.set(sessionName, makeAndroidSession(sessionName, { appBundleId })); + sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); mockDispatchCommand.mockReset(); mockDispatchCommand.mockResolvedValue({ diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-publication.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-publication.test.ts index 916e7b5238..e11375756c 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-publication.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-publication.test.ts @@ -43,7 +43,7 @@ function scenario(refCount = 20) { }; setSessionSnapshot(session, prior); markSessionPartialRefsIssued(session, ['old']); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const replaySession = replaySessionForTest(sessionStore, sessionName); const snapshot: SnapshotState = { createdAt: 2, diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts index eeef2819df..bea2d6185f 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts @@ -27,7 +27,10 @@ test('buildReplayFailureDivergence dedupes suggestions using the strongest basis const root = mkdtempForTestSync('agent-device-replay-suggest-dedupe-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockDispatchCommand.mockResolvedValue({ nodes: [ diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts index a9593097bd..4fecc2aa32 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts @@ -20,7 +20,7 @@ test('a typed Maestro replay error keeps its recovery hint', async () => { const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, `${stringify({ appId: 'com.example.app' })}---\n${stringify([])}`); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const req = { token: 'test-token', diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts index 13b01bec0e..d3bc88955b 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts @@ -59,7 +59,7 @@ async function buildFailureScenario( const root = mkdtempForTestSync('agent-device-maestro-suggestions-'); const sessionName = 'default'; const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const replaySession = replaySessionForTest(sessionStore, sessionName); mockDispatchCommand.mockResolvedValue({ nodes, truncated: false, backend: 'xctest' }); const failure = await captureMaestroFailure(command, path.join(root, 'flow.yaml')); @@ -177,7 +177,7 @@ test('typed Maestro failure diagnostics render expanded selector values without const root = mkdtempForTestSync('agent-device-maestro-expanded-selector-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); const label = 'Continue checkout'; fs.writeFileSync( @@ -240,7 +240,7 @@ test('typed Maestro nested scopes retain resolved target values after unwind', a const root = mkdtempForTestSync('agent-device-maestro-nested-redaction-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); const tracePath = path.join(root, 'replay-timing.ndjson'); const targetLabel = 'Nested checkout target'; @@ -336,7 +336,7 @@ test('typed Maestro renders flow-local values when static include resolution fai const root = mkdtempForTestSync('agent-device-maestro-include-redaction-'); const sessionName = 'default'; const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); const flowName = 'checkout-details'; fs.writeFileSync( diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts index 933c71086e..2ad7742cdd 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts @@ -47,7 +47,7 @@ async function runWithNetworkFlag(publicNetworkOnly: boolean | undefined) { const root = mkdtempForTestSync('agent-device-maestro-remote-wire-'); const flowPath = writeFlow(root, 'flow.yaml'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const req = { token: 'test-token', diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts index 0e0a7e8154..aa47b06842 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts @@ -22,7 +22,7 @@ test('a typed Maestro selector conflict names the session store key, not "defaul const sessionStore = new SessionStore(path.join(root, 'sessions')); const session = makeIosSession('default'); session.sessionScope = { kind: 'cwd', id: '8bea844ab16aa9b3' }; - sessionStore.set(SCOPED_ADDRESS, session); + sessionStore.publish(SCOPED_ADDRESS, session); const req = { token: 'test-token', diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts index 6385f6edbf..36fd939d73 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts @@ -116,7 +116,7 @@ test('runReplayCommand reports snapshot diagnostics from per-action session samp ['snapshot', 'snapshot', 'snapshot', 'snapshot', 'snapshot', ''].join('\n'), ); const sessionStore = new SessionStore(path.join(root, 'state')); - sessionStore.set( + sessionStore.publish( 's', makeIosSession('s', { snapshotDiagnostics: { samples: [] }, @@ -180,7 +180,7 @@ test('runReplayCommand reports snapshot diagnostics on replay failure', async () ['snapshot', 'snapshot', 'snapshot', 'snapshot', 'click "Missing"', ''].join('\n'), ); const sessionStore = new SessionStore(path.join(root, 'state')); - sessionStore.set( + sessionStore.publish( 's', makeIosSession('s', { snapshotDiagnostics: { samples: [] }, diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts index fafe08603d..0c58468436 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts @@ -46,7 +46,7 @@ test('runReplayCommand writes per-action timing events to active trace', async ( fs.writeFileSync(tracePath, ''); const sessionStore = new SessionStore(path.join(root, 'state')); - sessionStore.set('s', { + sessionStore.publish('s', { name: 's', device: { platform: 'apple', diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-acceptance.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-acceptance.test.ts index 617aabcd7e..2461acd073 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-acceptance.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-acceptance.test.ts @@ -77,7 +77,10 @@ test('a healed script survives repair + fresh-session replay: self-contained ope const root = mkdtempForTestSync('agent-device-replay-repair-accept-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch --platform ios --metro-port 8081', SAVE_ANNOTATION, @@ -163,7 +166,7 @@ test('a healed script survives repair + fresh-session replay: self-contained ope invoke: makeRecordingReplayInvoke({ sessionStore: freshSessionStore, sessionName: freshSessionName, - openReplacesSession: true, + openRebuildsActions: true, spy: invokedFresh, }), }); diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts index f6e7b0883d..33a6908f82 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts @@ -86,7 +86,10 @@ test('a record-and-heal divergence on the LAST step resumes with an empty tail a const root = mkdtempForTestSync('agent-device-replay-empty-tail-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch', @@ -213,7 +216,10 @@ test('a manual divergence (unannotated action-failure) on the LAST step resumes const root = mkdtempForTestSync('agent-device-replay-empty-tail-manual-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); // No target-v1 annotation: an unannotated action-failure always routes to // the `manual` fail-safe (no recorded targetEvidence). const filePath = writeReplayFile(root, ['open "Demo" --relaunch', 'click label="Save"']); @@ -327,7 +333,10 @@ test('a caution (identity-mismatch) divergence on the LAST step resumes with an const root = mkdtempForTestSync('agent-device-replay-empty-tail-caution-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const SAVE_ANNOTATION = '# agent-device:target-v1 {"id":"save","role":"button","label":"Save","ancestry":[],"sibling":0,"viewportOrder":0,"verification":"verified"}'; const filePath = writeReplayFile(root, [ @@ -451,7 +460,10 @@ test('--from N stays legal for a caution divergence even after the N + 1 empty-t const root = mkdtempForTestSync('agent-device-replay-empty-tail-caution-from-n-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const SAVE_ANNOTATION = '# agent-device:target-v1 {"id":"save","role":"button","label":"Save","ancestry":[],"sibling":0,"viewportOrder":0,"verification":"verified"}'; const filePath = writeReplayFile(root, [ @@ -545,7 +557,10 @@ test('an unauthorized --from one past the plan end is rejected on an ARMED sessi const root = mkdtempForTestSync('agent-device-replay-empty-tail-state-repair-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch', @@ -628,7 +643,10 @@ test('a stale --plan-digest on an empty-tail resume is rejected WITHOUT consumin const root = mkdtempForTestSync('agent-device-replay-empty-tail-digest-retry-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch', diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-loop.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-loop.test.ts index 2a00c0e8ac..efef45cdff 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-loop.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-loop.test.ts @@ -84,7 +84,7 @@ function setup(prefix: string, sessionOverrides = {}) { const root = mkdtempForTestSync(prefix); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, sessionOverrides)); + sessionStore.publish(sessionName, makeIosSession(sessionName, sessionOverrides)); return { root, sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; } @@ -243,7 +243,7 @@ test('R2 bypass guard: a PLAIN full replay (no --save-script) on an armed sessio expect(sessionStore.get(sessionName)!.actions.length).toBe(armedActionCount); }); -test('R6 no amputation: a pre-populated session whose step-1 open REPLACES the session healed-slices exactly this run', async () => { +test('R6 no amputation: rebuilding the action list during open heals exactly this run', async () => { // Pre-seed with 2 prior, unrelated actions. const { root, sessionStore, sessionName, logPath } = setup( 'agent-device-replay-repair-amputate-', @@ -260,13 +260,12 @@ test('R6 no amputation: a pre-populated session whose step-1 open REPLACES the s 'click id="b"', ]); - // open REPLACES the session with a fresh `actions: []` one (the real - // new-session branch, session-open-surface.ts) — the case the old pre-loop - // boundary=N would amputate (slice(2) drops the healed open + first click). + // Resetting the action list would make the pre-loop boundary amputate the + // healed open and first click. The coordinator must use the rebuilt record. const invoke = makeRecordingReplayInvoke({ sessionStore, sessionName, - openReplacesSession: true, + openRebuildsActions: true, evidence: (req) => (req.command === 'click' ? freshEvidence('x', 'X') : undefined), }); @@ -528,7 +527,7 @@ test('Fix 3: only the TERMINAL close is skipped during a repair — a mid-plan c sessionStore, sessionName, spy, - openReplacesSession: true, + openRebuildsActions: true, }); const response = await runReplayForTest({ diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts index e998f0bcf0..81a7d9fbdb 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts @@ -139,7 +139,10 @@ function setup(prefix: string) { const root = mkdtempForTestSync(prefix); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); return { root, sessionStore, diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-close-ordering.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-close-ordering.test.ts index c4065cff2d..f618e7a798 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-close-ordering.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-close-ordering.test.ts @@ -94,6 +94,7 @@ test('BLOCKER 2 (new): a repair close whose PLATFORM close fails never commits a const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-platform-close-fail-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); const session = makeCompleteRepairSession(sessionStore, sessionName, root); const healedPath = path.join(root, 'flow.healed.ad'); @@ -175,6 +176,7 @@ test('BLOCKER 3 (second follow-up): a retry after a SUCCESSFUL platform close bu const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-close-idempotent-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); makeCompleteRepairSession(sessionStore, sessionName, root); const healedPath = path.join(root, 'flow.healed.ad'); // A prior COMPLETE (sentinel-marked) healed artifact already sits at the @@ -273,6 +275,7 @@ test('BLOCKER 3 (third follow-up): an untargeted close that performed NO platfor const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-close-identity-untargeted-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); makeCompleteRepairSession(sessionStore, sessionName, root); const healedPath = path.join(root, 'flow.healed.ad'); // A prior COMPLETE artifact already sits at the default path so the commit @@ -318,6 +321,7 @@ test('BLOCKER 3 (third follow-up): a retry targeting a DIFFERENT app than the su const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-close-identity-changed-target-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); makeCompleteRepairSession(sessionStore, sessionName, root); const healedPath = path.join(root, 'flow.healed.ad'); fs.writeFileSync( diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-force.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-force.test.ts index 4f2192f2ea..034783ee9f 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-force.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction-force.test.ts @@ -111,6 +111,7 @@ test('#1258: close --save-script --force overwrites an existing COMPLETE healed const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-force-overwrite-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); makeCompleteRepairSession(sessionStore, sessionName, root); // A prior COMPLETE (sentinel-marked) healed artifact already sits at the // default path — `--force` must overwrite it instead of refusing. @@ -392,6 +393,7 @@ test('#1258 force is per-target: re-arming --save-script= WITHOUT --force dro 'agent-device-repair-transaction-retarget-clears-force-', ); // Armed and forced for target (flow.healed.ad). + sessionStore.retire(sessionStore.lookup(sessionName)!); const session = makeCompleteRepairSession(sessionStore, sessionName, root); session.scriptPublication = repairPublication('complete', { boundary: 0, @@ -437,6 +439,7 @@ test('#1258 force per-target, contrast: re-arming --save-script= WITH --force const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-retarget-force-overwrites-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); const session = makeCompleteRepairSession(sessionStore, sessionName, root); session.scriptPublication = repairPublication('complete', { boundary: 0, diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts index eecc71704a..66e45ec990 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts @@ -32,7 +32,10 @@ export function setup(prefix: string) { const root = mkdtempForTestSync(prefix); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); return { root, sessionStore, @@ -74,6 +77,6 @@ export function makeCompleteRepairSession( }, ], }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); return session; } diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.test.ts b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.test.ts index a8841a4ffe..8412b5609d 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.test.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair-transaction.test.ts @@ -306,7 +306,7 @@ test('C5a: an incomplete repair reaped by idle-reap leaves a tombstone (no heale // nothing (not complete) and a tombstone is left behind (the exact teardown // step daemon-runtime.ts's teardownDaemonSession runs). sessionStore.finalizeRepairTeardown(storeSessionForTest(sessionStore, session)); - sessionStore.delete(sessionName); + sessionStore.retire(sessionStore.lookup(sessionName)!); expect(fs.existsSync(path.join(root, 'flow.healed.ad'))).toBe(false); const tombstone = sessionStore.readRepairTombstone(sessionName); @@ -400,7 +400,7 @@ test('BLOCKER 1: a --from continuation on a reaped session returns SESSION_NOT_F // Idle-reap tears the incomplete repair down, leaving a tombstone. sessionStore.finalizeRepairTeardown(sessionStore.lookup(sessionName)!); - sessionStore.delete(sessionName); + sessionStore.retire(sessionStore.lookup(sessionName)!); expect(sessionStore.readRepairTombstone(sessionName)).toBeDefined(); // A `--from` continuation targeting the (now reaped) session must surface @@ -423,6 +423,7 @@ test('BLOCKER 2b/2c: a close whose commit FAILS (no-clobber) keeps the session f const { root, sessionStore, sessionName, logPath, leaseRegistry } = setup( 'agent-device-repair-transaction-commit-fail-', ); + sessionStore.retire(sessionStore.lookup(sessionName)!); makeCompleteRepairSession(sessionStore, sessionName, root); // A prior COMPLETE (sentinel-marked) healed artifact already sits at the // default path — the commit must refuse to clobber it. diff --git a/src/daemon/__tests__/replay-repair/session-replay-repair.fixtures.ts b/src/daemon/__tests__/replay-repair/session-replay-repair.fixtures.ts index 40af9dc39e..41efd58afa 100644 --- a/src/daemon/__tests__/replay-repair/session-replay-repair.fixtures.ts +++ b/src/daemon/__tests__/replay-repair/session-replay-repair.fixtures.ts @@ -1,5 +1,6 @@ import { storeSessionForTest } from '../../../__tests__/test-utils/store-factory.ts'; import { isSessionRecording } from '../../session-script-publication-capability.ts'; +import { NO_SCRIPT_PUBLICATION } from '../../session-script-publication-state.ts'; /** * Shared fixtures for the ADR 0012 decision 6 repair-loop tests. The mock * `invoke` in these tests must ACTUALLY record via `sessionStore.recordAction` @@ -32,11 +33,10 @@ export type RecordingReplayInvokeConfig = { /** Records every request seen, in order — for asserting dispatch order/flags. */ spy?: DaemonRequest[]; /** - * When true, `open` REPLACES the session with a fresh `actions: []` one — - * mimicking `session-open-surface.ts`'s new-session branch. Default records - * onto the existing session (creating one only if none exists yet). + * Rebuild the same lifetime with an empty action list during open, exercising + * healed-slice boundaries when the underlying record changes. */ - openReplacesSession?: boolean; + openRebuildsActions?: boolean; /** * Steps that fail: return `{ ok: false }` WITHOUT recording — mimicking a * dispatch failure that never reaches `finalizeTouchInteraction`. Keyed by @@ -86,10 +86,13 @@ function isFailStep(failSteps: ReadonlySet | undefined, req: DaemonReque } function resolveInvokeSession(config: RecordingReplayInvokeConfig, req: DaemonRequest) { - const existing = config.sessionStore.get(config.sessionName); - const mustCreate = req.command === 'open' && (config.openReplacesSession || !existing); - if (!mustCreate && existing) return existing; + const ref = config.sessionStore.lookup(config.sessionName); + if (ref) { + return req.command === 'open' && config.openRebuildsActions + ? config.sessionStore.update(ref, { actions: [], scriptPublication: NO_SCRIPT_PUBLICATION }) + : config.sessionStore.requireCurrent(ref); + } const created = makeIosSession(config.sessionName); - config.sessionStore.set(config.sessionName, created); + config.sessionStore.publish(config.sessionName, created); return created; } diff --git a/src/daemon/__tests__/replay-runtime/application.test.ts b/src/daemon/__tests__/replay-runtime/application.test.ts index d72e748a3d..9dd0b598e6 100644 --- a/src/daemon/__tests__/replay-runtime/application.test.ts +++ b/src/daemon/__tests__/replay-runtime/application.test.ts @@ -102,7 +102,7 @@ test('replay routes native and Maestro sources through one application seam and for (const replayCase of cases) { const root = mkdtempForTestSync(`agent-device-replay-application-${replayCase.kind}-`); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const req = await replayCase.buildRequest(root); const invoked: DaemonRequest[] = []; @@ -130,7 +130,7 @@ test('replay routes native and Maestro sources through one application seam and test('replay admits an annotated target before dispatching its mutation', async () => { const root = mkdtempForTestSync('agent-device-replay-application-admission-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const filePath = writeReplayFile(root, [ '# agent-device:target-v1 {"id":"save","role":"button","label":"Save","ancestry":[],"sibling":0,"viewportOrder":0,"verification":"verified"}', 'click label="Save"', @@ -168,7 +168,7 @@ test('replay admits an annotated target before dispatching its mutation', async test('replay projects the exact failure cause and retains artifacts from the failed step', async () => { const root = mkdtempForTestSync('agent-device-replay-application-failure-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const artifactPath = path.join(root, 'failure.png'); fs.writeFileSync(artifactPath, 'artifact'); const filePath = writeReplayFile(root, ['open "Demo"']); diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts index 6417a461dd..c5f8e9045e 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts @@ -31,7 +31,10 @@ test('a mid-loop throw reports exactly the artifacts of the steps that ran befor const root = mkdtempForTestSync('agent-device-replay-artifact-ledger-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); // Two real files, so `collectReplayActionArtifactPaths`' existence check // keeps them (it drops any candidate path that is not a file on disk). @@ -87,7 +90,10 @@ test('a completed run reports every step’s artifacts through the threaded ledg const root = mkdtempForTestSync('agent-device-replay-artifact-ledger-ok-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const firstArtifact = path.join(root, 'step-1.png'); const secondArtifact = path.join(root, 'step-2.png'); diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-binding.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-binding.test.ts index 9e1273e9f9..e87d9667d7 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-binding.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-binding.test.ts @@ -81,7 +81,7 @@ test('typed Maestro keeps a port-only runtime digest stable after launch binds t metroPort: 8083, }); if (!request.runtime) throw new Error('open must carry effective runtime hints'); - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); sessionStore.setRuntimeHints(sessionName, request.runtime); return { ok: true, data: {} }; } diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts index 46a45168ec..c08638c2b8 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts @@ -37,7 +37,7 @@ test('divergence cause and action strings pass through the central redactor at c const root = mkdtempForTestSync('agent-device-replay-divergence-redact-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['click "Save"']); mockDispatchCommand.mockRejectedValue(new Error('no device runner available')); @@ -71,7 +71,10 @@ test('a fill divergence never serializes the typed text at any response level', const root = mkdtempForTestSync('agent-device-replay-fill-leak-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, [`fill 'label="Email"' ${JSON.stringify(sentinel)}`]); // Selector miss forces the divergence on the fill step; the failure // message is a realistic selector error, not an echo of the typed text. @@ -125,7 +128,10 @@ test('a capture-failed screen hint redacts a secret in the capture error', async const root = mkdtempForTestSync('agent-device-replay-screen-redact-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['click "Save"']); // The post-failure snapshot capture throws with a secret-bearing message. mockDispatchCommand.mockRejectedValue(new Error('snapshot failed: api_key=sk-live-abc123def456')); @@ -159,7 +165,10 @@ test('an expanded ${VAR} value echoed by a selector error never reaches the publ const root = mkdtempForTestSync('agent-device-replay-var-leak-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['press label="${SECRET}"']); mockDispatchCommand.mockRejectedValue(new Error('no device runner available')); @@ -203,7 +212,10 @@ test('an expanded built-in AD_DEVICE_ID never reaches the public divergence', as const sessionName = 'static-session-context'; const root = mkdtempForTestSync('agent-device-replay-builtin-var-leak-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['press label="${AD_DEVICE_ID}"']); mockDispatchCommand.mockRejectedValue(new Error('no device runner available')); diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts index d5333ef699..a8f83bc810 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts @@ -40,7 +40,10 @@ test('a failing replay step returns REPLAY_DIVERGENCE with cause preserved and c const root = mkdtempForTestSync('agent-device-replay-divergence-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); // The post-failure screen digest capture (and the suggestions re-resolution @@ -166,7 +169,10 @@ test('a normalized nested failure preserves typed recovery signals on REPLAY_DIV const root = mkdtempForTestSync('agent-device-replay-recovery-signals-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['click "Save"']); mockDispatchCommand.mockRejectedValue(new Error('no device runner available')); @@ -201,7 +207,10 @@ test('a capture the runner declared sparse reaches the divergence details as a v const root = mkdtempForTestSync('agent-device-replay-sparse-verdict-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['click "Not Now"']); mockDispatchCommand.mockRejectedValue(new Error('no device runner available')); @@ -243,7 +252,10 @@ test('a failing replay step captures an available screen digest with blessed ref const root = mkdtempForTestSync('agent-device-replay-divergence-screen-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['click "Save"']); mockDispatchCommand.mockResolvedValue({ @@ -290,7 +302,10 @@ test('a failing replay step ranks a re-resolved suggestion when the recorded sel const root = mkdtempForTestSync('agent-device-replay-divergence-suggest-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); // The recorded selector is label-only, so it still structurally matches a // node in the fresh capture even though the underlying tap failed (e.g. the // node moved off-screen or was momentarily not hittable) — the exact class @@ -387,7 +402,7 @@ test('a failure inside a retry-wrapped runFlow include reports the include file const root = mkdtempForTestSync('agent-device-replay-retry-provenance-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const childPath = writeMaestroInclude(root); const mainPath = path.join(root, 'main.yaml'); fs.writeFileSync( @@ -444,7 +459,7 @@ test('a failure inside a runtime runFlow.when-wrapped include reports the includ const root = mkdtempForTestSync('agent-device-replay-when-provenance-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const childPath = writeMaestroInclude(root); const mainPath = path.join(root, 'main.yaml'); fs.writeFileSync( @@ -500,7 +515,7 @@ test('typed Maestro failures rank suggestions with Maestro regex selector semant const root = mkdtempForTestSync('agent-device-maestro-suggestion-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync( flowPath, @@ -556,7 +571,7 @@ test('typed Maestro failures never serialize input text', async () => { const root = mkdtempForTestSync('agent-device-maestro-text-redaction-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); const typedText = 'highly-sensitive-value'; fs.writeFileSync( diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts index 8de8149e7a..8ae4ab2f4b 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts @@ -53,7 +53,7 @@ test('--keep-session suppresses a close that is terminal among executable action const root = mkdtempForTestSync('agent-device-replay-keep-marker-tail-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'close', 'replay "./nested-flow.ad"']); const commands: string[] = []; @@ -64,7 +64,7 @@ test('--keep-session suppresses a close that is terminal among executable action sessionStore, invoke: async (req) => { commands.push(req.command); - if (req.command === 'close') sessionStore.delete(sessionName); + if (req.command === 'close') sessionStore.retire(sessionStore.lookup(sessionName)!); return { ok: true, data: {} }; }, }); @@ -79,7 +79,7 @@ test('--keep-session fails explicitly when the completed replay has no live sess const root = mkdtempForTestSync('agent-device-replay-keep-postcondition-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Log out"']); const response = await runReplayForTest({ @@ -88,7 +88,7 @@ test('--keep-session fails explicitly when the completed replay has no live sess logPath: path.join(root, 'daemon.log'), sessionStore, invoke: async (req) => { - if (req.command === 'click') sessionStore.delete(sessionName); + if (req.command === 'click') sessionStore.retire(sessionStore.lookup(sessionName)!); return { ok: true, data: {} }; }, }); @@ -106,7 +106,7 @@ test('--keep-session suppresses only the authored terminal close and reports the const root = mkdtempForTestSync('agent-device-replay-keep-session-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"', 'close']); const commands: string[] = []; @@ -117,7 +117,7 @@ test('--keep-session suppresses only the authored terminal close and reports the sessionStore, invoke: async (req) => { commands.push(req.command); - if (req.command === 'close') sessionStore.delete(sessionName); + if (req.command === 'close') sessionStore.retire(sessionStore.lookup(sessionName)!); return { ok: true, data: {} }; }, }); @@ -133,7 +133,7 @@ test('--keep-session preserves an interior close instead of broad command filter const root = mkdtempForTestSync('agent-device-replay-keep-interior-close-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'close', 'open "Next"']); const commands: string[] = []; @@ -156,7 +156,7 @@ test('--keep-session is a no-op for an already close-less script', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-close-less-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const invoke = vi.fn(async (_req: DaemonRequest) => ({ ok: true as const, data: {} })); @@ -178,7 +178,7 @@ test('--keep-session rejects Maestro YAML before engine dispatch', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-maestro-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = path.join(root, 'flow.yaml'); fs.writeFileSync(filePath, ['appId: com.example.app', '---', '- launchApp'].join('\n')); const invoke = vi.fn(async () => ({ ok: true as const, data: {} })); diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-plan.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-plan.test.ts index 9ae7567682..301c8794b3 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-plan.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-plan.test.ts @@ -47,7 +47,7 @@ test('resume skips steps 1..from-1 without invoking them and executes only from const root = mkdtempForTestSync('agent-device-replay-resume-skip-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [ 'open "Demo"', 'click label="Continue"', @@ -106,7 +106,7 @@ test('resume requires both --from and --plan-digest together', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-pair-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const fromOnly = await runReplayForTest({ @@ -138,7 +138,7 @@ test('resume rejects an out-of-range --from before any action', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-range-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const response = await runReplayForTest({ @@ -173,7 +173,7 @@ test("a rejected --from/--plan-digest resume never reaches prepareReplaySession' const root = mkdtempForTestSync('agent-device-replay-resume-no-mutate-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Continue"', 'click "Save"']); // Arm a repair transaction and stamp a corrective-resume watermark @@ -189,7 +189,6 @@ test("a rejected --from/--plan-digest resume never reaches prepareReplaySession' // from firing first (it needs `sessionActionsLength` to equal this), so // the rejection below is provably the plan-digest check, not a different one. armedSession.pendingRecordAndHeal = { expectedFrom: 2, actionsCountAtDivergence: 999 }; - sessionStore.set(sessionName, armedSession); const beforeView = coordinator.view(); const beforeActionsLength = sessionStore.get(sessionName)!.actions.length; @@ -223,7 +222,7 @@ test('resume rejects a stale --plan-digest after the script changed', async () = const root = mkdtempForTestSync('agent-device-replay-resume-stale-digest-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const firstAttempt = await runReplayForTest({ @@ -270,7 +269,7 @@ test('resume rejects a digest from a different effective platform or target befo const root = mkdtempForTestSync('agent-device-replay-resume-effective-target-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [ 'context platform=android target=tv', 'open "Demo"', @@ -326,7 +325,7 @@ test('resume rejects resuming past a retry-wrapped step in the skipped range', a const root = mkdtempForTestSync('agent-device-replay-resume-control-flow-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const mainPath = path.join(root, 'main.yaml'); fs.writeFileSync( mainPath, @@ -396,7 +395,7 @@ test('typed Maestro resume digest binds an inferred session target', async () => const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const session = makeIosSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.app\n---\n- back\n'); @@ -416,10 +415,9 @@ test('typed Maestro resume digest binds an inferred session target', async () => resume: { from: number; planDigest: string }; }; - sessionStore.set(sessionName, { - ...session, - device: { ...session.device, target: 'tv' }, - }); + sessionStore.update(sessionStore.lookup(sessionName)!, (current) => ({ + device: { ...current.device, target: 'tv' }, + })); const resumedAttempt = await runReplayForTest({ req: baseReq({ positionals: [flowPath], @@ -447,7 +445,7 @@ test('typed Maestro rejects selectors that conflict with an active session', asy const root = mkdtempForTestSync('agent-device-maestro-session-conflict-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.app\n---\n- back\n'); const invoke = vi.fn(async () => ({ ok: true as const, data: {} })); @@ -573,7 +571,7 @@ test('typed Maestro resume digest binds effective stored runtime hints', async ( const root = mkdtempForTestSync('agent-device-maestro-session-runtime-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); sessionStore.setRuntimeHints(sessionName, { platform: 'ios', metroHost: '127.0.0.1', diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts index d7d47319c0..32300cd8ed 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts @@ -52,6 +52,6 @@ export function seedReplayFixtureSession( sessionStore: SessionStore, platform: 'android' | 'ios' | undefined, ): void { - if (platform === 'android') sessionStore.set('s', makeAndroidSession('s')); - if (platform === 'ios') sessionStore.set('s', makeIosSession('s')); + if (platform === 'android') sessionStore.publish('s', makeAndroidSession('s')); + if (platform === 'ios') sessionStore.publish('s', makeIosSession('s')); } diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime.test.ts index 15f5460ac2..bdaced7b33 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime.test.ts @@ -43,7 +43,7 @@ test('a successful replay prints one line with the step count and wall time', as const root = mkdtempForTestSync('agent-device-replay-success-message-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const response = await runReplayForTest({ @@ -72,7 +72,7 @@ test('a close-less replay reports sessionActive: true (real producer, session st const root = mkdtempForTestSync('agent-device-replay-session-active-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const response = await runReplayForTest({ @@ -93,7 +93,7 @@ test('a replay whose terminal close removes the session reports sessionActive: f const root = mkdtempForTestSync('agent-device-replay-session-closed-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'close']); const response = await runReplayForTest({ @@ -105,7 +105,7 @@ test('a replay whose terminal close removes the session reports sessionActive: f // removing the session from the store. Every other command is a no-op, // same as the rest of this file's `invoke` stubs. invoke: async (req) => { - if (req.command === 'close') sessionStore.delete(sessionName); + if (req.command === 'close') sessionStore.retire(sessionStore.lookup(sessionName)!); return { ok: true, data: {} }; }, }); @@ -120,7 +120,7 @@ test('Maestro YAML uses the typed engine while .ad remains generic', async () => const root = mkdtempForTestSync('agent-device-typed-maestro-route-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); fs.writeFileSync( yamlPath, @@ -169,7 +169,7 @@ test('bare Maestro YAML requires explicit --maestro routing', async () => { const root = mkdtempForTestSync('agent-device-maestro-explicit-route-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); fs.writeFileSync(yamlPath, 'appId: com.example.app\n---\n- launchApp\n'); @@ -194,7 +194,7 @@ test('ADR 0016 / #1384: a Maestro replay reports sessionActive: true (real produ const root = mkdtempForTestSync('agent-device-maestro-session-active-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); fs.writeFileSync(yamlPath, 'appId: com.example.app\n---\n- launchApp\n'); @@ -220,7 +220,7 @@ test('typed Maestro nested commands receive the runtime hints bound into the pla const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const session = makeIosSession(sessionName); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...session, device: { ...session.device, simulatorSetPath: '/tmp/custom-simulator-set' }, }); @@ -269,7 +269,7 @@ test('typed Maestro writes source-aware redacted step timing traces', async () = const root = mkdtempForTestSync('agent-device-maestro-step-trace-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); const tracePath = path.join(root, 'replay-timing.ndjson'); fs.writeFileSync(yamlPath, 'appId: com.example.app\n---\n- inputText: highly-sensitive\n'); @@ -329,7 +329,7 @@ test('replay trace failures do not change action semantics', async () => { const root = mkdtempForTestSync('agent-device-replay-trace-failure-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); fs.writeFileSync(yamlPath, 'appId: com.example.app\n---\n- back\n'); @@ -352,7 +352,7 @@ test('generic replay traces redact typed text', async () => { const root = mkdtempForTestSync('agent-device-replay-trace-redaction-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const secret = 'highly-sensitive-value'; const filePath = writeReplayFile(root, [`type "${secret}"`]); const tracePath = path.join(root, 'replay-timing.ndjson'); @@ -378,7 +378,7 @@ test('Maestro YAML rejects .ad repair recording before executing any command', a const root = mkdtempForTestSync('agent-device-typed-maestro-save-script-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); fs.writeFileSync(yamlPath, 'appId: com.example.app\n---\n- launchApp\n'); const invoke = vi.fn(async () => ({ ok: true as const, data: {} })); @@ -407,7 +407,7 @@ test('Maestro YAML cannot append commands to an active .ad repair session', asyn const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const session = makeRepairArmedSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const yamlPath = path.join(root, 'flow.yaml'); fs.writeFileSync(yamlPath, 'appId: com.example.app\n---\n- launchApp\n'); const invoke = vi.fn(async () => ({ ok: true as const, data: {} })); @@ -433,7 +433,7 @@ test('replay rejects legacy JSON payload files', async () => { const root = mkdtempForTestSync('agent-device-replay-json-rejected-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = path.join(root, 'replay.json'); fs.writeFileSync(filePath, JSON.stringify({ optimizedActions: [] }, null, 2)); @@ -466,7 +466,7 @@ test('replay rejects an unknown --replay-backend value before any step dispatch const root = mkdtempForTestSync('agent-device-replay-unknown-backend-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const invoke = vi.fn(async () => ({ ok: true as const, data: {} })); @@ -495,7 +495,7 @@ test('replay still dispatches a plain .ad script with replayBackend: "maestro"', const root = mkdtempForTestSync('agent-device-replay-ad-maestro-backend-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const invoke = vi.fn(async () => ({ ok: true as const, data: {} })); @@ -517,7 +517,7 @@ test('replay rejects malformed .ad lines with unclosed quotes', async () => { const root = mkdtempForTestSync('agent-device-replay-invalid-ad-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [String.raw`click "id=\"broken\"`]); const response = await runReplayForTest({ @@ -540,7 +540,10 @@ test('--update never rewrites the .ad file, even when a re-resolvable suggestion const root = mkdtempForTestSync('agent-device-replay-update-no-write-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const filePath = writeReplayFile(root, ['click label="Save"']); const before = fs.readFileSync(filePath, 'utf8'); const statBefore = fs.statSync(filePath); @@ -590,7 +593,7 @@ test('a successful --update replay reports healed: 0 (heal is retired, not just const root = mkdtempForTestSync('agent-device-replay-update-healed-zero-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); const response = await runReplayForTest({ @@ -611,7 +614,7 @@ test('--update no longer refuses env directives (the guard existed only for rewr const root = mkdtempForTestSync('agent-device-replay-update-env-ok-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['env NAME=World', 'open "Demo"']); const response = await runReplayForTest({ @@ -629,7 +632,7 @@ test('--update no longer refuses ${VAR} interpolation (the guard existed only fo const root = mkdtempForTestSync('agent-device-replay-update-interp-ok-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['click label="${NAME}"']); const response = await runReplayForTest({ diff --git a/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts index 41aa965eef..555fc33bcf 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts @@ -45,7 +45,7 @@ beforeEach(() => { test('a bundled replay runs after the script file is gone from the daemon host', async () => { const root = mkdtempForTestSync('agent-device-replay-source-bundle-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const scriptPath = path.join(root, 'flow.ad'); fs.writeFileSync(scriptPath, 'open "Demo"\nclick "Save"\n'); const bundle = replayScriptSourceBundleFor(scriptPath); @@ -75,7 +75,7 @@ test('a bundled replay runs after the script file is gone from the daemon host', test('a bundled Maestro replay resolves runFlow includes from the bundle, not the daemon disk', async () => { const root = mkdtempForTestSync('agent-device-replay-maestro-bundle-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.app\n---\n- runFlow: included.yaml\n'); fs.writeFileSync(path.join(root, 'included.yaml'), '---\n- back\n'); @@ -107,7 +107,7 @@ test('a bundled Maestro replay resolves runFlow includes from the bundle, not th test('a Maestro include missing from the bundle fails naming the include path', async () => { const root = mkdtempForTestSync('agent-device-replay-maestro-missing-include-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const flowPath = path.join(root, 'flow.yaml'); const flow = 'appId: com.example.app\n---\n- runFlow: absent.yaml\n'; fs.writeFileSync(flowPath, flow); @@ -141,7 +141,7 @@ test('a Maestro include missing from the bundle fails naming the include path', test('a request carrying only a path is refused with the client-must-send-sources message', async () => { const root = mkdtempForTestSync('agent-device-replay-path-only-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const scriptPath = path.join(root, 'flow.ad'); fs.writeFileSync(scriptPath, 'open "Demo"\n'); @@ -171,7 +171,7 @@ test('a request carrying only a path is refused with the client-must-send-source test('a failing step reports the caller-resolved script path and line', async () => { const root = mkdtempForTestSync('agent-device-replay-source-display-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const scriptPath = path.join(root, 'flow.ad'); fs.writeFileSync(scriptPath, 'open "Demo"\nclick "Nope"\n'); diff --git a/src/daemon/__tests__/replay-suite/session-test-runner.test.ts b/src/daemon/__tests__/replay-suite/session-test-runner.test.ts index 6275640088..a8630547f2 100644 --- a/src/daemon/__tests__/replay-suite/session-test-runner.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-runner.test.ts @@ -31,7 +31,7 @@ vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) test('session_list includes device_udid and ios_simulator_device_set for iOS sessions', async () => { const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( 'ios-scoped', makeSession('ios-scoped', { platform: 'apple', @@ -42,7 +42,7 @@ test('session_list includes device_udid and ios_simulator_device_set for iOS ses simulatorSetPath: '/tmp/tenant-a/simulators', }), ); - sessionStore.set( + sessionStore.publish( 'android-1', makeSession('android-1', { platform: 'android', @@ -52,7 +52,7 @@ test('session_list includes device_udid and ios_simulator_device_set for iOS ses booted: true, }), ); - sessionStore.set( + sessionStore.publish( 'macos-1', makeSession('macos-1', { platform: 'apple', @@ -220,7 +220,7 @@ test('test cleans up suite-owned sessions after each executed script', async () logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), sessionStore, invoke: async (req) => { - sessionStore.set( + sessionStore.publish( req.session, makeSession(req.session, { platform: 'android', diff --git a/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts index da008d36ab..2b68cd939a 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts @@ -136,7 +136,6 @@ function startMockRecording(params: { metadata: { phase: 'active' }, }), }; - sessionStore.set(req.session, session); } return { ok: true, data: { recording: 'started', outPath: state.recordingPath } }; } @@ -152,7 +151,6 @@ async function stopMockRecording(params: { if (session) { await session.screenRecording?.handle.finish(); session.screenRecording = undefined; - sessionStore.set(req.session, session); state.liveSlotCleared = sessionStore.get(req.session)?.screenRecording === undefined; } fs.writeFileSync(state.recordingPath, 'video'); @@ -290,7 +288,7 @@ test('test finalizes replay video exactly once when cancellation arrives after s nestedRequests.push(nestedReq); if (nestedReq.command === 'open') { const provisionalSession = makeIosSession(nestedReq.session); - sessionStore.set(nestedReq.session, provisionalSession); + sessionStore.publish(nestedReq.session, provisionalSession); const hookResponse = await nestedReq.internal?.openLifecycle?.beforeDispatch?.(); if (hookResponse && !hookResponse.ok) return hookResponse; events.push('open:dispatch'); diff --git a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts index 13438a94ef..443cda9c61 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts @@ -354,7 +354,7 @@ test('test stops before retrying when a rejected close leaves the prior macOS se }, invoke: async (req) => { replayAttempts += 1; - sessionStore.set(req.session, makeMacOsSession(req.session)); + sessionStore.publish(req.session, makeMacOsSession(req.session)); return { ok: false, error: { code: 'COMMAND_FAILED', message: 'open "System Settings" failed' }, @@ -497,7 +497,7 @@ test('test aggregates snapshot diagnostics from replay session samples', async ( backend: 'android', platform: 'android', }); - sessionStore.set(req.session, session); + sessionStore.publish(req.session, session); return { ok: true, data: { replayed: 1, healed: 0 } }; }, }); @@ -554,7 +554,7 @@ test('test aggregates snapshot diagnostics from failed replay session samples', backend: 'android', platform: 'android', }); - sessionStore.set(req.session, session); + sessionStore.publish(req.session, session); return { ok: false, error: { code: 'COMMAND_FAILED', message: 'open failed' }, diff --git a/src/daemon/__tests__/replay-target/session-replay-dispatch-selector-miss.test.ts b/src/daemon/__tests__/replay-target/session-replay-dispatch-selector-miss.test.ts index fa550aaa33..2fee6c82cc 100644 --- a/src/daemon/__tests__/replay-target/session-replay-dispatch-selector-miss.test.ts +++ b/src/daemon/__tests__/replay-target/session-replay-dispatch-selector-miss.test.ts @@ -64,7 +64,10 @@ beforeEach(() => { function setupSession(root: string): { sessionStore: SessionStore; sessionName: string } { const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); return { sessionStore, sessionName }; } diff --git a/src/daemon/__tests__/replay-target/session-replay-scenario.fixtures.ts b/src/daemon/__tests__/replay-target/session-replay-scenario.fixtures.ts index f129fc90fb..91d4f070ab 100644 --- a/src/daemon/__tests__/replay-target/session-replay-scenario.fixtures.ts +++ b/src/daemon/__tests__/replay-target/session-replay-scenario.fixtures.ts @@ -22,7 +22,7 @@ function iosReplayScene(prefix: string): ReplaySessionScene { const root = mkdtempForTestSync(prefix); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosAppSession(sessionName)); + sessionStore.publish(sessionName, makeIosAppSession(sessionName)); return { root, sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; } diff --git a/src/daemon/__tests__/replay-target/session-replay-selector-routes.test.ts b/src/daemon/__tests__/replay-target/session-replay-selector-routes.test.ts index 1290acc40b..a30e64b37c 100644 --- a/src/daemon/__tests__/replay-target/session-replay-selector-routes.test.ts +++ b/src/daemon/__tests__/replay-target/session-replay-selector-routes.test.ts @@ -39,7 +39,7 @@ test('replay executes selector reads before reporting a covered-target divergenc const root = mkdtempForTestSync('agent-device-replay-selector-routes-'); const sessionName = 'default'; const sessionStore = new SessionStore(path.join(root, 'sessions')); - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [ 'open "Demo"', String.raw`get text "id=\"field-name\""`, diff --git a/src/daemon/__tests__/request-dispatch-disclosure.test.ts b/src/daemon/__tests__/request-dispatch-disclosure.test.ts index 9ecc449a4b..24a5233d3a 100644 --- a/src/daemon/__tests__/request-dispatch-disclosure.test.ts +++ b/src/daemon/__tests__/request-dispatch-disclosure.test.ts @@ -122,7 +122,7 @@ async function route( androidObservation: AndroidObservationAdapter = clearAndroidObservationFixture, ) { const sessionStore = makeSessionStore('agent-device-dispatch-route-'); - sessionStore.set(SESSION, session); + sessionStore.publish(SESSION, session); const handler = createRequestHandler({ logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), token: 'test-token', diff --git a/src/daemon/__tests__/request-execution-scope-lease-expiry.test.ts b/src/daemon/__tests__/request-execution-scope-lease-expiry.test.ts index 16d48f801d..744997ecfe 100644 --- a/src/daemon/__tests__/request-execution-scope-lease-expiry.test.ts +++ b/src/daemon/__tests__/request-execution-scope-lease-expiry.test.ts @@ -27,7 +27,7 @@ test('expired leases remove owned sessions before the next command and free capa now: () => now, }); const lease = leaseRegistry.allocateLease({ tenantId: 'tenant-a', runId: 'run-1' }); - sessionStore.set( + sessionStore.publish( 'default', makeSession('default', { device: LINUX_DEVICE, diff --git a/src/daemon/__tests__/request-execution-scope-open-device-wait.test.ts b/src/daemon/__tests__/request-execution-scope-open-device-wait.test.ts index c623e1f707..b533eedfd2 100644 --- a/src/daemon/__tests__/request-execution-scope-open-device-wait.test.ts +++ b/src/daemon/__tests__/request-execution-scope-open-device-wait.test.ts @@ -91,6 +91,7 @@ function openOnUncontendedDevice( name: string, sessionStore: ReturnType, order: string[], + releaseAfterMs?: number, ): Promise { return scope.runLocked(async () => { const currentOwner = sessionStore.findByDevice(CONTESTED_DEVICE.id); @@ -98,7 +99,8 @@ function openOnUncontendedDevice( return `refused:${currentOwner.address}`; } - sessionStore.set(name, sessionOnDevice(name)); + const ref = sessionStore.publish(name, sessionOnDevice(name)); + if (releaseAfterMs !== undefined) setTimeout(() => sessionStore.retire(ref), releaseAfterMs); order.push(name); return `opened:${name}`; }); @@ -107,11 +109,10 @@ function openOnUncontendedDevice( test('an open that lost the race to a free device re-waits and opens rather than refusing', async () => { const sessionStore = makeSessionStore('agent-device-open-wait-race-'); const leaseRegistry = new LeaseRegistry(); - sessionStore.set('holder', sessionOnDevice('holder')); + const holder = sessionStore.publish('holder', sessionOnDevice('holder')); // The holder lets go shortly after; the first open to bind the device lets go again after that, // the way any session closed by its owner would. - setTimeout(() => sessionStore.delete('holder'), 50); - setTimeout(() => sessionStore.delete('first-opener'), 400); + setTimeout(() => sessionStore.retire(holder), 50); const first = await createRequestExecutionScope({ req: openRequest('first-opener'), @@ -128,7 +129,7 @@ test('an open that lost the race to a free device re-waits and opens rather than const startedAtMs = Date.now(); // Each open binds a device to its own session under its own device lock, which is what a real // open does. The second can therefore only get in once the first releases it. - const firstOpened = openOnUncontendedDevice(first, 'first-opener', sessionStore, order); + const firstOpened = openOnUncontendedDevice(first, 'first-opener', sessionStore, order, 350); const secondOpened = openOnUncontendedDevice(second, 'second-opener', sessionStore, order); await expect(firstOpened).resolves.toBe('opened:first-opener'); @@ -147,7 +148,7 @@ test('an open that lost the race to a free device re-waits and opens rather than test('a close that frees the device mid-wait gets through while the open is waiting', async () => { const sessionStore = makeSessionStore('agent-device-open-wait-close-'); const leaseRegistry = new LeaseRegistry(); - sessionStore.set('holder', sessionOnDevice('holder')); + const holder = sessionStore.publish('holder', sessionOnDevice('holder')); const order: string[] = []; const opened = createRequestExecutionScope({ @@ -157,7 +158,7 @@ test('a close that frees the device mid-wait gets through while the open is wait }).then((scope) => scope.runLocked(async () => { order.push('open-bound'); - sessionStore.set('waiter', sessionOnDevice('waiter')); + sessionStore.publish('waiter', sessionOnDevice('waiter')); return 'opened'; }), ); @@ -172,7 +173,7 @@ test('a close that frees the device mid-wait gets through while the open is wait await expect( closer.runLocked(async () => { order.push('close-freed-the-device'); - sessionStore.delete('holder'); + sessionStore.retire(holder); return 'closed'; }), ).resolves.toBe('closed'); diff --git a/src/daemon/__tests__/request-execution-scope.test.ts b/src/daemon/__tests__/request-execution-scope.test.ts index a4f0ce5a32..8c6fe6c221 100644 --- a/src/daemon/__tests__/request-execution-scope.test.ts +++ b/src/daemon/__tests__/request-execution-scope.test.ts @@ -194,7 +194,7 @@ test('leased session admission uses stored lease metadata and heartbeats', async clientId: 'client-a', deviceKey: 'ios:sim-1', }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -237,7 +237,7 @@ test('leased session heartbeat is serialized with the request execution lock', a clientId: 'client-a', deviceKey: 'ios:sim-1', }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -293,7 +293,7 @@ test('leased session heartbeat is serialized with the request execution lock', a test('a later external command cannot interleave with replay observation finalization', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const leaseRegistry = new LeaseRegistry(); const replay = await createRequestExecutionScope({ req: makeRequest({ command: 'replay' }), @@ -350,7 +350,7 @@ test('a fresh replay keeps its session lock after a nested open binds the device const replayRun = replay.runLocked( async () => await new Promise((resolve) => { - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); finishReplay = resolve; sessionOpened(); }), @@ -379,7 +379,7 @@ test('leased session rejects mismatched lease id before dispatch', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); const leaseRegistry = new LeaseRegistry(); const lease = leaseRegistry.allocateLease({ tenantId: 'tenant-a', runId: 'run-1' }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -410,7 +410,7 @@ test.each([ const sessionStore = makeSessionStore('agent-device-request-scope-'); const leaseRegistry = new LeaseRegistry(); const lease = leaseRegistry.allocateLease({ tenantId: 'tenant-a', runId: 'run-1' }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -438,7 +438,7 @@ test.each([ test('local unleased session admission still succeeds', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const scope = await createRequestExecutionScope({ req: makeRequest({ command: 'snapshot' }), @@ -451,7 +451,7 @@ test('local unleased session admission still succeeds', async () => { test('local unleased session ignores stale lease id without tenant scope', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const scope = await createRequestExecutionScope({ req: makeRequest({ command: 'snapshot', @@ -473,7 +473,7 @@ test('provider lease admission succeeds without a device key', async () => { leaseBackend: 'android-instance', leaseProvider: 'limrun', }); - sessionStore.set( + sessionStore.publish( 'default', makeAndroidSession('default', { lease: { @@ -509,7 +509,7 @@ test('an admitted request that outlives the lease TTL keeps its lease and sessio now: () => now, }); const lease = leaseRegistry.allocateLease({ tenantId: 'tenant-a', runId: 'run-1' }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -550,7 +550,7 @@ test('expired leased session cleanup waits for the request execution lock', asyn now: () => now, }); const lease = leaseRegistry.allocateLease({ tenantId: 'tenant-a', runId: 'run-1' }); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { lease: { @@ -641,7 +641,7 @@ test('tenant lease rejection flushes diagnostics into the effective session requ test('prepareLockedRequestScope preserves existing-session selector validation', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('default', makeAndroidSession('default')); + sessionStore.publish('default', makeAndroidSession('default')); const scope = await createRequestExecutionScope({ req: makeRequest({ command: 'snapshot', @@ -672,7 +672,7 @@ test('prepareLockedRequestScope blocks commands for invalidated recordings befor showTouches: true, invalidatedReason: 'iOS runner session restarted during recording', }); - sessionStore.set('default', session); + sessionStore.publish('default', session); const scope = await createRequestExecutionScope({ req: makeRequest({ command: 'snapshot' }), sessionStore, @@ -700,7 +700,7 @@ test('prepareLockedRequestScope blocks commands for invalidated recordings befor test('prepareLockedRequestScope passes the session runner log path into handler context', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const scope = await createRequestExecutionScope({ req: makeRequest({ command: 'snapshot' }), sessionStore, @@ -723,7 +723,7 @@ test('prepareLockedRequestScope passes the session runner log path into handler test('prepareLockedRequestScope streams ordinary diagnostics into the active trace', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); const tracePath = path.join(TEST_ROOT, 'active-session.trace'); - sessionStore.set( + sessionStore.publish( 'default', makeIosSession('default', { trace: { outPath: tracePath, startedAt: Date.now() }, @@ -767,7 +767,7 @@ test('runLocked rejects a canceled request before executing work', async () => { test('runLocked rejects a request canceled while waiting for its execution lock', async () => { const requestId = 'request-scope-canceled-after-lock'; const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); const leaseRegistry = new LeaseRegistry(); const first = await createRequestExecutionScope({ req: makeRequest({ command: 'click' }), @@ -867,7 +867,7 @@ test('router: deferred tenant connect still refuses an app-target close before d // run-scoped), so a missing lease field alone is not proof of ownership. test('router: an existing lease-less session under tenant isolation still refuses close', async () => { const sessionStore = makeSessionStore('agent-device-request-scope-'); - sessionStore.set('tenant-a:default', makeIosSession('tenant-a:default')); + sessionStore.publish('tenant-a:default', makeIosSession('tenant-a:default')); const leaseRegistry = new LeaseRegistry(); const scope = await createRequestExecutionScope({ @@ -906,11 +906,11 @@ async function createScopeAcrossTwoImplicitWorkspaceSessions(command: string) { const scope = resolveSessionScope({ ...makeRequest({ command }), meta: { cwd: root } }); if (scope.kind !== 'cwd') throw new Error('expected a cwd session scope'); const sessionStore = makeSessionStore('agent-device-request-scope-ambiguity-'); - sessionStore.set( + sessionStore.publish( `cwd:${scope.id}:ios`, makeIosSession('default', { sessionScope: { kind: 'cwd', id: scope.id } }), ); - sessionStore.set( + sessionStore.publish( `cwd:${scope.id}:android`, makeAndroidSession('default', { sessionScope: { kind: 'cwd', id: scope.id } }), ); diff --git a/src/daemon/__tests__/request-handler-chain-provider-scope.test.ts b/src/daemon/__tests__/request-handler-chain-provider-scope.test.ts index cbf61f271b..52cbdf6e35 100644 --- a/src/daemon/__tests__/request-handler-chain-provider-scope.test.ts +++ b/src/daemon/__tests__/request-handler-chain-provider-scope.test.ts @@ -37,7 +37,7 @@ function makeRequest(command: string, sessionName: string): DaemonRequest { function baseChainParams(sessionName: string) { const sessionStore = makeSessionStore('agent-device-provider-scope-'); - sessionStore.set(sessionName, makeIosSession(sessionName)); + sessionStore.publish(sessionName, makeIosSession(sessionName)); return { req: makeRequest(INTERNAL_COMMANDS.runtime, sessionName), sessionName, diff --git a/src/daemon/__tests__/request-handler-chain.test.ts b/src/daemon/__tests__/request-handler-chain.test.ts index c6ce269e2d..ee766f96b5 100644 --- a/src/daemon/__tests__/request-handler-chain.test.ts +++ b/src/daemon/__tests__/request-handler-chain.test.ts @@ -40,7 +40,7 @@ function makeRequest(command: string, positionals: string[] = []): DaemonRequest function makeChainParams(req: DaemonRequest) { const sessionStore = makeSessionStore('agent-device-request-chain-'); - sessionStore.set('chain-test', makeIosSession('chain-test')); + sessionStore.publish('chain-test', makeIosSession('chain-test')); return { req, sessionName: 'chain-test', @@ -244,7 +244,7 @@ test('swipe rejects repetition inputs that can monopolize the request', async () test('duration-less public coordinate swipe retains Linux drag behavior', async () => { const sessionStore = makeSessionStore('agent-device-linux-swipe-'); - sessionStore.set('linux-swipe', makeSession('linux-swipe', { device: LINUX_DEVICE })); + sessionStore.publish('linux-swipe', makeSession('linux-swipe', { device: LINUX_DEVICE })); const drags: number[][] = []; let captureCount = 0; const provider = await createLocalLinuxToolProvider({ diff --git a/src/daemon/__tests__/request-router-android-modal.test.ts b/src/daemon/__tests__/request-router-android-modal.test.ts index 4cd127c611..0320d46bc9 100644 --- a/src/daemon/__tests__/request-router-android-modal.test.ts +++ b/src/daemon/__tests__/request-router-android-modal.test.ts @@ -151,7 +151,7 @@ test('generic Android gesture commands dismiss blocking system dialogs during re gestureRuntimeSpies.scrollDirection.mockClear(); const sessionStore = makeSessionStore('agent-device-router-android-modal-'); - sessionStore.set('default', makeAndroidSession('default')); + sessionStore.publish('default', makeAndroidSession('default')); const { openAndroidApp } = await import('@agent-device/platform-android/mechanics'); @@ -202,7 +202,7 @@ test('generic Android gesture commands continue when recording dialog inspection }); const sessionStore = makeSessionStore('agent-device-router-android-modal-'); - sessionStore.set('default', makeAndroidSession('default')); + sessionStore.publish('default', makeAndroidSession('default')); const { openAndroidApp } = await import('@agent-device/platform-android/mechanics'); vi.mocked(openAndroidApp).mockClear(); @@ -254,7 +254,7 @@ test('generic Android gesture commands skip local dialog recovery for provider d const sessionStore = makeSessionStore('agent-device-router-android-modal-provider-'); const session = makeAndroidSession('default'); - sessionStore.set('default', session); + sessionStore.publish('default', session); const runtime: ProviderDeviceRuntime = { provider: 'webdriver-fake', diff --git a/src/daemon/__tests__/request-router-android-perf.test.ts b/src/daemon/__tests__/request-router-android-perf.test.ts index 4f9ec43836..21608bbb8e 100644 --- a/src/daemon/__tests__/request-router-android-perf.test.ts +++ b/src/daemon/__tests__/request-router-android-perf.test.ts @@ -15,7 +15,7 @@ import { function makeAndroidSessionStore(name: string): SessionStore { const sessionStore = new SessionStore(`/tmp/${name}`); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), device: { diff --git a/src/daemon/__tests__/request-router-android-snapshot-helper.test.ts b/src/daemon/__tests__/request-router-android-snapshot-helper.test.ts index e4f936007e..3bb4b23ce4 100644 --- a/src/daemon/__tests__/request-router-android-snapshot-helper.test.ts +++ b/src/daemon/__tests__/request-router-android-snapshot-helper.test.ts @@ -14,7 +14,7 @@ import { function makeAndroidSessionStore(name: string): SessionStore { const sessionStore = new SessionStore(`/tmp/${name}`); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), device: { diff --git a/src/daemon/__tests__/request-router-artifacts-web.test.ts b/src/daemon/__tests__/request-router-artifacts-web.test.ts index 14e74b3dc2..0923d10c49 100644 --- a/src/daemon/__tests__/request-router-artifacts-web.test.ts +++ b/src/daemon/__tests__/request-router-artifacts-web.test.ts @@ -19,7 +19,7 @@ import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts'; // device is web, the same round trip `request-router-events.test.ts` already proves for `events`. test('artifacts lists a daemon-tracked artifact produced during a web session', async () => { const sessionStore = makeSessionStore('agent-device-router-artifacts-web-'); - sessionStore.set('web-session', makeSession('web-session', { device: WEB_DESKTOP_DEVICE })); + sessionStore.publish('web-session', makeSession('web-session', { device: WEB_DESKTOP_DEVICE })); const artifactDir = mkdtempForTestSync('agent-device-router-artifacts-web-file-'); const artifactPath = path.join(artifactDir, 'web-smoke.png'); fs.writeFileSync(artifactPath, 'fixture-bytes'); diff --git a/src/daemon/__tests__/request-router-cost.test.ts b/src/daemon/__tests__/request-router-cost.test.ts index 8eeb772cf2..df934bbb85 100644 --- a/src/daemon/__tests__/request-router-cost.test.ts +++ b/src/daemon/__tests__/request-router-cost.test.ts @@ -85,7 +85,7 @@ beforeEach(() => { test('(a) flag-off identity: meta.includeCost absent === no meta at all, byte-identical and no cost', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('cost-session', makeIosSession('cost-session')); + sessionStore.publish('cost-session', makeIosSession('cost-session')); const respNoMeta = await handler(baseRequest()); const respMetaWithoutCost = await handler(baseRequest({ meta: {} })); @@ -107,7 +107,7 @@ test('(a) flag-off identity: meta.includeCost absent === no meta at all, byte-id test('(b) flag-on additive-only: cost block is the ONLY delta vs flag-off', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('cost-session', makeIosSession('cost-session')); + sessionStore.publish('cost-session', makeIosSession('cost-session')); const respFlagOff = await handler(baseRequest()); const respFlagOn = await handler(baseRequest({ meta: { includeCost: true } })); @@ -134,7 +134,7 @@ test('(b) flag-on additive-only: cost block is the ONLY delta vs flag-off', asyn test('(c) runnerRoundTrips counts real iOS-runner round-trip diagnostics in scope', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('cost-session', makeIosSession('cost-session')); + sessionStore.publish('cost-session', makeIosSession('cost-session')); // The bound operation runs inside the request's diagnostics scope, so emitting here is // equivalent to the runner-session emitting these phases per round-trip. @@ -157,7 +157,7 @@ test('(c) runnerRoundTrips counts real iOS-runner round-trip diagnostics in scop test('(c2) nodeCount reports the node-tree size whenever data carries a nodes array, additive-only', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('cost-session', makeIosSession('cost-session')); + sessionStore.publish('cost-session', makeIosSession('cost-session')); // The nodeCount read is command-agnostic: it triggers on any response.data that // carries a `nodes` array (in production only the snapshot node-tree commands @@ -191,7 +191,7 @@ test('(c2) nodeCount reports the node-tree size whenever data carries a nodes ar test('(d) error path: a failing request with includeCost:true produces NO cost', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('cost-session', makeIosSession('cost-session')); + sessionStore.publish('cost-session', makeIosSession('cost-session')); // Conflicting explicit selector under a reject lock policy fails before the bound execution. const failingRequest = baseRequest({ diff --git a/src/daemon/__tests__/request-router-events.test.ts b/src/daemon/__tests__/request-router-events.test.ts index 6db32722a2..1c4e7a8d32 100644 --- a/src/daemon/__tests__/request-router-events.test.ts +++ b/src/daemon/__tests__/request-router-events.test.ts @@ -180,7 +180,7 @@ test('events flushes pending event writes before reading', async () => { // every other platform. test('events reads the daemon-owned session timeline for a web-backed session', async () => { const sessionStore = makeSessionStore('agent-device-router-events-web-'); - sessionStore.set('web-session', makeSession('web-session', { device: WEB_DESKTOP_DEVICE })); + sessionStore.publish('web-session', makeSession('web-session', { device: WEB_DESKTOP_DEVICE })); sessionStore.recordEvent('web-session', { kind: 'action.recorded', command: 'click', @@ -218,7 +218,7 @@ test('events reads the daemon-owned session timeline for a web-backed session', test('request timeline records thrown request failures after scope creation', async () => { const sessionStore = makeSessionStore('agent-device-router-events-throws-'); - sessionStore.set('events-session', makeIosSession('events-session')); + sessionStore.publish('events-session', makeIosSession('events-session')); const handler = createRequestHandler({ logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), diff --git a/src/daemon/__tests__/request-router-lock-policy.test.ts b/src/daemon/__tests__/request-router-lock-policy.test.ts index 45da103c58..d1debf25b5 100644 --- a/src/daemon/__tests__/request-router-lock-policy.test.ts +++ b/src/daemon/__tests__/request-router-lock-policy.test.ts @@ -122,7 +122,7 @@ function installGatedDispatch(): { test('direct daemon requests cannot bypass reject lock policy for existing sessions', async () => { const sessionStore = makeSessionStore('agent-device-router-lock-'); - sessionStore.set('qa-ios', makeIosSession('qa-ios')); + sessionStore.publish('qa-ios', makeIosSession('qa-ios')); const handler = createRequestHandler({ logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), @@ -455,7 +455,7 @@ test('fresh named sessions reject incompatible selector combinations before bind test('batch steps cannot bypass reject lock policy on nested direct requests', async () => { const sessionStore = makeSessionStore('agent-device-router-lock-'); - sessionStore.set('qa-ios', makeIosSession('qa-ios')); + sessionStore.publish('qa-ios', makeIosSession('qa-ios')); const handler = createRequestHandler({ logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), @@ -499,7 +499,7 @@ test('batch steps cannot bypass reject lock policy on nested direct requests', a test('direct daemon requests apply strip lock policy for existing sessions before dispatch', async () => { const sessionStore = makeSessionStore('agent-device-router-lock-'); - sessionStore.set('qa-ios', makeIosSession('qa-ios')); + sessionStore.publish('qa-ios', makeIosSession('qa-ios')); systemRuntimeSpies.appSwitcher.mockClear(); const handler = createRequestHandler({ @@ -538,7 +538,7 @@ test('strip lock policy still refuses a request naming a different device, befor // The wrong-device footgun: `strip` used to delete --udid and run the command against the bound // session's device instead. A request that names another device must fail, not silently retarget. const sessionStore = makeSessionStore('agent-device-router-lock-'); - sessionStore.set('qa-ios', makeIosSession('qa-ios')); + sessionStore.publish('qa-ios', makeIosSession('qa-ios')); let dispatchCalls = 0; legacyDispatchCapture.mockImplementation(async () => { dispatchCalls += 1; @@ -575,7 +575,7 @@ test('strip lock policy still refuses a request naming a different device, befor test('batch preserves tenant-scoped session names across nested requests', async () => { const sessionStore = makeSessionStore('agent-device-router-lock-'); - sessionStore.set('tenant-a:default', makeIosSession('tenant-a:default')); + sessionStore.publish('tenant-a:default', makeIosSession('tenant-a:default')); const leaseRegistry = new LeaseRegistry(); const lease = leaseRegistry.allocateLease({ tenantId: 'tenant-a', diff --git a/src/daemon/__tests__/request-router-open.test.ts b/src/daemon/__tests__/request-router-open.test.ts index ec084f7c75..d182e86469 100644 --- a/src/daemon/__tests__/request-router-open.test.ts +++ b/src/daemon/__tests__/request-router-open.test.ts @@ -402,7 +402,7 @@ test('close releases the session lease', async () => { runId: 'run-1', clientId: 'client-a', }); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', device: makeIosDevice('SIM-CLOSE'), createdAt: Date.now(), @@ -432,7 +432,7 @@ test('close releases the session lease', async () => { test('close fails synchronously when root composition omits platform resource cleanup', async () => { const sessionStore = makeSessionStore('agent-device-router-open-'); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', device: makeIosDevice('SIM-CLOSE-MISSING-CLEANUP'), createdAt: Date.now(), @@ -475,7 +475,7 @@ test('close rejects a different client before cleanup', async () => { runId: 'run-1', clientId: 'client-a', }); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', device: makeIosDevice('SIM-CLOSE-CLIENT'), createdAt: Date.now(), diff --git a/src/daemon/__tests__/request-router-record-runtime-lock.test.ts b/src/daemon/__tests__/request-router-record-runtime-lock.test.ts index 3355a9594d..942ee36e1c 100644 --- a/src/daemon/__tests__/request-router-record-runtime-lock.test.ts +++ b/src/daemon/__tests__/request-router-record-runtime-lock.test.ts @@ -75,7 +75,7 @@ test.each([ async (device) => { const sessionName = `record-${device.platform}-unsupported`; const sessionStore = makeSessionStore(`request-router-record-${device.platform}-unsupported-`); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { name: sessionName, device, createdAt: 1, diff --git a/src/daemon/__tests__/request-router-recording-health.test.ts b/src/daemon/__tests__/request-router-recording-health.test.ts index 1b6be6aa8f..b46d1930f9 100644 --- a/src/daemon/__tests__/request-router-recording-health.test.ts +++ b/src/daemon/__tests__/request-router-recording-health.test.ts @@ -53,7 +53,7 @@ test('router blocks non-record commands when recording was invalidated', async ( startedAt: Date.now() - 1_000, invalidatedReason: 'iOS runner session restarted during recording', }); - sessionStore.set('default', session); + sessionStore.publish('default', session); const handler = createRequestHandler({ logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), @@ -104,7 +104,7 @@ test('router allows canonical iOS simulator gestures during overlay recording af startedAt: Date.now() - 1_000, runnerSessionId: 'runner-before', }); - sessionStore.set('default', session); + sessionStore.publish('default', session); mockObserveRunnerSession.mockResolvedValue({ alive: true, sessionId: 'runner-after', diff --git a/src/daemon/__tests__/request-router-replay-scope.test.ts b/src/daemon/__tests__/request-router-replay-scope.test.ts index 5413f916f7..9e1bf571a6 100644 --- a/src/daemon/__tests__/request-router-replay-scope.test.ts +++ b/src/daemon/__tests__/request-router-replay-scope.test.ts @@ -67,7 +67,7 @@ test('replay runs active-session actions inside the parent request provider scop const replayPath = path.join(root, 'flow.ad'); fs.writeFileSync(replayPath, 'app-switcher\nscroll down\n'); const sessionStore = makeSessionStore('agent-device-replay-scope-'); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const appleRunnerProvider = vi.fn(() => undefined); const handler = createRequestHandler({ @@ -105,7 +105,7 @@ test('replay routes session-changing actions through the full request path', asy const replayPath = path.join(root, 'flow.ad'); fs.writeFileSync(replayPath, 'runtime set --platform ios --metro-host localhost\napp-switcher\n'); const sessionStore = makeSessionStore('agent-device-replay-full-route-'); - sessionStore.set('default', makeIosSession('default', { appBundleId: 'com.example.app' })); + sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const appleRunnerProvider = vi.fn(() => undefined); const handler = createRequestHandler({ @@ -231,7 +231,7 @@ test('fresh replay retains a dynamically selected device through finalization', }); await readinessEntered; - sessionStore.set('external', makeIosSession('external')); + sessionStore.publish('external', makeIosSession('external')); let externalRequestSettled = false; const externalResponse = handler({ token: 'test-token', diff --git a/src/daemon/__tests__/request-router-response-level.test.ts b/src/daemon/__tests__/request-router-response-level.test.ts index c7fcbffb55..82e89aa411 100644 --- a/src/daemon/__tests__/request-router-response-level.test.ts +++ b/src/daemon/__tests__/request-router-response-level.test.ts @@ -64,7 +64,7 @@ function makeIosSession(name: string): SessionState { function makeHandler() { const sessionStore = makeSessionStore('agent-device-router-level-'); - sessionStore.set('level-session', makeIosSession('level-session')); + sessionStore.publish('level-session', makeIosSession('level-session')); return { sessionStore, handler: createRequestHandler({ diff --git a/src/daemon/__tests__/request-router-screenshot.test.ts b/src/daemon/__tests__/request-router-screenshot.test.ts index 2e6a8229be..db017bd1cd 100644 --- a/src/daemon/__tests__/request-router-screenshot.test.ts +++ b/src/daemon/__tests__/request-router-screenshot.test.ts @@ -75,7 +75,7 @@ function screenshotRouter( options: ScreenshotRuntimeFixtureOptions = {}, ): ScreenshotRouter { const sessionStore = makeSessionStore('agent-device-router-screenshot-'); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const runtime = screenshotRuntimeFixture(options); const handler = createRequestHandler({ logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), @@ -202,8 +202,8 @@ test('click forwards macOS menubar session surface to the bound runtime', async test('router serializes concurrent commands for the same device across sessions', async () => { const sessionStore = makeSessionStore('agent-device-router-screenshot-'); - sessionStore.set('session-a', makeSession('session-a')); - sessionStore.set('session-b', makeSession('session-b')); + sessionStore.publish('session-a', makeSession('session-a')); + sessionStore.publish('session-b', makeSession('session-b')); const order: string[] = []; let active = 0; diff --git a/src/daemon/__tests__/request-router-typed-error.test.ts b/src/daemon/__tests__/request-router-typed-error.test.ts index bd54eb4d2b..6170f5cf40 100644 --- a/src/daemon/__tests__/request-router-typed-error.test.ts +++ b/src/daemon/__tests__/request-router-typed-error.test.ts @@ -100,7 +100,7 @@ test('fact-owned UNSUPPORTED_OPERATION errors do not fabricate legacy supportedO const { sessionStore, handler } = makeHandler(); // Linux refuses native perf from exact runtime facts. The retired capability matrix has no // trustworthy platform summary to graft onto that operation-level refusal. - sessionStore.set( + sessionStore.publish( 'typed-error', makeSession('typed-error', { ...TENANT_SESSION_DEFAULTS, @@ -125,7 +125,7 @@ test('fact-owned UNSUPPORTED_OPERATION errors do not fabricate legacy supportedO test('DEVICE_IN_USE errors are flagged retriable; supportedOn stays absent', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('typed-error', makeIosSession('typed-error')); + sessionStore.publish('typed-error', makeIosSession('typed-error')); // R56 put `app-switcher` on a bound operation, so the failure is raised where the device work // happens rather than by the retired dispatcher. systemRuntimeSpies.appSwitcher.mockRejectedValue(new AppError('DEVICE_IN_USE', 'device busy')); @@ -140,7 +140,7 @@ test('DEVICE_IN_USE errors are flagged retriable; supportedOn stays absent', asy test('deterministic errors (INVALID_ARGS) are returned with the default shape — no typed-error fields', async () => { const { sessionStore, handler } = makeHandler(); - sessionStore.set('typed-error', makeIosSession('typed-error')); + sessionStore.publish('typed-error', makeIosSession('typed-error')); // Conflicting explicit selector under a reject lock policy fails with INVALID_ARGS // before dispatch — a deterministic error. @@ -171,7 +171,7 @@ test('BLOCKER 2 (second follow-up): a repair-close platform-close failure surfac ...TENANT_SESSION_DEFAULTS, actions: [{ ts: 1, command: 'open', positionals: ['Demo'], flags: {} }], }); - sessionStore.set('typed-error', session); + sessionStore.publish('typed-error', session); // DEVICE_NOT_FOUND is not in `retriableForErrorCode`'s conservative allow // list — if the handler ever regressed to relying on that code-level @@ -216,7 +216,7 @@ test('#1391: an ordinary close-time script-save failure surfaces details.reason/ status: 'armed', target: { kind: 'explicit', path: targetPath, force: false }, }; - sessionStore.set('typed-error', session); + sessionStore.publish('typed-error', session); try { // Untargeted close: no positionals, so no platform close is dispatched diff --git a/src/daemon/__tests__/request-runtime-binding-router.test.ts b/src/daemon/__tests__/request-runtime-binding-router.test.ts index 19253287fa..ecb28d7b19 100644 --- a/src/daemon/__tests__/request-runtime-binding-router.test.ts +++ b/src/daemon/__tests__/request-runtime-binding-router.test.ts @@ -50,7 +50,7 @@ test('primary request failure survives a rejecting binding disposal', async () = function makeHandler(gateway: DeviceRuntimeGateway) { const sessionStore = makeSessionStore('request-runtime-binding-router-'); - sessionStore.set('session', { + sessionStore.publish('session', { name: 'session', device: { platform: 'android', id: 'emulator-5554', name: 'Pixel', kind: 'emulator' }, appBundleId: 'com.example.app', diff --git a/src/daemon/__tests__/request-save-script-transports.test.ts b/src/daemon/__tests__/request-save-script-transports.test.ts index 51f3f78847..1dd4273295 100644 --- a/src/daemon/__tests__/request-save-script-transports.test.ts +++ b/src/daemon/__tests__/request-save-script-transports.test.ts @@ -68,7 +68,7 @@ function setup(): Harness { roots.push(root); const sessionStore = new SessionStore(path.join(root, 'sessions')); const session = makeIosSession(SESSION); - sessionStore.set(SESSION, session); + sessionStore.publish(SESSION, session); const handleRequest = createRequestHandler({ logPath: path.join(root, 'daemon.log'), token: TOKEN, diff --git a/src/daemon/__tests__/runtime-binding-conformance.ts b/src/daemon/__tests__/runtime-binding-conformance.ts index bfc125365c..43ed1454ff 100644 --- a/src/daemon/__tests__/runtime-binding-conformance.ts +++ b/src/daemon/__tests__/runtime-binding-conformance.ts @@ -148,7 +148,7 @@ export const conformedRuntimeBindings = { resolve: async (device, bindings) => { const sessionStore = makeSessionStore('agent-device-runtime-binding-conformance-'); const session = makeSession('diff-runtime', { device, appBundleId: 'com.example.app' }); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const response = await dispatchSnapshotDiffViaRuntime({ req: { command: 'diff', diff --git a/src/daemon/__tests__/selector-capture-binding.test.ts b/src/daemon/__tests__/selector-capture-binding.test.ts index 1d94126750..d0046d80d7 100644 --- a/src/daemon/__tests__/selector-capture-binding.test.ts +++ b/src/daemon/__tests__/selector-capture-binding.test.ts @@ -134,7 +134,7 @@ test('a session without a tracked app selects the without-active-app plan', asyn test('the bound construction path admits and binds before it builds a runtime', async () => { const fixture = selectorCaptureFixture(); const sessionStore = makeSessionStore(); - sessionStore.set('bound', makeAndroidSession('bound')); + sessionStore.publish('bound', makeAndroidSession('bound')); const resolved = await createBoundSelectorRuntime( { @@ -158,7 +158,7 @@ test('the bound construction path refuses an unavailable operation without build capture: { available: false, reason: 'unsupported-platform-leaf' }, }); const sessionStore = makeSessionStore(); - sessionStore.set('bound', makeAndroidSession('bound')); + sessionStore.publish('bound', makeAndroidSession('bound')); const resolved = await createBoundSelectorRuntime( { diff --git a/src/daemon/__tests__/selector-capture-runtime.test.ts b/src/daemon/__tests__/selector-capture-runtime.test.ts index 5173d75ad5..db9dd81471 100644 --- a/src/daemon/__tests__/selector-capture-runtime.test.ts +++ b/src/daemon/__tests__/selector-capture-runtime.test.ts @@ -38,7 +38,7 @@ test('selector capture cache is keyed by scoped presentation options', async () nodes: [{ ref: 'e1', index: 0, type: 'Button', label: 'A' }], }, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); boundCapture.mockImplementation(async (input) => ({ backend: 'xctest', producer: 'apple-runner', @@ -213,7 +213,7 @@ function proofRuntime(params: { params.sessionName, params.storedSnapshot ? { snapshot: params.storedSnapshot } : {}, ); - sessionStore.set(params.sessionName, session); + sessionStore.publish(params.sessionName, session); boundCapture.mockResolvedValue({ backend: 'xctest', producer: 'apple-runner', @@ -316,7 +316,7 @@ test('a later fact-less capture does not erase an earlier repair proof', async ( function makeCaptureRuntime(sessionName: string) { const sessionStore = makeSessionStore('agent-device-selector-capture-'); const session = makeIosSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const runtime = createSelectorCaptureRuntime({ ref: sessionStore.lookup(sessionName), device: session.device, diff --git a/src/daemon/__tests__/selector-recording.test.ts b/src/daemon/__tests__/selector-recording.test.ts index 77825b263b..fada69c772 100644 --- a/src/daemon/__tests__/selector-recording.test.ts +++ b/src/daemon/__tests__/selector-recording.test.ts @@ -46,7 +46,7 @@ test('a repair-armed session excludes get/is/find by default but keeps recording boundary: 0, }, }); - store.set('default', session); + store.publish('default', session); recordIfSession(store, store.lookup('default'), req('get'), {}); recordIfSession(store, store.lookup('default'), req('is'), {}); @@ -71,7 +71,7 @@ test('a repair-armed session still records get/is/find dispatched as replay plan boundary: 0, }, }); - store.set('default', session); + store.publish('default', session); recordIfSession(store, store.lookup('default'), planStepReq('get'), {}); recordIfSession(store, store.lookup('default'), planStepReq('is'), {}); @@ -90,7 +90,7 @@ test('--record forces get/is/find through even while repair-armed', () => { boundary: 0, }, }); - store.set('default', session); + store.publish('default', session); recordIfSession(store, store.lookup('default'), req('get', { record: true }), {}); recordIfSession(store, store.lookup('default'), req('is', { record: true }), {}); @@ -103,7 +103,7 @@ test('outside a repair-armed session, get/is/find/wait all record normally', () const store = makeStore(); const session = makeIosSession('default'); expect(session.scriptPublication).toBeUndefined(); - store.set('default', session); + store.publish('default', session); recordIfSession(store, store.lookup('default'), req('get'), {}); recordIfSession(store, store.lookup('default'), req('is'), {}); @@ -120,7 +120,7 @@ test('outside a repair-armed session, get/is/find/wait all record normally', () test('wait absent records positionals without target-v1 annotation', () => { const store = makeStore(); - store.set('default', makeIosSession('default')); + store.publish('default', makeIosSession('default')); recordIfSession(store, store.lookup('default'), waitAbsentReq(), { waitedMs: 0 }); diff --git a/src/daemon/__tests__/session-idle-activity.test.ts b/src/daemon/__tests__/session-idle-activity.test.ts index 02557fdbdf..cb591d4773 100644 --- a/src/daemon/__tests__/session-idle-activity.test.ts +++ b/src/daemon/__tests__/session-idle-activity.test.ts @@ -46,7 +46,7 @@ function makeRequest(overrides: Partial = {}): DaemonRequest { function storeWithSession() { const sessionStore = makeSessionStore('agent-device-idle-activity-'); - sessionStore.set('default', makeIosSession('default')); + sessionStore.publish('default', makeIosSession('default')); return sessionStore; } diff --git a/src/daemon/__tests__/session-idle-expiry-harness.ts b/src/daemon/__tests__/session-idle-expiry-harness.ts index d1e913409c..f92bd4082e 100644 --- a/src/daemon/__tests__/session-idle-expiry-harness.ts +++ b/src/daemon/__tests__/session-idle-expiry-harness.ts @@ -106,7 +106,7 @@ export function createIdleExpiryHarness(): Readonly<{ createdAt: NOW - WINDOW_MS - 1, deviceClaim: { ...CLAIM }, }); - store.set(name, session); + store.publish(name, session); return session; }, // Past its window on time alone, and holding no claim — nothing another agent waits on. @@ -130,7 +130,7 @@ export function createIdleExpiryHarness(): Readonly<{ createdAt: NOW - WINDOW_MS - 1, deviceClaim: acquired.ownership, }); - fixture.sessionStore.set(name, session); + fixture.sessionStore.publish(name, session); return { session, deviceClaim: acquired.ownership }; }, claimFileHeld: (deviceClaim) => fs.existsSync(resolveDeviceClaimPath(deviceClaim.deviceKey)), diff --git a/src/daemon/__tests__/session-routing.test.ts b/src/daemon/__tests__/session-routing.test.ts index 2de5ff75dc..d12a00ea29 100644 --- a/src/daemon/__tests__/session-routing.test.ts +++ b/src/daemon/__tests__/session-routing.test.ts @@ -36,7 +36,7 @@ function makeStore(t: TestContext): SessionStore { test('does not reuse lone active session for implicit default session from another scope', (t) => { const store = makeStore(t); - store.set('android', makeSession('android')); + store.publish('android', makeSession('android')); const cwd = mkdtempForTestSync('agent-device-cwd-scope-'); t.onTestFinished(() => { fs.rmSync(cwd, { recursive: true, force: true }); @@ -223,7 +223,7 @@ test('names an implicit session by the platform it selects', (t) => { test('gives each platform its own implicit session in one workspace', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); // #2580: binding Android from the same checkout used to be refused because the workspace // session was already bound to Apple. @@ -239,7 +239,7 @@ test('joins the workspace session that a named platform agrees with', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); const openedWithoutPlatform = `cwd:${scopeId}:default`; - store.set(openedWithoutPlatform, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(openedWithoutPlatform, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); // Routing must keep the store key, so a platform-tagged command writes into the artifacts // of the session it joined instead of a second directory. @@ -251,7 +251,7 @@ test('joins the workspace session that a named platform agrees with', (t) => { test('does not join a workspace session bound to another platform', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); const resolved = resolveEffectiveSessionName( implicitRequest(cwd, { platform: 'android', device: 'Pixel 9' }, 'boot'), @@ -265,7 +265,7 @@ test('routes a platform-less request to the workspace sole implicit session', (t const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); const iosSession = `cwd:${scopeId}:ios`; - store.set(iosSession, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(iosSession, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'press'), store, { attachesToSession: true, @@ -277,8 +277,8 @@ test('routes a platform-less request to the workspace sole implicit session', (t test('refuses a platform-less request when the workspace holds several implicit sessions', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); - store.set(`cwd:${scopeId}:android`, makeWorkspaceSession(scopeId, ANDROID_EMULATOR)); + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:android`, makeWorkspaceSession(scopeId, ANDROID_EMULATOR)); assert.throws( () => @@ -303,7 +303,7 @@ test('routes a platform-less request to a sole default-leaf session', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); const openedWithoutPlatform = `cwd:${scopeId}:default`; - store.set(openedWithoutPlatform, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(openedWithoutPlatform, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'press'), store, { attachesToSession: true, @@ -318,8 +318,8 @@ test('refuses a platform-less request when a default-leaf session shares the wor // The #2580 shape: iOS was opened without `--platform`, so it owns the `default` leaf, and // Android then took its own platform leaf. Preferring `default` here would run a bare `press` // on the iOS device the caller is no longer addressing. - store.set(`cwd:${scopeId}:default`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); - store.set(`cwd:${scopeId}:android`, makeWorkspaceSession(scopeId, ANDROID_EMULATOR)); + store.publish(`cwd:${scopeId}:default`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:android`, makeWorkspaceSession(scopeId, ANDROID_EMULATOR)); assert.throws( () => @@ -343,8 +343,8 @@ test('refuses a platform-less request when a default-leaf session shares the wor test('keeps inventory commands routable across implicit session ambiguity', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); - store.set(`cwd:${scopeId}:android`, makeWorkspaceSession(scopeId, ANDROID_EMULATOR)); + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:android`, makeWorkspaceSession(scopeId, ANDROID_EMULATOR)); // `session list` is how a caller discovers the two addresses, so it must not be refused. const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'session_list'), store, { @@ -357,7 +357,10 @@ test('keeps inventory commands routable across implicit session ambiguity', (t) test('keeps a named session out of implicit workspace routing', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set('qa', { ...makeWorkspaceSession(scopeId, ANDROID_EMULATOR), sessionScope: undefined }); + store.publish('qa', { + ...makeWorkspaceSession(scopeId, ANDROID_EMULATOR), + sessionScope: undefined, + }); const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'press'), store, { attachesToSession: true, @@ -369,7 +372,10 @@ test('keeps a named session out of implicit workspace routing', (t) => { test('ignores an implicit session owned by another workspace', (t) => { const { cwd } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set('cwd:0000000000000000:ios', makeWorkspaceSession('0000000000000000', IOS_SIMULATOR)); + store.publish( + 'cwd:0000000000000000:ios', + makeWorkspaceSession('0000000000000000', IOS_SIMULATOR), + ); const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'press'), store, { attachesToSession: true, @@ -410,8 +416,8 @@ test('treats an unusable platform value as no platform at all', (t) => { test('refuses a platform selector that several implicit sessions equally answer', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); - store.set(`cwd:${scopeId}:macos`, { + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:macos`, { ...makeWorkspaceSession(scopeId, { platform: 'apple', appleOs: 'macos', @@ -441,8 +447,8 @@ test('prefers the session already keyed to the requested platform', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); // Both sessions answer `--platform ios`: the one keyed to it, and one opened without a platform. - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); - store.set( + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish( `cwd:${scopeId}:default`, makeWorkspaceSession(scopeId, { ...IOS_SIMULATOR, id: 'SIM-002', name: 'iPhone 16 Plus' }), ); @@ -456,7 +462,7 @@ test('joins the session already keyed to the platform-less default leaf', (t) => const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); const defaultAddress = `cwd:${scopeId}:default`; - store.set(defaultAddress, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(defaultAddress, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'press'), store, { attachesToSession: true, @@ -469,7 +475,7 @@ test('keeps a same-platform device mismatch on the bound session instead of fork const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); const boundSession = `cwd:${scopeId}:default`; - store.set(boundSession, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(boundSession, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); // `--platform ios` agrees with the bound session; only the device disagrees. Routing hands the // request to that session so the shared selector rules refuse the mismatch, rather than opening @@ -486,7 +492,7 @@ test('keeps a same-platform target mismatch on the bound session', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); const boundSession = `cwd:${scopeId}:default`; - store.set(boundSession, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(boundSession, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); const resolved = resolveEffectiveSessionName( implicitRequest(cwd, { platform: 'ios', target: 'tv' }), @@ -515,7 +521,7 @@ test('routes a session-lock platform into the implicit session key', (t) => { test('routes a session-lock platform past a workspace session bound to another platform', (t) => { const { cwd, scopeId } = makeWorkspaceCwd(t); const store = makeStore(t); - store.set(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); + store.publish(`cwd:${scopeId}:ios`, makeWorkspaceSession(scopeId, IOS_SIMULATOR)); // Candidate matching that reads only `flags` sees no platform here, so the iOS session appears // to agree and the request runs on it; the lock policy then leaves the platform unset because a @@ -536,10 +542,10 @@ test('counts one session reachable under two addresses once', (t) => { const store = makeStore(t); const address = `cwd:${scopeId}:ios`; const session = makeWorkspaceSession(scopeId, IOS_SIMULATOR); - store.set(address, session); + store.publish(address, session); // A writer that stores by `SessionState.name` publishes the same session a second time. It must // not read as a second session the caller has to disambiguate. - store.set(session.name, session); + store.publish(session.name, session); const resolved = resolveEffectiveSessionName(implicitRequest(cwd, {}, 'press'), store, { attachesToSession: true, diff --git a/src/daemon/__tests__/session-store.test.ts b/src/daemon/__tests__/session-store.test.ts index 0455a280cb..1258c6d85a 100644 --- a/src/daemon/__tests__/session-store.test.ts +++ b/src/daemon/__tests__/session-store.test.ts @@ -117,7 +117,7 @@ test('session lease metadata round-trips through the store', () => { expiresAt: 123_456, }; - store.set(session.name, session); + store.publish(session.name, session); assert.deepEqual(store.get(session.name)?.lease, session.lease); }); @@ -125,7 +125,7 @@ test('session lease metadata round-trips through the store', () => { test('sessions without lease metadata remain valid', () => { const { store, session } = makeFixture('agent-device-session-unleased-'); - store.set(session.name, session); + store.publish(session.name, session); assert.equal(store.get(session.name)?.lease, undefined); }); @@ -848,7 +848,7 @@ test('BLOCKER 3: finalizeRepairTeardown auto-commit records a terminal close, pr test('noteSessionActivity stamps the live record and ignores an unknown address', () => { const { store, session } = makeFixture('agent-device-store-note-activity-'); - store.set('default', session); + store.publish('default', session); assert.equal(session.lastActivityAtMs, undefined); store.noteSessionActivity('default', 5_000); diff --git a/src/daemon/__tests__/snapshot-command-runtime.test.ts b/src/daemon/__tests__/snapshot-command-runtime.test.ts index e44cf95932..347ad83554 100644 --- a/src/daemon/__tests__/snapshot-command-runtime.test.ts +++ b/src/daemon/__tests__/snapshot-command-runtime.test.ts @@ -39,7 +39,7 @@ for (const command of ['snapshot', 'diff snapshot'] as const) { test(`${command} forwards request cancellation into snapshot dispatch`, async () => { const sessionName = 'default'; const sessionStore = makeSessionStore('agent-device-snapshot-cancellation-'); - sessionStore.set(sessionName, makeAndroidSession(sessionName)); + sessionStore.publish(sessionName, makeAndroidSession(sessionName)); const requestId = `snapshot-cancellation-${command.replace(' ', '-')}`; const registration = registerRequestAbort(requestId); if (!registration) throw new Error('expected request abort registration'); diff --git a/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts b/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts index 07e2da8d76..bb5b8e9767 100644 --- a/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts +++ b/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts @@ -44,7 +44,7 @@ function snapshotDeviceRuntimeGateway(): DeviceRuntimeGateway { const harness = await runtimeHarness({}); const sessionStore = makeSessionStore('agent-device-diff-runtime-plan-'); - sessionStore.set('diff-runtime', { + sessionStore.publish('diff-runtime', { ...harness.session, ...(activeApp ? { appBundleId: 'com.example.app' } : {}), }); @@ -181,7 +181,7 @@ test('preserves initialized, unchanged, and changed diff results through one bou captures: [baseCapture, baseCapture, changedCapture], }); const sessionStore = makeSessionStore('agent-device-diff-runtime-results-'); - sessionStore.set('diff-runtime', { ...harness.session, appBundleId: 'com.example.app' }); + sessionStore.publish('diff-runtime', { ...harness.session, appBundleId: 'com.example.app' }); const run = async () => await dispatchSnapshotDiffViaRuntime({ @@ -230,7 +230,7 @@ test('a sparse internal diff capture returns no screenshot fallback artifact', a ], }); const sessionStore = makeSessionStore('agent-device-diff-runtime-sparse-'); - sessionStore.set('diff-runtime', { ...harness.session, appBundleId: 'com.example.app' }); + sessionStore.publish('diff-runtime', { ...harness.session, appBundleId: 'com.example.app' }); const response = await dispatchSnapshotDiffViaRuntime({ req: { command: 'diff', positionals: ['snapshot'], token: 't', session: 'diff-runtime' }, diff --git a/src/daemon/__tests__/snapshot-quality-latch.test.ts b/src/daemon/__tests__/snapshot-quality-latch.test.ts index 337bf2e031..5a5ba0616b 100644 --- a/src/daemon/__tests__/snapshot-quality-latch.test.ts +++ b/src/daemon/__tests__/snapshot-quality-latch.test.ts @@ -190,7 +190,7 @@ function scenario() { const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; const session = makeIosSession(sessionName, { appBundleId: 'com.example.app' }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); return { sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; } diff --git a/src/daemon/__tests__/snapshot-runtime-binding.test.ts b/src/daemon/__tests__/snapshot-runtime-binding.test.ts index b512636855..3b87f1b483 100644 --- a/src/daemon/__tests__/snapshot-runtime-binding.test.ts +++ b/src/daemon/__tests__/snapshot-runtime-binding.test.ts @@ -29,7 +29,7 @@ test('the owning interface binds exactly the session device the facts were admit }; const sessionStore = makeSessionStore(); // An active app: the fixture admits captureSnapshot for a local iOS simulator only with one. - sessionStore.set( + sessionStore.publish( 'bind-test', makeIosSession('bind-test', { device: IOS_SIMULATOR, appBundleId: 'com.example.app' }), ); diff --git a/src/daemon/__tests__/snapshot-runtime-disclosure.test.ts b/src/daemon/__tests__/snapshot-runtime-disclosure.test.ts index 75e59093d1..4ce0805023 100644 --- a/src/daemon/__tests__/snapshot-runtime-disclosure.test.ts +++ b/src/daemon/__tests__/snapshot-runtime-disclosure.test.ts @@ -159,7 +159,7 @@ function scenario(params: { storedRepair?: boolean }) { iosSystemSurfaceBundleId: 'com.apple.SafariViewService', }; } - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); return { sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; } diff --git a/src/daemon/__tests__/sparse-fallback-screenshot.test.ts b/src/daemon/__tests__/sparse-fallback-screenshot.test.ts index a4a1a8d00a..703c779cdb 100644 --- a/src/daemon/__tests__/sparse-fallback-screenshot.test.ts +++ b/src/daemon/__tests__/sparse-fallback-screenshot.test.ts @@ -25,7 +25,10 @@ async function scenario() { const root = await mkdtempForTest('agent-device-sparse-fallback-'); const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); return { sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; } diff --git a/src/daemon/__tests__/system-button-runtime.test.ts b/src/daemon/__tests__/system-button-runtime.test.ts index f6e1b49a12..a95d1f0f7b 100644 --- a/src/daemon/__tests__/system-button-runtime.test.ts +++ b/src/daemon/__tests__/system-button-runtime.test.ts @@ -170,7 +170,7 @@ test('request router joins home admission to execution, recording, and ref inval const sessionStore = makeSessionStore('agent-device-home-generic-'); const session = makeSession('system-button-runtime', { device: iosSimulator }); activateCompleteRefFrame(session); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const handler = createRequestHandler({ logPath: '/tmp/daemon.log', token: 't', diff --git a/src/daemon/__tests__/tv-remote-runtime.test.ts b/src/daemon/__tests__/tv-remote-runtime.test.ts index 17fdb7713c..171cecb8b6 100644 --- a/src/daemon/__tests__/tv-remote-runtime.test.ts +++ b/src/daemon/__tests__/tv-remote-runtime.test.ts @@ -214,7 +214,7 @@ test('request router joins tv-remote admission to execution and ref invalidation const sessionStore = makeSessionStore('agent-device-tv-remote-generic-'); const session = makeSession('tv-remote-runtime', { device: vegaVvd }); activateCompleteRefFrame(session); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const handler = createRequestHandler({ logPath: '/tmp/daemon.log', token: 't', diff --git a/src/daemon/__tests__/viewport-runtime.test.ts b/src/daemon/__tests__/viewport-runtime.test.ts index 08235b3961..e48fc75d0f 100644 --- a/src/daemon/__tests__/viewport-runtime.test.ts +++ b/src/daemon/__tests__/viewport-runtime.test.ts @@ -165,7 +165,7 @@ test('request router joins viewport admission to execution, recording, and ref i const sessionStore = makeSessionStore('agent-device-viewport-generic-'); const session = makeSession('viewport-runtime', { device: webDevice }); activateCompleteRefFrame(session); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const handler = createRequestHandler({ logPath: '/tmp/daemon.log', token: 't', diff --git a/src/daemon/__tests__/wait-runtime.test.ts b/src/daemon/__tests__/wait-runtime.test.ts index 98f776547d..3871013494 100644 --- a/src/daemon/__tests__/wait-runtime.test.ts +++ b/src/daemon/__tests__/wait-runtime.test.ts @@ -210,7 +210,7 @@ async function runWait( backend: 'web', } as unknown as NonNullable; } - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const response = await handleSnapshotCommands({ req: waitRequest(positionals, flags), sessionName: session.name, diff --git a/src/daemon/handlers/__tests__/install-source.test.ts b/src/daemon/handlers/__tests__/install-source.test.ts index ca21dfbcc4..e1243b8412 100644 --- a/src/daemon/handlers/__tests__/install-source.test.ts +++ b/src/daemon/handlers/__tests__/install-source.test.ts @@ -75,7 +75,7 @@ test('install_source materializes and deploys through one admitted runtime bindi kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const materialize = vi.fn(async (): Promise => ({ installablePath: '/tmp/materialized/app.apk', cleanup: async () => {}, @@ -123,7 +123,7 @@ test('install_source rejects unavailable facts before binding or materializing', kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const materialize = vi.fn(async (): Promise => ({ installablePath: '/tmp/materialized/app.apk', cleanup: async () => {}, @@ -159,7 +159,7 @@ test('install_source fails closed when a provider owner does not expose readines kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const localMaterialization = vi.fn(async (): Promise => ({ installablePath: '/tmp/local-fallback.apk', cleanup: async () => {}, @@ -215,7 +215,7 @@ test('install_source cleans materialized paths when deployment fails after admis kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const cleanup = vi.fn(async () => {}); const runtime = createSourceRuntime( session.device, @@ -249,7 +249,7 @@ test('install_source preserves the Android identity failure when its runtime can kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const runtime = createSourceRuntime( session.device, async () => ({ installablePath: '/tmp/materialized/app.apk', cleanup: async () => {} }), @@ -279,7 +279,7 @@ test('install_source returns the typed iOS artifact identity supplied by its run kind: 'simulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const runtime = createSourceRuntime( session.device, async () => ({ @@ -325,7 +325,7 @@ test('install_source accepts the public leaf selector of an Apple session it is kind: 'simulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const runtime = createSourceRuntime( session.device, async () => ({ @@ -358,7 +358,7 @@ test('install_source still refuses a leaf selector that names a different platfo kind: 'simulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const runtime = createSourceRuntime( session.device, async () => ({ installablePath: '/tmp/App.app', cleanup: async () => {} }), diff --git a/src/daemon/handlers/__tests__/react-native.test.ts b/src/daemon/handlers/__tests__/react-native.test.ts index 68dbd0853f..7d5b49252a 100644 --- a/src/daemon/handlers/__tests__/react-native.test.ts +++ b/src/daemon/handlers/__tests__/react-native.test.ts @@ -32,7 +32,7 @@ beforeEach(() => { test('react-native dismiss-overlay taps collapsed warning close affordance instead of banner center', async () => { const sessionName = 'rn-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot .mockResolvedValueOnce({ snapshot: { @@ -97,7 +97,7 @@ test('react-native dismiss-overlay taps collapsed warning close affordance inste test('react-native dismiss-overlay prefers non-trailing collapsed warning close controls', async () => { const sessionName = 'rn-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ @@ -151,7 +151,7 @@ test('react-native dismiss-overlay prefers non-trailing collapsed warning close test('react-native dismiss-overlay does not confuse app dismiss buttons with overlay controls', async () => { const sessionName = 'rn-collapsed-with-app-dismiss-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot .mockResolvedValueOnce({ snapshot: { @@ -222,7 +222,7 @@ test('react-native dismiss-overlay does not confuse app dismiss buttons with ove test('react-native dismiss-overlay rejects unsafe collapsed warning coordinate fallback', async () => { const sessionName = 'rn-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ @@ -266,7 +266,7 @@ test('react-native dismiss-overlay rejects unsafe collapsed warning coordinate f test('react-native dismiss-overlay dismisses RedBox error overlays instead of minimizing them', async () => { const sessionName = 'rn-redbox-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot .mockResolvedValueOnce({ snapshot: { @@ -336,7 +336,7 @@ test('react-native dismiss-overlay dismisses RedBox error overlays instead of mi test('react-native dismiss-overlay reports unverified dismiss when RedBox controls remain', async () => { const sessionName = 'rn-redbox-still-full-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); const fullRedBoxSnapshot = { snapshot: { nodes: [ @@ -397,7 +397,7 @@ test('react-native dismiss-overlay reports unverified dismiss when RedBox contro test('react-native dismiss-overlay uses Dismiss when RedBox Minimize is absent', async () => { const sessionName = 'rn-redbox-dismiss-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ @@ -448,7 +448,7 @@ test('react-native dismiss-overlay uses Dismiss when RedBox Minimize is absent', test('react-native dismiss-overlay accepts RedBox control labels with keyboard shortcut suffixes', async () => { const sessionName = 'rn-redbox-shortcut-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ @@ -499,7 +499,7 @@ test('react-native dismiss-overlay accepts RedBox control labels with keyboard s test('react-native dismiss-overlay prefers concrete RedBox buttons over labeled wrappers', async () => { const sessionName = 'rn-redbox-wrapper-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ @@ -556,7 +556,7 @@ test('react-native dismiss-overlay prefers concrete RedBox buttons over labeled test('react-native dismiss-overlay reports verified success after a clean post-dismiss snapshot', async () => { const sessionName = 'rn-verify-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, 'android')); + sessionStore.publish(sessionName, makeSession(sessionName, 'android')); mockCaptureSnapshot .mockResolvedValueOnce({ snapshot: { @@ -635,7 +635,7 @@ test('react-native dismiss-overlay reports sparse verdict instead of no overlay createdAt: Date.now(), }; const previousSnapshot = session.snapshot; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ @@ -687,7 +687,7 @@ test('react-native dismiss-overlay reports sparse verdict instead of no overlay test('react-native dismiss-overlay reports unverified dismiss when post-dismiss snapshot is sparse', async () => { const sessionName = 'rn-verify-sparse-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot .mockResolvedValueOnce({ snapshot: { @@ -755,7 +755,7 @@ test('react-native dismiss-overlay reports unverified dismiss when post-dismiss test('react-native dismiss-overlay reports still-visible overlays with recovery guidance', async () => { const sessionName = 'rn-verify-still-visible-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, 'android')); + sessionStore.publish(sessionName, makeSession(sessionName, 'android')); const overlaySnapshot = { snapshot: { nodes: [ @@ -805,7 +805,7 @@ test('react-native dismiss-overlay reports still-visible overlays with recovery test('react-native dismiss-overlay ignores app copy that only mentions RN overlay terms', async () => { const sessionName = 'rn-copy-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockCaptureSnapshot.mockResolvedValue({ snapshot: { nodes: [ diff --git a/src/daemon/handlers/__tests__/record-runtime-stop-recovery.test.ts b/src/daemon/handlers/__tests__/record-runtime-stop-recovery.test.ts index 041ce1de26..b5b9d60c42 100644 --- a/src/daemon/handlers/__tests__/record-runtime-stop-recovery.test.ts +++ b/src/daemon/handlers/__tests__/record-runtime-stop-recovery.test.ts @@ -65,9 +65,9 @@ test('record stop after daemon-state loss reattaches only through the persisted 'capture.mp4', ); await harness.run(['start', outPath]); - const adopted = harness.sessionStore.get(harness.sessionName); - if (!adopted) throw new Error('Expected adopted recording session'); - harness.sessionStore.set(harness.sessionName, { ...adopted, screenRecording: undefined }); + const ref = harness.sessionStore.lookup(harness.sessionName); + if (!ref) throw new Error('Expected adopted recording session'); + harness.sessionStore.update(ref, { screenRecording: undefined }); const stopped = await harness.run(['stop']); @@ -87,9 +87,9 @@ test('record stop terminalizes cleanup-only exact recovery without starting a re 'capture.mp4', ); await harness.run(['start', outPath]); - const adopted = harness.sessionStore.get(harness.sessionName); - if (!adopted) throw new Error('Expected adopted recording session'); - harness.sessionStore.set(harness.sessionName, { ...adopted, screenRecording: undefined }); + const ref = harness.sessionStore.lookup(harness.sessionName); + if (!ref) throw new Error('Expected adopted recording session'); + harness.sessionStore.update(ref, { screenRecording: undefined }); const stopped = await harness.run(['stop']); @@ -120,9 +120,9 @@ test('record stop rejects a cross-session recovery manifest before exact-owner b ...record.envelope, sessionId: 'recording-b', }); - const adopted = harness.sessionStore.get(harness.sessionName); - if (!adopted) throw new Error('Expected adopted recording session'); - harness.sessionStore.set(harness.sessionName, { ...adopted, screenRecording: undefined }); + const ref = harness.sessionStore.lookup(harness.sessionName); + if (!ref) throw new Error('Expected adopted recording session'); + harness.sessionStore.update(ref, { screenRecording: undefined }); const stopped = await harness.run(['stop']); diff --git a/src/daemon/handlers/__tests__/record-runtime.fixtures.ts b/src/daemon/handlers/__tests__/record-runtime.fixtures.ts index ebab2a2433..d7afd8e640 100644 --- a/src/daemon/handlers/__tests__/record-runtime.fixtures.ts +++ b/src/daemon/handlers/__tests__/record-runtime.fixtures.ts @@ -53,7 +53,7 @@ export function makeRecordRuntimeHarness( createdAt: 1, actions: [], }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const runtime = makeRuntime(session, options.runtime); const common = { sessionName, diff --git a/src/daemon/handlers/__tests__/session-appstate-input.test.ts b/src/daemon/handlers/__tests__/session-appstate-input.test.ts index bf1a1327e6..308f84dcc8 100644 --- a/src/daemon/handlers/__tests__/session-appstate-input.test.ts +++ b/src/daemon/handlers/__tests__/session-appstate-input.test.ts @@ -32,7 +32,7 @@ import type { BindDeviceRuntime } from '../../request-runtime-binding.ts'; test('appstate on iOS requires active session on selected device', async () => { const sessionStore = makeSessionStore(); const sessionName = 'default'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'apple', id: 'sim-1', @@ -78,7 +78,7 @@ test('appstate on iOS requires active session on selected device', async () => { test('appstate returns session appName when bundle id is unavailable', async () => { const sessionStore = makeSessionStore(); const sessionName = 'sim'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'apple', id: 'sim-1', @@ -128,7 +128,7 @@ test('appstate returns session appName when bundle id is unavailable', async () test('appstate fails when iOS session has no tracked app', async () => { const sessionStore = makeSessionStore(); const sessionName = 'sim'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -256,7 +256,7 @@ test('clipboard requires an active session or explicit device selector', async ( test('clipboard rejects unsupported iOS physical devices', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-device-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -342,7 +342,7 @@ function iosSessionRequest(sessionName: string): DaemonRequest { test('appstate on iOS reads the session app state from the runner when its owner admits it', async () => { const sessionStore = makeSessionStore(); const sessionName = 'sim'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, IOS_SESSION_DEVICE), appBundleId: 'dev.e2e.benchmark', appName: 'Benchmark', @@ -377,7 +377,7 @@ test('appstate on iOS reads the session app state from the runner when its owner test('appstate on iOS keeps the session answer, with no state, when no runner is live to ask', async () => { const sessionStore = makeSessionStore(); const sessionName = 'sim'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, IOS_SESSION_DEVICE), appBundleId: 'dev.e2e.benchmark', appName: 'Benchmark', @@ -404,7 +404,7 @@ test('appstate on iOS keeps the session answer, with no state, when no runner is test('appstate on iOS keeps the session answer, with no state, when the runner cannot read one', async () => { const sessionStore = makeSessionStore(); const sessionName = 'sim'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, IOS_SESSION_DEVICE), appBundleId: 'dev.e2e.benchmark', appName: 'Benchmark', diff --git a/src/daemon/handlers/__tests__/session-boot-shutdown.test.ts b/src/daemon/handlers/__tests__/session-boot-shutdown.test.ts index a325dedd7c..a9b87ae488 100644 --- a/src/daemon/handlers/__tests__/session-boot-shutdown.test.ts +++ b/src/daemon/handlers/__tests__/session-boot-shutdown.test.ts @@ -44,7 +44,7 @@ test('boot requires session or explicit selector', async () => { test('boot prefers explicit device selector over active session device', async () => { const sessionStore = makeSessionStore(); const sessionName = 'default'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'android', @@ -473,7 +473,7 @@ test('shutdown rejects active session device and points to close --shutdown', as target: 'mobile', booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, selectedDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, selectedDevice)); mockResolveTargetDevice.mockResolvedValue(selectedDevice); const response = await handleSessionCommands({ diff --git a/src/daemon/handlers/__tests__/session-clipboard.test.ts b/src/daemon/handlers/__tests__/session-clipboard.test.ts index e7ea6eba93..b6cb57eb3d 100644 --- a/src/daemon/handlers/__tests__/session-clipboard.test.ts +++ b/src/daemon/handlers/__tests__/session-clipboard.test.ts @@ -73,7 +73,7 @@ function harness( function request(positionals: string[]) { const sessionName = 'clipboard-session'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, androidDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, androidDevice)); mockResolveTargetDevice.mockResolvedValue(androidDevice); return { sessionName, diff --git a/src/daemon/handlers/__tests__/session-device-claims.test.ts b/src/daemon/handlers/__tests__/session-device-claims.test.ts index d45941b2c1..8ba2fa6bb8 100644 --- a/src/daemon/handlers/__tests__/session-device-claims.test.ts +++ b/src/daemon/handlers/__tests__/session-device-claims.test.ts @@ -75,7 +75,6 @@ const mockResolveTargetDevice = vi.mocked(resolveTargetDevice); const mockEnsureDeviceReady = vi.mocked(ensureDeviceReady); const mockApplyRuntimeHints = vi.mocked(applyRuntimeHintValues); const mockResolveAndroidPackage = vi.mocked(resolveAndroidPackageForOpen); -const roots: string[] = []; const reconcileOrphanedDeviceClaim = async () => ({ status: 'retained' as const, reason: 'test-no-recovery', @@ -115,14 +114,12 @@ afterEach(() => { mockApplyRuntimeHints.mockResolvedValue(undefined); mockResolveAndroidPackage.mockResolvedValue(undefined); delete process.env.AGENT_DEVICE_CLAIMS_DIR; - for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); }); function setup(): { store: SessionStore; stateDir: string } { const stateDir = mkdtempForTestSync('agent-device-session-device-claim-'); const claimsDir = path.join(stateDir, 'claims'); process.env.AGENT_DEVICE_CLAIMS_DIR = claimsDir; - roots.push(stateDir); return { store: new SessionStore(path.join(stateDir, 'sessions')), stateDir }; } @@ -452,7 +449,7 @@ test('local close clears its matching device claim after teardown', async () => }); assert.equal(acquired.status, 'acquired'); if (acquired.status !== 'acquired') return; - store.set('close-claim', { + store.publish('close-claim', { name: 'close-claim', device: android, deviceClaim: acquired.ownership, @@ -497,7 +494,7 @@ test('#1391: a close-time script save failure still clears the device claim and target: { kind: 'explicit', path: targetPath, force: false }, }, }); - store.set('close-save-script-failure', session); + store.publish('close-save-script-failure', session); mockDispatch.mockResolvedValue(undefined); // Like the platform-close-error tests above, a failed close-time save is diff --git a/src/daemon/handlers/__tests__/session-doctor-app-runtime.test.ts b/src/daemon/handlers/__tests__/session-doctor-app-runtime.test.ts index 5cd51d1ad4..84394ce3d2 100644 --- a/src/daemon/handlers/__tests__/session-doctor-app-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-app-runtime.test.ts @@ -31,7 +31,7 @@ vi.mock('../session-doctor-metro.ts', () => ({ test('doctor app checks retain runtime admission failures as a failed target-app check', async () => { const sessionName = 'doctor-app-runtime-failure'; const sessionStore = makeSessionStore('agent-device-doctor-app-runtime-'); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { name: sessionName, device: LINUX_DEVICE, createdAt: Date.now(), @@ -135,7 +135,7 @@ test('doctor preserves the legacy unsupported HarmonyOS target-app check', async }; const sessionName = 'doctor-harmony-app-runtime'; const sessionStore = makeSessionStore('agent-device-doctor-harmony-'); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { name: sessionName, device: harmonyDevice, createdAt: Date.now(), diff --git a/src/daemon/handlers/__tests__/session-doctor-options.test.ts b/src/daemon/handlers/__tests__/session-doctor-options.test.ts index 683584b204..bb575d87a2 100644 --- a/src/daemon/handlers/__tests__/session-doctor-options.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-options.test.ts @@ -11,8 +11,8 @@ test('the same-device warning names the other session by its address, not its na const store = makeSessionStore(); const here = 'cwd:aaaaaaaaaaaaaaaa:default'; const other = 'cwd:bbbbbbbbbbbbbbbb:default'; - store.set(here, makeIosSession('default')); - store.set(other, makeIosSession('default')); + store.publish(here, makeIosSession('default')); + store.publish(other, makeIosSession('default')); const [check] = sessionChecks(store, here, store.get(here)); @@ -26,7 +26,7 @@ test('the same-device warning names the other session by its address, not its na test('a session alone on its device passes with its own address in evidence', () => { const store = makeSessionStore(); const here = 'cwd:aaaaaaaaaaaaaaaa:default'; - store.set(here, makeIosSession('default')); + store.publish(here, makeIosSession('default')); const [check] = sessionChecks(store, here, store.get(here)); diff --git a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts index 2adb0e5820..dad880c522 100644 --- a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts @@ -62,7 +62,7 @@ beforeEach(() => { async function runDoctorWithSessionDevice(device: DeviceInfo): Promise { const sessionStore = makeSessionStore('agent-device-doctor-warmup-'); - sessionStore.set('doctor-session', { + sessionStore.publish('doctor-session', { name: 'doctor-session', createdAt: Date.now(), device, diff --git a/src/daemon/handlers/__tests__/session-install-source-ref-frame.test.ts b/src/daemon/handlers/__tests__/session-install-source-ref-frame.test.ts index 81a4a9cd2d..fc0fcfc6ea 100644 --- a/src/daemon/handlers/__tests__/session-install-source-ref-frame.test.ts +++ b/src/daemon/handlers/__tests__/session-install-source-ref-frame.test.ts @@ -19,7 +19,7 @@ const invoke = async (): Promise => { test('install_source admission failure preserves an active ref frame', async () => { const sessionStore = makeSessionStore('agent-device-session-install-source-ref-admission-'); const session = activeSession(); - sessionStore.set('default', session); + sessionStore.publish('default', session); const inspectFacts = vi.fn(async (device) => { const facts = await mockInspectDeviceRuntimeFacts(device); return { @@ -46,7 +46,7 @@ test('install_source admission failure preserves an active ref frame', async () test('install_source materialization failure preserves an active ref frame', async () => { const sessionStore = makeSessionStore('agent-device-session-install-source-ref-materialization-'); const session = activeSession(); - sessionStore.set('default', session); + sessionStore.publish('default', session); mockMaterializeAppSourceRuntime.mockRejectedValueOnce(new Error('artifact download failed')); const response = await dispatchInstallSource({ sessionStore }); @@ -63,7 +63,7 @@ test('install_source materialization failure preserves an active ref frame', asy test('install_source deploy attempt expires an active ref frame when the bound operation fails', async () => { const sessionStore = makeSessionStore('agent-device-session-install-source-ref-deploy-'); const session = activeSession(); - sessionStore.set('default', session); + sessionStore.publish('default', session); mockDeployMaterializedAppRuntime.mockImplementationOnce(async () => { expect(refFrameState(session)).toBe('expired'); expect(session.snapshotScopeSource).toBeUndefined(); diff --git a/src/daemon/handlers/__tests__/session-prepare.test.ts b/src/daemon/handlers/__tests__/session-prepare.test.ts index 5804f8e8b5..2906f6442f 100644 --- a/src/daemon/handlers/__tests__/session-prepare.test.ts +++ b/src/daemon/handlers/__tests__/session-prepare.test.ts @@ -52,7 +52,7 @@ async function runPrepare(flags: { timeoutMs?: number }): Promise<{ }> { const sessionName = 'prepare-envelope-margin'; const sessionStore = makeSessionStore('agent-device-prepare-handler-'); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { name: sessionName, device: IOS_SIMULATOR, createdAt: Date.now(), diff --git a/src/daemon/handlers/__tests__/session-push-ref-frame.test.ts b/src/daemon/handlers/__tests__/session-push-ref-frame.test.ts index f8576d1661..97409a3a41 100644 --- a/src/daemon/handlers/__tests__/session-push-ref-frame.test.ts +++ b/src/daemon/handlers/__tests__/session-push-ref-frame.test.ts @@ -18,7 +18,7 @@ const invoke = async (): Promise => { test('push admission failure preserves an active ref frame', async () => { const sessionStore = makeSessionStore('agent-device-session-push-ref-admission-'); const session = activeSession(); - sessionStore.set('default', session); + sessionStore.publish('default', session); const inspectFacts = vi.fn(async (device) => { const facts = await mockInspectDeviceRuntimeFacts(device); return { @@ -45,7 +45,7 @@ test('push admission failure preserves an active ref frame', async () => { test('push dispatch attempt expires an active ref frame when the bound operation fails', async () => { const sessionStore = makeSessionStore('agent-device-session-push-ref-dispatch-'); const session = activeSession(); - sessionStore.set('default', session); + sessionStore.publish('default', session); mockPushNotificationRuntime.mockRejectedValueOnce(new Error('provider dispatch failed')); await expect(dispatchPush({ sessionStore })).rejects.toThrow('provider dispatch failed'); diff --git a/src/daemon/handlers/__tests__/session-push.test.ts b/src/daemon/handlers/__tests__/session-push.test.ts index 39ef7e957f..12649806cc 100644 --- a/src/daemon/handlers/__tests__/session-push.test.ts +++ b/src/daemon/handlers/__tests__/session-push.test.ts @@ -49,7 +49,7 @@ test('push requires active session or explicit device selector', async () => { test('push validates payload before runtime facts admission', async () => { const sessionStore = makeSessionStore('agent-device-session-push-invalid-'); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), actions: [], @@ -83,7 +83,7 @@ test('push validates payload before runtime facts admission', async () => { test('push runs readiness and notification through one admitted runtime binding', async () => { const sessionStore = makeSessionStore('agent-device-session-push-runtime-'); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), actions: [], @@ -141,7 +141,7 @@ test('push runs readiness and notification through one admitted runtime binding' test('push treats an existing brace-prefixed payload as a file before inline JSON parsing', async () => { const sessionStore = makeSessionStore('agent-device-session-push-payload-file-'); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), actions: [], @@ -192,7 +192,7 @@ test('push treats an existing brace-prefixed payload as a file before inline JSO test('push stops at unavailable facts without binding', async () => { const sessionStore = makeSessionStore('agent-device-session-push-unsupported-'); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), actions: [], @@ -229,7 +229,7 @@ test('push stops at unavailable facts without binding', async () => { test('push fails closed before binding when a provider owner lacks readiness', async () => { const sessionStore = makeSessionStore('agent-device-session-push-provider-readiness-'); - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: Date.now(), actions: [], diff --git a/src/daemon/handlers/__tests__/session-reinstall.test.ts b/src/daemon/handlers/__tests__/session-reinstall.test.ts index 56a7dbca85..c255a49ad0 100644 --- a/src/daemon/handlers/__tests__/session-reinstall.test.ts +++ b/src/daemon/handlers/__tests__/session-reinstall.test.ts @@ -70,7 +70,7 @@ test('reinstall requires active session or explicit device selector', async () = test('reinstall validates required args before runtime admission', async () => { const store = makeStore(); - store.set( + store.publish( 'default', makeSession('default', { platform: 'apple', @@ -108,7 +108,7 @@ test('install binds the deployment use exactly once after one facts admission', kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const root = mkdtempForTestSync('agent-device-install-runtime-'); const appPath = path.join(root, 'Sample.apk'); fs.writeFileSync(appPath, 'placeholder'); @@ -157,7 +157,7 @@ test('install fails closed before binding when its provider owner lacks deployme kind: 'device', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const root = mkdtempForTestSync('agent-device-install-provider-unavailable-'); const appPath = path.join(root, 'Sample.apk'); fs.writeFileSync(appPath, 'placeholder'); @@ -216,7 +216,7 @@ test('reinstall cleans an uploaded artifact after the request-scoped operation', kind: 'simulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const root = mkdtempForTestSync('agent-device-uploaded-artifact-'); const appPath = path.join(root, 'Sample.app'); fs.writeFileSync(appPath, 'placeholder'); @@ -252,7 +252,7 @@ test('HarmonyOS reinstall updates the active session identity from the runtime r kind: 'emulator', booted: true, }); - store.set(session.name, session); + store.publish(session.name, session); const root = mkdtempForTestSync('agent-device-harmony-reinstall-'); const appPath = path.join(root, 'Sample.hap'); fs.writeFileSync(appPath, 'placeholder'); diff --git a/src/daemon/handlers/__tests__/session-relaunch-close.test.ts b/src/daemon/handlers/__tests__/session-relaunch-close.test.ts index 0ee75ae19a..9af06a3e6f 100644 --- a/src/daemon/handlers/__tests__/session-relaunch-close.test.ts +++ b/src/daemon/handlers/__tests__/session-relaunch-close.test.ts @@ -157,7 +157,7 @@ function sessionRequest( test('open --relaunch closes and reopens active session app', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'android-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -191,7 +191,7 @@ test('open --relaunch closes and reopens active session app', async () => { test('open --relaunch leaves the old frame expired when the close dispatch fails after dispatch (ADR 0014)', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'android-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -283,7 +283,10 @@ test('open --relaunch on physical iOS retains runner through close/open', async kind: 'device' as const, booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, { device, appName: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeSession(sessionName, { device, appName: 'com.example.app' }), + ); const calls: string[] = []; mockResolveTargetDevice.mockResolvedValue(device); @@ -317,7 +320,10 @@ test('open --relaunch on iOS simulator collapses into one terminate-running open kind: 'simulator' as const, booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, { device, appName: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeSession(sessionName, { device, appName: 'com.example.app' }), + ); const calls: string[] = []; mockResolveTargetDevice.mockResolvedValue(device); @@ -354,7 +360,10 @@ test('open --relaunch on iOS simulator delegates termination to the kind: 'simulator' as const, booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, { device, appName: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeSession(sessionName, { device, appName: 'com.example.app' }), + ); const calls: string[] = []; mockResolveTargetDevice.mockResolvedValue(device); @@ -387,7 +396,10 @@ test('open --relaunch --clear-app-state on iOS simulator keeps close-first order kind: 'simulator' as const, booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, { device, appName: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeSession(sessionName, { device, appName: 'com.example.app' }), + ); const calls: string[] = []; mockResolveTargetDevice.mockResolvedValue(device); @@ -420,7 +432,7 @@ test('open --relaunch includes timing and waits for iOS runner prewarm after ope kind: 'device' as const, booted: true, }; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device, appName: 'Example', appBundleId: 'com.example.app' }), ); @@ -501,7 +513,7 @@ test('open --relaunch on iOS without existing session closes then opens target a test('close on macOS session stops runner and dismisses automation alert before delete', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'macos-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -542,7 +554,7 @@ test('close on macOS session stops runner and dismisses automation alert before test('close on iOS simulator session retains runner and deletes the session', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'ios-simulator-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -567,7 +579,7 @@ test('close on iOS simulator session retains runner and deletes the session', as test('close on iOS simulator with scoped simulator set retains runner and deletes the session', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'ios-scoped-simulator-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -600,7 +612,7 @@ test('close on leased iOS simulator session stops runner before deleting session deviceKey: 'ios:sim-1', clientId: 'client-a', }); - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -636,7 +648,7 @@ test('close on leased iOS simulator session stops runner before deleting session test('close --shutdown on iOS simulator stops runner before deleting session', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'ios-simulator-shutdown-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -662,7 +674,7 @@ test('close --shutdown on iOS simulator stops runner before deleting session', a test('close on iOS stops runner before app close dispatch and performs final idempotent stop', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'ios-close-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -699,7 +711,7 @@ test('close on iOS stops runner before app close dispatch and performs fin test('close on iOS simulator retains runner while terminating app', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'ios-simulator-close-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -736,7 +748,7 @@ test('close on iOS simulator retains runner while terminating app', async test('app-only close terminates an iOS simulator app without ending its session', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'ios-simulator-app-only-close'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -775,7 +787,7 @@ test('app-only close terminates an iOS simulator app without ending its session' test('close on macOS stops runner before app close dispatch and dismisses automation alert', async () => { const sessionStore = makeSessionStore('agent-device-relaunch-close-'); const sessionName = 'macos-close-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { diff --git a/src/daemon/handlers/__tests__/session-relaunch-guards.test.ts b/src/daemon/handlers/__tests__/session-relaunch-guards.test.ts index e4ac9574d0..c36126c636 100644 --- a/src/daemon/handlers/__tests__/session-relaunch-guards.test.ts +++ b/src/daemon/handlers/__tests__/session-relaunch-guards.test.ts @@ -115,7 +115,7 @@ test('open --relaunch rejects Android app binary paths for active sessions', asy }); session.appName = 'com.example.app'; session.appBundleId = 'com.example.app'; - sessionStore.set('default', session); + sessionStore.publish('default', session); const response = await handleSessionCommands({ req: { @@ -162,7 +162,7 @@ test('open --relaunch rejects Android app binary paths before resolving a new de test('open on in-use device returns DEVICE_IN_USE before readiness checks', async () => { const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( 'busy-session', makeSession('busy-session', { platform: 'apple', @@ -225,7 +225,7 @@ test('open on device owned by recording session returns recording recovery hint' showTouches: false, recordOnlySession: true, }); - sessionStore.set('default', recordingSession); + sessionStore.publish('default', recordingSession); mockResolveTargetDevice.mockResolvedValue({ platform: 'apple', diff --git a/src/daemon/handlers/__tests__/session-runtime-command.test.ts b/src/daemon/handlers/__tests__/session-runtime-command.test.ts index 32f465ac08..c9c8dfc034 100644 --- a/src/daemon/handlers/__tests__/session-runtime-command.test.ts +++ b/src/daemon/handlers/__tests__/session-runtime-command.test.ts @@ -96,7 +96,7 @@ test('runtime clear removes applied transport hints for the active app', async ( metroHost: '10.0.0.10', metroPort: 8081, }); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'android', id: 'emulator-5554', @@ -146,7 +146,7 @@ test('runtime clear expires the ref frame at the admitted hint mutation boundary }), appBundleId: 'com.example.demo', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); sessionStore.setRuntimeHints(sessionName, { platform: 'android', metroHost: '10.0.2.2', @@ -190,7 +190,7 @@ test('runtime clear rejects a false runtime-hints fact before its one implementa metroHost: '10.0.0.10', metroPort: 8081, }); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, device), appBundleId: 'com.example.demo', }); @@ -247,7 +247,7 @@ test('runtime gesture-viewport admits and binds the exact viewport operation onc mkdtempForTestSync('runtime-gesture-viewport'), 'runtime-gesture-viewport.log', ); - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'android', diff --git a/src/daemon/handlers/__tests__/session-script-publication.test.ts b/src/daemon/handlers/__tests__/session-script-publication.test.ts index 6c4f289358..074e66896c 100644 --- a/src/daemon/handlers/__tests__/session-script-publication.test.ts +++ b/src/daemon/handlers/__tests__/session-script-publication.test.ts @@ -87,7 +87,7 @@ function request(outputPath?: string, force?: boolean): DaemonRequest { test('publishes without close, returns the path/count, and leaves a terminal live session', () => { const outputPath = path.join(root, 'screen-x.ad'); const session = armedSession(); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(outputPath), @@ -120,7 +120,7 @@ test('no-clobber failure preserves bytes and armed state, then --force retries s const outputPath = path.join(root, 'screen-x.ad'); fs.writeFileSync(outputPath, 'original\n'); const session = armedSession(); - store.set('authoring', session); + store.publish('authoring', session); const refused = handleSessionScriptPublication({ req: request(outputPath), @@ -149,7 +149,7 @@ test('retargeting without --force clears force authorization from the previous t const session = armedSession({ scriptPublication: authoringPublication('armed', { path: originalPath, force: true }), }); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(retargetPath), @@ -166,7 +166,7 @@ test('retargeting without --force clears force authorization from the previous t test('refuses unarmed and repair-owned sessions before filesystem work', () => { const outputPath = path.join(root, 'missing', 'screen-x.ad'); - store.set('authoring', makeIosSession('authoring')); + const unarmedRef = store.publish('authoring', makeIosSession('authoring')); const unarmed = handleSessionScriptPublication({ req: request(outputPath), sessionName: 'authoring', @@ -178,7 +178,8 @@ test('refuses unarmed and repair-owned sessions before filesystem work', () => { }); expect(fs.existsSync(path.dirname(outputPath))).toBe(false); - store.set( + store.retire(unarmedRef); + store.publish( 'authoring', armedSession({ scriptPublication: repairPublication('armed', { boundary: 0 }) }), ); @@ -196,7 +197,7 @@ test('refuses unarmed and repair-owned sessions before filesystem work', () => { test('rejects an explicitly empty destination path', () => { const session = armedSession(); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(''), @@ -219,7 +220,7 @@ test('invalid destination guard remains armed and creates no target directory', { ts: 2, command: 'wait', positionals: ['stable'], flags: {} }, ], }); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(outputPath), @@ -247,7 +248,7 @@ test('missing initial open is non-retriable within the armed session', () => { }, ], }); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(outputPath), @@ -295,7 +296,7 @@ test('publishes leading-at typed values without mistaking them for session refs' }, ], }); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(outputPath), @@ -330,7 +331,7 @@ test('refuses mutating find steps that cannot enforce target identity on replay' }, ], }); - store.set('authoring', session); + store.publish('authoring', session); const response = handleSessionScriptPublication({ req: request(outputPath), diff --git a/src/daemon/handlers/__tests__/session-selector-dispatch.test.ts b/src/daemon/handlers/__tests__/session-selector-dispatch.test.ts index cfdf9d8fd2..3b8750fc95 100644 --- a/src/daemon/handlers/__tests__/session-selector-dispatch.test.ts +++ b/src/daemon/handlers/__tests__/session-selector-dispatch.test.ts @@ -173,7 +173,7 @@ test('keyboard dismiss crosses the ADR 0014 seam while keyboard status preserves const { inspectFacts, bindDevice } = keyboardCapableRuntime(device); // dismiss mutates the device → frame expires. - sessionStore.set(sessionName, makeSession(sessionName, device)); + const dismissRef = sessionStore.publish(sessionName, makeSession(sessionName, device)); await handleSessionCommands({ req: { token: 't', @@ -192,7 +192,8 @@ test('keyboard dismiss crosses the ADR 0014 seam while keyboard status preserves expect(refFrameState(sessionStore.get(sessionName)!)).toBe('expired'); // status is a read-only probe → frame preserved (undefined === active). - sessionStore.set(sessionName, makeSession(sessionName, device)); + sessionStore.retire(dismissRef); + sessionStore.publish(sessionName, makeSession(sessionName, device)); await handleSessionCommands({ req: { token: 't', @@ -234,7 +235,7 @@ test('keyboard dismiss expires the frame before the invocation runs, even when i }, }); - sessionStore.set(sessionName, makeSession(sessionName, device)); + sessionStore.publish(sessionName, makeSession(sessionName, device)); await expect( handleSessionCommands({ req: { diff --git a/src/daemon/handlers/__tests__/snapshot-alert.test.ts b/src/daemon/handlers/__tests__/snapshot-alert.test.ts index c472de676b..8fe7a1fc4e 100644 --- a/src/daemon/handlers/__tests__/snapshot-alert.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-alert.test.ts @@ -61,7 +61,7 @@ beforeEach(() => { test('alert accept retries a typed alert absence and succeeds on the second attempt', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); let calls = 0; mockRunnerCommand.mockImplementation(async () => { @@ -85,7 +85,7 @@ test('alert accept retries a typed alert absence and succeeds on the second atte test('alert accept adds a scoped-snapshot hint after retrying alert-not-found failures', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); mockRunnerCommand.mockRejectedValue(alertAbsence()); let thrown: unknown; @@ -103,7 +103,7 @@ test('alert accept adds a scoped-snapshot hint after retrying alert-not-found fa test('alert dismiss retries a typed absence whatever the message says', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); let calls = 0; mockRunnerCommand.mockImplementation(async () => { diff --git a/src/daemon/handlers/__tests__/snapshot-handler-freshness.test.ts b/src/daemon/handlers/__tests__/snapshot-handler-freshness.test.ts index b9b340b0fe..7e709aa4bb 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler-freshness.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler-freshness.test.ts @@ -93,7 +93,7 @@ function makeAndroidTimeoutEvidenceSession(sessionName: string): SessionStore { createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); return sessionStore; } @@ -165,7 +165,7 @@ test('snapshot annotations survive a deferred post-gesture capture into CLI JSON const snapshotQuality = { state: 'healthy', backend: 'tree' }; session.snapshot = { nodes: baselineNodes, createdAt: Date.now(), backend: 'android' }; session.postGestureStabilization = { action: 'swipe', positionals: [], markedAt: Date.now() }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: changedNodes, @@ -226,7 +226,7 @@ test('snapshot warns when recent snapshot node count collapses sharply', async ( createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue( androidCapture( @@ -267,7 +267,7 @@ test('snapshot does not warn on expected node drop across presentation modes', a backend: 'xctest', presentationKey: buildSnapshotPresentationKey({ interactiveOnly: false }), }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: Array.from({ length: 8 }, (_, index) => ({ @@ -302,7 +302,7 @@ test('snapshot automatically retries stale Android trees after recent navigation const sessionStore = makeSessionStore(); const sessionName = 'android-stale-retries-to-fresh'; const session = makeAndroidFreshnessSession(sessionName, 'press', inboxBaselineNodes(24)); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture .mockResolvedValueOnce( @@ -340,7 +340,7 @@ test('snapshot warns when Android freshness retries still return the previous ro const sessionStore = makeSessionStore(); const sessionName = 'android-stale-after-press'; const session = makeAndroidFreshnessSession(sessionName, 'press', inboxBaselineNodes(24)); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue( androidCapture(androidTextRows(24, inboxRow), { rawNodeCount: 24, maxDepth: 2 }), @@ -367,7 +367,7 @@ test('snapshot warns when Android freshness retries still return the previous ro test('snapshot response includes normalized visibility metadata', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-visibility'; - sessionStore.set(sessionName, makeSession(sessionName, androidDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, androidDevice)); legacyDispatchCapture.mockResolvedValue({ nodes: [ @@ -416,7 +416,7 @@ test('diff snapshot carries stale-tree warnings for recent Android presses', asy const sessionStore = makeSessionStore(); const sessionName = 'android-diff-stale-after-press'; const session = makeAndroidFreshnessSession(sessionName, 'press', inboxBaselineNodes(24)); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue( androidCapture(androidTextRows(24, inboxRow), { rawNodeCount: 24, maxDepth: 2 }), @@ -451,7 +451,7 @@ test('Android ref refresh mode does not retry narrow snapshots as sharp drops', 'press', buildNodes(androidTextRows(50, (row) => `Previous row ${row}`)), ); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue( androidCapture( diff --git a/src/daemon/handlers/__tests__/snapshot-handler-wait.test.ts b/src/daemon/handlers/__tests__/snapshot-handler-wait.test.ts index 809b64c98a..990bb1ffd2 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler-wait.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler-wait.test.ts @@ -82,7 +82,7 @@ async function runWaitCommand( positionals: string[], ) { const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, device, appAttach(device))); + sessionStore.publish(sessionName, makeSession(sessionName, device, appAttach(device))); return await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'wait', { positionals }), sessionName, @@ -177,7 +177,7 @@ test('wait text on Android uses freshness-aware capture instead of one-shot snap const sessionStore = makeSessionStore(); const sessionName = 'android-wait-freshness'; const session = makeAndroidFreshnessSession(sessionName, 'press', inboxBaselineNodes(18)); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture .mockResolvedValueOnce( @@ -359,7 +359,7 @@ test('wait selector bypasses a fresh matching session snapshot', async () => { }, ], }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: [ { diff --git a/src/daemon/handlers/__tests__/snapshot-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-handler.test.ts index fada858f56..255eb439a5 100644 --- a/src/daemon/handlers/__tests__/snapshot-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-handler.test.ts @@ -69,7 +69,7 @@ beforeEach(() => { test('snapshot rejects @ref scope without existing session snapshot', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -98,7 +98,7 @@ test('snapshot rejects @ref scope without existing session snapshot', async () = test('snapshot on iOS rejects sessions without a tracked app', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim-no-app'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const runtime = countingSnapshotRuntime(); const response = await handleSnapshotCommands({ @@ -128,7 +128,7 @@ test('snapshot on iOS without a tracked app carries the detected open command as ); const sessionStore = makeSessionStore(); const sessionName = 'ios-sim-no-app-hinted'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'snapshot'), @@ -155,7 +155,7 @@ test('snapshot on iOS without a tracked app carries the detected open command as test('snapshot on provider-backed iOS runs without a tracked app', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-cloud-no-app'; - sessionStore.set(sessionName, makeSession(sessionName, providerIosDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, providerIosDevice)); setActiveProviderDeviceRuntimes([makeProviderRuntimeOwning(providerIosDevice)]); legacyDispatchCapture.mockResolvedValue({ nodes: [{ index: 0, depth: 0, type: 'XCUIElementTypeButton', label: 'Sign in' }], @@ -185,7 +185,7 @@ test('snapshot on provider-backed iOS runs without a tracked app', async () => { test('diff on local iOS still requires a tracked app', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim-no-app-diff'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'diff', { positionals: ['snapshot'] }), @@ -205,7 +205,7 @@ test('diff on local iOS still requires a tracked app', async () => { test('snapshot on iOS runs when the session tracks an app', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-sim-app'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, iosSimulatorDevice), appBundleId: 'org.reactnavigation.playground', }); @@ -243,7 +243,7 @@ test('snapshot re-activates a complete frame; diff preserves it (ADR 0014)', asy }; // A prior device action expired the frame. expireRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: [{ index: 0, depth: 0, type: 'android.widget.Button', label: 'Fresh' }], truncated: false, @@ -295,7 +295,7 @@ async function runVersionedRefsCommand(params: { function makeVersionedRefsScenario(sessionName: string) { const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, androidDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, androidDevice)); legacyDispatchCapture.mockResolvedValue({ nodes: [{ index: 0, depth: 0, type: 'android.widget.Button', label: 'Fresh' }], truncated: false, @@ -384,7 +384,7 @@ test('daemon-private snapshot observation advances capture state without publish test('snapshot surfaces filtered-to-zero Android guidance for interactive snapshots', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-empty-interactive'; - sessionStore.set(sessionName, makeSession(sessionName, androidDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, androidDevice)); legacyDispatchCapture.mockResolvedValue(androidCapture([], { rawNodeCount: 42, maxDepth: 8 })); diff --git a/src/daemon/handlers/__tests__/snapshot-scoped-refs.test.ts b/src/daemon/handlers/__tests__/snapshot-scoped-refs.test.ts index 57ca82d23a..ccd14733c9 100644 --- a/src/daemon/handlers/__tests__/snapshot-scoped-refs.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-scoped-refs.test.ts @@ -18,7 +18,7 @@ test('snapshot resolves @ref scope with the stored source after scoped output re const sessionStore = makeSessionStore('agent-device-snapshot-scoped-refs-'); const sessionName = 'android-ref-scope-repeat'; const session = makeAndroidSession(sessionName, { snapshot: androidRefScopeSourceSnapshot() }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: scopedScriptErrorNodes(), @@ -46,7 +46,7 @@ test('a mutation clears scoped-snapshot lineage so a repeated snapshot -s @ref c const sessionStore = makeSessionStore('agent-device-snapshot-scoped-refs-'); const sessionName = 'android-ref-scope-broken-by-mutation'; const session = makeAndroidSession(sessionName, { snapshot: androidRefScopeSourceSnapshot() }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: scopedScriptErrorNodes(), @@ -75,7 +75,7 @@ test('empty @ref-scoped snapshot output does not replace the stored session snap const sessionStore = makeSessionStore('agent-device-snapshot-scoped-refs-'); const sessionName = 'android-empty-scope-preserve'; const session = makeAndroidSession(sessionName, { snapshot: currentScreenSnapshot() }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ nodes: [], diff --git a/src/daemon/handlers/__tests__/snapshot-settings-handler.test.ts b/src/daemon/handlers/__tests__/snapshot-settings-handler.test.ts index 12f0e4c36f..4415941789 100644 --- a/src/daemon/handlers/__tests__/snapshot-settings-handler.test.ts +++ b/src/daemon/handlers/__tests__/snapshot-settings-handler.test.ts @@ -80,7 +80,7 @@ beforeEach(() => { test('settings rejects unsupported iOS physical devices', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-device'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -109,7 +109,7 @@ test('settings rejects unsupported iOS physical devices', async () => { test('settings clear-app-state dispatches explicit app id without an active app session', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-clear-state'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { @@ -131,7 +131,7 @@ test('settings clear-app-state dispatches explicit app id without an active app test('settings clear-app-state rejects missing app id when no app session is bound', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-clear-state-missing-app'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { positionals: ['clear-app-state'] }), @@ -151,7 +151,7 @@ test('settings clear-app-state rejects missing app id when no app session is bou test('settings reset-keychain dispatches without an app id or active app session', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-reset-keychain'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { @@ -172,7 +172,7 @@ test('settings reset-keychain dispatches without an app id or active app session test('settings reset-keychain rejects an extra app argument instead of dropping it', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-reset-keychain-extra-arg'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { @@ -193,7 +193,7 @@ test('settings reset-keychain rejects an extra app argument instead of dropping test('settings text-size reads the category the owner holds without mutating anything', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-text-size-read'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { positionals: ['text-size'] }), @@ -216,7 +216,7 @@ test('settings text-size reads the category the owner holds without mutating any test('settings text-size refuses the macOS host on both legs with the same code', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-text-size-read'; - sessionStore.set(sessionName, makeSession(sessionName, macOsDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, macOsDevice)); const read = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { positionals: ['text-size'] }), @@ -247,7 +247,7 @@ test('settings text-size applies a ladder category through the write leg', async const sessionName = 'ios-text-size-write'; const session = makeSession(sessionName, iosSimulatorDevice); activateCompleteRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { @@ -278,7 +278,7 @@ test('settings text-size refuses an Apple leaf with no content size before it ex const sessionName = 'tvos-text-size'; const session = makeSession(sessionName, tvOsSimulatorDevice); activateCompleteRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); for (const positionals of [['text-size'], ['text-size', 'large']]) { const response = await handleSnapshotCommands({ @@ -304,7 +304,7 @@ test('settings text-size refuses an Apple leaf with no content size before it ex test('settings text-size refuses an off-ladder category with the whole ladder', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-text-size-invalid'; - sessionStore.set(sessionName, makeSession(sessionName, iosSimulatorDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, iosSimulatorDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { positionals: ['text-size', 'gigantic'] }), @@ -349,7 +349,7 @@ test('settings usage hint documents canonical faceid states', async () => { test('settings on macOS rejects wifi before dispatch with explicit subset guidance', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-settings-wifi'; - sessionStore.set(sessionName, makeSession(sessionName, macOsDevice)); + sessionStore.publish(sessionName, makeSession(sessionName, macOsDevice)); const response = await handleSnapshotCommands({ req: snapshotRequest(sessionName, 'settings', { positionals: ['wifi', 'on'] }), diff --git a/src/daemon/handlers/__tests__/trace-runtime.test.ts b/src/daemon/handlers/__tests__/trace-runtime.test.ts index d06b58c472..2966e5bac0 100644 --- a/src/daemon/handlers/__tests__/trace-runtime.test.ts +++ b/src/daemon/handlers/__tests__/trace-runtime.test.ts @@ -22,7 +22,7 @@ function fixture() { createdAt: Date.now(), actions: [], }; - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); return { root, session, sessionStore }; } diff --git a/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts b/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts index fca72e152e..5b8124c022 100644 --- a/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts +++ b/src/daemon/handlers/__tests__/wait-landmark-recording.test.ts @@ -89,7 +89,7 @@ async function runWait(options: { recording?: boolean; req?: DaemonRequest } = { 'default', options.recording ? { scriptPublication: authoringPublication('armed') } : {}, ); - sessionStore.set('default', session); + sessionStore.publish('default', session); const response = await dispatchWaitViaRuntime({ req: options.req ?? waitReq(), sessionName: 'default', diff --git a/src/daemon/interaction/internal/__tests__/find-production-route.test.ts b/src/daemon/interaction/internal/__tests__/find-production-route.test.ts index dd39762ca3..35adfffcda 100644 --- a/src/daemon/interaction/internal/__tests__/find-production-route.test.ts +++ b/src/daemon/interaction/internal/__tests__/find-production-route.test.ts @@ -93,7 +93,7 @@ async function runFindThroughLeaf(options: { const sessionStore = makeSessionStore(); const sessionName = 'default'; const session = makeSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockDispatch.mockImplementation(async (_device, command) => command === 'snapshot' ? { nodes: freshFindTree(), backend: 'xctest' } : {}, @@ -110,7 +110,6 @@ async function runFindThroughLeaf(options: { const current = sessionStore.get(sessionName); if (current) { current.snapshot = divergedSessionTree(); - sessionStore.set(sessionName, current); } } // The real leaf, not a stub. diff --git a/src/daemon/interaction/internal/__tests__/find-read-only.test.ts b/src/daemon/interaction/internal/__tests__/find-read-only.test.ts index 49e2fd8dd1..dad3650ff6 100644 --- a/src/daemon/interaction/internal/__tests__/find-read-only.test.ts +++ b/src/daemon/interaction/internal/__tests__/find-read-only.test.ts @@ -175,7 +175,7 @@ test('read-only find while recording is intentionally deferred from target-v1 ev const sessionStore = makeSessionStore(); const sessionName = 'default'; const session = makeAuthoringSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockDispatch.mockImplementation(async (_device, command) => { if (command === 'snapshot') { return { diff --git a/src/daemon/interaction/internal/__tests__/find-single-bind.test.ts b/src/daemon/interaction/internal/__tests__/find-single-bind.test.ts index 51f4707d27..1acf192fae 100644 --- a/src/daemon/interaction/internal/__tests__/find-single-bind.test.ts +++ b/src/daemon/interaction/internal/__tests__/find-single-bind.test.ts @@ -25,7 +25,7 @@ async function runMutatingFind(positionals: string[], node: Record command === 'snapshot' ? { nodes: [node] } : {}, ); diff --git a/src/daemon/interaction/internal/__tests__/find-target-activation-disclosure.test.ts b/src/daemon/interaction/internal/__tests__/find-target-activation-disclosure.test.ts index c795e37258..2ed86a118b 100644 --- a/src/daemon/interaction/internal/__tests__/find-target-activation-disclosure.test.ts +++ b/src/daemon/interaction/internal/__tests__/find-target-activation-disclosure.test.ts @@ -80,7 +80,7 @@ async function findClick(captures: Record[] | CaptureScript, af const session = makeIosSession('default', { appBundleId: 'com.example.app' }); if (afterScroll) markDeferredInteractionOutcome({ session, command: 'scroll', positionals: [], flags: {} }); - sessionStore.set('default', session); + sessionStore.publish('default', session); let call = 0; legacyDispatchCapture.mockImplementation( async (_device, _command, _positionals, _out, context) => diff --git a/src/daemon/interaction/internal/__tests__/find-touch-runtime-fixture.ts b/src/daemon/interaction/internal/__tests__/find-touch-runtime-fixture.ts index 987c6e70f5..fc71d14823 100644 --- a/src/daemon/interaction/internal/__tests__/find-touch-runtime-fixture.ts +++ b/src/daemon/interaction/internal/__tests__/find-touch-runtime-fixture.ts @@ -70,7 +70,7 @@ export async function runFindClickScenario(options: { const sessionStore = makeSessionStore(); const sessionName = 'default'; const session = options.session ?? makeSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); if (options.nodes !== undefined) { mockDispatch.mockImplementation(async (_device, command) => { diff --git a/src/daemon/interaction/internal/__tests__/interaction-capture-disclosure.test.ts b/src/daemon/interaction/internal/__tests__/interaction-capture-disclosure.test.ts index 03c0e94f7d..93b7160a79 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-capture-disclosure.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-capture-disclosure.test.ts @@ -55,7 +55,7 @@ async function pressSelector(params: { captureCalls: { count: number }; }) { const sessionStore = makeSessionStore(); - sessionStore.set(params.sessionName, makeSession(params.sessionName)); + sessionStore.publish(params.sessionName, makeSession(params.sessionName)); const response = await handleInteractionCommands({ req: { token: 't', @@ -106,7 +106,7 @@ test('a press that consumes no capture is not disclosed against an older tree', const sessionStore = makeSessionStore(); const session = makeSession(sessionName); session.snapshot = capturedTree({ sessionName, targetActivation: FACT }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { @@ -144,7 +144,7 @@ async function pressAfterUnsettledScroll(selector: string) { const sessionStore = makeSessionStore(); const session = makeSession('default'); markDeferredInteractionOutcome({ session, command: 'scroll', positionals: ['down'], flags: {} }); - sessionStore.set('default', session); + sessionStore.publish('default', session); let call = 0; legacyDispatchCapture.mockImplementation(async () => { call += 1; diff --git a/src/daemon/interaction/internal/__tests__/interaction-common.test.ts b/src/daemon/interaction/internal/__tests__/interaction-common.test.ts index 9ee05da84a..3835266ac0 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-common.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-common.test.ts @@ -38,7 +38,7 @@ test('parameterized fill scrubs backend and nested settle echoes at the response const placeholder = '${PASSWORD}'; const sessionStore = makeSessionStore(); const session = makeAuthoringSession('parameterized-fill'); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const payload = { text: secret, message: `prefix${secret}suffix`, @@ -148,7 +148,7 @@ test('parameterized fill scrubs concatenated backend values and object keys thro createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockImplementation(async (_device, command) => command === 'fill' ? { @@ -209,7 +209,7 @@ test('parameterized fill collapses whitespace-only backend echoes through the ha createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue({ [`prefix${secret}suffix`]: { [`key${secret}tail`]: `value${secret}tail`, @@ -255,7 +255,7 @@ test.each([ const placeholder = `\${${recordAs}}`; const sessionStore = makeSessionStore(); const session = makeAuthoringSession(`parameterized-overlap-${recordAs}`); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const payload = { text: literal, refLabel: `prefix${literal}suffix`, diff --git a/src/daemon/interaction/internal/__tests__/interaction-dispatch-disclosure.test.ts b/src/daemon/interaction/internal/__tests__/interaction-dispatch-disclosure.test.ts index 70092d2dff..8e11181547 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-dispatch-disclosure.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-dispatch-disclosure.test.ts @@ -87,7 +87,7 @@ async function press({ createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const response = await routeInteraction({ req: { token: 't', session: session.name, command, positionals, flags }, sessionName: session.name, @@ -184,7 +184,7 @@ async function swipeRefusedOnSecondRepetition(): Promise { } const sessionStore = makeSessionStore(); const session = makeSession('dispatch-disclosure-swipe'); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const response = await routeInteraction({ req: { token: 't', @@ -216,7 +216,7 @@ async function pressThenForegroundReadRefused(): Promise { appBundleId: 'com.example.app', }); const sessionStore = makeSessionStore(); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const readRefusal = new AppError('COMMAND_FAILED', 'adb device offline', { dispatched: 'no' }); const androidObservation: AndroidObservationAdapter = { ...clearAndroidObservationFixture, @@ -325,7 +325,7 @@ test('a read-only command discloses no over a producer verdict it throws', async test('a plain Error a backend throws reaches the wire with dispatched unknown', async () => { const sessionStore = makeSessionStore(); const session = makeSession('dispatch-disclosure-plain-error'); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const bindings = getRuntimeBindings(); const bindDevice = vi.fn(async () => { throw new Error('socket hang up'); diff --git a/src/daemon/interaction/internal/__tests__/interaction-gesture-drag.test.ts b/src/daemon/interaction/internal/__tests__/interaction-gesture-drag.test.ts index 593cffce83..79bc1e2b24 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-gesture-drag.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-gesture-drag.test.ts @@ -89,7 +89,7 @@ test('recorded ref drag dispatches once and stores portable selectors with both const sessionStore = makeSessionStore(); const sessionName = 'recorded-drag'; const session = makeDragSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await runDrag(sessionStore, sessionName); @@ -140,7 +140,7 @@ test('recorded ref drag dispatches once and stores portable selectors with both test('a second ref drag is rejected before dispatch after the first drag expires the frame', async () => { const sessionStore = makeSessionStore(); const sessionName = 'stale-drag'; - sessionStore.set(sessionName, makeDragSession(sessionName)); + sessionStore.publish(sessionName, makeDragSession(sessionName)); expect((await runDrag(sessionStore, sessionName))?.ok).toBe(true); const response = await runDrag(sessionStore, sessionName); diff --git a/src/daemon/interaction/internal/__tests__/interaction-get.test.ts b/src/daemon/interaction/internal/__tests__/interaction-get.test.ts index 0b287b4cf7..9b35dba6fc 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-get.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-get.test.ts @@ -92,7 +92,7 @@ test('get text prefers underlying value for text surfaces and avoids recording g backend: 'xctest', producer: 'apple-runner', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockRejectedValue( new Error('dispatch should not be called for snapshot-derived get text'), @@ -143,7 +143,7 @@ test('get text uses backend read expansion when the resolved node has a rect', a backend: 'xctest', producer: 'apple-runner', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockReadTextAtPoint.mockResolvedValue( elementTextRead('package com.example.app\nclass MainActivity {}'), @@ -192,7 +192,7 @@ test('get text answers from the captured tree when the bound owner advertises no backend: 'xctest', producer: 'apple-runner', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); elementReadFixtureState.readTextAtPointAvailable = false; const response = await handleInteractionCommands({ @@ -226,7 +226,10 @@ test('get text answers from the captured tree when the bound owner advertises no test('an eligible direct iOS selector cannot operate before admission', async () => { const sessionStore = makeSessionStore(); const sessionName = 'get-text-direct-before-admission'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); elementReadFixtureState.captureSnapshotAvailable = false; mockRunAppleRunnerCommand.mockResolvedValue({ found: true, @@ -262,7 +265,10 @@ test('an eligible direct iOS selector cannot operate before admission', async () test('get text simple iOS id selector resolves through the bound capture, not a runner query', async () => { const sessionStore = makeSessionStore(); const sessionName = 'get-text-ios-direct-selector'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); legacyDispatchCapture.mockResolvedValue({ backend: 'xctest', producer: 'apple-runner', @@ -320,7 +326,10 @@ test('get text simple iOS id selector resolves through the bound capture, not a test('get text iOS label selector uses snapshot disambiguation instead of runner query', async () => { const sessionStore = makeSessionStore(); const sessionName = 'get-text-ios-label-selector'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); legacyDispatchCapture.mockResolvedValue({ backend: 'xctest', producer: 'apple-runner', @@ -393,7 +402,7 @@ test('get text iOS label selector uses snapshot disambiguation instead of runner test('is visible preserves CLI snapshot flags during runtime snapshot capture', async () => { const sessionStore = makeSessionStore(); const sessionName = 'snapshot-flags'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command !== 'snapshot') throw new Error(`unexpected command: ${command}`); @@ -456,7 +465,7 @@ test('is visible reuses fresh cached iOS snapshots with rects', async () => { backend: 'xctest', producer: 'apple-runner', }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockRejectedValue(new Error('unexpected fresh snapshot')); const response = await handleInteractionCommands({ @@ -490,7 +499,7 @@ test('is visible recaptures web snapshots when cached nodes may lack rects', asy }, { snapshotInteractiveOnly: false }, ); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockResolvedValue( makeVisibleButtonSnapshot('Submit order', { backend: 'web', @@ -528,7 +537,10 @@ test('is visible recaptures web snapshots when cached nodes may lack rects', asy test('a failing is predicate is COMMAND_FAILED, never a zero-exit pass', async () => { const sessionStore = makeSessionStore(); const sessionName = 'is-selected-ios-direct-selector-false'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockRunAppleRunnerCommand.mockResolvedValue({ found: true, nodes: [ @@ -570,7 +582,7 @@ test('a failing is predicate is COMMAND_FAILED, never a zero-exit pass', async ( test('is visible passes for list text that inherits viewport visibility from an ancestor', async () => { const sessionStore = makeSessionStore(); const sessionName = 'visible-list-item'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command !== 'snapshot') throw new Error(`unexpected command: ${command}`); @@ -623,7 +635,7 @@ test('is visible passes for list text that inherits viewport visibility from an test('is visible fails for nodes outside the current viewport', async () => { const sessionStore = makeSessionStore(); const sessionName = 'visible-offscreen'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command !== 'snapshot') throw new Error(`unexpected command: ${command}`); @@ -669,7 +681,7 @@ test('is visible fails for nodes outside the current viewport', async () => { test('is reports Android permission dialog blocker when app content assertion fails', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-permission-blocked'; - sessionStore.set( + sessionStore.publish( sessionName, makeBaseAndroidSession(sessionName, { appBundleId: 'com.example.demo' }), ); @@ -734,7 +746,7 @@ test('ADR 0014 evidence #17: get text @ref reads the retained frame tree, not a backend: 'xctest', producer: 'apple-runner', }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockRejectedValue(new Error('get text @ref must not recapture')); // Resolves against the frame tree's @e2 (Continue), never the observation's @@ -764,7 +776,7 @@ test('get text @ref warns while the frame is expired (retained evidence still re // A device action expired the frame; the read still resolves against the // retained frame tree and stays fail-open with a warning (ADR 0014). expireRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockRejectedValue( new Error('dispatch should not be called for snapshot-derived get text'), ); @@ -782,7 +794,7 @@ test('get text with a pinned stale ref gets the precise warning', async () => { const sessionName = 'pinned-get-text'; const session = makeStaleRefSession(sessionName); session.snapshotGeneration = 4; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockRejectedValue( new Error('dispatch should not be called for snapshot-derived get text'), ); diff --git a/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts b/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts index 30ed2bf4b5..95827268d6 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-ios-tap-outcome.test.ts @@ -109,7 +109,7 @@ test('a changed post-action capture corroborates an iOS tap reported as failed', appBundleId: 'com.example.app', snapshot: snapshot(profileNodes), }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command === 'press') { throw new AppError( @@ -139,7 +139,7 @@ test('an unchanged post-action capture keeps a failed iOS tap failed', async () appBundleId: 'com.example.app', snapshot: snapshot(profileNodes), }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command === 'press') { throw new AppError( @@ -165,7 +165,7 @@ test('a private-ax baseline pins the corroboration probe to private-ax', async ( // correctly refuses to compare, and a landed tap surfaces as a failure. const sessionName = 'ios-private-ax-pinned-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -206,7 +206,7 @@ test('a canonical selector capture replaces a raw baseline before corroboration' // capture at all) and the recorded failure surfaces unchanged. const sessionName = 'ios-raw-baseline-no-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -233,7 +233,7 @@ test('a canonical selector capture replaces a raw baseline before corroboration' test('a tree baseline does not pin the corroboration probe backend', async () => { const sessionName = 'ios-tree-baseline-unpinned-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -267,7 +267,7 @@ test('a tree baseline does not pin the corroboration probe backend', async () => test('a changed capture from a different iOS backend keeps the tap failure', async () => { const sessionName = 'ios-cross-backend-tap-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -301,7 +301,7 @@ test('a producer or generation switch cannot corroborate a failed tap', async () appBundleId: 'com.example.app', snapshot: baseline, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const after = snapshot(imageViewerNodes); after.comparisonKey = 'apple-runner:launch-a'; @@ -332,7 +332,7 @@ test('a capture of a system surface cannot corroborate a tap taken against the a appBundleId: 'com.example.app', snapshot: baseline, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const after = snapshot(imageViewerNodes); after.comparisonKey = systemSurfaceCaptureComparisonKey(IOS_SIMULATOR.id); after.iosSystemSurfaceBundleId = 'com.apple.SafariViewService'; @@ -354,7 +354,7 @@ test('a capture of a system surface cannot corroborate a tap taken against the a test('a sparse changed capture keeps the tap failure', async () => { const sessionName = 'ios-sparse-tap-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -387,7 +387,7 @@ test('a sparse changed capture keeps the tap failure', async () => { test('a corroboration capture failure keeps the tap failure', async () => { const sessionName = 'ios-capture-failed-tap-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -417,7 +417,7 @@ test('the canonical selector capture aligns presentation before corroboration', const sessionStore = makeSessionStore(); const baseline = snapshot(profileNodes); baseline.presentationKey = 'unreadable-presentation'; - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -446,7 +446,7 @@ test('corroborates a tap when the request carries no flags and the baseline used const sessionStore = makeSessionStore(); const baseline = snapshot(profileNodes); baseline.presentationKey = buildSnapshotPresentationKey({ depth: 2, raw: false }); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -497,7 +497,7 @@ test('a changed capture after ordinary agent turn latency still corroborates the const sessionStore = makeSessionStore(); const baseline = snapshot(profileNodes); baseline.createdAt = Date.now() - 6_000; - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -530,7 +530,7 @@ test('the canonical selector capture replaces a stale baseline before corroborat const sessionStore = makeSessionStore(); const baseline = snapshot(profileNodes); baseline.createdAt = Date.now() - 15_001; - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -560,7 +560,7 @@ test('the canonical selector capture replaces a keyless baseline before corrobor const sessionStore = makeSessionStore(); const baseline = snapshot(profileNodes); baseline.presentationKey = undefined; - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -588,7 +588,7 @@ test('the canonical selector capture replaces a keyless baseline before corrobor test('runtime-resolved taps use the same corroboration boundary', async () => { const sessionName = 'ios-runtime-tap-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -622,7 +622,7 @@ test('runtime-resolved taps use the same corroboration boundary', async () => { test('runtime coordinate taps use the same corroboration boundary', async () => { const sessionName = 'ios-runtime-coordinate-corroboration'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -654,7 +654,7 @@ test('corroborated runtime taps retain target evidence through save and replay', const root = mkdtempForTestSync('agent-device-ios-tap-replay-'); const sessionName = 'ios-recorded-tap'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app', @@ -707,7 +707,7 @@ test('corroborated runtime taps retain target evidence through save and replay', recording = false; const replaySessionName = 'ios-replayed-tap'; const replayStore = makeSessionStore(); - replayStore.set( + replayStore.publish( replaySessionName, makeIosSession(replaySessionName, { appBundleId: 'com.example.app', diff --git a/src/daemon/interaction/internal/__tests__/interaction-settle-private-ax-route.test.ts b/src/daemon/interaction/internal/__tests__/interaction-settle-private-ax-route.test.ts index f1846e7c16..97e062f404 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-settle-private-ax-route.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-settle-private-ax-route.test.ts @@ -61,7 +61,7 @@ beforeEach(() => { test('daemon press --settle pins private-ax on emitted snapshot runner requests', async () => { const sessionName = 'press-settle-private-ax-route'; const sessionStore = makeSessionStore(); - sessionStore.set( + sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.fixture', diff --git a/src/daemon/interaction/internal/__tests__/interaction-settle.test.ts b/src/daemon/interaction/internal/__tests__/interaction-settle.test.ts index 48cc20728e..a8b8e4a7a1 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-settle.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-settle.test.ts @@ -97,7 +97,7 @@ function seedSession(sessionName: string, sessionStore: ReturnType { const sessionStore = makeSessionStore(); const sessionName = 'recording-press'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); // verify:true forces full runtime resolution, which captures node/tree. const response = await runCommand(sessionStore, sessionName, 'press', ['@e1'], { verify: true }); @@ -131,7 +131,7 @@ test('press @ref while recording attaches target-v1 evidence to the recorded act test('press @ref without recording never computes target-v1 evidence', async () => { const sessionStore = makeSessionStore(); const sessionName = 'non-recording-press'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: false })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: false })); const response = await runCommand(sessionStore, sessionName, 'press', ['@e1'], { verify: true }); @@ -145,7 +145,7 @@ test('press @ref without recording never computes target-v1 evidence', async () test('get text @ref while recording attaches target-v1 evidence to the recorded action, never to session history payloads', async () => { const sessionStore = makeSessionStore(); const sessionName = 'recording-get-ref'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); const response = await runCommand(sessionStore, sessionName, 'get', ['text', '@e1']); @@ -173,7 +173,7 @@ test('get text @ref while recording attaches target-v1 evidence to the recorded test('get text @ref without recording never computes target-v1 evidence', async () => { const sessionStore = makeSessionStore(); const sessionName = 'non-recording-get-ref'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: false })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: false })); const response = await runCommand(sessionStore, sessionName, 'get', ['text', '@e1']); @@ -186,7 +186,7 @@ test('get text simple iOS id selector while recording skips the direct runner qu const sessionStore = makeSessionStore(); const sessionName = 'recording-get-direct-gate'; const session = makeAuthoringSession(sessionName, { appBundleId: 'com.example.app' }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command === 'snapshot') { return { @@ -325,7 +325,7 @@ test('press on an identity-empty container: container-based daemon response, des // recording) without Android's real-adb dialog-readiness probes, which // would burn wall-clock time this unit lane must not spend. const session = makeAuthoringSession(sessionName, { appBundleId: 'com.example.app' }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); legacyDispatchCapture.mockImplementation(async (_device, command) => { if (command === 'snapshot') { return { backend: 'xctest', nodes: IDENTITY_EMPTY_ROW_NODES }; @@ -370,7 +370,7 @@ function mockSnapshotWithSaveButton() { test('is visible while recording attaches target-v1 evidence and strips the resolution payload everywhere', async () => { const sessionStore = makeSessionStore(); const sessionName = 'recording-is'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); mockSnapshotWithSaveButton(); const response = await runCommand(sessionStore, sessionName, 'is', ['visible', 'label="Save"']); @@ -402,7 +402,7 @@ test('is visible while recording attaches target-v1 evidence and strips the reso test('is exists while recording stays intentionally unannotated (deferred coverage)', async () => { const sessionStore = makeSessionStore(); const sessionName = 'recording-is-exists'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); mockSnapshotWithSaveButton(); const response = await runCommand(sessionStore, sessionName, 'is', ['exists', 'label="Save"']); @@ -416,7 +416,7 @@ test('is exists while recording stays intentionally unannotated (deferred covera test('is absent while recording stays an ordinary unannotated observation', async () => { const sessionStore = makeSessionStore(); const sessionName = 'recording-is-absent'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: true })); mockSnapshotWithSaveButton(); const response = await runCommand(sessionStore, sessionName, 'is', [ @@ -447,7 +447,7 @@ test('is absent while recording stays an ordinary unannotated observation', asyn test('is visible without recording never computes target-v1 evidence', async () => { const sessionStore = makeSessionStore(); const sessionName = 'non-recording-is'; - sessionStore.set(sessionName, makeSessionWithSnapshot(sessionName, { recording: false })); + sessionStore.publish(sessionName, makeSessionWithSnapshot(sessionName, { recording: false })); mockSnapshotWithSaveButton(); const response = await runCommand(sessionStore, sessionName, 'is', ['visible', 'label="Save"']); diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-android-freshness.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-android-freshness.test.ts index 7a1296b902..70669107e3 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-android-freshness.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-android-freshness.test.ts @@ -87,7 +87,7 @@ test('press @ref refreshes Android snapshot when freshness tracking is active', baselineCount: 1, routeComparable: false, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: [ @@ -159,7 +159,7 @@ test('ADR 0014: Android freshness cannot retarget an admitted ref by positional baselineCount: 1, routeComparable: false, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); // The freshness refresh returns a DIFFERENT element at @e1's index — after // navigation the button at that position is now "Cancel", not "Continue". @@ -224,7 +224,7 @@ test('press @ref falls back to cached Android ref when freshness refresh fails', baselineCount: 1, routeComparable: true, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockRejectedValueOnce(new Error('uiautomator timeout')); @@ -293,7 +293,7 @@ test('coordinate press preserves Android route freshness from last comparable sn backend: 'android', comparisonSafe: false, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-android-readiness.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-android-readiness.test.ts index 56642c945d..df7be26605 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-android-readiness.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-android-readiness.test.ts @@ -100,7 +100,7 @@ test('press @ref fails when Android tap escapes to launcher', async () => { createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); mockGetAndroidAppState.mockResolvedValue({ @@ -148,7 +148,7 @@ test('press @ref fails when Android tap escapes to Settings', async () => { createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); mockGetAndroidAppState.mockResolvedValue({ @@ -203,7 +203,7 @@ test.each(ANDROID_PERMISSION_PROMPT_PACKAGES)( createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); mockGetAndroidAppState.mockResolvedValue({ @@ -268,7 +268,7 @@ test('a ref action aborts with the shared ref_frame_expired rejection after Andr }; session.snapshotGeneration = 900; // A freshly issued complete frame is active. - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { token: 't', session: sessionName, command: 'press', positionals: ['@e1'], flags: {} }, diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios-eligibility.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios-eligibility.test.ts index ec1e54d5ad..53be62df8a 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios-eligibility.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios-eligibility.test.ts @@ -66,7 +66,10 @@ beforeEach(() => { test('ordinary click uses canonical capture even when the owner binds selector tap', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-selector-without-direct-operation'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockCaptureSnapshotForSession.mockResolvedValueOnce({ nodes: attachRefs([ { @@ -111,7 +114,10 @@ test('ordinary click uses canonical capture even when the owner binds selector t test('fill simple iOS id selector resolves runtime text input evidence before coordinate fill', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-runtime-selector-fill'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockCaptureSnapshotForSession.mockResolvedValueOnce({ nodes: attachRefs([ @@ -175,7 +181,10 @@ test('fill simple iOS id selector resolves runtime text input evidence before co test('click simple iOS selector forwards Maestro non-hittable coordinate fallback', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-maestro-selector-fallback'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockTapElementSelector.mockResolvedValue({ message: 'tapped via non-hittable coordinate fallback', @@ -223,7 +232,7 @@ test('click simple iOS id selector waits for snapshot path after pending gesture const sessionName = 'ios-direct-selector-after-swipe'; const session = makeIosSession(sessionName, { appBundleId: 'com.example.app' }); session.postGestureStabilization = { action: 'swipe', positionals: [], markedAt: Date.now() }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: attachRefs([ diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts index c1948270a4..b001eac25d 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts @@ -52,7 +52,10 @@ test.each([ async (code, message) => { const sessionStore = makeSessionStore(); const sessionName = `ios-maestro-direct-selector-${code}`; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); mockTapElementSelector.mockRejectedValue(new AppError(code, message)); const response = await handleInteractionCommands({ @@ -77,7 +80,7 @@ test.each([ test('Maestro selector click crosses the ADR 0014 fused seam and expires the ref frame', async () => { const sessionStore = makeSessionStore(); const sessionName = 'maestro-direct-ios-seam'; - sessionStore.set(sessionName, makeStaleRefSession(sessionName)); + sessionStore.publish(sessionName, makeStaleRefSession(sessionName)); const click = await runInteraction(sessionStore, sessionName, 'click', ['label=Continue'], { maestro: { allowNonHittableCoordinateFallback: true }, @@ -121,7 +124,7 @@ for (const row of ROWS) { assert.equal(typeof row.fallsBack, 'boolean', `${row.id} must state fallsBack`); const sessionStore = makeSessionStore(); const sessionName = `maestro-direct-${row.id}`; - sessionStore.set(sessionName, makeStaleRefSession(sessionName)); + sessionStore.publish(sessionName, makeStaleRefSession(sessionName)); mockTapElementSelector.mockRejectedValueOnce(failure()); vi.mocked(captureSnapshotWithInteractor).mockResolvedValue({ nodes: makeTwoButtonNodes(), diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-fill.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-fill.test.ts index be1a3b3c49..905028089a 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-fill.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-fill.test.ts @@ -96,7 +96,7 @@ test('fill @ref fails fast when the target is off-screen', async () => { createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await withRunner(() => handleInteractionCommands({ @@ -153,7 +153,7 @@ test('fill @ref fails closed when stored ref bounds are invalid (ADR 0014)', asy createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockFillPoint.mockRejectedValue( new Error('dispatch must not run: no positional recapture on unusable frame evidence'), @@ -204,7 +204,7 @@ test('fill @ref rejects after a device action expired the frame', async () => { }; // ADR 0014: an unobserved device action expired the frame. expireRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockFillPoint.mockRejectedValue( new Error('dispatch should not be called for an expired-frame ref'), ); @@ -244,7 +244,7 @@ test('fill with a pinned stale ref rejects; pinned current is clean', async () = session.snapshotGeneration = 3; // Re-issue the frame over the overridden snapshot so its source tree matches. activateCompleteRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const stale = await runInteraction(sessionStore, sessionName, 'fill', ['@e1~s2', 'hello']); expect(stale?.ok).toBe(false); @@ -290,7 +290,7 @@ test("ADR 0014 blocker-2: a mutating find's internal fill from an expired frame // device side effect changed the screen. activateCompleteRefFrame(session); expireRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockFillPoint.mockResolvedValue({ filled: true }); // Contrast: a user-supplied `@ref` against the expired frame rejects before @@ -355,7 +355,7 @@ test('fill @ref keeps the original editable node when its parent is the hittable createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockFillPoint.mockResolvedValue({ filled: true }); diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-payload.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-payload.test.ts index d19e1bd7fe..3d424ce2d6 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-payload.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-payload.test.ts @@ -88,7 +88,7 @@ test('press coordinates appends touch-visualization events while recording', asy startedAt: Date.now() - 1_000, showTouches: true, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ ok: true, @@ -139,7 +139,7 @@ test('press coordinates on iOS recording captures a full snapshot for the touch startedAt: Date.now() - 1_000, showTouches: true, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ x: 220, y: 600 }); // Regression: a filtered snapshot has no Application/Window node, so viewport inference would @@ -198,7 +198,7 @@ test('press coordinates on Android recording uses physical screen size when no s showTouches: true, }); session.snapshot = undefined; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ x: 300, y: 2300 }); @@ -234,7 +234,7 @@ test('press coordinates on Android recording caches physical screen size across showTouches: true, }); session.snapshot = undefined; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ x: 300, y: 2300 }); @@ -281,7 +281,7 @@ test('press coordinates without recording skips Android screen-size lookup', asy const sessionName = 'android-direct-press-no-recording'; const session = makeAndroidSession(sessionName); session.snapshot = undefined; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ x: 300, y: 2300 }); @@ -314,7 +314,7 @@ test('press coordinates during recording still dispatches when Android screen-si showTouches: true, }); session.snapshot = undefined; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ x: 300, y: 2300 }); mockGetAndroidScreenSize.mockRejectedValue(new Error('adb unavailable')); @@ -368,7 +368,7 @@ test('press @ref preserves native timing in recorded result and touch visualizat startedAt: 1_000, showTouches: true, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const originalNow = Date.now; let now = 1_500; diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-press-admission.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-press-admission.test.ts index ea0c48158c..9a855d8abb 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-press-admission.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-press-admission.test.ts @@ -85,7 +85,7 @@ test('click --button middle on macOS fails with an explicit unsupported-operatio kind: 'device', booted: true, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockRejectedValue( new Error('dispatch should not be called for unsupported middle click'), @@ -116,7 +116,7 @@ test('click --button middle on macOS fails with an explicit unsupported-operatio test('press coordinates does not treat extra trailing args as selector', async () => { const sessionStore = makeSessionStore(); const sessionName = 'default'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); const response = await handleInteractionCommands({ req: { @@ -141,7 +141,7 @@ test('press coordinates does not treat extra trailing args as selector', async ( test('#1654: the resolved-target payload fails closed when its ref provenance disagrees', async () => { const sessionStore = makeSessionStore(); const sessionName = 'find-preresolved-mismatched-ref'; - sessionStore.set(sessionName, makeStaleRefSession(sessionName)); + sessionStore.publish(sessionName, makeStaleRefSession(sessionName)); const preresolved = makeFindPreresolvedTree(); const response = await runFindInternalClick(sessionStore, sessionName, { @@ -160,7 +160,7 @@ test('press selector then press @ref rejects refs that outlived the stored snaps const sessionStore = makeSessionStore(); const sessionName = 'stale-ref-warns'; const session = makeStaleRefSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: makeTwoButtonNodes(), backend: 'xctest', @@ -196,7 +196,7 @@ test('a ref press crosses the ADR 0014 side-effect seam and expires the ref fram const sessionStore = makeSessionStore(); const sessionName = 'seam-expiry'; const session = makeStaleRefSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: makeTwoButtonNodes(), backend: 'xctest', @@ -219,7 +219,7 @@ test('ADR 0014 evidence #1: a second ref mutation rejects (bare and pinned) unti session.snapshotGeneration = 500; // A complete snapshot issued the frame at generation 500. activateCompleteRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: makeTwoButtonNodes(), backend: 'xctest', @@ -251,7 +251,7 @@ test('press @ref directly after refs were issued does not warn', async () => { const sessionStore = makeSessionStore(); const sessionName = 'fresh-ref-no-warning'; const session = makeStaleRefSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await runInteraction(sessionStore, sessionName, 'press', ['@e1']); expect(response?.ok).toBe(true); @@ -264,7 +264,7 @@ test('re-issuing a complete frame lets press @ref succeed again without warning' const sessionStore = makeSessionStore(); const sessionName = 'reissued-refs-no-warning'; const session = makeStaleRefSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: makeTwoButtonNodes(), backend: 'xctest', @@ -283,7 +283,6 @@ test('re-issuing a complete frame lets press @ref succeed again without warning' // snapshot-handler tests). Without it the frame would stay expired. const stored = sessionStore.get(sessionName)!; activateCompleteRefFrame(stored); - sessionStore.set(sessionName, stored); const refPress = await runInteraction(sessionStore, sessionName, 'press', ['@e1']); expect(refPress?.ok).toBe(true); @@ -299,7 +298,7 @@ test('press with a pinned ref matching the current frame epoch is clean', async const sessionName = 'pinned-current-clean'; const session = makeStaleRefSession(sessionName); session.snapshotGeneration = 5; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await runInteraction(sessionStore, sessionName, 'press', ['@e1~s5']); expect(response?.ok).toBe(true); @@ -313,7 +312,7 @@ test('press with a pinned ref from an older generation rejects with the precise const sessionName = 'pinned-stale-precise'; const session = makeStaleRefSession(sessionName); session.snapshotGeneration = 15; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockRejectedValue(new Error('touch should not be called for a stale iOS ref')); const response = await runInteraction(sessionStore, sessionName, 'press', ['@e1~s12']); @@ -343,7 +342,7 @@ test('ADR 0014 evidence #6: a read-only capture does not invalidate a mutation r backend: 'xctest', }); expect(refFrameState(session)).toBe('active'); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); const response = await runInteraction(sessionStore, sessionName, 'press', ['@e1']); @@ -362,7 +361,7 @@ test('a malformed generation suffix is INVALID_ARGS with the ref grammar hint', const sessionStore = makeSessionStore(); const sessionName = 'pinned-malformed'; const session = makeStaleRefSession(sessionName); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); for (const [command, positionals] of [ ['press', ['@e1~x3']], @@ -392,7 +391,7 @@ test('after a session reopen, a pin from the previous lifetime rejects (reseeded // one replacement deep (a per-lifetime count from 1 would collide here). const reopened = makeStaleRefSession(sessionName); setSessionSnapshot(reopened, { ...reopened.snapshot! }); - sessionStore.set(sessionName, reopened); + sessionStore.publish(sessionName, reopened); // Probabilistic (~1/900000 collision) — accepted residual risk. expect(reopened.snapshotGeneration).not.toBe(oldGeneration); mockTapPoint.mockRejectedValue(new Error('touch should not be called for a stale iOS ref')); diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-press.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-press.test.ts index be2046aeda..01ef13c6f2 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-press.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-press.test.ts @@ -107,7 +107,7 @@ test('click on a macOS menubar wrapper ref promotes to the same-rect menu bar it createdAt: Date.now(), backend: 'macos-helper', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { @@ -164,7 +164,7 @@ test('press @ref promotes a non-hittable node to its hittable ancestor before ta createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); @@ -226,7 +226,7 @@ test('press @ref does not promote to a full-screen hittable ancestor', async () createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); @@ -285,7 +285,7 @@ test('click --button secondary on @ref dispatches a secondary press on macOS and createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ button: 'secondary' }); @@ -321,7 +321,7 @@ test('click --button secondary on @ref dispatches a secondary press on macOS and test('#1654: a mutating find click acts on the node find resolved, not a re-resolved @ref', async () => { const sessionStore = makeSessionStore(); const sessionName = 'find-preresolved-click'; - sessionStore.set(sessionName, makeStaleRefSession(sessionName)); + sessionStore.publish(sessionName, makeStaleRefSession(sessionName)); const preresolved = makeFindPreresolvedTree(); const response = await runFindInternalClick(sessionStore, sessionName, { @@ -337,7 +337,7 @@ test('#1654: a mutating find click acts on the node find resolved, not a re-reso test('#1654 control: without the resolved-target payload the same click still resolves @ref from the session tree', async () => { const sessionStore = makeSessionStore(); const sessionName = 'find-preresolved-control'; - sessionStore.set(sessionName, makeStaleRefSession(sessionName)); + sessionStore.publish(sessionName, makeStaleRefSession(sessionName)); const response = await runFindInternalClick(sessionStore, sessionName, {}); @@ -355,7 +355,7 @@ test('#1654: the leaf performs no ref lookup at all — a session that could not const sessionName = 'find-preresolved-no-session-tree'; const session = makeSession(sessionName); session.snapshot = undefined; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const preresolved = makeFindPreresolvedTree(); const withoutPreresolution = await runFindInternalClick(sessionStore, sessionName, {}); @@ -374,7 +374,7 @@ test('#1654: the shared guards still run on the pre-resolved node', async () => // node is still refused, at the same ADR 0011 `runtime-ref` occlusion cell. const sessionStore = makeSessionStore(); const sessionName = 'find-preresolved-occluded'; - sessionStore.set(sessionName, makeStaleRefSession(sessionName)); + sessionStore.publish(sessionName, makeStaleRefSession(sessionName)); const preresolved = makeFindPreresolvedTree(); const blocked = { ...preresolved.node, @@ -396,7 +396,10 @@ test('#1654: the shared guards still run on the pre-resolved node', async () => test('a read right after a press captures the post-tap screen instead of reusing the pre-tap tree', async () => { const sessionStore = makeSessionStore(); const sessionName = 'read-after-press'; - sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); + sessionStore.publish( + sessionName, + makeIosSession(sessionName, { appBundleId: 'com.example.app' }), + ); const screen = (step: string) => ({ backend: 'xctest' as const, producer: 'apple-runner' as const, diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts index 246ca33562..3f0a119d67 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts @@ -296,7 +296,7 @@ test('press @ref --verify surfaces evidence through the interactionResultExtra a createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); // Post-action capture reports an extra node, so changedFromBefore should // read true against the pre-action (stored) snapshot's single node. @@ -378,7 +378,7 @@ test('press @ref without --verify never includes an evidence field', async () => createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockResolvedValue({ pressed: true }); const response = await handleInteractionCommands({ @@ -422,7 +422,7 @@ test('fill selector --verify surfaces evidence through the interactionResultExtr createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: [ @@ -483,7 +483,7 @@ test('fill @ref --verify surfaces evidence in the ref response branch', async () createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: [ @@ -552,7 +552,7 @@ test('fill @ref without --verify never includes an evidence field', async () => createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { @@ -601,7 +601,7 @@ test('fill @ref preserves fallback coordinates for recording when platform resul startedAt: Date.now() - 1_000, showTouches: true, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockFillPoint.mockResolvedValue({ filled: true }); const response = await handleInteractionCommands({ diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-runtime.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-runtime.test.ts index 6368761b3f..fda5a2b5da 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-runtime.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-runtime.test.ts @@ -82,7 +82,7 @@ test('press @ref taps the resolved point once and records the ref', async () => }, ]); session.snapshot = { nodes, createdAt: Date.now(), backend: 'xctest' }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { @@ -123,7 +123,7 @@ test('press @ref fails closed when the authorized ref has no usable bounds (ADR createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockRejectedValue( new Error('dispatch must not run: no positional recapture on missing frame evidence'), @@ -180,7 +180,7 @@ test('press @ref fails closed when stored ref bounds are invalid (ADR 0014)', as createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockTapPoint.mockRejectedValue( new Error('dispatch must not run: no positional recapture on unusable frame evidence'), @@ -236,7 +236,7 @@ test('press @ref fails fast when the target is off-screen', async () => { createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await withRunner(() => handleInteractionCommands({ @@ -307,7 +307,7 @@ test('press @ref with a trailing label recovers within the authorized frame (no createdAt: Date.now(), backend: 'android', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockRejectedValue( new Error('no positional recapture: recovery stays in-frame'), diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch.test.ts index 9fec76b04e..ac5e6465bf 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch.test.ts @@ -74,7 +74,7 @@ test('press coordinates dispatches press and records as press', async () => { const sessionStore = makeSessionStore(); const sessionName = 'default'; const storedSession = makeSession(sessionName); - sessionStore.set(sessionName, storedSession); + sessionStore.publish(sessionName, storedSession); const response = await handleInteractionCommands({ req: { @@ -116,7 +116,7 @@ test.each([ const sessionName = `single-bind-${command}`; const session = makeSession(sessionName); if (web) session.device = WEB_DESKTOP_DEVICE; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { token: 't', session: sessionName, command, positionals: [...positionals], flags: {} }, @@ -134,7 +134,7 @@ test.each([ test('click rejects macOS desktop surface interactions until helper routing exists', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-desktop-click'; - sessionStore.set(sessionName, makeMacOsDesktopSession(sessionName)); + sessionStore.publish(sessionName, makeMacOsDesktopSession(sessionName)); const response = await handleInteractionCommands({ req: { @@ -160,7 +160,7 @@ test('click rejects macOS desktop surface interactions until helper routing exis test('fill rejects macOS menubar surface interactions until helper routing exists', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-menubar-fill'; - sessionStore.set(sessionName, makeMacOsMenubarSession(sessionName)); + sessionStore.publish(sessionName, makeMacOsMenubarSession(sessionName)); const response = await handleInteractionCommands({ req: { @@ -201,7 +201,7 @@ test('longpress @ref resolves the target and dispatches coordinate longpress', a createdAt: Date.now(), backend: 'xctest', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { @@ -247,7 +247,7 @@ test('hover @ref on web dispatches through the provider hoverRef route, not coor createdAt: Date.now(), backend: 'web', }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { token: 't', @@ -275,7 +275,7 @@ test('hover selector on web resolves the target and dispatches coordinate hover' const sessionName = 'hover-selector'; const session = makeSession(sessionName); session.device = WEB_DESKTOP_DEVICE; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockCaptureSnapshotForSession.mockResolvedValue({ nodes: attachRefs([ { @@ -315,7 +315,7 @@ test('hover selector on web resolves the target and dispatches coordinate hover' test('hover is refused by capability on touch platforms before any dispatch', async () => { const sessionStore = makeSessionStore(); const sessionName = 'hover-ios'; - sessionStore.set(sessionName, makeSession(sessionName)); + sessionStore.publish(sessionName, makeSession(sessionName)); const response = await handleInteractionCommands({ req: { diff --git a/src/daemon/interaction/internal/__tests__/interaction-type-android-readiness.test.ts b/src/daemon/interaction/internal/__tests__/interaction-type-android-readiness.test.ts index 9371210df3..35d40b2108 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-type-android-readiness.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-type-android-readiness.test.ts @@ -44,7 +44,7 @@ test('type continues and composes the recording readiness warning', async () => startedAt: 0, }); const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleInteractionCommands({ req: { diff --git a/src/daemon/selector-runtime-backend.test.ts b/src/daemon/selector-runtime-backend.test.ts index 977f32607a..a1d9a82584 100644 --- a/src/daemon/selector-runtime-backend.test.ts +++ b/src/daemon/selector-runtime-backend.test.ts @@ -30,7 +30,7 @@ test('wait text passes its poll deadline signal to the Apple runner fast path', createdAt: Date.now(), actions: [], }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); let observedSignal: AbortSignal | undefined; // R35: every selector runtime is bound, so the poll deadline must reach the platform through // the bound capture's per-capture signal — the seam the runner used to be reached through. @@ -101,7 +101,7 @@ test('daemon wait stable pins private-ax on emitted snapshot runner requests', a actions: [], snapshot, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const capture = vi.fn(async (_input: CaptureSnapshotInput): Promise => ({ backend: 'xctest', producer: 'apple-runner', diff --git a/src/daemon/server/daemon-idle-reap.test.ts b/src/daemon/server/daemon-idle-reap.test.ts index 2efee9bd79..92832ac32f 100644 --- a/src/daemon/server/daemon-idle-reap.test.ts +++ b/src/daemon/server/daemon-idle-reap.test.ts @@ -54,12 +54,12 @@ test('resolveDaemonIdleReapMs ignores invalid overrides', () => { test('hasReapBlockingOpenSessions reflects the session store', () => { assert.equal(hasReapBlockingOpenSessions(sessionStore), false); - sessionStore.set('default', makeSession()); + sessionStore.publish('default', makeSession()); assert.equal(hasReapBlockingOpenSessions(sessionStore), true); }); test('hasActiveRecording is true only when a stored session carries a recording', () => { - sessionStore.set('default', makeSession()); + sessionStore.publish('default', makeSession()); assert.equal(hasActiveRecording(sessionStore), false); const recordingSession = makeSession(); @@ -69,7 +69,9 @@ test('hasActiveRecording is true only when a stored session carries a recording' startedAt: Date.now(), showTouches: false, }); - sessionStore.set('default', recordingSession); + sessionStore.update(sessionStore.lookup('default')!, { + screenRecording: recordingSession.screenRecording, + }); assert.equal(hasActiveRecording(sessionStore), true); }); @@ -77,7 +79,7 @@ test('isDaemonIdle requires no in-flight requests, no sessions, and no recording assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 0 }), true); assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 1 }), false); - sessionStore.set('default', makeSession()); + sessionStore.publish('default', makeSession()); assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 0 }), false); }); @@ -86,7 +88,7 @@ test('isDaemonIdle requires no in-flight requests, no sessions, and no recording // reaped (with a tombstone) rather than pinning the daemon forever. --- test('a repair-armed, un-committed session does NOT block idle-reap', () => { - sessionStore.set( + sessionStore.publish( 'default', makeSession({ scriptPublication: { @@ -102,7 +104,7 @@ test('a repair-armed, un-committed session does NOT block idle-reap', () => { }); test('a repair-armed session that has already COMMITTED still blocks idle-reap (until close deletes it)', () => { - sessionStore.set( + sessionStore.publish( 'default', makeSession({ scriptPublication: { @@ -118,13 +120,13 @@ test('a repair-armed session that has already COMMITTED still blocks idle-reap ( }); test('an ordinary (non-repair) open session still blocks idle-reap', () => { - sessionStore.set('default', makeSession()); + sessionStore.publish('default', makeSession()); assert.equal(hasReapBlockingOpenSessions(sessionStore), true); assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 0 }), false); }); test('a normal session alongside a reapable repair session still blocks idle-reap', () => { - sessionStore.set( + sessionStore.publish( 'default', makeSession({ name: 'default', @@ -136,7 +138,7 @@ test('a normal session alongside a reapable repair session still blocks idle-rea }, }), ); - sessionStore.set('other', makeSession({ name: 'other' })); + sessionStore.publish('other', makeSession({ name: 'other' })); assert.equal(hasReapBlockingOpenSessions(sessionStore), true); assert.equal(isDaemonIdle({ sessionStore, inFlightRequestCount: 0 }), false); }); @@ -164,7 +166,7 @@ test('idle reap fires after the idle window when nothing is using the daemon', a test('idle reap does not fire while a session is open', async () => { vi.useFakeTimers(); let reaped = 0; - sessionStore.set('default', makeSession()); + sessionStore.publish('default', makeSession()); const idleReap = createDaemonIdleReap({ sessionStore, getInFlightRequestCount: () => 0, @@ -192,7 +194,7 @@ test('idle reap does not fire while a recording is active', async () => { startedAt: Date.now(), showTouches: false, }); - sessionStore.set('default', recordingSession); + sessionStore.publish('default', recordingSession); const idleReap = createDaemonIdleReap({ sessionStore, getInFlightRequestCount: () => 0, diff --git a/src/daemon/server/daemon-runtime-device-claims.test.ts b/src/daemon/server/daemon-runtime-device-claims.test.ts index 05bfb11e22..86c91fb19f 100644 --- a/src/daemon/server/daemon-runtime-device-claims.test.ts +++ b/src/daemon/server/daemon-runtime-device-claims.test.ts @@ -36,7 +36,7 @@ function setup(): { session: SessionState; sessionStore: SessionStore; stateDir: actions: [], }; const sessionStore = new SessionStore(path.join(stateDir, 'sessions')); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); return { session, sessionStore, stateDir }; } diff --git a/src/daemon/server/daemon-runtime-recording-teardown.test.ts b/src/daemon/server/daemon-runtime-recording-teardown.test.ts index f4eae37115..1bdf9c6495 100644 --- a/src/daemon/server/daemon-runtime-recording-teardown.test.ts +++ b/src/daemon/server/daemon-runtime-recording-teardown.test.ts @@ -31,7 +31,7 @@ test('daemon shutdown awaits durable recording finalization inside its extended }), ); const session = makeRecordingSession({ name: 'shutdown-recording', sessionStore, finish }); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const stderrChunks: string[] = []; const teardown = teardownDaemonSessionForShutdown({ @@ -68,7 +68,7 @@ test('daemon shutdown resolves durable recording resources through the effective }), }); session.name = 'default'; - sessionStore.set(effectiveSessionName, session); + sessionStore.publish(effectiveSessionName, session); const stderrChunks: string[] = []; await teardownDaemonSessionForShutdown({ diff --git a/src/daemon/server/daemon-runtime-web-cleanup.test.ts b/src/daemon/server/daemon-runtime-web-cleanup.test.ts index 69459e22e5..0e20e81ea1 100644 --- a/src/daemon/server/daemon-runtime-web-cleanup.test.ts +++ b/src/daemon/server/daemon-runtime-web-cleanup.test.ts @@ -32,7 +32,7 @@ test('daemon-startup web cleanup passes open web sessions to the reaper', async try { await installFakeManagedAgentBrowser(stateDir); const sessionStore = new SessionStore(path.join(stateDir, 'sessions')); - sessionStore.set('web-session', { + sessionStore.publish('web-session', { name: 'web-session', device: WEB_DESKTOP_DEVICE, createdAt: Date.now(), diff --git a/src/daemon/server/daemon-runtime-web-close-teardown.test.ts b/src/daemon/server/daemon-runtime-web-close-teardown.test.ts index 64e8d377de..1613a97b87 100644 --- a/src/daemon/server/daemon-runtime-web-close-teardown.test.ts +++ b/src/daemon/server/daemon-runtime-web-close-teardown.test.ts @@ -31,7 +31,7 @@ test('daemon shutdown awaits a slow web close inside its extended budget', async await installFakeManagedAgentBrowser(root); const sessionStore = new SessionStore(path.join(root, 'sessions')); const session = makeWebSession('shutdown-slow-web-session'); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); mockRunCmd.mockImplementation( async () => await new Promise((resolve) => { @@ -74,7 +74,7 @@ test('daemon shutdown closes an open web session immediately, without waiting fo // Teardown runs while the session it is tearing down is still in the store (session deletion // happens after), which is also what keeps agent-browser's provider-startup orphan sweep from // treating this session's own browser as an orphan and scanning real host processes for it. - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); mockRunCmd.mockResolvedValue({ stdout: JSON.stringify({ success: true, data: {} }), stderr: '', @@ -107,7 +107,7 @@ test('daemon shutdown reports a web close failure on stderr instead of losing it await installFakeManagedAgentBrowser(root); const sessionStore = new SessionStore(path.join(root, 'sessions')); const session = makeWebSession('shutdown-web-close-failure-session'); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); mockRunCmd.mockResolvedValue({ stdout: JSON.stringify({ success: false, error: 'no active browser session' }), stderr: '', diff --git a/src/daemon/server/daemon-session-idle-expiry-scheduling.test.ts b/src/daemon/server/daemon-session-idle-expiry-scheduling.test.ts index 4d1fa1051a..2780321e94 100644 --- a/src/daemon/server/daemon-session-idle-expiry-scheduling.test.ts +++ b/src/daemon/server/daemon-session-idle-expiry-scheduling.test.ts @@ -316,14 +316,10 @@ test('a session that moved to another device is fenced by that device, not the s }); const command = withKeyedLock(locks, 'session:default', async () => { await commandHeld; - sessionStore.set( - 'default', - makeIosSession('default', { - createdAt: NOW - WINDOW_MS - 1, - device: { ...IOS_SIMULATOR, id: 'sim-2', name: 'iPhone 17' }, - deviceClaim: { ...CLAIM, deviceKey: 'ios:sim-2' }, - }), - ); + sessionStore.update(sessionStore.lookup('default')!, { + device: { ...IOS_SIMULATOR, id: 'sim-2', name: 'iPhone 17' }, + deviceClaim: { ...CLAIM, deviceKey: 'ios:sim-2' }, + }); }); // A command on the NEW session's device is in flight the whole time. @@ -454,14 +450,14 @@ test('a settle that outruns its wait still finishes the release it started', asy test('a daemon beginning to leave does not start settling the next session', async () => { const { sessionStore } = makeFixture('agent-device-idle-expiry-closing-'); - sessionStore.set( + sessionStore.publish( 'first', makeIosSession('first', { createdAt: NOW - WINDOW_MS - 1, deviceClaim: { ...CLAIM, deviceKey: 'ios:sim-1' }, }), ); - sessionStore.set( + sessionStore.publish( 'second', makeIosSession('second', { createdAt: NOW - WINDOW_MS - 1, diff --git a/src/daemon/server/daemon-session-idle-expiry.test.ts b/src/daemon/server/daemon-session-idle-expiry.test.ts index b27d64b898..350a31f022 100644 --- a/src/daemon/server/daemon-session-idle-expiry.test.ts +++ b/src/daemon/server/daemon-session-idle-expiry.test.ts @@ -234,7 +234,7 @@ test('a session closed by its owner stops the retry clock tracking it', async () await sweeps.swept(); assert.equal(settleCalls, 1); - sessionStore.delete('default'); + sessionStore.retire(sessionStore.lookup('default')!); await runUntilIdle(controller, 10); assert.equal(settleCalls, 1, 'nothing is left to retry once the session is gone'); controller.cancel(); @@ -243,7 +243,7 @@ test('a session closed by its owner stops the retry clock tracking it', async () test('a settled session of another kind is never touched by the sweep', async () => { const { sessionStore } = makeFixture('agent-device-idle-expiry-kind-'); // Past its deadline in time, but holding a remote lease: that lease owns the device. - sessionStore.set( + sessionStore.publish( 'leased', makeIosSession('leased', { createdAt: NOW - WINDOW_MS - 1, @@ -258,7 +258,7 @@ test('a settled session of another kind is never touched by the sweep', async () ); // Past its deadline too, but holding no claim: there is no device for another agent to wait on, // so there is nothing here an expiry could reclaim and nothing to release. - sessionStore.set('plain', unclaimedExpiredSession('plain')); + sessionStore.publish('plain', unclaimedExpiredSession('plain')); const settledNames: string[] = []; const controller = createSessionIdleExpiry({ @@ -292,7 +292,8 @@ test('a held-back settle leaves a repair transaction uncommitted, so a later pas createdAt: NOW - WINDOW_MS - 1, deviceClaim: { ...deviceClaim }, }); - sessionStore.set('default', session); + sessionStore.retire(sessionStore.lookup('default')!); + sessionStore.publish('default', session); const claimsDir = path.dirname(resolveDeviceClaimPath(deviceClaim.deviceKey)); const sweeps = createSweepBarrier(); @@ -353,7 +354,8 @@ test('a committed expiry finalizes the repair transaction it ends', async () => createdAt: NOW - WINDOW_MS - 1, deviceClaim: { ...deviceClaim }, }); - sessionStore.set('default', session); + sessionStore.retire(sessionStore.lookup('default')!); + sessionStore.publish('default', session); const sweeps = createSweepBarrier(); const controller = createSessionIdleExpiry({ diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities-install-projection.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities-install-projection.test.ts index 07dd92244b..8b64cca232 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities-install-projection.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities-install-projection.test.ts @@ -142,7 +142,7 @@ test('capabilities fixture preserves transport mode and owner-specific refusal r function createAndroidCapabilitiesSession(suffix: string) { const sessionName = `android-capabilities-${suffix}`; const sessionStore = makeSessionStore(`agent-device-capabilities-${suffix}-`); - sessionStore.set(sessionName, makeAndroidSession(sessionName)); + sessionStore.publish(sessionName, makeAndroidSession(sessionName)); return { sessionName, sessionStore }; } diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts index d6ff281cb3..c02e9d3e85 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts @@ -49,7 +49,7 @@ function assertAndroidCapabilityHonesty(availableCommands: unknown): void { /** The one interaction-capable Android owner both projection tests below read. */ async function projectAndroidCapabilities(sessionName: string) { const sessionStore = makeSessionStore('agent-device-capabilities-'); - sessionStore.set(sessionName, makeAndroidSession(sessionName)); + sessionStore.publish(sessionName, makeAndroidSession(sessionName)); const runtime = createAdmissionRuntime({ appLogAvailable: true, ensureReadyAvailable: true, @@ -121,7 +121,7 @@ test('capabilities omits the commands this Android owner does not admit', async test('capabilities excludes logs from an unavailable provider-mode XCTest runtime fact', async () => { const sessionName = 'provider-xctest-capabilities'; const sessionStore = makeSessionStore('agent-device-capabilities-provider-xctest-'); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { name: sessionName, device: { platform: 'apple', @@ -176,7 +176,7 @@ test('capabilities excludes logs from an unavailable provider-mode XCTest runtim test('capabilities excludes network when the runtime fact is unavailable', async () => { const sessionName = 'android-capabilities-no-network'; const sessionStore = makeSessionStore('agent-device-capabilities-no-network-'); - sessionStore.set(sessionName, makeAndroidSession(sessionName)); + sessionStore.publish(sessionName, makeAndroidSession(sessionName)); const runtime = createAdmissionRuntime({ appLogAvailable: true, networkAvailable: false, @@ -217,7 +217,7 @@ test('capabilities includes apps for the available HarmonyOS runtime fact', asyn target: 'mobile', booted: true, } as const; - sessionStore.set(sessionName, makeSession(sessionName, { device: harmonyDevice })); + sessionStore.publish(sessionName, makeSession(sessionName, { device: harmonyDevice })); const runtime = createAdmissionRuntime({ appLogAvailable: true, networkAvailable: false, @@ -298,7 +298,7 @@ test.each(APPS_UNAVAILABLE_CAPABILITY_CASES)( async ({ label, device, providerMode }) => { const sessionName = `capabilities-${label.toLowerCase().replaceAll(' ', '-')}`; const sessionStore = makeSessionStore(`agent-device-capabilities-${label}-`); - sessionStore.set(sessionName, makeSession(sessionName, { device })); + sessionStore.publish(sessionName, makeSession(sessionName, { device })); const runtime = createAdmissionRuntime({ appLogAvailable: false, networkAvailable: false, @@ -332,7 +332,7 @@ test.each(APPS_UNAVAILABLE_CAPABILITY_CASES)( test('capabilities excludes appstate when its runtime fact is unavailable', async () => { const sessionName = 'android-capabilities-no-appstate'; const sessionStore = makeSessionStore('agent-device-capabilities-no-appstate-'); - sessionStore.set(sessionName, makeAndroidSession(sessionName)); + sessionStore.publish(sessionName, makeAndroidSession(sessionName)); const runtime = createAdmissionRuntime({ appLogAvailable: true, appStateAvailable: false, @@ -364,7 +364,7 @@ test('capabilities excludes appstate when its runtime fact is unavailable', asyn test('capabilities excludes appstate when its readiness fact is unavailable', async () => { const sessionName = 'android-capabilities-no-readiness'; const sessionStore = makeSessionStore('agent-device-capabilities-no-readiness-'); - sessionStore.set(sessionName, makeAndroidSession(sessionName)); + sessionStore.publish(sessionName, makeAndroidSession(sessionName)); const runtime = createAdmissionRuntime({ appLogAvailable: true, appStateAvailable: true, @@ -424,7 +424,7 @@ test.each([ async (_name, device) => { const sessionName = device.id + '-session'; const sessionStore = makeSessionStore('agent-device-capabilities-' + device.id + '-'); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { name: sessionName, device, createdAt: Date.now(), diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts index 9918c3aa1d..002a466fc7 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-close-cwd-scoped-resources.test.ts @@ -75,7 +75,7 @@ test('close finishes the recording of a cwd-scoped session by store address', as const sessionStore = makeSessionStore(); const session = makeIosSimulatorRecordingSession(sessionStore, ADDRESS, { name: NAME }); const finish = recordingFinishMock(session); - sessionStore.set(ADDRESS, session); + sessionStore.publish(ADDRESS, session); const resourcePath = screenRecordingResourceStore.resolvePath( sessionStore.resolveSessionDir(ADDRESS), ); @@ -86,7 +86,7 @@ test('close finishes the recording of a cwd-scoped session by store address', as device: { ...IOS_SIM, id: 'decoy-udid', name: 'iPhone 15' }, }); const decoyFinish = recordingFinishMock(decoy); - sessionStore.set(NAME, decoy); + sessionStore.publish(NAME, decoy); const decoyResourcePath = screenRecordingResourceStore.resolvePath( sessionStore.resolveSessionDir(NAME), ); @@ -143,7 +143,7 @@ test('close stops the app log of a cwd-scoped session by store address', async ( }, metadata: { phase: 'active' }, }); - sessionStore.set(ADDRESS, { + sessionStore.publish(ADDRESS, { ...makeSession(NAME, IOS_SIM), appBundleId: 'com.apple.Preferences', appLog: { handle, envelope }, @@ -215,7 +215,10 @@ test('close finishes the audio probe of a cwd-scoped session by store address', marker: { pid: 4242, startTime: 'boot+1', command: 'helper' }, }), }); - sessionStore.set(ADDRESS, { ...makeSession(NAME, IOS_SIM), audioProbe: { handle, envelope } }); + sessionStore.publish(ADDRESS, { + ...makeSession(NAME, IOS_SIM), + audioProbe: { handle, envelope }, + }); const resourcePath = audioProbeResourceStore.resolvePath(sessionDir); audioProbeResourceStore.write(resourcePath, envelope); @@ -255,7 +258,10 @@ test('close stops the perf capture of a cwd-scoped session by store address', as lifecycle: 'open', descriptor: { version: 1, body: { kind: 'fixture' } }, }); - sessionStore.set(ADDRESS, { ...makeSession(NAME, IOS_SIM), perfCapture: { handle, envelope } }); + sessionStore.publish(ADDRESS, { + ...makeSession(NAME, IOS_SIM), + perfCapture: { handle, envelope }, + }); const resourcePath = perfCaptureResourceStore.resolvePath(sessionDir); perfCaptureResourceStore.write(resourcePath, envelope); diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-close-error-precedence.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-close-error-precedence.test.ts index 3d2019eb61..ae89f86624 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-close-error-precedence.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-close-error-precedence.test.ts @@ -38,7 +38,7 @@ test('targeted close preserves the platform-close AppError and still runs later booted: true, }, }); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const platformCloseError = new AppError('DEVICE_UNAVAILABLE', 'platform close failed', { reason: 'device_disconnected', @@ -111,7 +111,7 @@ test('a failed platform close retains the device claim and reports it', async () } const session = makeIosSimulatorRecordingSession(sessionStore, sessionName, { device }); session.deviceClaim = acquired.ownership; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const platformCloseError = new AppError('DEVICE_UNAVAILABLE', 'platform close failed', { reason: 'device_disconnected', @@ -228,7 +228,7 @@ test('a failing best-effort cleanup also retains the device claim and reports it ? Envelope : never, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); // The platform close itself succeeds; only the best-effort cleanup step fails, so the // blocking error arrives as the cleanup aggregate rather than as platformCloseError. @@ -321,7 +321,7 @@ test('a successful close clears the device claim', async () => { ...makeSession(sessionName, device), deviceClaim: acquired.ownership, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleSessionCommands({ req: { @@ -362,7 +362,7 @@ test('targeted close skips platform dispatch and preserves the error when the re }), appBundleId: 'com.example.app', } as unknown as SessionState; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const preCloseError = new AppError('RUNNER_UNAVAILABLE', 'runner stop failed', { reason: 'runner_stop_failed', diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts index 05af3277fa..58f8e46dcc 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts @@ -89,7 +89,7 @@ test('an app-only close without a target fails before admission or ref-frame exp }; const session = makeSession(sessionName, device); activateCompleteRefFrame(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const revisionBeforeClose = readSessionRuntimeRevision(session); const response = await close({ @@ -120,7 +120,7 @@ test('close rejects a false close-target fact before its one implementation bind kind: 'emulator' as const, booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, device)); + sessionStore.publish(sessionName, makeSession(sessionName, device)); mockInspectDeviceRuntimeFacts.mockImplementationOnce(async (candidate) => { const facts = lifecycleRuntimeFacts(candidate); const unavailable = { @@ -163,7 +163,7 @@ test('a close-capable runtime with false runtime-hints facts never loads the run }; const session = makeSession(sessionName, device); session.appBundleId = 'com.example.harmony'; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockInspectDeviceRuntimeFacts.mockImplementationOnce(async (candidate) => { const facts = lifecycleRuntimeFacts(candidate); return { @@ -205,7 +205,7 @@ test('an app-only close with stored hints selects no runtime-hint cleanup siblin }; const session = makeSession(sessionName, device); session.appBundleId = 'com.example.app'; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); sessionStore.setRuntimeHints(sessionName, { platform: 'android', metroHost: '10.0.2.2' }); mockInspectDeviceRuntimeFacts.mockImplementationOnce(async (candidate) => { const facts = lifecycleRuntimeFacts(candidate); @@ -254,7 +254,7 @@ test('a close with persisted native hints fails closed when the distinct cleanup }; const session = makeSession(sessionName, device); session.appBundleId = 'com.example.app'; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); sessionStore.setRuntimeHints(sessionName, { platform: 'ios', metroHost: '10.0.2.2' }); mockInspectDeviceRuntimeFacts.mockImplementationOnce(async (candidate) => { const facts = lifecycleRuntimeFacts(candidate); @@ -296,7 +296,7 @@ test('a supported Android close clears admitted runtime hints exactly once', asy }; const session = makeSession(sessionName, device); session.appBundleId = 'com.example.app'; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); sessionStore.setRuntimeHints(sessionName, { platform: 'android', metroHost: '10.0.2.2', @@ -332,7 +332,7 @@ test('close expires the ref frame immediately before its admitted platform mutat }; const session = makeSession(sessionName, device); session.appBundleId = 'com.example.app'; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockDispatch.mockImplementationOnce(async () => { expect(refFrameState(session)).toBe('expired'); }); diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-close-resource-cleanup.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-close-resource-cleanup.test.ts index 41f5193bdf..d366a41a62 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-close-resource-cleanup.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-close-resource-cleanup.test.ts @@ -39,7 +39,7 @@ test('close stops Android snapshot helper session before deleting session', asyn kind: 'emulator', booted: true, }; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, device), appBundleId: 'com.example.app', }); @@ -133,7 +133,7 @@ test('close stops active host audio probe before deleting session', async () => }), audioProbe: { handle, envelope }, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); audioProbeResourceStore.write( audioProbeResourceStore.resolvePath(sessionStore.resolveSessionDir(sessionName)), envelope, @@ -161,7 +161,7 @@ test('close stops active host audio probe before deleting session', async () => test('close dispatches web session cleanup without a positional target', async () => { const sessionStore = makeSessionStore(); const sessionName = 'web-close-session'; - sessionStore.set(sessionName, makeSession(sessionName, WEB_DESKTOP_DEVICE)); + sessionStore.publish(sessionName, makeSession(sessionName, WEB_DESKTOP_DEVICE)); const response = await handleSessionCommands({ req: { @@ -199,7 +199,7 @@ test('close preserves the session and lease when provider release fails so it ca deviceKey: 'ios:bs-device', clientId: 'client-a', }); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, WEB_DESKTOP_DEVICE), lease: { leaseId: lease.leaseId, diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-close-save-script.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-close-save-script.test.ts index 6c4c5a933a..68d45ca903 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-close-save-script.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-close-save-script.test.ts @@ -32,7 +32,7 @@ test('close --save-script on a never-armed session is rejected before teardown, // teardown to observably touch. const session = makeIosSimulatorRecordingSession(sessionStore, sessionName); const finish = recordingFinishMock(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); await expect( handleSessionCommands({ @@ -106,7 +106,7 @@ test('close --save-script on a session with an active .ad repair transaction is boundary: 0, }, }; - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleSessionCommands({ req: { diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-close-shutdown-platform.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-close-shutdown-platform.test.ts index ae4e4ec1fa..63cdbda0e4 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-close-shutdown-platform.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-close-shutdown-platform.test.ts @@ -52,7 +52,7 @@ function expectShutdownFailure( test('close --shutdown calls shutdownSimulator for iOS simulator and includes result in response', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-shutdown-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -111,7 +111,7 @@ test('close --shutdown keeps a selected provider-owned iOS simulator off local s kind: 'simulator' as const, booted: true, }; - sessionStore.set(sessionName, makeSession(sessionName, device)); + sessionStore.publish(sessionName, makeSession(sessionName, device)); const providerClose = vi.fn(async () => undefined); const providerFinalize = vi.fn(async () => ({})); mockInspectDeviceRuntimeFacts.mockImplementationOnce(async (candidate) => { @@ -159,7 +159,7 @@ test('close --shutdown keeps a selected provider-owned iOS simulator off local s test('close --shutdown calls shutdownAndroidEmulator for Android emulator and includes result in response', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-shutdown-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'android', @@ -209,7 +209,7 @@ test('close --shutdown calls shutdownAndroidEmulator for Android emulator and in test('close --shutdown is ignored for non-simulator iOS devices', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-device-shutdown-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -247,7 +247,7 @@ test('close --shutdown is ignored for non-simulator iOS devices', async () => { test('close --shutdown is ignored for Android devices', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-device-shutdown-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'android', @@ -285,7 +285,7 @@ test('close --shutdown is ignored for Android devices', async () => { test('close --shutdown returns success and failure payload when shutdownAndroidEmulator throws', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-shutdown-failure-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'android', @@ -325,7 +325,7 @@ test('close --shutdown returns success and failure payload when shutdownAndroidE test('close --shutdown returns success and failure payload when shutdownSimulator throws', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-shutdown-failure-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts index bbf3f549e7..52563445af 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts @@ -358,7 +358,7 @@ test('batch step pins nested requests to the resolved session', async () => { test('close clears retained materialized install paths bound to the session', async () => { const sessionStore = makeSessionStore('agent-device-devices-batch-runtime-'); const sessionName = 'materialized-close-active'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-apps-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-apps-runtime.test.ts index 9e54b04cb9..81d4f960a5 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-apps-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-apps-runtime.test.ts @@ -104,7 +104,7 @@ beforeEach(() => { test('macOS apps consumes generic readiness and app inventory through one runtime bind', async () => { const sessionName = 'macos-apps'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, MACOS_DEVICE)); + sessionStore.publish(sessionName, makeSession(sessionName, MACOS_DEVICE)); const req: DaemonRequest = { token: 'test-token', session: sessionName, diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-harmonyos.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-harmonyos.test.ts index 9ef2745297..ad184e7641 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-harmonyos.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-harmonyos.test.ts @@ -98,7 +98,7 @@ beforeEach(() => { async function listApps(appsFilter: 'all' | 'user-installed'): Promise { const sessionName = 'harmony-apps'; const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, makeSession(sessionName, HARMONY_DEVICE)); + sessionStore.publish(sessionName, makeSession(sessionName, HARMONY_DEVICE)); const req: DaemonRequest = { token: 'test-token', session: sessionName, diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-scoped-paths.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-scoped-paths.test.ts index 824e0758b3..a6a2683972 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-inventory-scoped-paths.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-inventory-scoped-paths.test.ts @@ -27,7 +27,7 @@ function scopedSession(): SessionState { async function runSessionList(): Promise { const sessionStore = makeSessionStore('agent-device-inventory-scoped-'); - sessionStore.set(SCOPED_KEY, scopedSession()); + sessionStore.publish(SCOPED_KEY, scopedSession()); const req: DaemonRequest = { token: 't', session: 'default', @@ -54,30 +54,30 @@ test('session list returns the caller cwd and local named sessions without cross }; const callerScope = resolveImplicitSessionScope(req)!; const callerSessionAddress = `cwd:${callerScope.id}:default`; - sessionStore.set(callerSessionAddress, { + sessionStore.publish(callerSessionAddress, { ...scopedSession(), sessionScope: callerScope, }); - sessionStore.set('qa-cart-integrity', { + sessionStore.publish('qa-cart-integrity', { ...scopedSession(), name: 'qa-cart-integrity', sessionScope: { kind: 'named-local' }, }); - sessionStore.set('tenant-a:qa', { + sessionStore.publish('tenant-a:qa', { ...scopedSession(), name: 'tenant-a:qa', sessionScope: { kind: 'named-local' }, }); - sessionStore.set('default', { + sessionStore.publish('default', { ...scopedSession(), sessionScope: { kind: 'global-default' }, }); - sessionStore.set('cwd:other:default', { + sessionStore.publish('cwd:other:default', { ...scopedSession(), sessionScope: { kind: 'cwd', id: 'other' }, }); const tenantSessionAddress = tenantScopedSessionName('tenant-a', 'remote-recording'); - sessionStore.set(tenantSessionAddress, { + sessionStore.publish(tenantSessionAddress, { ...scopedSession(), name: tenantSessionAddress, sessionScope: { kind: 'tenant', id: 'tenant-a' }, @@ -103,14 +103,14 @@ test('session list returns only sessions owned by the requesting tenant', async const sessionStore = makeSessionStore('agent-device-inventory-tenant-'); for (const tenantId of ['tenant-a', 'tenant-b']) { const address = tenantScopedSessionName(tenantId, 'default'); - sessionStore.set(address, { + sessionStore.publish(address, { ...scopedSession(), name: address, sessionScope: { kind: 'tenant', id: tenantId }, }); } for (const address of ['qa-cart-integrity', 'tenant-a:qa']) { - sessionStore.set(address, { + sessionStore.publish(address, { ...scopedSession(), name: address, sessionScope: { kind: 'named-local' }, diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts index f5d2f37a62..8d05648d5d 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts @@ -186,7 +186,7 @@ test('open runtime payload clears stale applied transport hints before launch', metroHost: '10.0.0.10', metroPort: 8081, }); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, makeAndroidEmulator()), appBundleId: 'com.example.demo', appName: 'Demo', diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-existing.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-existing.test.ts index 554d189172..e0d28abc70 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-existing.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-existing.test.ts @@ -20,7 +20,7 @@ import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts' test('open web URL on iOS device session without active app falls back to Safari', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-device-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -62,7 +62,7 @@ test('open web URL on iOS device session without active app falls back to Safari test('open web URL on iOS simulator session without active app falls back to Safari', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-simulator-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { platform: 'apple', @@ -112,7 +112,7 @@ test('open web URL on iOS simulator session without active app falls back to Saf test('open app and URL on existing iOS device session keeps app context', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-device-session'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'apple', id: 'ios-device-1', @@ -159,7 +159,7 @@ test('open app and URL on existing iOS device session keeps app context', async test('open app on existing macOS session resolves and stores bundle id', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-session'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'apple', appleOs: 'macos', @@ -234,7 +234,7 @@ test('open rejects --surface on non-macOS devices', async () => { test('open on existing macOS frontmost-app session preserves surface without --surface flag', async () => { const sessionStore = makeSessionStore(); const sessionName = 'macos-frontmost-existing'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'apple', appleOs: 'macos', @@ -295,7 +295,7 @@ test('open on existing macOS frontmost-app session preserves surface without --s test('open on existing iOS session refreshes unavailable simulator by name', async () => { const sessionStore = makeSessionStore(); const sessionName = 'ios-session'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'apple', id: 'stale-sim', @@ -361,7 +361,7 @@ test('open on existing iOS session refreshes unavailable simulator by name', asy test('open app on existing Android session resolves and stores package id', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-session'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'android', id: 'emulator-5554', @@ -404,7 +404,7 @@ test('open app on existing Android session resolves and stores package id', asyn test('open intent target on existing Android session clears stale package context', async () => { const sessionStore = makeSessionStore(); const sessionName = 'android-session'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'android', id: 'emulator-5554', @@ -455,7 +455,7 @@ test('open on existing Android session preserves a comparable freshness baseline type: 'android.widget.TextView', label: `Inbox row ${index + 1}`, })); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, { platform: 'android', id: 'emulator-5554', diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts index 96010f58fb..4a3aa559fc 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts @@ -137,7 +137,7 @@ function sessionRequest( test('open web URL on existing iOS simulator session binds Safari and drops stale app bundle id', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'ios-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -179,7 +179,7 @@ test('open web URL on existing iOS simulator session binds Safari and drops stal test('open URL on existing macOS session clears stale app bundle id', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'macos-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -216,7 +216,7 @@ test('open URL on existing macOS session clears stale app bundle id', async () = test('open URL on existing iOS device session preserves app bundle id context', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'ios-device-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -251,7 +251,7 @@ test('open URL on existing iOS device session preserves app bundle id context', test('open custom URL on existing iOS simulator session preserves app bundle id context', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'ios-simulator-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -364,7 +364,7 @@ test('open iOS simulator app prewarms runner cache during cold boot', async () = test('open iOS app session prewarms runner session when app bundle id is known', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'ios-device-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -398,7 +398,7 @@ test('open iOS Maestro app link waits for runner prewarm before launching app', const sessionName = 'ios-maestro-open-link'; const events: string[] = []; let finishPrewarm: (() => void) | undefined; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -460,7 +460,7 @@ test('open iOS Maestro app link resets a simulator runner prewarmed before URL d kind: 'simulator' as const, booted: true, }; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device, @@ -499,7 +499,7 @@ test('open iOS Maestro app link resets a simulator runner prewarmed before URL d test('open iOS Maestro app link reports blocking runner prewarm failures before launching app', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'ios-maestro-open-link-prewarm-failed'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { @@ -542,7 +542,7 @@ test('open iOS Maestro app link reports blocking runner prewarm failures before test('open iOS URL without app bundle id skips runner prewarm', async () => { const sessionStore = makeSessionStore('agent-device-session-open-url-prewarm-'); const sessionName = 'ios-device-session'; - sessionStore.set( + sessionStore.publish( sessionName, makeSession(sessionName, { device: { diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts index 3a90103ced..5c3f52f660 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts @@ -38,7 +38,7 @@ test('close finalizes an active iOS simulator recording before deleting the sess const sessionName = 'ios-active-recording-close-session'; const session = makeIosSimulatorRecordingSession(sessionStore, sessionName); const finish = recordingFinishMock(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); const response = await handleSessionCommands({ req: { @@ -75,7 +75,7 @@ test('close surfaces a recording finalization failure through the cleanup-failur }); const finish = recordingFinishMock(session); const forceCleanup = recordingCleanupMock(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); await expect( handleSessionCommands({ @@ -105,7 +105,7 @@ test('daemon resource teardown finalizes recording before lifecycle runner dispo const sessionStore = makeSessionStore(); const session = makeIosSimulatorRecordingSession(sessionStore, sessionName); const finish = recordingFinishMock(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); await teardownSessionResources({ appLog: 'already-settled', @@ -138,7 +138,7 @@ test('daemon session teardown surfaces a recording finalization failure', async }); const finish = recordingFinishMock(session); const forceCleanup = recordingCleanupMock(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); await expect( teardownSessionResources({ @@ -162,7 +162,7 @@ test('daemon session teardown retains recording evidence when finish and forced }); const finish = recordingFinishMock(session); const forceCleanup = recordingCleanupMock(session); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); await expect( teardownSessionResources({ @@ -229,7 +229,7 @@ test('daemon session teardown attempts every resource after an earlier cleanup r // Perf cleanup runs before the snapshot-helper cleanup. const sessionStore = makeSessionStore(); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); await expect( teardownSessionResources({ @@ -279,7 +279,7 @@ test('daemon session teardown closes an open web session immediately, not on age // Teardown always runs while the session it is tearing down is still in the store (session // deletion happens after), which is what keeps the provider-startup orphan sweep from treating // this session's own browser as an orphan and scanning real host processes for it. - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockRunCmd.mockResolvedValue(agentBrowserJsonResult({ success: true, data: {} })); await teardownSessionResources({ @@ -303,7 +303,7 @@ test('daemon session teardown surfaces a web close failure through the cleanup-f const sessionStore = makeSessionStore(); await installFakeManagedAgentBrowser(sessionStore.resolveDaemonStateDir()); const session = makeSession(sessionName, WEB_DESKTOP_DEVICE); - sessionStore.set(sessionName, session); + sessionStore.publish(sessionName, session); mockRunCmd.mockResolvedValue( agentBrowserJsonResult({ success: false, error: 'no active browser session' }), ); diff --git a/src/daemon/session-observability/internal/__tests__/session-audio.test.ts b/src/daemon/session-observability/internal/__tests__/session-audio.test.ts index 37699f875d..7cf3bc0032 100644 --- a/src/daemon/session-observability/internal/__tests__/session-audio.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-audio.test.ts @@ -200,7 +200,7 @@ function audioParams( test('audio probe validates daemon duration bounds', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); - sessionStore.set('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); + sessionStore.publish('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); const { params, bindDevice } = audioParams('web', sessionStore, WEB_DESKTOP_DEVICE, { positionals: ['probe', 'start', '99', '1000'], cells: { capture: false, query: true }, @@ -215,7 +215,7 @@ test('audio probe validates daemon duration bounds', async () => { test('audio probe validates daemon bucket bounds', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); - sessionStore.set('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); + sessionStore.publish('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); const { params, bindDevice } = audioParams('web', sessionStore, WEB_DESKTOP_DEVICE, { positionals: ['probe', 'start', '1000', '99'], cells: { capture: false, query: true }, @@ -230,7 +230,7 @@ test('audio probe validates daemon bucket bounds', async () => { test('audio probe rejects timing positionals for status', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); - sessionStore.set('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); + sessionStore.publish('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); const { params, bindDevice } = audioParams('web', sessionStore, WEB_DESKTOP_DEVICE, { positionals: ['probe', 'status', '1000'], cells: { capture: false, query: true }, @@ -242,7 +242,7 @@ test('audio probe rejects timing positionals for status', async () => { test('audio refuses with the owner-stated hint when no fact admits it', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); - sessionStore.set('ios-device', makeIosSession('ios-device', { device: IOS_DEVICE })); + sessionStore.publish('ios-device', makeIosSession('ios-device', { device: IOS_DEVICE })); const { params, bindDevice } = audioParams('ios-device', sessionStore, IOS_DEVICE, { positionals: ['probe', 'status'], cells: { capture: false, query: false }, @@ -260,7 +260,7 @@ test('audio refuses with the owner-stated hint when no fact admits it', async () test('audio probe start binds once, adopts the durable handle, and answers from it', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); const session = makeMacOsSession('macos'); - sessionStore.set('macos', session); + sessionStore.publish('macos', session); const runtime = fakeCaptureRuntime(session.device, runningStatus()); const { params, bindDevice } = audioParams('macos', sessionStore, session.device, { positionals: ['probe', 'start', '1000', '500'], @@ -289,7 +289,7 @@ test('audio probe start binds once, adopts the durable handle, and answers from test('audio probe starts host helper for iOS simulator audio', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); const session = makeIosSession('ios-sim'); - sessionStore.set('ios-sim', session); + sessionStore.publish('ios-sim', session); const runtime = fakeCaptureRuntime(session.device, runningStatus()); const { params } = audioParams('ios-sim', sessionStore, session.device, { positionals: ['probe', 'start', '1000', '500'], @@ -307,7 +307,7 @@ test('audio probe starts host helper for iOS simulator audio', async () => { test(ANDROID_AUDIO_CONTRACT_EVIDENCE.testName, async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); const session = makeAndroidSession('android'); - sessionStore.set('android', session); + sessionStore.publish('android', session); const runtime = fakeCaptureRuntime(session.device, runningStatus({ peakDbfs: [-13] })); const { params } = audioParams('android', sessionStore, session.device, { positionals: ['probe', 'start', '1000', '500'], @@ -325,7 +325,7 @@ test(ANDROID_AUDIO_CONTRACT_EVIDENCE.testName, async () => { test('audio probe stop finishes the durable resource and clears the slot', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); const session = makeMacOsSession('macos'); - sessionStore.set('macos', session); + sessionStore.publish('macos', session); const runtime = fakeCaptureRuntime(session.device, runningStatus()); const start = audioParams('macos', sessionStore, session.device, { positionals: ['probe', 'start', '1000', '500'], @@ -355,7 +355,7 @@ test('audio probe stop finishes the durable resource and clears the slot', async test('audio probe status without an active probe reports not-started', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); const session = makeMacOsSession('macos'); - sessionStore.set('macos', session); + sessionStore.publish('macos', session); const { params } = audioParams('macos', sessionStore, session.device, { positionals: ['probe', 'status'], cells: { capture: true, query: false }, @@ -370,7 +370,7 @@ test('audio probe status without an active probe reports not-started', async () test('audio probe forwards daemon millisecond timing to the web query operation', async () => { const sessionStore = makeSessionStore('agent-device-session-audio-'); - sessionStore.set('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); + sessionStore.publish('web', makeSession('web', { device: WEB_DESKTOP_DEVICE })); const audioProbeQuery = vi.fn( async (_input: { action: string; durationMs: number; bucketMs: number }) => runningStatus({ source: 'media-elements', backend: 'agent-browser' }), diff --git a/src/daemon/session-observability/internal/__tests__/session-logs.test.ts b/src/daemon/session-observability/internal/__tests__/session-logs.test.ts index bb675c4384..86b19e980f 100644 --- a/src/daemon/session-observability/internal/__tests__/session-logs.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-logs.test.ts @@ -95,8 +95,7 @@ test('logs path binds only inspect and preserves public status projection', asyn test('logs doctor binds only doctor and merges live-state notes', async () => { const { sessionStore, sessionName } = openSession(); const session = sessionStore.get(sessionName)!; - sessionStore.set(sessionName, { - ...session, + sessionStore.update(sessionStore.lookup(sessionName)!, { appLog: makeTestAppLogResource(session, { backend: 'ios-simulator', state: 'ended', @@ -177,8 +176,7 @@ test.each([ '$action proves the app session before binding start operations', async ({ action, flags = {}, message }) => { const { sessionStore, sessionName } = openSession(); - const session = sessionStore.get(sessionName)!; - sessionStore.set(sessionName, { ...session, appBundleId: undefined }); + sessionStore.update(sessionStore.lookup(sessionName)!, { appBundleId: undefined }); const response = await runLogs( sessionStore, @@ -282,7 +280,7 @@ test('post-transfer SessionStore failure disposes the transferred handle and pre function openSession() { const sessionStore = makeSessionStore(); const sessionName = 'logs-session'; - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...makeSession(sessionName, DEVICE), appBundleId: 'com.example.app', }); diff --git a/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts b/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts index 7f07ef5112..78f56520df 100644 --- a/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts @@ -24,7 +24,7 @@ test.each(NETWORK_RUNTIME_PROJECTION_PARITY)( kind: 'emulator', booted: true, }); - sessionStore.set(sessionName, { + sessionStore.publish(sessionName, { ...session, appBundleId: 'com.example.app', appLog: makeTestAppLogResource(session, { @@ -104,7 +104,7 @@ test('network admission remains fail-closed before parsing invalid input', async name: 'Harmony device', kind: 'device', }); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const runtime = createNetworkRuntime(session.device, async () => emptyAppLogResult('harmonyos'), { available: false, reason: 'unsupported-platform-leaf', @@ -147,7 +147,7 @@ test('an explicit web provider preserves empty-success projection', async () => kind: 'device', target: 'desktop', }); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const runtime = createNetworkRuntime( session.device, async () => ({ source: 'provider', backend: 'agent-browser', entries: [], notes: [] }), @@ -181,7 +181,7 @@ test.each(['browserstack', 'aws-device-farm', 'limrun'] as const)( name: `${provider} device`, kind: 'device', }); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const runtime = createNetworkRuntime( session.device, async () => emptyAppLogResult('android'), diff --git a/src/daemon/session-observability/internal/__tests__/session-network.test.ts b/src/daemon/session-observability/internal/__tests__/session-network.test.ts index 2c54450d96..1bb7d3a0b3 100644 --- a/src/daemon/session-observability/internal/__tests__/session-network.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-network.test.ts @@ -39,7 +39,7 @@ test('network validates the legacy limit after runtime-fact admission', async () name: 'Pixel', kind: 'emulator', }); - sessionStore.set(session.name, session); + sessionStore.publish(session.name, session); const runtime = createNetworkRuntime(session.device, async (input) => emptyAppLogResult('android', input), ); diff --git a/src/daemon/session-observability/internal/__tests__/session-observability.test.ts b/src/daemon/session-observability/internal/__tests__/session-observability.test.ts index a4164676e1..a3df8a5f50 100644 --- a/src/daemon/session-observability/internal/__tests__/session-observability.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-observability.test.ts @@ -104,6 +104,9 @@ test.each(REMOVED_AGGREGATE_PERF_POSITIONALS.map((positionals) => [positionals] function makeAndroidStore() { const sessionStore = makeSessionStore('agent-device-session-observability-'); - sessionStore.set('android', makeAndroidSession('android', { appBundleId: 'com.example.app' })); + sessionStore.publish( + 'android', + makeAndroidSession('android', { appBundleId: 'com.example.app' }), + ); return sessionStore; } diff --git a/src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts b/src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts index 365f668842..c87b1c2582 100644 --- a/src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts @@ -239,7 +239,10 @@ test.each(['shutdown', 'retire'] as const)( function makeStore() { const sessionStore = makeSessionStore('agent-device-perf-runtime-'); - sessionStore.set('android', makeAndroidSession('android', { appBundleId: 'com.example.app' })); + sessionStore.publish( + 'android', + makeAndroidSession('android', { appBundleId: 'com.example.app' }), + ); return sessionStore; } diff --git a/src/daemon/session-store.ts b/src/daemon/session-store.ts index 96affb7e06..996ec78211 100644 --- a/src/daemon/session-store.ts +++ b/src/daemon/session-store.ts @@ -60,11 +60,8 @@ export class SessionStore { } /** - * Returns the LIVE record, not a copy: mutating a field on the result is a durable write - * to store state whether or not {@link SessionStore.set} is called afterwards. Which - * modules may do that is declared in `SESSION_STATE_FIELD_OWNERS` - * (`scripts/layering/session-state.ts`) and enforced by the layering gate's R7, because - * nothing here can check the invariant a given field carries. + * Returns the live record. Field owners may mutate it through their transitions; + * record rebuilds use a lifetime-checked update. R7 enforces field ownership. */ get(name: string): SessionState | undefined { return this.sessions.get(name)?.current; @@ -143,27 +140,6 @@ export class SessionStore { return Object.freeze({ address, session: entry.current, lifetime: entry }); } - /** - * Insert or replace a session. Calling this with a record obtained from - * {@link SessionStore.get} is a no-op — the reference is already stored — so the call - * documents intent rather than committing anything; a genuinely new record needs it. - */ - set(name: string, session: SessionState): void { - // A key with no record is a NEW occupant, and the previous occupant's idle-expiry marker must - // stop explaining this key's absences from now on. Clearing it here rather than at `open` covers - // every way a record arrives — `open`'s provisional record, a record-only `record` session — and - // cannot be forgotten by a future insertion path. A replacing `open` on a live session takes the - // other branch and keeps whatever marker that session will earn for itself. - const entry = this.sessions.get(name); - if (entry) entry.current = session; - else this.publish(name, session); - } - - delete(name: string): boolean { - this.runtimeHints.delete(name); - return this.sessions.delete(name); - } - *values(): IterableIterator { for (const entry of this.sessions.values()) yield entry.current; } diff --git a/test/integration/nightly/concurrency-torture/bindings.ts b/test/integration/nightly/concurrency-torture/bindings.ts index 06fc88d0b1..267aac83c9 100644 --- a/test/integration/nightly/concurrency-torture/bindings.ts +++ b/test/integration/nightly/concurrency-torture/bindings.ts @@ -21,6 +21,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import type { CommandFlags } from '@agent-device/contracts/command'; import type { DaemonRequest } from '../../../../src/daemon/daemon-request.ts'; +import type { SessionRef } from '../../../../src/daemon/session-state.ts'; import type { SessionStore } from '../../../../src/daemon/session-store.ts'; import { resolveRequestExecutionLockPlan } from '../../../../src/daemon/request-binding.ts'; import { shouldLockSessionExecution } from '../../../../src/daemon/daemon-command-registry.ts'; @@ -71,6 +72,7 @@ export type LeaseScope = { * reference can never be mistaken for a live one. */ export type SessionInstance = { + ref: SessionRef; instanceId: number; name: string; deviceId: string; diff --git a/test/integration/nightly/concurrency-torture/harness.ts b/test/integration/nightly/concurrency-torture/harness.ts index f300b586c9..6cabd1ab17 100644 --- a/test/integration/nightly/concurrency-torture/harness.ts +++ b/test/integration/nightly/concurrency-torture/harness.ts @@ -403,9 +403,10 @@ class TortureWorld { ownerStartTime: null, }, }; - this.sessionStore.set(name, state); + const ref = this.sessionStore.publish(name, state); const instanceId = this.nextInstanceId++; const instance: SessionInstance = { + ref, instanceId, name, deviceId: device.id, @@ -449,10 +450,7 @@ class TortureWorld { this.fail('no leaked leases', `release threw for ${instance.name}: ${error.message}`); } this.claims.clear(instance.claim); - const stored = this.sessionStore.get(instance.name); - if (stored?.lease?.leaseId === instance.lease.leaseId) { - this.sessionStore.delete(instance.name); - } + this.sessionStore.retire(instance.ref); instance.reaped = true; if (this.deviceOwner.get(instance.deviceId) === instance.instanceId) { this.deviceOwner.delete(instance.deviceId); diff --git a/test/integration/provider-scenarios/android-tv-remote.test.ts b/test/integration/provider-scenarios/android-tv-remote.test.ts index cb279667fb..bb9baccd7d 100644 --- a/test/integration/provider-scenarios/android-tv-remote.test.ts +++ b/test/integration/provider-scenarios/android-tv-remote.test.ts @@ -31,7 +31,7 @@ test('Provider-backed integration Android TV remote flow sends D-pad keyevents', deviceInventoryProvider: async () => [PROVIDER_SCENARIO_ANDROID_TV], }), async (daemon) => { - daemon.setSession('default', { + daemon.publishSession('default', { name: 'default', device: PROVIDER_SCENARIO_ANDROID_TV, createdAt: Date.now(), diff --git a/test/integration/provider-scenarios/apple-app-log-runtime-provider.test.ts b/test/integration/provider-scenarios/apple-app-log-runtime-provider.test.ts index 61b8d33646..19365aabec 100644 --- a/test/integration/provider-scenarios/apple-app-log-runtime-provider.test.ts +++ b/test/integration/provider-scenarios/apple-app-log-runtime-provider.test.ts @@ -16,7 +16,7 @@ test('provider-inventory Apple logs doctor stays on the scoped Apple tool provid appleToolProvider: () => appleTool.provider, deviceInventoryProvider: async () => [PROVIDER_SCENARIO_IOS_SIMULATOR], }); - daemon.setSession('default', { + daemon.publishSession('default', { name: 'default', device: PROVIDER_SCENARIO_IOS_SIMULATOR, appBundleId: 'com.example.app', diff --git a/test/integration/provider-scenarios/harness.ts b/test/integration/provider-scenarios/harness.ts index 01159d16bb..34c024dba6 100644 --- a/test/integration/provider-scenarios/harness.ts +++ b/test/integration/provider-scenarios/harness.ts @@ -83,7 +83,7 @@ export type ProviderScenarioHarness = { token: string; session: (name?: string) => SessionState | undefined; sessionDir: (name?: string) => string; - setSession: (name: string, session: SessionState) => void; + publishSession: (name: string, session: SessionState) => void; close: () => Promise; }; @@ -229,7 +229,7 @@ export async function createProviderScenarioHarness( token: PROVIDER_SCENARIO_TOKEN, session: (name = 'default') => sessionStore.get(name), sessionDir: (name = 'default') => sessionStore.resolveSessionDir(name), - setSession: (name, session) => sessionStore.set(name, session), + publishSession: (name, session) => sessionStore.publish(name, session), close: async () => { await deviceRuntimeGateway.shutdown(); await removeProviderScenarioTempDir(sessionDir); diff --git a/test/integration/provider-scenarios/ios-fold.test.ts b/test/integration/provider-scenarios/ios-fold.test.ts index 6dc08a5000..186f1bb774 100644 --- a/test/integration/provider-scenarios/ios-fold.test.ts +++ b/test/integration/provider-scenarios/ios-fold.test.ts @@ -105,7 +105,7 @@ test('timed fold keyframes reach simulator HID through the public client and dae }, }), }); - daemon.setSession( + daemon.publishSession( 'default', makeIosAppSession('default', { device: PROVIDER_SCENARIO_IOS_SIMULATOR }), ); @@ -278,7 +278,7 @@ async function runFoldLedgerScenario(params: { deviceInventoryProvider: async () => [PROVIDER_SCENARIO_IOS_SIMULATOR], appleToolProvider: () => provider, }); - daemon.setSession( + daemon.publishSession( 'default', makeIosAppSession('default', { device: PROVIDER_SCENARIO_IOS_SIMULATOR }), ); diff --git a/test/integration/provider-scenarios/ios-wait-selector.test.ts b/test/integration/provider-scenarios/ios-wait-selector.test.ts index 0feac0c5e1..4894ce235b 100644 --- a/test/integration/provider-scenarios/ios-wait-selector.test.ts +++ b/test/integration/provider-scenarios/ios-wait-selector.test.ts @@ -54,7 +54,7 @@ test.each([ }, }), }); - daemon.setSession('default', { + daemon.publishSession('default', { ...makeIosAppSession('default'), device: PROVIDER_SCENARIO_IOS_SIMULATOR, }); diff --git a/test/integration/provider-scenarios/network-runtime-provider.test.ts b/test/integration/provider-scenarios/network-runtime-provider.test.ts index 3172f7d8e2..84f61db51c 100644 --- a/test/integration/provider-scenarios/network-runtime-provider.test.ts +++ b/test/integration/provider-scenarios/network-runtime-provider.test.ts @@ -20,7 +20,7 @@ test('iOS simulator network recovery stays on the request-scoped Apple tool prov appleToolProvider: () => appleTool.provider, deviceInventoryProvider: async () => [PROVIDER_SCENARIO_IOS_SIMULATOR], }); - daemon.setSession('default', { + daemon.publishSession('default', { name: 'default', device: PROVIDER_SCENARIO_IOS_SIMULATOR, appBundleId: 'com.example.app', diff --git a/test/integration/provider-scenarios/stale-provider-runtime-admission.fixtures.ts b/test/integration/provider-scenarios/stale-provider-runtime-admission.fixtures.ts index cf618d1f0b..8b6895ff1b 100644 --- a/test/integration/provider-scenarios/stale-provider-runtime-admission.fixtures.ts +++ b/test/integration/provider-scenarios/stale-provider-runtime-admission.fixtures.ts @@ -74,7 +74,7 @@ export async function inspectProviderDeploymentAdmission(params: { fs.writeFileSync(appPath, 'fixture'); try { - sessionStore.set('default', { + sessionStore.publish('default', { name: 'default', createdAt: 1, actions: [], diff --git a/test/integration/provider-scenarios/vega-tv-remote.test.ts b/test/integration/provider-scenarios/vega-tv-remote.test.ts index 0fdd4c4c6e..3ddad7f116 100644 --- a/test/integration/provider-scenarios/vega-tv-remote.test.ts +++ b/test/integration/provider-scenarios/vega-tv-remote.test.ts @@ -54,7 +54,7 @@ test('Provider-backed Vega TV flow routes lifecycle and every remote control as deviceInventoryProvider: async () => [VEGA_TV], }), async (daemon) => { - daemon.setSession('default', { + daemon.publishSession('default', { name: 'default', device: VEGA_TV, createdAt: Date.now(), From c52b822d66299fe16b59abeb96c516a9c77e3aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 4 Oct 2026 04:14:54 +0200 Subject: [PATCH 2/4] docs: clarify stored fixture record identity --- src/__tests__/test-utils/store-factory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__tests__/test-utils/store-factory.ts b/src/__tests__/test-utils/store-factory.ts index c646a9b861..6ea8b8f073 100644 --- a/src/__tests__/test-utils/store-factory.ts +++ b/src/__tests__/test-utils/store-factory.ts @@ -12,6 +12,7 @@ export function makeStoredSessionRef(session: SessionState, address = session.na return makeSessionStore().publish(address, session); } +/** Publishes a fixture or reuses its current record. After an update, pass the rebuilt record. */ export function storeSessionForTest( store: SessionStore, session: SessionState, From 1dc77a69a347696981a0b52b75c97f4cca733f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 4 Oct 2026 04:18:35 +0200 Subject: [PATCH 3/4] test: reuse current fixture sessions and remove dead store methods --- .../src/recording/failed-finish.test.ts | 4 ---- .../__tests__/replay-suite/session-test-suite.test.ts | 11 ++++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/platform-android/src/recording/failed-finish.test.ts b/packages/platform-android/src/recording/failed-finish.test.ts index 38f9be8955..14d02ff0b3 100644 --- a/packages/platform-android/src/recording/failed-finish.test.ts +++ b/packages/platform-android/src/recording/failed-finish.test.ts @@ -142,10 +142,6 @@ async function adoptAndroidRecording(params: { const sessionsDir = mkdtempForTestSync('agent-device-android-failed-finish-session-'); let session: AndroidRecordingSession = {}; const sessionStore = { - get: () => session, - set: (_name: string, next: AndroidRecordingSession) => { - session = next; - }, resolveSessionDir: (name: string) => path.join(sessionsDir, name), }; const binding = { diff --git a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts index 443cda9c61..5106db8f23 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts @@ -37,6 +37,7 @@ import { makeMacOsSession, } from '../../../__tests__/test-utils/session-factories.ts'; import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; +import { storeSessionForTest } from '../../../__tests__/test-utils/store-factory.ts'; const ANDROID_ONE: DeviceInfo = { platform: 'android', @@ -354,7 +355,11 @@ test('test stops before retrying when a rejected close leaves the prior macOS se }, invoke: async (req) => { replayAttempts += 1; - sessionStore.publish(req.session, makeMacOsSession(req.session)); + storeSessionForTest( + sessionStore, + sessionStore.get(req.session) ?? makeMacOsSession(req.session), + req.session, + ); return { ok: false, error: { code: 'COMMAND_FAILED', message: 'open "System Settings" failed' }, @@ -497,7 +502,7 @@ test('test aggregates snapshot diagnostics from replay session samples', async ( backend: 'android', platform: 'android', }); - sessionStore.publish(req.session, session); + storeSessionForTest(sessionStore, session, req.session); return { ok: true, data: { replayed: 1, healed: 0 } }; }, }); @@ -554,7 +559,7 @@ test('test aggregates snapshot diagnostics from failed replay session samples', backend: 'android', platform: 'android', }); - sessionStore.publish(req.session, session); + storeSessionForTest(sessionStore, session, req.session); return { ok: false, error: { code: 'COMMAND_FAILED', message: 'open failed' }, From d5472b0523774bd26573d6d197881dadcdad5541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 4 Oct 2026 07:56:29 +0200 Subject: [PATCH 4/4] fix: distinguish missing capture fixture sessions --- .../capture-kit/src/durable-capture/session-binding.fixtures.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts b/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts index 5598a96464..43717f4573 100644 --- a/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts +++ b/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts @@ -20,7 +20,7 @@ export function makeCaptureFixtureStore(resolveSessionDir: (address: string) }, lookup: (address: string): FixtureSessionRef => { const entry = entries.get(address); - if (!entry) throw new AppError('COMMAND_FAILED', 'Test session retired'); + if (!entry) throw new AppError('COMMAND_FAILED', 'Test session not found'); return Object.freeze({ address, session: entry.current, lifetime: entry }); }, resolveCurrent,