157 lines
6.1 KiB
TypeScript
157 lines
6.1 KiB
TypeScript
// 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"]),
|
|
}))
|