@neurocode-ai/tui 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bunfig.toml +4 -0
- package/package.json +72 -0
- package/src/app.tsx +1134 -0
- package/src/attention.ts +260 -0
- package/src/audio.d.ts +9 -0
- package/src/audio.ts +53 -0
- package/src/clipboard.ts +124 -0
- package/src/component/bg-pulse-render.ts +436 -0
- package/src/component/bg-pulse.tsx +99 -0
- package/src/component/command-palette.tsx +79 -0
- package/src/component/dialog-agent.tsx +31 -0
- package/src/component/dialog-console-org.tsx +135 -0
- package/src/component/dialog-debug.tsx +90 -0
- package/src/component/dialog-mcp.tsx +85 -0
- package/src/component/dialog-model.tsx +197 -0
- package/src/component/dialog-move-session.tsx +353 -0
- package/src/component/dialog-provider.tsx +469 -0
- package/src/component/dialog-retry-action.tsx +160 -0
- package/src/component/dialog-session-delete-failed.tsx +99 -0
- package/src/component/dialog-session-list.tsx +364 -0
- package/src/component/dialog-session-rename.tsx +31 -0
- package/src/component/dialog-skill.tsx +70 -0
- package/src/component/dialog-stash.tsx +87 -0
- package/src/component/dialog-status.tsx +168 -0
- package/src/component/dialog-tag.tsx +47 -0
- package/src/component/dialog-theme-list.tsx +50 -0
- package/src/component/dialog-variant.tsx +39 -0
- package/src/component/dialog-workspace-create.tsx +308 -0
- package/src/component/dialog-workspace-file-changes.tsx +144 -0
- package/src/component/dialog-workspace-list.tsx +112 -0
- package/src/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/component/error-component.tsx +240 -0
- package/src/component/logo.tsx +61 -0
- package/src/component/plugin-route-missing.tsx +14 -0
- package/src/component/prompt/autocomplete.tsx +781 -0
- package/src/component/prompt/cwd.ts +0 -0
- package/src/component/prompt/frecency.tsx +1 -0
- package/src/component/prompt/history.tsx +1 -0
- package/src/component/prompt/index.tsx +1713 -0
- package/src/component/prompt/local-attachment.ts +48 -0
- package/src/component/prompt/move.tsx +205 -0
- package/src/component/prompt/stash.tsx +1 -0
- package/src/component/prompt/workspace.tsx +137 -0
- package/src/component/register-spinner.ts +6 -0
- package/src/component/spinner.tsx +26 -0
- package/src/component/startup-loading.tsx +63 -0
- package/src/component/todo-item.tsx +32 -0
- package/src/component/use-connected.tsx +12 -0
- package/src/component/workspace-label.tsx +19 -0
- package/src/config/index.tsx +129 -0
- package/src/config/keybind.ts +471 -0
- package/src/context/args.tsx +16 -0
- package/src/context/clipboard.tsx +18 -0
- package/src/context/data.tsx +569 -0
- package/src/context/directory.ts +17 -0
- package/src/context/editor.ts +408 -0
- package/src/context/epilogue.tsx +6 -0
- package/src/context/event.ts +36 -0
- package/src/context/exit.tsx +8 -0
- package/src/context/helper.tsx +26 -0
- package/src/context/kv.tsx +66 -0
- package/src/context/local.tsx +542 -0
- package/src/context/location.tsx +14 -0
- package/src/context/path-format.tsx +24 -0
- package/src/context/permission.tsx +26 -0
- package/src/context/project.tsx +115 -0
- package/src/context/prompt.tsx +18 -0
- package/src/context/route.tsx +60 -0
- package/src/context/runtime.tsx +62 -0
- package/src/context/sdk.tsx +151 -0
- package/src/context/sync.tsx +666 -0
- package/src/context/theme.tsx +332 -0
- package/src/context/thinking.ts +67 -0
- package/src/editor-zed.ts +286 -0
- package/src/editor.ts +101 -0
- package/src/feature-plugins/builtins.ts +36 -0
- package/src/feature-plugins/home/footer.tsx +100 -0
- package/src/feature-plugins/home/tips-view.tsx +287 -0
- package/src/feature-plugins/home/tips.tsx +59 -0
- package/src/feature-plugins/sidebar/context.tsx +65 -0
- package/src/feature-plugins/sidebar/files.tsx +70 -0
- package/src/feature-plugins/sidebar/footer.tsx +98 -0
- package/src/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
- package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
- package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
- package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
- package/src/feature-plugins/system/notifications.ts +94 -0
- package/src/feature-plugins/system/plugins.tsx +269 -0
- package/src/feature-plugins/system/which-key.tsx +608 -0
- package/src/index.tsx +1 -0
- package/src/keymap.tsx +290 -0
- package/src/logo.ts +11 -0
- package/src/parsers-config.ts +386 -0
- package/src/plugin/adapters.tsx +355 -0
- package/src/plugin/api.ts +52 -0
- package/src/plugin/command-shim.ts +109 -0
- package/src/plugin/runtime.tsx +81 -0
- package/src/plugin/slots.tsx +65 -0
- package/src/prompt/display.ts +48 -0
- package/src/prompt/frecency.tsx +80 -0
- package/src/prompt/history.tsx +111 -0
- package/src/prompt/part.ts +29 -0
- package/src/prompt/stash.tsx +89 -0
- package/src/prompt/traits.ts +29 -0
- package/src/routes/home/session-destination.tsx +41 -0
- package/src/routes/home.tsx +95 -0
- package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/routes/session/dialog-message.tsx +109 -0
- package/src/routes/session/dialog-subagent.tsx +26 -0
- package/src/routes/session/dialog-timeline.tsx +47 -0
- package/src/routes/session/footer.tsx +91 -0
- package/src/routes/session/index.tsx +2710 -0
- package/src/routes/session/permission.tsx +718 -0
- package/src/routes/session/question.tsx +514 -0
- package/src/routes/session/sidebar.tsx +103 -0
- package/src/routes/session/subagent-footer.tsx +132 -0
- package/src/runtime.tsx +9 -0
- package/src/terminal-win32.ts +130 -0
- package/src/theme/assets/aura.json +69 -0
- package/src/theme/assets/ayu.json +80 -0
- package/src/theme/assets/carbonfox.json +248 -0
- package/src/theme/assets/catppuccin-frappe.json +230 -0
- package/src/theme/assets/catppuccin-macchiato.json +230 -0
- package/src/theme/assets/catppuccin.json +112 -0
- package/src/theme/assets/cobalt2.json +225 -0
- package/src/theme/assets/cursor.json +249 -0
- package/src/theme/assets/dracula.json +219 -0
- package/src/theme/assets/everforest.json +241 -0
- package/src/theme/assets/flexoki.json +237 -0
- package/src/theme/assets/github.json +233 -0
- package/src/theme/assets/gruvbox.json +242 -0
- package/src/theme/assets/kanagawa.json +77 -0
- package/src/theme/assets/lucent-orng.json +234 -0
- package/src/theme/assets/material.json +235 -0
- package/src/theme/assets/matrix.json +77 -0
- package/src/theme/assets/mercury.json +252 -0
- package/src/theme/assets/monokai.json +221 -0
- package/src/theme/assets/nightowl.json +221 -0
- package/src/theme/assets/nord.json +223 -0
- package/src/theme/assets/one-dark.json +84 -0
- package/src/theme/assets/opencode.json +245 -0
- package/src/theme/assets/orng.json +249 -0
- package/src/theme/assets/osaka-jade.json +93 -0
- package/src/theme/assets/palenight.json +222 -0
- package/src/theme/assets/rosepine.json +234 -0
- package/src/theme/assets/solarized.json +223 -0
- package/src/theme/assets/synthwave84.json +226 -0
- package/src/theme/assets/tokyonight.json +243 -0
- package/src/theme/assets/vercel.json +245 -0
- package/src/theme/assets/vesper.json +218 -0
- package/src/theme/assets/zenburn.json +223 -0
- package/src/theme/index.ts +1089 -0
- package/src/ui/border.ts +21 -0
- package/src/ui/dialog-alert.tsx +66 -0
- package/src/ui/dialog-confirm.tsx +108 -0
- package/src/ui/dialog-export-options.tsx +217 -0
- package/src/ui/dialog-help.tsx +40 -0
- package/src/ui/dialog-prompt.tsx +126 -0
- package/src/ui/dialog-select.tsx +790 -0
- package/src/ui/dialog.tsx +231 -0
- package/src/ui/link.tsx +34 -0
- package/src/ui/spinner.ts +368 -0
- package/src/ui/toast.tsx +102 -0
- package/src/util/collapse-tool-output.ts +19 -0
- package/src/util/error.ts +182 -0
- package/src/util/filetype.ts +130 -0
- package/src/util/format.ts +20 -0
- package/src/util/layout.ts +25 -0
- package/src/util/locale.ts +86 -0
- package/src/util/model.ts +28 -0
- package/src/util/path.ts +12 -0
- package/src/util/persistence.ts +33 -0
- package/src/util/presentation.ts +38 -0
- package/src/util/provider-origin.ts +7 -0
- package/src/util/record.ts +3 -0
- package/src/util/renderer.ts +7 -0
- package/src/util/revert-diff.ts +18 -0
- package/src/util/scroll.ts +27 -0
- package/src/util/selection.ts +79 -0
- package/src/util/session.ts +3 -0
- package/src/util/signal.ts +51 -0
- package/src/util/system.ts +20 -0
- package/src/util/tool-display.ts +13 -0
- package/src/util/transcript.ts +112 -0
- package/sst-env.d.ts +10 -0
- package/test/app-lifecycle.test.tsx +128 -0
- package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
- package/test/cli/cmd/tui/model-options.test.ts +32 -0
- package/test/cli/cmd/tui/notifications.test.ts +267 -0
- package/test/cli/cmd/tui/provider-options.test.ts +41 -0
- package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
- package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
- package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
- package/test/cli/cmd/tui/sync.test.tsx +65 -0
- package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
- package/test/cli/tui/data.test.tsx +486 -0
- package/test/cli/tui/dialog-prompt.test.tsx +147 -0
- package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
- package/test/cli/tui/diff-viewer.test.tsx +268 -0
- package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
- package/test/cli/tui/prompt-submit-race.test.ts +98 -0
- package/test/cli/tui/thinking.test.ts +36 -0
- package/test/cli/tui/use-event.test.tsx +148 -0
- package/test/clipboard.test.ts +19 -0
- package/test/component/dialog-session-list.test.ts +46 -0
- package/test/config.test.tsx +127 -0
- package/test/context/local.test.ts +22 -0
- package/test/editor.test.ts +32 -0
- package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
- package/test/fixture/fixture.ts +13 -0
- package/test/fixture/tui-environment.tsx +32 -0
- package/test/fixture/tui-plugin.ts +36 -0
- package/test/fixture/tui-runtime.ts +12 -0
- package/test/fixture/tui-sdk.ts +109 -0
- package/test/index.test.tsx +6 -0
- package/test/keymap.test.tsx +141 -0
- package/test/plugin/runtime.test.ts +50 -0
- package/test/plugin/slots.test.tsx +38 -0
- package/test/prompt/display.test.ts +33 -0
- package/test/prompt/history.test.ts +39 -0
- package/test/prompt/jsonl.test.ts +24 -0
- package/test/prompt/local-attachment.test.ts +43 -0
- package/test/prompt/part.test.ts +53 -0
- package/test/prompt/persistence.test.ts +23 -0
- package/test/prompt/traits.test.ts +25 -0
- package/test/runtime.test.tsx +37 -0
- package/test/theme.test.ts +81 -0
- package/test/util/error.test.ts +49 -0
- package/test/util/filetype.test.ts +16 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/model.test.ts +9 -0
- package/test/util/presentation.test.ts +8 -0
- package/test/util/renderer.test.ts +30 -0
- package/test/util/revert-diff.test.ts +35 -0
- package/test/util/session.test.ts +10 -0
- package/test/util/tool-display.test.ts +40 -0
- package/test/util/transcript.test.ts +421 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import Notifications from "../../../../src/feature-plugins/system/notifications"
|
|
3
|
+
import type { Event, PermissionRequest, QuestionRequest, Session } from "@neurocode-ai/sdk/v2"
|
|
4
|
+
import type { TuiAttentionNotifyInput } from "@neurocode-ai/plugin/tui"
|
|
5
|
+
import { createTuiPluginApi } from "../../../fixture/tui-plugin"
|
|
6
|
+
|
|
7
|
+
async function setup() {
|
|
8
|
+
const notifications: TuiAttentionNotifyInput[] = []
|
|
9
|
+
const handlers = new Map<Event["type"], ((event: Event) => void)[]>()
|
|
10
|
+
const session = (id: string, title: string, parentID?: string): Session => ({
|
|
11
|
+
id,
|
|
12
|
+
title,
|
|
13
|
+
slug: id,
|
|
14
|
+
projectID: "project",
|
|
15
|
+
directory: "/workspace",
|
|
16
|
+
...(parentID && { parentID }),
|
|
17
|
+
version: "0.0.0-test",
|
|
18
|
+
time: { created: 0, updated: 0 },
|
|
19
|
+
})
|
|
20
|
+
const sessions: Record<string, Session> = {
|
|
21
|
+
session: session("session", "Demo session"),
|
|
22
|
+
subagent: session("subagent", "Subagent session", "session"),
|
|
23
|
+
abort: session("abort", "Abort session"),
|
|
24
|
+
timeout: session("timeout", "Timeout session"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await Notifications.tui(
|
|
28
|
+
createTuiPluginApi({
|
|
29
|
+
attention: {
|
|
30
|
+
async notify(input) {
|
|
31
|
+
notifications.push(input)
|
|
32
|
+
return { ok: true, notification: true, sound: true }
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
event: {
|
|
36
|
+
on: <Type extends Event["type"]>(type: Type, handler: (event: Extract<Event, { type: Type }>) => void) => {
|
|
37
|
+
const list = handlers.get(type) ?? []
|
|
38
|
+
const wrapped = handler as (event: Event) => void
|
|
39
|
+
list.push(wrapped)
|
|
40
|
+
handlers.set(type, list)
|
|
41
|
+
return () => {
|
|
42
|
+
handlers.set(
|
|
43
|
+
type,
|
|
44
|
+
(handlers.get(type) ?? []).filter((item) => item !== wrapped),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
state: {
|
|
50
|
+
session: {
|
|
51
|
+
get: (sessionID: string) => sessions[sessionID],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
undefined,
|
|
56
|
+
{} as never,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
notifications,
|
|
61
|
+
emit(event: Event) {
|
|
62
|
+
for (const handler of handlers.get(event.type) ?? []) handler(event)
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function question(id: string, sessionID = "session"): QuestionRequest {
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
sessionID,
|
|
71
|
+
questions: [],
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function permission(id: string, sessionID = "session"): PermissionRequest {
|
|
76
|
+
return {
|
|
77
|
+
id,
|
|
78
|
+
sessionID,
|
|
79
|
+
permission: "edit",
|
|
80
|
+
patterns: [],
|
|
81
|
+
metadata: {},
|
|
82
|
+
always: [],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const questionNotification: TuiAttentionNotifyInput = {
|
|
87
|
+
title: "Demo session",
|
|
88
|
+
message: "Question needs input",
|
|
89
|
+
notification: { when: "blurred" },
|
|
90
|
+
sound: { name: "question", when: "always" },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const permissionNotification: TuiAttentionNotifyInput = {
|
|
94
|
+
title: "Demo session",
|
|
95
|
+
message: "Permission needs input",
|
|
96
|
+
notification: { when: "blurred" },
|
|
97
|
+
sound: { name: "permission", when: "always" },
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("internal notifications TUI plugin", () => {
|
|
101
|
+
test("notifies for question and permission requests with blurred notifications and always-on sounds", async () => {
|
|
102
|
+
const harness = await setup()
|
|
103
|
+
|
|
104
|
+
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") })
|
|
105
|
+
harness.emit({ id: "event-2", type: "permission.asked", properties: permission("permission-1") })
|
|
106
|
+
|
|
107
|
+
expect(harness.notifications).toEqual([questionNotification, permissionNotification])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("dedupes pending questions and permissions until they are resolved", async () => {
|
|
111
|
+
const harness = await setup()
|
|
112
|
+
|
|
113
|
+
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1") })
|
|
114
|
+
harness.emit({ id: "event-2", type: "question.asked", properties: question("question-1") })
|
|
115
|
+
harness.emit({
|
|
116
|
+
id: "event-3",
|
|
117
|
+
type: "question.replied",
|
|
118
|
+
properties: { sessionID: "session", requestID: "question-1", answers: [] },
|
|
119
|
+
})
|
|
120
|
+
harness.emit({ id: "event-4", type: "question.asked", properties: question("question-1") })
|
|
121
|
+
|
|
122
|
+
harness.emit({ id: "event-5", type: "permission.asked", properties: permission("permission-1") })
|
|
123
|
+
harness.emit({ id: "event-6", type: "permission.asked", properties: permission("permission-1") })
|
|
124
|
+
harness.emit({
|
|
125
|
+
id: "event-7",
|
|
126
|
+
type: "permission.replied",
|
|
127
|
+
properties: { sessionID: "session", requestID: "permission-1", reply: "once" },
|
|
128
|
+
})
|
|
129
|
+
harness.emit({ id: "event-8", type: "permission.asked", properties: permission("permission-1") })
|
|
130
|
+
|
|
131
|
+
expect(harness.notifications).toEqual([
|
|
132
|
+
questionNotification,
|
|
133
|
+
questionNotification,
|
|
134
|
+
permissionNotification,
|
|
135
|
+
permissionNotification,
|
|
136
|
+
])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("notifies when an active session becomes idle and suppresses no-op idle", async () => {
|
|
140
|
+
const harness = await setup()
|
|
141
|
+
|
|
142
|
+
harness.emit({
|
|
143
|
+
id: "event-1",
|
|
144
|
+
type: "session.status",
|
|
145
|
+
properties: { sessionID: "session", status: { type: "idle" } },
|
|
146
|
+
})
|
|
147
|
+
harness.emit({
|
|
148
|
+
id: "event-2",
|
|
149
|
+
type: "session.status",
|
|
150
|
+
properties: { sessionID: "session", status: { type: "busy" } },
|
|
151
|
+
})
|
|
152
|
+
harness.emit({
|
|
153
|
+
id: "event-3",
|
|
154
|
+
type: "session.status",
|
|
155
|
+
properties: { sessionID: "session", status: { type: "idle" } },
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(harness.notifications).toEqual([
|
|
159
|
+
{
|
|
160
|
+
title: "Demo session",
|
|
161
|
+
message: "Session done",
|
|
162
|
+
notification: { when: "blurred" },
|
|
163
|
+
sound: { name: "done", when: "always" },
|
|
164
|
+
},
|
|
165
|
+
])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => {
|
|
169
|
+
const harness = await setup()
|
|
170
|
+
|
|
171
|
+
harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1", "subagent") })
|
|
172
|
+
harness.emit({
|
|
173
|
+
id: "event-2",
|
|
174
|
+
type: "session.status",
|
|
175
|
+
properties: { sessionID: "subagent", status: { type: "busy" } },
|
|
176
|
+
})
|
|
177
|
+
harness.emit({
|
|
178
|
+
id: "event-3",
|
|
179
|
+
type: "session.status",
|
|
180
|
+
properties: { sessionID: "subagent", status: { type: "idle" } },
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
expect(harness.notifications).toEqual([
|
|
184
|
+
{
|
|
185
|
+
title: "Subagent session",
|
|
186
|
+
message: "Question needs input",
|
|
187
|
+
notification: false,
|
|
188
|
+
sound: { name: "question", when: "always" },
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
title: "Subagent session",
|
|
192
|
+
message: "Session done",
|
|
193
|
+
notification: false,
|
|
194
|
+
sound: { name: "subagent_done", when: "always" },
|
|
195
|
+
},
|
|
196
|
+
])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test("notifies session errors once and suppresses the following idle done notification", async () => {
|
|
200
|
+
const harness = await setup()
|
|
201
|
+
|
|
202
|
+
harness.emit({
|
|
203
|
+
id: "event-1",
|
|
204
|
+
type: "session.status",
|
|
205
|
+
properties: { sessionID: "session", status: { type: "busy" } },
|
|
206
|
+
})
|
|
207
|
+
harness.emit({
|
|
208
|
+
id: "event-2",
|
|
209
|
+
type: "session.error",
|
|
210
|
+
properties: { sessionID: "session", error: { name: "UnknownError", data: { message: "boom" } } },
|
|
211
|
+
})
|
|
212
|
+
harness.emit({
|
|
213
|
+
id: "event-3",
|
|
214
|
+
type: "session.status",
|
|
215
|
+
properties: { sessionID: "session", status: { type: "idle" } },
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(harness.notifications).toEqual([
|
|
219
|
+
{
|
|
220
|
+
title: "Demo session",
|
|
221
|
+
message: "Session error",
|
|
222
|
+
notification: { when: "blurred" },
|
|
223
|
+
sound: { name: "error", when: "always" },
|
|
224
|
+
},
|
|
225
|
+
])
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test("special-cases aborts and model response timeouts", async () => {
|
|
229
|
+
const harness = await setup()
|
|
230
|
+
|
|
231
|
+
harness.emit({
|
|
232
|
+
id: "event-1",
|
|
233
|
+
type: "session.status",
|
|
234
|
+
properties: { sessionID: "abort", status: { type: "busy" } },
|
|
235
|
+
})
|
|
236
|
+
harness.emit({
|
|
237
|
+
id: "event-2",
|
|
238
|
+
type: "session.error",
|
|
239
|
+
properties: { sessionID: "abort", error: { name: "MessageAbortedError", data: { message: "Aborted" } } },
|
|
240
|
+
})
|
|
241
|
+
harness.emit({
|
|
242
|
+
id: "event-3",
|
|
243
|
+
type: "session.status",
|
|
244
|
+
properties: { sessionID: "timeout", status: { type: "busy" } },
|
|
245
|
+
})
|
|
246
|
+
harness.emit({
|
|
247
|
+
id: "event-4",
|
|
248
|
+
type: "session.error",
|
|
249
|
+
properties: { sessionID: "timeout", error: { name: "UnknownError", data: { message: "SSE read timed out" } } },
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
expect(harness.notifications).toEqual([
|
|
253
|
+
{
|
|
254
|
+
title: "Abort session",
|
|
255
|
+
message: "Session aborted",
|
|
256
|
+
notification: { when: "blurred" },
|
|
257
|
+
sound: { name: "error", when: "always" },
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
title: "Timeout session",
|
|
261
|
+
message: "Model stopped responding",
|
|
262
|
+
notification: { when: "blurred" },
|
|
263
|
+
sound: { name: "error", when: "always" },
|
|
264
|
+
},
|
|
265
|
+
])
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { normalizeCustomProviderID, providerOptions } from "../../../../src/component/dialog-provider"
|
|
3
|
+
|
|
4
|
+
describe("providerOptions", () => {
|
|
5
|
+
test("includes a synthetic Other option for custom providers", () => {
|
|
6
|
+
expect(providerOptions([{ id: "openai", name: "OpenAI" }]).at(-1)).toMatchObject({
|
|
7
|
+
title: "Other",
|
|
8
|
+
description: "Custom provider",
|
|
9
|
+
category: "Providers",
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test("does not use Other as the generic provider category", () => {
|
|
14
|
+
expect(providerOptions([{ id: "mistral", name: "Mistral" }])[0]?.category).toBe("Providers")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("keeps popular providers first and sorts the rest alphabetically", () => {
|
|
18
|
+
expect(
|
|
19
|
+
providerOptions([
|
|
20
|
+
{ id: "openai", name: "OpenAI" },
|
|
21
|
+
{ id: "custom-z", name: "Zebra Provider" },
|
|
22
|
+
{ id: "anthropic", name: "Anthropic" },
|
|
23
|
+
{ id: "mistral", name: "Mistral" },
|
|
24
|
+
{ id: "aws", name: "AWS Bedrock" },
|
|
25
|
+
]).map((option) => option.value),
|
|
26
|
+
).toEqual(["openai", "anthropic", "aws", "mistral", "custom-z", "__NEUROCODE_custom_provider__"])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("does not collide with a configured provider named other", () => {
|
|
30
|
+
const values = providerOptions([{ id: "other", name: "Other Provider" }]).map((option) => option.value)
|
|
31
|
+
expect(new Set(values).size).toBe(values.length)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("normalizes and validates custom provider ids", () => {
|
|
35
|
+
expect(normalizeCustomProviderID(" custom-provider ")).toBe("custom-provider")
|
|
36
|
+
expect(normalizeCustomProviderID("custom_provider")).toBe("custom_provider")
|
|
37
|
+
expect(normalizeCustomProviderID("@ai-sdk/custom-provider")).toBe("custom-provider")
|
|
38
|
+
expect(normalizeCustomProviderID("-custom-provider")).toBeUndefined()
|
|
39
|
+
expect(normalizeCustomProviderID("Custom Provider")).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { testRender } from "@opentui/solid"
|
|
3
|
+
import { onMount } from "solid-js"
|
|
4
|
+
import { ArgsProvider } from "../../../../src/context/args"
|
|
5
|
+
import { KVProvider, useKV } from "../../../../src/context/kv"
|
|
6
|
+
import { ProjectProvider, useProject } from "../../../../src/context/project"
|
|
7
|
+
import { SDKProvider } from "../../../../src/context/sdk"
|
|
8
|
+
import { SyncProvider, useSync } from "../../../../src/context/sync"
|
|
9
|
+
import { PermissionProvider } from "../../../../src/context/permission"
|
|
10
|
+
import { ExitProvider } from "../../../../src/context/exit"
|
|
11
|
+
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
|
|
12
|
+
import { TestTuiContexts } from "../../../fixture/tui-environment"
|
|
13
|
+
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
|
|
14
|
+
|
|
15
|
+
export async function wait(fn: () => boolean, timeout = 2000) {
|
|
16
|
+
const start = Date.now()
|
|
17
|
+
while (!fn()) {
|
|
18
|
+
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
|
19
|
+
await Bun.sleep(10)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Ctx = { kv: ReturnType<typeof useKV>; project: ReturnType<typeof useProject>; sync: ReturnType<typeof useSync> }
|
|
24
|
+
|
|
25
|
+
export async function mount(override?: FetchHandler, state?: string) {
|
|
26
|
+
const calls = createFetch(override)
|
|
27
|
+
const events = createEventSource()
|
|
28
|
+
let sync!: ReturnType<typeof useSync>
|
|
29
|
+
let project!: ReturnType<typeof useProject>
|
|
30
|
+
let kv!: ReturnType<typeof useKV>
|
|
31
|
+
let done!: () => void
|
|
32
|
+
const ready = new Promise<void>((resolve) => {
|
|
33
|
+
done = resolve
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function Probe() {
|
|
37
|
+
const ctx: Ctx = { kv: useKV(), project: useProject(), sync: useSync() }
|
|
38
|
+
onMount(() => {
|
|
39
|
+
sync = ctx.sync
|
|
40
|
+
project = ctx.project
|
|
41
|
+
kv = ctx.kv
|
|
42
|
+
done()
|
|
43
|
+
})
|
|
44
|
+
return <box />
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const app = await testRender(() => (
|
|
48
|
+
<TestTuiContexts paths={state ? { state } : undefined}>
|
|
49
|
+
<ArgsProvider>
|
|
50
|
+
<KVProvider>
|
|
51
|
+
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
|
|
52
|
+
<PermissionProvider>
|
|
53
|
+
<ProjectProvider>
|
|
54
|
+
<ExitProvider exit={() => {}}>
|
|
55
|
+
<SyncProvider>
|
|
56
|
+
<Probe />
|
|
57
|
+
</SyncProvider>
|
|
58
|
+
</ExitProvider>
|
|
59
|
+
</ProjectProvider>
|
|
60
|
+
</PermissionProvider>
|
|
61
|
+
</SDKProvider>
|
|
62
|
+
</KVProvider>
|
|
63
|
+
</ArgsProvider>
|
|
64
|
+
</TestTuiContexts>
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
await ready
|
|
68
|
+
await wait(() => sync.status === "complete")
|
|
69
|
+
return { app, emit: events.emit, kv, project, sync, session: calls.session }
|
|
70
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { expect, test } from "bun:test"
|
|
3
|
+
import type { GlobalEvent } from "@neurocode-ai/sdk/v2"
|
|
4
|
+
import { tmpdir } from "../../../fixture/fixture"
|
|
5
|
+
import { json, mount, wait } from "./sync-fixture"
|
|
6
|
+
|
|
7
|
+
const sessionID = "ses_hydration_race"
|
|
8
|
+
const messageID = "msg_hydration_race"
|
|
9
|
+
const partID = "prt_hydration_race"
|
|
10
|
+
const session = {
|
|
11
|
+
id: sessionID,
|
|
12
|
+
title: "race",
|
|
13
|
+
time: { created: 0, updated: 0 },
|
|
14
|
+
version: "1.15.13",
|
|
15
|
+
directory: "/tmp/opencode/packages/opencode",
|
|
16
|
+
}
|
|
17
|
+
const assistant = {
|
|
18
|
+
id: messageID,
|
|
19
|
+
sessionID,
|
|
20
|
+
role: "assistant" as const,
|
|
21
|
+
agent: "build",
|
|
22
|
+
modelID: "model",
|
|
23
|
+
providerID: "test",
|
|
24
|
+
mode: "build",
|
|
25
|
+
parentID: "msg_user",
|
|
26
|
+
path: { cwd: session.directory, root: session.directory },
|
|
27
|
+
cost: 0,
|
|
28
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
29
|
+
time: { created: 1, completed: 2 },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function global(payload: GlobalEvent["payload"]): GlobalEvent {
|
|
33
|
+
return { directory: "/tmp/other", project: "proj_test", payload }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("stale session hydration does not overwrite live message parts", async () => {
|
|
37
|
+
await using tmp = await tmpdir()
|
|
38
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
39
|
+
|
|
40
|
+
let resolveMessages!: (response: Response) => void
|
|
41
|
+
const messages = new Promise<Response>((resolve) => {
|
|
42
|
+
resolveMessages = resolve
|
|
43
|
+
})
|
|
44
|
+
let requested = false
|
|
45
|
+
const { app, emit, sync } = await mount((url) => {
|
|
46
|
+
if (url.pathname === `/session/${sessionID}`) return json(session)
|
|
47
|
+
if (url.pathname === `/session/${sessionID}/message`) {
|
|
48
|
+
requested = true
|
|
49
|
+
return messages
|
|
50
|
+
}
|
|
51
|
+
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
|
52
|
+
return undefined
|
|
53
|
+
}, tmp.path)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const hydrate = sync.session.sync(sessionID)
|
|
57
|
+
await wait(() => requested)
|
|
58
|
+
emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } }))
|
|
59
|
+
emit(
|
|
60
|
+
global({
|
|
61
|
+
id: "evt_part",
|
|
62
|
+
type: "message.part.updated",
|
|
63
|
+
properties: {
|
|
64
|
+
sessionID,
|
|
65
|
+
time: 2,
|
|
66
|
+
part: { id: partID, sessionID, messageID, type: "text", text: "visible live content" },
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
await wait(() => sync.data.part[messageID]?.[0]?.type === "text")
|
|
71
|
+
|
|
72
|
+
resolveMessages(
|
|
73
|
+
json([
|
|
74
|
+
{
|
|
75
|
+
info: assistant,
|
|
76
|
+
parts: [{ id: partID, sessionID, messageID, type: "text", text: "" }],
|
|
77
|
+
},
|
|
78
|
+
]),
|
|
79
|
+
)
|
|
80
|
+
await hydrate
|
|
81
|
+
|
|
82
|
+
expect(sync.data.part[messageID][0]).toMatchObject({ text: "visible live content" })
|
|
83
|
+
} finally {
|
|
84
|
+
app.renderer.destroy()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("orphan live deltas do not suppress hydrated parts", async () => {
|
|
89
|
+
await using tmp = await tmpdir()
|
|
90
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
91
|
+
|
|
92
|
+
let resolveMessages!: (response: Response) => void
|
|
93
|
+
const messages = new Promise<Response>((resolve) => {
|
|
94
|
+
resolveMessages = resolve
|
|
95
|
+
})
|
|
96
|
+
let requested = false
|
|
97
|
+
const { app, emit, sync } = await mount((url) => {
|
|
98
|
+
if (url.pathname === `/session/${sessionID}`) return json(session)
|
|
99
|
+
if (url.pathname === `/session/${sessionID}/message`) {
|
|
100
|
+
requested = true
|
|
101
|
+
return messages
|
|
102
|
+
}
|
|
103
|
+
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
|
104
|
+
return undefined
|
|
105
|
+
}, tmp.path)
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const hydrate = sync.session.sync(sessionID)
|
|
109
|
+
await wait(() => requested)
|
|
110
|
+
emit(
|
|
111
|
+
global({
|
|
112
|
+
id: "evt_delta",
|
|
113
|
+
type: "message.part.delta",
|
|
114
|
+
properties: { sessionID, messageID, partID, field: "text", delta: "ignored until part exists" },
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
resolveMessages(
|
|
118
|
+
json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "hydrated" }] }]),
|
|
119
|
+
)
|
|
120
|
+
await hydrate
|
|
121
|
+
|
|
122
|
+
expect(sync.data.part[messageID][0]).toMatchObject({ text: "hydrated" })
|
|
123
|
+
} finally {
|
|
124
|
+
app.renderer.destroy()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("hydration does not clear text streamed before it starts", async () => {
|
|
129
|
+
await using tmp = await tmpdir()
|
|
130
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
131
|
+
|
|
132
|
+
let resolveMessages!: (response: Response) => void
|
|
133
|
+
const messages = new Promise<Response>((resolve) => {
|
|
134
|
+
resolveMessages = resolve
|
|
135
|
+
})
|
|
136
|
+
let requested = false
|
|
137
|
+
const { app, emit, sync } = await mount((url) => {
|
|
138
|
+
if (url.pathname === `/session/${sessionID}`) return json(session)
|
|
139
|
+
if (url.pathname === `/session/${sessionID}/message`) {
|
|
140
|
+
requested = true
|
|
141
|
+
return messages
|
|
142
|
+
}
|
|
143
|
+
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
|
144
|
+
return undefined
|
|
145
|
+
}, tmp.path)
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } }))
|
|
149
|
+
emit(
|
|
150
|
+
global({
|
|
151
|
+
id: "evt_part",
|
|
152
|
+
type: "message.part.updated",
|
|
153
|
+
properties: {
|
|
154
|
+
sessionID,
|
|
155
|
+
time: 1,
|
|
156
|
+
part: { id: partID, sessionID, messageID, type: "text", text: "" },
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
emit(
|
|
161
|
+
global({
|
|
162
|
+
id: "evt_delta",
|
|
163
|
+
type: "message.part.delta",
|
|
164
|
+
properties: { sessionID, messageID, partID, field: "text", delta: "visible streamed content" },
|
|
165
|
+
}),
|
|
166
|
+
)
|
|
167
|
+
await wait(() => sync.data.part[messageID]?.[0]?.type === "text" && sync.data.part[messageID][0].text !== "")
|
|
168
|
+
const hydrate = sync.session.sync(sessionID)
|
|
169
|
+
await wait(() => requested)
|
|
170
|
+
resolveMessages(json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "" }] }]))
|
|
171
|
+
await hydrate
|
|
172
|
+
|
|
173
|
+
expect(sync.data.part[messageID][0]).toMatchObject({ text: "visible streamed content" })
|
|
174
|
+
} finally {
|
|
175
|
+
app.renderer.destroy()
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("live messages merged during hydration retain the 100 message window", async () => {
|
|
180
|
+
await using tmp = await tmpdir()
|
|
181
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
182
|
+
|
|
183
|
+
let resolveMessages!: (response: Response) => void
|
|
184
|
+
const messages = new Promise<Response>((resolve) => {
|
|
185
|
+
resolveMessages = resolve
|
|
186
|
+
})
|
|
187
|
+
let requested = false
|
|
188
|
+
const { app, emit, sync } = await mount((url) => {
|
|
189
|
+
if (url.pathname === `/session/${sessionID}`) return json(session)
|
|
190
|
+
if (url.pathname === `/session/${sessionID}/message`) {
|
|
191
|
+
requested = true
|
|
192
|
+
return messages
|
|
193
|
+
}
|
|
194
|
+
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
|
195
|
+
return undefined
|
|
196
|
+
}, tmp.path)
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const hydrate = sync.session.sync(sessionID)
|
|
200
|
+
await wait(() => requested)
|
|
201
|
+
const live = { ...assistant, id: "msg_z_live" }
|
|
202
|
+
emit(global({ id: "evt_live", type: "message.updated", properties: { sessionID, info: live } }))
|
|
203
|
+
await wait(() => sync.data.message[sessionID]?.some((message) => message.id === live.id) ?? false)
|
|
204
|
+
resolveMessages(
|
|
205
|
+
json(
|
|
206
|
+
Array.from({ length: 100 }, (_, index) => {
|
|
207
|
+
const id = `msg_${String(index).padStart(3, "0")}`
|
|
208
|
+
return {
|
|
209
|
+
info: { ...assistant, id },
|
|
210
|
+
parts: [{ id: `prt_${id}`, sessionID, messageID: id, type: "text", text: id }],
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
await hydrate
|
|
216
|
+
|
|
217
|
+
expect(sync.data.message[sessionID]).toHaveLength(100)
|
|
218
|
+
expect(sync.data.message[sessionID].at(-1)?.id).toBe(live.id)
|
|
219
|
+
expect(sync.data.message[sessionID].some((message) => message.id === "msg_000")).toBe(false)
|
|
220
|
+
expect(sync.data.part.msg_000).toBeUndefined()
|
|
221
|
+
} finally {
|
|
222
|
+
app.renderer.destroy()
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("a message removed during hydration does not regain stale parts", async () => {
|
|
227
|
+
await using tmp = await tmpdir()
|
|
228
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
229
|
+
|
|
230
|
+
let resolveMessages!: (response: Response) => void
|
|
231
|
+
const messages = new Promise<Response>((resolve) => {
|
|
232
|
+
resolveMessages = resolve
|
|
233
|
+
})
|
|
234
|
+
let requested = false
|
|
235
|
+
const { app, emit, sync } = await mount((url) => {
|
|
236
|
+
if (url.pathname === `/session/${sessionID}`) return json(session)
|
|
237
|
+
if (url.pathname === `/session/${sessionID}/message`) {
|
|
238
|
+
requested = true
|
|
239
|
+
return messages
|
|
240
|
+
}
|
|
241
|
+
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
|
242
|
+
return undefined
|
|
243
|
+
}, tmp.path)
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } }))
|
|
247
|
+
await wait(() => sync.data.message[sessionID]?.length === 1)
|
|
248
|
+
const hydrate = sync.session.sync(sessionID)
|
|
249
|
+
await wait(() => requested)
|
|
250
|
+
emit(global({ id: "evt_removed", type: "message.removed", properties: { sessionID, messageID } }))
|
|
251
|
+
await wait(() => sync.data.message[sessionID]?.length === 0)
|
|
252
|
+
resolveMessages(
|
|
253
|
+
json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "stale" }] }]),
|
|
254
|
+
)
|
|
255
|
+
await hydrate
|
|
256
|
+
|
|
257
|
+
expect(sync.data.message[sessionID]).toEqual([])
|
|
258
|
+
expect(sync.data.part[messageID]).toBeUndefined()
|
|
259
|
+
} finally {
|
|
260
|
+
app.renderer.destroy()
|
|
261
|
+
}
|
|
262
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* Reproducer for #26560 — TUI crashes with
|
|
4
|
+
* `TypeError: undefined is not an object (evaluating 'f.data.map')`
|
|
5
|
+
* when entering a session whose messages endpoint returns a non-2xx.
|
|
6
|
+
* The failure path is `sync.tsx#sync.session.sync` reading
|
|
7
|
+
* `messages.data!` while the SDK leaves `data` undefined on error.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from "bun:test"
|
|
10
|
+
import { tmpdir } from "../../../fixture/fixture"
|
|
11
|
+
import { directory, json, mount } from "./sync-fixture"
|
|
12
|
+
|
|
13
|
+
const sessionID = "ses_undef"
|
|
14
|
+
|
|
15
|
+
describe("tui sync (#26560)", () => {
|
|
16
|
+
test("entering a session whose messages endpoint errors does not crash sync", async () => {
|
|
17
|
+
await using tmp = await tmpdir()
|
|
18
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
19
|
+
|
|
20
|
+
const sessionPayload = {
|
|
21
|
+
id: sessionID,
|
|
22
|
+
title: "broken",
|
|
23
|
+
time: { created: 0, updated: 0 },
|
|
24
|
+
version: "1.14.42",
|
|
25
|
+
directory,
|
|
26
|
+
project_id: "proj_test",
|
|
27
|
+
}
|
|
28
|
+
const { app, sync } = await mount((url) => {
|
|
29
|
+
if (url.pathname === `/session/${sessionID}`) return json(sessionPayload)
|
|
30
|
+
if (url.pathname === `/session/${sessionID}/messages`) return json({}, { status: 500 })
|
|
31
|
+
if (url.pathname === `/session/${sessionID}/todo`) return json([])
|
|
32
|
+
if (url.pathname === `/session/${sessionID}/diff`) return json([])
|
|
33
|
+
if (url.pathname === "/session") return json([sessionPayload])
|
|
34
|
+
return undefined
|
|
35
|
+
}, tmp.path)
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await expect(sync.session.sync(sessionID)).resolves.toBeUndefined()
|
|
39
|
+
} finally {
|
|
40
|
+
app.renderer.destroy()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|