var test = require('node:test'); var assert = require('node:assert/strict'); var cp = require('node:child_process'); var path = require('node:path'); var playwright = require('playwright'); test('Open Clay enters the native Mate route with one writable draft and exact review/session; Back restores project', async function (t) { var child = cp.fork(path.join(__dirname, 'fixtures/contextual-clay-server.cjs'), [], { stdio: ['ignore', 'pipe', 'pipe', 'ipc'] }); t.after(function () { child.kill('SIGTERM'); }); var output = ''; child.stdout.on('data', function (value) { output += value; }); child.stderr.on('data', function (value) { output += value; }); var info = await new Promise(function (resolve, reject) { var timer = setTimeout(function () { reject(new Error(output)); }, 15000); child.once('message', function (info) { clearTimeout(timer); resolve(info); }); }); var browser = await playwright.chromium.launch(); t.after(async function () { await browser.close(); }); var page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); page.setDefaultTimeout(8000); page.on('pageerror', function (error) { t.diagnostic('Browser error: ' + error.stack); }); var origin = 'http://127.0.0.1:' + info.port; await page.goto(origin + '/fixture/login?to=alpha'); await page.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; }); var projectSession = await page.evaluate(async function () { return (await import('/p/alpha/modules/store.js')).store.get('activeSessionId'); }); await page.locator('#input').fill('Original project draft'); var trigger = page.locator('.icon-strip-brand'); await trigger.click(); assert.equal(await page.locator('#contextual-clay [data-clay-expand] span').textContent(), 'Open Clay'); var frame = page.frameLocator('#contextual-clay iframe'); await frame.locator('#messages').getByText('Isolated canonical integration conversation.', { exact: true }).waitFor(); await frame.locator('#input-wrapper:not([inert])').waitFor(); var input = frame.locator('#input'); await input.fill('Help me organise this project. Keep my current session open while we review the next steps.'); var frameWindow = page.frames().find(function (item) { return item.url().includes('contextual=1'); }); await frameWindow.evaluate(async function () { window.rendererIdentity = 'preserved'; window.sent = []; var ws = (await import('./modules/ws-ref.js')).getWs(); var send = ws.send.bind(ws); ws.send = function (value) { window.sent.push(JSON.parse(value)); return send(value); }; }); for (var width of [1280, 390]) { await page.setViewportSize({ width: width, height: 844 }); await frame.locator('#contextual-attach-btn').click(); for (var selector of ['#attach-file-btn', '#attach-image-btn']) { assert.equal(await frame.locator(selector).isVisible(), true, selector + ' is available through + at ' + width); assert.ok((await frame.locator(selector).boundingBox()).width >= 36); } await frame.locator('#attach-file-btn').press('Escape'); } await frame.locator('#contextual-attach-btn').click(); var fileChooser = page.waitForEvent('filechooser'); await frame.locator('#attach-file-btn').click(); await (await fileChooser).setFiles({ name: 'handoff.txt', mimeType: 'text/plain', buffer: Buffer.from('Isolated attachment continuity check.') }); await frame.locator('#image-preview-bar').getByText('handoff.txt', { exact: false }).waitFor(); await frame.locator('#contextual-attach-btn').click(); var imageChooser = page.waitForEvent('filechooser'); await frame.locator('#attach-image-btn').click(); await (await imageChooser).setFiles({ name: 'pixel.png', mimeType: 'image/png', buffer: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1sAAAAASUVORK5CYII=', 'base64') }); await frame.locator('#image-preview-bar img').waitFor(); var selectedViewer = await browser.newPage(); await selectedViewer.goto(origin + '/fixture/login?to=alpha'); await selectedViewer.goto(origin + '/p/beta/'); await selectedViewer.locator('#messages').getByText('BETA_RENDERED_DESTINATION', { exact: true }).waitFor(); await page.bringToFront(); var review = page.request.get(origin + '/fixture/propose?viewer=beta&target=alpha'); await frame.locator('.ask-user-container:not(.answered)').waitFor(); var reviewId = await frame.locator('.ask-user-container:not(.answered)').getAttribute('data-tool-id'); await page.getByRole('button', { name: 'Open Clay', exact: true }).click(); await page.waitForURL('**/p/' + info.sourceSlug + '/'); await page.waitForFunction(function (session) { return document.body.classList.contains('contextual-clay-native') && document.getElementById('input').value.includes('Help me organise') && !document.getElementById('input-wrapper').inert; }, info.sourceSessionId); assert.equal(await page.evaluate(async function () { var state = (await import('/p/alpha/modules/store.js')).store; return state.get('currentSlug'); }), info.sourceSlug); assert.equal(await page.evaluate(async function () { return (await import('/p/alpha/modules/store.js')).store.get('activeSessionId'); }), info.sourceSessionId); assert.equal(await page.locator('body').evaluate(function (body) { return body.classList.contains('mate-workspace-active'); }), true); assert.equal(await page.locator('#contextual-clay').isVisible(), false, 'No expanded iframe overlay'); assert.equal(await input.inputValue(), '', 'The hidden popover does not keep a second draft'); assert.equal(await input.evaluate(function (input) { return input.readOnly; }), true); assert.match(await page.locator('#image-preview-bar').textContent(), /handoff.txt/); assert.equal(await page.locator('#image-preview-bar img').count(), 1); await page.evaluate(async function () { window.nativeSent = []; var ws = (await import('/p/alpha/modules/ws-ref.js')).getWs(); var send = ws.send.bind(ws); ws.send = function (value) { window.nativeSent.push(JSON.parse(value)); return send(value); }; }); var nativeReview = page.locator('.ask-user-container[data-tool-id="' + reviewId + '"]'); await nativeReview.getByText('Keep unchanged', { exact: true }).click(); await nativeReview.locator('.ask-user-submit').click(); assert.equal((await (await review).json()).status, 'cancelled'); assert.equal(await page.evaluate(function () { return window.nativeSent.filter(function (event) { return event.type === 'ask_user_response'; }).length; }), 1); assert.equal(await frameWindow.evaluate(function () { return window.sent.filter(function (event) { return event.type === 'ask_user_response'; }).length; }), 0); await page.evaluate(async function () { var store = (await import('/p/alpha/modules/store.js')).store; window.nativeStore = store; window.nativeWsModule = await import('/p/alpha/modules/ws-ref.js'); window.handoffs = []; store.subscribe(function (value, prev) { if (value.contextualWorkspace !== prev.contextualWorkspace) window.handoffs.push({ stage: value.contextualWorkspace && value.contextualWorkspace.stage, images: value.contextualWorkspace && value.contextualWorkspace.draft && value.contextualWorkspace.draft.images.length }); }); }); await page.bringToFront(); await page.evaluate(async function () { window.oldNativeSocket = (await import('/p/alpha/modules/ws-ref.js')).getWs(); window.oldNativeSocket.close(); }); await page.waitForFunction(function () { return window.oldNativeSocket.readyState === 3 && window.handoffs.some(function (handoff) { return handoff.stage === 'opening'; }); }); await page.waitForFunction(function () { return window.nativeStore.get('connected') && window.nativeStore.get('contextualWorkspace').stage === 'workspace' && !window.nativeStore.get('replayingHistory') && window.nativeWsModule.getWs() !== window.oldNativeSocket && document.getElementById('input').value.includes('Help me organise') && document.querySelectorAll('#image-preview-bar img').length === 1 && !document.getElementById('input-wrapper').inert; }); t.diagnostic('Reconnect state: ' + JSON.stringify(await page.evaluate(async function () { var store = (await import('/p/alpha/modules/store.js')).store; return { stage: store.get('contextualWorkspace').stage, connected: store.get('connected'), hidden: document.hidden, images: document.querySelectorAll('#image-preview-bar img').length, native: document.body.className, input: document.getElementById('input').value }; }))); t.diagnostic('Reconnect handoffs: ' + JSON.stringify(await page.evaluate(function () { return window.handoffs; }))); assert.equal(await page.locator('#image-preview-bar img').count(), 1, 'Reconnect preserves attachment ownership'); assert.match(await page.locator('#image-preview-bar').textContent(), /handoff.txt/); await page.evaluate(async function () { window.nativeStop = []; var ws = (await import('/p/alpha/modules/ws-ref.js')).getWs(); var send = ws.send.bind(ws); ws.send = function (value) { window.nativeStop.push(JSON.parse(value)); return send(value); }; }); var nativeDraft = 'Edited in the native Clay workspace. Help me organise this project. Keep my current session open while we review the next steps.'; await page.locator('#input').fill(nativeDraft); await page.request.get(origin + '/fixture/stream'); await page.locator('#messages').getByText('CONTEXTUAL_STREAM_CONTINUES', { exact: false }).waitFor(); for (var width of [1280, 390]) { await page.setViewportSize({ width: width, height: 844 }); await page.waitForFunction(function () { var input = document.getElementById('input'); return input.scrollHeight <= input.clientHeight + 1 || getComputedStyle(input).overflowY === 'auto'; }); var stop = await page.locator('#generation-stop').boundingBox(); var mic = await page.locator('#stt-btn').boundingBox(); assert.ok(stop.x + stop.width <= mic.x, 'Native Stop has a separate slot at ' + width); assert.equal(await page.locator('#input').inputValue(), nativeDraft); await page.screenshot({ path: '/tmp/contextual-native-mate-' + width + '.png', animations: 'disabled' }); await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-dark'); }); await page.screenshot({ path: '/tmp/contextual-native-mate-dark-' + width + '.png', animations: 'disabled' }); await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-light'); }); } await page.locator('#generation-stop').click(); assert.equal(await page.evaluate(function () { return window.nativeStop.filter(function (event) { return event.type === 'stop'; }).length; }), 1); assert.equal(await frameWindow.evaluate(function () { return window.sent.filter(function (event) { return event.type === 'stop'; }).length; }), 0); await page.getByRole('button', { name: 'Back to project', exact: true }).click(); await page.waitForURL('**/p/alpha/'); await page.waitForFunction(function () { return document.getElementById('input').value === 'Original project draft' && !document.getElementById('input-wrapper').inert; }); assert.equal(await page.evaluate(async function () { return (await import('/p/alpha/modules/store.js')).store.get('activeSessionId'); }), projectSession); await trigger.click(); assert.equal(await input.inputValue(), nativeDraft); assert.equal(await frameWindow.evaluate(function () { return window.rendererIdentity; }), 'preserved'); assert.equal(await input.evaluate(function (input) { return input.readOnly; }), false); assert.equal(await frame.locator('#image-preview-bar img').count(), 1); assert.match(await frame.locator('#image-preview-bar').textContent(), /handoff.txt/); await frame.locator('#messages').getByText('CONTEXTUAL_STREAM_CONTINUES', { exact: false }).waitFor(); assert.equal(await frame.locator('.ask-user-container[data-tool-id="' + reviewId + '"]').getAttribute('class').then(function (value) { return value.includes('answered'); }), true); await trigger.click(); await page.waitForURL('**/p/' + info.sourceSlug + '/'); await page.locator('#contextual-workspace-back').waitFor(); await trigger.click(); await page.waitForURL('**/p/alpha/'); await trigger.click(); await page.request.get(origin + '/fixture/remove-contextual-session'); await frameWindow.evaluate(async function () { (await import('./modules/ws-ref.js')).getWs().close(); }); await frame.locator('#contextual-session-warning').waitFor(); await page.getByRole('button', { name: 'Open Clay', exact: true }).click(); assert.equal(page.url(), origin + '/p/alpha/', 'A removed exact conversation cannot silently open a different native session'); assert.equal(await input.inputValue(), nativeDraft); t.diagnostic('Actual route/native Mate shell, exact session, review decline, stream, draft/image/file transfer, direct file chooser handlers at both widths, Back and trigger cycle passed.'); });