mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 08:02:54 +00:00
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day minimum-release-age supply-chain guard; nothing here needs it). The bump is a no-op on the existing config. Enable 3 error rules (backlog autofixed to zero in this commit) and 4 warn rules (surface signal without gating CI): error (autofixed, behavior-preserving): - unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:) - typescript/no-import-type-side-effects (~36: all-inline-type -> import type) - unicorn/no-array-reverse (19: copy-then-reverse -> toReversed) warn (real signal, current fires are test-only/correct): - unicorn/no-array-fill-with-reference-type (aliasing footgun guard) - typescript/no-unsafe-function-type (bans bare Function type) - unicorn/prefer-array-flat-map (map().flat() -> flatMap()) - unicorn/prefer-regexp-test (.match() in bool ctx -> .test()) mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix ran from root and covered mobile/ too. Verification (all green): oxlint 0 errors (root+mobile+aux configs), oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed, builds (electron-vite + web + cli) succeed. node: rewrites confirmed to skip embedded SSH/CLI string payloads (AST-only); all toReversed sites verified to operate on fresh copies or write-once locals. * chore(lint): bump mobile oxlint to 1.71 so inherited rules parse mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile && oxlint' failed to parse the new rule. Bump mobile to match root (1.71). Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit pass, vitest 978 passed / 0 failed. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
175 lines
4.9 KiB
TypeScript
175 lines
4.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import {
|
|
DaemonSpawner,
|
|
getDaemonPidPath,
|
|
getDaemonSocketPath,
|
|
getDaemonTokenPath
|
|
} from './daemon-spawner'
|
|
import { startDaemon, type DaemonHandle } from './daemon-main'
|
|
import { DaemonClient } from './client'
|
|
import type { SubprocessHandle } from './session'
|
|
import { PROTOCOL_VERSION } from './types'
|
|
|
|
function createTestDir(): string {
|
|
return mkdtempSync(join(tmpdir(), 'daemon-spawner-test-'))
|
|
}
|
|
|
|
function createMockSubprocess(): SubprocessHandle {
|
|
let onExitCb: ((code: number) => void) | null = null
|
|
return {
|
|
pid: 88888,
|
|
getForegroundProcess: vi.fn(() => null),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(() => setTimeout(() => onExitCb?.(0), 5)),
|
|
forceKill: vi.fn(),
|
|
signal: vi.fn(),
|
|
onData(_cb: (data: string) => void) {},
|
|
onExit(cb: (code: number) => void) {
|
|
onExitCb = cb
|
|
},
|
|
dispose: vi.fn()
|
|
}
|
|
}
|
|
|
|
describe('DaemonSpawner', () => {
|
|
let dir: string
|
|
let spawner: DaemonSpawner
|
|
let activeDaemons: DaemonHandle[]
|
|
|
|
beforeEach(() => {
|
|
dir = createTestDir()
|
|
activeDaemons = []
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await spawner?.shutdown()
|
|
for (const d of activeDaemons) {
|
|
await d.shutdown().catch(() => {})
|
|
}
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
function createSpawner(): DaemonSpawner {
|
|
spawner = new DaemonSpawner({
|
|
runtimeDir: dir,
|
|
launcher: async (socketPath, tokenPath) => {
|
|
const handle = await startDaemon({
|
|
socketPath,
|
|
tokenPath,
|
|
spawnSubprocess: () => createMockSubprocess()
|
|
})
|
|
activeDaemons.push(handle)
|
|
return { shutdown: () => handle.shutdown() }
|
|
}
|
|
})
|
|
return spawner
|
|
}
|
|
|
|
describe('ensureRunning', () => {
|
|
it('uses protocol-scoped socket and token paths', () => {
|
|
const socketPath = getDaemonSocketPath(dir)
|
|
const tokenPath = getDaemonTokenPath(dir)
|
|
const pidPath = getDaemonPidPath(dir)
|
|
|
|
if (process.platform === 'win32') {
|
|
expect(socketPath).toContain(`orca-terminal-host-v${PROTOCOL_VERSION}`)
|
|
} else {
|
|
expect(socketPath).toBe(join(dir, `daemon-v${PROTOCOL_VERSION}.sock`))
|
|
}
|
|
expect(tokenPath).toBe(join(dir, `daemon-v${PROTOCOL_VERSION}.token`))
|
|
expect(pidPath).toBe(join(dir, `daemon-v${PROTOCOL_VERSION}.pid`))
|
|
})
|
|
|
|
it('starts daemon and returns connection info', async () => {
|
|
const s = createSpawner()
|
|
const info = await s.ensureRunning()
|
|
|
|
if (process.platform === 'win32') {
|
|
expect(info.socketPath).toContain(`orca-terminal-host-v${PROTOCOL_VERSION}`)
|
|
} else {
|
|
expect(info.socketPath).toContain(dir)
|
|
}
|
|
expect(info.tokenPath).toContain(dir)
|
|
})
|
|
|
|
it('returns same info on subsequent calls', async () => {
|
|
const s = createSpawner()
|
|
const info1 = await s.ensureRunning()
|
|
const info2 = await s.ensureRunning()
|
|
|
|
expect(info1.socketPath).toBe(info2.socketPath)
|
|
expect(info1.tokenPath).toBe(info2.tokenPath)
|
|
})
|
|
|
|
it('daemon is connectable after ensureRunning', async () => {
|
|
const s = createSpawner()
|
|
const info = await s.ensureRunning()
|
|
|
|
const client = new DaemonClient({
|
|
socketPath: info.socketPath,
|
|
tokenPath: info.tokenPath
|
|
})
|
|
await client.ensureConnected()
|
|
expect(client.isConnected()).toBe(true)
|
|
client.disconnect()
|
|
})
|
|
|
|
it('daemon can create sessions', async () => {
|
|
const s = createSpawner()
|
|
const info = await s.ensureRunning()
|
|
|
|
const client = new DaemonClient({
|
|
socketPath: info.socketPath,
|
|
tokenPath: info.tokenPath
|
|
})
|
|
await client.ensureConnected()
|
|
|
|
const result = await client.request<{ isNew: boolean }>('createOrAttach', {
|
|
sessionId: 'test-session',
|
|
cols: 80,
|
|
rows: 24
|
|
})
|
|
expect(result.isNew).toBe(true)
|
|
client.disconnect()
|
|
})
|
|
})
|
|
|
|
describe('shutdown', () => {
|
|
it('stops the daemon', async () => {
|
|
const s = createSpawner()
|
|
const info = await s.ensureRunning()
|
|
await s.shutdown()
|
|
|
|
const client = new DaemonClient({
|
|
socketPath: info.socketPath,
|
|
tokenPath: info.tokenPath
|
|
})
|
|
await expect(client.ensureConnected()).rejects.toThrow()
|
|
})
|
|
|
|
it('can be called when daemon is not running', async () => {
|
|
const s = createSpawner()
|
|
await expect(s.shutdown()).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('allows re-start after shutdown', async () => {
|
|
const s = createSpawner()
|
|
await s.ensureRunning()
|
|
await s.shutdown()
|
|
|
|
const info = await s.ensureRunning()
|
|
const client = new DaemonClient({
|
|
socketPath: info.socketPath,
|
|
tokenPath: info.tokenPath
|
|
})
|
|
await client.ensureConnected()
|
|
expect(client.isConnected()).toBe(true)
|
|
client.disconnect()
|
|
})
|
|
})
|
|
})
|