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('two ordinary Mates and canonical retain isolated conversations through popover, strip hint and native workspace', 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 (v) { output += v; }); child.stderr.on('data', function (v) { output += v; }); 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; var mates = info.ordinaryMates; await page.goto(origin + '/fixture/login?to=alpha'); await page.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; }); var preference = { left: 420, top: 80, width: 510, height: 620 }; assert.equal((await page.request.put(origin + '/api/user/contextual-clay-geometry', { headers: { 'X-Clay-Settings': '1' }, data: preference })).status(), 200); await page.reload(); await page.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; }); await page.evaluate(async function () { window.mateStore = (await import('/p/alpha/modules/store.js')).store; }); var projectSession = await page.evaluate(function () { return window.mateStore.get('activeSessionId'); }); await page.locator('#input').fill('Original project writing'); function icon(mate) { return page.locator('.icon-strip-project-mate[data-mate-id="' + mate.id + '"]'); } function pane(mate) { return page.frameLocator('#contextual-clay iframe[data-mate-id="' + mate.id + '"]'); } async function ready(mate, index) { await pane(mate).locator('#messages').getByText('ORDINARY_MATE_' + index + '_PRIVATE_HISTORY', { exact: false }).waitFor(); await pane(mate).locator('#input-wrapper:not([inert])').waitFor(); } await icon(mates[0]).click(); await ready(mates[0], 0); var hint = page.locator('#contextual-mate-hint'); assert.match(await hint.textContent(), /Click again to open Arch/); assert.equal(await icon(mates[0]).getAttribute('aria-describedby'), 'contextual-mate-hint'); assert.equal(await page.locator('#contextual-clay strong').textContent(), mates[0].name); var box = await page.locator('#contextual-clay').boundingBox(); var badge = await hint.boundingBox(); assert.ok(badge.x + badge.width <= box.x - 6 || badge.y + badge.height <= box.y - 6); await pane(mates[0]).locator('#input').fill('Arch owns this draft only.'); var heldUpload; var uploadReady; var started = new Promise(function (resolve) { uploadReady = resolve; }); var uploadPattern = '**/p/' + mates[0].slug + '/api/upload'; await page.route(uploadPattern, function (route) { heldUpload = route; uploadReady(); }); await pane(mates[0]).locator('#contextual-attach-btn').click(); var choose = page.waitForEvent('filechooser'); await pane(mates[0]).locator('#attach-file-btn').click(); await (await choose).setFiles({ name: 'arch-private.txt', mimeType: 'text/plain', buffer: Buffer.from('Arch attachment') }); await started; await icon(mates[1]).click(); await ready(mates[1], 1); assert.equal(await pane(mates[1]).locator('#image-preview-bar').getByText('arch-private.txt').count(), 0); await heldUpload.continue(); await page.unroute(uploadPattern); await icon(mates[0]).click(); await pane(mates[0]).locator('#image-preview-bar').getByText('arch-private.txt', { exact: false }).waitFor(); var question = page.request.get(origin + '/fixture/ordinary-question?index=0'); var card = pane(mates[0]).locator('.ask-user-container:not(.answered)'); await card.waitFor(); var questionId = await card.getAttribute('data-tool-id'); await icon(mates[1]).click(); await ready(mates[1], 1); assert.equal(await pane(mates[1]).locator('#input').getAttribute('placeholder'), 'Message this Mate…'); assert.equal(await pane(mates[1]).locator('#input').getAttribute('aria-label'), 'Message ' + mates[1].name); assert.equal(await pane(mates[1]).locator('#input').inputValue(), ''); assert.equal(await pane(mates[1]).locator('.ask-user-container').count(), 0); assert.equal(await pane(mates[1]).locator('#image-preview-bar').getByText('arch-private.txt').count(), 0); await pane(mates[1]).locator('#input').fill('Second Mate owns a different draft.'); assert.equal(await page.locator('#contextual-clay iframe:visible').count(), 1); assert.ok((await hint.boundingBox()).x + (await hint.boundingBox()).width < 1280); await icon(mates[1]).hover(); assert.equal(await page.locator('.icon-strip-tooltip').count(), 0, 'Persistent hint replaces the competing hover tooltip'); await page.screenshot({ path: '/tmp/contextual-mates-long-name-light.png', animations: 'disabled' }); var visualFrame = page.frames().find(function (frame) { return frame.url().includes(mates[1].slug); }); await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-dark'); }); await visualFrame.evaluate(async function () { (await import('./modules/theme.js')).applyTheme('clay-dark'); }); await page.screenshot({ path: '/tmp/contextual-mates-long-name-dark.png', animations: 'disabled' }); await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-light'); }); await visualFrame.evaluate(async function () { (await import('./modules/theme.js')).applyTheme('clay-light'); }); await page.locator('.icon-strip-brand').click(); var canonical = page.frameLocator('#contextual-clay iframe[data-canonical="true"]'); await canonical.locator('#messages').getByText('Isolated canonical integration conversation.', { exact: true }).waitFor(); await canonical.locator('#input-wrapper:not([inert])').waitFor(); assert.equal(await canonical.locator('#input').inputValue(), ''); await canonical.locator('#input').fill('Canonical private draft.'); assert.equal(await canonical.locator('#messages').getByText('ORDINARY_MATE_0_PRIVATE_HISTORY', { exact: false }).count(), 0); for (var index of [0, 1]) { for (var fresh of ['true', 'false']) { var catalog = await (await page.request.get(origin + '/fixture/ordinary-catalog?index=' + index + '&fresh=' + fresh)).json(); assert.ok(Array.isArray(catalog.tools)); assert.ok(catalog.canonicalNames.length >= 28); catalog.canonicalNames.forEach(function (name) { assert.equal(catalog.tools.some(function (tool) { return tool.name === name; }), false, 'Ordinary Mate lacks canonical ' + name); }); } } var ordinaryWindow = page.frames().find(function (frame) { return frame.url().includes(mates[1].slug); }); assert.equal(await ordinaryWindow.evaluate(async function () { return (await import('./modules/contextual-clay-session.js')).contextualScreenReference(); }), null); assert.equal(await pane(mates[0]).locator('#messages').getByText('Isolated canonical integration conversation.', { exact: true }).count(), 0); var foreign = await browser.newContext(); assert.equal((await foreign.request.get(origin + '/p/' + mates[0].slug + '/', { headers: { Cookie: 'relay_auth_user=fixture-foreign-token' } })).status(), 403); await foreign.close(); await icon(mates[0]).click(); assert.equal(await pane(mates[0]).locator('#input').inputValue(), 'Arch owns this draft only.'); assert.equal(await card.getAttribute('data-tool-id'), questionId); assert.equal(await page.locator('#input').inputValue(), 'Original project writing'); await icon(mates[0]).click(); await page.waitForURL('**/p/' + mates[0].slug + '/'); await page.waitForFunction(function () { return document.body.classList.contains('contextual-clay-native') && !document.getElementById('input-wrapper').inert; }); assert.equal(await page.evaluate(function () { return window.mateStore.get('activeSessionId'); }), mates[0].sessionId); assert.equal(await page.locator('#input').inputValue(), 'Arch owns this draft only.'); assert.equal(await pane(mates[0]).locator('#input').inputValue(), ''); assert.equal(await hint.isVisible(), false); assert.equal(await page.locator('body.mate-workspace-active').count(), 1); assert.match(await page.locator('#image-preview-bar').textContent(), /arch-private/); var nativeCard = page.locator('.ask-user-container[data-tool-id="' + questionId + '"]'); await nativeCard.getByText('Tests', { exact: true }).click(); await nativeCard.locator('.ask-user-submit').click(); assert.equal((await (await question).json()).status, 'submitted'); await page.locator('#input').fill('Edited in native Arch workspace.'); await icon(mates[1]).click(); assert.equal(new URL(page.url()).pathname, '/p/' + mates[0].slug + '/'); assert.equal(await page.locator('#input').inputValue(), 'Edited in native Arch workspace.'); assert.equal(await page.evaluate(function () { return window.mateStore.get('activeSessionId'); }), mates[0].sessionId); assert.equal(await pane(mates[1]).locator('#input').inputValue(), 'Second Mate owns a different draft.'); await page.locator('[data-clay-close]').click(); assert.equal(new URL(page.url()).pathname, '/p/' + mates[0].slug + '/'); await page.waitForFunction(function () { return document.body.classList.contains('contextual-clay-native'); }); assert.equal(await page.locator('#input').inputValue(), 'Edited in native Arch workspace.'); await page.locator('#contextual-workspace-back').click(); await page.waitForURL('**/p/alpha/'); await page.waitForFunction(function () { return document.getElementById('input').value === 'Original project writing'; }); assert.equal(await page.evaluate(function () { return window.mateStore.get('activeSessionId'); }), projectSession); await icon(mates[1]).click(); assert.equal(await pane(mates[1]).locator('#input').inputValue(), 'Second Mate owns a different draft.'); await hint.click(); await page.waitForURL('**/p/' + mates[1].slug + '/'); await page.waitForFunction(function () { return document.body.classList.contains('contextual-clay-native'); }); assert.equal(await page.locator('#input').inputValue(), 'Second Mate owns a different draft.'); await page.locator('#contextual-workspace-back').click(); await page.waitForURL('**/p/alpha/'); await page.waitForFunction(function () { return !window.mateStore.get('contextualWorkspace'); }); await icon(mates[0]).focus(); await page.keyboard.press('Enter'); assert.equal(await pane(mates[0]).locator('#input').inputValue(), 'Edited in native Arch workspace.'); await pane(mates[0]).locator('#input').press('Escape'); assert.equal(await hint.isVisible(), false); assert.equal(await icon(mates[0]).getAttribute('aria-describedby'), null); await icon(mates[0]).click({ button: 'right' }); await page.locator('.project-ctx-menu').waitFor(); assert.equal(await page.locator('#contextual-clay').isVisible(), false); await page.keyboard.press('Escape'); await icon(mates[0]).focus(); await page.keyboard.press('Shift+F10'); await page.locator('.project-ctx-menu').waitFor(); await page.keyboard.press('Escape'); assert.equal(await page.locator('#contextual-clay').isVisible(), false); await icon(mates[1]).click(); await page.setViewportSize({ width: 390, height: 844 }); await page.waitForFunction(function () { return !!document.querySelector('.contextual-mate-mobile-trigger'); }); assert.equal(await icon(mates[1]).isVisible(), true); assert.equal(await hint.isVisible(), true); badge = await hint.boundingBox(); box = await page.locator('#contextual-clay').boundingBox(); assert.ok(badge.x >= 46 && badge.x + badge.width <= 390 && badge.y + badge.height <= box.y); await page.screenshot({ path: '/tmp/contextual-mates-narrow-light.png', animations: 'disabled' }); await page.evaluate(async function () { (await import('/p/alpha/modules/theme.js')).applyTheme('clay-dark'); }); var secondFrame = page.frames().find(function (frame) { return frame.url().includes(mates[1].slug); }); await secondFrame.evaluate(async function () { (await import('./modules/theme.js')).applyTheme('clay-dark'); }); await page.screenshot({ path: '/tmp/contextual-mates-narrow-dark.png', animations: 'disabled' }); assert.deepEqual((await (await page.request.get(origin + '/api/user/contextual-clay-geometry')).json()).geometry, preference); await icon(mates[1]).click(); await page.waitForURL('**/p/' + mates[1].slug + '/'); await page.locator('#contextual-workspace-back').click(); await page.waitForURL('**/p/alpha/'); await page.setViewportSize({ width: 1280, height: 800 }); await page.waitForFunction(function () { return !window.mateStore.get('contextualWorkspace'); }); await icon(mates[1]).click(); await page.evaluate(function (slug) { window.savedProjectRegistry = window.mateStore.get('projectsHubList'); window.mateStore.set({ projectsHubList: window.savedProjectRegistry.map(function (project) { return project.slug === slug ? Object.assign({}, project, { contentAccess: false }) : project; }) }); }, mates[1].slug); assert.equal(await page.locator('#contextual-clay').isVisible(), false); assert.equal(await hint.isVisible(), false); await page.evaluate(function () { window.mateStore.set({ projectsHubList: window.savedProjectRegistry }); }); await page.evaluate(async function (id) { var list = window.mateStore.get('cachedMatesList'); window.savedMateRegistry = list; window.mateStore.set({ cachedMatesList: list.map(function (mate) { return mate.id === id ? Object.assign({}, mate, { createdBy: 'foreign' }) : mate; }) }); (await import('/p/alpha/modules/project-mate-navigation.js')).activateContextualMate(id); }, mates[1].id); assert.equal(await page.locator('#contextual-clay').isVisible(), false); await page.evaluate(function () { window.mateStore.set({ cachedMatesList: window.savedMateRegistry }); }); await icon(mates[0]).click(); await page.evaluate(function (id) { var items = window.mateStore.get('cachedMatesList'); window.mateStore.set({ cachedMatesList: items.map(function (mate) { return mate.id === id ? Object.assign({}, mate, { archived: true }) : mate; }) }); }, mates[0].id); assert.equal(await page.locator('#contextual-clay').isVisible(), false); assert.equal(await hint.isVisible(), false); await page.evaluate(async function (id) { await (await import('/p/alpha/modules/project-mate-navigation.js')).activateContextualMate(id); }, mates[0].id); assert.equal(await page.locator('#contextual-clay').isVisible(), false); await icon(mates[1]).click(); await page.evaluate(function (id) { window.mateStore.set({ cachedMatesList: window.mateStore.get('cachedMatesList').filter(function (mate) { return mate.id !== id; }) }); }, mates[1].id); assert.equal(await page.locator('#contextual-clay').isVisible(), false); await page.locator('.icon-strip-brand').click(); assert.equal(await canonical.locator('#input').inputValue(), 'Canonical private draft.'); var direct = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await direct.goto(origin + '/fixture/login?to=alpha'); await direct.goto(origin + '/p/' + mates[1].slug + '/'); await direct.locator('#messages').getByText('ORDINARY_MATE_1_PRIVATE_HISTORY', { exact: false }).waitFor(); await direct.evaluate(async function () { window.directStore = (await import('./modules/store.js')).store; }); await direct.waitForFunction(function (session) { return window.directStore.get('activeSessionId') === session && !window.directStore.get('replayingHistory'); }, mates[1].sessionId); await direct.locator('#input').fill('Writing in a directly opened native Mate.'); var directIcon = direct.locator('.icon-strip-project-mate[data-mate-id="' + mates[1].id + '"]'); await directIcon.click(); var directPane = direct.frameLocator('#contextual-clay iframe'); await directPane.locator('#input').waitFor(); await direct.waitForFunction(function () { return document.getElementById('input-wrapper').inert && document.getElementById('input').value === ''; }); await direct.waitForFunction(function () { var frame = document.querySelector('#contextual-clay iframe'); return frame && frame.contentDocument.getElementById('input') && frame.contentDocument.getElementById('input').value === 'Writing in a directly opened native Mate.'; }); assert.equal(await directPane.locator('#input').inputValue(), 'Writing in a directly opened native Mate.'); await directPane.locator('#input').fill('Edited in direct-entry popover.'); await direct.locator('[data-clay-close]').click(); assert.equal(await direct.locator('#input').inputValue(), 'Edited in direct-entry popover.'); assert.equal(await direct.locator('#input-wrapper').evaluate(function (el) { return el.inert; }), false); await directIcon.click(); await directPane.locator('#input-wrapper:not([inert])').waitFor(); await directIcon.click(); await direct.waitForFunction(function () { return document.body.classList.contains('contextual-clay-native'); }); assert.equal(await direct.locator('#input').inputValue(), 'Edited in direct-entry popover.'); await direct.locator('#contextual-workspace-back').click(); await direct.waitForURL('**/p/alpha/'); await direct.goto(origin + '/p/' + mates[1].slug + '/'); await direct.locator('#messages').getByText('ORDINARY_MATE_1_PRIVATE_HISTORY', { exact: false }).waitFor(); await directIcon.click(); await direct.evaluate(async function (id) { var state = (await import('./modules/store.js')).store; state.set({ cachedMatesList: state.get('cachedMatesList').filter(function (mate) { return mate.id !== id; }) }); }, mates[1].id); assert.equal(await direct.locator('#contextual-clay').isVisible(), false); assert.equal(await direct.locator('#input-wrapper').evaluate(function (el) { return el.inert; }), false, 'Removed native origin cannot trap project navigation'); await direct.close(); var touchContext = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true }); var touch = await touchContext.newPage(); await touch.goto(origin + '/fixture/login?to=alpha'); await touch.waitForFunction(function () { return !document.querySelector('.icon-strip-brand').disabled; }); await touch.getByRole('button', { name: 'Chat', exact: true }).tap(); await touch.locator('.mobile-chat-chip[data-mate-id="' + mates[1].id + '"]').tap(); var touchPane = touch.frameLocator('#contextual-clay iframe'); await touchPane.locator('#messages').getByText('ORDINARY_MATE_1_PRIVATE_HISTORY', { exact: false }).waitFor(); assert.match(await touch.locator('#contextual-mate-hint').textContent(), /^Tap again to open /); var touchIcon = touch.locator('.contextual-mate-mobile-trigger'); assert.ok((await touchIcon.boundingBox()).width >= 44); await touchPane.locator('#input-wrapper:not([inert])').waitFor(); await touchPane.locator('#input').fill('Touch presentation retains its exact Mate.'); await touchIcon.tap(); await touch.waitForURL('**/p/' + mates[1].slug + '/'); await touch.waitForFunction(function () { return document.body.classList.contains('contextual-clay-native'); }); assert.equal(await touch.locator('#input').inputValue(), 'Touch presentation retains its exact Mate.'); await touch.locator('#contextual-workspace-back').tap(); await touch.waitForURL('**/p/alpha/'); await touchContext.close(); assert.deepEqual(errors, []); t.diagnostic('Two ordinary Mates plus canonical: isolated sessions/drafts/attachments/question, first/second clicks, native routes/Back, pill actions and placement, keyboard/right-click, narrow icon, archived/deleted refusal and ordinary canonical-catalog exclusion.'); });