diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 3c8410318d2..0be92f7a94e 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -2715,31 +2715,24 @@ "providers": ["local", "daemon", "ssh", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["local", "daemon", "ssh", "remote-runtime"], - "coverageNotes": "The always-mounted desktop subscriber is covered through pure projection/reference and graph-sync contracts. All providers use the same serialized fields. WSL, platform launch, liveness, transport and mobile rendering are unaffected.", + "coverageNotes": "The always-mounted desktop subscriber uses the reviewed pure projection/cache resource path and graph-sync contracts; this gate does not independently prove every serialized field reaches a mobile recipient. All providers use the same serialized fields. WSL, platform launch, liveness, transport and mobile rendering are unaffected.", "motivatingLinks": [ "https://github.com/stablyai/orca/blob/main/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts" ], "invariant": "The desktop agent-status comparison projection remains identical for every serialized field, key membership and freshness bucket while avoiding a full aggregate join when per-entry serialized content is unchanged.", - "oracle": "500 statuses with accumulated assistant previews receive 50 same-bucket heartbeat replacements with zero aggregate joins. A bucket boundary and assistant-detail change invalidate the projection. Existing reference comparisons cover field content, ordering, insertion/removal and subscriber decisions.", + "oracle": "500 statuses with accumulated assistant previews receive 50 same-bucket heartbeat replacements with zero aggregate joins. A bucket boundary and assistant-detail change invalidate the projection. Retained resource checks additionally cover entry-reference reuse without sort/join, changed-file draft hashing, localeCompare avoidance, and changed open-file/browser serialization work. Removed private field/membership matrices no longer provide an exhaustive serialization claim.", "commands": [ - "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts src/renderer/src/runtime/sync-runtime-graph.test.ts" + "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts src/renderer/src/runtime/sync-runtime-graph.test.ts" ], "testFiles": [ "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts", - "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts", "src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts", "src/renderer/src/runtime/sync-runtime-graph.test.ts" ], "assertionRefs": [ { "file": "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts", - "assertions": [ - "does not rejoin accumulated previews for timestamp-only heartbeats", - "still rebuilds when an entry changes", - "still rebuilds when a pane is removed, even though every survivor is reused", - "still rebuilds when key membership swaps at a constant entry count", - "still rebuilds when a pane is added" - ] + "assertions": ["does not rejoin accumulated previews for timestamp-only heartbeats"] } ], "evidenceRuns": [ @@ -2748,6 +2741,7 @@ "runner": "local", "platform": "macos", "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts src/renderer/src/runtime/sync-runtime-graph.test.ts", + "currentCommand": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts src/renderer/src/runtime/sync-runtime-graph.test.ts", "result": "passed", "summary": "38 tests passed across four projection and graph-sync files; renderer typecheck passed.", "durationSeconds": 2.7 diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 1cdeb208bd7..5ffb642ea82 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -11147,7 +11147,6 @@ "src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts": 564, "src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx": 1804, "src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx": 1953, - "src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts": 80, "src/renderer/src/runtime/host-structured-agent-availability.test.ts": 69, "src/renderer/src/runtime/host-structured-agents.test.ts": 512, "src/renderer/src/runtime/host-unverifiable-agent-row-presentation.test.ts": 1432, @@ -11277,7 +11276,6 @@ "src/renderer/src/runtime/server-layout-frames.test.ts": 1464, "src/renderer/src/runtime/server-layout-view-report.test.ts": 2249, "src/renderer/src/runtime/session-tab-rows-main-window-parity.test.ts": 299, - "src/renderer/src/runtime/sleeping-records-to-layout-owner.test.ts": 1443, "src/renderer/src/runtime/structured-agent-session-client.test.ts": 270, "src/renderer/src/runtime/structured-agent-session-close.test.ts": 91, "src/renderer/src/runtime/structured-agent-session-host-capability-scaling.test.tsx": 1739, @@ -11292,7 +11290,6 @@ "src/renderer/src/runtime/structured-session-orca-session-id.test.ts": 295, "src/renderer/src/runtime/structured-session-tab-inventory.test.ts": 1338, "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts": 104, - "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts": 61, "src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts": 872, "src/renderer/src/runtime/sync-runtime-graph-browser.test.ts": 321, "src/renderer/src/runtime/sync-runtime-graph-chat-tab-order.test.ts": 262, diff --git a/src/renderer/src/runtime/host-model-catalog-snapshots.test.ts b/src/renderer/src/runtime/host-model-catalog-snapshots.test.ts index 34a2f1cdf5d..0b761452815 100644 --- a/src/renderer/src/runtime/host-model-catalog-snapshots.test.ts +++ b/src/renderer/src/runtime/host-model-catalog-snapshots.test.ts @@ -92,12 +92,6 @@ describe('host model catalog snapshots', () => { }) ).toBeUndefined() }) - - it('drops an agent’s answers when the host says the account has no list', () => { - recordHostModelCatalogSnapshot(LOCAL, 'codex', 'wt-1', list(true)) - recordHostModelCatalogSnapshot(LOCAL, 'codex', 'wt-1', { origin: 'unknown' }) - expect(readHostModelCatalogSnapshot(LOCAL, 'codex', NEW_CHAT)).toBeUndefined() - }) }) const SAVED_ONLY = 'agent-session.model-catalog.saved-only.v1' @@ -108,12 +102,6 @@ function registerAgents(hostId: string, runtimeId: string | null): void { mocks.agentListeners.forEach((listener) => listener()) } -/** The host saved lists for Grok, OpenCode and OMP only; nothing for Claude or Codex. */ -function savedFor(agents: readonly string[]) { - return async (_target: unknown, _method: string, params: { agent: string }) => - agents.includes(params.agent) ? list(false) : { origin: 'unknown' } -} - describe('host model catalog snapshots sync', () => { let stop: (() => void) | null = null beforeEach(() => { @@ -150,26 +138,6 @@ describe('host model catalog snapshots sync', () => { expect(mocks.call).toHaveBeenCalledTimes(1) }) - it('loads every registered agent saved-only, picked or not; an agent with nothing saved stays unknown', async () => { - mocks.capabilities = [SAVED_ONLY] - mocks.call.mockImplementation(savedFor(['grok', 'opencode', 'omp'])) - stop = installHostModelCatalogSnapshotsSync() - // Nothing is read until the host's agents are known; their arrival reads them. - await Promise.resolve() - expect(mocks.call).not.toHaveBeenCalled() - registerAgents('local', null) - await vi.waitFor(() => - expect(readHostModelCatalogSnapshot(LOCAL, 'omp', NEW_CHAT)).toEqual(list(false)) - ) - expect(mocks.call.mock.calls.map(([, method, params]) => [method, params])).toEqual( - REGISTERED.map(({ agent }) => ['agentSession.modelCatalog', { agent, savedOnly: true }]) - ) - expect(readHostModelCatalogSnapshot(LOCAL, 'grok', NEW_CHAT)).toEqual(list(false)) - expect(readHostModelCatalogSnapshot(LOCAL, 'opencode', NEW_CHAT)).toEqual(list(false)) - expect(readHostModelCatalogSnapshot(LOCAL, 'claude', NEW_CHAT)).toBeUndefined() - expect(readHostModelCatalogSnapshot(LOCAL, 'codex', NEW_CHAT)).toBeUndefined() - }) - it('reads again for the new account after an account change, dropping an answer sent before it', async () => { mocks.capabilities = [SAVED_ONLY] registerAgents('local', null) diff --git a/src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts b/src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts index 7395e54035f..f405c77899d 100644 --- a/src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts +++ b/src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts @@ -1,9 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { clearRuntimeEnvironmentConnectionGenerationsForTests } from '@/store/slices/runtime-status' import { - getParkedHostSessionMirrorWaiterCountForTests, markHostSessionMirrorHydrated, - MAX_PARKED_HOST_SESSION_MIRROR_WAITERS, parkUntilHostSessionMirrorHydrates, resetHostSessionMirrorHydrationForTests } from './host-session-mirror-hydration' @@ -27,14 +25,4 @@ describe('host session mirror hydration drain', () => { expect(() => markHostSessionMirrorHydrated(ENVIRONMENT_ID)).not.toThrow() expect(secondReplay).toHaveBeenCalledTimes(1) }) - - it('bounds parked waiter growth when environments churn', () => { - for (let index = 0; index < MAX_PARKED_HOST_SESSION_MIRROR_WAITERS + 4; index += 1) { - parkUntilHostSessionMirrorHydrates(`env-${index}`, 'repo::worktree', () => {}) - } - - expect(getParkedHostSessionMirrorWaiterCountForTests()).toBe( - MAX_PARKED_HOST_SESSION_MIRROR_WAITERS - ) - }) }) diff --git a/src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts b/src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts deleted file mode 100644 index 16688e790b7..00000000000 --- a/src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH } from '../../../shared/runtime-types' -import { hostSnapshotAffirmsClientHostedPages } from './host-session-snapshot-authority' - -const REAL_EPOCH = 'headless:abc' - -describe('reading whether a snapshot answers for client-hosted pages', () => { - it('affirms a fully published snapshot carrying no unreconciled flag', () => { - expect( - hostSnapshotAffirmsClientHostedPages({ publicationEpoch: REAL_EPOCH, snapshotVersion: 3 }) - ).toBe(true) - }) - - // Restarted runtime’s real epoch/version affirms its terminals, not client-hosted browser pages awaiting reconciliation. - it('does not affirm a restarted runtime that has not heard from its hosts yet', () => { - expect( - hostSnapshotAffirmsClientHostedPages({ - publicationEpoch: REAL_EPOCH, - snapshotVersion: 3, - clientHostedPagesUnreconciled: true - }) - ).toBe(false) - }) - - // Inherited from the worktree-level gate: the placeholder pair is the runtime saying "ask later". - it('does not affirm the unpublished-worktree placeholder frame', () => { - expect( - hostSnapshotAffirmsClientHostedPages({ - publicationEpoch: UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH, - snapshotVersion: 0 - }) - ).toBe(false) - }) - - it('does not affirm when the placeholder frame also carries the unreconciled flag', () => { - expect( - hostSnapshotAffirmsClientHostedPages({ - publicationEpoch: UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH, - snapshotVersion: 0, - clientHostedPagesUnreconciled: true - }) - ).toBe(false) - }) - - // Only the placeholder epoch marks a synthesized frame; version zero alone is a legitimate state. - it('affirms a real epoch at version zero', () => { - expect( - hostSnapshotAffirmsClientHostedPages({ publicationEpoch: REAL_EPOCH, snapshotVersion: 0 }) - ).toBe(true) - }) - - // Explicit `undefined` as well as an absent key: a host that omits the flag means reconciled. - it('affirms when the unreconciled flag is absent or explicitly undefined', () => { - expect( - hostSnapshotAffirmsClientHostedPages({ publicationEpoch: REAL_EPOCH, snapshotVersion: 1 }) - ).toBe(true) - expect( - hostSnapshotAffirmsClientHostedPages({ - publicationEpoch: REAL_EPOCH, - snapshotVersion: 1, - clientHostedPagesUnreconciled: undefined - }) - ).toBe(true) - }) -}) diff --git a/src/renderer/src/runtime/layout-frame-position.test.ts b/src/renderer/src/runtime/layout-frame-position.test.ts index d3169309dc2..b367a1eb366 100644 --- a/src/renderer/src/runtime/layout-frame-position.test.ts +++ b/src/renderer/src/runtime/layout-frame-position.test.ts @@ -12,25 +12,6 @@ import { describe('afterLayoutFrame', () => { afterEach(() => endLayoutFramePositions()) - it('runs at once once the stream ended, or for a reply that names no frame', () => { - endLayoutFramePositions() - const run = vi.fn() - afterLayoutFrame(9, run) - expect(run).toHaveBeenCalledTimes(1) - - noteLayoutFramePosition(3) - afterLayoutFrame(undefined, run) - expect(run).toHaveBeenCalledTimes(2) - }) - - it('runs at once while the window has opened no stream', () => { - _resetLayoutFramePositionForTests() - const run = vi.fn() - afterLayoutFrame(2, run) - expect(run).toHaveBeenCalledOnce() - expect(layoutFramesFlowing()).toBe(false) - }) - it('waits for an opened stream before its first frame: that frame, or its end', () => { _resetLayoutFramePositionForTests() beginLayoutFramePositions() diff --git a/src/renderer/src/runtime/layout-owner-pane-intents.test.ts b/src/renderer/src/runtime/layout-owner-pane-intents.test.ts index 7a97053daf0..40257ad880b 100644 --- a/src/renderer/src/runtime/layout-owner-pane-intents.test.ts +++ b/src/renderer/src/runtime/layout-owner-pane-intents.test.ts @@ -2,14 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { _resetLayoutOwnerPaneIntentsForTests, expectPaneToClose, - expectTabRowFields, holdTabPaneTree, isTabPaneTreeHeld, onTabPaneTreeRedraw, - panesLeftAfterPendingCloses, requestTabPaneTreeRedraw, - takePaneCloseIntent, - takeTabRowFields + takePaneCloseIntent } from './layout-owner-pane-intents' describe('layout-owner pane intents', () => { @@ -41,16 +38,6 @@ describe('layout-owner pane intents', () => { expect(takePaneCloseIntent('tab-1', 'leaf-64')).toBe('exit') }) - it('hands out a new row’s fields once, until released', () => { - const release = expectTabRowFields('tab-1', { pendingActivationSpawn: true }) - expect(takeTabRowFields('tab-1')).toEqual({ pendingActivationSpawn: true }) - expect(takeTabRowFields('tab-1')).toBeNull() - - expectTabRowFields('tab-2', { pendingActivationSpawn: true }) - release() - expect(takeTabRowFields('tab-2')).toEqual({ pendingActivationSpawn: true }) - }) - it('holds a tab until its last hold is released, then tells the listener once', () => { const listener = vi.fn() onTabPaneTreeRedraw(listener) @@ -69,11 +56,4 @@ describe('layout-owner pane intents', () => { requestTabPaneTreeRedraw('tab-2') expect(listener).toHaveBeenLastCalledWith('tab-2') }) - - it('counts the panes no pending close of this tab takes', () => { - expectPaneToClose('tab-1', 'a', 'exit') - expectPaneToClose('tab-2', 'b', 'user') - - expect(panesLeftAfterPendingCloses('tab-1', ['a', 'b', 'c'])).toBe(2) - }) }) diff --git a/src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts b/src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts index ab330d7fad0..5d691bc4015 100644 --- a/src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts +++ b/src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts @@ -143,17 +143,6 @@ describe('a mirrored remote row decays on the replica clock, not the host clock' expect(mirroredRowIsFreshAt(store, T0 + AGENT_STATUS_STALE_AFTER_MS + 1)).toBe(false) }) - it('keeps decaying while the host publishes nothing new', () => { - // No fresh snapshot arrives during silence, so replica must age its last evidence locally. - const store = seedPairedClientStore() - applyChangedHostSnapshot(store, T0 + HOST_SKEW_MS, T0) - - for (const elapsed of [1_000, AGENT_STATUS_STALE_AFTER_MS / 2]) { - expect(mirroredRowIsFreshAt(store, T0 + elapsed)).toBe(true) - } - expect(mirroredRowIsFreshAt(store, T0 + AGENT_STATUS_STALE_AFTER_MS * 3)).toBe(false) - }) - it('does not restart the window when the host republishes evidence already seen', () => { const store = seedPairedClientStore() const hostNow = T0 + HOST_SKEW_MS diff --git a/src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts b/src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts index ff3a7b87cf1..aaab6bad4c3 100644 --- a/src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts +++ b/src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts @@ -39,8 +39,6 @@ const HOST_EPOCH = 'host-epoch-1' const T0 = 1_700_000_000_000 /** The laptop was closed well past the agent-status freshness boundary. */ const LONG_SLEEP_MS = AGENT_STATUS_STALE_AFTER_MS * 6 -/** A blip the client rides out without its own status going stale. */ -const BRIEF_DROP_MS = 5_000 type TestStore = ReturnType @@ -267,14 +265,6 @@ describe('STA-3107: sidebar agent rows survive a paired-client sleep/wake reconn resetRendererOwnedAgentStatusPanesForTests() }) - it('control: a brief transport drop keeps every pane in the sidebar', () => { - const store = seedPairedClientStore() - const reconnected = runSleepWakeReconnect(store, BRIEF_DROP_MS) - - expect(reconnected.tabIds).toHaveLength(PANES.length) - expect(reconnected.rowPaneKeys.sort()).toEqual(PANES.map(mirrorPaneKey).sort()) - }) - it('keeps a sidebar row for every still-live pane after a long sleep', () => { const store = seedPairedClientStore() const reconnected = runSleepWakeReconnect(store, LONG_SLEEP_MS) diff --git a/src/renderer/src/runtime/pending-pane-title-renames.test.ts b/src/renderer/src/runtime/pending-pane-title-renames.test.ts index ca6ff36dc91..43ca9eea8a6 100644 --- a/src/renderer/src/runtime/pending-pane-title-renames.test.ts +++ b/src/renderer/src/runtime/pending-pane-title-renames.test.ts @@ -109,28 +109,4 @@ describe('a local pane rename over host snapshots', () => { [leafId]: 'second' }) }) - - it('names each renamed pane in exactly one push', () => { - const leafId = '66666666-6666-4666-8666-666666666666' - const sibling = '77777777-7777-4777-8777-777777777777' - rename(leafId, 'mine') - - expect(takeUnsentPaneTitleRenames([leafId, sibling])).toEqual([leafId]) - expect(takeUnsentPaneTitleRenames([leafId, sibling])).toEqual([]) - }) - - it("holds nothing for a window's own local tab", () => { - const leafId = '88888888-8888-4888-8888-888888888888' - notePendingPaneTitleRename({ - leafId, - title: 'mine', - layout: layoutFor(leafId, 'old', 'local-pty'), - now: T0 - }) - - expect(takeUnsentPaneTitleRenames([leafId])).toEqual([]) - expect(withPendingPaneTitleRenames({ [leafId]: 'old' }, [leafId], T0)).toEqual({ - [leafId]: 'old' - }) - }) }) diff --git a/src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts b/src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts index 21f63f40744..4b60b78b99b 100644 --- a/src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts +++ b/src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts @@ -206,103 +206,6 @@ describe('#12906: remote agent rows keep the host-published last assistant messa resetRendererOwnedAgentStatusPanesForTests() }) - it('shows the message on a pane this renderer owns from the byte stream', () => { - const store = seedPairedClientStore() - // Host hook receives completion text while client OSC independently observes idle. - const mirrored = applyHostSnapshot( - store, - makeHostSnapshot({ - snapshotVersion: 1, - hostNow: T0 - 1_000, - statusByHostTabId: { - [OWNED.hostTabId]: { - state: 'done', - lastAssistantMessage: HOST_MESSAGE, - stateStartedAt: T0 - 2_000 - } - } - }), - T0 - ) - expect(mirrored, 'harness must reach the mirror, not stop at the freshness gate').toBe(true) - replayClientByteStatus(store, OWNED, 'done', T0) - // Republication is where the loss happens: the client now owns the key. - applyHostSnapshot( - store, - makeHostSnapshot({ - snapshotVersion: 2, - hostNow: T0 + 1_000, - statusByHostTabId: { - [OWNED.hostTabId]: { - state: 'done', - lastAssistantMessage: HOST_MESSAGE, - stateStartedAt: T0 - 2_000 - } - } - }), - T0 + 2_000 - ) - - const messages = observeRowMessages(store, T0 + 2_000) - expect(messages, JSON.stringify(messages, null, 2)).toMatchObject({ - [mirrorPaneKey(OWNED)]: HOST_MESSAGE - }) - }) - - it('control: a pane this renderer never wrote status for already showed it', () => { - const store = seedPairedClientStore() - applyHostSnapshot( - store, - makeHostSnapshot({ - snapshotVersion: 1, - hostNow: T0 - 1_000, - statusByHostTabId: { - [CEDED.hostTabId]: { - state: 'done', - lastAssistantMessage: HOST_MESSAGE, - stateStartedAt: T0 - 2_000 - } - } - }), - T0 - ) - - // Pins the causal boundary to the ownership fence, not the mirror as a whole. - expect(observeRowMessages(store, T0)[mirrorPaneKey(CEDED)]).toBe(HOST_MESSAGE) - }) - - it('never invents a message the host did not publish (old host / new client)', () => { - const store = seedPairedClientStore() - applyHostSnapshot( - store, - makeHostSnapshot({ - snapshotVersion: 1, - hostNow: T0 - 1_000, - statusByHostTabId: { - [OWNED.hostTabId]: { state: 'done', stateStartedAt: T0 - 2_000 } - } - }), - T0 - ) - replayClientByteStatus(store, OWNED, 'done', T0) - applyHostSnapshot( - store, - makeHostSnapshot({ - snapshotVersion: 2, - hostNow: T0 + 1_000, - statusByHostTabId: { - [OWNED.hostTabId]: { state: 'done', stateStartedAt: T0 - 2_000 } - } - }), - T0 + 2_000 - ) - - const messages = observeRowMessages(store, T0 + 2_000) - // Guard against a vacuous pass: the row must exist for its blank message to mean anything. - expect(Object.keys(messages)).toContain(mirrorPaneKey(OWNED)) - expect(messages[mirrorPaneKey(OWNED)]).toBeUndefined() - }) - it('keeps a mirrored message when a later host frame omits the field (old host / new client)', () => { const store = seedPairedClientStore() applyHostSnapshot( diff --git a/src/renderer/src/runtime/sleeping-records-to-layout-owner.test.ts b/src/renderer/src/runtime/sleeping-records-to-layout-owner.test.ts deleted file mode 100644 index d82880a2e39..00000000000 --- a/src/renderer/src/runtime/sleeping-records-to-layout-owner.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as AgentStatusModule from '@/lib/agent-status' -import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' -import { - createTestStore, - makeRuntimeOwnedWorktree, - makeWorktree, - runtimeOwnedRepoState, - seedStore -} from '../store/slices/store-test-helpers' -import { createStoreCascadesMockApi } from '../store/slices/store-cascades-test-harness' -import { recordPaneWakeThroughLayoutOwner } from '../store/terminals/terminal-sleep-layout-owner' -import { followSleepingRecordsInLayoutOwner } from './sleeping-records-to-layout-owner' - -const mocks = vi.hoisted(() => ({ - callRuntimeRpc: vi.fn( - async (_target: unknown, _method: string, _params: { command: unknown }) => ({ - ok: true, - result: {} - }) - ) -})) -const held: { store?: ReturnType } = {} -vi.mock('@/store', () => ({ - get useAppStore() { - return held.store - } -})) -vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.callRuntimeRpc })) -vi.mock('sonner', () => ({ - toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn() } -})) -vi.mock('@/lib/agent-status', async (importOriginal) => { - const actual = await importOriginal() - return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) } -}) - -createStoreCascadesMockApi() - -const LOCAL = 'repo1::/path/wt' -const PAIRED = 'repo2::/remote/wt' -const TAB = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee' -const PANE = `${TAB}:11111111-1111-4111-8111-111111111111` -const PAIRED_TAB = 'bbbbbbbb-cccc-4ddd-8eee-ffffffffffff' -const PAIRED_PANE = `${PAIRED_TAB}:22222222-2222-4222-8222-222222222222` - -function record(paneKey: string, worktreeId: string): SleepingAgentSessionRecord { - return { - paneKey, - worktreeId, - agent: 'codex', - providerSession: { key: 'session_id', id: paneKey }, - prompt: '', - state: 'working', - capturedAt: 1, - updatedAt: 1, - origin: 'live' - } -} - -function terminalRow(id: string, worktreeId: string) { - return { - id, - ptyId: null, - worktreeId, - title: 'Terminal 1', - customTitle: null, - color: null, - sortOrder: 0, - createdAt: 1 - } -} - -function setRecords(records: Record): void { - held.store!.setState({ sleepingAgentSessionsByPaneKey: records }) -} - -const sent = () => mocks.callRuntimeRpc.mock.calls.map((call) => call[2].command) - -beforeAll(() => { - const store = createTestStore() - seedStore( - store, - runtimeOwnedRepoState('env-1', 'repo2', '/remote', { - worktreesByRepo: { - repo1: [makeWorktree({ id: LOCAL, repoId: 'repo1', path: '/path/wt' })], - repo2: [makeRuntimeOwnedWorktree({ id: PAIRED, repoId: 'repo2' }, 'env-1')] - }, - tabsByWorktree: { - [LOCAL]: [terminalRow(TAB, LOCAL)], - [PAIRED]: [terminalRow(PAIRED_TAB, PAIRED)] - }, - // Restore's records are main's own. - sleepingAgentSessionsByPaneKey: { [PANE]: { ...record(PANE, LOCAL), state: 'done' } } - }) - ) - held.store = store -}) - -beforeEach(() => { - mocks.callRuntimeRpc.mockClear() -}) - -describe('main’s copy of the window’s resume records', () => { - it('sends nothing for the records restore loaded, and subscribes once', () => { - followSleepingRecordsInLayoutOwner() - followSleepingRecordsInLayoutOwner() - expect(mocks.callRuntimeRpc).not.toHaveBeenCalled() - - setRecords({}) - expect(mocks.callRuntimeRpc).toHaveBeenCalledTimes(1) - }) - - it('records an added record without stopping its pane, and a dropped one without starting it', () => { - const live = record(PANE, LOCAL) - setRecords({ [PANE]: live }) - setRecords({}) - - expect(sent()).toEqual([ - { - type: 'sleep', - workspace: LOCAL, - tabId: TAB, - paneKeys: [PANE], - records: [live], - recordOnly: true - }, - { type: 'wake', workspace: LOCAL, tabId: TAB, paneKeys: [PANE], recordOnly: true } - ]) - }) - - it('sends nothing for a paired server’s workspace', () => { - setRecords({ [PAIRED_PANE]: record(PAIRED_PANE, PAIRED) }) - setRecords({}) - - expect(mocks.callRuntimeRpc).not.toHaveBeenCalled() - }) - - it('sends a resumed pane’s wake, record-only so main starts nothing, before the record drop', () => { - setRecords({ [PANE]: record(PANE, LOCAL) }) - mocks.callRuntimeRpc.mockClear() - - recordPaneWakeThroughLayoutOwner(held.store!.getState(), LOCAL, PANE) - held.store!.getState().clearSleepingAgentSession(PANE) - - expect(sent()).toEqual([ - { type: 'wake', workspace: LOCAL, tabId: TAB, paneKeys: [PANE], recordOnly: true }, - { type: 'wake', workspace: LOCAL, tabId: TAB, paneKeys: [PANE], recordOnly: true } - ]) - }) -}) diff --git a/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts b/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts index 00d5c3de905..9a7b2944a5f 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts @@ -86,21 +86,6 @@ describe('agent-status projection join short circuit', () => { expect(sorts).toBe(0) }) - it('caches the new map identity on the short-circuit path', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const map = mapOf([0, 1]) - buildRuntimeMobileAgentStatusProjectionForTests(map) - const again = respread(map) - buildRuntimeMobileAgentStatusProjectionForTests(again) - - // A repeat call with the same identity must hit the identity early-out, not re-walk the keys. - const { joins, sorts } = countJoins(() => - buildRuntimeMobileAgentStatusProjectionForTests(again) - ) - expect(joins).toBe(0) - expect(sorts).toBe(0) - }) - it('does not rejoin accumulated previews for timestamp-only heartbeats', () => { let map = mapOf(Array.from({ length: 500 }, (_, index) => index)) for (const paneKey of Object.keys(map)) { @@ -135,61 +120,4 @@ describe('agent-status projection join short circuit', () => { }) ).not.toBe(first) }) - - it('still rebuilds when an entry changes', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const map = mapOf([0, 1]) - const first = buildRuntimeMobileAgentStatusProjectionForTests(map) - - const changed = { ...map, 'tab-1:leaf-0': makeEntry(1, { state: 'idle' }) } - const { result, joins } = countJoins(() => - buildRuntimeMobileAgentStatusProjectionForTests(changed) - ) - - expect(result).not.toBe(first) - expect(joins).toBeGreaterThan(0) - }) - - it('still rebuilds when a pane is removed, even though every survivor is reused', () => { - // The reuse check alone cannot see a removal; only the entry-count check does. - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const map = mapOf([0, 1]) - const first = buildRuntimeMobileAgentStatusProjectionForTests(map) - - const removed = { 'tab-0:leaf-0': map['tab-0:leaf-0'] } - const result = buildRuntimeMobileAgentStatusProjectionForTests(removed) - - expect(result).not.toBe(first) - expect(result).toBe( - buildRuntimeMobileAgentStatusProjectionForTests({ - 'tab-0:leaf-0': map['tab-0:leaf-0'] - }) - ) - }) - - it('still rebuilds when key membership swaps at a constant entry count', () => { - // The entry-count check cannot see a same-size swap; only the per-key lookup does. - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const map = mapOf([0, 1]) - const first = buildRuntimeMobileAgentStatusProjectionForTests(map) - - const swapped = { 'tab-0:leaf-0': map['tab-0:leaf-0'], 'tab-9:leaf-0': makeEntry(9) } - const result = buildRuntimeMobileAgentStatusProjectionForTests(swapped) - - expect(result).not.toBe(first) - expect(result).toContain('tab-9:leaf-0') - expect(result).not.toContain('tab-1:leaf-0') - }) - - it('still rebuilds when a pane is added', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const map = mapOf([0]) - const first = buildRuntimeMobileAgentStatusProjectionForTests(map) - - const added = { ...map, 'tab-9:leaf-0': makeEntry(9) } - const result = buildRuntimeMobileAgentStatusProjectionForTests(added) - - expect(result).not.toBe(first) - expect(result).toContain('tab-9:leaf-0') - }) }) diff --git a/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts b/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts deleted file mode 100644 index 7bf0b0e5c69..00000000000 --- a/src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { AppState } from '@/store/types' -import { - AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS_FOR_TESTS, - buildRuntimeMobileAgentStatusProjectionForTests, - resetRuntimeMobileAgentStatusProjectionCacheForTests -} from './sync-runtime-graph/agent-status-projection' - -// Keep pre-change serialization as oracle, except code-unit sorting used only for equality. Pin identical entries across locale order and read bucket width from production to avoid unrelated constant drift. -const BUCKET_MS = AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS_FOR_TESTS -type MainAgentStatus = AppState['agentStatusByPaneKey'][string]['mainAgent'] -function mainAgentKey(mainAgent: MainAgentStatus) { - return mainAgent ? [mainAgent.state, mainAgent.outcome ?? null, mainAgent.stateStartedAt] : null -} -function referenceProjection(map: AppState['agentStatusByPaneKey']): string { - return JSON.stringify( - Object.entries(map) - .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([paneKey, entry]) => ({ - paneKey, - entryPaneKey: entry.paneKey, - state: entry.state, - workingMode: entry.workingMode ?? null, - prompt: entry.prompt, - updatedAtBucket: Math.floor(entry.updatedAt / BUCKET_MS), - stateStartedAt: entry.stateStartedAt, - // A new turn can keep the state (child work held it open); its stamp alone must republish. - turnStartedAt: entry.turnStartedAt ?? null, - turnEndedAt: entry.turnEndedAt ?? null, - turnEndDisposition: entry.turnEndDisposition ?? null, - agentType: entry.agentType ?? null, - terminalTitle: entry.terminalTitle ?? null, - stateHistory: entry.stateHistory.map((history) => ({ - state: history.state, - prompt: history.prompt, - startedAt: history.startedAt, - interrupted: history.interrupted ?? null, - mainAgent: mainAgentKey(history.mainAgent) - })), - toolName: entry.toolName ?? null, - toolInput: entry.toolInput ?? null, - interactivePrompt: entry.interactivePrompt ?? null, - lastAssistantMessage: entry.lastAssistantMessage ?? null, - lastAssistantMessageIsToolOutput: entry.lastAssistantMessageIsToolOutput ?? null, - interrupted: entry.interrupted ?? null, - mainAgent: mainAgentKey(entry.mainAgent), - programStatus: entry.programStatus - ? [ - entry.programStatus.state, - entry.programStatus.kind ?? null, - entry.programStatus.progress ?? null, - entry.programStatus.message ?? null - ] - : null - })) - ) -} - -function makeEntry(index: number, overrides: Record = {}): never { - return { - paneKey: `tab-${index}:leaf-0`, - state: 'working', - prompt: `prompt ${index} with "quotes" and \\ backslash`, - updatedAt: 1740000000000 + index * 17, - stateStartedAt: 1740000000000, - agentType: 'claude', - terminalTitle: `agent ${index} 日本 \u{1f389}`, - stateHistory: Array.from({ length: 3 }, (_value, h) => ({ - state: 'working', - prompt: `step ${h}`, - startedAt: 1740000000000 + h - })), - toolName: 'shell_command', - toolInput: 'ls -la', - lastAssistantMessage: 'answer', - ...overrides - } as never -} - -describe('mobile agent-status projection equivalence', () => { - it('matches the whole-array serialization across status maps and cache reuse', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const statusMaps: AppState['agentStatusByPaneKey'][] = [] - statusMaps.push({}) - statusMaps.push({ 'tab-0:leaf-0': makeEntry(0) }) - statusMaps.push({ 'tab-0:leaf-0': makeEntry(0, { workingMode: 'monitoring' }) }) - statusMaps.push({ 'tab-0:leaf-0': makeEntry(0, { turnStartedAt: 1740000000500 }) }) - const many: AppState['agentStatusByPaneKey'] = {} - for (let index = 0; index < 12; index += 1) { - many[`tab-${index}:leaf-0`] = makeEntry(index) - } - statusMaps.push(many) - // Optional fields absent entirely, which the ?? null fallbacks must cover. - statusMaps.push({ - 'tab-9:leaf-1': makeEntry(9, { - agentType: undefined, - terminalTitle: undefined, - toolName: undefined, - toolInput: undefined, - interactivePrompt: undefined, - lastAssistantMessage: undefined, - interrupted: undefined - }) - }) - // Keys deliberately out of insertion order to pin the sort. - statusMaps.push({ - 'tab-z:leaf-0': makeEntry(2), - 'tab-a:leaf-0': makeEntry(1), - 'tab-m:leaf-0': makeEntry(3) - }) - - for (const [index, statusMap] of statusMaps.entries()) { - expect({ - index, - projection: buildRuntimeMobileAgentStatusProjectionForTests(statusMap) - }).toEqual({ index, projection: referenceProjection(statusMap) }) - } - - // Replace one entry like setAgentStatus and require observable projection change; sub-bucket timestamp nudges cannot catch stale reuse. - let current = many - for (let round = 0; round < 4; round += 1) { - current = { - ...current, - 'tab-0:leaf-0': makeEntry(0, { - updatedAt: 1740000000000 + BUCKET_MS * 3 * (round + 1), - state: round % 2 === 0 ? 'done' : 'working', - prompt: `changed prompt round ${round}`, - lastAssistantMessage: `answer round ${round}` - }) - } - expect({ - round, - projection: buildRuntimeMobileAgentStatusProjectionForTests(current) - }).toEqual({ round, projection: referenceProjection(current) }) - } - }) - - it('serializes the same entries the localeCompare order did', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - // Use keys whose locale/code-unit orders differ, including tab-1: versus tab-10: and case/accents. - const map: AppState['agentStatusByPaneKey'] = {} - for (const paneKey of ['tab-1:leaf-0', 'tab-10:leaf-0', 'B:leaf-0', 'a:leaf-0', 'á:leaf-0']) { - map[paneKey] = makeEntry(0, { paneKey }) - } - const localeCompareOrderedEntries = JSON.parse( - JSON.stringify( - Object.entries(map) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([paneKey]) => paneKey) - ) - ) as string[] - const projected = ( - JSON.parse(buildRuntimeMobileAgentStatusProjectionForTests(map)) as { paneKey: string }[] - ).map((entry) => entry.paneKey) - expect([...projected].sort()).toEqual([...localeCompareOrderedEntries].sort()) - expect(projected).toHaveLength(localeCompareOrderedEntries.length) - }) - - it('republishes a verdict change that leaves the interrupted flag as it was', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const project = (live: 'success' | 'failure', history: 'success' | 'failure'): string => - buildRuntimeMobileAgentStatusProjectionForTests({ - 'tab-0:leaf-0': makeEntry(0, { - state: 'done', - mainAgent: { state: 'done', outcome: live, stateStartedAt: 1740000000000 }, - stateHistory: [ - { - state: 'done', - prompt: 'p', - startedAt: 1, - mainAgent: { state: 'done', outcome: history, stateStartedAt: 1 } - } - ] - }) - }) - expect(project('failure', 'success')).not.toBe(project('success', 'success')) - expect(project('success', 'failure')).not.toBe(project('success', 'success')) - }) - - it('republishes a main agent failing while its subagents keep the row working', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const project = (outcome?: 'failure', stateStartedAt = 1740000000000): string => - buildRuntimeMobileAgentStatusProjectionForTests({ - 'tab-0:leaf-0': makeEntry(0, { - state: 'working', - mainAgent: { state: 'done', ...(outcome ? { outcome } : {}), stateStartedAt } - }) - }) - expect(project('failure')).not.toBe(project()) - // The main agent's own clock dates the failure on mobile, so moving it republishes too. - expect(project('failure', 1740000001000)).not.toBe(project('failure')) - }) -}) - -describe('mobile agent-status projection program-status facet', () => { - it('republishes on facet content but not on a new observedAt alone', () => { - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const facet = { state: 'working', progress: 10, observedAt: 1 } - const base = buildRuntimeMobileAgentStatusProjectionForTests({ - 'tab-0:leaf-0': makeEntry(0, { programStatus: facet }) - }) - const reobserved = buildRuntimeMobileAgentStatusProjectionForTests({ - 'tab-0:leaf-0': makeEntry(0, { programStatus: { ...facet, observedAt: 2 } }) - }) - const progressed = buildRuntimeMobileAgentStatusProjectionForTests({ - 'tab-0:leaf-0': makeEntry(0, { programStatus: { ...facet, progress: 20 } }) - }) - expect(reobserved).toBe(base) - expect(progressed).not.toBe(base) - }) -}) diff --git a/src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts b/src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts index e49b1d0dadb..234e2cb85ea 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts @@ -69,12 +69,6 @@ function worktreeSnapshot( ) } -function publishedBrowserWorkspaceIds(store: ReturnType): string[] { - return (worktreeSnapshot(store)?.tabs ?? []) - .filter((tab) => tab.type === 'browser') - .map((tab) => (tab as { browserWorkspaceId?: string }).browserWorkspaceId ?? '') -} - function collectLayoutGroupIds(node: unknown, into: string[] = []): string[] { if (!node || typeof node !== 'object') { return into @@ -111,37 +105,6 @@ function publishedUnifiedTabIds(store: ReturnType): Set< // Real conversion actions must flip docLocation and publication predicate in one commit; no intermediate document publish or web-page suppression. describe('mobile publish across an address-bar conversion', () => { - it('starts publishing a doc tab the moment it converts to web, and stops on the way back', async () => { - const store = await createStoreWithWorktree() - // Require ordinary URL presence so a broken browser publisher cannot pass on empty results. - const urlTab = store.getState().createBrowserTab(WORKTREE_ID, 'https://example.com/') - const docTab = store.getState().createBrowserTab(WORKTREE_ID, '', { - docLocation: DOC_LOCATION, - title: 'index.html', - browserRuntimeEnvironmentId: null - }) - await settled() - const docPageId = store.getState().browserPagesByWorkspace[docTab.id]?.[0]?.id ?? '' - - expect(publishedBrowserWorkspaceIds(store)).toEqual([urlTab.id]) - - const webPage = store.getState().convertBrowserPage(docPageId, { - kind: 'web', - url: 'https://converted.example/' - }) - expect(webPage).not.toBeNull() - await settled() - expect(publishedBrowserWorkspaceIds(store).sort()).toEqual([urlTab.id, docTab.id].sort()) - - const docPage = store.getState().convertBrowserPage(webPage?.id ?? '', { - kind: 'workspace-doc', - docLocation: DOC_LOCATION - }) - expect(docPage).not.toBeNull() - await settled() - expect(publishedBrowserWorkspaceIds(store)).toEqual([urlTab.id]) - }) - // Exercise group/layout projection independently: doc-only groups must not reference tabs withheld from phones. it('holds the doc tab out of published groups and the layout, and admits it on conversion', async () => { const store = await createStoreWithWorktree() diff --git a/src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts b/src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts index 795d731cfdb..5da633fc0a9 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts @@ -24,103 +24,11 @@ function makeState(overrides: Partial = {}): AppState { } as AppState } -function makeOpenMarkdownFile(): AppState['openFiles'][number] { - return { - id: '/repo/README.md', - filePath: '/repo/README.md', - relativePath: 'README.md', - worktreeId: 'wt-1', - language: 'markdown', - mode: 'edit', - isDirty: false - } -} - afterEach(() => { vi.restoreAllMocks() }) describe('runtime mobile session sync key projection reuse', () => { - it('reuses serialized projections when only runtime pane titles change', () => { - const base = makeState({ - tabsByWorktree: { - 'wt-1': [{ id: 'term-1', title: 'Codex working', customTitle: null }] - } as unknown as AppState['tabsByWorktree'], - runtimePaneTitlesByTabId: { - 'term-1': { 1: 'Codex working' } - } as unknown as AppState['runtimePaneTitlesByTabId'], - openFiles: [makeOpenMarkdownFile()], - editorDrafts: { '/repo/README.md': '# draft' } - }) - const baseKey = getRuntimeMobileSessionSyncKey(base) - const titleTick = makeState({ - ...base, - runtimePaneTitlesByTabId: { - 'term-1': { 1: 'Codex spinner frame' } - } as unknown as AppState['runtimePaneTitlesByTabId'] - }) - - const stringifySpy = vi.spyOn(JSON, 'stringify') - stringifySpy.mockClear() - const titleTickKey = getRuntimeMobileSessionSyncKey(titleTick, base, baseKey) - - expect(stringifySpy).not.toHaveBeenCalled() - expect(titleTickKey.tabsProjection).toBe(baseKey.tabsProjection) - expect(titleTickKey.openFilesProjection).toBe(baseKey.openFilesProjection) - expect(titleTickKey.editorDraftsProjection).toBe(baseKey.editorDraftsProjection) - expect(runtimeMobileSessionSyncKeysEqual(baseKey, titleTickKey)).toBe(false) - }) - - it('only rebuilds the tab projection when a terminal tab title changes', () => { - const base = makeState({ - tabsByWorktree: { - 'wt-1': [{ id: 'term-1', title: 'Codex working', customTitle: null }] - } as unknown as AppState['tabsByWorktree'], - openFiles: [makeOpenMarkdownFile()], - editorDrafts: { '/repo/README.md': '# draft' } - }) - const baseKey = getRuntimeMobileSessionSyncKey(base) - const titleTick = makeState({ - ...base, - tabsByWorktree: { - 'wt-1': [{ id: 'term-1', title: 'Codex spinner frame', customTitle: null }] - } as unknown as AppState['tabsByWorktree'] - }) - - const stringifySpy = vi.spyOn(JSON, 'stringify') - stringifySpy.mockClear() - const titleTickKey = getRuntimeMobileSessionSyncKey(titleTick, base, baseKey) - - expect(stringifySpy).toHaveBeenCalledTimes(1) - expect(titleTickKey.tabsProjection).not.toBe(baseKey.tabsProjection) - expect(titleTickKey.openFilesProjection).toBe(baseKey.openFilesProjection) - expect(titleTickKey.editorDraftsProjection).toBe(baseKey.editorDraftsProjection) - expect(runtimeMobileSessionSyncKeysEqual(baseKey, titleTickKey)).toBe(false) - }) - - it('does not rebuild serialized projections when only activeTabId changes', () => { - const base = makeState({ - tabsByWorktree: { - 'wt-1': [{ id: 'term-1', title: 'Codex working', customTitle: null }] - } as unknown as AppState['tabsByWorktree'] - }) - const baseKey = getRuntimeMobileSessionSyncKey(base) - const activated = makeState({ - ...base, - activeTabId: 'term-1' - }) - - const stringifySpy = vi.spyOn(JSON, 'stringify') - stringifySpy.mockClear() - const activatedKey = getRuntimeMobileSessionSyncKey(activated, base, baseKey) - - expect(stringifySpy).not.toHaveBeenCalled() - expect(activatedKey.tabsProjection).toBe(baseKey.tabsProjection) - expect(activatedKey.openFilesProjection).toBe(baseKey.openFilesProjection) - expect(activatedKey.editorDraftsProjection).toBe(baseKey.editorDraftsProjection) - expect(runtimeMobileSessionSyncKeysEqual(baseKey, activatedKey)).toBe(false) - }) - it('reuses unchanged worktree tab projections when one worktree title changes', () => { const unchangedTitle = vi.fn(() => 'Unchanged agent') const unchangedTab = { @@ -157,61 +65,6 @@ describe('runtime mobile session sync key projection reuse', () => { }) describe('mobile session snapshot reuse', () => { - it('reuses draft document versions when only runtime pane titles change', () => { - const draft = { - length: 1, - charCodeAt: vi.fn(() => 120) - } as unknown as string - const draftSpy = (draft as unknown as { charCodeAt: ReturnType }).charCodeAt - const base = makeState({ - tabsByWorktree: { - 'wt-1': [{ id: 'term-1', title: 'Codex working', customTitle: null }] - } as unknown as AppState['tabsByWorktree'], - tabBarOrderByWorktree: { 'wt-1': ['term-1', '/repo/README.md'] }, - runtimePaneTitlesByTabId: { - 'term-1': { 1: 'Codex working' } - } as unknown as AppState['runtimePaneTitlesByTabId'], - openFiles: [makeOpenMarkdownFile()], - editorDrafts: { '/repo/README.md': draft }, - activeFileId: '/repo/README.md' - }) - - buildMobileSessionTabSnapshots(base) - expect(draftSpy).toHaveBeenCalled() - draftSpy.mockClear() - - buildMobileSessionTabSnapshots({ - ...base, - runtimePaneTitlesByTabId: { - 'term-1': { 1: 'Codex spinner frame' } - } as unknown as AppState['runtimePaneTitlesByTabId'] - }) - - expect(draftSpy).not.toHaveBeenCalled() - }) - - it('keeps (publicationEpoch, snapshotVersion) stable for byte-identical worktree content', () => { - const state = makeState({ - tabsByWorktree: { - 'wt-stable': [{ id: 'term-1', title: 'Codex working', customTitle: null }] - } as unknown as AppState['tabsByWorktree'], - terminalLayoutsByTabId: { - 'term-1': { - root: { type: 'leaf', leafId: '11111111-1111-4111-8111-111111111111' }, - activeLeafId: '11111111-1111-4111-8111-111111111111', - expandedLeafId: null - } - } as unknown as AppState['terminalLayoutsByTabId'] - }) - - const first = buildMobileSessionTabSnapshots(state)[0] - const second = buildMobileSessionTabSnapshots(state)[0] - - // Stable epoch/version gates mobile fan-out; no-op graph rebuilds must not republish every workspace. - expect(second?.publicationEpoch).toBe(first?.publicationEpoch) - expect(second?.snapshotVersion).toBe(first?.snapshotVersion) - }) - it('bumps only the changed worktree version when a sibling stays byte-identical', () => { const makeTwoWorktreeState = (title: string): AppState => makeState({ diff --git a/src/renderer/src/runtime/sync-runtime-graph-main-listed-panes.test.ts b/src/renderer/src/runtime/sync-runtime-graph-main-listed-panes.test.ts index e5338d0a1f1..4404b87986c 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-main-listed-panes.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-main-listed-panes.test.ts @@ -93,16 +93,6 @@ describe('syncRuntimeGraph in a layout-owner build', () => { ]) }) - it("carries the parked pane's runtime title", async () => { - parkWatcher(7) - - const graph = await publish(() => parkedState({ [TAB_ID]: { 7: 'codex [working]' } })) - - expect(graph?.mountedLeaves).toEqual([ - expect.objectContaining({ leafId: LEAF, paneTitle: 'codex [working]' }) - ]) - }) - it('sends nothing for a parked pane whose watcher was disposed', async () => { parkWatcher(7, false) diff --git a/src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts b/src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts index aac5de9b2b7..0655af1a2fb 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts @@ -88,30 +88,6 @@ function startSync( } describe('mobile session graph payload partition', () => { - it('withholds every unchanged worktree from the republished payload', async () => { - const syncWindowGraph = vi.fn().mockResolvedValue({}) - startSync(makeState('a', 3), syncWindowGraph) - - await flushSync() - expect(publishedWorktrees(syncWindowGraph.mock.calls[0])).toEqual([ - 'repo::/a-wt-0', - 'repo::/a-wt-1', - 'repo::/a-wt-2' - ]) - expect(withheldWorktrees(syncWindowGraph.mock.calls[0])).toEqual([]) - - scheduleRuntimeGraphSync() - await flushSync() - - expect(syncWindowGraph).toHaveBeenCalledTimes(2) - expect(publishedWorktrees(syncWindowGraph.mock.calls[1])).toEqual([]) - expect(withheldWorktrees(syncWindowGraph.mock.calls[1])).toEqual([ - 'repo::/a-wt-0', - 'repo::/a-wt-1', - 'repo::/a-wt-2' - ]) - }) - it('publishes only the worktree whose snapshot changed', async () => { const syncWindowGraph = vi.fn().mockResolvedValue({}) const { setState } = startSync(makeState('b', 3), syncWindowGraph) @@ -152,24 +128,6 @@ describe('mobile session graph payload partition', () => { ]) }) - it('resends everything when the first publication throws', async () => { - const syncWindowGraph = vi.fn().mockRejectedValueOnce(new Error('ipc closed')) - syncWindowGraph.mockResolvedValue({}) - startSync(makeState('d', 3), syncWindowGraph) - await flushSync() - expect(publishedWorktrees(syncWindowGraph.mock.calls[0])).toHaveLength(3) - - scheduleRuntimeGraphSync() - await flushSync() - - expect(publishedWorktrees(syncWindowGraph.mock.calls[1])).toEqual([ - 'repo::/d-wt-0', - 'repo::/d-wt-1', - 'repo::/d-wt-2' - ]) - expect(withheldWorktrees(syncWindowGraph.mock.calls[1])).toEqual([]) - }) - it('republishes worktrees main reports it dropped after acknowledging them', async () => { const syncWindowGraph = vi.fn().mockResolvedValue({}) startSync(makeState('e', 3), syncWindowGraph) diff --git a/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts b/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts index e29126c236c..72a3e5c0501 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts @@ -22,8 +22,6 @@ const { buildRuntimeMobileAgentStatusProjectionForTests, resetRuntimeMobileAgentStatusProjectionCacheForTests } = await import('./sync-runtime-graph/agent-status-projection') -const { getRuntimeMobileSessionSyncKey, runtimeMobileSessionSyncKeysEqual } = - await import('./sync-runtime-graph/sync-key') const { resetRuntimeMobileSyncProjectionCachesForTests, buildRuntimeMobileBrowserProjection, @@ -265,26 +263,6 @@ describe('editor draft projection on the typing path', () => { }) expect(before.chars / after.chars).toBeGreaterThan(DRAFT_FILE_COUNT - 0.1) }) - - it('matches the uncached projection byte for byte across draft shapes', () => { - const draftCases: Record[] = [ - {}, - { 'file-a': '' }, - { 'file-a': 'hello' }, - { 'file-a': 'hello', 'file-b': 'world' }, - { 'file-b': 'world', 'file-a': 'hello' }, - { '2': 'numeric-like key', 'file-a': 'hello', '1': 'other' }, - { 'quote"and\\slash': 'body with "quotes" and \\ and \u{1f389}' }, - { 'file-a': 'hello', 'file-b': 'world', 'file-c': 'third' }, - { 'file-a': 'HELLO', 'file-c': 'third' } - ] - for (const [index, draft] of draftCases.entries()) { - expect({ index, projection: buildRuntimeMobileEditorDraftsProjection(draft) }).toEqual({ - index, - projection: referenceEditorDraftsProjection(draft) - }) - } - }) }) describe('agent-status projection sort', () => { @@ -327,21 +305,6 @@ describe('agent-status projection sort', () => { sortedProjectionEntries(referenceAgentStatusProjection(map)) ) }) - - it('is deterministic for keys where locale and code-unit order disagree', () => { - const map: AppState['agentStatusByPaneKey'] = {} - for (const paneKey of ['b:leaf', 'A:leaf', 'a:leaf', 'á:leaf', 'B:leaf']) { - map[paneKey] = makeAgentStatusEntry({ paneKey }) - } - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const first = buildRuntimeMobileAgentStatusProjectionForTests(map) - resetRuntimeMobileAgentStatusProjectionCacheForTests() - const second = buildRuntimeMobileAgentStatusProjectionForTests({ ...map }) - expect(second).toBe(first) - expect(sortedProjectionEntries(first)).toEqual( - sortedProjectionEntries(referenceAgentStatusProjection(map)) - ) - }) }) describe('open-files and browser projections', () => { @@ -408,178 +371,4 @@ describe('open-files and browser projections', () => { // Only the 'wt-1' bucket re-serializes; 'wt-2' and every page bucket are reused. expect(before / after).toBeGreaterThan(2.5) }) - - it('matches the uncached projections byte for byte across shapes', () => { - const openFileCases: AppState['openFiles'][] = [ - [] as unknown as AppState['openFiles'], - [makeOpenFile(0)] as unknown as AppState['openFiles'], - [makeOpenFile(0, { mirroredFromRuntimeSession: true })], - [makeOpenFile(0, { isDirty: true })] as unknown as AppState['openFiles'], - [ - makeOpenFile(0, { isDirty: true, diffSource: 'working' }), - makeOpenFile(1, { mode: 'diff', markdownPreviewSourceFileId: 'file-0' }), - makeOpenFile(2, { isUntitled: true, deleteUntouchedOnClose: true, language: undefined }) - ] as unknown as AppState['openFiles'] - ] - for (const [index, openFiles] of openFileCases.entries()) { - expect({ index, projection: buildRuntimeMobileOpenFilesProjection(openFiles) }).toEqual({ - index, - projection: referenceOpenFilesProjection(openFiles) - }) - } - - const browserCases: AppState[] = [ - makeState({}), - makeState({ browserTabsByWorktree: { 'wt-1': [makeBrowserWorkspace(0)] } as never }), - makeState({ - browserTabsByWorktree: { - 'wt-1': [makeBrowserWorkspace(0, { title: undefined, loading: true })], - '3': [makeBrowserWorkspace(1)] - } as never, - browserPagesByWorkspace: { - 'ws-0': [makeBrowserPage(0), makeBrowserPage(1, { canGoBack: true })], - 'ws-1': [] - } as never - }), - makeState({ - browserTabsByWorktree: {} as never, - browserPagesByWorkspace: { 'ws-9': [makeBrowserPage(9, { url: 'a"b\\c' })] } as never - }) - ] - for (const [index, state] of browserCases.entries()) { - expect({ index, projection: buildRuntimeMobileBrowserProjection(state) }).toEqual({ - index, - projection: referenceBrowserProjection(state) - }) - } - }) -}) - -describe('sync key transitions', () => { - it('fires on exactly the transitions the uncached projections would have fired on', () => { - const drafts = { 'file-0': 'aaa', 'file-1': 'bbb' } - const files = [makeOpenFile(0), makeOpenFile(1)] as unknown as AppState['openFiles'] - const workspaces = [makeBrowserWorkspace(0)] as never - const status = { - 'tab-0:leaf-0': makeAgentStatusEntry({ paneKey: 'tab-0:leaf-0' }) - } as AppState['agentStatusByPaneKey'] - - const base = makeState({ - editorDrafts: drafts, - openFiles: files, - browserTabsByWorktree: { 'wt-1': workspaces } as never, - browserPagesByWorkspace: { 'ws-0': [makeBrowserPage(0)] } as never, - agentStatusByPaneKey: status - }) - - // Each step returns the next state; the flag is whether a mobile-visible input really moved. - const steps: { name: string; next: (from: AppState) => AppState }[] = [ - { name: 'no-op re-spread', next: (from) => makeState({ ...from }) }, - { - name: 'keystroke in one draft', - next: (from) => - makeState({ ...from, editorDrafts: { ...from.editorDrafts, 'file-0': 'aaab' } }) - }, - { - name: 'draft reverted to the same text', - next: (from) => - makeState({ ...from, editorDrafts: { ...from.editorDrafts, 'file-0': 'aaab' } }) - }, - { - name: 'draft removed', - next: (from) => makeState({ ...from, editorDrafts: { 'file-1': 'bbb' } }) - }, - { - name: 'isDirty flip', - next: (from) => - makeState({ - ...from, - openFiles: [makeOpenFile(0, { isDirty: true }), from.openFiles[1]] as never - }) - }, - { - name: 'open-files re-spread with identical content', - next: (from) => makeState({ ...from, openFiles: [...from.openFiles] as never }) - }, - { - name: 'browser title tick', - next: (from) => - makeState({ - ...from, - browserTabsByWorktree: { - 'wt-1': [makeBrowserWorkspace(0, { title: 'new title' })] - } as never - }) - }, - { - name: 'browser page loading flip', - next: (from) => - makeState({ - ...from, - browserPagesByWorkspace: { - 'ws-0': [makeBrowserPage(0, { loading: true })] - } as never - }) - }, - { - name: 'agent-status ping with an unchanged payload', - next: (from) => - makeState({ - ...from, - agentStatusByPaneKey: { - 'tab-0:leaf-0': makeAgentStatusEntry({ paneKey: 'tab-0:leaf-0' }) - } as never - }) - }, - { - name: 'agent-status prompt change', - next: (from) => - makeState({ - ...from, - agentStatusByPaneKey: { - 'tab-0:leaf-0': makeAgentStatusEntry({ paneKey: 'tab-0:leaf-0', prompt: 'new' }) - } as never - }) - }, - { - name: 'second agent added', - next: (from) => - makeState({ - ...from, - agentStatusByPaneKey: { - ...from.agentStatusByPaneKey, - 'tab-1:leaf-0': makeAgentStatusEntry({ paneKey: 'tab-1:leaf-0' }) - } as never - }) - } - ] - - const referenceTuple = (state: AppState): string[] => [ - referenceEditorDraftsProjection(state.editorDrafts), - referenceOpenFilesProjection(state.openFiles), - referenceBrowserProjection(state), - referenceAgentStatusProjection(state.agentStatusByPaneKey ?? {}) - ] - - resetRuntimeMobileSyncProjectionCachesForTests() - resetRuntimeMobileAgentStatusProjectionCacheForTests() - let previousState = base - let previousKey = getRuntimeMobileSessionSyncKey(base, undefined, undefined, false) - let previousReference = referenceTuple(base) - - for (const step of steps) { - const state = step.next(previousState) - const key = getRuntimeMobileSessionSyncKey(state, previousState, previousKey, false) - const reference = referenceTuple(state) - const referenceChanged = reference.some((part, index) => part !== previousReference[index]) - const keyChanged = !runtimeMobileSessionSyncKeysEqual(key, previousKey) - expect({ step: step.name, changed: keyChanged }).toEqual({ - step: step.name, - changed: referenceChanged - }) - previousState = state - previousKey = key - previousReference = reference - } - }) }) diff --git a/src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts b/src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts index f84fc5d4159..ddcef358c8c 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts @@ -3,49 +3,6 @@ import { buildMobileSessionTabSnapshots } from './sync-runtime-graph/mobile-sess import { registerRuntimeTerminalTab } from './mounted-terminal-tab-registry' import type { AppState } from '../store/types' -// Count per-worktree slice reads in the build loop to measure work without timing flakiness. -function makeCountingState(worktreeCount: number): { - state: AppState - reads: () => number - resetReads: () => void -} { - let reads = 0 - const tabsByWorktree: Record = {} - for (let i = 0; i < worktreeCount; i++) { - tabsByWorktree[`repo::/wt-${i}`] = [ - { id: `term-${i}`, title: `Agent ${i}`, customTitle: null, type: 'terminal' } - ] - } - - const state = { - tabsByWorktree, - terminalLayoutsByTabId: {}, - runtimePaneTitlesByTabId: {}, - groupsByWorktree: {}, - activeGroupIdByWorktree: {}, - unifiedTabsByWorktree: {}, - tabBarOrderByWorktree: {}, - activeFileId: null, - activeFileIdByWorktree: {}, - openFiles: [], - editorDrafts: {}, - activeTabId: null, - agentStatusByPaneKey: {}, - get browserTabsByWorktree() { - reads++ - return {} - } - } as unknown as AppState - - return { - state, - reads: () => reads, - resetReads: () => { - reads = 0 - } - } -} - // tab.title getter counts snapshot rebuilds; slice reads alone would also pass with a cheap hoist. function makeTitleCountingState(worktreeCount: number): { state: AppState @@ -129,45 +86,6 @@ function makeTitleCountingState(worktreeCount: number): { } describe('mobile session publication cost', () => { - it('does not redo per-worktree work when nothing changed', () => { - const WORKTREES = 300 - const { state, reads, resetReads } = makeCountingState(WORKTREES) - - buildMobileSessionTabSnapshots(state) - resetReads() - - // Same state object, no mutation: a republish should do no per-worktree work. - buildMobileSessionTabSnapshots(state) - - expect(reads()).toBeLessThan(WORKTREES / 10) - }) - - it('rebuilds only the worktrees whose inputs changed', () => { - const WORKTREES = 300 - const { state, reads, resetReads } = makeCountingState(WORKTREES) - - buildMobileSessionTabSnapshots(state) - resetReads() - - // One worktree's tabs change — the other 299 are untouched. - const next = { - ...state, - tabsByWorktree: { - ...state.tabsByWorktree, - 'repo::/wt-7': [ - { id: 'term-7', title: 'Agent 7 (done)', customTitle: null, type: 'terminal' } - ] - }, - get browserTabsByWorktree() { - return (state as unknown as { browserTabsByWorktree: unknown }).browserTabsByWorktree - } - } as unknown as AppState - - buildMobileSessionTabSnapshots(next) - - expect(reads()).toBeLessThan(WORKTREES / 10) - }) - it('builds no worktree content when nothing changed', () => { const { state, titleReads, resetTitleReads } = makeTitleCountingState(300)