From 3bd90e8bc46fc43fc92b67770b5f1aad2c4ec51e Mon Sep 17 00:00:00 2001 From: Trevor Sullivan Date: Thu, 10 Sep 2026 20:54:12 -0600 Subject: [PATCH] fix(tui): avoid invalid dummy session id on continue --- packages/tui/src/app.tsx | 7 ++- packages/tui/test/app-lifecycle.test.tsx | 58 ++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 3f1da522bb06..2cb9342b7d1e 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -285,10 +285,10 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { Promise; pluginHost: TuiPlugi } else { route.navigate({ type: "session", sessionID: match }) } + } else if (sync.status === "complete") { + continued = true + toast.show({ message: "No session found to continue", variant: "warning" }) } }) diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index 570663424718..96e8408911e7 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -65,19 +65,22 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => { const core = await import("@opentui/core") mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer })) const events = createEventSource() + const requestedUrls: string[] = [] const calls = createFetch((url) => { + requestedUrls.push(url.pathname) if (url.pathname === "/session") return json([ { - id: "dummy", + id: "ses_demo", title: "Demo session", - slug: "dummy", + slug: "ses_demo", projectID: "project", directory, version: "0.0.0-test", time: { created: 0, updated: 0 }, }, ]) + return undefined }) const originalWrite = process.stdout.write.bind(process.stdout) let stdout = "" @@ -119,10 +122,59 @@ test("app.exit prints the session epilogue after scoped cleanup", async () => { await task expect(stdout).toContain("Demo session") - expect(stdout).toContain("opencode -s dummy") + expect(stdout).toContain("opencode -s ses_demo") + expect(requestedUrls.some((path) => path.includes("dummy"))).toBe(false) } finally { process.stdout.write = originalWrite if (!setup.renderer.isDestroyed) setup.renderer.destroy() mock.restore() } }) + +test("continue without sessions does not crash with dummy session", async () => { + const setup = await createTestRenderer({ width: 80, height: 24, useThread: false }) + const core = await import("@opentui/core") + mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer })) + const events = createEventSource() + const requestedUrls: string[] = [] + const calls = createFetch((url) => { + requestedUrls.push(url.pathname) + if (url.pathname === "/session") return json([]) + return undefined + }) + let api: TuiPluginApi | undefined + let started!: () => void + const ready = new Promise((resolve) => { + started = resolve + }) + + try { + const { run } = await import("../src/app") + const task = Effect.runPromise( + run({ + url: "http://test", + directory, + config: createTuiResolvedConfig({ plugin_enabled: {} }), + fetch: calls.fetch, + events: events.source, + args: { continue: true }, + pluginHost: { + async start(input) { + api = input.api + started() + }, + async dispose() {}, + }, + }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))), + ) + + await ready + await setup.renderOnce() + expect(requestedUrls.some((path) => path.includes("dummy"))).toBe(false) + api?.keymap.dispatchCommand("app.exit") + await task + } finally { + if (!setup.renderer.isDestroyed) setup.renderer.destroy() + mock.restore() + } +})