@@ -0,0 +1,156 @@
|
||||
// The agent page on F10: its loader reads through the caller and returns plain data with every label
|
||||
// translated, and its view renders what reached the browser, the run page's first paint being the seed of
|
||||
// the live island. A path that is not one run is the kernel's 404, which is what the shell answers.
|
||||
|
||||
import { AppError, buildRegistry, Container, forbidden, type TransportCaller } from "@conusai-kit/core"
|
||||
import { builtInCatalogues, createI18n, uiLabels } from "@conusai-kit/i18n"
|
||||
import { createDataClient, DataProvider } from "@conusai-kit/ui/data"
|
||||
import { UiLabelsProvider } from "@conusai-kit/ui/primitives"
|
||||
import { renderToStaticMarkup as renderMarkup } from "react-dom/server"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { appModules } from "../../index.js"
|
||||
import { modulePages } from "../../pages.js"
|
||||
import type { ApprovalView, RunView } from "../contract/index.js"
|
||||
import { agentPage } from "../ui/pages.js"
|
||||
|
||||
const kitStrings = uiLabels(createI18n("en", builtInCatalogues))
|
||||
const renderToStaticMarkup = (node: React.ReactNode) =>
|
||||
renderMarkup(<UiLabelsProvider labels={kitStrings}>{node}</UiLabelsProvider>)
|
||||
|
||||
const catalogues = buildRegistry([...appModules]).catalogues
|
||||
const i18n = createI18n("en", {
|
||||
en: { agent: catalogues.en?.agent ?? {}, app: { failed: "That did not work: {detail}" } },
|
||||
})
|
||||
const slugs = { org: "acme", workspace: "ws", project: "p" }
|
||||
|
||||
const run: RunView = {
|
||||
runId: "01a19940-0000-7000-0000-000000000001",
|
||||
goal: "Order the parts",
|
||||
status: "running",
|
||||
failureCode: null,
|
||||
failureDetail: null,
|
||||
plan: [
|
||||
{
|
||||
key: "read",
|
||||
label: "Read",
|
||||
tool: "documents.read",
|
||||
args: {},
|
||||
needsApproval: false,
|
||||
status: "pending",
|
||||
attempts: 0,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
usageCostMicrocents: 120,
|
||||
usageSteps: 3,
|
||||
percent: 0.5,
|
||||
cancelRequested: false,
|
||||
answer: null,
|
||||
answerProvenance: null,
|
||||
actorId: "user-1",
|
||||
createdAt: "2026-11-02T10:00:00.000Z",
|
||||
startedAt: "2026-11-02T10:00:01.000Z",
|
||||
finishedAt: null,
|
||||
}
|
||||
|
||||
const approval: ApprovalView = {
|
||||
runId: run.runId,
|
||||
stepKey: "read",
|
||||
goal: run.goal,
|
||||
tool: "orders.create",
|
||||
arguments: { total: 4200 },
|
||||
reviewUrl: null,
|
||||
createdAt: "2026-11-02T10:00:00.000Z",
|
||||
}
|
||||
|
||||
const callerOf = (answers: Record<string, unknown>, asked: string[] = []): TransportCaller =>
|
||||
({
|
||||
call: async (contract: { name: string }) => {
|
||||
asked.push(contract.name)
|
||||
const answer = answers[contract.name]
|
||||
if (answer instanceof Error) throw answer
|
||||
return answer
|
||||
},
|
||||
}) as unknown as TransportCaller
|
||||
|
||||
const load = (segments: string[], answers: Record<string, unknown>, asked?: string[]) =>
|
||||
agentPage.load({ slugs, segments, search: {}, caller: callerOf(answers, asked), i18n, container: new Container() })
|
||||
|
||||
/** The page as the shell renders it: the loader's data through JSON, as the browser receives it, then the view. */
|
||||
const render = async (segments: string[], answers: Record<string, unknown>) => {
|
||||
const data = await load(segments, answers)
|
||||
expect(JSON.parse(JSON.stringify(data))).toStrictEqual(data)
|
||||
const { View } = agentPage
|
||||
return renderToStaticMarkup(
|
||||
<DataProvider client={createDataClient({ fetch: async () => new Response(null, { status: 501 }) })}>
|
||||
<View slugs={slugs} segments={segments} locale="en" data={JSON.parse(JSON.stringify(data))} />
|
||||
</DataProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
const lists = { "agent.run.list": { runs: [run] }, "agent.approval.list": { approvals: [approval] } }
|
||||
|
||||
describe("the agent page", () => {
|
||||
it("is listed for the shell", async () => {
|
||||
await expect(modulePages.agent?.()).resolves.toBe(agentPage)
|
||||
})
|
||||
|
||||
it("lists the runs, each with its status in words, its progress and a link to it", async () => {
|
||||
const html = await render([], lists)
|
||||
const link = html.match(/<a\b[^>]*>Order the parts<\/a>/)?.[0] ?? ""
|
||||
expect(link.match(/class="([^"]*)"/)?.[1]?.split(/\s+/)).toContain("kit-row-link")
|
||||
expect(link).toContain(`href="/acme/ws/p/agent/${run.runId}"`)
|
||||
expect(link).toContain('data-slot="row-link"')
|
||||
expect(html).toContain('<progress value="0.5" max="1">50%</progress>')
|
||||
expect(html).toContain("user-1")
|
||||
expect(html).toContain("orders.create")
|
||||
})
|
||||
|
||||
it("says when there is no run", async () => {
|
||||
const data = await load([], { "agent.run.list": { runs: [] }, "agent.approval.list": { approvals: [] } })
|
||||
if (data.kind !== "list") throw new Error("not the list")
|
||||
expect(data.runs).toEqual([])
|
||||
expect(await render([], { ...lists, "agent.run.list": { runs: [] } })).toContain(data.runLabels.empty)
|
||||
})
|
||||
|
||||
it("renders one run from its status, the seed of the live island", async () => {
|
||||
const asked: string[] = []
|
||||
const html = await render([run.runId], { "agent.run.status": run })
|
||||
await load([run.runId], { "agent.run.status": run }, asked)
|
||||
expect(asked).toEqual(["agent.run.status"])
|
||||
expect(html).toContain(">Order the parts</h1>")
|
||||
expect(html).toContain('href="/acme/ws/p/agent"')
|
||||
expect(html).toContain("<dt>Usage</dt><dd>3 steps, 120 microcents</dd>")
|
||||
expect(html).toContain('data-live="agent.run"')
|
||||
})
|
||||
|
||||
it("answers a path that is not one run with the kernel's 404, without reading", async () => {
|
||||
const asked: string[] = []
|
||||
const refused = load([run.runId, "extra"], lists, asked)
|
||||
await expect(refused).rejects.toBeInstanceOf(AppError)
|
||||
await expect(refused).rejects.toMatchObject({ status: 404 })
|
||||
expect(asked).toEqual([])
|
||||
})
|
||||
|
||||
it("hands a procedure's refusal on, as the AppError the shell answers", async () => {
|
||||
const call = vi.fn(async () => {
|
||||
throw forbidden("this page needs agent.run.read")
|
||||
})
|
||||
await expect(
|
||||
agentPage.load({
|
||||
slugs,
|
||||
segments: [],
|
||||
search: {},
|
||||
caller: { call } as unknown as TransportCaller,
|
||||
i18n,
|
||||
container: new Container(),
|
||||
}),
|
||||
).rejects.toMatchObject({ status: 403 })
|
||||
})
|
||||
})
|
||||
|
||||
// This journey requires explicit optional-module admission.
|
||||
vi.mock("../../inclusion.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../inclusion.js")>()),
|
||||
...(await import("../../../test/support/native-readiness.js")).selectedModules(["agent"]),
|
||||
}))
|
||||
Reference in New Issue
Block a user