fix: stabilize v2 runtime behavior
This commit is contained in:
parent
ac2a78391f
commit
655adbf46e
35 changed files with 742 additions and 425 deletions
|
|
@ -1,12 +1,12 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import Notifications from "../../../../src/feature-plugins/system/notifications"
|
||||
import type { Event, PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2"
|
||||
import type { PermissionRequest, QuestionRequest, Session, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import type { TuiAttentionNotifyInput } from "@opencode-ai/plugin/tui"
|
||||
import { createTuiPluginApi } from "../../../fixture/tui-plugin"
|
||||
|
||||
async function setup() {
|
||||
const notifications: TuiAttentionNotifyInput[] = []
|
||||
const handlers = new Map<Event["type"], ((event: Event) => void)[]>()
|
||||
const handlers = new Map<V2Event["type"], ((event: V2Event) => void)[]>()
|
||||
const session = (id: string, title: string, parentID?: string): Session => ({
|
||||
id,
|
||||
title,
|
||||
|
|
@ -33,9 +33,12 @@ async function setup() {
|
|||
},
|
||||
},
|
||||
event: {
|
||||
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => {
|
||||
on: <Type extends V2Event["type"]>(
|
||||
type: Type,
|
||||
handler: (event: Extract<V2Event, { type: Type }>) => void,
|
||||
) => {
|
||||
const list = handlers.get(type) ?? []
|
||||
const wrapped = handler as (event: Event) => void
|
||||
const wrapped = handler as (event: V2Event) => void
|
||||
list.push(wrapped)
|
||||
handlers.set(type, list)
|
||||
return () => {
|
||||
|
|
@ -58,7 +61,7 @@ async function setup() {
|
|||
|
||||
return {
|
||||
notifications,
|
||||
emit(event: Event) {
|
||||
emit(event: V2Event) {
|
||||
for (const handler of handlers.get(event.type) ?? []) handler(event)
|
||||
},
|
||||
}
|
||||
|
|
@ -83,11 +86,11 @@ function permission(id: string, sessionID = "session"): PermissionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
function stepStarted(id: string, sessionID = "session"): Event {
|
||||
function stepStarted(id: string, sessionID = "session"): V2Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.started",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
|
|
@ -97,11 +100,11 @@ function stepStarted(id: string, sessionID = "session"): Event {
|
|||
}
|
||||
}
|
||||
|
||||
function stepEnded(id: string, sessionID = "session", finish = "stop"): Event {
|
||||
function stepEnded(id: string, sessionID = "session", finish = "stop"): V2Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.ended",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
|
|
@ -112,11 +115,11 @@ function stepEnded(id: string, sessionID = "session", finish = "stop"): Event {
|
|||
}
|
||||
}
|
||||
|
||||
function stepFailed(id: string, sessionID = "session"): Event {
|
||||
function stepFailed(id: string, sessionID = "session"): V2Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.failed",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
|
|
@ -143,8 +146,8 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("notifies for question and permission requests with blurred notifications and always-on sounds", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "permission.asked", properties: permission("permission-1") })
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "permission.asked", data: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([questionNotification, permissionNotification])
|
||||
})
|
||||
|
|
@ -152,23 +155,23 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("dedupes pending questions and permissions until they are resolved", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-2", type: "question.asked", data: question("question-1") })
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
type: "question.replied",
|
||||
properties: { sessionID: "session", requestID: "question-1", answers: [] },
|
||||
data: { sessionID: "session", requestID: "question-1", answers: [] },
|
||||
})
|
||||
harness.emit({ id: "event-4", type: "question.asked", properties: question("question-1") })
|
||||
harness.emit({ id: "event-4", type: "question.asked", data: question("question-1") })
|
||||
|
||||
harness.emit({ id: "event-5", type: "permission.asked", properties: permission("permission-1") })
|
||||
harness.emit({ id: "event-6", type: "permission.asked", properties: permission("permission-1") })
|
||||
harness.emit({ id: "event-5", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-6", type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({
|
||||
id: "event-7",
|
||||
type: "permission.replied",
|
||||
properties: { sessionID: "session", requestID: "permission-1", reply: "once" },
|
||||
data: { sessionID: "session", requestID: "permission-1", reply: "once" },
|
||||
})
|
||||
harness.emit({ id: "event-8", type: "permission.asked", properties: permission("permission-1") })
|
||||
harness.emit({ id: "event-8", type: "permission.asked", data: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
questionNotification,
|
||||
|
|
@ -198,7 +201,7 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1", "subagent") })
|
||||
harness.emit({ id: "event-1", type: "question.asked", data: question("question-1", "subagent") })
|
||||
harness.emit(stepStarted("event-2", "subagent"))
|
||||
harness.emit(stepEnded("event-3", "subagent"))
|
||||
|
||||
|
|
@ -242,13 +245,13 @@ describe("internal notifications TUI plugin", () => {
|
|||
harness.emit({
|
||||
id: "event-2",
|
||||
type: "session.error",
|
||||
properties: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
||||
data: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
||||
})
|
||||
harness.emit(stepStarted("event-3", "timeout"))
|
||||
harness.emit({
|
||||
id: "event-4",
|
||||
type: "session.error",
|
||||
properties: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
|
||||
data: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
|
||||
})
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
|
|
|
|||
14
packages/tui/test/cli/tui/collapse-tool-output.test.ts
Normal file
14
packages/tui/test/cli/tui/collapse-tool-output.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { collapseToolOutput } from "../../../src/util/collapse-tool-output"
|
||||
|
||||
test("limits command input and output to the same line budget", () => {
|
||||
const command = Array.from({ length: 8 }, (_, index) => `command ${index + 1}`).join("\n")
|
||||
const output = Array.from({ length: 4 }, (_, index) => `output ${index + 1}`).join("\n")
|
||||
const collapsed = collapseToolOutput(`$ ${command}\n\n${output}`, 10, 1_000)
|
||||
|
||||
expect(collapsed.overflow).toBe(true)
|
||||
expect(collapsed.output.split("\n")).toHaveLength(10)
|
||||
expect(collapsed.output).toContain("$ command 1")
|
||||
expect(collapsed.output).toContain("command 8\n\noutput 1…")
|
||||
expect(collapsed.output).not.toContain("output 2")
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import type { V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { onMount } from "solid-js"
|
||||
import { ProjectProvider } from "../../../src/context/project"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
|
|
@ -17,12 +17,8 @@ async function wait(fn: () => boolean, timeout = 2000) {
|
|||
}
|
||||
}
|
||||
|
||||
function global(payload: Event): GlobalEvent {
|
||||
return { directory, project: "proj_test", payload }
|
||||
}
|
||||
|
||||
function emitEvent(events: ReturnType<typeof createEventStream>, payload: Event) {
|
||||
events.emit(global(payload))
|
||||
function emitEvent(events: ReturnType<typeof createEventStream>, event: V2Event) {
|
||||
events.emit({ ...event, location: { directory } })
|
||||
}
|
||||
|
||||
test("refreshes resources into reactive getters", async () => {
|
||||
|
|
@ -205,7 +201,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
emitEvent(events, {
|
||||
id: "evt_step_started",
|
||||
type: "session.next.step.started",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-live",
|
||||
assistantMessageID: "message-live",
|
||||
timestamp: 1,
|
||||
|
|
@ -218,7 +214,7 @@ test("tracks session status from active sessions and execution events", async ()
|
|||
emitEvent(events, {
|
||||
id: "evt_step_ended",
|
||||
type: "session.next.step.ended",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-live",
|
||||
assistantMessageID: "message-live",
|
||||
timestamp: 2,
|
||||
|
|
@ -291,7 +287,7 @@ test("refreshes integrations after integration updates", async () => {
|
|||
expect(data.location.integration.list()).toEqual([])
|
||||
const before = { ...requests }
|
||||
|
||||
emitEvent(events, { id: "evt_integration", type: "integration.updated", properties: {} })
|
||||
emitEvent(events, { id: "evt_integration", type: "integration.updated", data: {} })
|
||||
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" })
|
||||
|
|
@ -329,7 +325,7 @@ test("refreshes effective catalog data after catalog updates", async () => {
|
|||
try {
|
||||
await wait(() => requests.model > 0 && requests.provider > 0)
|
||||
const before = { ...requests }
|
||||
emitEvent(events, { id: "evt_catalog", type: "catalog.updated", properties: {} })
|
||||
emitEvent(events, { id: "evt_catalog", type: "catalog.updated", data: {} })
|
||||
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
|
|
@ -374,7 +370,7 @@ test("refreshes references after updates", async () => {
|
|||
try {
|
||||
await mounted
|
||||
await wait(() => requests === 1)
|
||||
emitEvent(events, { id: "evt_reference_1", type: "reference.updated", properties: {} })
|
||||
emitEvent(events, { id: "evt_reference_1", type: "reference.updated", data: {} })
|
||||
await wait(() => data.location.reference.list()?.length === 1)
|
||||
expect(data.location.reference.list()?.[0]?.name).toBe("docs")
|
||||
} finally {
|
||||
|
|
@ -409,7 +405,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_permission_asked_1",
|
||||
type: "permission.v2.asked",
|
||||
properties: {
|
||||
data: {
|
||||
id: "per_1",
|
||||
sessionID: "ses_1",
|
||||
action: "bash",
|
||||
|
|
@ -419,7 +415,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_permission_asked_2",
|
||||
type: "permission.v2.asked",
|
||||
properties: {
|
||||
data: {
|
||||
id: "per_2",
|
||||
sessionID: "ses_1",
|
||||
action: "read",
|
||||
|
|
@ -431,7 +427,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_permission_replied_1",
|
||||
type: "permission.v2.replied",
|
||||
properties: { sessionID: "ses_1", requestID: "per_1", reply: "once" },
|
||||
data: { sessionID: "ses_1", requestID: "per_1", reply: "once" },
|
||||
})
|
||||
await wait(() => data.session.permission.list("ses_1")?.length === 1)
|
||||
expect(data.session.permission.list("ses_1")?.[0]?.id).toBe("per_2")
|
||||
|
|
@ -439,7 +435,7 @@ test("adds and dismisses permission requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_permission_replied_2",
|
||||
type: "permission.v2.replied",
|
||||
properties: { sessionID: "ses_1", requestID: "per_2", reply: "reject" },
|
||||
data: { sessionID: "ses_1", requestID: "per_2", reply: "reject" },
|
||||
})
|
||||
await wait(() => data.session.permission.list("ses_1")?.length === 0)
|
||||
} finally {
|
||||
|
|
@ -474,7 +470,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_question_asked_1",
|
||||
type: "question.v2.asked",
|
||||
properties: {
|
||||
data: {
|
||||
id: "que_1",
|
||||
sessionID: "ses_1",
|
||||
questions: [{ question: "Which option?", header: "Option", options: [], multiple: false }],
|
||||
|
|
@ -483,7 +479,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_question_asked_2",
|
||||
type: "question.v2.asked",
|
||||
properties: {
|
||||
data: {
|
||||
id: "que_2",
|
||||
sessionID: "ses_1",
|
||||
questions: [{ question: "Which environment?", header: "Environment", options: [], multiple: false }],
|
||||
|
|
@ -494,7 +490,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_question_replied_1",
|
||||
type: "question.v2.replied",
|
||||
properties: { sessionID: "ses_1", requestID: "que_1", answers: [["First"]] },
|
||||
data: { sessionID: "ses_1", requestID: "que_1", answers: [["First"]] },
|
||||
})
|
||||
await wait(() => data.session.question.list("ses_1")?.length === 1)
|
||||
expect(data.session.question.list("ses_1")?.[0]?.id).toBe("que_2")
|
||||
|
|
@ -502,7 +498,7 @@ test("adds and dismisses question requests from live events", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_question_rejected_2",
|
||||
type: "question.v2.rejected",
|
||||
properties: { sessionID: "ses_1", requestID: "que_2" },
|
||||
data: { sessionID: "ses_1", requestID: "que_2" },
|
||||
})
|
||||
await wait(() => data.session.question.list("ses_1")?.length === 0)
|
||||
} finally {
|
||||
|
|
@ -542,12 +538,12 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_agent_1",
|
||||
type: "session.next.agent.switched",
|
||||
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
|
||||
data: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
|
||||
})
|
||||
emitEvent(events, {
|
||||
id: "evt_model_1",
|
||||
type: "session.next.model.switched",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_model_1",
|
||||
timestamp: 0,
|
||||
|
|
@ -557,7 +553,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_step_started_1",
|
||||
type: "session.next.step.started",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
timestamp: 1,
|
||||
|
|
@ -568,7 +564,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_input_1",
|
||||
type: "session.next.tool.input.started",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
timestamp: 2,
|
||||
|
|
@ -579,7 +575,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_called_1",
|
||||
type: "session.next.tool.called",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 2,
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
|
|
@ -592,7 +588,7 @@ test("settles pending tools when a live failure arrives", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_failed_1",
|
||||
type: "session.next.tool.failed",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
timestamp: 3,
|
||||
assistantMessageID: "msg_explicit_assistant_9",
|
||||
|
|
@ -673,7 +669,7 @@ test("renders admitted prompts only after they become model-visible", async () =
|
|||
emitEvent(events, {
|
||||
id: "evt_admitted_1",
|
||||
type: "session.next.prompt.admitted",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_user_1",
|
||||
timestamp: 0,
|
||||
|
|
@ -686,7 +682,7 @@ test("renders admitted prompts only after they become model-visible", async () =
|
|||
emitEvent(events, {
|
||||
id: "evt_prompted_1",
|
||||
type: "session.next.prompted",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_user_1",
|
||||
timestamp: 0,
|
||||
|
|
@ -744,7 +740,7 @@ test("projects live context updates with their message ID", async () => {
|
|||
emitEvent(events, {
|
||||
id: "evt_context_1",
|
||||
type: "session.next.context.updated",
|
||||
properties: {
|
||||
data: {
|
||||
sessionID: "session-1",
|
||||
messageID: "msg_context_1",
|
||||
timestamp: 1,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,39 @@ test("groups across empty assistant reasoning parts", () => {
|
|||
])
|
||||
})
|
||||
|
||||
test("completes exploration groups when another row follows", () => {
|
||||
const finished = assistant("assistant-2", [
|
||||
{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } },
|
||||
])
|
||||
finished.finish = "stop"
|
||||
const messages: SessionMessage[] = [
|
||||
assistant("assistant-1", [
|
||||
{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 1 } },
|
||||
]),
|
||||
{ type: "user", id: "user-1", text: "Continue", time: { created: 2 } },
|
||||
finished,
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "read-1" }],
|
||||
},
|
||||
{ type: "message", messageID: "user-1" },
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
pending: [],
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
|
||||
},
|
||||
{ type: "assistant-footer", messageID: "assistant-2" },
|
||||
])
|
||||
})
|
||||
|
||||
function assistant(id: string, content: SessionMessageAssistant["content"]): SessionMessageAssistant {
|
||||
return {
|
||||
type: "assistant",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import type { OpencodeClient, V2Event } from "@opencode-ai/sdk/v2"
|
||||
import { onMount } from "solid-js"
|
||||
import { ProjectProvider, useProject } from "../../../src/context/project"
|
||||
import { SDKProvider } from "../../../src/context/sdk"
|
||||
import { SDKProvider, useSDK } from "../../../src/context/sdk"
|
||||
import { useEvent } from "../../../src/context/event"
|
||||
import { createClient, createEventStream, createFetch, directory } from "../../fixture/tui-sdk"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
|
@ -19,41 +19,40 @@ async function wait(fn: () => boolean, timeout = 2000) {
|
|||
}
|
||||
}
|
||||
|
||||
function event(payload: Event, input: { directory: string; project?: string; workspace?: string }): GlobalEvent {
|
||||
function event(payload: V2Event, input: { directory: string; project?: string; workspace?: string }): V2Event {
|
||||
return {
|
||||
directory: input.directory,
|
||||
project: input.project,
|
||||
workspace: input.workspace,
|
||||
payload,
|
||||
...payload,
|
||||
location: { directory: input.directory, workspaceID: input.workspace },
|
||||
}
|
||||
}
|
||||
|
||||
function vcs(branch: string): Event {
|
||||
function vcs(branch: string): V2Event {
|
||||
return {
|
||||
id: `evt_vcs_${branch}`,
|
||||
type: "vcs.branch.updated",
|
||||
properties: {
|
||||
data: {
|
||||
branch,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function update(version: string): Event {
|
||||
function update(version: string): V2Event {
|
||||
return {
|
||||
id: `evt_update_${version}`,
|
||||
type: "installation.update-available",
|
||||
properties: {
|
||||
data: {
|
||||
version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function mount() {
|
||||
async function mount(reload?: () => Promise<OpencodeClient>) {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(undefined, events)
|
||||
const seen: Event[] = []
|
||||
const seen: V2Event[] = []
|
||||
const workspaces: Array<string | undefined> = []
|
||||
let project!: ReturnType<typeof useProject>
|
||||
let sdk!: ReturnType<typeof useSDK>
|
||||
let done!: () => void
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
done = resolve
|
||||
|
|
@ -61,11 +60,12 @@ async function mount() {
|
|||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<SDKProvider client={createClient(calls.fetch)}>
|
||||
<SDKProvider client={createClient(calls.fetch)} reload={reload}>
|
||||
<ProjectProvider>
|
||||
<Probe
|
||||
onReady={async (ctx) => {
|
||||
project = ctx.project
|
||||
sdk = ctx.sdk
|
||||
await project.sync()
|
||||
done()
|
||||
}}
|
||||
|
|
@ -78,15 +78,16 @@ async function mount() {
|
|||
))
|
||||
|
||||
await ready
|
||||
return { app, emit: events.emit, project, seen, workspaces }
|
||||
return { app, emit: events.emit, project, sdk, seen, workspaces }
|
||||
}
|
||||
|
||||
function Probe(props: {
|
||||
seen: Event[]
|
||||
seen: V2Event[]
|
||||
workspaces: Array<string | undefined>
|
||||
onReady: (ctx: { project: ReturnType<typeof useProject> }) => void
|
||||
onReady: (ctx: { project: ReturnType<typeof useProject>; sdk: ReturnType<typeof useSDK> }) => void
|
||||
}) {
|
||||
const project = useProject()
|
||||
const sdk = useSDK()
|
||||
const event = useEvent()
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -94,7 +95,7 @@ function Probe(props: {
|
|||
props.seen.push(evt)
|
||||
props.workspaces.push(workspace)
|
||||
})
|
||||
props.onReady({ project })
|
||||
props.onReady({ project, sdk })
|
||||
})
|
||||
|
||||
return <box />
|
||||
|
|
@ -109,7 +110,7 @@ describe("useEvent", () => {
|
|||
|
||||
await wait(() => seen.length === 1)
|
||||
|
||||
expect(seen).toEqual([vcs("main")])
|
||||
expect(seen).toEqual([event(vcs("main"), { directory: "/tmp/other", workspace: "ws_a" })])
|
||||
expect(workspaces).toEqual(["ws_a"])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
|
|
@ -125,7 +126,7 @@ describe("useEvent", () => {
|
|||
|
||||
await wait(() => seen.length === 1)
|
||||
|
||||
expect(seen).toEqual([vcs("ws")])
|
||||
expect(seen).toEqual([event(vcs("ws"), { directory: "/tmp/other", workspace: "ws_b" })])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -140,7 +141,54 @@ describe("useEvent", () => {
|
|||
|
||||
await wait(() => seen.length === 1)
|
||||
|
||||
expect(seen).toEqual([update("1.2.3")])
|
||||
expect(seen).toEqual([event(update("1.2.3"), { directory: "global" })])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("reloads the host and reconnects the event stream", async () => {
|
||||
let calls = 0
|
||||
const events = createEventStream()
|
||||
const replacement = createClient(createFetch(undefined, events).fetch)
|
||||
const { app, sdk, seen } = await mount(async () => {
|
||||
calls += 1
|
||||
return replacement
|
||||
})
|
||||
|
||||
try {
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
await sdk.reload?.()
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
events.emit(event(vcs("reloaded"), { directory: "/tmp/reloaded" }))
|
||||
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "reloaded"))
|
||||
|
||||
expect(calls).toBe(1)
|
||||
expect(sdk.client).toBe(replacement)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("keeps the current event stream alive while the host reload is pending", async () => {
|
||||
let complete!: (client: OpencodeClient) => void
|
||||
const pending = new Promise<OpencodeClient>((resolve) => {
|
||||
complete = resolve
|
||||
})
|
||||
const replacementEvents = createEventStream()
|
||||
const replacement = createClient(createFetch(undefined, replacementEvents).fetch)
|
||||
const { app, emit, sdk, seen } = await mount(() => pending)
|
||||
|
||||
try {
|
||||
await wait(() => sdk.connection.status() === "connected")
|
||||
const reload = sdk.reload?.()
|
||||
emit(event(vcs("during-reload"), { directory: "/tmp/reload" }))
|
||||
await wait(() => seen.some((item) => item.type === "vcs.branch.updated" && item.data.branch === "during-reload"))
|
||||
|
||||
expect(sdk.connection.status()).toBe("connected")
|
||||
complete(replacement)
|
||||
await reload
|
||||
expect(sdk.client).toBe(replacement)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue