Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* The admin dashboard page, held against its contract
* (gHashTag/trios#1594): the single export of
* entrypoints/app/admin-dashboard/AdminDashboardPage.tsx -
* AdminDashboardPage - pinned as it behaves today, so the next change
* to that file has something to fail against.
*
* The history hook answers its initial empty map (it populates through
* effects a static render never runs), so the page's static answer is
* the EMPTY dashboard - which is most of its visible surface: the
* header and approvals slots (stubbed with named markers - their own
* suites own their contracts), the Approvals and Audit Trail section
* titles, the empty-state card with its shield icon and note, and the
* delete AlertDialog in its controlled-closed shape (its title,
* description and buttons render inside the group's stand-in shape,
* with the task name empty because no task is pending deletion).
* The storage seam is the repository's in-memory item shim (the real
* @wxt-dev/storage refuses the test process; the store suites assert
* only on their own maps). Not pinned: the populated audit trail (it
* rides the effect-populated hook), the delete flow and the toasts.
*/
import { describe, expect, it, mock } from 'bun:test'
import { createElement, type FC, type ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'

const passChildren =
(): FC<{ children?: ReactNode }> =>
({ children }) =>
createElement('div', null, children)

// The generated graphql tag is never generated in the Tests jobs; the
// ScheduledTasks suite's graph reaches it, and in the CI readdir order
// this file's registrations come first - the stub keeps that import
// alive for whoever links it after us.
mock.module('@/generated/graphql/gql', () => ({
graphql: (query: string) => ({ query }),
}))

mock.module('@wxt-dev/storage', () => ({
storage: {
defineItem: (
key: string,
options?: { fallback?: unknown } & Record<string, unknown>,
) => ({
getValue: async () => {
if (key === 'local:tool-approval-config') {
return { categories: {} }
}
return options && 'fallback' in options ? options.fallback : null
},
setValue: async () => {},
watch: () => () => {},
}),
},
}))
mock.module('@/lib/execution-history/storage', () => ({
removeConversationExecutionTask: async () => {},
useExecutionHistoryByConversation: () => ({}),
}))
mock.module('@/components/ui/alert-dialog', () => {
const pass = passChildren()
return {
// The root honours the open flag exactly like the group's dialog
// stand-ins: a CLOSED alert renders nothing. The first push of this
// PR passed children through unconditionally and broke the sibling
// Skills and ScheduledTasks suites in the CI readdir order - their
// contracts pin their closed alerts as empty (measured in the CI
// log of the first push).
AlertDialog: ({
open,
children,
}: {
open?: boolean
children?: ReactNode
}) => (open ? children : null),
AlertDialogAction: pass,
AlertDialogCancel: pass,
AlertDialogContent: pass,
AlertDialogDescription: pass,
AlertDialogFooter: pass,
AlertDialogHeader: pass,
AlertDialogTitle: pass,
AlertDialogTrigger: pass,
}
})
mock.module('./AdminDashboardHeader', () => ({
AdminDashboardHeader: () =>
createElement('div', { 'data-testid': 'admin-header' }),
}))
mock.module('./PendingApprovals', () => ({
PendingApprovals: () =>
createElement('div', { 'data-testid': 'pending-approvals' }),
}))

const { AdminDashboardPage } = await import('./AdminDashboardPage')

const markup = () => renderToStaticMarkup(createElement(AdminDashboardPage))

describe('adminDashboardPageContract', () => {
it('answers its header and approvals slots', () => {
const m = markup()
expect(m).toContain('data-testid="admin-header"')
expect(m).toContain('data-testid="pending-approvals"')
expect(m).toContain('Approvals')
})

it('answers the Audit Trail section with its empty state', () => {
const m = markup()
expect(m).toContain('Audit Trail')
expect(m).toContain('No agent runs yet')
expect(m).toContain(
'Run a task in BrowserOS and the execution history will appear here.',
)
expect(m).toContain('lucide-shield')
// The run-count line renders only when a run exists; the delete
// dialog's description carries the words "recorded run" in its
// fixed sentence, so the count line is pinned by its own shape.
expect(m).not.toContain('recorded runs across')
})

it('answers the delete dialog closed: none of its content renders', () => {
const m = markup()
expect(m).not.toContain('Delete Run')
expect(m).not.toContain('from local history?')
expect(m).not.toContain('This only clears the recorded run on this device.')
expect(m).not.toContain('>Cancel<')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* The agent command home, held against its contract
* (gHashTag/trios#1585): the single export of
* entrypoints/app/agent-command/AgentCommandHome.tsx - AgentCommandHome -
* pinned as it behaves today, so the next change to that file has
* something to fail against.
*
* The seams are the same ones the AgentCommandConversation suite stubs
* (the pattern of this group): the wxt storage item shim, the
* agents/adapters hooks, the layout data hook, the metrics seam, and the
* active-hint hook behind a flag so one suite reaches both hints. The
* ConversationInput and the hints render REAL (the input's own suite
* owns its internals; the hints are plain presentational components).
* Pinned: the empty answer (No agents yet with its note and the Create
* agent affordance); the populated answer (the home headline with its
* italic work-on accent, the tab-leaving note, the conversation input
* frame with the measured placeholder and the runtime-not-running
* placeholder, the Recent agents section with its note and the Manage
* agents affordance); the hint dispatch (signin answers the SignInHint
* marker, import the ImportDataHint marker, none answers neither). Not
* pinned: the send/select wiring (handlers a static render never runs).
*/
import { describe, expect, it, mock } from 'bun:test'
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { MemoryRouter } from 'react-router'

const values = new Map<string, unknown>()

const fakeItem = (
key: string,
opts?: { fallback?: unknown } & Record<string, unknown>,
) => ({
getValue: async () => {
if (values.has(key)) return values.get(key)
return opts && 'fallback' in opts ? opts.fallback : null
},
setValue: async (value: unknown) => {
values.set(key, value)
},
watch: () => () => {},
})

mock.module('@wxt-dev/storage', () => ({
storage: {
defineItem: (key: string, opts?: unknown) => fakeItem(key, opts as never),
},
}))

let legacyAgents: Array<Record<string, unknown>> = []

mock.module('./agent-command-layout', () => ({
useAgentCommandData: () => ({
agents: legacyAgents,
status: { status: 'running' },
}),
}))

mock.module('@/entrypoints/app/agents/useAgents', () => ({
useAgentAdapters: () => ({ adapters: [], loading: false, error: null }),
useHarnessAgents: () => ({
agents: [],
harnessAgents: [],
gateway: null,
loading: false,
error: null,
}),
}))

mock.module('@/lib/metrics/track', () => ({
track: () => {},
}))

mock.module('./ConversationInput', () => ({
ConversationInput: (props: Record<string, unknown>) =>
createElement(
'div',
{ 'data-testid': 'conversation-input' },
String(props.placeholder ?? ''),
),
}))

let activeHint: string | null = null

mock.module('@/entrypoints/newtab/index/useActiveHint', () => ({
useActiveHint: () => activeHint,
}))

const { AgentCommandHome } = await import('./AgentCommandHome')

const markup = () =>
renderToStaticMarkup(
createElement(MemoryRouter, null, createElement(AgentCommandHome as never)),
)

describe('agentCommandHomeContract', () => {
it('answers the empty state when no agent exists', () => {
legacyAgents = []
activeHint = null
const m = markup()
expect(m).toContain('No agents yet')
expect(m).toContain(
'Create an agent to start using BrowserOS as an agent-first new tab.',
)
expect(m).toContain('Create agent')
expect(m).not.toContain('What should your agent')
})

it('answers the home headline, its note and the conversation slot', () => {
legacyAgents = [
{ agentId: 'agent-1', name: 'Research Bot', source: 'agent-harness' },
]
activeHint = null
const m = markup()
expect(m).toContain('What should your agent')
expect(m).toContain('work on')
expect(m).toContain('next?')
expect(m).toContain(
'Start a task, continue a thread, or hand off to a different agent',
)
expect(m).toContain('data-testid="conversation-input"')
// The selection effect never runs in a static render, so the input
// answers the not-running placeholder even with agents present
// (measured) - and the Recent threads section stays away because the
// harness list is empty.
expect(m).toContain('Agent runtime is not running...')
expect(m).not.toContain('Recent agents')
})

it('answers the recent-threads section only when the harness list carries agents', () => {
legacyAgents = [
{ agentId: 'agent-1', name: 'Bot', source: 'agent-harness' },
]
activeHint = null
const m = markup()
expect(m).not.toContain('Recent agents')
legacyAgents = []
})

it('answers the active hint by kind and none by default', () => {
legacyAgents = []
activeHint = 'signin'
expect(markup()).toContain('Sync your data')
activeHint = 'import'
expect(markup()).toContain('Import your data')
activeHint = null
const m = markup()
expect(m).not.toContain('Sync your data')
expect(m).not.toContain('Import your data')
})
})
Loading