@cueloop/client 0.1.0-alpha.0 → 0.1.0-alpha.100
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 +41 -27
- package/src/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +364 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1238 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/app/App.autoclose.test.tsx +144 -0
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +12 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1650 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +550 -0
- package/src/integrations/components/share-dialog-store.ts +55 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2018 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -80
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.test.tsx +0 -217
- package/src/App.tsx +0 -0
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -113
- package/src/editor.ts +0 -34
- package/src/run.ts +0 -28
- package/src/serve.ts +0 -75
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Prototype review end to end, in the opt-in EXPERIMENTAL pixel mode ([experimental] prototype_pixels): a click on the rendered page selects a DOM element and opens the compose card directly - the same inline composer plan and diff use - a typed comment saves through the controller on cmd+enter, and the annotation anchors to the element's selector. The headless browser is replaced with a fake so no Chrome or kitty output is needed. Char-frame assertions over the real App and a real in-process daemon. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
setPrototypeRendererFactory,
|
|
13
|
+
type PrototypeElement,
|
|
14
|
+
type PrototypeRenderer,
|
|
15
|
+
} from "../prototype/prototype-browser";
|
|
16
|
+
import {
|
|
17
|
+
isolateUserConfig,
|
|
18
|
+
press,
|
|
19
|
+
renderReadyApp,
|
|
20
|
+
settle,
|
|
21
|
+
typeText,
|
|
22
|
+
waitForState,
|
|
23
|
+
waitForText,
|
|
24
|
+
waitForTextGone,
|
|
25
|
+
} from "../testing/test-support";
|
|
26
|
+
|
|
27
|
+
const HTML = "<main><div class='card'><h2>Pro</h2><p>$24/mo</p></div></main>";
|
|
28
|
+
|
|
29
|
+
const FAKE_ELEMENT: PrototypeElement = {
|
|
30
|
+
selector: "main > div.card",
|
|
31
|
+
quote: "Pro $24/mo",
|
|
32
|
+
// mid-page, so the popover anchors well away from the click point below
|
|
33
|
+
box: { x: 440, y: 320, width: 320, height: 220 },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let rendered = false;
|
|
37
|
+
let screenshotCount = 0;
|
|
38
|
+
let scrollDeltas: number[] = [];
|
|
39
|
+
const fakeRenderer: PrototypeRenderer = {
|
|
40
|
+
viewport: { width: 1280, height: 800 },
|
|
41
|
+
screenshot: async () => {
|
|
42
|
+
rendered = true;
|
|
43
|
+
screenshotCount += 1;
|
|
44
|
+
|
|
45
|
+
return new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
46
|
+
},
|
|
47
|
+
elementAt: async () => FAKE_ELEMENT,
|
|
48
|
+
scrollBy: async (deltaY) => {
|
|
49
|
+
scrollDeltas.push(deltaY);
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
close: async () => undefined,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let home: string;
|
|
57
|
+
let server: DaemonServer;
|
|
58
|
+
let session: Thread;
|
|
59
|
+
let restoreUserConfig: () => void;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-prototype-"));
|
|
63
|
+
// the pixel mockup is opt-in; these tests exercise that experimental path
|
|
64
|
+
writeFileSync(join(home, "config.toml"), "[experimental]\nprototype_pixels = true\n");
|
|
65
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
66
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
67
|
+
server.start();
|
|
68
|
+
session = server.core.sessionCreate({
|
|
69
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
70
|
+
artifact: {
|
|
71
|
+
type: "prototype",
|
|
72
|
+
content: HTML,
|
|
73
|
+
meta: { title: "Pricing cards", prototypePath: "/repo/cards.html" },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
rendered = false;
|
|
77
|
+
screenshotCount = 0;
|
|
78
|
+
scrollDeltas = [];
|
|
79
|
+
setPrototypeRendererFactory(async () => fakeRenderer);
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
setPrototypeRendererFactory(null);
|
|
83
|
+
restoreUserConfig();
|
|
84
|
+
server.stop();
|
|
85
|
+
rmSync(home, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
async function renderApp() {
|
|
89
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
90
|
+
width: 120,
|
|
91
|
+
height: 32,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await waitForText(setup, "cueloop");
|
|
95
|
+
|
|
96
|
+
return setup;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("prototype review", () => {
|
|
100
|
+
test("click an element, comment on it, and it saves to the daemon", async () => {
|
|
101
|
+
// Arrange - wait until the fake browser produced its first screenshot, so
|
|
102
|
+
// the sheet is interactive before the click
|
|
103
|
+
const setup = await renderApp();
|
|
104
|
+
|
|
105
|
+
await waitForState(setup, () => rendered);
|
|
106
|
+
await settle(setup);
|
|
107
|
+
// selecting an element must not re-screenshot the page (the compose-open
|
|
108
|
+
// lag regression): the click opens the composer, not a fresh Chromium capture
|
|
109
|
+
const screenshotsAfterLoad = screenshotCount;
|
|
110
|
+
|
|
111
|
+
// Act - a click opens the compose card directly on the element
|
|
112
|
+
await setup.mockMouse.click(6, 6);
|
|
113
|
+
await waitForText(setup, "●");
|
|
114
|
+
expect(screenshotCount).toBe(screenshotsAfterLoad);
|
|
115
|
+
// the keymap-suppression flag must propagate before typing so keys reach the
|
|
116
|
+
// textarea, not the global keymap
|
|
117
|
+
await settle(setup);
|
|
118
|
+
await typeText(setup, "tighten the padding");
|
|
119
|
+
// cmd+enter sends, matching the plan and diff composer; plain enter is a newline
|
|
120
|
+
setup.mockInput.pressKey("RETURN", { meta: true });
|
|
121
|
+
|
|
122
|
+
// Assert - the annotation is stored against the element's selector
|
|
123
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
124
|
+
const stored = server.core.sessionGet(session.id).annotations[0]!;
|
|
125
|
+
|
|
126
|
+
expect(stored.kind).toBe("comment");
|
|
127
|
+
expect(stored.body).toBe("tighten the padding");
|
|
128
|
+
expect(stored.anchor.selector).toBe("main > div.card");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("escape cancels the compose, matching the plan composer", async () => {
|
|
132
|
+
// Arrange
|
|
133
|
+
const setup = await renderApp();
|
|
134
|
+
|
|
135
|
+
await waitForState(setup, () => rendered);
|
|
136
|
+
await settle(setup);
|
|
137
|
+
await setup.mockMouse.click(6, 6);
|
|
138
|
+
await waitForText(setup, "●");
|
|
139
|
+
await settle(setup);
|
|
140
|
+
|
|
141
|
+
// Act
|
|
142
|
+
await press(setup, "escape");
|
|
143
|
+
|
|
144
|
+
// Assert - the composer closes and nothing was saved
|
|
145
|
+
await waitForTextGone(setup, "●");
|
|
146
|
+
expect(server.core.sessionGet(session.id).annotations).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("wheel over the preview scrolls the page down, then up, and clears a selection", async () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const setup = await renderApp();
|
|
152
|
+
|
|
153
|
+
await waitForState(setup, () => rendered);
|
|
154
|
+
await settle(setup);
|
|
155
|
+
|
|
156
|
+
// open a selection, then scroll: the scroll drops it (it would drift once
|
|
157
|
+
// the page moves under it)
|
|
158
|
+
await setup.mockMouse.click(6, 6);
|
|
159
|
+
await waitForText(setup, "●");
|
|
160
|
+
// scrolling moves the page, so it DOES re-screenshot (unlike a click)
|
|
161
|
+
const screenshotsBeforeScroll = screenshotCount;
|
|
162
|
+
|
|
163
|
+
await setup.mockMouse.scroll(6, 6, "down");
|
|
164
|
+
await waitForState(setup, () => scrollDeltas.length === 1);
|
|
165
|
+
await waitForTextGone(setup, "●");
|
|
166
|
+
await waitForState(setup, () => screenshotCount > screenshotsBeforeScroll);
|
|
167
|
+
|
|
168
|
+
// Act - scroll back up
|
|
169
|
+
await setup.mockMouse.scroll(6, 6, "up");
|
|
170
|
+
await waitForState(setup, () => scrollDeltas.length === 2);
|
|
171
|
+
|
|
172
|
+
// Assert - down was a positive delta, up was negative
|
|
173
|
+
expect(scrollDeltas[0]!).toBeGreaterThan(0);
|
|
174
|
+
expect(scrollDeltas[1]!).toBeLessThan(0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/** Observer mode (tier 2): the App with readOnly renders and navigates like the controller's, but every mutating primitive answers "observer - read-only" and leaves daemon state untouched. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import { makeAnchor, parseBlocks, type Thread } from "@cueloop/schema";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
clickText,
|
|
13
|
+
dragText,
|
|
14
|
+
frameRow,
|
|
15
|
+
isolateUserConfig,
|
|
16
|
+
navCommand,
|
|
17
|
+
pressKey,
|
|
18
|
+
renderReadyApp,
|
|
19
|
+
typeText,
|
|
20
|
+
waitForText,
|
|
21
|
+
} from "../testing/test-support";
|
|
22
|
+
|
|
23
|
+
const PLAN = `# Migration Plan
|
|
24
|
+
|
|
25
|
+
## Context
|
|
26
|
+
|
|
27
|
+
The daemon persists sessions to disk atomically.
|
|
28
|
+
|
|
29
|
+
## Steps
|
|
30
|
+
|
|
31
|
+
- move the store
|
|
32
|
+
- add recovery
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
let home: string;
|
|
36
|
+
let restoreUserConfig: () => void;
|
|
37
|
+
let server: DaemonServer;
|
|
38
|
+
let session: Thread;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-observer-"));
|
|
42
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
43
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
44
|
+
server.start();
|
|
45
|
+
session = server.core.sessionCreate({
|
|
46
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
47
|
+
artifact: {
|
|
48
|
+
type: "plan",
|
|
49
|
+
content: PLAN,
|
|
50
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
restoreUserConfig();
|
|
56
|
+
server.stop();
|
|
57
|
+
rmSync(home, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function renderObserver() {
|
|
61
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
62
|
+
width: 120,
|
|
63
|
+
height: 32,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await waitForText(setup, "cueloop");
|
|
67
|
+
|
|
68
|
+
return setup;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type Setup = Awaited<ReturnType<typeof renderObserver>>;
|
|
72
|
+
|
|
73
|
+
/** Session state that any mutating primitive would change. */
|
|
74
|
+
function snapshot() {
|
|
75
|
+
const stored = server.core.sessionGet(session.id);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
annotations: stored.annotations.length,
|
|
79
|
+
workingCopy: stored.workingCopy,
|
|
80
|
+
status: stored.status,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("observer rendering", () => {
|
|
85
|
+
test("the minimal header mirrors the thread name, with no observer badge", async () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const setup = await renderObserver();
|
|
88
|
+
|
|
89
|
+
// Assert - the header shows the thread title only; read-only is enforced by
|
|
90
|
+
// the blocked primitives below, not a header badge
|
|
91
|
+
const frame = setup.captureCharFrame();
|
|
92
|
+
|
|
93
|
+
expect(frame).toContain("Migration Plan");
|
|
94
|
+
expect(frame).not.toContain("· observer");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("observer primitives are blocked", () => {
|
|
99
|
+
const attempts: Array<[string, (setup: Setup) => Promise<void>]> = [
|
|
100
|
+
["comment (typing)", (setup) => typeText(setup, "c")],
|
|
101
|
+
["cut (option+x)", (setup) => navCommand(setup, "x")],
|
|
102
|
+
["edit (ctrl+e)", (setup) => pressKey(setup, "e", { ctrl: true })],
|
|
103
|
+
["submit (cmd+enter)", (setup) => pressKey(setup, "RETURN", { meta: true })],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const [primitive, attempt] of attempts) {
|
|
107
|
+
test(`${primitive} answers observer - read-only and mutates nothing`, async () => {
|
|
108
|
+
const setup = await renderObserver();
|
|
109
|
+
const before = snapshot();
|
|
110
|
+
|
|
111
|
+
await clickText(setup, "daemon");
|
|
112
|
+
await attempt(setup);
|
|
113
|
+
const frame = await waitForText(setup, "observer - read-only");
|
|
114
|
+
|
|
115
|
+
// no draft card and no submit card ever appear
|
|
116
|
+
expect(frame).not.toContain("● c");
|
|
117
|
+
expect(frame).not.toContain("[approve]");
|
|
118
|
+
expect(snapshot()).toEqual(before);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test("typing over a marked span is blocked too", async () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
const setup = await renderObserver();
|
|
125
|
+
|
|
126
|
+
// Act
|
|
127
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
128
|
+
await typeText(setup, "c");
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
const frame = await waitForText(setup, "observer - read-only");
|
|
132
|
+
|
|
133
|
+
expect(frame).not.toContain("● c");
|
|
134
|
+
expect(snapshot().annotations).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("a resolved review is read-only for its owner too", () => {
|
|
139
|
+
test("typing, deleting a card, and editing answer review submitted - read-only", async () => {
|
|
140
|
+
// Arrange: a comment exists and the message is in
|
|
141
|
+
server.core.sessionAnnotate(session.id, {
|
|
142
|
+
id: "a_done",
|
|
143
|
+
kind: "comment",
|
|
144
|
+
anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
|
|
145
|
+
body: "settled",
|
|
146
|
+
});
|
|
147
|
+
server.core.sessionSendMessage(session.id, "approved", "");
|
|
148
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
149
|
+
width: 120,
|
|
150
|
+
height: 32,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await waitForText(setup, "settled");
|
|
154
|
+
const before = snapshot();
|
|
155
|
+
|
|
156
|
+
// Act + Assert: a draft never opens
|
|
157
|
+
await clickText(setup, "daemon");
|
|
158
|
+
await typeText(setup, "c");
|
|
159
|
+
await waitForText(setup, "review submitted - read-only");
|
|
160
|
+
expect(setup.captureCharFrame()).not.toContain("● c");
|
|
161
|
+
|
|
162
|
+
// Act + Assert: the focused card cannot be deleted or edited
|
|
163
|
+
await navCommand(setup, "n");
|
|
164
|
+
await pressKey(setup, "BACKSPACE", { meta: true });
|
|
165
|
+
await pressKey(setup, "e", { ctrl: true });
|
|
166
|
+
await navCommand(setup, "x");
|
|
167
|
+
expect(snapshot()).toEqual(before);
|
|
168
|
+
expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("observer navigation still works", () => {
|
|
173
|
+
test("↓ moves the caret between blocks", async () => {
|
|
174
|
+
// Arrange
|
|
175
|
+
const setup = await renderObserver();
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
179
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
180
|
+
await setup.renderOnce();
|
|
181
|
+
|
|
182
|
+
// Assert: the caret cell is painted on the paragraph's row
|
|
183
|
+
const caretRow = setup
|
|
184
|
+
.captureSpans()
|
|
185
|
+
.lines.findIndex((line) =>
|
|
186
|
+
line.spans.some((span) => span.bg.toInts().slice(0, 3).join() === "86,91,104"),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(caretRow).toBe(frameRow(setup, "persists sessions"));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("option+n focuses annotations made by the controller", async () => {
|
|
193
|
+
// Arrange
|
|
194
|
+
server.core.sessionAnnotate(session.id, {
|
|
195
|
+
id: "a_obs1",
|
|
196
|
+
kind: "comment",
|
|
197
|
+
anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
|
|
198
|
+
body: "From the controller.",
|
|
199
|
+
});
|
|
200
|
+
const setup = await renderObserver();
|
|
201
|
+
|
|
202
|
+
// Act - the observer navigates to the annotation without mutating anything
|
|
203
|
+
await navCommand(setup, "n");
|
|
204
|
+
await setup.renderOnce();
|
|
205
|
+
|
|
206
|
+
// Assert - the observer reads the controller's comment inline in the thread
|
|
207
|
+
await waitForText(setup, "From the controller.");
|
|
208
|
+
expect(snapshot().annotations).toBe(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Regressions from the focus/perf round: ctrl+q must quit from any keyboard state, and clicking a
|
|
2
|
+
* sidebar thread must give it the selected accent at once (the cursor follows the click). */
|
|
3
|
+
|
|
4
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
5
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
clickText,
|
|
13
|
+
isolateUserConfig,
|
|
14
|
+
pressKey,
|
|
15
|
+
renderReadyApp,
|
|
16
|
+
waitForText,
|
|
17
|
+
} from "../testing/test-support";
|
|
18
|
+
|
|
19
|
+
const ACCENT_DARK: [number, number, number] = [203, 166, 247];
|
|
20
|
+
|
|
21
|
+
let home: string;
|
|
22
|
+
let restoreUserConfig: () => void;
|
|
23
|
+
let server: DaemonServer;
|
|
24
|
+
|
|
25
|
+
function plan(title: string): void {
|
|
26
|
+
server.core.sessionCreate({
|
|
27
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
28
|
+
artifact: { type: "plan", content: `# ${title}\n`, meta: { title, planPath: "plan.md" } },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-regression-"));
|
|
34
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
|
+
server.start();
|
|
37
|
+
plan("First review");
|
|
38
|
+
plan("Second review");
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
restoreUserConfig();
|
|
42
|
+
server.stop();
|
|
43
|
+
rmSync(home, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The text of the sidebar title span currently painted in the accent colour, or undefined. */
|
|
47
|
+
function accentTitle(setup: Awaited<ReturnType<typeof renderReadyApp>>): string | undefined {
|
|
48
|
+
for (const line of setup.captureSpans().lines) {
|
|
49
|
+
for (const span of line.spans) {
|
|
50
|
+
const rgb = span.fg?.toInts();
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
rgb &&
|
|
54
|
+
rgb[0] === ACCENT_DARK[0] &&
|
|
55
|
+
rgb[1] === ACCENT_DARK[1] &&
|
|
56
|
+
rgb[2] === ACCENT_DARK[2]
|
|
57
|
+
) {
|
|
58
|
+
const text = span.text.trim();
|
|
59
|
+
|
|
60
|
+
if (text.includes("review")) return text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("ctrl+q quits even when a menu holds the keyboard", async () => {
|
|
69
|
+
let exitCode: number | undefined;
|
|
70
|
+
const setup = await renderReadyApp(<App home={home} onExit={(code) => (exitCode = code)} />, {
|
|
71
|
+
width: 120,
|
|
72
|
+
height: 32,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await waitForText(setup, "review");
|
|
76
|
+
|
|
77
|
+
// open the settings dialog from the top-left gear - it takes the keyboard
|
|
78
|
+
await setup.mockMouse.click(1, 0);
|
|
79
|
+
await waitForText(setup, "Keybinds");
|
|
80
|
+
|
|
81
|
+
await pressKey(setup, "q", { ctrl: true });
|
|
82
|
+
|
|
83
|
+
expect(exitCode).toBe(0);
|
|
84
|
+
setup.renderer.destroy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("clicking a sidebar thread gives it the selected accent at once", async () => {
|
|
88
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
89
|
+
|
|
90
|
+
await waitForText(setup, "Second review");
|
|
91
|
+
|
|
92
|
+
// click whichever thread is not already the selected (accent) one
|
|
93
|
+
const target = accentTitle(setup)?.includes("Second") ? "First review" : "Second review";
|
|
94
|
+
|
|
95
|
+
await clickText(setup, target);
|
|
96
|
+
|
|
97
|
+
expect(accentTitle(setup)).toContain(target.split(" ")[0]!);
|
|
98
|
+
setup.renderer.destroy();
|
|
99
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** Plan rendering: code blocks verbatim + contained, spacing never collapses. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import { isolateUserConfig, waitForText, renderReadyApp } from "../testing/test-support";
|
|
12
|
+
|
|
13
|
+
const PLAN = `# Render Plan
|
|
14
|
+
|
|
15
|
+
## Sensor
|
|
16
|
+
|
|
17
|
+
- wire the duck over MQTT
|
|
18
|
+
|
|
19
|
+
\`\`\`tsx
|
|
20
|
+
export function Gate({ full }: { full: number }) {
|
|
21
|
+
const blocked = full < 0.6;
|
|
22
|
+
return <text>{blocked ? "blocked" : "clear"}</text>;
|
|
23
|
+
}
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
## Gate
|
|
27
|
+
|
|
28
|
+
Deploys block below the threshold.
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
let home: string;
|
|
32
|
+
let restoreUserConfig: () => void;
|
|
33
|
+
let server: DaemonServer;
|
|
34
|
+
let session: Thread;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-render-"));
|
|
38
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
39
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
40
|
+
server.start();
|
|
41
|
+
session = server.core.sessionCreate({
|
|
42
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
43
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Render Plan" } },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
restoreUserConfig();
|
|
48
|
+
server.stop();
|
|
49
|
+
rmSync(home, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
async function frame(): Promise<string> {
|
|
53
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
54
|
+
width: 120,
|
|
55
|
+
height: 36,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await waitForText(setup, "Render Plan");
|
|
59
|
+
// async highlights settle within the visual-idle wait
|
|
60
|
+
await setup.waitForVisualIdle();
|
|
61
|
+
|
|
62
|
+
return setup.captureCharFrame();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("code blocks", () => {
|
|
66
|
+
test("lines render verbatim: indentation preserved, no word-wrap", async () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const rendered = await frame();
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(rendered).toContain(" const blocked = full < 0.6;");
|
|
72
|
+
expect(rendered).toContain(' return <text>{blocked ? "blocked" : "clear"}</text>;');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("the block carries its language tag", async () => {
|
|
76
|
+
// Arrange
|
|
77
|
+
const rendered = await frame();
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(rendered).toContain("tsx");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("block spacing", () => {
|
|
85
|
+
// the review rail paints box edges on the right, so read the document
|
|
86
|
+
// column (everything left of the first rail glyph) before asserting spacing
|
|
87
|
+
const planColumn = (line: string): string => line.split(/[│╭╰]/)[0]!.trimEnd();
|
|
88
|
+
|
|
89
|
+
test("a code block never glues to the list item above it", async () => {
|
|
90
|
+
// Arrange
|
|
91
|
+
const lines = (await frame()).split("\n").map(planColumn);
|
|
92
|
+
const listItemLineIndex = lines.findIndex((line) => line.includes("wire the duck over MQTT"));
|
|
93
|
+
const codeHeader = lines.findIndex((line) => line.trim().startsWith("tsx"));
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(listItemLineIndex).toBeGreaterThan(-1);
|
|
97
|
+
expect(codeHeader).toBeGreaterThan(listItemLineIndex);
|
|
98
|
+
expect(lines.slice(listItemLineIndex + 1, codeHeader).some((line) => line === "")).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a heading never sits directly on the previous block", async () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const lines = (await frame()).split("\n").map(planColumn);
|
|
104
|
+
const gate = lines.findIndex((line) => line.trim() === "Gate");
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
expect(gate).toBeGreaterThan(0);
|
|
108
|
+
expect(lines[gate - 1]).toBe("");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("consecutive list items stay tight", async () => {
|
|
112
|
+
// Arrange
|
|
113
|
+
const listPlan = "# T\n\n- one\n- two\n";
|
|
114
|
+
const s2 = server.core.sessionCreate({
|
|
115
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
116
|
+
artifact: { type: "plan", content: listPlan, meta: {} },
|
|
117
|
+
});
|
|
118
|
+
const setup = await renderReadyApp(<App home={home} sessionId={s2.id} />, {
|
|
119
|
+
width: 120,
|
|
120
|
+
height: 30,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await waitForText(setup, "one");
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
const lines = setup
|
|
127
|
+
.captureCharFrame()
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((line) => line.trimEnd());
|
|
130
|
+
const one = lines.findIndex((line) => line.includes("· one"));
|
|
131
|
+
|
|
132
|
+
expect(lines[one + 1]).toContain("· two");
|
|
133
|
+
});
|
|
134
|
+
});
|