Files
aiturk-hermes-ide/apps/mobile/scripts/browser-smoke.mjs
T

179 lines
12 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 { chromium } from 'playwright'
import { createServer } from 'node:http'
import { readFile, mkdir } from 'node:fs/promises'
import { extname, resolve } from 'node:path'
import assert from 'node:assert/strict'
const root = resolve('dist')
const server = createServer(async (request, response) => {
try {
const path = decodeURIComponent(new URL(request.url, 'http://localhost').pathname)
const file = resolve(root, '.' + (path === '/' ? '/index.html' : path))
if (!file.startsWith(root + '\\') && !file.startsWith(root + '/')) throw new Error('outside assets')
const data = await readFile(file)
response.writeHead(200, { 'Content-Type': ({ '.html': 'text/html', '.mjs': 'text/javascript', '.js': 'text/javascript', '.css': 'text/css' })[extname(file)] || 'application/octet-stream' }); response.end(data)
} catch { response.writeHead(404); response.end() }
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const browser = await chromium.launch({ channel: 'msedge', headless: true })
try {
const page = await browser.newPage({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 1 })
page.setDefaultTimeout(15000)
const errors = []; page.on('pageerror', error => errors.push(error.message))
await page.goto(`http://127.0.0.1:${server.address().port}`)
await page.getByRole('heading', { name: 'Fikrini konuş. Birlikte geliştir.' }).waitFor()
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
assert.ok((await page.getByRole('button', { name: 'Eşleştirme kodu oluştur' }).boundingBox()).height >= 48)
// UI wiring only: real account authentication is covered by gateway HTTP/DB
// tests. Never submit a credential to the live portal from this smoke test.
const loginCalls = []
await page.route('https://turkservis.online/v1/aiturk/mobile-auth/**', async route => {
const action = new URL(route.request().url()).pathname.split('/').at(-1); loginCalls.push(action)
const result = action === 'start' ? { deviceCode: 'tsml_fixture', userCode: 'ABCDEFGH',
verificationUri: 'https://turkservis.online/account/aiturk/mobile-login/11111111-1111-4111-8111-111111111111' }
: action === 'poll' ? { state: 'approved', accountToken: 'tsml_fixture' } : { revoked: true }
await route.fulfill({ status: 200, contentType: 'application/json', headers: { 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify(result) })
})
await page.getByRole('button', { name: 'AITURK hesabımla giriş yap' }).click()
await page.getByText('Yalnız kendi hesabınızda bu kodu onaylayın: ABCDEFGH').waitFor()
await page.getByRole('button', { name: 'Girişi kontrol et' }).click()
await page.getByText('Hesaba giriş yapıldı.', { exact: true }).waitFor()
let claimed = false
await page.route('https://turkservis.online/v1/aiturk/mobile/invites/**/claim', async route => {
claimed = true
const body = route.request().postDataJSON(); assert.equal(body.secret, 'a'.repeat(43))
await route.fulfill({ status: 201, contentType: 'application/json', headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ device_id: '33333333-3333-4333-8333-333333333333', device_token: 'fixture-device-secret', pairing_id: 'fixture-pair', user_code: 'QRSTUVWX' }) })
})
const qrImage = await page.evaluate(async () => {
const { drawQr } = await import('./qr.mjs'), canvas = document.createElement('canvas')
window.fixtureDesktopCertificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' })
const desktopFingerprint = window.fixtureDesktopCertificate.getFingerprints().find(item => item.algorithm === 'sha-256').value
drawQr(canvas, JSON.stringify({ kind: 'aiturk-mobile-invite', version: 2, desktopFingerprint, inviteId: '11111111-1111-4111-8111-111111111111',
desktopId: '22222222-2222-4222-8222-222222222222', secret: 'a'.repeat(43), expiresAt: new Date(Date.now() + 600000).toISOString() }))
return canvas.toDataURL('image/png').split(',')[1]
})
await page.locator('#qr-file').setInputFiles({ name: 'invite.png', mimeType: 'image/png', buffer: Buffer.from(qrImage, 'base64') })
await page.getByText('Bilgisayarda bekleyen telefonu getirip şu kodu onaylayın: QRSTUVWX').waitFor()
assert.equal(claimed, true)
const phoneQr = await page.evaluate(() => {
const canvas = document.getElementById('phone-identity-qr')
const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height)
return { decoded: jsQR(pixels.data, pixels.width, pixels.height)?.data,
expected: document.getElementById('phone-identity').value, visible: !canvas.hidden }
})
assert.equal(phoneQr.visible, true)
assert.equal(phoneQr.decoded, phoneQr.expected)
// Run the actual app -> AccountClient -> WebRTC -> VoiceSession wiring.
// Portal, IDE responses and Google frames are synthetic; no paid/API traffic.
await page.route('https://turkservis.online/v1/aiturk/mobile/devices', route => route.fulfill({
json: { devices: [{ id: '22222222-2222-4222-8222-222222222222', name: 'Test bilgisayarı', platform: 'windows', self: false }] } }))
await page.route('https://turkservis.online/v1/aiturk/mobile/ice', route => route.fulfill({ json: { iceServers: [] } }))
await page.route('https://turkservis.online/v1/aiturk/mobile/channels**', async route => {
if (route.request().method() === 'POST' && !route.request().url().endsWith('/heartbeat')) {
const { sdp } = route.request().postDataJSON()
await page.evaluate(async sdp => {
const { completeIce, sendFrame } = await import('./transport.mjs')
const { assertRemoteIdentity } = await import('./peer-identity.mjs')
const trustedPhone = JSON.parse(document.getElementById('phone-identity').value)
assertRemoteIdentity(sdp, trustedPhone.fingerprint)
const pc = new RTCPeerConnection({ certificates: [window.fixtureDesktopCertificate] }); window.fixtureDesktop = pc; window.fixtureActions = []
pc.ondatachannel = event => { event.channel.onmessage = incoming => {
const request = JSON.parse(incoming.data); window.fixtureActions.push(request.action)
const results = {
'sessions.list': { sessions: [{ id: 'fixture-session', title: 'Test işi', model: 'fixture-model' }] },
'session.open': { model: 'fixture-model', messages: [], approval: null, running: false },
'voice.token': { token: 'fixture-only', model: 'gemini-3.1-flash-live-preview' },
}
sendFrame(event.channel, { id: request.id, result: results[request.action] || {} })
} }
await pc.setRemoteDescription({ type: 'offer', sdp })
await pc.setLocalDescription(await pc.createAnswer()); await completeIce(pc)
window.fixtureAnswer = pc.localDescription.sdp
}, sdp)
await route.fulfill({ json: {} })
} else await route.fulfill({ json: { sdp: await page.evaluate(() => window.fixtureAnswer) } })
})
await page.evaluate(() => {
window.fixtureVoiceSockets = []
window.WebSocket = class {
constructor(url) {
if (!url.startsWith('wss://generativelanguage.googleapis.com/')) throw new Error('Unexpected test socket')
this.readyState = 1; this.bufferedAmount = 0; window.fixtureVoiceSockets.push(this)
queueMicrotask(() => this.onopen?.())
}
send(raw) {
if (JSON.parse(raw).setup) queueMicrotask(() => this.onmessage?.({ data: JSON.stringify({ setupComplete: {} }) }))
}
close() { this.readyState = 3; this.onclose?.() }
}
})
await page.getByRole('button', { name: 'Onaydan sonra bilgisayarları bul' }).click()
await page.getByRole('button', { name: 'Bilgisayara bağlan', exact: true }).click()
await page.getByText('Bilgisayar bağlı. IDE konuşmasını seçin.', { exact: true }).waitFor()
await page.getByRole('button', { name: 'Konuşmaları yenile' }).click()
await page.getByRole('button', { name: 'Bu konuşmayı seç', exact: true }).click()
await page.getByText('IDE konuşması açıldı.', { exact: true }).waitFor()
await page.locator('#consent').check()
await page.locator('#voice-budget').fill('0.01')
await page.locator('#voice-minutes').fill('3')
await page.getByRole('button', { name: 'Asistanla yazış', exact: true }).click()
await page.getByText('Google asistanı bağlı.', { exact: true }).waitFor()
assert.match(await page.locator('#usage').textContent(), /henüz bilinmiyor/)
await page.evaluate(() => window.fixtureVoiceSockets[0].onmessage({ data: JSON.stringify({ usageMetadata: {
promptTokenCount: 1000, responseTokenCount: 1000, totalTokenCount: 2000,
promptTokensDetails: [{ modality: 'AUDIO', tokenCount: 1000 }],
responseTokensDetails: [{ modality: 'AUDIO', tokenCount: 1000 }],
} }) }))
await page.locator('#voice-status').filter({ hasText: 'maliyet sınırına ulaşıldı' }).waitFor()
assert.match(await page.locator('#usage').textContent(), /0\.0150–0\.0150 USD/)
assert.equal(await page.evaluate(() => window.fixtureVoiceSockets[0].readyState), 3)
assert.deepEqual(await page.evaluate(() => window.fixtureActions), ['sessions.list', 'session.open', 'voice.token'])
assert.deepEqual(await page.evaluate(() => JSON.parse(localStorage.getItem('aiturk.voice-limits'))), { minutes: 3, usd: 0.01 })
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false)
await mkdir('test-results', { recursive: true })
await page.screenshot({ path: 'test-results/android-voice-limit.png', fullPage: true })
await page.evaluate(() => window.fixtureDesktop.close())
// A signaling service returning another machine's valid SDP must not replace
// the identity captured from the user's QR or receive any IDE requests.
await page.evaluate(async () => {
window.fixtureDesktopCertificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' })
})
await page.getByRole('button', { name: 'Bilgisayara bağlan', exact: true }).click()
await page.locator('#status').filter({ hasText: 'Cihaz kimliği doğrulanamadı' }).waitFor()
assert.deepEqual(await page.evaluate(() => window.fixtureActions), [])
await page.evaluate(() => window.fixtureDesktop.close())
await page.getByRole('button', { name: 'Bu uygulamadaki hesaptan çık' }).click()
await page.getByText('Bu uygulamanın oturumu kapatıldı.', { exact: false }).waitFor()
assert.deepEqual(loginCalls, ['start', 'poll', 'logout'])
await mkdir('test-results', { recursive: true })
await page.screenshot({ path: 'test-results/android-onboarding.png', fullPage: true })
const result = await page.evaluate(async () => {
const { completeIce, DataRequests, sendFrame } = await import('./transport.mjs')
const phone = new RTCPeerConnection(), desktop = new RTCPeerConnection()
try {
const channel = phone.createDataChannel('aiturk-mobile-v1'), requests = new DataRequests(channel, () => {})
const remoteReady = new Promise(resolve => { desktop.ondatachannel = event => {
event.channel.onmessage = incoming => {
const request = JSON.parse(incoming.data)
sendFrame(event.channel, { id: request.id, result: { action: request.action, text: 'Türkçe 🟢 '.repeat(24000) } })
}
event.channel.onopen = resolve
} })
await phone.setLocalDescription(await phone.createOffer()); await completeIce(phone)
await desktop.setRemoteDescription(phone.localDescription)
await desktop.setLocalDescription(await desktop.createAnswer()); await completeIce(desktop)
await phone.setRemoteDescription(desktop.localDescription)
await Promise.race([remoteReady, new Promise((_, reject) => setTimeout(() => reject(new Error('WebRTC timeout')), 15000))])
if (channel.readyState !== 'open') await new Promise(resolve => channel.onopen = resolve)
const response = await requests.call('session.history', { sessionId: 'fixture' })
return { length: response.text.length, expected: 'Türkçe 🟢 '.repeat(24000).length, action: response.action,
connectionState: phone.connectionState, dtls: phone.sctp.transport.state }
} finally { phone.close(); desktop.close() }
})
assert.equal(result.length, result.expected); assert.equal(result.dtls, 'connected'); assert.equal(result.action, 'session.history')
assert.deepEqual(errors, [])
console.log(JSON.stringify({ onboarding: 'passed', mockedLoginUi: 'passed', realQrImageWithMockedClaim: 'passed',
appWebRTCWithSyntheticGoogleUsageLimit: 'passed', qrPinnedDesktopReplacement: 'rejected', realWebRTC: result, errors }))
} finally { await browser.close(); server.close() }