mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove redundant cases and repair audit assertions (#33334)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -309,12 +309,8 @@ describe('isPrSidebarDetailsPlaceholder / prSidebarDetailsNeedFetch', () => {
|
||||
})
|
||||
|
||||
describe('buildMobilePrSidebarIdentity', () => {
|
||||
it('keys identity on worktree + branch, not head SHA', () => {
|
||||
it('keys identity on worktree and branch', () => {
|
||||
expect(buildMobilePrSidebarIdentity({ worktreeId: 'w1', branch: 'feat' })).toBe('w1\u0000feat')
|
||||
// Head advances on commit must not form a new identity — soft refresh keeps ready UI.
|
||||
expect(buildMobilePrSidebarIdentity({ worktreeId: 'w1', branch: 'feat' })).toBe(
|
||||
buildMobilePrSidebarIdentity({ worktreeId: 'w1', branch: 'feat' })
|
||||
)
|
||||
})
|
||||
|
||||
it('is null without a branch and changes when branch or worktree changes', () => {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
} from './pending-session-selection'
|
||||
import { NOTES_UNCONFIRMED_MESSAGE, PROMPT_UNCONFIRMED_MESSAGE } from './new-tab-agent-host-launch'
|
||||
import { AGENT_PROMPT_NOT_SENT_MESSAGE } from './pr-ai-triage-launch'
|
||||
import { releaseTerminalCreateLock } from './terminal-create-lock'
|
||||
import { useMobileSessionTerminalCreateActions } from './use-mobile-session-terminal-create-actions'
|
||||
|
||||
vi.mock('../platform/haptics', () => ({ triggerSuccess: vi.fn(), triggerError: vi.fn() }))
|
||||
@@ -457,36 +456,6 @@ describe('launches that carry a prompt', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('lands on the tab and frees the + lock before a paste-after-start agent replies', async () => {
|
||||
const reply = launchReply(
|
||||
{ kind: 'terminal', handle: 'term_7' },
|
||||
{ delivery: 'submit', outcome: 'handed-to-terminal' }
|
||||
)
|
||||
const state = scope(scriptedClient(reply).client)
|
||||
let landedBeforeReply: PendingSessionSelection | null = null
|
||||
state.client = requestPortRpcClient(async (_method, params) => {
|
||||
// The host lists the tab as soon as it exists, then waits for the agent to paste into it.
|
||||
const pending = state.pendingSelectionRef.current
|
||||
const landed = resolveLaunchedSelection(pending, [reservedTab(params, 'term_7')])
|
||||
state.pendingSelectionRef.current = landed.selection
|
||||
if (landed.landedTabId && pending?.kind === 'launched') {
|
||||
releaseTerminalCreateLock(state, pending.lock)
|
||||
}
|
||||
landedBeforeReply = state.pendingSelectionRef.current
|
||||
expect(state.creatingTerminalRef.current).toBeNull()
|
||||
return reply
|
||||
})
|
||||
|
||||
await create_(state, 'aider', { agentPrompt: 'run the tests' })
|
||||
|
||||
expect(landedBeforeReply).toEqual(
|
||||
expect.objectContaining({ kind: 'terminal', handle: 'term_7' })
|
||||
)
|
||||
// The late reply leaves the landed pick alone.
|
||||
expect(state.pendingSelectionRef.current).toBe(landedBeforeReply)
|
||||
expect(state.setCreating).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("keeps a second launch's + lock when the first one's reply comes in", async () => {
|
||||
const reply = launchReply(
|
||||
{ kind: 'terminal', handle: 'term_7' },
|
||||
|
||||
@@ -380,32 +380,6 @@ describe('useMobileSessionViewMode', () => {
|
||||
expect(readSessionViewOverridesPreference).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('finishes an early toggle save after the route unmounts', async () => {
|
||||
const overridesLoad = deferred<Map<string, MobileSessionView>>()
|
||||
vi.mocked(readSessionViewOverridesPreference).mockReturnValue(
|
||||
overridesLoad.promise.then((overrides) => ({ overrides, loaded: true }))
|
||||
)
|
||||
function Harness(): null {
|
||||
controller = useMobileSessionViewMode({ hostId: 'h', worktreeId: 'w' })
|
||||
return null
|
||||
}
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness))
|
||||
await Promise.resolve()
|
||||
})
|
||||
act(() => controller?.toggleTabChatView('new-tab'))
|
||||
act(() => renderer?.unmount())
|
||||
renderer = null
|
||||
|
||||
await act(async () => {
|
||||
overridesLoad.resolve(new Map([['saved-tab', 'terminal']]))
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
expect(updateSessionViewOverride).toHaveBeenCalledWith('h', 'w', 'new-tab', 'chat')
|
||||
})
|
||||
|
||||
it('does not reload a failed override after the route unmounts', async () => {
|
||||
const failedWrite = deferred<void>()
|
||||
vi.mocked(updateSessionViewOverride).mockReturnValue(failedWrite.promise)
|
||||
|
||||
@@ -36,7 +36,7 @@ const SNAPSHOT: AgentSessionSubscribeEvent = {
|
||||
window: {
|
||||
oldest: { epoch: 'epoch-1', sequence: 1 },
|
||||
newest: { epoch: 'epoch-1', sequence: 1 },
|
||||
nextCursor: { epoch: 'epoch-1', sequence: 2 }
|
||||
nextCursor: { epoch: 'epoch-1', sequence: 1 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: 1 },
|
||||
hasOlder: false,
|
||||
|
||||
@@ -83,10 +83,7 @@ async function sentDelivery(items: AgentJournalRenderItem[]): Promise<unknown> {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
const event = snapshotEvent()
|
||||
if (event.type === 'snapshot') {
|
||||
event.page.items = items
|
||||
}
|
||||
const event = snapshotEvent({ items })
|
||||
act(() => listener?.(event))
|
||||
await act(async () => {
|
||||
await hook!.sendWithOutcome('carry on')
|
||||
@@ -149,10 +146,7 @@ async function afterStop(items: AgentJournalRenderItem[]): Promise<unknown> {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
const event = snapshotEvent()
|
||||
if (event.type === 'snapshot') {
|
||||
event.page.items = [RUNNING_TURN, ...items]
|
||||
}
|
||||
const event = snapshotEvent({ items: [...items, RUNNING_TURN] })
|
||||
act(() => listener?.(event))
|
||||
act(() => hook?.cancel())
|
||||
expect(hook?.turnIndicator.stopping).toBe(true)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// The published shapes the queued-message hook tests feed the mobile session: host answers,
|
||||
// drafts, submissions and stream frames.
|
||||
|
||||
import type { AgentJournalSubmission } from '../../../src/shared/agent-session-journal-types'
|
||||
import type {
|
||||
AgentJournalRenderItem,
|
||||
AgentJournalSubmission
|
||||
} from '../../../src/shared/agent-session-journal-types'
|
||||
import type {
|
||||
AgentSessionQueuedMessage,
|
||||
AgentSessionQueuePause,
|
||||
@@ -88,33 +91,45 @@ export function queuedDraft(
|
||||
|
||||
export function snapshotEvent(input?: {
|
||||
queuedMessages?: AgentSessionQueuedMessage[] | null
|
||||
items?: AgentJournalRenderItem[]
|
||||
fence?: number
|
||||
runningTurn?: boolean
|
||||
submissions?: AgentJournalSubmission[]
|
||||
}): AgentSessionSubscribeEvent {
|
||||
}): Extract<AgentSessionSubscribeEvent, { type: 'snapshot' }> {
|
||||
const items: AgentJournalRenderItem[] =
|
||||
input?.items ??
|
||||
(input?.runningTurn
|
||||
? [
|
||||
{
|
||||
itemId: 'turn-item-1',
|
||||
revision: 1,
|
||||
body: { kind: 'turn', turnId: 'turn-1', state: 'running' },
|
||||
sequence: 1,
|
||||
observedAt: 1
|
||||
}
|
||||
]
|
||||
: [])
|
||||
const oldest = items[0]
|
||||
const newest = items.at(-1)
|
||||
const fence = input?.fence ?? 3
|
||||
return {
|
||||
type: 'snapshot',
|
||||
sessionId: SESSION_ID,
|
||||
fence: 3,
|
||||
fence,
|
||||
page: {
|
||||
sessionId: SESSION_ID,
|
||||
epoch: 'epoch-1',
|
||||
fence: 3,
|
||||
fence,
|
||||
direction: 'tail',
|
||||
items: input?.runningTurn
|
||||
? [
|
||||
{
|
||||
itemId: 'turn-item-1',
|
||||
revision: 1,
|
||||
body: { kind: 'turn', turnId: 'turn-1', state: 'running' },
|
||||
sequence: 1,
|
||||
observedAt: 1
|
||||
}
|
||||
]
|
||||
: [],
|
||||
items,
|
||||
removedItemIds: [],
|
||||
submissions: input?.submissions ?? [],
|
||||
window: { oldest: null, newest: null, nextCursor: { epoch: 'epoch-1', sequence: 0 } },
|
||||
liveCursor: { epoch: 'epoch-1', sequence: 0 },
|
||||
window: {
|
||||
oldest: oldest ? { epoch: 'epoch-1', sequence: oldest.sequence } : null,
|
||||
newest: newest ? { epoch: 'epoch-1', sequence: newest.sequence } : null,
|
||||
nextCursor: { epoch: 'epoch-1', sequence: oldest?.sequence ?? 0 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: newest?.sequence ?? 0 },
|
||||
hasOlder: false,
|
||||
hasNewer: false
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// Older hosts retain the copy-before-delete fallback; capable hosts edit in place.
|
||||
|
||||
import { createElement } from 'react'
|
||||
import { agentJournalSubmissionKey } from '../../../src/shared/agent-session-journal-item-key'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { structuredAgentSessionPayloadFingerprint } from '../../../src/shared/structured-agent-session-mutation'
|
||||
@@ -320,6 +321,19 @@ describe('mobile structured queued messages', () => {
|
||||
queuedDraft({ messageId: 'drained' }),
|
||||
queuedDraft({ messageId: 'still-waiting' })
|
||||
],
|
||||
items: [
|
||||
{
|
||||
itemId: agentJournalSubmissionKey('fresh-hand-off-id'),
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 10,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'sent message' }]
|
||||
}
|
||||
}
|
||||
],
|
||||
submissions: [acceptedSubmission('fresh-hand-off-id', 'drained')]
|
||||
})
|
||||
)
|
||||
@@ -331,6 +345,19 @@ describe('mobile structured queued messages', () => {
|
||||
CAPABLE,
|
||||
snapshotEvent({
|
||||
queuedMessages: [queuedDraft({ messageId: 'same-id' })],
|
||||
items: [
|
||||
{
|
||||
itemId: agentJournalSubmissionKey('same-id'),
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 10,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'sent message' }]
|
||||
}
|
||||
}
|
||||
],
|
||||
submissions: [acceptedSubmission('same-id')]
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createElement } from 'react'
|
||||
import { snapshotEvent } from './use-mobile-structured-agent-session-queued.test-fixture'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { agentSessionFailureWords } from '../../../src/shared/agent-session-failure-words'
|
||||
@@ -33,29 +34,7 @@ function retry(sequence: number, attempt: number): AgentJournalRenderItem {
|
||||
}
|
||||
|
||||
function snapshot(items: AgentJournalRenderItem[]): AgentSessionSubscribeEvent {
|
||||
const newest = items.length
|
||||
return {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence: 3,
|
||||
page: {
|
||||
sessionId: 'session-1',
|
||||
epoch: 'epoch-1',
|
||||
fence: 3,
|
||||
direction: 'tail',
|
||||
items,
|
||||
removedItemIds: [],
|
||||
submissions: [],
|
||||
window: {
|
||||
oldest: { epoch: 'epoch-1', sequence: 1 },
|
||||
newest: { epoch: 'epoch-1', sequence: newest },
|
||||
nextCursor: { epoch: 'epoch-1', sequence: newest + 1 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: newest },
|
||||
hasOlder: false,
|
||||
hasNewer: false
|
||||
}
|
||||
}
|
||||
return snapshotEvent({ items })
|
||||
}
|
||||
|
||||
// Module scope: the harness assigns these, which a test body's own `let` would narrow to null.
|
||||
|
||||
@@ -3,9 +3,12 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentJournalDispatchState } from '../../../src/shared/agent-session-journal-types'
|
||||
import { DISPATCH_REJECTED_CANCELLED } from '../../../src/shared/structured-agent-session-dispatch-rejection'
|
||||
import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { fieldsOf, ok } from './use-mobile-structured-agent-session-queued.test-fixture'
|
||||
import {
|
||||
fieldsOf,
|
||||
ok,
|
||||
snapshotEvent
|
||||
} from './use-mobile-structured-agent-session-queued.test-fixture'
|
||||
import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity'
|
||||
import { structuredSendResultFixture } from './structured-agent-send-result.test-fixture'
|
||||
import { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session'
|
||||
@@ -40,31 +43,6 @@ function sendResult(dispatchState: AgentJournalDispatchState, reason: string | n
|
||||
})
|
||||
}
|
||||
|
||||
function snapshotEvent(): Extract<AgentSessionSubscribeEvent, { type: 'snapshot' }> {
|
||||
return {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence: 3,
|
||||
page: {
|
||||
sessionId: 'session-1',
|
||||
epoch: 'epoch-1',
|
||||
fence: 3,
|
||||
direction: 'tail',
|
||||
items: [],
|
||||
removedItemIds: [],
|
||||
submissions: [],
|
||||
window: {
|
||||
oldest: null,
|
||||
newest: null,
|
||||
nextCursor: { epoch: 'epoch-1', sequence: 0 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: 0 },
|
||||
hasOlder: false,
|
||||
hasNewer: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('mobile structured send actions', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
let hook: ReturnType<typeof useMobileStructuredAgentSession> | null = null
|
||||
@@ -277,20 +255,38 @@ describe('mobile structured send actions', () => {
|
||||
throw new Error('expected a submission answer')
|
||||
}
|
||||
original.submission.recovered = true
|
||||
const event = snapshotEvent()
|
||||
event.page.submissions = [original.submission]
|
||||
event.page.items = [
|
||||
{
|
||||
itemId: agentJournalSubmissionKey(original.submission.clientMessageId),
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 10,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'same text' }] }
|
||||
const event = snapshotEvent({
|
||||
items: [
|
||||
{
|
||||
itemId: agentJournalSubmissionKey(original.submission.clientMessageId),
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 10,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'same text' }] }
|
||||
}
|
||||
],
|
||||
submissions: [original.submission]
|
||||
})
|
||||
|
||||
sendRequest.mockImplementation(async (method, params) => {
|
||||
if (method !== 'agentSession.send') {
|
||||
return ok({ models: [], current: {} })
|
||||
}
|
||||
]
|
||||
sendRequest.mockImplementation(async (method) =>
|
||||
method === 'agentSession.send' ? sendResult('accepted') : ok({ models: [], current: {} })
|
||||
)
|
||||
const id = String(fieldsOf(fieldsOf(params).envelope).clientOperationId)
|
||||
const value = structuredSendResultFixture('accepted')
|
||||
if (!('submission' in value)) {
|
||||
throw new Error('expected a submission answer')
|
||||
}
|
||||
value.clientMessageId = id
|
||||
value.submission.clientMessageId = id
|
||||
return ok({
|
||||
ok: true,
|
||||
replayed: false,
|
||||
fence: 3,
|
||||
cursor: { epoch: 'epoch-1', sequence: 2 },
|
||||
value
|
||||
})
|
||||
})
|
||||
await mountSession()
|
||||
act(() => listener?.(event))
|
||||
const messages = hook!.session.messages
|
||||
@@ -313,24 +309,25 @@ describe('mobile structured send actions', () => {
|
||||
const newId = sentIds()[0]!
|
||||
expect(newId).not.toBe(original.submission.clientMessageId)
|
||||
followUp.submission.clientMessageId = newId
|
||||
const reply = snapshotEvent()
|
||||
reply.page.submissions = [original.submission, followUp.submission]
|
||||
reply.page.items = [
|
||||
...event.page.items,
|
||||
{ ...event.page.items[0]!, itemId: sent, sequence: 2, observedAt: 20 },
|
||||
{
|
||||
itemId: 'assistant-reply',
|
||||
revision: 1,
|
||||
sequence: 3,
|
||||
observedAt: 30,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'Reply to the new send' }]
|
||||
const reply = snapshotEvent({
|
||||
items: [
|
||||
...event.page.items,
|
||||
{ ...event.page.items[0]!, itemId: sent, sequence: 2, observedAt: 20 },
|
||||
{
|
||||
itemId: 'assistant-reply',
|
||||
revision: 1,
|
||||
sequence: 3,
|
||||
observedAt: 30,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'Reply to the new send' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
reply.page.liveCursor = { epoch: 'epoch-1', sequence: 3 }
|
||||
],
|
||||
submissions: [original.submission, followUp.submission]
|
||||
})
|
||||
|
||||
act(() => listener?.(reply))
|
||||
expect(hook!.session.messages.map((message) => message.id)).toEqual([
|
||||
messages[0]?.id,
|
||||
@@ -356,25 +353,27 @@ describe('mobile structured send actions', () => {
|
||||
throw new Error('expected submission')
|
||||
}
|
||||
value.submission.rejection = fact
|
||||
const event = snapshotEvent()
|
||||
const sentRow = agentJournalSubmissionKey(value.submission.clientMessageId)
|
||||
event.page.items = [
|
||||
{
|
||||
itemId: 'auth-row',
|
||||
sequence: 1,
|
||||
revision: 1,
|
||||
observedAt: 1,
|
||||
body: { kind: 'status', tone: 'error', ...words }
|
||||
},
|
||||
{
|
||||
itemId: sentRow,
|
||||
sequence: 2,
|
||||
revision: 1,
|
||||
observedAt: 2,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'my message' }] }
|
||||
}
|
||||
]
|
||||
event.page.submissions = [value.submission]
|
||||
const event = snapshotEvent({
|
||||
items: [
|
||||
{
|
||||
itemId: 'auth-row',
|
||||
sequence: 1,
|
||||
revision: 1,
|
||||
observedAt: 1,
|
||||
body: { kind: 'status', tone: 'error', ...words }
|
||||
},
|
||||
{
|
||||
itemId: sentRow,
|
||||
sequence: 2,
|
||||
revision: 1,
|
||||
observedAt: 2,
|
||||
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'my message' }] }
|
||||
}
|
||||
],
|
||||
submissions: [value.submission]
|
||||
})
|
||||
|
||||
sendRequest.mockImplementation(async (method) => {
|
||||
if (method !== 'agentSession.send') {
|
||||
return ok({ models: [], current: {} })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createElement } from 'react'
|
||||
import { snapshotEvent } from './use-mobile-structured-agent-session-queued.test-fixture'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types'
|
||||
@@ -48,28 +49,7 @@ const ITEMS: AgentJournalRenderItem[] = [
|
||||
]
|
||||
|
||||
function snapshot(items: AgentJournalRenderItem[]): AgentSessionSubscribeEvent {
|
||||
return {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence: 3,
|
||||
page: {
|
||||
sessionId: 'session-1',
|
||||
epoch: 'epoch-1',
|
||||
fence: 3,
|
||||
direction: 'tail',
|
||||
items,
|
||||
removedItemIds: [],
|
||||
submissions: [],
|
||||
window: {
|
||||
oldest: { epoch: 'epoch-1', sequence: 1 },
|
||||
newest: { epoch: 'epoch-1', sequence: 3 },
|
||||
nextCursor: { epoch: 'epoch-1', sequence: 4 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: 3 },
|
||||
hasOlder: false,
|
||||
hasNewer: false
|
||||
}
|
||||
}
|
||||
return snapshotEvent({ items })
|
||||
}
|
||||
|
||||
describe('useMobileStructuredAgentSession transcript', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createElement } from 'react'
|
||||
import { snapshotEvent as publishedSnapshot } from './use-mobile-structured-agent-session-queued.test-fixture'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
@@ -6,7 +7,6 @@ import type {
|
||||
AgentJournalRenderItem,
|
||||
AgentJournalResolution
|
||||
} from '../../../src/shared/agent-session-journal-types'
|
||||
import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire'
|
||||
import type { SessionOptionDescriptor } from '../../../src/shared/native-chat-session-options'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity'
|
||||
@@ -32,58 +32,24 @@ function ok(result: unknown) {
|
||||
return { ok: true, result, _meta: { runtimeId: 'runtime-1' } }
|
||||
}
|
||||
|
||||
function snapshotEvent(fence = 3): AgentSessionSubscribeEvent {
|
||||
return {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence,
|
||||
page: {
|
||||
sessionId: 'session-1',
|
||||
epoch: 'epoch-1',
|
||||
fence,
|
||||
direction: 'tail',
|
||||
items: [],
|
||||
removedItemIds: [],
|
||||
submissions: [],
|
||||
window: {
|
||||
oldest: null,
|
||||
newest: null,
|
||||
nextCursor: { epoch: 'epoch-1', sequence: 0 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: 0 },
|
||||
hasOlder: false,
|
||||
hasNewer: false
|
||||
}
|
||||
}
|
||||
function snapshotEvent(fence = 3, items: AgentJournalRenderItem[] = []) {
|
||||
return publishedSnapshot({ fence, items })
|
||||
}
|
||||
|
||||
function snapshotWithMessage(): AgentSessionSubscribeEvent {
|
||||
const event = snapshotEvent()
|
||||
return {
|
||||
...event,
|
||||
page: {
|
||||
...event.page,
|
||||
items: [
|
||||
{
|
||||
itemId: 'msg-1',
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 10,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'sent before the blip' }]
|
||||
}
|
||||
}
|
||||
],
|
||||
window: {
|
||||
oldest: { epoch: 'epoch-1', sequence: 1 },
|
||||
newest: { epoch: 'epoch-1', sequence: 1 },
|
||||
nextCursor: { epoch: 'epoch-1', sequence: 2 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: 1 }
|
||||
function snapshotWithMessage() {
|
||||
return snapshotEvent(3, [
|
||||
{
|
||||
itemId: 'msg-1',
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 10,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
blocks: [{ type: 'text', text: 'sent before the blip' }]
|
||||
}
|
||||
}
|
||||
} as AgentSessionSubscribeEvent
|
||||
])
|
||||
}
|
||||
|
||||
function pendingResolution(): AgentJournalResolution {
|
||||
@@ -322,7 +288,7 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('re-holds after a reconnect that outlives the host release grace', async () => {
|
||||
it('re-holds before resubscribing after a reconnect', async () => {
|
||||
act(() => {
|
||||
renderer = create(createElement(Harness, { connected: true }))
|
||||
})
|
||||
@@ -333,8 +299,7 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
)
|
||||
await vi.waitFor(() => expect(subscribe).toHaveBeenCalledTimes(1))
|
||||
|
||||
// A transport loss retires the connection-scoped hold; after the host's 15s grace
|
||||
// it may evict the provider child. Reconnect must acquire before replaying the stream.
|
||||
// Reconnect must acquire the connection-scoped hold before replaying the stream.
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(Harness, { connected: false }))
|
||||
})
|
||||
@@ -402,15 +367,7 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
act(() => listener?.(snapshotEvent(3)))
|
||||
act(() => listener?.(snapshotEvent(3)))
|
||||
act(() =>
|
||||
listener?.({
|
||||
...snapshotEvent(3),
|
||||
page: {
|
||||
...snapshotEvent(3).page,
|
||||
items: [approvalItem(), questionItem()]
|
||||
}
|
||||
})
|
||||
)
|
||||
act(() => listener?.(snapshotEvent(3, [approvalItem(), questionItem()])))
|
||||
|
||||
if (!hook) {
|
||||
throw new Error('hook not ready')
|
||||
@@ -569,32 +526,24 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
})
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
act(() =>
|
||||
listener?.({
|
||||
...snapshotEvent(3),
|
||||
page: {
|
||||
...snapshotEvent(3).page,
|
||||
items: [
|
||||
approvalItemWithIdentity('approval-old', 4),
|
||||
questionItemWithIdentity('question-old', 8)
|
||||
]
|
||||
}
|
||||
})
|
||||
listener?.(
|
||||
snapshotEvent(3, [
|
||||
approvalItemWithIdentity('approval-old', 4),
|
||||
questionItemWithIdentity('question-old', 8)
|
||||
])
|
||||
)
|
||||
)
|
||||
const approvalToken = hook!.permission!.options[0]!.send
|
||||
const questionToken = hook!.question!.optionTokens[0]!
|
||||
const freeText = formatQuestionFreeTextAnswer(hook!.question!, 'old answer')
|
||||
|
||||
act(() =>
|
||||
listener?.({
|
||||
...snapshotEvent(3),
|
||||
page: {
|
||||
...snapshotEvent(3).page,
|
||||
items: [
|
||||
approvalItemWithIdentity('approval-new', 9),
|
||||
questionItemWithIdentity('question-new', 10)
|
||||
]
|
||||
}
|
||||
})
|
||||
listener?.(
|
||||
snapshotEvent(3, [
|
||||
approvalItemWithIdentity('approval-new', 9),
|
||||
questionItemWithIdentity('question-new', 10)
|
||||
])
|
||||
)
|
||||
)
|
||||
sendRequest.mockClear()
|
||||
|
||||
@@ -638,15 +587,7 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
act(() =>
|
||||
listener?.({
|
||||
...snapshotEvent(3),
|
||||
page: {
|
||||
...snapshotEvent(3).page,
|
||||
items: [approvalItem(), questionItem()]
|
||||
}
|
||||
})
|
||||
)
|
||||
act(() => listener?.(snapshotEvent(3, [approvalItem(), questionItem()])))
|
||||
sendRequest.mockImplementation(async (method, params) => {
|
||||
if (method === 'agentSession.respondToApproval') {
|
||||
throw markRpcDeliveryUnknown(new Error('Connection closed'))
|
||||
@@ -679,12 +620,7 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
act(() =>
|
||||
listener?.({
|
||||
...snapshotEvent(3),
|
||||
page: { ...snapshotEvent(3).page, items: [approvalItem()] }
|
||||
})
|
||||
)
|
||||
act(() => listener?.(snapshotEvent(3, [approvalItem()])))
|
||||
let attempts = 0
|
||||
sendRequest.mockImplementation(async (method, params) => {
|
||||
if (method === 'agentSession.respondToApproval' && attempts++ === 0) {
|
||||
@@ -749,15 +685,7 @@ describe('useMobileStructuredAgentSession', () => {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
await vi.waitFor(() => expect(listener).toEqual(expect.any(Function)))
|
||||
act(() =>
|
||||
listener?.({
|
||||
...snapshotEvent(3),
|
||||
page: {
|
||||
...snapshotEvent(3).page,
|
||||
items: [runningStatusItem()]
|
||||
}
|
||||
})
|
||||
)
|
||||
act(() => listener?.(snapshotEvent(3, [runningStatusItem()])))
|
||||
sendRequest.mockImplementation(async (method, params) => {
|
||||
if (method === 'agentSession.cancel') {
|
||||
throw markRpcDeliveryUnknown(new Error('Connection closed'))
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('useMobileStructuredAgentState older history', () => {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence: 1,
|
||||
page: page(newest, true)
|
||||
page: { ...page(newest, true), direction: 'tail' }
|
||||
})
|
||||
})
|
||||
await vi.waitFor(() => expect(hook.current!.state.status).toBe('ready'))
|
||||
@@ -241,7 +241,7 @@ describe('useMobileStructuredAgentState live feed', () => {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence: 1,
|
||||
page: { ...page([said(58), said(59)], false), liveCursor: cursorAt(59) }
|
||||
page: { ...page([said(58), said(59)], false), direction: 'tail', liveCursor: cursorAt(59) }
|
||||
})
|
||||
)
|
||||
await act(async () => {
|
||||
|
||||
@@ -52,11 +52,7 @@ function userRow(clientMessageId: string, text: string, sequence: number): Agent
|
||||
}
|
||||
|
||||
function journal(items: AgentJournalRenderItem[], submissions: AgentJournalSubmission[]) {
|
||||
const event = snapshotEvent({ submissions })
|
||||
if (event.type === 'snapshot') {
|
||||
event.page.items = items
|
||||
}
|
||||
return event
|
||||
return snapshotEvent({ items, submissions })
|
||||
}
|
||||
|
||||
describe('a structured send in the open chat', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createElement } from 'react'
|
||||
import { snapshotEvent } from './use-mobile-structured-agent-session-queued.test-fixture'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types'
|
||||
@@ -21,30 +22,7 @@ function snapshot(
|
||||
fence: number,
|
||||
activity?: AgentSessionTurnActivity
|
||||
): AgentSessionSubscribeEvent {
|
||||
const newest = items.length
|
||||
return {
|
||||
type: 'snapshot',
|
||||
sessionId: 'session-1',
|
||||
fence,
|
||||
...(activity ? { activity } : {}),
|
||||
page: {
|
||||
sessionId: 'session-1',
|
||||
epoch: 'epoch-1',
|
||||
fence,
|
||||
direction: 'tail',
|
||||
items,
|
||||
removedItemIds: [],
|
||||
submissions: [],
|
||||
window: {
|
||||
oldest: { epoch: 'epoch-1', sequence: 1 },
|
||||
newest: { epoch: 'epoch-1', sequence: newest },
|
||||
nextCursor: { epoch: 'epoch-1', sequence: newest + 1 }
|
||||
},
|
||||
liveCursor: { epoch: 'epoch-1', sequence: newest },
|
||||
hasOlder: false,
|
||||
hasNewer: false
|
||||
}
|
||||
}
|
||||
return { ...snapshotEvent({ items, fence }), ...(activity ? { activity } : {}) }
|
||||
}
|
||||
|
||||
/** What the one live indicator row reads, resolved off the session journal. */
|
||||
|
||||
@@ -3,6 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import {
|
||||
type MobileTerminalInventoryRefreshOptions,
|
||||
useMobileTerminalInventoryRecovery,
|
||||
useMobileTerminalInventoryRecoveryBridge
|
||||
} from './use-mobile-terminal-inventory-recovery'
|
||||
@@ -30,7 +31,9 @@ function deferred<T>() {
|
||||
const client = {
|
||||
getState: () => 'connected'
|
||||
} as unknown as RpcClient
|
||||
const fetchTerminals = vi.fn(async () => true)
|
||||
const fetchTerminals = vi.fn<(options?: MobileTerminalInventoryRefreshOptions) => Promise<boolean>>(
|
||||
async () => true
|
||||
)
|
||||
type RecoveryActions = ReturnType<typeof useMobileTerminalInventoryRecovery>
|
||||
let actions: RecoveryActions | null = null
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('the page pane hook', () => {
|
||||
expect(bridge.cleared).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('takes the tap that arrived while it was applying the one before it', () => {
|
||||
it('applies successive pane requests and names each value when clearing', () => {
|
||||
render(true)
|
||||
deliver(`tab-a:${LEAF}`)
|
||||
deliver(`tab-b:${LEAF}`)
|
||||
@@ -172,13 +172,6 @@ describe('the page pane hook', () => {
|
||||
expect(bridge.cleared.map((entry) => entry.value)).toEqual([`tab-a:${LEAF}`, `tab-b:${LEAF}`])
|
||||
})
|
||||
|
||||
it('takes a different pane as its own request', () => {
|
||||
render(true)
|
||||
deliver(`tab-a:${LEAF}`)
|
||||
deliver(`tab-b:${LEAF}`)
|
||||
expect(switched.map((tab) => tab.id)).toEqual(['first', 'second'])
|
||||
})
|
||||
|
||||
it('asks for nothing and applies nothing when the route carries no pane', () => {
|
||||
// Load-bearing rather than defensive: the shell's own erase comes back as a route that moved,
|
||||
// and so does every `ready` answered after it.
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildMobileBranchCompareSection,
|
||||
canOpenMobileBranchCompareDiff,
|
||||
formatMobileBranchCompareSummary,
|
||||
type MobileGitBranchCompareResult
|
||||
formatMobileBranchCompareSummary
|
||||
} from './mobile-branch-compare'
|
||||
|
||||
describe('mobile branch compare helpers', () => {
|
||||
it('keeps the mobile branch compare type in lockstep with the runtime contract', () => {
|
||||
expectTypeOf<MobileGitBranchCompareResult>().toEqualTypeOf<GitBranchCompareResult>()
|
||||
})
|
||||
|
||||
it('sorts committed branch entries by path', () => {
|
||||
const section = buildMobileBranchCompareSection([
|
||||
{ path: 'zeta.ts', status: 'modified' },
|
||||
@@ -32,6 +26,7 @@ describe('mobile branch compare helpers', () => {
|
||||
mergeBase: 'c'.repeat(40),
|
||||
changedFiles: 2,
|
||||
commitsAhead: 1,
|
||||
errorMessage: undefined,
|
||||
status: 'ready'
|
||||
})
|
||||
).toBe('2 files - 1 commit - vs origin/main')
|
||||
@@ -46,6 +41,8 @@ describe('mobile branch compare helpers', () => {
|
||||
headOid: 'b'.repeat(40),
|
||||
mergeBase: 'c'.repeat(40),
|
||||
changedFiles: 1,
|
||||
commitsAhead: undefined,
|
||||
errorMessage: undefined,
|
||||
status: 'ready'
|
||||
})
|
||||
).toBe(true)
|
||||
@@ -58,6 +55,8 @@ describe('mobile branch compare helpers', () => {
|
||||
headOid: null,
|
||||
mergeBase: null,
|
||||
changedFiles: 0,
|
||||
commitsAhead: undefined,
|
||||
errorMessage: undefined,
|
||||
status: 'unborn-head'
|
||||
})
|
||||
).toBe(false)
|
||||
|
||||
@@ -58,12 +58,10 @@ src/session/use-mobile-pr-sidebar-controller.test.ts
|
||||
src/session/use-mobile-session-terminal-create-actions.test.ts
|
||||
src/session/use-mobile-structured-agent-session.test.tsx
|
||||
src/session/use-mobile-structured-prompt-responses.test.tsx
|
||||
src/session/use-mobile-terminal-inventory-recovery.test.ts
|
||||
src/settings/native-notification-delivery-settings.test.tsx
|
||||
src/settings/notification-display-test.test.tsx
|
||||
src/settings/settings-screen-state.test.tsx
|
||||
src/settings/voice-settings-poller-refresh.test.tsx
|
||||
src/source-control/mobile-branch-compare.test.ts
|
||||
src/source-control/mobile-create-pr-action.test.ts
|
||||
src/source-control/mobile-git-status.test.ts
|
||||
src/source-control/mobile-hosted-review-create-intent-runner.test.ts
|
||||
|
||||
+17
-14
@@ -201,20 +201,23 @@ describe('Store', () => {
|
||||
expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves explicit "left" / "right" terminalMacOptionAsAlt through migration', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: {},
|
||||
settings: { terminalMacOptionAsAlt: 'left' },
|
||||
ui: {},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: {}
|
||||
})
|
||||
const store = await createStore()
|
||||
expect(store.getSettings().terminalMacOptionAsAlt).toBe('left')
|
||||
expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true)
|
||||
})
|
||||
it.each(['left', 'right'] as const)(
|
||||
'preserves explicit "%s" terminalMacOptionAsAlt through migration',
|
||||
async (side) => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: {},
|
||||
settings: { terminalMacOptionAsAlt: side },
|
||||
ui: {},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: {}
|
||||
})
|
||||
const store = await createStore()
|
||||
expect(store.getSettings().terminalMacOptionAsAlt).toBe(side)
|
||||
expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it('respects already-migrated settings with explicit "true"', async () => {
|
||||
writeDataFile({
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
makeRepo,
|
||||
makeWorktreeLineage
|
||||
} from '../persistence-test-harness'
|
||||
import {
|
||||
makeTerminalTab,
|
||||
makeTerminalTab as makeSessionTerminalTab
|
||||
} from '../persistence-session-fixtures'
|
||||
import { makeTerminalTab } from '../persistence-session-fixtures'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { rmSync, mkdtempSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
@@ -86,9 +83,6 @@ describe('Store', () => {
|
||||
await closeTestStores()
|
||||
rmSync(testState.dir, { recursive: true, force: true })
|
||||
})
|
||||
it('keeps the persistence harness terminal-tab fixture as the same public function', () => {
|
||||
expect(makeTerminalTab).toBe(makeSessionTerminalTab)
|
||||
})
|
||||
|
||||
it('addRepo stores a repo retrievable by getRepo', async () => {
|
||||
const store = await createStore()
|
||||
@@ -170,57 +164,6 @@ describe('Store', () => {
|
||||
expect(store.getRepo('sibling')?.projectGroupId).toBe(sibling.id)
|
||||
})
|
||||
|
||||
it('adapts flat folder-scan groups into sparse nested folder scopes on load', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [
|
||||
makeRepo({ id: 'api', path: '/workspace/platform/api', projectGroupId: 'root' }),
|
||||
makeRepo({ id: 'web', path: '/workspace/platform/web', projectGroupId: 'root' }),
|
||||
makeRepo({
|
||||
id: 'repo1',
|
||||
path: '/workspace/platform/packages/shared/repo1',
|
||||
projectGroupId: 'root'
|
||||
}),
|
||||
makeRepo({
|
||||
id: 'repo2',
|
||||
path: '/workspace/platform/packages/shared/repo2',
|
||||
projectGroupId: 'root'
|
||||
})
|
||||
],
|
||||
worktreeMeta: {},
|
||||
settings: {},
|
||||
ui: {},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
projectGroups: [
|
||||
{
|
||||
id: 'root',
|
||||
name: 'Platform',
|
||||
parentPath: '/workspace/platform',
|
||||
parentGroupId: null,
|
||||
createdFrom: 'folder-scan',
|
||||
tabOrder: 0,
|
||||
isCollapsed: false,
|
||||
color: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const store = await createStore()
|
||||
const groups = store.getProjectGroups()
|
||||
const shared = groups.find((group) => group.name === 'packages/shared')
|
||||
|
||||
expect(groups.map((group) => [group.name, group.parentGroupId, group.parentPath])).toEqual([
|
||||
['Platform', null, '/workspace/platform'],
|
||||
['packages/shared', 'root', '/workspace/platform/packages/shared']
|
||||
])
|
||||
expect(store.getRepo('api')?.projectGroupId).toBe('root')
|
||||
expect(store.getRepo('web')?.projectGroupId).toBe('root')
|
||||
expect(store.getRepo('repo1')?.projectGroupId).toBe(shared?.id)
|
||||
expect(store.getRepo('repo2')?.projectGroupId).toBe(shared?.id)
|
||||
})
|
||||
|
||||
it('creates a project group when persisted group history is very large', async () => {
|
||||
const projectGroups: ProjectGroup[] = Array.from({ length: 130_000 }, (_, index) => ({
|
||||
id: `group-${index}`,
|
||||
@@ -466,40 +409,6 @@ describe('Store', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('removeProjectForHost keeps the surviving host session for a shared repo id + path', async () => {
|
||||
const store = await createStore()
|
||||
// Identical repo IDs and paths require host-scoped session pruning.
|
||||
store.addRepo(makeRepo({ id: 'shared', path: '/repo' }))
|
||||
store.addRepo(
|
||||
makeRepo({
|
||||
id: 'shared',
|
||||
path: '/repo',
|
||||
connectionId: 'ssh-a',
|
||||
executionHostId: 'ssh:ssh-a'
|
||||
})
|
||||
)
|
||||
store.setWorktreeMeta(at('shared::/repo', 'local'), { displayName: 'local' })
|
||||
|
||||
store.setWorkspaceSession({
|
||||
...getDefaultWorkspaceSession(),
|
||||
lastVisitedAtByWorktreeId: { 'shared::/repo': 111 }
|
||||
})
|
||||
store.setWorkspaceSession(
|
||||
{
|
||||
...getDefaultWorkspaceSession(),
|
||||
lastVisitedAtByWorktreeId: { 'shared::/repo': 222 }
|
||||
},
|
||||
'ssh:ssh-a'
|
||||
)
|
||||
|
||||
store.removeProjectForHost({ id: 'shared', executionHostId: 'ssh:ssh-a' })
|
||||
|
||||
expect(
|
||||
store.getWorkspaceSession('ssh:ssh-a').lastVisitedAtByWorktreeId?.['shared::/repo']
|
||||
).toBeUndefined()
|
||||
expect(store.getWorkspaceSession().lastVisitedAtByWorktreeId?.['shared::/repo']).toBe(111)
|
||||
})
|
||||
|
||||
it('removeProjectForHost on the local host keeps a surviving SSH host session', async () => {
|
||||
const store = await createStore()
|
||||
store.addRepo(makeRepo({ id: 'shared', path: '/repo' }))
|
||||
@@ -665,29 +574,6 @@ describe('Store', () => {
|
||||
expect(store.getRepos().map((repo) => repo.path)).toEqual(originalPaths)
|
||||
})
|
||||
|
||||
it('removeProjectForHost prunes the SSH host meta (tagged hostId) for a shared id', async () => {
|
||||
const store = await createStore()
|
||||
store.addRepo(makeRepo({ id: 'shared', path: '/local/repo' }))
|
||||
store.addRepo(
|
||||
makeRepo({
|
||||
id: 'shared',
|
||||
path: '/remote/repo',
|
||||
connectionId: 'ssh-old',
|
||||
executionHostId: 'ssh:ssh-old'
|
||||
})
|
||||
)
|
||||
store.setWorktreeMeta(at('shared::/remote/repo/wt', 'ssh:ssh-old'), {
|
||||
displayName: 'remote-wt'
|
||||
})
|
||||
// Hostless legacy metadata belongs to local; remote removal must preserve it.
|
||||
store.setWorktreeMeta(at('shared::/local/repo/wt', 'local'), { displayName: 'local-wt' })
|
||||
|
||||
store.removeProjectForHost({ id: 'shared', executionHostId: 'ssh:ssh-old' })
|
||||
|
||||
expect(store.getWorktreeMeta('shared::/remote/repo/wt')).toBeUndefined()
|
||||
expect(store.getWorktreeMeta('shared::/local/repo/wt')).toBeDefined()
|
||||
})
|
||||
|
||||
it('removeProjectForHost prunes everything when the id exists on no other host', async () => {
|
||||
const store = await createStore()
|
||||
store.addRepo(makeRepo({ id: 'only', connectionId: 'ssh-x', executionHostId: 'ssh:ssh-x' }))
|
||||
|
||||
@@ -707,38 +707,6 @@ export function registerPtyConnectionInterruptInferenceTests(): void {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not request interrupt inference from a selection-copy keydown without terminal input', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
transportFactoryQueue.push(transport)
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(1_100)
|
||||
const paneKey = makePaneKey('tab-1', LEAF_1)
|
||||
mockStoreState.agentStatusByPaneKey[paneKey] = {
|
||||
state: 'working',
|
||||
prompt: 'copy selection',
|
||||
updatedAt: 1_000,
|
||||
stateStartedAt: 900,
|
||||
agentType: 'codex',
|
||||
paneKey,
|
||||
terminalTitle: 'Codex',
|
||||
stateHistory: []
|
||||
}
|
||||
const terminalTarget = createKeyboardEventTarget()
|
||||
const pane = createPane(1)
|
||||
;(pane.terminal as { element?: unknown; hasSelection: ReturnType<typeof vi.fn> }).element =
|
||||
terminalTarget.target
|
||||
;(pane.terminal as { hasSelection: ReturnType<typeof vi.fn> }).hasSelection.mockReturnValue(
|
||||
true
|
||||
)
|
||||
|
||||
connectPanePty(pane as never, createManager(1) as never, createDeps() as never)
|
||||
terminalTarget.dispatch(keyEvent({ key: 'c', ctrlKey: true }))
|
||||
vi.advanceTimersByTime(500)
|
||||
|
||||
expect(window.api.agentStatus.inferInterrupt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('removes the terminal key listener on dispose before a remount adds another', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const firstTransport = createMockTransport()
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@ export function registerPtyConnectionNotificationSettingsGatingTests(): void {
|
||||
)
|
||||
})
|
||||
|
||||
it('raises terminal attention for agent completion when OS completion notifications are disabled', async () => {
|
||||
it('forwards agent completion to notification policy when desktop banners are disabled', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport('pty-codex')
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
+10
-6
@@ -113,7 +113,7 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
|
||||
const pane = createPane(1)
|
||||
captureCallbackTerminalWrites(pane)
|
||||
const { writes } = captureCallbackTerminalWrites(pane)
|
||||
|
||||
// Why a counting getter: the restore task's `.finally` reads isVisibleRef.current
|
||||
// on every re-arm, so read count is a direct, deterministic measure of how many
|
||||
@@ -143,12 +143,16 @@ describe('connectPanePty', () => {
|
||||
})
|
||||
await flushAsyncTicks(20)
|
||||
|
||||
expect(getMainBufferSnapshot).toHaveBeenCalledTimes(1)
|
||||
expect(writes).toContain('snapshot-state\r\n')
|
||||
disposable.dispose()
|
||||
const readsAtDispose = visibilityReads
|
||||
await flushAsyncTicks(200)
|
||||
|
||||
// A terminated chain settles in a handful of turns; the self-feeding loop grew
|
||||
// once per microtask turn and would blow past this by orders of magnitude.
|
||||
expect(visibilityReads - readsAtDispose).toBeLessThan(50)
|
||||
try {
|
||||
await flushAsyncTicks(200)
|
||||
expect(visibilityReads - readsAtDispose).toBeLessThan(50)
|
||||
} finally {
|
||||
// Stop a regressed re-arm loop after measuring it so failures can finish.
|
||||
visible = false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ export function registerPtyConnectionRemotePauseDropNoticeTests(): void {
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('still restores on every reveal when the host cannot report drops', async () => {
|
||||
it('restores on reveal when the host cannot report drops', async () => {
|
||||
const { transport, binding, setVisible } = await connectRemotePane(false)
|
||||
|
||||
await setVisible(false)
|
||||
|
||||
+1
-13
@@ -1,6 +1,6 @@
|
||||
import type * as React from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushAsyncTicks, renderHeadlessBuffer } from './pty-connection-test-async'
|
||||
import { flushAsyncTicks } from './pty-connection-test-async'
|
||||
import {
|
||||
terminalWriteText,
|
||||
createMockTransport,
|
||||
@@ -233,16 +233,4 @@ describe('pushed remote snapshot replay grid', () => {
|
||||
expect(session.pane.terminal.resize).not.toHaveBeenCalledWith(HOST_COLS, HOST_ROWS)
|
||||
session.dispose()
|
||||
})
|
||||
|
||||
it('only reproduces the host frame when it is parsed at the host grid', async () => {
|
||||
const atHostGrid = await renderHeadlessBuffer([HOST_FRAME], HOST_COLS, HOST_ROWS)
|
||||
const atPaneGrid = await renderHeadlessBuffer([HOST_FRAME], PANE_COLS, HOST_ROWS - 4)
|
||||
|
||||
// Why this is the user-visible failure: the alternate screen has no
|
||||
// scrollback, so rows scrolled off by a shorter grid are gone for good and
|
||||
// an idle TUI never repaints them.
|
||||
expect(atHostGrid.filter((line) => line.startsWith('host row'))).toHaveLength(HOST_ROWS)
|
||||
expect(atPaneGrid.filter((line) => line.startsWith('host row')).length).toBeLessThan(HOST_ROWS)
|
||||
expect(atPaneGrid).not.toContain('host row 1')
|
||||
})
|
||||
})
|
||||
|
||||
+2
-3
@@ -271,7 +271,7 @@ export function registerPtyConnectionRuntimeOwnerSpawnRoutingTests(): void {
|
||||
expect(deps.syncPanePtyLayoutBinding).toHaveBeenCalledWith(2, 'remote:env-1@@terminal-2')
|
||||
})
|
||||
|
||||
it('persists a restarted pane PTY id and uses it on the next remount', async () => {
|
||||
it('publishes a restarted pane PTY id and reuses its restored hint on remount', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
|
||||
const restartedTransport = createMockTransport()
|
||||
@@ -325,8 +325,7 @@ export function registerPtyConnectionRuntimeOwnerSpawnRoutingTests(): void {
|
||||
expect(remountDeps.syncPanePtyLayoutBinding).toHaveBeenCalledWith(1, 'pty-restarted')
|
||||
})
|
||||
|
||||
// Why: BEL (0x07) is the attention signal — onBell raises the bell event + an OS notification.
|
||||
it('wires onBell to the bell attention event and OS notification', async () => {
|
||||
it('forwards onBell to pane attention and notification policy', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
+1
-1
@@ -322,7 +322,7 @@ describe('connectPanePty: a restore the host kept asleep', () => {
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('stops the background retry when the pane is opened or closed', async () => {
|
||||
it('stops the background retry when the pane is disposed', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = deferringTransport()
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
+1
-1
@@ -138,7 +138,7 @@ describe('fresh spawn leaves a local pane unbound', () => {
|
||||
return buildPaneConnectionDeps(() => mockStoreState, overrides)
|
||||
}
|
||||
|
||||
it('remounts the pane when the spawn resolves without a PTY id', async () => {
|
||||
it('requests pane recovery when the spawn resolves without a PTY id', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
// A spawn that produced nothing: no id returned and nothing bound after it.
|
||||
|
||||
@@ -75,24 +75,30 @@ const TEXT_FIELDS: Record<string, TextField> = {
|
||||
lastMessage: { cap: 512, carry: (lastMessage) => ({ lastMessage }), read: (c) => c?.lastMessage }
|
||||
}
|
||||
|
||||
function hostileText(cap: number): [string, string][] {
|
||||
function hostileText(cap: number): [string, string, string | undefined][] {
|
||||
return [
|
||||
['a tab', 'col1\tcol2'],
|
||||
['a CRLF', 'line one\r\nline two'],
|
||||
['a line separator', 'one\u2028two'],
|
||||
['a paragraph separator', 'one\u2029two'],
|
||||
['a next-line control', 'one\u0085two'],
|
||||
['a no-break space inside and around', '\u00a0keep\u00a0this\u00a0'],
|
||||
['an escape sequence', 'plain \u001b[31mred'],
|
||||
['a delete', 'rub\u007fout'],
|
||||
['whitespace only', ' \t\r\n\u2028 '],
|
||||
// A space at character cap - 1, cap and cap + 1: just inside the cut, on it, and past it.
|
||||
...[-1, 0, 1].map((offset): [string, string] => [
|
||||
`a space at character cap${offset < 0 ? '' : '+'}${offset}`,
|
||||
`${'x'.repeat(cap + offset - 1)} tail`
|
||||
]),
|
||||
['a surrogate pair split by the cut', `${'x'.repeat(cap - 1)}\u{1f600}tail`],
|
||||
['a surrogate pair ending at the cut', `${'x'.repeat(cap - 2)}\u{1f600}tail`]
|
||||
['a tab', 'col1\tcol2', 'col1 col2'],
|
||||
['a CRLF', 'line one\r\nline two', 'line one line two'],
|
||||
['a line separator', 'one\u2028two', 'one two'],
|
||||
['a paragraph separator', 'one\u2029two', 'one two'],
|
||||
['a next-line control', 'one\u0085two', 'one two'],
|
||||
['a no-break space inside and around', '\u00a0keep\u00a0this\u00a0', 'keep\u00a0this'],
|
||||
['an escape sequence', 'plain \u001b[31mred', 'plain [31mred'],
|
||||
['a delete', 'rub\u007fout', 'rub out'],
|
||||
['whitespace only', ' \t\r\n\u2028 ', undefined],
|
||||
['a space at character cap-1', `${'x'.repeat(cap - 2)} tail`, `${'x'.repeat(cap - 2)} t`],
|
||||
['a space at character cap+0', `${'x'.repeat(cap - 1)} tail`, 'x'.repeat(cap - 1)],
|
||||
['a space at character cap+1', `${'x'.repeat(cap)} tail`, 'x'.repeat(cap)],
|
||||
[
|
||||
'a surrogate pair split by the cut',
|
||||
`${'x'.repeat(cap - 1)}\u{1f600}tail`,
|
||||
'x'.repeat(cap - 1)
|
||||
],
|
||||
[
|
||||
'a surrogate pair ending at the cut',
|
||||
`${'x'.repeat(cap - 2)}\u{1f600}tail`,
|
||||
`${'x'.repeat(cap - 2)}\u{1f600}`
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -105,25 +111,30 @@ function breaksOneLine(text: string): boolean {
|
||||
|
||||
describe('child-work admission parses provider text into what the codec stores', () => {
|
||||
const cases = Object.entries(TEXT_FIELDS).flatMap(([field, spec]) =>
|
||||
hostileText(spec.cap).map(([label, raw]) => [field, label, raw, spec] as const)
|
||||
hostileText(spec.cap).map(
|
||||
([label, raw, expected]) => [field, label, raw, spec, expected] as const
|
||||
)
|
||||
)
|
||||
|
||||
it.each(cases)('%s with %s is admitted at a fixed point', (_field, _label, raw, spec) => {
|
||||
const { store, admission } = setup()
|
||||
expect(admission.announce(observation(spec.carry(raw)))).toMatchObject({ accepted: true })
|
||||
const stored = spec.read(store.getChild('child-1'))
|
||||
expect(stored).toBe(normalizeChildWorkText(raw, spec.cap))
|
||||
if (stored === undefined) {
|
||||
return
|
||||
it.each(cases)(
|
||||
'%s with %s is admitted at a fixed point',
|
||||
(_field, _label, raw, spec, expected) => {
|
||||
const { store, admission } = setup()
|
||||
expect(admission.announce(observation(spec.carry(raw)))).toMatchObject({ accepted: true })
|
||||
const stored = spec.read(store.getChild('child-1'))
|
||||
expect(stored).toBe(expected)
|
||||
if (stored === undefined) {
|
||||
return
|
||||
}
|
||||
expect(normalizeChildWorkText(stored, spec.cap)).toBe(stored)
|
||||
// Independent of the normalizer: one line, trimmed, within the cap, no half pair at the cut.
|
||||
expect(breaksOneLine(stored)).toBe(false)
|
||||
expect(stored).toBe(stored.trim())
|
||||
expect(stored.length).toBeLessThanOrEqual(spec.cap)
|
||||
const last = stored.charCodeAt(stored.length - 1)
|
||||
expect(last >= 0xd800 && last <= 0xdbff).toBe(false)
|
||||
}
|
||||
expect(normalizeChildWorkText(stored, spec.cap)).toBe(stored)
|
||||
// Independent of the normalizer: one line, trimmed, within the cap, no half pair at the cut.
|
||||
expect(breaksOneLine(stored)).toBe(false)
|
||||
expect(stored).toBe(stored.trim())
|
||||
expect(stored.length).toBeLessThanOrEqual(spec.cap)
|
||||
const last = stored.charCodeAt(stored.length - 1)
|
||||
expect(last >= 0xd800 && last <= 0xdbff).toBe(false)
|
||||
})
|
||||
)
|
||||
|
||||
it('keeps a surrogate pair whole when it ends exactly at the cap', () => {
|
||||
const { store, admission } = setup()
|
||||
|
||||
@@ -93,10 +93,11 @@ describe('AgentChildWorkRecord', () => {
|
||||
})
|
||||
|
||||
it('parses a copied record with outcome, tokens, timing and bounded invocation history', () => {
|
||||
const parsed = parseAgentChildWorkRecord({ ...child(), revision: 7 })
|
||||
const value = { ...child(), revision: 7 }
|
||||
const parsed = parseAgentChildWorkRecord(value)
|
||||
|
||||
expect(parsed).toEqual({ ...child(), revision: 7 })
|
||||
expect(parsed).not.toBe(child())
|
||||
expect(parsed).not.toBe(value)
|
||||
expect(parsed?.parent).not.toBe(parent)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user