mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +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.
215 lines
6.2 KiB
TypeScript
215 lines
6.2 KiB
TypeScript
import { createElement, type ReactNode } from 'react'
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { MAX_QUICK_COMMANDS } from '../terminal/quick-commands'
|
|
import { QuickCommandsSheet } from './QuickCommandsSheet'
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
alert: vi.fn(),
|
|
commands: [] as TerminalQuickCommand[],
|
|
persist: vi.fn()
|
|
}))
|
|
const quickCommandEditorForm = 'QuickCommandEditorForm'
|
|
const quickCommandsList = 'QuickCommandsList'
|
|
const command: TerminalQuickCommand = {
|
|
id: 'command',
|
|
label: 'Test',
|
|
action: 'terminal-command',
|
|
command: 'pnpm test',
|
|
appendEnter: true,
|
|
scope: { type: 'global' }
|
|
}
|
|
|
|
vi.mock('react-native', () => ({
|
|
Alert: { alert: mocks.alert },
|
|
Pressable: 'Pressable',
|
|
StyleSheet: { create: <T>(styles: T) => styles },
|
|
Text: 'Text',
|
|
View: 'View'
|
|
}))
|
|
|
|
vi.mock('lucide-react-native', () => ({ ChevronLeft: 'ChevronLeft' }))
|
|
|
|
vi.mock('../components/BottomDrawer', () => ({
|
|
BottomDrawer: ({ children }: { children: ReactNode }) => children
|
|
}))
|
|
|
|
vi.mock('./QuickCommandEditorForm', () => ({
|
|
QuickCommandEditorForm: 'QuickCommandEditorForm'
|
|
}))
|
|
|
|
vi.mock('./QuickCommandsList', () => ({
|
|
QuickCommandAgentPicker: 'QuickCommandAgentPicker',
|
|
QuickCommandsList: 'QuickCommandsList'
|
|
}))
|
|
|
|
vi.mock('./use-quick-commands', () => ({
|
|
useQuickCommands: () => ({
|
|
commands: mocks.commands,
|
|
loading: false,
|
|
ready: true,
|
|
error: null,
|
|
persist: mocks.persist
|
|
})
|
|
}))
|
|
|
|
function deferred<T>() {
|
|
let resolve: (value: T) => void = () => {}
|
|
const promise = new Promise<T>((done) => {
|
|
resolve = done
|
|
})
|
|
return { promise, resolve }
|
|
}
|
|
|
|
describe('QuickCommandsSheet', () => {
|
|
let renderer: ReactTestRenderer | null = null
|
|
beforeEach(() => {
|
|
mocks.alert.mockReset()
|
|
mocks.commands = []
|
|
mocks.persist.mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
act(() => renderer?.unmount())
|
|
renderer = null
|
|
})
|
|
|
|
it('keeps the sheet open when a launch is rejected', async () => {
|
|
const onClose = vi.fn()
|
|
const onLaunch = vi.fn(() => false)
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsSheet, {
|
|
visible: true,
|
|
onClose,
|
|
client: {} as RpcClient,
|
|
repoId: 'repo-1',
|
|
repoName: 'Repo',
|
|
onLaunch
|
|
})
|
|
)
|
|
})
|
|
|
|
act(() => renderer!.root.findByType(quickCommandsList).props.onLaunch(command))
|
|
|
|
expect(onLaunch).toHaveBeenCalledWith(command)
|
|
expect(onClose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('submits one full-list mutation for a same-frame double tap', async () => {
|
|
const save = deferred<boolean>()
|
|
mocks.persist.mockReturnValue(save.promise)
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsSheet, {
|
|
visible: true,
|
|
onClose: vi.fn(),
|
|
client: {} as RpcClient,
|
|
repoId: 'repo-1',
|
|
repoName: 'Repo',
|
|
onLaunch: () => true
|
|
})
|
|
)
|
|
})
|
|
|
|
act(() => renderer!.root.findByType(quickCommandsList).props.onAdd())
|
|
const editor = renderer!.root.findByType(quickCommandEditorForm)
|
|
act(() => {
|
|
editor.props.onChange({ label: 'Test' })
|
|
editor.props.onChange({ command: 'pnpm test' })
|
|
})
|
|
const readyEditor = renderer!.root.findByType(quickCommandEditorForm)
|
|
act(() => {
|
|
readyEditor.props.onSave()
|
|
readyEditor.props.onSave()
|
|
})
|
|
|
|
expect(mocks.persist).toHaveBeenCalledTimes(1)
|
|
await act(async () => {
|
|
save.resolve(true)
|
|
await save.promise
|
|
})
|
|
})
|
|
|
|
it('keeps creation closed when the host command limit is reached', async () => {
|
|
mocks.commands = Array.from({ length: MAX_QUICK_COMMANDS }, (_, index) => ({
|
|
...command,
|
|
id: `command-${index}`
|
|
}))
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsSheet, {
|
|
visible: true,
|
|
onClose: vi.fn(),
|
|
client: {} as RpcClient,
|
|
repoId: 'repo-1',
|
|
repoName: 'Repo',
|
|
onLaunch: () => true
|
|
})
|
|
)
|
|
})
|
|
|
|
const list = renderer!.root.findByType(quickCommandsList)
|
|
expect(list.props.canAdd).toBe(false)
|
|
act(() => list.props.onAdd())
|
|
expect(renderer!.root.findAllByType(quickCommandEditorForm)).toHaveLength(0)
|
|
})
|
|
|
|
it('shows only global commands and defaults new commands to global without a repo', async () => {
|
|
const repoCommand: TerminalQuickCommand = {
|
|
...command,
|
|
id: 'repo-command',
|
|
scope: { type: 'repo', repoId: 'repo-1' }
|
|
}
|
|
mocks.commands = [command, repoCommand]
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsSheet, {
|
|
visible: true,
|
|
onClose: vi.fn(),
|
|
client: {} as RpcClient,
|
|
repoId: null,
|
|
repoName: 'Folder workspace',
|
|
onLaunch: () => true
|
|
})
|
|
)
|
|
})
|
|
|
|
const list = renderer!.root.findByType(quickCommandsList)
|
|
expect(list.props.globalCommands).toEqual([command])
|
|
expect(list.props.repoCommands).toEqual([])
|
|
act(() => list.props.onAdd())
|
|
expect(renderer!.root.findByType(quickCommandEditorForm).props.draft.scope).toEqual({
|
|
type: 'global'
|
|
})
|
|
})
|
|
|
|
it('does not delete a shared command until the destructive action is confirmed', async () => {
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsSheet, {
|
|
visible: true,
|
|
onClose: vi.fn(),
|
|
client: {} as RpcClient,
|
|
repoId: 'repo-1',
|
|
repoName: 'Repo',
|
|
onLaunch: () => true
|
|
})
|
|
)
|
|
})
|
|
|
|
act(() => renderer!.root.findByType(quickCommandsList).props.onDelete(command))
|
|
expect(mocks.persist).not.toHaveBeenCalled()
|
|
|
|
const actions = mocks.alert.mock.calls[0]?.[2] as
|
|
| Array<{ style?: string; onPress?: () => void }>
|
|
| undefined
|
|
act(() => actions?.find((action) => action.style === 'destructive')?.onPress?.())
|
|
|
|
expect(mocks.persist).toHaveBeenCalledTimes(1)
|
|
expect(mocks.persist).toHaveBeenCalledWith({ type: 'delete', id: command.id })
|
|
})
|
|
})
|