Separate encrypted reasoning from actual compaction, stop migration after native return, and retain full history and recent exchanges. Add typed settings, Gemini fallback, verified provider windows, bounded summaries and durable concurrent checkpoints. Validation: 245 bridge tests plus focused concurrency tests; 71 settings/connection tests; 66 supplementary tests; TypeScript checks; renderer and Electron bundles; real primary/native/fallback summary probes and 700K input acceptance.
141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
/** Opt-in acceptance against a running local IDE backend. No credentials on disk. */
|
|
import https from 'node:https'
|
|
import crypto from 'node:crypto'
|
|
import type { PeerCertificate } from 'node:tls'
|
|
import { expect, it } from 'vitest'
|
|
import { WebSocket } from 'ws'
|
|
import { createSelfSignedCertificate } from './mobile-lan-cert'
|
|
import { createLanProxy } from './mobile-lan-proxy'
|
|
|
|
it.skipIf(!process.env.AITURK_TEST_BACKEND_PORT)(
|
|
'real backend: pinned TLS, screens, cron lifecycle and chat socket',
|
|
async () => {
|
|
const port = Number(process.env.AITURK_TEST_BACKEND_PORT)
|
|
expect(Number.isInteger(port) && port > 0 && port < 65536).toBe(true)
|
|
const baseUrl = `http://127.0.0.1:${port}`
|
|
const root = await (await fetch(baseUrl)).text()
|
|
const session = root.match(/window\.__HERMES_SESSION_TOKEN__\s*=\s*"([^"]+)"/)?.[1]
|
|
expect(Boolean(session)).toBe(true)
|
|
const identity = createSelfSignedCertificate()
|
|
const checkServerIdentity = (_host: string, cert: PeerCertificate) =>
|
|
crypto.createHash('sha256').update(cert.raw).digest('base64url') === identity.fingerprint
|
|
? undefined
|
|
: new Error('Certificate pin mismatch')
|
|
const proxy = createLanProxy({
|
|
tls: identity,
|
|
version: 'acceptance',
|
|
pcName: 'acceptance',
|
|
resolveBackend: async profile => {
|
|
if (profile !== 'default') throw new Error('Acceptance may only use the default profile')
|
|
return { baseUrl, token: session! }
|
|
}
|
|
})
|
|
const proxyPort = await proxy.listen(0, '127.0.0.1')
|
|
let deviceToken = ''
|
|
let createdId: string | undefined
|
|
const call = (path: string, method = 'GET', body?: unknown): Promise<{ status: number; data: any }> =>
|
|
new Promise((resolve, reject) => {
|
|
const req = https.request(
|
|
{
|
|
hostname: '127.0.0.1',
|
|
port: proxyPort,
|
|
path,
|
|
method,
|
|
ca: identity.cert,
|
|
checkServerIdentity,
|
|
headers: {
|
|
...(deviceToken ? { Authorization: `Bearer ${deviceToken}` } : {}),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
},
|
|
res => {
|
|
let value = ''
|
|
res.on('data', chunk => (value += chunk))
|
|
res.on('end', () => {
|
|
try {
|
|
resolve({ status: res.statusCode!, data: value ? JSON.parse(value) : null })
|
|
} catch {
|
|
reject(new Error(`Non-JSON response: ${path}`))
|
|
}
|
|
})
|
|
}
|
|
)
|
|
req.setTimeout(60000, () => req.destroy(new Error(`Timeout: ${path}`)))
|
|
req.on('error', reject)
|
|
req.end(body === undefined ? undefined : JSON.stringify(body))
|
|
})
|
|
try {
|
|
const pair = await call('/aiturk/pair/claim', 'POST', {
|
|
code: proxy.beginPairing().code,
|
|
device_name: 'Acceptance ephemeral'
|
|
})
|
|
expect(pair.status).toBe(200)
|
|
deviceToken = pair.data.token
|
|
for (const endpoint of [
|
|
'health',
|
|
'profiles',
|
|
'sessions',
|
|
'cron/jobs',
|
|
'cron/blueprints',
|
|
'cron/delivery-targets',
|
|
'model/info',
|
|
'skills',
|
|
'tools/toolsets',
|
|
'mcp/servers',
|
|
'plugins/kanban/boards'
|
|
]) {
|
|
const response = await call(`/api/${endpoint}?profile=default`)
|
|
expect(response.status, endpoint).toBe(200)
|
|
}
|
|
const created = await call('/api/cron/jobs?profile=default', 'POST', {
|
|
name: `AITURK mobile acceptance ${Date.now()}`,
|
|
schedule: '0 0 1 1 *',
|
|
prompt: 'Temporary acceptance record; deleted immediately without execution.',
|
|
deliver: 'local'
|
|
})
|
|
expect(created.status).toBe(200)
|
|
createdId = created.data.id
|
|
expect(typeof createdId).toBe('string')
|
|
const route = `/api/cron/jobs/${encodeURIComponent(createdId!)}`
|
|
expect((await call(`${route}/pause?profile=default`, 'POST')).status).toBe(200)
|
|
expect(
|
|
(await call(`${route}?profile=default`, 'PUT', { updates: { name: 'AITURK acceptance updated' } })).status
|
|
).toBe(200)
|
|
expect((await call(`${route}/resume?profile=default`, 'POST')).status).toBe(200)
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tlsOptions: https.RequestOptions = {
|
|
ca: identity.cert,
|
|
checkServerIdentity,
|
|
headers: { Authorization: `Bearer ${deviceToken}` }
|
|
}
|
|
const ws = new WebSocket(`wss://127.0.0.1:${proxyPort}/api/ws?profile=default`, tlsOptions)
|
|
const timer = setTimeout(() => {
|
|
ws.terminate()
|
|
reject(new Error('Chat ping timed out'))
|
|
}, 15000)
|
|
ws.on('open', () => ws.send(JSON.stringify({ jsonrpc: '2.0', id: 123, method: 'gateway.ping', params: {} })))
|
|
ws.on('message', raw => {
|
|
const message = JSON.parse(String(raw))
|
|
if (message.id === 123) {
|
|
clearTimeout(timer)
|
|
ws.close()
|
|
message.error ? reject(new Error('Chat ping rejected')) : resolve()
|
|
}
|
|
})
|
|
ws.on('error', error => {
|
|
clearTimeout(timer)
|
|
reject(error)
|
|
})
|
|
})
|
|
} finally {
|
|
if (createdId) {
|
|
const route = `/api/cron/jobs/${encodeURIComponent(createdId)}?profile=default`
|
|
expect((await call(route, 'DELETE')).status).toBe(200)
|
|
expect((await call(route)).status).toBe(404)
|
|
}
|
|
await proxy.stop()
|
|
}
|
|
},
|
|
180000
|
|
)
|