Files
aiturk-hermes-ide/apps/desktop/e2e/bot-mode-tab-shows-bot-name.spec.ts

138 lines
4.2 KiB
TypeScript

import fs from 'node:fs'
import path from 'node:path'
import {
buildAppEnv,
createSandbox,
launchDesktop,
type MockBackendFixture,
waitForAppReady,
writeEnvFile,
writeMockProviderConfig
} from './fixtures'
import { MOCK_REPLY, startMockServer } from './mock-server'
import { RealSessionBuilder } from './real-session-builder'
import { expect, test } from './test'
// Every bot's canonical chat is STORED under the same title ("Bot Chat" — the
// name the gateway resolves it by), so the main tab strip captioned every open
// bot chat identically and two bots' tabs were indistinguishable (#99152). The
// tab must read the bot's display name while the stored title stays canonical.
type Page = MockBackendFixture['page']
let fixture: MockBackendFixture | null = null
async function openBots(page: Page): Promise<void> {
const tab = page
.getByRole('button', { name: 'Bots', exact: true })
.or(page.getByRole('tab', { name: 'Bots', exact: true }))
.first()
await tab.click()
await expect(page.getByRole('button', { name: 'New bot or group chat' })).toBeVisible()
}
async function openUntil(action: () => Promise<void>, expected: () => Promise<void>, attempts = 3): Promise<void> {
for (let attempt = 1; ; attempt += 1) {
await action()
try {
await expected()
return
} catch (error) {
if (attempt >= attempts) {
throw error
}
}
}
}
async function seedBot(hermesHome: string, mockUrl: string, name: string): Promise<void> {
const dir = path.join(hermesHome, 'profiles', name)
fs.mkdirSync(dir, { recursive: true })
writeMockProviderConfig(dir, mockUrl)
writeEnvFile(dir)
const builder = await RealSessionBuilder.start(dir)
try {
await builder.createSession({ title: 'Bot Chat', turns: [`Hello ${name}`] })
} finally {
await builder.close()
}
}
/** Every tab caption in the main strip (the main `workspace` tab + tiles). */
function mainStripTabTitles(page: Page): Promise<string[]> {
return page.evaluate(() =>
[...document.querySelectorAll<HTMLElement>('[data-zone-tabstrip="grp-main"] [data-tree-tab]')].map(element =>
(element.textContent ?? '').trim()
)
)
}
test.beforeAll(async () => {
const mock = await startMockServer()
const sandbox = createSandbox('bots-tabname')
writeMockProviderConfig(sandbox.hermesHome, mock.url)
writeEnvFile(sandbox.hermesHome)
await seedBot(sandbox.hermesHome, mock.url, 'alpha')
await seedBot(sandbox.hermesHome, mock.url, 'beta')
const { app, page } = await launchDesktop(buildAppEnv(sandbox))
fixture = {
app,
page,
mock,
mockUrl: mock.url,
sandbox,
cleanup: async () => {
await app.close().catch(() => undefined)
await mock.close()
sandbox.cleanup()
}
}
await waitForAppReady(fixture, 120_000)
})
test.afterAll(async () => {
await fixture?.cleanup()
fixture = null
})
test("an open Bot Chat's tab reads the bot's name, not the canonical 'Bot Chat' title", async () => {
test.setTimeout(300_000)
const page = fixture!.page
await openBots(page)
const alphaRow = page.getByRole('button', { name: /^alpha\b/i }).filter({ visible: true }).first()
await expect(alphaRow).toBeVisible({ timeout: 30_000 })
await openUntil(
() => alphaRow.click(),
() =>
expect(page.getByText('Hello alpha', { exact: true }).filter({ visible: true }).first()).toBeVisible({
timeout: 45_000
})
)
// A `+` side thread beside the Bot Chat gives the main zone a tab strip —
// the surface where every bot chat used to read "Bot Chat".
await page.keyboard.press('Control+t')
const composer = page.locator('[data-slot="composer-root"] [contenteditable="true"]').filter({ visible: true }).first()
await expect(composer).toBeVisible({ timeout: 15_000 })
await composer.click()
await composer.fill('hello alpha thread')
await page.keyboard.press('Enter')
await expect(page.getByText(MOCK_REPLY).filter({ visible: true }).first()).toBeVisible({ timeout: 60_000 })
await expect.poll(() => mainStripTabTitles(page), { timeout: 15_000 }).toHaveLength(2)
const captions = await mainStripTabTitles(page)
expect(captions.some(caption => /alpha/i.test(caption))).toBe(true)
expect(captions.some(caption => /bot chat/i.test(caption))).toBe(false)
})