mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
test: remove 49 low-value graph, host and layout tests (#34473)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<typeof createTestStore>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<typeof createTestStore> } = {}
|
||||
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<typeof AgentStatusModule>()
|
||||
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<string, SleepingAgentSessionRecord>): 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 }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, unknown> = {}): 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)
|
||||
})
|
||||
})
|
||||
@@ -69,12 +69,6 @@ function worktreeSnapshot(
|
||||
)
|
||||
}
|
||||
|
||||
function publishedBrowserWorkspaceIds(store: ReturnType<typeof createTestStore>): 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<typeof createTestStore>): 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()
|
||||
|
||||
@@ -24,103 +24,11 @@ function makeState(overrides: Partial<AppState> = {}): 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<typeof vi.fn> }).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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string, string>[] = [
|
||||
{},
|
||||
{ '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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, unknown[]> = {}
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user