Files
aiturk-hermes-ide/apps/mobile/tests/adapter.test.mjs
T

76 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { DeliveryJournal, MobileGatewayClient } from '../../desktop/electron/mobile-adapter.mjs'
import { LiveTokenIssuer } from '../../desktop/electron/mobile-secrets.mjs'
test('durable reservation survives restart and concurrent resend; content stays out of journal', async () => {
const directory = await mkdtemp(join(tmpdir(), 'aiturk-delivery-'))
try {
const journal = new DeliveryJournal(directory), envelope = { text: 'private prompt', model: 'selected' }
const reservations = await Promise.all(Array.from({ length: 5 }, () => journal.reserve('phone', 'task', envelope)))
assert.equal(reservations.filter(r => r.fresh).length, 1)
const restarted = new DeliveryJournal(directory)
assert.equal((await restarted.reserve('phone', 'task', envelope)).state, 'unknown')
await journal.accepted(reservations.find(r => r.fresh))
assert.equal((await restarted.reserve('phone', 'task', envelope)).state, 'accepted')
await Promise.all([journal.update(reservations.find(r => r.fresh), 'complete'), journal.accepted(reservations.find(r => r.fresh))])
assert.equal((await restarted.reserve('phone', 'task', envelope)).state, 'complete', 'late acceptance must not overwrite completion')
await assert.rejects(restarted.reserve('phone', 'task', { text: 'changed' }), /farklı/)
const data = await readFile(join(directory, (await readdir(directory))[0]), 'utf8')
assert.ok(!data.includes('private prompt'))
} finally { await rm(directory, { recursive: true, force: true }) }
})
test('task events only settle the matching device task and runtime', async () => {
const events = [], writes = []
const client = new MobileGatewayClient(() => {}, event => events.push(event), {
journal: { update: async (...args) => writes.push(args) }, authorize: async () => {} })
client.tasks.set('correlation', { taskId: 'phone-task', sessionId: 'stored', runtimeId: 'runtime', reservation: {} })
client.onEvent({ type: 'task.complete', session_id: 'other', payload: { task_id: 'correlation', status: 'complete' } })
client.onEvent({ type: 'task.complete', session_id: 'runtime', payload: { task_id: 'another-task', status: 'complete' } })
assert.equal(events.length, 0)
const terminal = { type: 'task.complete', session_id: 'runtime', payload: { task_id: 'correlation', status: 'error', text: 'Test failed' } }
client.onEvent(terminal); client.onEvent(terminal)
assert.equal(events.length, 1); assert.equal(writes.length, 1)
assert.equal(events[0].taskId, 'phone-task'); assert.equal(events[0].status, 'error')
})
test('approval binds exact backend request and forbids permanent/model-generated permissions', async () => {
const rpcCalls = []
const client = new MobileGatewayClient(() => {}, () => {}, { authorize: async () => {} })
client.selected = { storedId: 'stored', runtimeId: 'runtime' }
client.onEvent({ session_id: 'runtime', type: 'approval.request', payload: { request_id: 'first', command: 'git status', choices: ['once', 'session', 'always', 'deny'] } })
client.rpc = async (...args) => { rpcCalls.push(args); return { resolved: 1 } }
for (const params of [{ requestId: 'old', choice: 'once' }, { requestId: 'first', choice: 'always' }])
await assert.rejects(client.handle({ id: 'req', action: 'approval.respond', params: { sessionId: 'stored', ...params } }))
assert.equal(rpcCalls.length, 0)
await client.handle({ id: 'req', action: 'approval.respond', params: { sessionId: 'stored', requestId: 'first', choice: 'once' } })
assert.deepEqual(rpcCalls[0], ['approval.respond', { session_id: 'runtime', request_id: 'first', choice: 'once', all: false }])
await assert.rejects(client.handle({ id: 'req', action: 'approval.respond', params: { sessionId: 'stored', requestId: 'first', choice: 'once' } }))
})
test('revocation rejects requests before touching IDE', async () => {
let touched = false
const client = new MobileGatewayClient(() => { touched = true }, () => {}, { authorize: async () => { throw new Error('revoked') } })
await assert.rejects(client.handle({ id: 'test', action: 'sessions.list' }), /revoked/)
assert.equal(touched, false)
})
test('ephemeral token is constrained, short lived, and rate limited; errors cannot echo key', async () => {
const requests = [], now = 1000000, key = 'fake-owned-test-key'
const issuer = new LiveTokenIssuer({ load: async () => ({ googleKey: key }) }, async (url, options) => {
requests.push({ url, options }); return { ok: true, json: async () => ({ name: 'auth_tokens/test' }) }
}, () => now)
const result = await issuer.issue('phone')
assert.ok(!JSON.stringify(result).includes(key))
const body = JSON.parse(requests[0].options.body)
assert.equal(body.uses, 1); assert.equal(Date.parse(body.expireTime) - now, 600000)
assert.ok(body.bidiGenerateContentSetup.model.startsWith('models/'))
await assert.rejects(issuer.issue('phone'), /30 saniye/)
const bad = new LiveTokenIssuer({ load: async () => ({ googleKey: key }) }, async () => ({ ok: false, status: 403, text: async () => key }))
await assert.rejects(bad.issue('phone'), error => !error.message.includes(key))
})