| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- /** @jsxImportSource @opentui/solid */
- import { expect, test } from "bun:test"
- import { testRender } from "@opentui/solid"
- import type { Event, GlobalEvent } from "@kirincode-ai/sdk/v2"
- import { onMount } from "solid-js"
- import { ProjectProvider } from "../../../src/context/project"
- import { SDKProvider } from "../../../src/context/sdk"
- import { DataProvider, useData } from "../../../src/context/data"
- import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
- import { TestTuiContexts } from "../../fixture/tui-environment"
- async function wait(fn: () => boolean, timeout = 2000) {
- const start = Date.now()
- while (!fn()) {
- if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
- await Bun.sleep(10)
- }
- }
- function global(payload: Event): GlobalEvent {
- return { directory, project: "proj_test", payload }
- }
- function emitEvent(events: ReturnType<typeof createEventSource>, payload: Event) {
- events.emit(global(payload))
- }
- test("refreshes resources into reactive getters", async () => {
- const events = createEventSource()
- const location = {
- directory,
- project: { id: "proj_test", directory },
- }
- const calls = createFetch((url) => {
- if (url.pathname === "/api/session/ses_test")
- return json({
- data: {
- id: "ses_test",
- projectID: "proj_test",
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: 0, updated: 0 },
- title: "Test session",
- location: { directory },
- },
- })
- if (url.pathname === "/api/agent")
- return json({
- location,
- data: [{ id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }],
- })
- return undefined
- }, events)
- let data!: ReturnType<typeof useData>
- let ready!: () => void
- const mounted = new Promise<void>((resolve) => {
- ready = resolve
- })
- function Probe() {
- data = useData()
- onMount(ready)
- return <box />
- }
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <Probe />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await mounted
- expect(data.location.default()).toEqual({ directory })
- expect(data.session.get("ses_test")).toBeUndefined()
- expect(data.location.agent.list(location)).toBeUndefined()
- await data.session.refresh("ses_test")
- await data.location.agent.refresh()
- expect(data.session.get("ses_test")?.title).toBe("Test session")
- expect(data.location.default()).toEqual({ directory, workspaceID: undefined })
- expect(data.location.agent.list(location)?.map((agent) => agent.id)).toEqual(["build"])
- } finally {
- app.renderer.destroy()
- }
- })
- test("refreshes integrations after integration updates", async () => {
- const events = createEventSource()
- const requests = { integration: 0, model: 0, provider: 0 }
- const calls = createFetch((url) => {
- if (url.pathname === "/api/model") {
- requests.model++
- return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
- }
- if (url.pathname === "/api/provider") {
- requests.provider++
- return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
- }
- if (url.pathname !== "/api/integration") return
- requests.integration++
- return json({
- location: { directory, project: { id: "proj_test", directory } },
- data:
- requests.integration === 1
- ? []
- : [
- {
- id: "openai",
- name: "OpenAI",
- methods: [{ type: "key" }],
- },
- ],
- })
- }, events)
- let data!: ReturnType<typeof useData>
- let ready!: () => void
- const mounted = new Promise<void>((resolve) => {
- ready = resolve
- })
- function Probe() {
- data = useData()
- onMount(ready)
- return <box />
- }
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <Probe />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await mounted
- await wait(() => data.location.integration.list() !== undefined)
- expect(data.location.integration.list()).toEqual([])
- const before = { ...requests }
- emitEvent(events, { id: "evt_integration", type: "integration.updated", properties: {} })
- await wait(() => data.location.integration.list()?.length === 1)
- await wait(() => requests.model > before.model && requests.provider > before.provider)
- expect(data.location.integration.list()?.[0]).toMatchObject({ id: "openai", name: "OpenAI" })
- } finally {
- app.renderer.destroy()
- }
- })
- test("refreshes effective catalog data after catalog updates", async () => {
- const events = createEventSource()
- const requests = { model: 0, provider: 0 }
- const calls = createFetch((url) => {
- if (url.pathname === "/api/model") {
- requests.model++
- return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
- }
- if (url.pathname === "/api/provider") {
- requests.provider++
- return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
- }
- }, events)
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <box />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await wait(() => requests.model > 0 && requests.provider > 0)
- const before = { ...requests }
- emitEvent(events, { id: "evt_catalog", type: "catalog.updated", properties: {} })
- await wait(() => requests.model > before.model && requests.provider > before.provider)
- } finally {
- app.renderer.destroy()
- }
- })
- test("refreshes references after updates", async () => {
- const events = createEventSource()
- let requests = 0
- const calls = createFetch((url) => {
- if (url.pathname !== "/api/reference") return
- requests++
- return json({
- location: { directory, project: { id: "proj_test", directory } },
- data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }],
- })
- }, events)
- let data!: ReturnType<typeof useData>
- let ready!: () => void
- const mounted = new Promise<void>((resolve) => {
- ready = resolve
- })
- function Probe() {
- data = useData()
- onMount(ready)
- return <box />
- }
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <Probe />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await mounted
- await wait(() => requests === 1)
- emitEvent(events, { id: "evt_reference_1", type: "reference.updated", properties: {} })
- await wait(() => data.location.reference.list()?.length === 1)
- expect(data.location.reference.list()?.[0]?.name).toBe("docs")
- } finally {
- app.renderer.destroy()
- }
- })
- test("settles pending tools when a live failure arrives", async () => {
- const events = createEventSource()
- const calls = createFetch(undefined, events)
- let sync!: ReturnType<typeof useData>
- let ready!: () => void
- const mounted = new Promise<void>((resolve) => {
- ready = resolve
- })
- function Probe() {
- sync = useData()
- onMount(ready)
- return <box />
- }
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <Probe />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await mounted
- emitEvent(events, {
- id: "evt_agent_1",
- type: "session.next.agent.switched",
- properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
- })
- emitEvent(events, {
- id: "evt_model_1",
- type: "session.next.model.switched",
- properties: {
- sessionID: "session-1",
- messageID: "msg_model_1",
- timestamp: 0,
- model: { id: "model-1", providerID: "provider-1" },
- },
- })
- emitEvent(events, {
- id: "evt_step_started_1",
- type: "session.next.step.started",
- properties: {
- sessionID: "session-1",
- assistantMessageID: "msg_explicit_assistant_9",
- timestamp: 1,
- agent: "build",
- model: { id: "model-1", providerID: "provider-1" },
- },
- })
- emitEvent(events, {
- id: "evt_input_1",
- type: "session.next.tool.input.started",
- properties: {
- sessionID: "session-1",
- assistantMessageID: "msg_explicit_assistant_9",
- timestamp: 2,
- callID: "call-1",
- name: "bash",
- },
- })
- emitEvent(events, {
- id: "evt_called_1",
- type: "session.next.tool.called",
- properties: {
- sessionID: "session-1",
- timestamp: 2,
- assistantMessageID: "msg_explicit_assistant_9",
- callID: "call-1",
- tool: "bash",
- input: {},
- provider: { executed: false, metadata: { fake: { call: true } } },
- },
- })
- emitEvent(events, {
- id: "evt_failed_1",
- type: "session.next.tool.failed",
- properties: {
- sessionID: "session-1",
- timestamp: 3,
- assistantMessageID: "msg_explicit_assistant_9",
- callID: "call-1",
- error: { type: "unknown", message: "aborted" },
- provider: { executed: false, metadata: { fake: { result: true } } },
- },
- })
- await wait(() => {
- const assistant = sync.session.message.list("session-1")?.[0]
- return (
- assistant?.type === "assistant" &&
- assistant.content[0]?.type === "tool" &&
- assistant.content[0].state.status === "error"
- )
- })
- const assistant = sync.session.message.list("session-1")?.[0]
- expect(assistant?.type).toBe("assistant")
- if (assistant?.type !== "assistant") return
- expect(assistant.id).toBe("msg_explicit_assistant_9")
- const tool = assistant.content[0]
- expect(tool?.type).toBe("tool")
- if (tool?.type !== "tool") return
- expect(tool.state.status).toBe("error")
- if (tool.state.status !== "error") return
- expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" })
- expect(tool.state.input).toEqual({})
- expect(tool.state.structured).toEqual({})
- expect(tool.state.content).toEqual([])
- expect(tool.provider).toEqual({
- executed: false,
- metadata: { fake: { call: true } },
- resultMetadata: { fake: { result: true } },
- })
- expect((sync.session.message.list("session-1") ?? []).map((message) => message.type)).toEqual([
- "assistant",
- "model-switched",
- "agent-switched",
- ])
- } finally {
- app.renderer.destroy()
- }
- })
- test("renders admitted prompts only after they become model-visible", async () => {
- const events = createEventSource()
- const calls = createFetch(undefined, events)
- let sync!: ReturnType<typeof useData>
- let ready!: () => void
- const mounted = new Promise<void>((resolve) => {
- ready = resolve
- })
- function Probe() {
- sync = useData()
- onMount(ready)
- return <box />
- }
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <Probe />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await mounted
- emitEvent(events, {
- id: "evt_admitted_1",
- type: "session.next.prompt.admitted",
- properties: {
- sessionID: "session-1",
- messageID: "msg_user_1",
- timestamp: 0,
- prompt: { text: "hello" },
- delivery: "steer",
- },
- })
- expect(sync.session.message.list("session-1") ?? []).toEqual([])
- emitEvent(events, {
- id: "evt_prompted_1",
- type: "session.next.prompted",
- properties: {
- sessionID: "session-1",
- messageID: "msg_user_1",
- timestamp: 0,
- prompt: { text: "hello" },
- delivery: "steer",
- },
- })
- await wait(() => sync.session.message.list("session-1")?.length === 1)
- const message = sync.session.message.list("session-1")?.[0]
- expect(message?.type).toBe("user")
- if (message?.type !== "user") return
- expect(message).toMatchObject({ id: "msg_user_1", text: "hello" })
- } finally {
- app.renderer.destroy()
- }
- })
- test("projects live context updates with their message ID", async () => {
- const events = createEventSource()
- const calls = createFetch(undefined, events)
- let sync!: ReturnType<typeof useData>
- let ready!: () => void
- const mounted = new Promise<void>((resolve) => {
- ready = resolve
- })
- function Probe() {
- sync = useData()
- onMount(ready)
- return <box />
- }
- const app = await testRender(() => (
- <TestTuiContexts>
- <SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
- <ProjectProvider>
- <DataProvider>
- <Probe />
- </DataProvider>
- </ProjectProvider>
- </SDKProvider>
- </TestTuiContexts>
- ))
- try {
- await mounted
- emitEvent(events, {
- id: "evt_context_1",
- type: "session.next.context.updated",
- properties: {
- sessionID: "session-1",
- messageID: "msg_context_1",
- timestamp: 1,
- text: "Updated context",
- },
- })
- await wait(() => sync.session.message.list("session-1")?.length === 1)
- expect(sync.session.message.list("session-1")?.[0]).toMatchObject({
- id: "msg_context_1",
- type: "system",
- text: "Updated context",
- })
- } finally {
- app.renderer.destroy()
- }
- })
|