mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-03 07:26:20 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1129adf595 | ||
|
|
d036479eff |
@@ -1,7 +1,18 @@
|
||||
import type { LocationGetOutput, LocationRef } from "@opencode-ai/client"
|
||||
import { createContext, createMemo, createSignal, onCleanup, useContext, type ParentProps } from "solid-js"
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
useContext,
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import { useClient } from "./client"
|
||||
import { useData } from "./data"
|
||||
import { useLog } from "./log"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { errorMessage } from "../util/error"
|
||||
|
||||
const context = createContext<{
|
||||
readonly current: LocationGetOutput | undefined
|
||||
@@ -9,16 +20,25 @@ const context = createContext<{
|
||||
readonly ref: LocationRef | undefined
|
||||
readonly error: { readonly location: LocationRef; readonly cause: unknown } | undefined
|
||||
set: (location?: LocationRef) => void
|
||||
retry: () => void
|
||||
}>()
|
||||
|
||||
export function LocationProvider(props: ParentProps) {
|
||||
const client = useClient()
|
||||
const data = useData()
|
||||
const toast = useToast()
|
||||
const log = useLog()
|
||||
const [ref, setRef] = createSignal<LocationRef>()
|
||||
const [error, setError] = createSignal<{ readonly location: LocationRef; readonly cause: unknown }>()
|
||||
let generation = 0
|
||||
const current = createMemo(() => data.location.info(ref()))
|
||||
|
||||
// A reconnect marks the connection ready before its buffered server.connected event.
|
||||
// Invalidate old HTTP attempts at disconnect, not only when the next sync starts.
|
||||
createEffect(() => {
|
||||
if (client.connection.status() !== "connected") generation++
|
||||
})
|
||||
|
||||
function sync(location?: LocationRef) {
|
||||
if (!location) return
|
||||
const attempt = ++generation
|
||||
@@ -28,16 +48,38 @@ export function LocationProvider(props: ParentProps) {
|
||||
? undefined
|
||||
: location
|
||||
setError(undefined)
|
||||
void data.location.sync(target).catch((cause) => {
|
||||
const current = ref()
|
||||
if (
|
||||
generation !== attempt ||
|
||||
current?.directory !== location.directory ||
|
||||
current.workspaceID !== location.workspaceID
|
||||
)
|
||||
return
|
||||
setError({ location, cause })
|
||||
})
|
||||
const active = () =>
|
||||
generation === attempt &&
|
||||
ref()?.directory === location.directory &&
|
||||
ref()?.workspaceID === location.workspaceID &&
|
||||
client.connection.status() === "connected"
|
||||
let resolved = false
|
||||
void data.location
|
||||
.syncInfo(target)
|
||||
.then(() => {
|
||||
// syncInfo is cached: the remaining sync loads catalogs for the resolved location.
|
||||
resolved = true
|
||||
return data.location.sync(target)
|
||||
})
|
||||
.catch((cause) => {
|
||||
if (!active()) return
|
||||
if (!resolved) {
|
||||
setError({ location, cause })
|
||||
return
|
||||
}
|
||||
log.error("Session data sync failed", { cause })
|
||||
toast.show({
|
||||
variant: "error",
|
||||
title: "Session data sync failed",
|
||||
message: `Some session data could not be loaded (${errorMessage(cause)}).`,
|
||||
action: {
|
||||
label: "Retry",
|
||||
run: () => {
|
||||
if (active()) sync(location)
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function set(location?: LocationRef) {
|
||||
@@ -60,6 +102,7 @@ export function LocationProvider(props: ParentProps) {
|
||||
return error()
|
||||
},
|
||||
set,
|
||||
retry: () => sync(ref()),
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -12,7 +12,7 @@ const directory = "/Users/kit/code/open-source/opencode-workerd-profile"
|
||||
function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = props.context.theme.contextual.elevated
|
||||
const [message, setMessage] = createSignal("Choose another directory to continue")
|
||||
const [message, setMessage] = createSignal("Retry or choose another directory")
|
||||
const open = () =>
|
||||
props.context.ui.dialog.show(() => (
|
||||
<DialogMoveSession
|
||||
@@ -50,15 +50,20 @@ function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
||||
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
|
||||
Workerd Modal workspace driver
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>build · GPT-5.6 Sol (high)</text>
|
||||
<text fg={theme.text.subdued}>build · Demo Model</text>
|
||||
<box height={1} />
|
||||
<text fg={theme.text.default}>You</text>
|
||||
<text fg={theme.text.subdued}>Test the mounted workspace and verify the deployment.</text>
|
||||
<box height={1} />
|
||||
<text fg={theme.text.default}>Build · GPT-5.6 Sol (high)</text>
|
||||
<text fg={theme.text.default}>Build · Demo Model</text>
|
||||
<text fg={theme.text.subdued}>The deployment is verified and the worktree is clean.</text>
|
||||
<box flexGrow={1} />
|
||||
<SessionLocationUnavailable directory={directory} onMove={open} />
|
||||
<SessionLocationUnavailable
|
||||
directory={directory}
|
||||
message="Could not initialize this location"
|
||||
onRetry={() => setMessage("Retried location sync")}
|
||||
onMove={open}
|
||||
/>
|
||||
</box>
|
||||
<StoryFooter
|
||||
context={props.context}
|
||||
|
||||
@@ -110,7 +110,7 @@ import { createSingleFlight } from "../../util/single-flight"
|
||||
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { generateThinkingSyntax } from "./thinking-syntax"
|
||||
import { createDelayedPresence } from "../../util/delayed-presence"
|
||||
import { SessionLocationMissing } from "./location-missing"
|
||||
import { SessionLocationError } from "./location-missing"
|
||||
import { isRecord } from "../../util/record"
|
||||
import { createHistoryPrepend } from "./history"
|
||||
import { useSessionTerminals } from "../../context/session-terminals"
|
||||
@@ -1485,11 +1485,7 @@ export function Session(props: {
|
||||
currentLocation.error?.location.workspaceID === session()!.location.workspaceID
|
||||
}
|
||||
>
|
||||
<SessionLocationMissing
|
||||
directory={session()!.location.directory}
|
||||
projectID={session()!.projectID}
|
||||
sessionID={route.sessionID}
|
||||
/>
|
||||
<SessionLocationError projectID={session()!.projectID} sessionID={route.sessionID} />
|
||||
</Match>
|
||||
<Match when={!disabled()}>
|
||||
<Prompt
|
||||
|
||||
@@ -1,36 +1,55 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { useLocation } from "../../context/location"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { abbreviateHome } from "../../util/path-format"
|
||||
import { SessionQuestion } from "./permission"
|
||||
import { usePromptMove } from "../../component/prompt/move"
|
||||
import { errorMessage } from "../../util/error"
|
||||
|
||||
export function SessionLocationMissing(props: { directory: string; projectID: string; sessionID: string }) {
|
||||
export function SessionLocationError(props: { projectID: string; sessionID: string }) {
|
||||
const location = useLocation()
|
||||
const move = usePromptMove({ projectID: () => props.projectID, sessionID: () => props.sessionID })
|
||||
return <SessionLocationUnavailable directory={props.directory} onMove={move.open} />
|
||||
return (
|
||||
<Show when={location.error}>
|
||||
{(error) => (
|
||||
<SessionLocationUnavailable
|
||||
directory={error().location.directory}
|
||||
message={errorMessage(error().cause)}
|
||||
onRetry={location.retry}
|
||||
onMove={move.open}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionLocationUnavailable(props: { directory: string; onMove: () => void }) {
|
||||
export function SessionLocationUnavailable(props: {
|
||||
directory: string
|
||||
message: string
|
||||
onRetry: () => void
|
||||
onMove: () => void
|
||||
}) {
|
||||
const paths = useTuiPaths()
|
||||
const theme = useTheme("elevated")
|
||||
const directory = createMemo(() => Locale.truncateMiddle(abbreviateHome(props.directory, paths.home), 72))
|
||||
|
||||
return (
|
||||
<SessionQuestion
|
||||
id="session.location-missing"
|
||||
group="Session recovery"
|
||||
choicesLabel="Recovery actions"
|
||||
id="session.location-sync-error"
|
||||
group="Session sync"
|
||||
choicesLabel="Sync actions"
|
||||
instance={props.directory}
|
||||
title="Session location unavailable"
|
||||
title="Could not load session location"
|
||||
body={
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<text fg={theme.text.subdued}>{directory()}</text>
|
||||
<text fg={theme.text.default}>Choose another directory to continue this session.</text>
|
||||
<text fg={theme.text.default}>{props.message}</text>
|
||||
</box>
|
||||
}
|
||||
options={{ move: "Choose directory" }}
|
||||
onSelect={props.onMove}
|
||||
options={{ retry: "Retry", move: "Choose directory" }}
|
||||
onSelect={(option) => (option === "retry" ? props.onRetry() : props.onMove())}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { createEffect } from "solid-js"
|
||||
import { ConfigProvider } from "../../src/config"
|
||||
import { ClientProvider, useClient } from "../../src/context/client"
|
||||
import { DataProvider, useData } from "../../src/context/data"
|
||||
import { LocationProvider, useLocation } from "../../src/context/location"
|
||||
import { createApi, createEventStream, createFetch, directory, json } from "../fixture/tui-client"
|
||||
import { TestTuiContexts } from "../fixture/tui-environment"
|
||||
import { createTuiResolvedConfig } from "../fixture/tui-runtime"
|
||||
import { useToast } from "../../src/ui/toast"
|
||||
|
||||
test.each([
|
||||
{ endpoint: "location", reconnect: false },
|
||||
{ endpoint: "agent", reconnect: false },
|
||||
{ endpoint: "agent", reconnect: true },
|
||||
])("a late failure cannot replace the new sync's state (%o)", async ({ endpoint, reconnect }) => {
|
||||
// Keep the held lookup separate from the client's launch-directory preload.
|
||||
const source = `${directory}/old`
|
||||
const requested = Promise.withResolvers<void>()
|
||||
const response = Promise.withResolvers<Response>()
|
||||
const events = createEventStream()
|
||||
let requests = 0
|
||||
let connections = 0
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === "/api/event") connections++
|
||||
const target = url.searchParams.get("location[directory]") ?? directory
|
||||
if (target === source && url.pathname === `/api/${endpoint}` && ++requests === 1) {
|
||||
requested.resolve()
|
||||
return response.promise
|
||||
}
|
||||
const location = { directory: target, project: { id: "project", directory: target, canonical: target } }
|
||||
if (url.pathname === "/api/location") return json(location)
|
||||
if (url.pathname === "/api/agent") return json({ location, data: [] })
|
||||
return undefined
|
||||
}, events)
|
||||
let location!: ReturnType<typeof useLocation>
|
||||
let data!: ReturnType<typeof useData>
|
||||
let toast!: ReturnType<typeof useToast>
|
||||
function Probe() {
|
||||
const client = useClient()
|
||||
location = useLocation()
|
||||
data = useData()
|
||||
toast = useToast()
|
||||
location.set({ directory: source })
|
||||
createEffect(() => {
|
||||
// Connection status changes before the buffered server.connected event is published.
|
||||
// Deliver the old HTTP failure in that gap, not after an arbitrary timer.
|
||||
if (client.connection.status() === "connected" && reconnect && connections > 1)
|
||||
response.resolve(json({ message: "Old location sync failed" }, { status: 500 }))
|
||||
})
|
||||
return <box />
|
||||
}
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<ConfigProvider config={createTuiResolvedConfig()}>
|
||||
<ClientProvider api={createApi(calls.fetch)}>
|
||||
<DataProvider directory={directory}>
|
||||
<LocationProvider>
|
||||
<Probe />
|
||||
</LocationProvider>
|
||||
</DataProvider>
|
||||
</ClientProvider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
app.renderer.start()
|
||||
try {
|
||||
await requested.promise
|
||||
const target = reconnect ? source : "/other"
|
||||
if (reconnect) {
|
||||
events.disconnect()
|
||||
await app.waitFor(() => connections > 1, { maxPasses: 120 })
|
||||
}
|
||||
if (!reconnect) location.set({ directory: target })
|
||||
if (!reconnect) response.resolve(json({ message: "Old location sync failed" }, { status: 500 }))
|
||||
await app.waitFor(() => data.location.agent.list({ directory: target }) !== undefined)
|
||||
await app.waitForVisualIdle()
|
||||
expect(location.ref).toEqual({ directory: target })
|
||||
expect(location.current?.directory).toBe(target)
|
||||
expect(location.error).toBeUndefined()
|
||||
expect(toast.currentToast).toBeNull()
|
||||
} finally {
|
||||
response.resolve(json({ message: "Old location sync failed" }, { status: 500 }))
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("catalog failures preserve resolved info and an old Retry cannot sync a different location", async () => {
|
||||
const requests: string[] = []
|
||||
const causes: unknown[] = []
|
||||
const failure = Promise.withResolvers<Response>()
|
||||
const calls = createFetch((url) => {
|
||||
const target = url.searchParams.get("location[directory]") ?? directory
|
||||
requests.push(`${target}:${url.pathname}`)
|
||||
const location = { directory: target, project: { id: "project", directory: target, canonical: target } }
|
||||
if (url.pathname === "/api/location") return json(location)
|
||||
if (url.pathname === "/api/agent" && target === directory) return failure.promise
|
||||
if (url.pathname === "/api/agent") return json({ location, data: [] })
|
||||
return undefined
|
||||
}, createEventStream())
|
||||
let location!: ReturnType<typeof useLocation>
|
||||
let data!: ReturnType<typeof useData>
|
||||
let toast!: ReturnType<typeof useToast>
|
||||
function Probe() {
|
||||
location = useLocation()
|
||||
data = useData()
|
||||
toast = useToast()
|
||||
location.set({ directory })
|
||||
return <box />
|
||||
}
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts
|
||||
log={(_, message, tags) => {
|
||||
if (message === "Session data sync failed") causes.push(tags.cause)
|
||||
}}
|
||||
>
|
||||
<ConfigProvider config={createTuiResolvedConfig()}>
|
||||
<ClientProvider api={createApi(calls.fetch)}>
|
||||
<DataProvider directory={directory}>
|
||||
<LocationProvider>
|
||||
<Probe />
|
||||
</LocationProvider>
|
||||
</DataProvider>
|
||||
</ClientProvider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
app.renderer.start()
|
||||
try {
|
||||
await app.waitFor(() => requests.includes(`${directory}:/api/agent`))
|
||||
const original = data.location.agent.sync({ directory }).catch((cause: unknown) => cause)
|
||||
failure.resolve(json({ message: "Agent catalog temporarily unavailable" }, { status: 500 }))
|
||||
await app.waitFor(() => toast.currentToast !== null)
|
||||
expect(causes).toEqual([await original])
|
||||
expect(causes[0]).toBe(await original)
|
||||
expect(location.current?.directory).toBe(directory)
|
||||
expect(location.error).toBeUndefined()
|
||||
const retry = toast.currentToast?.action?.run
|
||||
expect(retry).toBeDefined()
|
||||
location.set({ directory: "/other" })
|
||||
await app.waitFor(() => data.location.agent.list({ directory: "/other" }) !== undefined)
|
||||
const before = requests.length
|
||||
retry?.()
|
||||
await app.waitForVisualIdle()
|
||||
expect(requests).toHaveLength(before)
|
||||
expect(location.ref).toEqual({ directory: "/other" })
|
||||
expect(location.error).toBeUndefined()
|
||||
} finally {
|
||||
failure.resolve(json({ message: "Agent catalog temporarily unavailable" }, { status: 500 }))
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { ParentProps } from "solid-js"
|
||||
import { LogProvider, type LogSink } from "../../src/context/log"
|
||||
import { ClipboardProvider, type ClipboardService } from "../../src/context/clipboard"
|
||||
import { ToastProvider } from "../../src/ui/toast"
|
||||
|
||||
const clipboard: ClipboardService = {
|
||||
async read() {
|
||||
@@ -38,7 +39,9 @@ export function TestTuiContexts(
|
||||
>
|
||||
<TuiTerminalEnvironmentProvider value={{ platform: "linux" }}>
|
||||
<TuiStartupProvider value={{ skipInitialLoading: false }}>
|
||||
<ClipboardProvider value={props.clipboard ?? clipboard}>{props.children}</ClipboardProvider>
|
||||
<ClipboardProvider value={props.clipboard ?? clipboard}>
|
||||
<ToastProvider>{props.children}</ToastProvider>
|
||||
</ClipboardProvider>
|
||||
</TuiStartupProvider>
|
||||
</TuiTerminalEnvironmentProvider>
|
||||
</TuiPathsProvider>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { createTestRenderer } from "@opentui/core/testing"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { createEventStream, createFetch, directory, json } from "./fixture/tui-client"
|
||||
import { tmpdir } from "./fixture/fixture"
|
||||
|
||||
test.each([
|
||||
{ width: 70, endpoint: "agent", initial: true },
|
||||
{ width: 120, endpoint: "agent", initial: false },
|
||||
{ width: 100, endpoint: "model", initial: false },
|
||||
{ width: 100, endpoint: "mcp", initial: false },
|
||||
{ width: 100, endpoint: "location", initial: true },
|
||||
{ width: 100, endpoint: "location", initial: false },
|
||||
])("session sync offers truthful recovery (%o)", async ({ width, endpoint, initial }) => {
|
||||
await using state = await tmpdir()
|
||||
const setup = await createTestRenderer({ width, height: 30, useThread: false, kittyKeyboard: true })
|
||||
setup.renderer.start()
|
||||
const sessionID = `ses_location_sync_${endpoint}_${width}`
|
||||
const location = { directory, project: { id: "project", directory, canonical: directory } }
|
||||
let agents = 0
|
||||
let failures = 0
|
||||
let healthy = !initial
|
||||
let recovered = 0
|
||||
const events = createEventStream()
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname === `/api/session/${sessionID}`)
|
||||
return json({
|
||||
data: {
|
||||
id: sessionID,
|
||||
projectID: "project",
|
||||
title: "Location sync fixture",
|
||||
model: { providerID: "demo", id: "model" },
|
||||
location: { directory },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
},
|
||||
})
|
||||
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
|
||||
if (url.pathname === `/api/session/${sessionID}/inbox` || url.pathname === `/api/session/${sessionID}/permission`)
|
||||
return json({ data: [] })
|
||||
if (url.pathname === "/api/worktree/project") return json([{ directory: "/other" }])
|
||||
if (url.pathname === "/api/worktree/project/refresh") return new Response(null, { status: 204 })
|
||||
if (url.pathname === `/api/${endpoint}` && !healthy) {
|
||||
if (endpoint === "agent") agents++
|
||||
failures++
|
||||
return json({ message: "Service temporarily unavailable" }, { status: 500 })
|
||||
}
|
||||
if (url.pathname === `/api/${endpoint}` && failures > 0) recovered++
|
||||
if (url.pathname === "/api/location") return json(location)
|
||||
if (url.pathname === "/api/agent") {
|
||||
agents++
|
||||
return json({ location, data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }] })
|
||||
}
|
||||
if (url.pathname === "/api/provider") return json({ location, data: [{ id: "demo", name: "Demo" }] })
|
||||
if (url.pathname === "/api/model")
|
||||
return json({ location, data: [{ id: "model", providerID: "demo", name: "Demo Model", variants: [] }] })
|
||||
return undefined
|
||||
}, events)
|
||||
const server = Bun.serve({ port: 0, idleTimeout: 0, fetch: (request) => calls.fetch(request) })
|
||||
const { run } = await import("../src/app")
|
||||
const task = Effect.runPromise(
|
||||
run({
|
||||
app: { name: "test", version: "test", channel: "test" },
|
||||
server: { endpoint: { url: server.url.toString() } },
|
||||
config: { get: async () => ({ animations: false }), update: async () => ({}) },
|
||||
packages: { prepare: async () => ({ directory: "" }) },
|
||||
terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: () => {} }),
|
||||
args: { sessionID },
|
||||
log: () => {},
|
||||
}).pipe(Effect.provide(Global.layerWith({ state: state.path })), Effect.provide(FileSystem.layerNoop({}))),
|
||||
)
|
||||
try {
|
||||
const title = endpoint === "location" ? "Could not load session location" : "Session data sync failed"
|
||||
if (!initial) {
|
||||
await setup.waitForFrame((frame) => frame.includes("Demo Model"))
|
||||
await setup.mockInput.typeText("Keep this draft")
|
||||
await setup.waitForFrame((frame) => frame.includes("Keep this draft"))
|
||||
healthy = false
|
||||
events.disconnect()
|
||||
}
|
||||
const editor = setup.renderer.currentFocusedEditor
|
||||
await setup.waitFor(() => failures > 0, { maxPasses: 120 })
|
||||
// Also settle on the old panel so this remains an assertion failure on the base revision.
|
||||
await setup.waitForFrame((frame) => frame.includes("Session location unavailable") || frame.includes(title))
|
||||
await setup.waitForVisualIdle()
|
||||
const frame = setup.captureCharFrame()
|
||||
expect(frame).not.toContain("Choose another directory to continue this session.")
|
||||
expect(frame).toContain(title)
|
||||
// Undeclared HTTP 500 bodies are deliberately not decoded by the generated client.
|
||||
expect(frame).toContain("UnexpectedStatus")
|
||||
expect(frame).toContain("Retry")
|
||||
if (endpoint !== "location") {
|
||||
expect(agents).toBeGreaterThan(0)
|
||||
expect(frame).not.toContain("Choose directory")
|
||||
if (!initial) {
|
||||
expect(setup.renderer.currentFocusedEditor).toBe(editor)
|
||||
expect(editor?.plainText).toBe("Keep this draft")
|
||||
}
|
||||
}
|
||||
if (endpoint === "location") {
|
||||
expect(frame).toContain("Choose directory")
|
||||
const lines = frame.split("\n")
|
||||
const row = lines.findIndex((line) => line.includes("Choose directory"))
|
||||
await setup.mockMouse.click(lines[row]!.indexOf("Choose directory"), row)
|
||||
await setup.waitForFrame((frame) => frame.includes("Move session") && frame.includes("/other"))
|
||||
setup.mockInput.pressEscape()
|
||||
await setup.waitForFrame((frame) => !frame.includes("Move session") && frame.includes(title))
|
||||
}
|
||||
|
||||
const retry = async () => {
|
||||
const lines = setup.captureCharFrame().split("\n")
|
||||
const row = lines.findIndex((line) => line.includes("Retry"))
|
||||
expect(row).toBeGreaterThanOrEqual(0)
|
||||
await setup.mockMouse.click(lines[row]!.indexOf("Retry"), row)
|
||||
}
|
||||
const before = failures
|
||||
await retry()
|
||||
await setup.waitFor(() => failures > before)
|
||||
await setup.waitForFrame((frame) => frame.includes(title))
|
||||
healthy = true
|
||||
await retry()
|
||||
await setup.waitFor(() => recovered > 0)
|
||||
await setup.waitForFrame((frame) => frame.includes("Demo Model") && !frame.includes(title))
|
||||
expect(agents).toBeGreaterThan(0)
|
||||
expect(setup.captureCharFrame()).not.toContain("Choose directory")
|
||||
if (!initial && endpoint !== "location") {
|
||||
expect(setup.renderer.currentFocusedEditor).toBe(editor)
|
||||
expect(editor?.plainText).toBe("Keep this draft")
|
||||
await setup.mockInput.typeText(" after retry")
|
||||
await setup.waitForFrame((frame) => frame.includes("Keep this draft after retry"))
|
||||
}
|
||||
} finally {
|
||||
setup.renderer.destroy()
|
||||
await task.finally(() => server.stop(true))
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user