fix: logo 右半部分从 CODING 改为 CODE

去掉难以正确渲染的 N 和 G 字母,右半部分简化为 CODE(4 字母),
与左半部分 AIR 组合为 AIR CODE。
This commit is contained in:
airlongdian
2026-06-14 09:54:53 +08:00
commit c4f9fe109e
5757 changed files with 1170016 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test"
import { recentConnectedWorkspaces } from "../../../../src/component/dialog-workspace-create"
describe("recentConnectedWorkspaces", () => {
test("returns connected workspaces sorted by time used", () => {
const workspaces = [
{ id: "wrk_a", name: "alpha", timeUsed: 700 },
{ id: "wrk_b", name: "beta", timeUsed: 800 },
{ id: "wrk_c", name: "gamma", timeUsed: 400 },
{ id: "wrk_d", name: "delta", timeUsed: 300 },
{ id: "wrk_e", name: "epsilon", timeUsed: 200 },
]
const status = {
wrk_a: "connected",
wrk_b: "disconnected",
wrk_c: "error",
wrk_d: "connected",
wrk_e: "connected",
} as const
const { recent } = recentConnectedWorkspaces({
workspaces,
status: (workspaceID) => status[workspaceID as keyof typeof status],
})
expect(recent.map((workspace) => workspace.id)).toEqual(["wrk_a", "wrk_d", "wrk_e"])
})
})

View File

@@ -0,0 +1,30 @@
import { describe, expect, test } from "bun:test"
import { sortModelOptions } from "../../../../src/component/dialog-model"
describe("sortModelOptions", () => {
test("orders provider-scoped model choices by newest release first", () => {
const sorted = sortModelOptions(
[
{ title: "GPT 5.2", releaseDate: "2025-12-11" },
{ title: "GPT 5.4", releaseDate: "2026-03-05" },
{ title: "GPT 5.1", releaseDate: "2025-11-13" },
],
true,
)
expect(sorted.map((model) => model.title)).toEqual(["GPT 5.4", "GPT 5.2", "GPT 5.1"])
})
test("preserves free-first alphabetical ordering for the regular picker", () => {
const sorted = sortModelOptions(
[
{ title: "Beta", releaseDate: "2026-01-01" },
{ title: "Alpha", releaseDate: "2025-01-01", footer: "Free" },
{ title: "Gamma", releaseDate: "2024-01-01", footer: "Free" },
],
false,
)
expect(sorted.map((model) => model.title)).toEqual(["Alpha", "Gamma", "Beta"])
})
})

View File

@@ -0,0 +1,267 @@
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 { 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 session = (id: string, title: string, parentID?: string): Session => ({
id,
title,
slug: id,
projectID: "project",
directory: "/workspace",
...(parentID && { parentID }),
version: "0.0.0-test",
time: { created: 0, updated: 0 },
})
const sessions: Record<string, Session> = {
session: session("session", "Demo session"),
subagent: session("subagent", "Subagent session", "session"),
abort: session("abort", "Abort session"),
timeout: session("timeout", "Timeout session"),
}
await Notifications.tui(
createTuiPluginApi({
attention: {
async notify(input) {
notifications.push(input)
return { ok: true, notification: true, sound: true }
},
},
event: {
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => {
const list = handlers.get(type) ?? []
const wrapped = handler as (event: Event) => void
list.push(wrapped)
handlers.set(type, list)
return () => {
handlers.set(
type,
(handlers.get(type) ?? []).filter((item) => item !== wrapped),
)
}
},
},
state: {
session: {
get: (sessionID: string) => sessions[sessionID],
},
},
}),
undefined,
{} as never,
)
return {
notifications,
emit(event: Event) {
for (const handler of handlers.get(event.type) ?? []) handler(event)
},
}
}
function question(id: string, sessionID = "session"): QuestionRequest {
return {
id,
sessionID,
questions: [],
}
}
function permission(id: string, sessionID = "session"): PermissionRequest {
return {
id,
sessionID,
permission: "edit",
patterns: [],
metadata: {},
always: [],
}
}
const questionNotification: TuiAttentionNotifyInput = {
title: "Demo session",
message: "Question needs input",
notification: { when: "blurred" },
sound: { name: "question", when: "always" },
}
const permissionNotification: TuiAttentionNotifyInput = {
title: "Demo session",
message: "Permission needs input",
notification: { when: "blurred" },
sound: { name: "permission", when: "always" },
}
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") })
expect(harness.notifications).toEqual([questionNotification, permissionNotification])
})
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-3",
type: "question.replied",
properties: { sessionID: "session", requestID: "question-1", answers: [] },
})
harness.emit({ id: "event-4", type: "question.asked", properties: 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-7",
type: "permission.replied",
properties: { sessionID: "session", requestID: "permission-1", reply: "once" },
})
harness.emit({ id: "event-8", type: "permission.asked", properties: permission("permission-1") })
expect(harness.notifications).toEqual([
questionNotification,
questionNotification,
permissionNotification,
permissionNotification,
])
})
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" } },
})
expect(harness.notifications).toEqual([
{
title: "Demo session",
message: "Session done",
notification: { when: "blurred" },
sound: { name: "done", when: "always" },
},
])
})
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-2",
type: "session.status",
properties: { sessionID: "subagent", status: { type: "busy" } },
})
harness.emit({
id: "event-3",
type: "session.status",
properties: { sessionID: "subagent", status: { type: "idle" } },
})
expect(harness.notifications).toEqual([
{
title: "Subagent session",
message: "Question needs input",
notification: false,
sound: { name: "question", when: "always" },
},
{
title: "Subagent session",
message: "Session done",
notification: false,
sound: { name: "subagent_done", when: "always" },
},
])
})
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" } },
})
expect(harness.notifications).toEqual([
{
title: "Demo session",
message: "Session error",
notification: { when: "blurred" },
sound: { name: "error", when: "always" },
},
])
})
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({
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({
id: "event-4",
type: "session.error",
properties: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
})
expect(harness.notifications).toEqual([
{
title: "Abort session",
message: "Session aborted",
notification: { when: "blurred" },
sound: { name: "error", when: "always" },
},
{
title: "Timeout session",
message: "Model stopped responding",
notification: { when: "blurred" },
sound: { name: "error", when: "always" },
},
])
})
})

View File

@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test"
import { normalizeCustomProviderID, providerOptions } from "../../../../src/component/dialog-provider"
describe("providerOptions", () => {
test("includes a synthetic Other option for custom providers", () => {
expect(providerOptions([{ id: "openai", name: "OpenAI" }]).at(-1)).toMatchObject({
title: "Other",
description: "Custom provider",
category: "Providers",
})
})
test("does not use Other as the generic provider category", () => {
expect(providerOptions([{ id: "mistral", name: "Mistral" }])[0]?.category).toBe("Providers")
})
test("keeps popular providers first and sorts the rest alphabetically", () => {
expect(
providerOptions([
{ id: "openai", name: "OpenAI" },
{ id: "custom-z", name: "Zebra Provider" },
{ id: "anthropic", name: "Anthropic" },
{ id: "mistral", name: "Mistral" },
{ id: "aws", name: "AWS Bedrock" },
]).map((option) => option.value),
).toEqual(["openai", "anthropic", "aws", "mistral", "custom-z", "__opencode_custom_provider__"])
})
test("does not collide with a configured provider named other", () => {
const values = providerOptions([{ id: "other", name: "Other Provider" }]).map((option) => option.value)
expect(new Set(values).size).toBe(values.length)
})
test("normalizes and validates custom provider ids", () => {
expect(normalizeCustomProviderID(" custom-provider ")).toBe("custom-provider")
expect(normalizeCustomProviderID("custom_provider")).toBe("custom_provider")
expect(normalizeCustomProviderID("@ai-sdk/custom-provider")).toBe("custom-provider")
expect(normalizeCustomProviderID("-custom-provider")).toBeUndefined()
expect(normalizeCustomProviderID("Custom Provider")).toBeUndefined()
})
})

View File

@@ -0,0 +1,64 @@
/** @jsxImportSource @opentui/solid */
import { testRender } from "@opentui/solid"
import { onMount } from "solid-js"
import { ArgsProvider } from "../../../../src/context/args"
import { KVProvider, useKV } from "../../../../src/context/kv"
import { ProjectProvider, useProject } from "../../../../src/context/project"
import { SDKProvider } from "../../../../src/context/sdk"
import { SyncProvider, useSync } from "../../../../src/context/sync"
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
import { TestTuiContexts } from "../../../fixture/tui-environment"
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
export 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)
}
}
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()
let sync!: ReturnType<typeof useSync>
let project!: ReturnType<typeof useProject>
let kv!: ReturnType<typeof useKV>
let done!: () => void
const ready = new Promise<void>((resolve) => {
done = resolve
})
function Probe() {
const ctx: Ctx = { kv: useKV(), project: useProject(), sync: useSync() }
onMount(() => {
sync = ctx.sync
project = ctx.project
kv = ctx.kv
done()
})
return <box />
}
const app = await testRender(() => (
<TestTuiContexts paths={state ? { state } : undefined}>
<ArgsProvider>
<KVProvider>
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
<ProjectProvider>
<SyncProvider>
<Probe />
</SyncProvider>
</ProjectProvider>
</SDKProvider>
</KVProvider>
</ArgsProvider>
</TestTuiContexts>
))
await ready
await wait(() => sync.status === "complete")
return { app, emit: events.emit, kv, project, sync, session: calls.session }
}

View File

@@ -0,0 +1,262 @@
/** @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()
}
})

View File

@@ -0,0 +1,43 @@
/** @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()
}
})
})

View File

@@ -0,0 +1,63 @@
/** @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"
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 },
},
}
}
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("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()
} 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()
}
})
})