Harden Chrome bridge framing and preserve panel state for IDE beta 7
This commit is contained in:
1 parent
fb6f57cd36
commit
57876d11f0
8 files changed
+192
-18
No files matched your search
@@ -0,0 +1,103 @@
|
||||
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')
|
||||
}
|
||||
|
||||
async function panel(t, respond) {
|
||||
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 = []
|
||||
const port = { onMessage: eventHub(), onDisconnect: eventHub(), disconnect() {}, postMessage(message) {
|
||||
calls.push(message)
|
||||
queueMicrotask(() => respond(message, frame => port.onMessage.emit(frame), port))
|
||||
} }
|
||||
Object.assign(globalThis, { window: dom.window, document: dom.window.document, Option: dom.window.Option,
|
||||
chrome: { runtime: { connectNative: () => port }, storage: { local: { get: async () => ({}), set: async () => {} } } } })
|
||||
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, 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: '<img src=x onerror=alert(1)> ö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('<img'))
|
||||
p.get('message').value = 'Buradan devam edelim'
|
||||
p.get('composer').dispatchEvent(new p.dom.window.Event('submit', { cancelable: true }))
|
||||
await settle(() => 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('<body><p>Görünür içerik</p><input value="secret"><textarea>private draft</textarea><div hidden>hidden secret</div><script>private code</script></body>',
|
||||
{ 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)
|
||||
})
|
||||
Reference in new issue
Block a user