Files
aiturk-hermes-ide/apps/desktop/electron/aiturk-login.test.ts
T
yilsemandClaude Opus 5.5 58dbb4523b AITURK IDE 1.0.0-beta.17: telefon kodu yayini yeniden denenir
Uzun sure acik kalan IDE'de "Telefon ekle" ilk istegi olu bir keep-alive
baglantisinda kaybedebiliyordu; bulusma kaydi yayinlanamayinca kod yolu bes
dakika boyunca "Kodla baglanma su an kullanilamiyor" kaliyordu (QR calisiyordu).

- publishPairOffer: en cok 4 deneme (0,5 / 1,5 / 3 sn); ag hatasi, 5xx ve 429
  yeniden denenir, 422 kalicidir; yaniti kaybolan denemeden sonraki 409 kaydin
  yerinde oldugu anlamina gelir (iptal jetonu yoktur, kayit kendiliginden biter).
- Pencerede "Yeniden dene": ayni kod ve QR ile kaydi yeniden yayinlar.
- Role oturumu kaydi ve "Giris yap" baslangici icin tek ek deneme (zaman asimi
  yeniden denenmez).
- Gunluge yalniz "pair offer published / failed after N tries" duser; kod,
  lookup ve jeton yazilmaz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
2026-10-05 14:27:59 +03:00

657 lines
24 KiB
TypeScript

/**
* Tests for electron/aiturk-login.ts — the one-click TurkServis sign-in.
* Everything is injected (fake fetch, fake backend, fake CLI, fake clock), so
* no network, browser or real CLI is touched. The fixture "keys" below are
* obviously fake placeholders in the right shape.
*/
import { EventEmitter } from 'node:events'
import { describe, expect, it, vi } from 'vitest'
import {
AITURK_CLI_KEY_HOLD_MS,
AITURK_LOGIN_START_RETRY_DELAY_MS,
type AiturkCliExit,
AiturkLoginError,
createAiturkCliRunner,
createAiturkLogin,
resolveAiturkCliPath
} from './aiturk-login'
const DEVICE_CODE = `tsdl_${'d'.repeat(43)}`
const IDE_KEY = 'tsk_FAKE-ide-key-for-tests'
const CLI_KEY = 'tsk_FAKE-cli-key-for-tests'
const URI = 'https://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e'
const startBody = (over: Record<string, unknown> = {}) => ({
loginId: '0f8fad5b-d9cb-469f-a165-70867728950e',
deviceCode: DEVICE_CODE,
userCode: 'ABCD2345',
expiresIn: 600,
interval: 3,
verificationUri: URI,
...over
})
const approved = { state: 'approved', key: IDE_KEY, cliKey: CLI_KEY, account: 'acct_test' }
function harness(
options: {
start?: { status: number; body?: unknown }
/** Thrown by successive `device-auth/start` calls before any answer is given. */
startErrors?: Error[]
polls?: ({ status: number; body?: unknown } | Error)[]
cliExits?: AiturkCliExit[]
validate?: unknown
save?: unknown
openExternal?: () => boolean
} = {}
) {
const calls: { url: string; init: any }[] = []
const polls = [...(options.polls ?? [])]
const startErrors = [...(options.startErrors ?? [])]
const cliExits = [...(options.cliExits ?? [0])]
const clock = { now: 1_000_000 }
const timers: { fn: () => void; ms: number; cleared: boolean }[] = []
const fetch = vi.fn(async (url: string, init: any) => {
calls.push({ url, init })
if (url.endsWith('/start')) {
const failure = startErrors.shift()
if (failure) {
throw failure
}
const r = options.start ?? { status: 201, body: startBody() }
return { status: r.status, json: async () => r.body }
}
if (url.endsWith('/poll')) {
const r = polls.shift() ?? { status: 200, body: { state: 'pending' } }
if (r instanceof Error) {
throw r
}
return { status: r.status, json: async () => r.body }
}
return { status: 200, json: async () => ({}) }
})
const backendRequest = vi.fn(async (request: any) => {
if (request.path.endsWith('/validate')) {
return options.validate ?? { ok: true, reachable: true, models: ['account/model-a', 'account/model-b'], message: '' }
}
return options.save ?? { ok: true, id: 'turkservis', endpoints: [] }
})
const runCli = vi.fn(async (_key: string, _options: { keepOtherAccount: boolean }) =>
cliExits.length ? (cliExits.shift() as AiturkCliExit) : 0
)
const openExternal = vi.fn(options.openExternal ?? (() => true))
const login = createAiturkLogin({
fetch,
openExternal,
hostname: () => 'TEST-PC',
backendRequest,
runCli,
now: () => clock.now,
setTimer: (fn, ms) => {
const timer = { fn, ms, cleared: false }
timers.push(timer)
return timer
},
clearTimer: handle => {
;(handle as { cleared: boolean }).cleared = true
},
timeoutSignal: () => undefined
})
return { login, fetch, calls, backendRequest, runCli, openExternal, clock, timers }
}
const leaks = (value: unknown) => /tsk_|tsdl_/.test(typeof value === 'string' ? value : JSON.stringify(value) ?? '')
describe('aiturk login: start', () => {
it('asks the server for a code, opens the approval page and returns no secret', async () => {
const h = harness()
const started = await h.login.start({ profile: 'work' })
expect(started).toEqual({ userCode: 'ABCD2345', verificationUri: URI, expiresAt: 1_000_000 + 600_000 })
expect(leaks(started)).toBe(false)
expect(h.openExternal).toHaveBeenCalledExactlyOnceWith(URI)
const { url, init } = h.calls[0]
expect(url).toBe('https://turkservis.online/v1/aiturk/device-auth/start')
expect(init.method).toBe('POST')
expect(init.redirect).toBe('error')
expect(JSON.parse(init.body)).toEqual({ client: 'ide', device_name: 'TEST-PC', also_cli: true })
expect(init.headers.Authorization).toBeUndefined()
})
it.each([
'https://turkservis.online.evil.example/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e',
'http://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e',
'https://turkservis.online/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e?next=//evil',
'https://turkservis.online/account/aiturk/device-login/../../x',
'https://evil.example/account/aiturk/device-login/0f8fad5b-d9cb-469f-a165-70867728950e',
''
])('refuses a verification URL that is not exactly the approval page: %s', async verificationUri => {
const h = harness({ start: { status: 201, body: startBody({ verificationUri }) } })
const error = await h.login.start().catch(e => e)
expect(error).toBeInstanceOf(AiturkLoginError)
expect(error.message).toBe('aiturk-login:invalid-response')
expect(leaks(error.message)).toBe(false)
expect(h.openExternal).not.toHaveBeenCalled()
// The issued code is revoked instead of being left approvable.
expect(h.calls.at(-1)?.url).toBe('https://turkservis.online/v1/aiturk/device-auth/cancel')
expect(await h.login.poll()).toEqual({ state: 'expired' })
})
it('maps 429 to rate-limited and other failures to unavailable', async () => {
await expect(harness({ start: { status: 429 } }).login.start()).rejects.toThrow('aiturk-login:rate-limited')
await expect(harness({ start: { status: 422 } }).login.start()).rejects.toThrow('aiturk-login:unavailable')
await expect(harness({ start: { status: 500 } }).login.start()).rejects.toThrow('aiturk-login:unavailable')
})
it('tries start once more after a thrown network error (first request of a long-idle process)', async () => {
const h = harness({ startErrors: [new TypeError('fetch failed')] })
const pending = h.login.start()
await vi.waitFor(() => expect(h.timers.some(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)).toBe(true))
expect(AITURK_LOGIN_START_RETRY_DELAY_MS).toBe(500)
expect(h.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(1)
expect(h.openExternal).not.toHaveBeenCalled()
h.timers.find(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)!.fn()
expect(await pending).toEqual({ userCode: 'ABCD2345', verificationUri: URI, expiresAt: 1_000_000 + 600_000 })
expect(h.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(2)
// One code only: the approval page is opened once.
expect(h.openExternal).toHaveBeenCalledExactlyOnceWith(URI)
})
it('gives up after the second network error, and never retries an HTTP answer or a timeout', async () => {
const twice = harness({ startErrors: [new TypeError('fetch failed'), new TypeError('fetch failed')] })
const failing = twice.login.start().catch(e => e)
await vi.waitFor(() => expect(twice.timers.some(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)).toBe(true))
twice.timers.find(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)!.fn()
expect((await failing).message).toBe('aiturk-login:unavailable')
expect(twice.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(2)
expect(twice.timers.filter(timer => timer.ms === AITURK_LOGIN_START_RETRY_DELAY_MS)).toHaveLength(1)
for (const status of [500, 503, 429, 422]) {
const answered = harness({ start: { status } })
await expect(answered.login.start()).rejects.toThrow(/^aiturk-login:(?:unavailable|rate-limited)$/)
expect(answered.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(1)
expect(answered.timers).toHaveLength(0)
}
const timedOut = harness({ startErrors: [new DOMException('The operation was aborted due to timeout', 'TimeoutError')] })
await expect(timedOut.login.start()).rejects.toThrow('aiturk-login:unavailable')
expect(timedOut.calls.filter(call => call.url.endsWith('/start'))).toHaveLength(1)
expect(timedOut.timers).toHaveLength(0)
})
it('rejects malformed codes', async () => {
await expect(
harness({ start: { status: 201, body: startBody({ userCode: 'abcd2345' }) } }).login.start()
).rejects.toThrow('aiturk-login:invalid-response')
await expect(
harness({ start: { status: 201, body: startBody({ deviceCode: 'nope' }) } }).login.start()
).rejects.toThrow('aiturk-login:invalid-response')
})
it('keeps a single login: a second start revokes the first code', async () => {
const h = harness()
await h.login.start()
await h.login.start()
const urls = h.calls.map(c => c.url.split('/').pop())
expect(urls).toEqual(['start', 'cancel', 'start'])
expect(h.calls[1].init.headers.Authorization).toBe(`Bearer ${DEVICE_CODE}`)
})
it('cancels the code when the browser cannot be opened', async () => {
const h = harness({ openExternal: () => false })
await expect(h.login.start()).rejects.toThrow('aiturk-login:browser-failed')
expect(h.calls.at(-1)?.url).toMatch(/\/cancel$/)
})
})
describe('aiturk login: poll', () => {
it('reports pending while the user has not approved', async () => {
const h = harness({ polls: [{ status: 200, body: { state: 'pending' } }] })
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'pending' })
expect(h.calls[1].url).toBe('https://turkservis.online/v1/aiturk/device-auth/poll')
expect(h.calls[1].init.headers.Authorization).toBe(`Bearer ${DEVICE_CODE}`)
expect(h.calls[1].init.redirect).toBe('error')
expect(h.backendRequest).not.toHaveBeenCalled()
expect(h.runCli).not.toHaveBeenCalled()
})
it('keeps waiting through a transient network error', async () => {
const h = harness({ polls: [new Error(`boom ${DEVICE_CODE}`), { status: 503 }] })
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'pending' })
expect(await h.login.poll()).toEqual({ state: 'pending' })
})
it('reports expired on 401 and stops asking the server', async () => {
const h = harness({ polls: [{ status: 401, body: { detail: 'expired' } }] })
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'expired' })
const count = h.fetch.mock.calls.length
expect(await h.login.poll()).toEqual({ state: 'expired' })
expect(h.fetch.mock.calls.length).toBe(count)
})
it('expires by the clock without a request', async () => {
const h = harness()
await h.login.start()
h.clock.now += 600_000
expect(await h.login.poll()).toEqual({ state: 'expired' })
expect(h.calls.filter(c => c.url.endsWith('/poll'))).toHaveLength(0)
})
it('on approval saves the provider and pipes ONLY the CLI key to the CLI', async () => {
const h = harness({ polls: [{ status: 200, body: approved }] })
await h.login.start({ profile: 'work', connectionId: 'conn-1' })
const result = await h.login.poll()
expect(result).toEqual({ state: 'connected', cli: 'ok' })
expect(leaks(result)).toBe(false)
expect(h.backendRequest).toHaveBeenCalledTimes(2)
expect(h.backendRequest.mock.calls[0][0]).toEqual({
connectionId: 'conn-1',
path: '/api/providers/custom-endpoints/validate',
method: 'POST',
body: {
id: 'turkservis',
name: 'TurkServis · AITURK',
base_url: 'https://ai.turkservis.online/v1',
api_key: IDE_KEY,
model: '',
discover_models: true,
make_default: true
}
})
expect(h.backendRequest.mock.calls[1][0]).toEqual({
profile: 'work',
connectionId: 'conn-1',
path: '/api/providers/custom-endpoints',
method: 'POST',
body: {
id: 'turkservis',
name: 'TurkServis · AITURK',
base_url: 'https://ai.turkservis.online/v1',
api_key: IDE_KEY,
model: 'account/model-a',
models: ['account/model-a', 'account/model-b'],
discover_models: true,
make_default: true
}
})
expect(h.runCli).toHaveBeenCalledExactlyOnceWith(CLI_KEY, { keepOtherAccount: true })
// No key ever rides a URL.
expect(h.calls.some(c => leaks(c.url))).toBe(false)
})
it('does not ask for the one-time keys again on later polls', async () => {
const h = harness({ polls: [{ status: 200, body: approved }] })
await h.login.start()
const first = await h.login.poll()
const fetches = h.fetch.mock.calls.length
const second = await h.login.poll()
expect(second).toEqual(first)
expect(h.fetch.mock.calls.length).toBe(fetches)
expect(h.backendRequest).toHaveBeenCalledTimes(2)
expect(h.runCli).toHaveBeenCalledTimes(1)
})
it('shares one completion between overlapping polls', async () => {
const h = harness({ polls: [{ status: 200, body: approved }] })
await h.login.start()
const [a, b] = await Promise.all([h.login.poll(), h.login.poll()])
expect(a).toEqual({ state: 'connected', cli: 'ok' })
expect(b.state === 'connected' || b.state === 'pending').toBe(true)
expect(h.runCli).toHaveBeenCalledTimes(1)
})
it.each([
[0, 'ok'],
[2, 'outdated'],
[3, 'other-account'],
[4, 'unknown-account'],
[1, 'failed'],
[null, 'failed'],
['missing', 'missing']
] as const)('maps CLI exit %s to cli=%s', async (exit, cli) => {
const h = harness({ polls: [{ status: 200, body: approved }], cliExits: [exit] })
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'connected', cli })
})
it('treats a throwing CLI runner as failed without leaking', async () => {
const h = harness({ polls: [{ status: 200, body: approved }] })
h.runCli.mockRejectedValueOnce(new Error(`spawn failed ${CLI_KEY}`))
await h.login.start()
const result = await h.login.poll()
expect(result).toEqual({ state: 'connected', cli: 'failed' })
})
it('does not save or touch the CLI when the key is rejected by the backend', async () => {
const h = harness({
polls: [{ status: 200, body: approved }],
validate: { ok: false, reachable: true, models: [], message: `bad key ${IDE_KEY}` }
})
await h.login.start()
const result = await h.login.poll()
expect(result).toEqual({ state: 'error', error: 'provider-failed' })
expect(leaks(result)).toBe(false)
expect(h.backendRequest).toHaveBeenCalledTimes(1)
expect(h.runCli).not.toHaveBeenCalled()
})
it('does not invent a model when the catalog is empty', async () => {
const h = harness({
polls: [{ status: 200, body: approved }],
validate: { ok: true, reachable: true, models: [], message: '' }
})
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'error', error: 'no-models' })
expect(h.backendRequest).toHaveBeenCalledTimes(1)
})
it('never surfaces backend error text (which could echo the key)', async () => {
const h = harness({ polls: [{ status: 200, body: approved }] })
h.backendRequest.mockRejectedValueOnce(new Error(`500: echoed ${IDE_KEY}`))
await h.login.start()
const result = await h.login.poll()
expect(result).toEqual({ state: 'error', error: 'provider-failed' })
expect(leaks(result)).toBe(false)
})
it('rejects an approval whose key is malformed', async () => {
const h = harness({ polls: [{ status: 200, body: { state: 'approved', key: 'not a key', cliKey: CLI_KEY } }] })
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'error', error: 'invalid-response' })
expect(h.backendRequest).not.toHaveBeenCalled()
expect(h.runCli).not.toHaveBeenCalled()
})
})
describe('aiturk login: other CLI account', () => {
async function otherAccount(overwriteExit: AiturkCliExit = 0) {
const h = harness({ polls: [{ status: 200, body: approved }], cliExits: [3, overwriteExit] })
await h.login.start()
expect(await h.login.poll()).toEqual({ state: 'connected', cli: 'other-account' })
return h
}
it('overwrites only after the user confirms, without the keep flag', async () => {
const h = await otherAccount()
const result = await h.login.overwriteCli()
expect(result).toEqual({ cli: 'ok' })
expect(leaks(result)).toBe(false)
expect(h.runCli).toHaveBeenLastCalledWith(CLI_KEY, { keepOtherAccount: false })
expect(await h.login.poll()).toEqual({ state: 'connected', cli: 'ok' })
// Single use: the held key is gone.
await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite')
expect(h.runCli).toHaveBeenCalledTimes(2)
})
it('reports an overwrite that still fails', async () => {
const h = await otherAccount(4)
expect(await h.login.overwriteCli()).toEqual({ cli: 'unknown-account' })
})
it('drops the held CLI key after five minutes', async () => {
const h = await otherAccount()
const hold = h.timers.find(t => t.ms === AITURK_CLI_KEY_HOLD_MS)
expect(hold).toBeDefined()
h.clock.now += AITURK_CLI_KEY_HOLD_MS
hold?.fn()
await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite')
expect(h.runCli).toHaveBeenCalledTimes(1)
})
it('refuses a stale held key even if the timer never fired', async () => {
const h = await otherAccount()
h.clock.now += AITURK_CLI_KEY_HOLD_MS
await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite')
expect(h.runCli).toHaveBeenCalledTimes(1)
})
it('drops the held CLI key on cancel', async () => {
const h = await otherAccount()
await h.login.cancel()
const error = await h.login.overwriteCli().catch(e => e)
expect(error.message).toBe('aiturk-login:nothing-to-overwrite')
expect(leaks(error.message)).toBe(false)
expect(h.runCli).toHaveBeenCalledTimes(1)
})
it('has nothing to overwrite when the CLI simply succeeded', async () => {
const h = harness({ polls: [{ status: 200, body: approved }] })
await h.login.start()
await h.login.poll()
await expect(h.login.overwriteCli()).rejects.toThrow('aiturk-login:nothing-to-overwrite')
})
})
describe('aiturk login: cancel', () => {
it('revokes the code on the server and clears the session', async () => {
const h = harness()
await h.login.start()
expect(await h.login.cancel()).toEqual({ ok: true })
const last = h.calls.at(-1)
expect(last?.url).toBe('https://turkservis.online/v1/aiturk/device-auth/cancel')
expect(last?.init.headers.Authorization).toBe(`Bearer ${DEVICE_CODE}`)
const count = h.fetch.mock.calls.length
expect(await h.login.poll()).toEqual({ state: 'expired' })
expect(h.fetch.mock.calls.length).toBe(count)
})
it('is a no-op without a login and survives a failing server', async () => {
const h = harness()
expect(await h.login.cancel()).toEqual({ ok: true })
expect(h.fetch).not.toHaveBeenCalled()
await h.login.start()
h.fetch.mockRejectedValueOnce(new Error('offline'))
expect(await h.login.cancel()).toEqual({ ok: true })
})
})
describe('aiturk CLI runner', () => {
const CLI_PATH = 'C:\\Users\\Test User\\AppData\\Local\\TurkServis\\AITURK\\program\\bin\\aiturk.cmd'
const env = { LOCALAPPDATA: 'C:\\Users\\Test User\\AppData\\Local', SystemRoot: 'C:\\Windows' }
function fakeSpawn(behaviour: (child: any) => void = child => child.emit('close', 0)) {
const state: any = { calls: [], stdin: [] as string[], killed: false }
const spawn = vi.fn((command: string, args: string[], options: any) => {
const child: any = new EventEmitter()
child.stdin = new EventEmitter()
child.stdin.end = (chunk: string) => {
state.stdin.push(chunk)
queueMicrotask(() => behaviour(child))
}
child.kill = () => {
state.killed = true
}
state.calls.push({ command, args, options })
return child
})
return { spawn, state }
}
it('finds the CLI only at the fixed install path', () => {
expect(resolveAiturkCliPath({ env, existsSync: p => p === CLI_PATH, platform: 'win32' })).toBe(CLI_PATH)
expect(resolveAiturkCliPath({ env, existsSync: () => false, platform: 'win32' })).toBeNull()
expect(resolveAiturkCliPath({ env: {}, existsSync: () => true, platform: 'win32' })).toBeNull()
expect(resolveAiturkCliPath({ env: { LOCALAPPDATA: 'relative' }, existsSync: () => true, platform: 'win32' })).toBeNull()
expect(resolveAiturkCliPath({ env, existsSync: () => true, platform: 'linux' })).toBeNull()
})
it('reports missing without spawning anything', async () => {
const { spawn } = fakeSpawn()
const run = createAiturkCliRunner({ spawn, existsSync: () => false, env, platform: 'win32' })
expect(await run(CLI_KEY, { keepOtherAccount: true })).toBe('missing')
expect(spawn).not.toHaveBeenCalled()
})
it('sends the key on stdin only, through an absolute cmd.exe, without a shell', async () => {
const { spawn, state } = fakeSpawn()
const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32' })
expect(await run(CLI_KEY, { keepOtherAccount: true })).toBe(0)
const { command, args, options } = state.calls[0]
expect(command).toBe('C:\\Windows\\System32\\cmd.exe')
expect(args).toEqual(['/d', '/s', '/c', `""${CLI_PATH}" login --key-stdin --keep-other-account"`])
expect(options).toEqual({
shell: false,
windowsHide: true,
windowsVerbatimArguments: true,
stdio: ['pipe', 'ignore', 'ignore']
})
expect(leaks([command, args, options])).toBe(false)
expect(state.stdin).toEqual([`${CLI_KEY}\n`])
})
it('drops the keep flag for a confirmed overwrite', async () => {
const { spawn, state } = fakeSpawn()
const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32' })
await run(CLI_KEY, { keepOtherAccount: false })
expect(state.calls[0].args[3]).toBe(`""${CLI_PATH}" login --key-stdin"`)
})
it.each([0, 1, 2, 3, 4])('returns exit code %s', async code => {
const { spawn } = fakeSpawn(child => child.emit('close', code))
const run = createAiturkCliRunner({ spawn, existsSync: () => true, env, platform: 'win32' })
expect(await run(CLI_KEY, { keepOtherAccount: true })).toBe(code)
})
it('returns null on spawn error, thrown spawn and stdin EPIPE', async () => {
const errored = fakeSpawn(child => child.emit('error', new Error('ENOENT')))
expect(
await createAiturkCliRunner({ spawn: errored.spawn, existsSync: () => true, env, platform: 'win32' })(CLI_KEY, {
keepOtherAccount: true
})
).toBeNull()
const thrower = vi.fn(() => {
throw new Error('spawn EINVAL')
})
expect(
await createAiturkCliRunner({ spawn: thrower, existsSync: () => true, env, platform: 'win32' })(CLI_KEY, {
keepOtherAccount: true
})
).toBeNull()
const epipe = fakeSpawn(child => {
child.stdin.emit('error', new Error('EPIPE'))
child.emit('close', 2)
})
expect(
await createAiturkCliRunner({ spawn: epipe.spawn, existsSync: () => true, env, platform: 'win32' })(CLI_KEY, {
keepOtherAccount: true
})
).toBe(2)
})
it('kills a CLI that hangs past the timeout', async () => {
const { spawn, state } = fakeSpawn(() => {})
let fire = () => {}
const run = createAiturkCliRunner({
spawn,
existsSync: () => true,
env,
platform: 'win32',
setTimer: (fn, ms) => {
expect(ms).toBe(60_000)
fire = fn
return 1
},
clearTimer: () => {}
})
const pending = run(CLI_KEY, { keepOtherAccount: true })
fire()
expect(await pending).toBeNull()
expect(state.killed).toBe(true)
})
it('refuses an install path cmd.exe could reinterpret', async () => {
const { spawn } = fakeSpawn()
const run = createAiturkCliRunner({
spawn,
existsSync: () => true,
env: { LOCALAPPDATA: 'C:\\Users\\a%PATH%b\\AppData\\Local', SystemRoot: 'C:\\Windows' },
platform: 'win32'
})
expect(await run(CLI_KEY, { keepOtherAccount: true })).toBeNull()
expect(spawn).not.toHaveBeenCalled()
})
})