@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test, type Mock } from "bun:test";
|
|
2
|
+
import { ManualClock } from "@opentui/core/testing";
|
|
3
|
+
import { SCHEMA_VERSION, type Annotation, type ShareAccess, type Thread } from "@cueloop/schema";
|
|
4
|
+
import type { ThreadClient } from "@cueloop/daemon/client";
|
|
5
|
+
import {
|
|
6
|
+
createReviewController,
|
|
7
|
+
SHARE_RECONNECT_MAX_MS,
|
|
8
|
+
SHARE_RECONNECT_MIN_MS,
|
|
9
|
+
type ShareTransport,
|
|
10
|
+
} from "./thread-controller";
|
|
11
|
+
import { mergeFromShare, type ShareWatchHandlers } from "./share";
|
|
12
|
+
|
|
13
|
+
const publishShare = mock(async () => ({ line: "ssh p_abc123xy@cueloop.dev", copied: true }));
|
|
14
|
+
let remote: Thread;
|
|
15
|
+
const pullShare = mock(async () => remote);
|
|
16
|
+
const pushShare = mock(
|
|
17
|
+
async (
|
|
18
|
+
_shareId: string,
|
|
19
|
+
_annotations: Array<Omit<Annotation, "createdAt">>,
|
|
20
|
+
_access?: ShareAccess | "public",
|
|
21
|
+
) => {},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const revokeShare = mock(async (_shareId: string) => {});
|
|
25
|
+
|
|
26
|
+
const shareTransport: ShareTransport = {
|
|
27
|
+
publish: publishShare,
|
|
28
|
+
pull: pullShare,
|
|
29
|
+
push: pushShare,
|
|
30
|
+
watch: () => () => {},
|
|
31
|
+
revoke: revokeShare,
|
|
32
|
+
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
33
|
+
formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
|
|
34
|
+
collaboratorAnnotations: (session) => session.annotations.filter((entry) => entry.author),
|
|
35
|
+
mergeFromShare,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function sessionFixture(overrides: Partial<Thread> = {}): Thread {
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: SCHEMA_VERSION,
|
|
41
|
+
id: "ses_1",
|
|
42
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
43
|
+
artifact: { type: "plan", content: "# Plan\n", meta: {} },
|
|
44
|
+
revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00.000Z" }],
|
|
45
|
+
annotations: [],
|
|
46
|
+
message: null,
|
|
47
|
+
status: "pending",
|
|
48
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function annotation(id: string, author: string): Annotation {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
kind: "comment",
|
|
57
|
+
anchor: { quote: "Plan", prefix: "", suffix: "" },
|
|
58
|
+
body: "note",
|
|
59
|
+
author,
|
|
60
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface FakeThreadClient extends ThreadClient {
|
|
65
|
+
sessionComment: Mock<ThreadClient["sessionComment"]>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const unimplemented = (member: string) => () =>
|
|
69
|
+
Promise.reject(new Error(`fakeClient does not implement ${member}`));
|
|
70
|
+
|
|
71
|
+
function fakeClient(session: Thread): FakeThreadClient {
|
|
72
|
+
return {
|
|
73
|
+
onEvent: () => () => {},
|
|
74
|
+
subscribe: async () => {},
|
|
75
|
+
sessionGet: async () => session,
|
|
76
|
+
sessionList: async () => [session],
|
|
77
|
+
sessionComment: mock<ThreadClient["sessionComment"]>(async () => session),
|
|
78
|
+
sessionAnnotate: unimplemented("sessionAnnotate"),
|
|
79
|
+
sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
|
|
80
|
+
sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
|
|
81
|
+
sessionCutBlock: unimplemented("sessionCutBlock"),
|
|
82
|
+
sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
|
|
83
|
+
sessionCurate: unimplemented("sessionCurate"),
|
|
84
|
+
sessionSetAccess: mock(async () => session),
|
|
85
|
+
sessionNavigate: unimplemented("sessionNavigate"),
|
|
86
|
+
sessionBranch: unimplemented("sessionBranch"),
|
|
87
|
+
sessionSwitch: unimplemented("sessionSwitch"),
|
|
88
|
+
sessionLabel: unimplemented("sessionLabel"),
|
|
89
|
+
sessionFork: unimplemented("sessionFork"),
|
|
90
|
+
sessionSetViewed: unimplemented("sessionSetViewed"),
|
|
91
|
+
sessionSetTitle: unimplemented("sessionSetTitle"),
|
|
92
|
+
sessionSetShareId: mock(
|
|
93
|
+
async (_id: string, shareId: string) => ((session.shareId = shareId), session),
|
|
94
|
+
),
|
|
95
|
+
sessionSetShares: mock(
|
|
96
|
+
async (_id: string, shares: import("@cueloop/schema").ShareLink[]) => (
|
|
97
|
+
(session.shares = shares),
|
|
98
|
+
session
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
sessionMergeShared: mock(async (_id: string, incoming: { annotations: Annotation[] }) => {
|
|
102
|
+
const known = new Set(session.annotations.map((existing) => existing.id));
|
|
103
|
+
|
|
104
|
+
for (const note of incoming.annotations)
|
|
105
|
+
if (!known.has(note.id)) session.annotations.push(note);
|
|
106
|
+
|
|
107
|
+
return session;
|
|
108
|
+
}),
|
|
109
|
+
sessionDelete: unimplemented("sessionDelete"),
|
|
110
|
+
sessionSetSelfName: unimplemented("sessionSetSelfName"),
|
|
111
|
+
sessionSendMessage: unimplemented("sessionSendMessage"),
|
|
112
|
+
close: () => {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
117
|
+
|
|
118
|
+
async function connectedController(
|
|
119
|
+
session: Thread,
|
|
120
|
+
clock?: ManualClock,
|
|
121
|
+
transport: ShareTransport = shareTransport,
|
|
122
|
+
): Promise<{ controller: ReturnType<typeof createReviewController>; client: FakeThreadClient }> {
|
|
123
|
+
const client = fakeClient(session);
|
|
124
|
+
const controller = createReviewController({
|
|
125
|
+
sessionId: session.id,
|
|
126
|
+
openClient: async () => client,
|
|
127
|
+
clock,
|
|
128
|
+
shareTransport: transport,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
controller.connect();
|
|
132
|
+
await tick();
|
|
133
|
+
|
|
134
|
+
return { controller, client };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("share", () => {
|
|
138
|
+
test("publishes a public link and records it on the session", async () => {
|
|
139
|
+
// Arrange
|
|
140
|
+
const { controller, client } = await connectedController(sessionFixture());
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
controller.share();
|
|
144
|
+
await tick();
|
|
145
|
+
|
|
146
|
+
// Assert - a public link (no auth) is persisted with the returned id
|
|
147
|
+
expect(client.sessionSetShares).toHaveBeenCalledWith("ses_1", [
|
|
148
|
+
{ id: "p_abc123xy", name: undefined, requireAuth: false, allowlist: [], shareBranch: "main" },
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("surfaces the ssh line as a toast, not an inline status", async () => {
|
|
153
|
+
// Arrange
|
|
154
|
+
const { controller } = await connectedController(sessionFixture());
|
|
155
|
+
|
|
156
|
+
// Act
|
|
157
|
+
controller.share();
|
|
158
|
+
await tick();
|
|
159
|
+
|
|
160
|
+
// Assert
|
|
161
|
+
expect(controller.getSnapshot().toast).toEqual({
|
|
162
|
+
body: "ssh p_abc123xy@cueloop.dev",
|
|
163
|
+
title: "link copied",
|
|
164
|
+
});
|
|
165
|
+
expect(controller.getSnapshot().status).not.toContain("ssh p_abc123xy@cueloop.dev");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("setShareAccess", () => {
|
|
170
|
+
test("pushes the allowlist up to the gateway when the plan is already shared", async () => {
|
|
171
|
+
// Arrange
|
|
172
|
+
pushShare.mockClear();
|
|
173
|
+
const { controller } = await connectedController(sessionFixture({ shareId: "p_abc123xy" }));
|
|
174
|
+
|
|
175
|
+
// Act
|
|
176
|
+
controller.setShareAccess(["octocat"]);
|
|
177
|
+
await tick();
|
|
178
|
+
|
|
179
|
+
// Assert - the existing link must enforce the new access, so it rides the push
|
|
180
|
+
expect(pushShare.mock.calls[0]?.[0]).toBe("p_abc123xy");
|
|
181
|
+
expect(pushShare.mock.calls[0]?.[2]).toEqual({ githubLogins: ["octocat"] });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("does not push when the plan was never shared", async () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
pushShare.mockClear();
|
|
187
|
+
const { controller } = await connectedController(sessionFixture());
|
|
188
|
+
|
|
189
|
+
// Act
|
|
190
|
+
controller.setShareAccess(["octocat"]);
|
|
191
|
+
await tick();
|
|
192
|
+
|
|
193
|
+
// Assert
|
|
194
|
+
expect(pushShare).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("pullShared", () => {
|
|
199
|
+
test("unions collaborator notes and identities in when the plan was shared", async () => {
|
|
200
|
+
// Arrange
|
|
201
|
+
remote = sessionFixture({
|
|
202
|
+
annotations: [annotation("a1", "SHA256:mate")],
|
|
203
|
+
participants: [{ id: "SHA256:mate", provider: "ssh", name: "Sam" }],
|
|
204
|
+
});
|
|
205
|
+
const { controller, client } = await connectedController(
|
|
206
|
+
sessionFixture({ shareId: "p_abc123xy" }),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Act
|
|
210
|
+
controller.pullShared();
|
|
211
|
+
await tick();
|
|
212
|
+
|
|
213
|
+
// Assert
|
|
214
|
+
expect(pullShare).toHaveBeenCalledWith("p_abc123xy");
|
|
215
|
+
expect(client.sessionMergeShared).toHaveBeenCalledWith("ses_1", {
|
|
216
|
+
annotations: remote.annotations,
|
|
217
|
+
participants: remote.participants,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("does nothing for a plan that was never shared", async () => {
|
|
222
|
+
// Arrange
|
|
223
|
+
pullShare.mockClear();
|
|
224
|
+
const { controller, client } = await connectedController(sessionFixture());
|
|
225
|
+
|
|
226
|
+
// Act
|
|
227
|
+
controller.pullShared();
|
|
228
|
+
await tick();
|
|
229
|
+
|
|
230
|
+
// Assert
|
|
231
|
+
expect(pullShare).not.toHaveBeenCalled();
|
|
232
|
+
expect(client.sessionMergeShared).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("mirror on annotate", () => {
|
|
237
|
+
test("pushes an edited note up when the plan is shared", async () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
pushShare.mockClear();
|
|
240
|
+
const { controller } = await connectedController(
|
|
241
|
+
sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:me")] }),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Act
|
|
245
|
+
controller.updateAnnotation("a1", "revised body");
|
|
246
|
+
await tick();
|
|
247
|
+
|
|
248
|
+
// Assert
|
|
249
|
+
expect(pushShare).toHaveBeenCalledTimes(1);
|
|
250
|
+
expect(pushShare.mock.calls[0]?.[0]).toBe("p_abc123xy");
|
|
251
|
+
expect(pushShare.mock.calls[0]?.[1]).toEqual([
|
|
252
|
+
{
|
|
253
|
+
id: "a1",
|
|
254
|
+
kind: "comment",
|
|
255
|
+
anchor: { quote: "Plan", prefix: "", suffix: "" },
|
|
256
|
+
body: "revised body",
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("does not push when the plan was never shared", async () => {
|
|
262
|
+
// Arrange
|
|
263
|
+
pushShare.mockClear();
|
|
264
|
+
const { controller } = await connectedController(
|
|
265
|
+
sessionFixture({ annotations: [annotation("a1", "SHA256:me")] }),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Act
|
|
269
|
+
controller.updateAnnotation("a1", "revised body");
|
|
270
|
+
await tick();
|
|
271
|
+
|
|
272
|
+
// Assert
|
|
273
|
+
expect(pushShare).not.toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("does not push when the local write is rejected", async () => {
|
|
277
|
+
// Arrange - the daemon write fails (e.g. a resolved session)
|
|
278
|
+
pushShare.mockClear();
|
|
279
|
+
const { controller, client } = await connectedController(
|
|
280
|
+
sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:me")] }),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
client.sessionComment.mockImplementationOnce(async () => {
|
|
284
|
+
throw new Error("session is resolved");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Act
|
|
288
|
+
controller.updateAnnotation("a1", "revised body");
|
|
289
|
+
await tick();
|
|
290
|
+
|
|
291
|
+
// Assert - a failed local write must never leak to the share
|
|
292
|
+
expect(pushShare).not.toHaveBeenCalled();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("reply", () => {
|
|
297
|
+
test("a reply shares the root's anchor, names it in replyTo, and mirrors up", async () => {
|
|
298
|
+
// Arrange
|
|
299
|
+
pushShare.mockClear();
|
|
300
|
+
const { controller, client } = await connectedController(
|
|
301
|
+
sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:ana")] }),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Act
|
|
305
|
+
const id = controller.reply("a1", "agreed");
|
|
306
|
+
await tick();
|
|
307
|
+
|
|
308
|
+
// Assert
|
|
309
|
+
const wire = client.sessionComment.mock.calls.at(-1)![1];
|
|
310
|
+
|
|
311
|
+
expect(wire).toEqual({
|
|
312
|
+
id: id!,
|
|
313
|
+
kind: "comment",
|
|
314
|
+
anchor: { quote: "Plan", prefix: "", suffix: "" },
|
|
315
|
+
body: "agreed",
|
|
316
|
+
replyTo: "a1",
|
|
317
|
+
});
|
|
318
|
+
expect(pushShare.mock.calls[0]?.[1]).toEqual([wire]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("editing a reply keeps its reply link", async () => {
|
|
322
|
+
// Arrange
|
|
323
|
+
const { controller, client } = await connectedController(
|
|
324
|
+
sessionFixture({
|
|
325
|
+
annotations: [
|
|
326
|
+
annotation("a1", "SHA256:ana"),
|
|
327
|
+
{ ...annotation("a2", "SHA256:me"), replyTo: "a1" },
|
|
328
|
+
],
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Act
|
|
333
|
+
controller.updateAnnotation("a2", "revised reply");
|
|
334
|
+
await tick();
|
|
335
|
+
|
|
336
|
+
// Assert
|
|
337
|
+
expect(client.sessionComment.mock.calls.at(-1)?.[1]).toMatchObject({
|
|
338
|
+
id: "a2",
|
|
339
|
+
body: "revised reply",
|
|
340
|
+
replyTo: "a1",
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("a reply to a reply hangs off the discussion's root comment", async () => {
|
|
345
|
+
// Arrange
|
|
346
|
+
const { controller, client } = await connectedController(
|
|
347
|
+
sessionFixture({
|
|
348
|
+
annotations: [
|
|
349
|
+
annotation("a1", "SHA256:ana"),
|
|
350
|
+
{ ...annotation("a2", "SHA256:bob"), replyTo: "a1" },
|
|
351
|
+
],
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Act
|
|
356
|
+
controller.reply("a2", "same thread");
|
|
357
|
+
await tick();
|
|
358
|
+
|
|
359
|
+
// Assert
|
|
360
|
+
expect(client.sessionComment.mock.calls.at(-1)?.[1]).toMatchObject({ replyTo: "a1" });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("replying to an unknown annotation does nothing", async () => {
|
|
364
|
+
// Arrange
|
|
365
|
+
const { controller, client } = await connectedController(sessionFixture());
|
|
366
|
+
|
|
367
|
+
// Act + Assert
|
|
368
|
+
expect(controller.reply("missing", "x")).toBeUndefined();
|
|
369
|
+
expect(client.sessionComment).not.toHaveBeenCalled();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("startShareSync", () => {
|
|
374
|
+
/** A fake watch stream: records handlers so a test can push a session or drop the link. */
|
|
375
|
+
const streams: Array<{ shareId: string; handlers: ShareWatchHandlers; stopped: boolean }> = [];
|
|
376
|
+
const watchShare = mock((shareId: string, handlers: ShareWatchHandlers) => {
|
|
377
|
+
const stream = { shareId, handlers, stopped: false };
|
|
378
|
+
|
|
379
|
+
streams.push(stream);
|
|
380
|
+
|
|
381
|
+
return () => {
|
|
382
|
+
stream.stopped = true;
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
const liveTransport: ShareTransport = { ...shareTransport, watch: watchShare };
|
|
386
|
+
|
|
387
|
+
beforeEach(() => {
|
|
388
|
+
streams.length = 0;
|
|
389
|
+
watchShare.mockClear();
|
|
390
|
+
pullShare.mockClear();
|
|
391
|
+
remote = sessionFixture({ annotations: [] });
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("connect pulls once to catch up, then follows the stream and merges each session it delivers", async () => {
|
|
395
|
+
// Arrange
|
|
396
|
+
const clock = new ManualClock();
|
|
397
|
+
const { controller, client } = await connectedController(
|
|
398
|
+
sessionFixture({ shareId: "p_abc123xy" }),
|
|
399
|
+
clock,
|
|
400
|
+
liveTransport,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Act
|
|
404
|
+
controller.startShareSync();
|
|
405
|
+
await tick();
|
|
406
|
+
|
|
407
|
+
// Assert - one catch-up pull, one open stream on the share
|
|
408
|
+
expect(pullShare).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(streams).toHaveLength(1);
|
|
410
|
+
expect(streams[0]!.shareId).toBe("p_abc123xy");
|
|
411
|
+
|
|
412
|
+
// Act - the gateway pushes a change carrying Ana's note
|
|
413
|
+
streams[0]!.handlers.onSession(
|
|
414
|
+
sessionFixture({ annotations: [annotation("a9", "SHA256:ana")] }),
|
|
415
|
+
);
|
|
416
|
+
await tick();
|
|
417
|
+
|
|
418
|
+
// Assert - merged without another pull
|
|
419
|
+
expect(client.sessionMergeShared).toHaveBeenLastCalledWith(
|
|
420
|
+
"ses_1",
|
|
421
|
+
expect.objectContaining({
|
|
422
|
+
annotations: [expect.objectContaining({ id: "a9", author: "SHA256:ana" })],
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
expect(pullShare).toHaveBeenCalledTimes(1);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("a dropped stream reconnects with doubling backoff and catches up on each connect", async () => {
|
|
429
|
+
// Arrange
|
|
430
|
+
const clock = new ManualClock();
|
|
431
|
+
const { controller } = await connectedController(
|
|
432
|
+
sessionFixture({ shareId: "p_abc123xy" }),
|
|
433
|
+
clock,
|
|
434
|
+
liveTransport,
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
controller.startShareSync();
|
|
438
|
+
await tick();
|
|
439
|
+
|
|
440
|
+
// Act - the link drops: nothing until the floor delay has passed
|
|
441
|
+
streams[0]!.handlers.onClose("ssh exited 255");
|
|
442
|
+
clock.advance(SHARE_RECONNECT_MIN_MS - 1);
|
|
443
|
+
await tick();
|
|
444
|
+
expect(streams).toHaveLength(1);
|
|
445
|
+
clock.advance(1);
|
|
446
|
+
await tick();
|
|
447
|
+
|
|
448
|
+
// Assert - reconnected, with a fresh catch-up pull
|
|
449
|
+
expect(streams).toHaveLength(2);
|
|
450
|
+
expect(pullShare).toHaveBeenCalledTimes(2);
|
|
451
|
+
|
|
452
|
+
// Act - drops again: the wait doubles
|
|
453
|
+
streams[1]!.handlers.onClose("ssh exited 255");
|
|
454
|
+
clock.advance(SHARE_RECONNECT_MIN_MS);
|
|
455
|
+
await tick();
|
|
456
|
+
expect(streams).toHaveLength(2);
|
|
457
|
+
clock.advance(SHARE_RECONNECT_MIN_MS);
|
|
458
|
+
await tick();
|
|
459
|
+
expect(streams).toHaveLength(3);
|
|
460
|
+
|
|
461
|
+
// Act - a delivered session resets the backoff to the floor
|
|
462
|
+
streams[2]!.handlers.onSession(remote);
|
|
463
|
+
streams[2]!.handlers.onClose("ssh exited 255");
|
|
464
|
+
clock.advance(SHARE_RECONNECT_MIN_MS);
|
|
465
|
+
await tick();
|
|
466
|
+
|
|
467
|
+
// Assert
|
|
468
|
+
expect(streams).toHaveLength(4);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("stop closes the stream and a late close never reconnects", async () => {
|
|
472
|
+
// Arrange
|
|
473
|
+
const clock = new ManualClock();
|
|
474
|
+
const { controller } = await connectedController(
|
|
475
|
+
sessionFixture({ shareId: "p_abc123xy" }),
|
|
476
|
+
clock,
|
|
477
|
+
liveTransport,
|
|
478
|
+
);
|
|
479
|
+
const stop = controller.startShareSync();
|
|
480
|
+
|
|
481
|
+
await tick();
|
|
482
|
+
|
|
483
|
+
// Act
|
|
484
|
+
stop();
|
|
485
|
+
streams[0]!.handlers.onClose("killed");
|
|
486
|
+
clock.advance(SHARE_RECONNECT_MAX_MS * 2);
|
|
487
|
+
await tick();
|
|
488
|
+
|
|
489
|
+
// Assert
|
|
490
|
+
expect(streams[0]!.stopped).toBe(true);
|
|
491
|
+
expect(streams).toHaveLength(1);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("a restart replaces the stream instead of doubling it", async () => {
|
|
495
|
+
// Arrange
|
|
496
|
+
const clock = new ManualClock();
|
|
497
|
+
const { controller } = await connectedController(
|
|
498
|
+
sessionFixture({ shareId: "p_abc123xy" }),
|
|
499
|
+
clock,
|
|
500
|
+
liveTransport,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
controller.startShareSync();
|
|
504
|
+
await tick();
|
|
505
|
+
|
|
506
|
+
// Act
|
|
507
|
+
controller.startShareSync();
|
|
508
|
+
await tick();
|
|
509
|
+
streams[0]!.handlers.onClose("stale");
|
|
510
|
+
clock.advance(SHARE_RECONNECT_MAX_MS * 2);
|
|
511
|
+
await tick();
|
|
512
|
+
|
|
513
|
+
// Assert - the first stream was stopped and its close re-armed nothing
|
|
514
|
+
expect(streams[0]!.stopped).toBe(true);
|
|
515
|
+
expect(streams).toHaveLength(2);
|
|
516
|
+
expect(streams[1]!.stopped).toBe(false);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("revoke", () => {
|
|
521
|
+
test("unshare revokes the gateway blob for the shared thread", async () => {
|
|
522
|
+
revokeShare.mockClear();
|
|
523
|
+
const { controller } = await connectedController(sessionFixture({ shareId: "p_abc123xy" }));
|
|
524
|
+
|
|
525
|
+
controller.unshare();
|
|
526
|
+
await tick();
|
|
527
|
+
|
|
528
|
+
expect(revokeShare).toHaveBeenCalledWith("p_abc123xy");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("unshare on an unshared thread is a no-op", async () => {
|
|
532
|
+
revokeShare.mockClear();
|
|
533
|
+
const { controller } = await connectedController(sessionFixture());
|
|
534
|
+
|
|
535
|
+
controller.unshare();
|
|
536
|
+
await tick();
|
|
537
|
+
|
|
538
|
+
expect(revokeShare).not.toHaveBeenCalled();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test("deleting a shared thread revokes its share first", async () => {
|
|
542
|
+
revokeShare.mockClear();
|
|
543
|
+
const session = sessionFixture({ shareId: "p_abc123xy" });
|
|
544
|
+
const client = fakeClient(session);
|
|
545
|
+
|
|
546
|
+
client.sessionDelete = mock(async () => {});
|
|
547
|
+
const controller = createReviewController({
|
|
548
|
+
sessionId: session.id,
|
|
549
|
+
openClient: async () => client,
|
|
550
|
+
shareTransport,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
controller.connect();
|
|
554
|
+
await tick();
|
|
555
|
+
controller.deleteSession(session.id);
|
|
556
|
+
await tick();
|
|
557
|
+
|
|
558
|
+
expect(revokeShare).toHaveBeenCalledWith("p_abc123xy");
|
|
559
|
+
expect(client.sessionDelete).toHaveBeenCalledWith(session.id);
|
|
560
|
+
});
|
|
561
|
+
});
|