mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Every import, the catalog pin and the docs name @hanzo/app and github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and components/rooms.tsx is components/app.tsx.
105 lines
5.1 KiB
TypeScript
105 lines
5.1 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
||
import { ago, arrange, chats, PLAIN, runs, viewOf, type View } from '@hanzo/app/recent'
|
||
|
||
/**
|
||
* RECENTS IS ONE MODE'S LIST, ARRANGED BY A VIEW.
|
||
*
|
||
* Chat's rows come from its conversations and Dev's from its runs, never both.
|
||
* A view narrows by status and last activity, sorts, and groups; a kept view the
|
||
* mode's menu does not offer reads as the plain one.
|
||
*/
|
||
|
||
const now = new Date('2026-09-29T18:00:00Z')
|
||
const at = (hours: number) => new Date(now.getTime() - hours * 3_600_000).toISOString()
|
||
|
||
const runRows = runs([
|
||
{ id: 'r1', title: 'Add login', status: 'running', project: 'shop', createdAt: at(50), updatedAt: at(1) },
|
||
{ id: 'r2', title: 'Fix cart', status: 'done', repo: 'hanzoai/shop', createdAt: at(2), updatedAt: at(2) },
|
||
{ id: 'r3', title: 'Bump deps', status: 'error', project: '', createdAt: at(400), updatedAt: at(300) },
|
||
])
|
||
const chatRows = chats([
|
||
{ id: 'c1', title: 'Aurora', updatedAt: at(0.5) },
|
||
{ id: 'c2', title: 'Beacon', updatedAt: at(30) },
|
||
])
|
||
|
||
const view = (v: Partial<View>): View => ({ ...PLAIN, ...v })
|
||
const titles = (g: { title: string; rows: { title: string }[] }[]) => g.map((x) => [x.title, x.rows.map((r) => r.title)])
|
||
|
||
test('rows keep their mode: a chat is a chat and a run is a run', () => {
|
||
expect(chatRows.every((r) => r.kind === 'chat')).toBe(true)
|
||
expect(runRows.every((r) => r.kind === 'dev')).toBe(true)
|
||
expect(runRows.map((r) => r.project)).toEqual(['shop', 'shop', ''])
|
||
})
|
||
|
||
test('plain is one list, newest first, with no days', () => {
|
||
expect(titles(arrange(chatRows, PLAIN, now))).toEqual([['', ['Aurora', 'Beacon']]])
|
||
})
|
||
|
||
test('grouped by date, newest first, under the reader’s own days', () => {
|
||
const g = arrange(chatRows, view({ group: 'date' }), now)
|
||
expect(g.map((x) => x.title)).toEqual(['Today', 'Yesterday'])
|
||
expect(g[0]!.rows.map((r) => r.title)).toEqual(['Aurora'])
|
||
})
|
||
|
||
test('narrowed by status, by last activity and by words', () => {
|
||
expect(titles(arrange(runRows, view({ status: 'done', group: 'none' }), now))).toEqual([['', ['Fix cart']]])
|
||
expect(titles(arrange(runRows, view({ since: 'week', group: 'none' }), now))).toEqual([['', ['Add login', 'Fix cart']]])
|
||
})
|
||
|
||
test('grouped by state and by project, sorted by creation or title', () => {
|
||
expect(titles(arrange(runRows, view({ group: 'state' }), now))).toEqual([
|
||
['Running', ['Add login']],
|
||
['Done', ['Fix cart']],
|
||
['Failed', ['Bump deps']],
|
||
])
|
||
expect(titles(arrange(runRows, view({ group: 'project', sort: 'created' }), now))).toEqual([
|
||
['shop', ['Fix cart', 'Add login']],
|
||
['No project', ['Bump deps']],
|
||
])
|
||
expect(titles(arrange(runRows, view({ group: 'none', sort: 'title' }), now))).toEqual([['', ['Add login', 'Bump deps', 'Fix cart']]])
|
||
})
|
||
|
||
test('a pinned conversation comes first, under Pinned, whatever the grouping', () => {
|
||
const pinned = chats([
|
||
{ id: 'c1', title: 'Aurora', updatedAt: at(0.5) },
|
||
{ id: 'c2', title: 'Beacon', updatedAt: at(30), pinned: true },
|
||
])
|
||
expect(titles(arrange(pinned, view({ group: 'date' }), now))).toEqual([
|
||
['Pinned', ['Beacon']],
|
||
['Today', ['Aurora']],
|
||
])
|
||
expect(titles(arrange(pinned, PLAIN, now))).toEqual([
|
||
['Pinned', ['Beacon']],
|
||
['', ['Aurora']],
|
||
])
|
||
})
|
||
|
||
test('an archived conversation reads under the Archived status alone', () => {
|
||
const away = chats([{ id: 'c3', title: 'Comet', updatedAt: at(2), archived: true }])
|
||
expect(titles(arrange(away, view({ status: 'archived', group: 'none' }), now))).toEqual([['', ['Comet']]])
|
||
expect(viewOf('chat', { status: 'archived', group: 'starred' })).toEqual({ ...PLAIN, status: 'archived' })
|
||
})
|
||
|
||
test('a kept view reads only what the mode’s menu offers', () => {
|
||
expect(viewOf('chat', { status: 'done', group: 'state', sort: 'created', since: 'day' })).toEqual({ ...PLAIN, since: 'day' })
|
||
expect(viewOf('dev', { status: 'done', group: 'state', sort: 'created' })).toEqual({ status: 'done', since: 'any', group: 'state', sort: 'created' })
|
||
expect(viewOf('dev', 'garbage')).toEqual(PLAIN)
|
||
expect(viewOf('dev', null)).toEqual(PLAIN)
|
||
})
|
||
|
||
test('grouped by date and sorted by title, the days still read in order', () => {
|
||
expect(arrange(chatRows, view({ group: 'date', sort: 'title' }), now).map((g) => g.title)).toEqual(['Today', 'Yesterday'])
|
||
const old = chats([{ id: 'c3', title: 'Aardvark', updatedAt: at(900) }, { id: 'c1', title: 'Zed', updatedAt: at(1) }])
|
||
expect(arrange(old, view({ group: 'date', sort: 'title' }), now).map((g) => g.title)).toEqual(['Today', 'Older'])
|
||
})
|
||
|
||
|
||
test('a row says how long ago it was last active, in the fewest letters', () => {
|
||
const minutes = (m: number) => new Date(now.getTime() - m * 60_000).toISOString()
|
||
expect(['', 'soon'].map((x) => ago(x, now))).toEqual(['', ''])
|
||
expect([0, 0.5, 2, 12, 59].map((m) => ago(minutes(m), now))).toEqual(['now', 'now', '2m', '12m', '59m'])
|
||
expect([60, 180, 23 * 60 + 59].map((m) => ago(minutes(m), now))).toEqual(['1h', '3h', '23h'])
|
||
expect([1, 6, 7, 29, 30, 364, 365, 800].map((d) => ago(minutes(d * 1440), now))).toEqual(['1d', '6d', '1w', '4w', '1mo', '12mo', '1y', '2y'])
|
||
expect(ago(new Date(now.getTime() + 60_000).toISOString(), now), 'a clock a little ahead reads as now').toBe('now')
|
||
})
|