var test = require('node:test'), assert = require('node:assert/strict'), cp = require('node:child_process'), path = require('node:path'), playwright = require('playwright'); test('global Ask enters a fresh exact contextual Clay session without inline chat or duplicate delivery', async function (t) { var child = cp.fork(path.join(__dirname, 'fixtures/contextual-clay-server.cjs'), [], { env: Object.assign({}, process.env, { CLAY_CONTEXT_ACCESS_FIXTURE: '1' }), 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 (value) { clearTimeout(timer); resolve(value); }); }); var browser = await playwright.chromium.launch(); t.after(async function () { await browser.close(); }); var page = await browser.newPage({ viewport: { width: 1280, height: 800 }, reducedMotion: 'reduce' }); page.setDefaultTimeout(10000); var errors = []; page.on('pageerror', function (error) { errors.push(error.message); }); 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; }); await page.evaluate(async function () { window.askStore = (await import('/p/alpha/modules/store.js')).store; window.askSocket = await import('/p/alpha/modules/ws-ref.js'); }); var projectSession = await page.evaluate(function () { return window.askStore.get('activeSessionId'); }); await page.locator('#input').fill('Project draft remains'); await page.locator('.icon-strip-brand').click(); var original = page.frameLocator('#contextual-clay iframe[data-canonical="true"]'); await original.locator('#messages').getByText('Isolated canonical integration conversation.', { exact: true }).waitFor(); await original.locator('#input-wrapper:not([inert])').waitFor(); await original.locator('#input').fill('Prior pinned Clay draft'); await page.request.get(origin + '/fixture/stream'); await original.locator('#messages').getByText('CONTEXTUAL_STREAM_CONTINUES', { exact: false }).waitFor(); await page.locator('[data-clay-close]').click(); async function ask(text, keyboard) { await page.getByRole('button', { name: 'Search or ask Clay', exact: true }).click(); var input = page.locator('.cmd-palette-input'); await input.fill(text); await page.locator('.is-ask-clay').waitFor(); if (keyboard) { await input.press('ArrowUp'); await input.press('Enter'); } else await page.locator('.is-ask-clay').evaluate(function (node) { node.click(); node.click(); }); await page.locator('.cmd-palette:not(.project-switcher)').waitFor({ state: 'hidden' }); assert.equal(await page.locator('.search-clay-chat').count(), 0); } async function receipt() { await page.waitForFunction(function () { var value = window.askStore.get('contextualSearchAsk'); return value && value.status === 'acknowledged'; }); return page.evaluate(function () { var value = window.askStore.get('contextualSearchAsk'); return { sessionId: value.sessionId, requestId: value.requestId, clientMessageId: value.clientMessageId }; }); } function frame(id) { return page.frameLocator('#contextual-clay iframe[data-canonical="true"][data-session-id="' + id + '"]'); } async function sessions() { return (await page.request.get(origin + '/fixture/contextual-asks')).json(); } await ask('First fresh Ask from search', true); var first = await receipt(); assert.equal(new URL(page.url()).pathname, '/p/alpha/'); assert.equal(await page.evaluate(function () { return window.askStore.get('activeSessionId'); }), projectSession); assert.equal(await page.locator('#input').inputValue(), 'Project draft remains'); await frame(first.sessionId).locator('#messages').getByText('First fresh Ask from search', { exact: true }).waitFor(); assert.equal((await sessions()).length, 1); await page.locator('[data-mate-history] summary').click(); await page.getByRole('button', { name: 'Canonical integration source', exact: false }).click(); await page.waitForFunction(function () { var request = window.askStore.get('contextualConversationRequest'); return request && request.status === 'done'; }); await page.locator('[data-mate-history]:not([open])').waitFor(); assert.equal(await page.frameLocator('#contextual-clay iframe:visible').locator('#input').inputValue(), 'Prior pinned Clay draft'); await page.frameLocator('#contextual-clay iframe:visible').locator('#messages').getByText('CONTEXTUAL_STREAM_CONTINUES', { exact: false }).waitFor(); await page.locator('[data-mate-history] summary').click(); await page.getByRole('button', { name: 'First fresh Ask from search', exact: false }).click(); await page.waitForFunction(function () { var request = window.askStore.get('contextualConversationRequest'); return request && request.status === 'done'; }); await page.locator('[data-mate-history]:not([open])').waitFor(); await ask('Second fresh Ask from search', false); var second = await receipt(); assert.notEqual(first.sessionId, second.sessionId); assert.equal((await sessions()).length, 2); for (var entry of await sessions()) assert.equal(entry.messages.filter(function (message) { return message.type === 'user_message'; }).length, 1); var proposed = page.request.get(origin + '/fixture/ask-propose?session=' + second.sessionId); var exactReview = frame(second.sessionId).locator('.ask-user-container:not(.answered)'); await exactReview.locator('.product-review-action').waitFor(); assert.match(await exactReview.locator('.product-review-action').textContent(), /Beta destination/); await exactReview.getByText('Keep unchanged', { exact: true }).click(); await exactReview.locator('.ask-user-submit').click(); assert.equal((await (await proposed).json()).status, 'cancelled'); assert.equal(new URL(page.url()).pathname, '/p/alpha/'); var question = page.request.get(origin + '/fixture/ask-question?session=' + second.sessionId); var review = frame(second.sessionId).locator('.ask-user-container:not(.answered)'); await review.waitFor(); await review.getByText('Keep unchanged', { exact: true }).click(); await review.locator('.ask-user-submit').click(); assert.equal((await (await question).json()).status, 'submitted'); await page.request.get(origin + '/fixture/ask-stream?session=' + second.sessionId); await frame(second.sessionId).locator('#messages').getByText('NEW_ASK_STREAM_IN_PANE', { exact: false }).waitFor(); var currentFrame = page.frames().find(function (item) { return item.url().includes('&session=' + second.sessionId); }); await currentFrame.evaluate(async function () { window.askWire = []; var socket = (await import('./modules/ws-ref.js')).getWs(), send = socket.send.bind(socket); socket.send = function (data) { window.askWire.push(JSON.parse(data)); return send(data); }; }); await frame(second.sessionId).locator('#send-btn.stop').click(); assert.equal(await currentFrame.evaluate(function () { return window.askWire.filter(function (message) { return message.type === 'stop'; }).length; }), 1); await page.locator('[data-clay-expand]').click(); await page.waitForURL('**/p/' + info.sourceSlug + '/'); await page.waitForFunction(function (id) { return window.askStore.get('activeSessionId') === id && window.askStore.get('contextualWorkspace').stage === 'workspace'; }, second.sessionId); await page.locator('#messages').getByText('Second fresh Ask from search', { exact: true }).waitFor(); await page.locator('#contextual-workspace-back').click(); await page.waitForURL('**/p/alpha/'); await page.waitForFunction(function () { return !window.askStore.get('contextualWorkspace'); }); assert.equal(await page.locator('#input').inputValue(), 'Project draft remains'); // Both native background kinds retain their exact active session and draft. for (var target of [{ slug: info.sourceSlug, sessionId: info.sourceSessionId }, info.ordinaryMates[0]]) { await page.evaluate(async function (target) { (await import('/p/alpha/modules/project-session-navigation.js')).openProjectSession(target.slug, target.sessionId); }, target); await page.waitForFunction(function (target) { return window.askStore.get('currentSlug') === target.slug && window.askStore.get('activeSessionId') === target.sessionId && !window.askStore.get('replayingHistory'); }, target); await page.locator('#input').fill('Native background draft'); await ask('Ask over native ' + target.slug, false); var over = await receipt(); assert.equal(await page.evaluate(function () { return window.askStore.get('activeSessionId'); }), target.sessionId); assert.equal(await page.locator('#input').inputValue(), 'Native background draft'); assert.equal(new URL(page.url()).pathname, '/p/' + target.slug + '/'); await page.locator('[data-clay-expand]').click(); await page.waitForFunction(function (id) { return window.askStore.get('activeSessionId') === id && window.askStore.get('contextualWorkspace').stage === 'workspace'; }, over.sessionId); await page.locator('#contextual-workspace-back').click(); await page.waitForFunction(function (target) { return window.askStore.get('currentSlug') === target.slug && window.askStore.get('activeSessionId') === target.sessionId && !window.askStore.get('contextualWorkspace'); }, target); assert.equal(await page.locator('#input').inputValue(), 'Native background draft'); } await page.context().setOffline(true); await page.waitForFunction(function () { return !window.askStore.get('connected'); }); await ask('Retain this offline question', false); await page.locator('[data-contextual-ask-status]').filter({ hasText: 'Reconnect, then retry' }).waitFor(); await page.locator('[data-ask-stop]').click(); await page.context().setOffline(false); await page.waitForFunction(function () { return window.askStore.get('connected'); }); assert.equal((await sessions()).length, 4, 'Stop/offline reconnect creates no session or message'); await page.locator('[data-ask-retry]').click(); var retried = await receipt(); assert.equal((await sessions()).length, 5); var retriedFrame = page.frames().find(function (item) { return item.url().includes('&session=' + retried.sessionId); }); await retriedFrame.evaluate(async function () { (await import('./modules/ws-ref.js')).getWs().close(); }); await frame(retried.sessionId).locator('#input-wrapper:not([inert])').waitFor(); assert.equal((await sessions()).find(function (entry) { return entry.id === retried.sessionId; }).messages.filter(function (entry) { return entry.type === 'user_message'; }).length, 1); // An uncertain create reply can be safely looked up, but Stop forbids submit. await page.evaluate(function () { var socket = window.askSocket.getWs(), send = socket.send.bind(socket); window.releaseAsk = send; socket.send = function (data) { if (JSON.parse(data).type === 'home_clay_ask') window.heldAsk = data; else send(data); }; }); await ask('Stop while creating the new conversation', false); await page.locator('[data-ask-stop]').click(); await page.evaluate(function () { window.releaseAsk(window.heldAsk); }); await page.waitForTimeout(150); var stopped = (await sessions()).find(function (entry) { return entry.text === 'Stop while creating the new conversation'; }); assert.ok(stopped); assert.equal(stopped.messages.filter(function (entry) { return entry.type === 'user_message'; }).length, 0); assert.equal(await page.evaluate(function () { return window.askStore.get('contextualSearchAsk').status; }), 'stopped'); await page.evaluate(function () { window.askSocket.getWs().send = window.releaseAsk; }); await page.locator('[data-ask-retry]').click(); var resumed = await receipt(); assert.equal(resumed.sessionId, stopped.id); assert.equal((await sessions()).length, 6); await ask('First fresh Ask from search', false); var repeatedText = await receipt(); assert.notEqual(repeatedText.sessionId, first.sessionId, 'A separate explicit Ask with identical text still creates a fresh session'); assert.equal((await sessions()).length, 7); await page.evaluate(function () { var socket = window.askSocket.getWs(), send = socket.send.bind(socket); window.releaseAsk = send; socket.send = function (data) { if (JSON.parse(data).type === 'home_clay_ask') window.heldAsk = data; else send(data); }; }); await ask('Retain query when Clay disappears', false); await page.evaluate(function () { window.savedAskMates = window.askStore.get('cachedMatesList'); window.askStore.set({ cachedMatesList: window.savedAskMates.filter(function (mate) { return mate.builtinKey !== 'clay'; }) }); }); await page.locator('[data-contextual-ask-status]').filter({ hasText: 'Clay is no longer available' }).waitFor(); await page.evaluate(function () { window.releaseAsk(window.heldAsk); }); await page.waitForTimeout(150); var unavailable = (await sessions()).find(function (entry) { return entry.text === 'Retain query when Clay disappears'; }); assert.equal(unavailable.messages.filter(function (entry) { return entry.type === 'user_message'; }).length, 0); assert.equal(await page.evaluate(function () { return window.askStore.get('contextualSearchAsk').text; }), 'Retain query when Clay disappears'); await page.evaluate(function () { window.askSocket.getWs().send = window.releaseAsk; window.askStore.set({ cachedMatesList: window.savedAskMates }); }); await page.locator('[data-ask-retry]').click(); await receipt(); assert.equal((await sessions()).length, 8); await page.evaluate(function () { var socket = window.askSocket.getWs(), receive = socket.onmessage; socket.onmessage = function (event) { if (JSON.parse(event.data).type === 'home_clay_contextual_session') { window.heldAskReply = event; window.releaseAskReply = function () { receive.call(socket, event); }; } else receive.call(socket, event); }; }); await ask('Retain query after background navigation', false); await page.waitForFunction(function () { return !!window.heldAskReply; }); await page.getByRole('link', { name: 'BI', exact: true }).click(); await page.waitForURL('**/p/beta/'); await page.waitForFunction(function () { return window.askStore.get('connected'); }); await page.evaluate(function () { window.releaseAskReply(); }); assert.notEqual(await page.evaluate(function () { return window.askStore.get('contextualSearchAsk').status; }), 'acknowledged'); assert.equal((await sessions()).find(function (entry) { return entry.text === 'Retain query after background navigation'; }).messages.filter(function (entry) { return entry.type === 'user_message'; }).length, 0); await page.getByRole('button', { name: 'Search or ask Clay', exact: true }).click(); await page.locator('.cmd-palette-input').fill(''); await page.locator('.is-retry-clay-ask').click(); await receipt(); assert.equal((await sessions()).length, 9); await page.setViewportSize({ width: 390, height: 844 }); await page.screenshot({ path: '/tmp/contextual-search-fresh-narrow.png', animations: 'disabled' }); assert.deepEqual(errors, []); t.diagnostic('Central keyboard/Ask action -> fresh exact pane; duplicate activation once; two asks distinct; previous draft/stream accessible; actual review/stream/Stop confined to pane; native expansion/Back exact; project and both native backgrounds preserved; offline Stop/no replay, reconnect once and uncertain creation explicit resume reuse same session.'); });