feat(mobile): add Android development client and scoped IDE companion
This commit is contained in:
1 parent
3332458708
commit
9f9f3226a1
84 files changed
+4371
-4
No files matched your search
@@ -0,0 +1,54 @@
|
||||
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 })
|
||||
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)
|
||||
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', realWebRTC: result, errors }))
|
||||
} finally { await browser.close(); server.close() }
|
||||
@@ -0,0 +1,8 @@
|
||||
import { cp, mkdir } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
await mkdir(new URL('../dist', import.meta.url), { recursive: true })
|
||||
await cp(new URL('../src', import.meta.url), new URL('../dist', import.meta.url), { recursive: true })
|
||||
const core = dirname(createRequire(import.meta.url).resolve('@capacitor/core/package.json'))
|
||||
await cp(join(core, 'dist', 'capacitor.js'), new URL('../dist/capacitor.js', import.meta.url))
|
||||
console.log('AITURK Android web assets built.')
|
||||
@@ -0,0 +1,34 @@
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { resolve, join } from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createMobileWindow } from '../../desktop/electron/mobile-host.mjs'
|
||||
import { MobileVault } from '../../desktop/electron/mobile-secrets.mjs'
|
||||
const directory = await mkdtemp(join(tmpdir(), 'aiturk-mobile-electron-'))
|
||||
app.setPath('userData', join(directory, 'electron-profile'))
|
||||
void app.whenReady().then(async () => {
|
||||
let window
|
||||
try {
|
||||
assert.ok(safeStorage.isEncryptionAvailable(), 'Windows encryption unavailable')
|
||||
window = createMobileWindow({ directory, assets: resolve('apps/desktop/dist'), show: false,
|
||||
resolveBackend: async () => { throw new Error('No live backend in vault smoke test') } })
|
||||
await new Promise(resolve => window.webContents.once('did-finish-load', resolve))
|
||||
const result = await window.webContents.executeJavaScript(`(async () => {
|
||||
const before = await mobileHost.call('setup');
|
||||
await mobileHost.call('account.save', {token:'local-fake-account-for-encryption-test'});
|
||||
const after = await mobileHost.call('setup');
|
||||
return {before,after,heading:document.querySelector('h1').textContent};
|
||||
})()`)
|
||||
assert.equal(result.before.hasAccount, false); assert.equal(result.after.hasAccount, true)
|
||||
const blob = await readFile(join(directory, 'mobile', 'mobile-secrets.bin'))
|
||||
assert.ok(!blob.includes(Buffer.from('local-fake-account-for-encryption-test')))
|
||||
const vault = new MobileVault(join(directory, 'mobile'), safeStorage)
|
||||
assert.equal((await vault.load()).accountToken, 'local-fake-account-for-encryption-test')
|
||||
await vault.save({})
|
||||
const receipt = { realElectronIpc: 'passed', windowsSafeStorage: 'passed', heading: result.heading, checkedAt: new Date().toISOString() }
|
||||
await writeFile('apps/mobile/test-results/electron-smoke.json', JSON.stringify(receipt))
|
||||
console.log(JSON.stringify(receipt))
|
||||
window.destroy(); app.exit(0)
|
||||
} catch (error) { console.error(error); window?.destroy(); app.exit(1) }
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { DeliveryJournal, MobileGatewayClient } from '../../desktop/electron/mobile-adapter.mjs'
|
||||
const [port, directory] = process.argv.slice(2), events = []
|
||||
const client = new MobileGatewayClient(async () => ({ baseUrl: `http://127.0.0.1:${port}`,
|
||||
wsUrl: `ws://127.0.0.1:${port}/api/ws?token=local-mobile-integration-token`, token: 'local-mobile-integration-token' }),
|
||||
event => events.push(event), { deviceId: 'phone-fixture', journal: new DeliveryJournal(directory), authorize: async () => {} })
|
||||
try {
|
||||
const list = await client.handle({ id: 'list', action: 'sessions.list' })
|
||||
assert.ok(list.sessions.some(row => row.id === 'mobile-integration-session'))
|
||||
const selected = await client.handle({ id: 'open', action: 'session.open', params: { sessionId: 'mobile-integration-session' } })
|
||||
assert.equal(selected.model, 'mobile-fixture')
|
||||
const request = { id: 'send', action: 'task.submit', params: { taskId: 'one-task', sessionId: 'mobile-integration-session',
|
||||
model: selected.model, intent: 'research', text: 'MOBIL_GOREV_20260908: Yalnız bağlantı sınaması; dosya değiştirme.' } }
|
||||
assert.equal((await client.handle(request)).state, 'accepted')
|
||||
assert.equal((await client.handle(request)).duplicate, true)
|
||||
for (let i = 0; i < 300 && !events.some(event => event.event === 'message.complete'); i++) await new Promise(resolve => setTimeout(resolve, 100))
|
||||
const result = events.find(event => event.event === 'message.complete')
|
||||
assert.ok(result, 'real runtime completion event missing')
|
||||
assert.equal(result.status, 'complete', JSON.stringify(result)); assert.equal(result.text, 'MOBIL_RUNTIME_OK')
|
||||
console.log(JSON.stringify({ result, duplicateSuppressed: true }))
|
||||
} finally { client.close() }
|
||||
Reference in new issue
Block a user