mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
A room opened from the rail landed in the Inbox, beside what other networks carried in, and the home that named itself the workspace drew none of them. The rail's Team folders open a room on /home now, which resolves the key against the org's list and mounts Talk in place of its cards; the address carries it as ?room= the way /chat carries ?thread=. A Rooms card lists them and opens a new one, posted with the reader as its member because the platform adds nobody. The Inbox is what arrived and only that, and conversations.ts composes arrivals alone. The rows that left this site for hanzo.team and meet.hanzo.ai go, with their constants: Threads, Meet and Drafts & sent were links to a workspace this app now holds. New room takes their place. The Create menu's Channel is Room and opens here; Workflow opens /flows, the Automations room, so useFlow goes with it. HOME_ROOMS was always on since the messages ops shipped, and a switch with one position is not a switch. The gate is e2e/gates/room.spec.ts: the join, the poll, the pending row ahead of a slow answer, the POST body, the agent's answer on the next read, the roster beside the room, and a new room posted and opened. together.spec walks both doors and channels.spec answers a send with the row the platform stores. Co-authored-by: Hanzo Dev <[email protected]>
131 lines
5.4 KiB
TypeScript
131 lines
5.4 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { rail } from './rail'
|
|
import { serveExport } from './export'
|
|
import { signIn, answer } from '../session'
|
|
|
|
/**
|
|
* THE ORG'S CHANNELS, LISTED AND OPENED.
|
|
*
|
|
* This surface arrived quietly — `/v1/team/rooms` went from 404 to 403 — and a
|
|
* thing that appears without a gate can disappear the same way. What draws an
|
|
* open room is Home, resolving the rail's key (`team:<space>/<id>`) against
|
|
* `useTeamRooms()` and mounting `Talk` in place of its cards.
|
|
*
|
|
* WHAT THIS PROVES AND WHAT IT DOES NOT. The body below is the shape
|
|
* `components/workspace/rooms.ts` declares and reads (`page.rooms ?? []`), so
|
|
* this pins what the CLIENT expects: a parse that stops honouring `rooms`, a
|
|
* band that stops splitting on `direct`, or an archived room creeping back in
|
|
* all fail here. It cannot show that the server sends this, because that read
|
|
* answers only a validated principal and no gate holds one.
|
|
*/
|
|
const ROOMS = {
|
|
rooms: [
|
|
{ id: 'r1', space: 'acme', name: 'general', direct: false, private: false, archived: false, members: [] },
|
|
{ id: 'r2', space: 'acme', name: 'design', direct: false, private: false, archived: false, members: [] },
|
|
{ id: 'r3', space: 'acme', name: 'Ada', direct: true, private: true, archived: false, members: [] },
|
|
// Closed by the org. A sidebar that lists it makes leaving one meaningless.
|
|
{ id: 'r4', space: 'acme', name: 'retired', direct: false, private: false, archived: true, members: [] },
|
|
],
|
|
}
|
|
|
|
/**
|
|
* One message, stamped the way THIS surface stamps.
|
|
*
|
|
* `createdOn` in MILLIseconds — not `createdAt` in seconds, which is the
|
|
* Inbox's field and the Inbox's unit. Two message shapes in one workspace with
|
|
* two names and two scales: get it wrong and the row draws "Invalid Date",
|
|
* which is a mock's failure that reads exactly like the product's.
|
|
*/
|
|
const SAID = {
|
|
messages: [
|
|
{ id: 'm1', room: 'r1', author: 'ada', text: 'ship it', createdOn: Date.now() },
|
|
],
|
|
}
|
|
|
|
/** Answers a room's transcript, and a send with the row the platform stored. */
|
|
async function transcript(page: import('@playwright/test').Page): Promise<() => string | null> {
|
|
let posted: string | null = null
|
|
await page.route(
|
|
(url) => /\/v1\/team\/rooms\/[^/]+\/messages$/.test(url.pathname),
|
|
(route) => {
|
|
if (route.request().method() === 'POST') {
|
|
posted = route.request().postData()
|
|
const { text } = JSON.parse(posted ?? '{}') as { text?: string }
|
|
return route.fulfill({
|
|
status: 201,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ id: 'm2', room: 'r1', author: 'ada', text, createdOn: Date.now() }),
|
|
})
|
|
}
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(SAID),
|
|
})
|
|
},
|
|
)
|
|
return () => posted
|
|
}
|
|
|
|
test('the column bands channels and direct messages, and a press opens one', async ({ page }) => {
|
|
await signIn(page)
|
|
await answer(page, '/v1/team/rooms', ROOMS)
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/home`)
|
|
await rail(page, '[data-slot="sidebar"]')
|
|
|
|
const column = page.getByRole('button', { name: 'general', exact: true }).first()
|
|
await expect(column, 'the channels never arrived').toBeVisible({ timeout: 20_000 })
|
|
|
|
const body = page.locator('body')
|
|
await expect(body).toContainText('Channels')
|
|
await expect(body).toContainText('design')
|
|
// A conversation between two people is not a channel, and the column says so.
|
|
await expect(body).toContainText('Direct messages')
|
|
await expect(body).toContainText('Ada')
|
|
|
|
expect(await body.innerText(), 'a closed room is still listed').not.toContain('retired')
|
|
|
|
// PRESSING ONE OPENS IT. The list and the pane are the whole surface; a list
|
|
// that leads nowhere is what this was before the room had somewhere to go.
|
|
await column.click()
|
|
await expect(page.locator('main')).toContainText('#general', { timeout: 20_000 })
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|
|
|
|
test('a channel reads its transcript and can be written in', async ({ page }) => {
|
|
await signIn(page)
|
|
await answer(page, '/v1/team/rooms', ROOMS)
|
|
const posted = await transcript(page)
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/home`)
|
|
await rail(page, '[data-slot="sidebar"]')
|
|
await page.getByRole('button', { name: 'general', exact: true }).first().click()
|
|
|
|
const room = page.locator('main')
|
|
await expect(room).toContainText('#general', { timeout: 20_000 })
|
|
await expect(room).toContainText('ship it')
|
|
// Stamped, not "Invalid Date" — the row renders a clock time.
|
|
await expect(room).toContainText(/\d{1,2}:\d{2}/)
|
|
|
|
// A PANE THAT CAN SHOW A ROOM AND NOT ANSWER IN IT IS A DIRECTORY. The
|
|
// body carries the space as well as the text, because a room's address is
|
|
// the pair — two spaces of one org may each hold a #general.
|
|
const field = room.locator('textarea, input[type=text]').first()
|
|
await expect(field).toBeVisible()
|
|
await field.fill('hello team')
|
|
await field.press('Enter')
|
|
|
|
await expect.poll(() => posted(), { timeout: 10_000 }).toContain('"text":"hello team"')
|
|
expect(posted(), 'the room was addressed without its space').toContain('"space":"acme"')
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|