Files
orca/src/main/ipc/pet.test.ts
T
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#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.
2026-08-13 22:48:24 -07:00

176 lines
5.6 KiB
TypeScript

import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
appGetPathMock,
browserWindowFromWebContentsMock,
browserWindowGetFocusedWindowMock,
handleMock,
nativeImageCreateFromBufferMock,
showOpenDialogMock
} = vi.hoisted(() => ({
appGetPathMock: vi.fn(),
browserWindowFromWebContentsMock: vi.fn(),
browserWindowGetFocusedWindowMock: vi.fn(),
handleMock: vi.fn(),
nativeImageCreateFromBufferMock: vi.fn(),
showOpenDialogMock: vi.fn()
}))
vi.mock('electron', () => ({
app: {
getPath: appGetPathMock
},
BrowserWindow: {
fromWebContents: browserWindowFromWebContentsMock,
getFocusedWindow: browserWindowGetFocusedWindowMock
},
dialog: {
showOpenDialog: showOpenDialogMock
},
ipcMain: {
handle: handleMock
},
nativeImage: {
createFromBuffer: nativeImageCreateFromBufferMock
}
}))
import { registerPetHandlers } from './pet'
import type { CustomPet } from '../../shared/pet-types'
describe('registerPetHandlers', () => {
let tempDir: string
let userDataDir: string
const handlers = new Map<string, (event: unknown, ...args: unknown[]) => Promise<unknown>>()
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'orca-pet-test-'))
userDataDir = join(tempDir, 'user-data')
handlers.clear()
appGetPathMock.mockReset()
browserWindowFromWebContentsMock.mockReset()
browserWindowGetFocusedWindowMock.mockReset()
handleMock.mockReset()
nativeImageCreateFromBufferMock.mockReset()
showOpenDialogMock.mockReset()
appGetPathMock.mockReturnValue(userDataDir)
browserWindowFromWebContentsMock.mockReturnValue(null)
browserWindowGetFocusedWindowMock.mockReturnValue(null)
handleMock.mockImplementation((channel, handler) => {
handlers.set(channel, handler)
})
nativeImageCreateFromBufferMock.mockReturnValue({
isEmpty: () => true,
getSize: () => ({ width: 0, height: 0 })
})
})
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true })
})
function getHandler(channel: string): (event: unknown, ...args: unknown[]) => Promise<unknown> {
registerPetHandlers()
const handler = handlers.get(channel)
if (!handler) {
throw new Error(`${channel} handler not registered`)
}
return handler
}
it('imports a pet bundle whose manifest uses Windows separators', async () => {
const bundleDir = join(tempDir, 'windows-export.codex-pet')
const sheetBytes = Buffer.from('not decoded without frame metadata')
await mkdir(join(bundleDir, 'assets'), { recursive: true })
await writeFile(
join(bundleDir, 'pet.json'),
JSON.stringify({
id: 'windows-export',
displayName: 'Windows export',
spritesheetPath: String.raw`assets\spritesheet.png`
})
)
await writeFile(join(bundleDir, 'assets', 'spritesheet.png'), sheetBytes)
showOpenDialogMock.mockResolvedValue({ canceled: false, filePaths: [bundleDir] })
const result = (await getHandler('pet:importPetBundle')({ sender: {} })) as CustomPet
expect(result).toMatchObject({
label: 'Windows export',
fileName: 'spritesheet.png',
mimeType: 'image/png',
kind: 'bundle'
})
await expect(
readFile(join(userDataDir, 'sidekicks', 'custom', result.id, 'spritesheet.png'))
).resolves.toEqual(sheetBytes)
})
function webpVp8x(width: number, height: number): Buffer {
const u24 = (value: number): Buffer =>
Buffer.from([value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff])
const payload = Buffer.concat([Buffer.from([0, 0, 0, 0]), u24(width - 1), u24(height - 1)])
const size = Buffer.alloc(4)
size.writeUInt32LE(payload.byteLength, 0)
const riffSize = Buffer.alloc(4)
riffSize.writeUInt32LE(4 + 8 + payload.byteLength, 0)
return Buffer.concat([
Buffer.from('RIFF'),
riffSize,
Buffer.from('WEBP'),
Buffer.from('VP8X'),
size,
payload
])
}
async function writeSpriteBundle(
animations: Record<string, { row: number; frames: number; frameDurationsMs?: number[] }>
): Promise<string> {
const bundleDir = join(tempDir, 'durations.codex-pet')
await mkdir(bundleDir, { recursive: true })
await writeFile(
join(bundleDir, 'pet.json'),
JSON.stringify({
id: 'durations',
displayName: 'Durations',
spritesheetPath: 'sheet.webp',
frame: { width: 2, height: 2 },
animations
})
)
await writeFile(join(bundleDir, 'sheet.webp'), webpVp8x(4, 2))
return bundleDir
}
it('imports a bundle whose animations declare per-frame durations', async () => {
const bundleDir = await writeSpriteBundle({
idle: { row: 0, frames: 2, frameDurationsMs: [1680, 1920] }
})
showOpenDialogMock.mockResolvedValue({ canceled: false, filePaths: [bundleDir] })
const result = (await getHandler('pet:importPetBundle')({ sender: {} })) as CustomPet
expect(result.sprite?.animations?.idle).toEqual({
row: 0,
frames: 2,
frameDurationsMs: [1680, 1920]
})
})
it('rejects a bundle whose frame durations do not match the frame count', async () => {
const bundleDir = await writeSpriteBundle({
idle: { row: 0, frames: 2, frameDurationsMs: [1680] }
})
showOpenDialogMock.mockResolvedValue({ canceled: false, filePaths: [bundleDir] })
await expect(getHandler('pet:importPetBundle')({ sender: {} })).rejects.toThrow(
'declares 1 frame durations but 2 frames'
)
})
})