import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { test } from 'node:test' import { capturePageContext } from '../extension/page-context.js' const { JSDOM } = createRequire(import.meta.url)('jsdom') const eventHub = () => ({ listeners: [], addListener(callback) { this.listeners.push(callback) }, emit(value) { this.listeners.forEach(callback => callback(value)) } }) const tick = () => new Promise(resolve => setImmediate(resolve)) async function settle(condition) { for (let count = 0; count < 100; count++) { if (condition()) return; await tick() } assert.ok(condition(), 'UI did not settle') } const CONTROL_READY = { enabled: true, connected: true, registered: true, running: 0, tabs: [], shared: null, log: [{ status: 'ok', text: 'Sayfa açılıyor: example.test' }, { status: 'failed', text: 'Yazılıyor', note: 'Şifre alanına ajan yazamaz.' }] } async function panel(t, respond, { controlState = CONTROL_READY, session = {} } = {}) { const html = await readFile(new URL('../extension/panel.html', import.meta.url), 'utf8') const dom = new JSDOM(html, { url: 'https://extension-fixture.test/panel.html' }) const calls = [], controlCalls = [] const port = { onMessage: eventHub(), onDisconnect: eventHub(), disconnect() {}, postMessage(message) { calls.push(message) queueMicrotask(() => respond(message, frame => port.onMessage.emit(frame), port)) } } const runtimeMessages = eventHub() const sessionStore = { ...session } Object.assign(globalThis, { window: dom.window, document: dom.window.document, Option: dom.window.Option, chrome: { runtime: { connectNative: () => port, onMessage: runtimeMessages, sendMessage: async message => { controlCalls.push(message); return message.type === 'control.set' ? { ...controlState, enabled: message.enabled } : controlState } }, storage: { local: { get: async () => ({}), set: async () => {} }, onChanged: eventHub(), session: { get: async keys => Object.fromEntries([keys].flat().filter(key => key in sessionStore).map(key => [key, sessionStore[key]])), remove: async key => { delete sessionStore[key] }, set: async value => Object.assign(sessionStore, value) } }, tabs: { get: async id => ({ id, title: 'Kaynak sayfa', url: 'https://example.test/yazi?token=gizli#x' }) }, } }) await import(`../extension/panel.js?test=${Math.random()}`) t.after(() => { dom.window.close(); for (const key of ['window', 'document', 'Option', 'chrome']) delete globalThis[key] }) const get = id => dom.window.document.getElementById(id) await settle(() => !get('sessions').disabled) return { dom, calls, controlCalls, runtimeMessages, port, get, async select(id) { get('sessions').value = id get('sessions').dispatchEvent(new dom.window.Event('change')) await settle(() => !get('sessions').disabled) } } } function fixtureResponse(message, emit) { const result = message.action === 'status' ? { profile: 'default' } : message.action === 'sessions.list' ? { profile: 'default', sessions: [{ id: 'one', title: 'Birinci iş' }, { id: 'two', title: 'İkinci iş' }] } : message.action === 'session.open' ? { messages: [{ role: 'user', text: ' önceden yazıldı' }], model: 'IDE modeli' } : { messages: [] } emit({ id: message.id, result }) } test('panel safely renders real history and puts the submitted user before early streaming tokens', async t => { const p = await panel(t, (message, emit) => { if (message.action !== 'message.send') return fixtureResponse(message, emit) emit({ event: 'message.start', sessionId: 'one' }) emit({ event: 'message.delta', sessionId: 'one', text: 'İlk yanıt' }) emit({ id: message.id, result: { accepted: true } }) }) await p.select('one') assert.equal(p.get('transcript').querySelector('img'), null) assert.ok(p.get('transcript').textContent.includes(' p.get('message').value === '') const messages = [...p.get('transcript').querySelectorAll('.text')].map(node => node.textContent) assert.deepEqual(messages.slice(-2), ['Buradan devam edelim', 'İlk yanıt']) assert.equal(p.calls.filter(call => call.action === 'message.send').length, 1) assert.equal(p.calls.find(call => call.action === 'message.send').params.sessionId, 'one') }) test('task switching drops old events; disconnect keeps draft and does not retry a mutation', async t => { const p = await panel(t, (message, emit, port) => { if (message.action === 'message.send') { port.onDisconnect.emit(); return } fixtureResponse(message, emit) }) await p.select('one') await p.select('two') p.port.onMessage.emit({ event: 'message.delta', sessionId: 'one', text: 'YANLIŞ KONUŞMA' }) assert.ok(!p.get('transcript').textContent.includes('YANLIŞ')) p.get('message').value = 'Kaybolmasın' p.get('composer').dispatchEvent(new p.dom.window.Event('submit', { cancelable: true })) await settle(() => p.get('notice').textContent.includes('Taslağınız korundu')) assert.equal(p.get('message').value, 'Kaybolmasın') assert.equal(p.calls.filter(call => call.action === 'message.send').length, 1) assert.equal(p.get('send').disabled, true) }) test('events arriving while history opens are not silently lost', async t => { const p = await panel(t, (message, emit) => { if (message.action === 'session.open') { emit({ event: 'message.start', sessionId: message.params.sessionId }) emit({ event: 'message.delta', sessionId: message.params.sessionId, text: 'Açılırken gelen yanıt' }) } fixtureResponse(message, emit) }) await p.select('one') assert.ok(p.get('transcript').textContent.includes('Açılırken gelen yanıt')) }) test('explicit page capture excludes form values and strips URL credentials in query and fragment', t => { const dom = new JSDOM('

Görünür içerik

', { url: 'https://example.test/page?token=secret#private' }) Object.assign(globalThis, { window: dom.window, document: dom.window.document, location: dom.window.location, NodeFilter: dom.window.NodeFilter, getComputedStyle: dom.window.getComputedStyle }) dom.window.HTMLElement.prototype.getClientRects = () => [{ width: 100 }] t.after(() => { dom.window.close(); for (const key of ['window', 'document', 'location', 'NodeFilter', 'getComputedStyle']) delete globalThis[key] }) const context = capturePageContext() assert.equal(context.text, 'Görünür içerik') assert.equal(context.url, 'https://example.test/page') assert.equal(context.selection, false) }) test('agent control section shows the live state, never renders log text as HTML, and drives the background', async t => { const p = await panel(t, fixtureResponse) await settle(() => p.get('control-status').textContent.startsWith('Hazır')) assert.equal(p.get('control-enabled').checked, true) assert.equal(p.get('control-stop').hidden, true) assert.equal(p.get('control-log').querySelector('b'), null) assert.ok(p.get('control-log').textContent.includes('Yazılıyor')) assert.ok(p.get('control-log').textContent.includes('Şifre alanına ajan yazamaz.')) p.runtimeMessages.emit({ type: 'control.state', state: { ...CONTROL_READY, running: 1 } }) assert.equal(p.get('control-stop').hidden, false) assert.ok(p.get('control-status').textContent.includes('çalışıyor')) await p.select('one') p.get('control-share').dispatchEvent(new p.dom.window.Event('click')) p.get('control-stop').dispatchEvent(new p.dom.window.Event('click')) p.get('control-enabled').checked = false p.get('control-enabled').dispatchEvent(new p.dom.window.Event('change')) await settle(() => p.controlCalls.some(call => call.type === 'control.set')) assert.deepEqual(p.controlCalls.find(call => call.type === 'control.share'), { type: 'control.share', sessionId: 'one' }) assert.ok(p.controlCalls.some(call => call.type === 'control.stop')) assert.deepEqual(p.controlCalls.find(call => call.type === 'control.set'), { type: 'control.set', enabled: false }) await settle(() => p.get('control-status').textContent.startsWith('Kapalı')) }) test('a selection sent from the right-click menu waits for the first conversation and drops URL secrets', async t => { const p = await panel(t, fixtureResponse, { session: { pendingContext: { kind: 'selection', tabId: 7, text: 'Seçilen paragraf', at: Date.now() } } }) await settle(() => !p.get('attachment').hidden) assert.ok(p.get('attachment-label').textContent.includes('Seçim: Kaynak sayfa')) assert.ok(p.get('notice').textContent.includes('konuşması seçin')) await p.select('one') assert.equal(p.get('attachment').hidden, false, 'the first choice keeps the pending attachment') p.get('message').value = 'Bunu özetle' p.get('composer').dispatchEvent(new p.dom.window.Event('submit', { cancelable: true })) await settle(() => p.calls.some(call => call.action === 'message.send')) const sent = p.calls.find(call => call.action === 'message.send').params.text assert.ok(sent.includes('Seçilen paragraf')) assert.ok(sent.includes('https://example.test/yazi')) assert.ok(!sent.includes('token=gizli')) })