mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344.
452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
import { createElement } from 'react'
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
|
import type { RpcResponse } from '../transport/types'
|
|
import { useQuickCommands } from './use-quick-commands'
|
|
|
|
const FIRST: TerminalQuickCommand = {
|
|
id: 'first',
|
|
label: 'First',
|
|
action: 'terminal-command',
|
|
command: 'echo first',
|
|
appendEnter: true,
|
|
scope: { type: 'global' }
|
|
}
|
|
const SECOND: TerminalQuickCommand = {
|
|
id: 'second',
|
|
label: 'Second',
|
|
action: 'terminal-command',
|
|
command: 'echo second',
|
|
appendEnter: true,
|
|
scope: { type: 'global' }
|
|
}
|
|
|
|
function success(commands: TerminalQuickCommand[]): RpcResponse {
|
|
return {
|
|
ok: true,
|
|
result: { terminalQuickCommands: commands }
|
|
} as RpcResponse
|
|
}
|
|
|
|
function failure(message: string): RpcResponse {
|
|
return {
|
|
ok: false,
|
|
error: { code: 'internal_error', message }
|
|
} as RpcResponse
|
|
}
|
|
|
|
function deferred<T>() {
|
|
let resolve: (value: T) => void = () => {}
|
|
const promise = new Promise<T>((done) => {
|
|
resolve = done
|
|
})
|
|
return { promise, resolve }
|
|
}
|
|
|
|
describe('useQuickCommands', () => {
|
|
let renderer: ReactTestRenderer | null = null
|
|
let state: ReturnType<typeof useQuickCommands> | null = null
|
|
afterEach(() => {
|
|
act(() => renderer?.unmount())
|
|
renderer = null
|
|
state = null
|
|
})
|
|
|
|
async function mount(client: RpcClient, enabled = true): Promise<void> {
|
|
function Harness(): null {
|
|
state = useQuickCommands({ client, enabled })
|
|
return null
|
|
}
|
|
await act(async () => {
|
|
renderer = create(createElement(Harness))
|
|
await Promise.resolve()
|
|
})
|
|
}
|
|
|
|
it('does not fetch settings while the sheet is closed', async () => {
|
|
const client = { sendRequest: vi.fn() } as unknown as RpcClient
|
|
|
|
await mount(client, false)
|
|
|
|
expect(client.sendRequest).not.toHaveBeenCalled()
|
|
expect(state?.ready).toBe(false)
|
|
})
|
|
|
|
it('replays the load after a connection-migration cutover', async () => {
|
|
const client = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new LogicalClientCutoverError())
|
|
.mockResolvedValueOnce(success([FIRST]))
|
|
} as unknown as RpcClient
|
|
|
|
await mount(client)
|
|
|
|
expect(client.sendRequest).toHaveBeenCalledTimes(2)
|
|
expect(state?.commands).toEqual([FIRST])
|
|
expect(state?.ready).toBe(true)
|
|
expect(state?.error).toBeNull()
|
|
})
|
|
|
|
it('surfaces the cutover error once replays are exhausted', async () => {
|
|
const client = {
|
|
sendRequest: vi.fn(() => Promise.reject(new LogicalClientCutoverError()))
|
|
} as unknown as RpcClient
|
|
|
|
await mount(client)
|
|
|
|
// Initial attempt + 5 replays, then give up rather than loop forever.
|
|
expect(client.sendRequest).toHaveBeenCalledTimes(6)
|
|
expect(state?.ready).toBe(false)
|
|
expect(state?.error).toBe('RPC interrupted by connection migration')
|
|
})
|
|
|
|
it('does not replay non-cutover load failures', async () => {
|
|
const client = {
|
|
sendRequest: vi.fn(() => Promise.reject(new Error('boom')))
|
|
} as unknown as RpcClient
|
|
|
|
await mount(client)
|
|
|
|
expect(client.sendRequest).toHaveBeenCalledTimes(1)
|
|
expect(state?.ready).toBe(false)
|
|
expect(state?.error).toBe('boom')
|
|
})
|
|
|
|
it('stops replaying a cutover-interrupted load after the sheet closes', async () => {
|
|
let rejectLoad: (error: Error) => void = () => {}
|
|
const client = {
|
|
sendRequest: vi.fn(
|
|
() =>
|
|
new Promise<RpcResponse>((_resolve, reject) => {
|
|
rejectLoad = reject
|
|
})
|
|
)
|
|
} as unknown as RpcClient
|
|
|
|
function Harness({ enabled }: { enabled: boolean }): null {
|
|
state = useQuickCommands({ client, enabled })
|
|
return null
|
|
}
|
|
await act(async () => {
|
|
renderer = create(createElement(Harness, { enabled: true }))
|
|
await Promise.resolve()
|
|
})
|
|
await act(async () => {
|
|
renderer!.update(createElement(Harness, { enabled: false }))
|
|
await Promise.resolve()
|
|
})
|
|
await act(async () => {
|
|
rejectLoad(new LogicalClientCutoverError())
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(client.sendRequest).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('keeps mutations disabled when the remote list could not be loaded', async () => {
|
|
const client = {
|
|
sendRequest: vi.fn().mockResolvedValue(failure('load failed'))
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
await act(async () => {
|
|
const persisted = await state!.persist({ type: 'delete', id: FIRST.id })
|
|
expect(persisted).toBe(false)
|
|
})
|
|
|
|
expect(state?.ready).toBe(false)
|
|
expect(state?.error).toBe('load failed')
|
|
expect(client.sendRequest).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('keeps mutations disabled when a successful load has a malformed list', async () => {
|
|
const client = {
|
|
sendRequest: vi.fn().mockResolvedValue({ ok: true, result: {} } as RpcResponse)
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
await act(async () => {
|
|
const persisted = await state!.persist({ type: 'delete', id: FIRST.id })
|
|
expect(persisted).toBe(false)
|
|
})
|
|
|
|
expect(state?.ready).toBe(false)
|
|
expect(state?.error).toBe('Failed to load quick commands')
|
|
expect(client.sendRequest).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('keeps a later queued mutation after an earlier mutation fails', async () => {
|
|
const firstUpdate = deferred<RpcResponse>()
|
|
const secondUpdate = deferred<RpcResponse>()
|
|
const updateParams: unknown[] = []
|
|
const client = {
|
|
sendRequest: vi.fn((method: string, params?: unknown) => {
|
|
if (method === 'settings.getTerminalQuickCommands') {
|
|
return Promise.resolve(success([FIRST, SECOND]))
|
|
}
|
|
updateParams.push(params)
|
|
return updateParams.length === 1 ? firstUpdate.promise : secondUpdate.promise
|
|
})
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
expect(state?.commands).toEqual([FIRST, SECOND])
|
|
|
|
let firstPersist: Promise<boolean> = Promise.resolve(false)
|
|
let secondPersist: Promise<boolean> = Promise.resolve(false)
|
|
await act(async () => {
|
|
firstPersist = state!.persist({ type: 'delete', id: FIRST.id })
|
|
secondPersist = state!.persist({ type: 'delete', id: SECOND.id })
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(state?.commands).toEqual([])
|
|
expect(updateParams).toHaveLength(1)
|
|
expect(updateParams[0]).toEqual({ mutation: { type: 'delete', id: FIRST.id } })
|
|
|
|
await act(async () => {
|
|
firstUpdate.resolve(failure('first failed'))
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
})
|
|
expect(updateParams).toEqual([
|
|
{ mutation: { type: 'delete', id: FIRST.id } },
|
|
{ mutation: { type: 'delete', id: SECOND.id } }
|
|
])
|
|
expect(state?.commands).toEqual([FIRST])
|
|
|
|
await act(async () => {
|
|
secondUpdate.resolve(success([FIRST]))
|
|
await Promise.all([firstPersist, secondPersist])
|
|
})
|
|
expect(await firstPersist).toBe(false)
|
|
expect(await secondPersist).toBe(true)
|
|
expect(state?.commands).toEqual([FIRST])
|
|
expect(state?.error).toBeNull()
|
|
})
|
|
|
|
it('adopts the latest server-normalized list between queued mutations', async () => {
|
|
const normalizedFirst = { ...FIRST, label: 'Server normalized' }
|
|
const firstUpdate = deferred<RpcResponse>()
|
|
const secondUpdate = deferred<RpcResponse>()
|
|
const updateParams: unknown[] = []
|
|
const client = {
|
|
sendRequest: vi.fn((method: string, params?: unknown) => {
|
|
if (method === 'settings.getTerminalQuickCommands') {
|
|
return Promise.resolve(success([FIRST, SECOND]))
|
|
}
|
|
updateParams.push(params)
|
|
return updateParams.length === 1 ? firstUpdate.promise : secondUpdate.promise
|
|
})
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
let firstPersist: Promise<boolean> = Promise.resolve(false)
|
|
let secondPersist: Promise<boolean> = Promise.resolve(false)
|
|
await act(async () => {
|
|
firstPersist = state!.persist({
|
|
type: 'upsert',
|
|
command: { ...FIRST, label: 'Local label' }
|
|
})
|
|
secondPersist = state!.persist({ type: 'delete', id: SECOND.id })
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(updateParams[0]).toEqual({
|
|
mutation: { type: 'upsert', command: { ...FIRST, label: 'Local label' } }
|
|
})
|
|
await act(async () => {
|
|
firstUpdate.resolve(success([normalizedFirst, SECOND]))
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
})
|
|
expect(updateParams[1]).toEqual({ mutation: { type: 'delete', id: SECOND.id } })
|
|
|
|
await act(async () => {
|
|
secondUpdate.resolve(success([normalizedFirst]))
|
|
await Promise.all([firstPersist, secondPersist])
|
|
})
|
|
expect(state?.commands).toEqual([normalizedFirst])
|
|
})
|
|
|
|
it('preserves unrelated commands added by another client while the sheet is open', async () => {
|
|
const edited = { ...FIRST, label: 'Edited on mobile' }
|
|
const client = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(success([FIRST]))
|
|
// The host applies the targeted upsert to a list that desktop changed.
|
|
.mockResolvedValueOnce(success([edited, SECOND]))
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
await act(async () => {
|
|
await state!.persist({ type: 'upsert', command: edited })
|
|
})
|
|
|
|
expect(client.sendRequest).toHaveBeenLastCalledWith('settings.updateTerminalQuickCommands', {
|
|
mutation: { type: 'upsert', command: edited }
|
|
})
|
|
expect(state?.commands).toEqual([edited, SECOND])
|
|
})
|
|
|
|
it('isolates an old client mutation from a replacement client', async () => {
|
|
const oldUpdate = deferred<RpcResponse>()
|
|
const oldClient = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(success([FIRST]))
|
|
.mockReturnValueOnce(oldUpdate.promise)
|
|
} as unknown as RpcClient
|
|
const newClient = {
|
|
sendRequest: vi.fn().mockResolvedValue(success([SECOND]))
|
|
} as unknown as RpcClient
|
|
|
|
function Harness({ client }: { client: RpcClient }): null {
|
|
state = useQuickCommands({ client, enabled: true })
|
|
return null
|
|
}
|
|
await act(async () => {
|
|
renderer = create(createElement(Harness, { client: oldClient }))
|
|
await Promise.resolve()
|
|
})
|
|
let persisted: Promise<boolean> = Promise.resolve(false)
|
|
await act(async () => {
|
|
persisted = state!.persist({ type: 'delete', id: FIRST.id })
|
|
await Promise.resolve()
|
|
renderer!.update(createElement(Harness, { client: newClient }))
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(newClient.sendRequest).toHaveBeenCalledWith('settings.getTerminalQuickCommands')
|
|
expect(state?.commands).toEqual([SECOND])
|
|
|
|
await act(async () => {
|
|
oldUpdate.resolve(success([]))
|
|
await persisted
|
|
})
|
|
expect(state?.commands).toEqual([SECOND])
|
|
})
|
|
|
|
it('rolls back the latest optimistic mutation when persistence fails', async () => {
|
|
const client = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(success([FIRST]))
|
|
.mockResolvedValueOnce(failure('save failed'))
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
await act(async () => {
|
|
await state!.persist({ type: 'delete', id: FIRST.id })
|
|
})
|
|
|
|
expect(state?.commands).toEqual([FIRST])
|
|
expect(state?.error).toBe('save failed')
|
|
})
|
|
|
|
it('rolls back when a successful update has a malformed authoritative list', async () => {
|
|
const client = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(success([FIRST]))
|
|
.mockResolvedValueOnce({ ok: true, result: {} } as RpcResponse)
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
let persisted = true
|
|
await act(async () => {
|
|
persisted = await state!.persist({ type: 'delete', id: FIRST.id })
|
|
})
|
|
|
|
expect(persisted).toBe(false)
|
|
expect(state?.commands).toEqual([FIRST])
|
|
expect(state?.error).toBe('Failed to save quick command')
|
|
})
|
|
|
|
it('waits for an in-flight save before reloading after reopen', async () => {
|
|
const update = deferred<RpcResponse>()
|
|
let loadCount = 0
|
|
const client = {
|
|
sendRequest: vi.fn((method: string) => {
|
|
if (method === 'settings.updateTerminalQuickCommands') {
|
|
return update.promise
|
|
}
|
|
loadCount += 1
|
|
return Promise.resolve(success(loadCount === 1 ? [FIRST] : []))
|
|
})
|
|
} as unknown as RpcClient
|
|
|
|
function Harness({ enabled }: { enabled: boolean }): null {
|
|
state = useQuickCommands({ client, enabled })
|
|
return null
|
|
}
|
|
await act(async () => {
|
|
renderer = create(createElement(Harness, { enabled: true }))
|
|
await Promise.resolve()
|
|
})
|
|
|
|
let persisted: Promise<boolean> = Promise.resolve(false)
|
|
await act(async () => {
|
|
persisted = state!.persist({ type: 'delete', id: FIRST.id })
|
|
await Promise.resolve()
|
|
})
|
|
await act(async () => {
|
|
renderer!.update(createElement(Harness, { enabled: false }))
|
|
await Promise.resolve()
|
|
})
|
|
await act(async () => {
|
|
renderer!.update(createElement(Harness, { enabled: true }))
|
|
await Promise.resolve()
|
|
})
|
|
expect(loadCount).toBe(1)
|
|
|
|
await act(async () => {
|
|
update.resolve(success([]))
|
|
await persisted
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
})
|
|
|
|
expect(loadCount).toBe(2)
|
|
expect(state?.commands).toEqual([])
|
|
expect(state?.ready).toBe(true)
|
|
})
|
|
|
|
it('rolls back to the confirmed list when consecutive queued mutations fail', async () => {
|
|
const firstUpdate = deferred<RpcResponse>()
|
|
const secondUpdate = deferred<RpcResponse>()
|
|
const client = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(success([FIRST, SECOND]))
|
|
.mockReturnValueOnce(firstUpdate.promise)
|
|
.mockReturnValueOnce(secondUpdate.promise)
|
|
} as unknown as RpcClient
|
|
await mount(client)
|
|
|
|
let firstPersist: Promise<boolean> = Promise.resolve(false)
|
|
let secondPersist: Promise<boolean> = Promise.resolve(false)
|
|
await act(async () => {
|
|
firstPersist = state!.persist({ type: 'delete', id: FIRST.id })
|
|
secondPersist = state!.persist({ type: 'delete', id: SECOND.id })
|
|
await Promise.resolve()
|
|
})
|
|
|
|
await act(async () => {
|
|
firstUpdate.resolve(failure('first failed'))
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
secondUpdate.resolve(failure('second failed'))
|
|
await Promise.all([firstPersist, secondPersist])
|
|
})
|
|
|
|
expect(state?.commands).toEqual([FIRST, SECOND])
|
|
expect(state?.error).toBe('second failed')
|
|
})
|
|
})
|