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)) })