refactor(tui): use v2 client transport
This commit is contained in:
parent
ad4f1c1018
commit
8b682c42b6
26 changed files with 428 additions and 1507 deletions
|
|
@ -83,6 +83,48 @@ function permission(id: string, sessionID = "session"): PermissionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
function stepStarted(id: string, sessionID = "session"): Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.started",
|
||||
properties: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function stepEnded(id: string, sessionID = "session", finish = "stop"): Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.ended",
|
||||
properties: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
finish,
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function stepFailed(id: string, sessionID = "session"): Event {
|
||||
return {
|
||||
id,
|
||||
type: "session.next.step.failed",
|
||||
properties: {
|
||||
sessionID,
|
||||
assistantMessageID: `msg_${id}`,
|
||||
timestamp: 0,
|
||||
error: { type: "unknown", message: "boom" },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const questionNotification: TuiAttentionNotifyInput = {
|
||||
title: "Demo session",
|
||||
message: "Question needs input",
|
||||
|
|
@ -139,21 +181,9 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("notifies when an active session becomes idle and suppresses no-op idle", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({
|
||||
id: "event-1",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "session", status: { type: "idle" } },
|
||||
})
|
||||
harness.emit({
|
||||
id: "event-2",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "session", status: { type: "busy" } },
|
||||
})
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "session", status: { type: "idle" } },
|
||||
})
|
||||
harness.emit(stepEnded("event-1"))
|
||||
harness.emit(stepStarted("event-2"))
|
||||
harness.emit(stepEnded("event-3"))
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
{
|
||||
|
|
@ -169,16 +199,8 @@ describe("internal notifications TUI plugin", () => {
|
|||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1", "subagent") })
|
||||
harness.emit({
|
||||
id: "event-2",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "subagent", status: { type: "busy" } },
|
||||
})
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "subagent", status: { type: "idle" } },
|
||||
})
|
||||
harness.emit(stepStarted("event-2", "subagent"))
|
||||
harness.emit(stepEnded("event-3", "subagent"))
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
{
|
||||
|
|
@ -199,21 +221,9 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("notifies session errors once and suppresses the following idle done notification", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({
|
||||
id: "event-1",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "session", status: { type: "busy" } },
|
||||
})
|
||||
harness.emit({
|
||||
id: "event-2",
|
||||
type: "session.error",
|
||||
properties: { sessionID: "session", error: { name: "UnknownError", data: { message: "boom" } } },
|
||||
})
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "session", status: { type: "idle" } },
|
||||
})
|
||||
harness.emit(stepStarted("event-1"))
|
||||
harness.emit(stepFailed("event-2"))
|
||||
harness.emit(stepEnded("event-3"))
|
||||
|
||||
expect(harness.notifications).toEqual([
|
||||
{
|
||||
|
|
@ -228,21 +238,13 @@ describe("internal notifications TUI plugin", () => {
|
|||
test("special-cases aborts and model response timeouts", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({
|
||||
id: "event-1",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "abort", status: { type: "busy" } },
|
||||
})
|
||||
harness.emit(stepStarted("event-1", "abort"))
|
||||
harness.emit({
|
||||
id: "event-2",
|
||||
type: "session.error",
|
||||
properties: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
||||
})
|
||||
harness.emit({
|
||||
id: "event-3",
|
||||
type: "session.status",
|
||||
properties: { sessionID: "timeout", status: { type: "busy" } },
|
||||
})
|
||||
harness.emit(stepStarted("event-3", "timeout"))
|
||||
harness.emit({
|
||||
id: "event-4",
|
||||
type: "session.error",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { ProjectProvider, useProject } from "../../../../src/context/project"
|
|||
import { SDKProvider } from "../../../../src/context/sdk"
|
||||
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
||||
import { ExitProvider } from "../../../../src/context/exit"
|
||||
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
|
||||
import { createClient, createEventStream, createFetch, type FetchHandler } from "../../../fixture/tui-sdk"
|
||||
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
||||
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
|
||||
export { createEventStream, createFetch, directory, json, worktree } from "../../../fixture/tui-sdk"
|
||||
|
||||
export async function wait(fn: () => boolean, timeout = 2000) {
|
||||
const start = Date.now()
|
||||
|
|
@ -22,8 +22,8 @@ export async function wait(fn: () => boolean, timeout = 2000) {
|
|||
type Ctx = { kv: ReturnType<typeof useKV>; project: ReturnType<typeof useProject>; sync: ReturnType<typeof useSync> }
|
||||
|
||||
export async function mount(override?: FetchHandler, state?: string) {
|
||||
const calls = createFetch(override)
|
||||
const events = createEventSource()
|
||||
const events = createEventStream()
|
||||
const calls = createFetch(override, events)
|
||||
let sync!: ReturnType<typeof useSync>
|
||||
let project!: ReturnType<typeof useProject>
|
||||
let kv!: ReturnType<typeof useKV>
|
||||
|
|
@ -47,7 +47,7 @@ export async function mount(override?: FetchHandler, state?: string) {
|
|||
<TestTuiContexts paths={state ? { state } : undefined}>
|
||||
<ArgsProvider>
|
||||
<KVProvider>
|
||||
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
|
||||
<SDKProvider client={createClient(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<ExitProvider exit={() => {}}>
|
||||
<SyncProvider>
|
||||
|
|
|
|||
|
|
@ -1,262 +0,0 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import { tmpdir } from "../../../fixture/fixture"
|
||||
import { json, mount, wait } from "./sync-fixture"
|
||||
|
||||
const sessionID = "ses_hydration_race"
|
||||
const messageID = "msg_hydration_race"
|
||||
const partID = "prt_hydration_race"
|
||||
const session = {
|
||||
id: sessionID,
|
||||
title: "race",
|
||||
time: { created: 0, updated: 0 },
|
||||
version: "1.15.13",
|
||||
directory: "/tmp/opencode/packages/opencode",
|
||||
}
|
||||
const assistant = {
|
||||
id: messageID,
|
||||
sessionID,
|
||||
role: "assistant" as const,
|
||||
agent: "build",
|
||||
modelID: "model",
|
||||
providerID: "test",
|
||||
mode: "build",
|
||||
parentID: "msg_user",
|
||||
path: { cwd: session.directory, root: session.directory },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, completed: 2 },
|
||||
}
|
||||
|
||||
function global(payload: GlobalEvent["payload"]): GlobalEvent {
|
||||
return { directory: "/tmp/other", project: "proj_test", payload }
|
||||
}
|
||||
|
||||
test("stale session hydration does not overwrite live message parts", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
|
||||
let resolveMessages!: (response: Response) => void
|
||||
const messages = new Promise<Response>((resolve) => {
|
||||
resolveMessages = resolve
|
||||
})
|
||||
let requested = false
|
||||
const { app, emit, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(session)
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
requested = true
|
||||
return messages
|
||||
}
|
||||
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
return undefined
|
||||
}, tmp.path)
|
||||
|
||||
try {
|
||||
const hydrate = sync.session.sync(sessionID)
|
||||
await wait(() => requested)
|
||||
emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } }))
|
||||
emit(
|
||||
global({
|
||||
id: "evt_part",
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID,
|
||||
time: 2,
|
||||
part: { id: partID, sessionID, messageID, type: "text", text: "visible live content" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
await wait(() => sync.data.part[messageID]?.[0]?.type === "text")
|
||||
|
||||
resolveMessages(
|
||||
json([
|
||||
{
|
||||
info: assistant,
|
||||
parts: [{ id: partID, sessionID, messageID, type: "text", text: "" }],
|
||||
},
|
||||
]),
|
||||
)
|
||||
await hydrate
|
||||
|
||||
expect(sync.data.part[messageID][0]).toMatchObject({ text: "visible live content" })
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("orphan live deltas do not suppress hydrated parts", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
|
||||
let resolveMessages!: (response: Response) => void
|
||||
const messages = new Promise<Response>((resolve) => {
|
||||
resolveMessages = resolve
|
||||
})
|
||||
let requested = false
|
||||
const { app, emit, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(session)
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
requested = true
|
||||
return messages
|
||||
}
|
||||
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
return undefined
|
||||
}, tmp.path)
|
||||
|
||||
try {
|
||||
const hydrate = sync.session.sync(sessionID)
|
||||
await wait(() => requested)
|
||||
emit(
|
||||
global({
|
||||
id: "evt_delta",
|
||||
type: "message.part.delta",
|
||||
properties: { sessionID, messageID, partID, field: "text", delta: "ignored until part exists" },
|
||||
}),
|
||||
)
|
||||
resolveMessages(
|
||||
json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "hydrated" }] }]),
|
||||
)
|
||||
await hydrate
|
||||
|
||||
expect(sync.data.part[messageID][0]).toMatchObject({ text: "hydrated" })
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("hydration does not clear text streamed before it starts", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
|
||||
let resolveMessages!: (response: Response) => void
|
||||
const messages = new Promise<Response>((resolve) => {
|
||||
resolveMessages = resolve
|
||||
})
|
||||
let requested = false
|
||||
const { app, emit, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(session)
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
requested = true
|
||||
return messages
|
||||
}
|
||||
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
return undefined
|
||||
}, tmp.path)
|
||||
|
||||
try {
|
||||
emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } }))
|
||||
emit(
|
||||
global({
|
||||
id: "evt_part",
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID,
|
||||
time: 1,
|
||||
part: { id: partID, sessionID, messageID, type: "text", text: "" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
emit(
|
||||
global({
|
||||
id: "evt_delta",
|
||||
type: "message.part.delta",
|
||||
properties: { sessionID, messageID, partID, field: "text", delta: "visible streamed content" },
|
||||
}),
|
||||
)
|
||||
await wait(() => sync.data.part[messageID]?.[0]?.type === "text" && sync.data.part[messageID][0].text !== "")
|
||||
const hydrate = sync.session.sync(sessionID)
|
||||
await wait(() => requested)
|
||||
resolveMessages(json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "" }] }]))
|
||||
await hydrate
|
||||
|
||||
expect(sync.data.part[messageID][0]).toMatchObject({ text: "visible streamed content" })
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("live messages merged during hydration retain the 100 message window", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
|
||||
let resolveMessages!: (response: Response) => void
|
||||
const messages = new Promise<Response>((resolve) => {
|
||||
resolveMessages = resolve
|
||||
})
|
||||
let requested = false
|
||||
const { app, emit, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(session)
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
requested = true
|
||||
return messages
|
||||
}
|
||||
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
return undefined
|
||||
}, tmp.path)
|
||||
|
||||
try {
|
||||
const hydrate = sync.session.sync(sessionID)
|
||||
await wait(() => requested)
|
||||
const live = { ...assistant, id: "msg_z_live" }
|
||||
emit(global({ id: "evt_live", type: "message.updated", properties: { sessionID, info: live } }))
|
||||
await wait(() => sync.data.message[sessionID]?.some((message) => message.id === live.id) ?? false)
|
||||
resolveMessages(
|
||||
json(
|
||||
Array.from({ length: 100 }, (_, index) => {
|
||||
const id = `msg_${String(index).padStart(3, "0")}`
|
||||
return {
|
||||
info: { ...assistant, id },
|
||||
parts: [{ id: `prt_${id}`, sessionID, messageID: id, type: "text", text: id }],
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
await hydrate
|
||||
|
||||
expect(sync.data.message[sessionID]).toHaveLength(100)
|
||||
expect(sync.data.message[sessionID].at(-1)?.id).toBe(live.id)
|
||||
expect(sync.data.message[sessionID].some((message) => message.id === "msg_000")).toBe(false)
|
||||
expect(sync.data.part.msg_000).toBeUndefined()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("a message removed during hydration does not regain stale parts", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
|
||||
let resolveMessages!: (response: Response) => void
|
||||
const messages = new Promise<Response>((resolve) => {
|
||||
resolveMessages = resolve
|
||||
})
|
||||
let requested = false
|
||||
const { app, emit, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(session)
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
requested = true
|
||||
return messages
|
||||
}
|
||||
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
return undefined
|
||||
}, tmp.path)
|
||||
|
||||
try {
|
||||
emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } }))
|
||||
await wait(() => sync.data.message[sessionID]?.length === 1)
|
||||
const hydrate = sync.session.sync(sessionID)
|
||||
await wait(() => requested)
|
||||
emit(global({ id: "evt_removed", type: "message.removed", properties: { sessionID, messageID } }))
|
||||
await wait(() => sync.data.message[sessionID]?.length === 0)
|
||||
resolveMessages(
|
||||
json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "stale" }] }]),
|
||||
)
|
||||
await hydrate
|
||||
|
||||
expect(sync.data.message[sessionID]).toEqual([])
|
||||
expect(sync.data.part[messageID]).toBeUndefined()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
/**
|
||||
* Reproducer for #26560 — TUI crashes with
|
||||
* `TypeError: undefined is not an object (evaluating 'f.data.map')`
|
||||
* when entering a session whose messages endpoint returns a non-2xx.
|
||||
* The failure path is `sync.tsx#sync.session.sync` reading
|
||||
* `messages.data!` while the SDK leaves `data` undefined on error.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { tmpdir } from "../../../fixture/fixture"
|
||||
import { directory, json, mount } from "./sync-fixture"
|
||||
|
||||
const sessionID = "ses_undef"
|
||||
|
||||
describe("tui sync (#26560)", () => {
|
||||
test("entering a session whose messages endpoint errors does not crash sync", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
|
||||
const sessionPayload = {
|
||||
id: sessionID,
|
||||
title: "broken",
|
||||
time: { created: 0, updated: 0 },
|
||||
version: "1.14.42",
|
||||
directory,
|
||||
project_id: "proj_test",
|
||||
}
|
||||
const { app, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(sessionPayload)
|
||||
if (url.pathname === `/session/${sessionID}/messages`) return json({}, { status: 500 })
|
||||
if (url.pathname === `/session/${sessionID}/todo`) return json([])
|
||||
if (url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
if (url.pathname === "/session") return json([sessionPayload])
|
||||
return undefined
|
||||
}, tmp.path)
|
||||
|
||||
try {
|
||||
await expect(sync.session.sync(sessionID)).resolves.toBeUndefined()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -1,65 +1,24 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { tmpdir } from "../../../fixture/fixture"
|
||||
import { mount, wait } from "./sync-fixture"
|
||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mount } from "./sync-fixture"
|
||||
|
||||
function branchEvent(branch: string, workspace?: string): GlobalEvent {
|
||||
return {
|
||||
directory: "/tmp/other",
|
||||
project: "proj_test",
|
||||
workspace,
|
||||
payload: {
|
||||
id: `evt_vcs_${branch}`,
|
||||
type: "vcs.branch.updated",
|
||||
properties: { branch },
|
||||
},
|
||||
test("legacy sync is an inert compatibility context", async () => {
|
||||
const { app, session, sync } = await mount()
|
||||
|
||||
try {
|
||||
expect(sync.status).toBe("complete")
|
||||
expect(sync.ready).toBe(true)
|
||||
expect(sync.data.session).toEqual([])
|
||||
expect(sync.data.message).toEqual({})
|
||||
expect(sync.data.provider).toEqual([])
|
||||
expect(sync.session.get("ses_test")).toBeUndefined()
|
||||
|
||||
await sync.bootstrap()
|
||||
await sync.session.refresh()
|
||||
await sync.session.sync("ses_test")
|
||||
|
||||
expect(session).toEqual([])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
describe("tui sync", () => {
|
||||
test("refresh scopes sessions by default and lists project sessions when disabled", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
const { app, kv, sync, session } = await mount(undefined, tmp.path)
|
||||
|
||||
try {
|
||||
expect(kv.get("session_directory_filter_enabled", true)).toBe(true)
|
||||
expect(session.at(-1)?.searchParams.get("roots")).toBeNull()
|
||||
expect(session.at(-1)?.searchParams.get("scope")).toBeNull()
|
||||
expect(session.at(-1)?.searchParams.get("path")).toBe("packages/tui")
|
||||
|
||||
kv.set("session_directory_filter_enabled", false)
|
||||
await sync.session.refresh()
|
||||
|
||||
expect(session.at(-1)?.searchParams.get("scope")).toBe("project")
|
||||
expect(session.at(-1)?.searchParams.get("path")).toBeNull()
|
||||
expect(session.at(-1)?.searchParams.get("roots")).toBeNull()
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("vcs branch updates only apply for the active workspace", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
const { app, emit, project, sync } = await mount(undefined, tmp.path)
|
||||
|
||||
try {
|
||||
expect(sync.data.vcs?.branch).toBe("main")
|
||||
|
||||
project.workspace.set("ws_a")
|
||||
emit(branchEvent("other", "ws_b"))
|
||||
await Bun.sleep(30)
|
||||
|
||||
expect(sync.data.vcs?.branch).toBe("main")
|
||||
|
||||
emit(branchEvent("feature", "ws_a"))
|
||||
await wait(() => sync.data.vcs?.branch === "feature")
|
||||
|
||||
expect(sync.data.vcs?.branch).toBe("feature")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue