@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -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 +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -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 +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -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 +148 -0
- package/src/use-annotation-surface.tsx +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- 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 -312
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. */
|
|
1
|
+
/** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. Countdown seconds tick on an injected ManualClock, so tests advance time instead of waiting it out. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -6,113 +6,139 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { ManualClock } from "@opentui/core/testing";
|
|
9
10
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
-
import type {
|
|
11
|
+
import type { Thread } from "@cueloop/schema";
|
|
11
12
|
import { App } from "./App";
|
|
13
|
+
import { isolateUserConfig, press, settle, waitForText, pressKey } from "./test-support";
|
|
12
14
|
|
|
13
15
|
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
14
16
|
|
|
15
17
|
let home: string;
|
|
16
18
|
let server: DaemonServer;
|
|
17
|
-
let session:
|
|
19
|
+
let session: Thread;
|
|
18
20
|
let configPath: string;
|
|
21
|
+
let restoreUserConfig: () => void;
|
|
22
|
+
let clock: ManualClock;
|
|
19
23
|
|
|
20
24
|
beforeEach(() => {
|
|
21
25
|
home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
|
|
22
26
|
configPath = join(home, "config.toml");
|
|
23
|
-
|
|
27
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
24
28
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
25
29
|
server.start();
|
|
26
30
|
session = server.core.sessionCreate({
|
|
27
31
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
28
32
|
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
29
33
|
});
|
|
34
|
+
clock = new ManualClock();
|
|
30
35
|
});
|
|
31
36
|
afterEach(() => {
|
|
32
|
-
|
|
37
|
+
restoreUserConfig();
|
|
33
38
|
server.stop();
|
|
34
39
|
rmSync(home, { recursive: true, force: true });
|
|
35
40
|
});
|
|
36
41
|
|
|
37
42
|
async function renderApp(onExit?: (code: number) => void) {
|
|
38
|
-
const setup = await testRender(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const setup = await testRender(
|
|
44
|
+
<App home={home} sessionId={session.id} onExit={onExit} clock={clock} />,
|
|
45
|
+
{
|
|
46
|
+
width: 120,
|
|
47
|
+
height: 30,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await waitForText(setup, "cueloop");
|
|
52
|
+
|
|
46
53
|
return setup;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
50
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
51
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
52
|
-
else if (k === "escape") setup.mockInput.pressKey("ESCAPE");
|
|
53
|
-
else await setup.mockInput.typeText(k);
|
|
54
|
-
await Bun.sleep(15);
|
|
55
|
-
await setup.renderOnce();
|
|
56
|
-
}
|
|
57
57
|
|
|
58
58
|
async function submitApprove(setup: Setup): Promise<void> {
|
|
59
|
-
await
|
|
59
|
+
await pressKey(setup, "RETURN", { meta: true }); // open submit (approve default: no pending items)
|
|
60
60
|
await press(setup, "enter"); // confirm
|
|
61
|
-
|
|
62
|
-
while (Date.now() < deadline && !setup.captureCharFrame().includes("review approved")) {
|
|
63
|
-
await Bun.sleep(25);
|
|
64
|
-
await setup.renderOnce();
|
|
65
|
-
}
|
|
61
|
+
await waitForText(setup, "review approved");
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
describe("completion overlay", () => {
|
|
69
|
-
test("submit with auto-close off
|
|
65
|
+
test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
|
|
66
|
+
// Arrange
|
|
70
67
|
let exited = -1;
|
|
71
68
|
const setup = await renderApp((code) => (exited = code));
|
|
69
|
+
|
|
70
|
+
// Act
|
|
72
71
|
await submitApprove(setup);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
|
|
73
|
+
// Assert
|
|
74
|
+
const frame = await waitForText(setup, "closing in 5s");
|
|
75
|
+
|
|
76
|
+
expect(frame).toContain("review approved");
|
|
77
|
+
expect(frame).toContain("close [return]");
|
|
78
|
+
expect(frame).toContain("return to plan [esc]");
|
|
79
|
+
|
|
80
|
+
// Act
|
|
76
81
|
await press(setup, "enter");
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
77
84
|
expect(exited).toBe(0);
|
|
78
85
|
});
|
|
79
86
|
|
|
80
|
-
test("a
|
|
87
|
+
test("a remembers the countdown as the persisted default", async () => {
|
|
88
|
+
// Arrange
|
|
81
89
|
const setup = await renderApp();
|
|
90
|
+
|
|
82
91
|
await submitApprove(setup);
|
|
92
|
+
await waitForText(setup, "closing in 5s");
|
|
93
|
+
|
|
94
|
+
// Act
|
|
83
95
|
await press(setup, "a");
|
|
84
|
-
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
85
98
|
expect(existsSync(configPath)).toBe(true);
|
|
86
|
-
expect(readFileSync(configPath, "utf8")).toContain("auto_close =
|
|
99
|
+
expect(readFileSync(configPath, "utf8")).toContain("auto_close = 5");
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
// the countdown keeps running; each second is one manual-clock tick
|
|
103
|
+
clock.advance(1000);
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
await waitForText(setup, "closing in 4s");
|
|
87
107
|
});
|
|
88
108
|
|
|
89
109
|
test("esc dismisses to the resolved read-only view", async () => {
|
|
110
|
+
// Arrange
|
|
90
111
|
const setup = await renderApp();
|
|
112
|
+
|
|
91
113
|
await submitApprove(setup);
|
|
92
|
-
await
|
|
114
|
+
await waitForText(setup, "closing in 5s");
|
|
115
|
+
|
|
116
|
+
// Act
|
|
93
117
|
// a bare ESC sits in the input parser's escape-sequence disambiguation
|
|
94
|
-
// window before it is delivered -
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
expect(frame).toContain("resolved: approve");
|
|
102
|
-
expect(frame).not.toContain("always close after submit");
|
|
118
|
+
// window before it is delivered - the frame wait absorbs it
|
|
119
|
+
await press(setup, "escape");
|
|
120
|
+
|
|
121
|
+
// Assert - the countdown is cancelled and we are back in the (resolved) thread view
|
|
122
|
+
const frame = await waitForText(setup, "Ship the thing");
|
|
123
|
+
|
|
124
|
+
expect(frame).not.toContain("closing in");
|
|
103
125
|
});
|
|
104
126
|
|
|
105
127
|
test("configured auto_close counts down and exits without interaction", async () => {
|
|
128
|
+
// Arrange
|
|
106
129
|
writeFileSync(configPath, "[ui]\nauto_close = 1\n");
|
|
107
130
|
let exited = -1;
|
|
108
131
|
const setup = await renderApp((code) => (exited = code));
|
|
132
|
+
|
|
109
133
|
await submitApprove(setup);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
await waitForText(setup, "closing in 1s");
|
|
135
|
+
expect(exited).toBe(-1);
|
|
136
|
+
|
|
137
|
+
// Act
|
|
138
|
+
clock.advance(1000);
|
|
139
|
+
await settle(setup);
|
|
140
|
+
|
|
141
|
+
// Assert
|
|
116
142
|
expect(exited).toBe(0);
|
|
117
|
-
}
|
|
143
|
+
});
|
|
118
144
|
});
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
/** Diff-review flow in the virtual terminal
|
|
1
|
+
/** Diff-review flow in the virtual terminal: the diff sheet annotates like the thread view. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
-
import type {
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
11
|
+
import { NERD } from "./components/primitives/icons";
|
|
12
|
+
import {
|
|
13
|
+
clickText,
|
|
14
|
+
dragText,
|
|
15
|
+
isolateUserConfig,
|
|
16
|
+
press,
|
|
17
|
+
pressKey,
|
|
18
|
+
renderReadyApp,
|
|
19
|
+
typeText,
|
|
20
|
+
waitForState,
|
|
21
|
+
waitForText,
|
|
22
|
+
} from "./test-support";
|
|
12
23
|
|
|
13
24
|
const PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
14
25
|
index 111..222 100644
|
|
@@ -22,11 +33,13 @@ index 111..222 100644
|
|
|
22
33
|
`;
|
|
23
34
|
|
|
24
35
|
let home: string;
|
|
36
|
+
let restoreUserConfig: () => void;
|
|
25
37
|
let server: DaemonServer;
|
|
26
|
-
let session:
|
|
38
|
+
let session: Thread;
|
|
27
39
|
|
|
28
40
|
beforeEach(() => {
|
|
29
41
|
home = mkdtempSync(join(tmpdir(), "cueloop-diff-"));
|
|
42
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
30
43
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
31
44
|
server.start();
|
|
32
45
|
session = server.core.sessionCreate({
|
|
@@ -35,65 +48,166 @@ beforeEach(() => {
|
|
|
35
48
|
});
|
|
36
49
|
});
|
|
37
50
|
afterEach(() => {
|
|
51
|
+
restoreUserConfig();
|
|
38
52
|
server.stop();
|
|
39
53
|
rmSync(home, { recursive: true, force: true });
|
|
40
54
|
});
|
|
41
55
|
|
|
42
|
-
async function renderApp() {
|
|
43
|
-
const setup = await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
await setup.renderOnce();
|
|
49
|
-
return setup;
|
|
50
|
-
}
|
|
56
|
+
async function renderApp(sessionId = session.id) {
|
|
57
|
+
const setup = await renderReadyApp(<App home={home} sessionId={sessionId} />, {
|
|
58
|
+
width: 120,
|
|
59
|
+
height: 30,
|
|
60
|
+
});
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else await setup.mockInput.typeText(k);
|
|
56
|
-
await Bun.sleep(15);
|
|
57
|
-
await setup.renderOnce();
|
|
62
|
+
await waitForText(setup, "new Map()");
|
|
63
|
+
|
|
64
|
+
return setup;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
describe("diff review", () => {
|
|
61
68
|
test("renders file header, hunks, and signed lines", async () => {
|
|
69
|
+
// Arrange
|
|
62
70
|
const setup = await renderApp();
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
63
73
|
const frame = setup.captureCharFrame();
|
|
64
|
-
|
|
74
|
+
|
|
75
|
+
expect(frame).toContain("src/store.ts");
|
|
65
76
|
expect(frame).toContain("@@ -1,4 +1,4 @@");
|
|
66
|
-
expect(frame).
|
|
67
|
-
expect(frame).
|
|
77
|
+
expect(frame).toMatch(/- {2,}private items = \[\];/);
|
|
78
|
+
expect(frame).toMatch(/\+ {2,}private items = new Map\(\);/);
|
|
68
79
|
});
|
|
69
80
|
|
|
70
|
-
test("
|
|
81
|
+
test("a drag marks the code, typing comments on it, and the comment lands in feedback", async () => {
|
|
82
|
+
// Arrange
|
|
71
83
|
const setup = await renderApp();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await
|
|
75
|
-
|
|
76
|
-
await setup
|
|
84
|
+
|
|
85
|
+
// Act - mark "new Map()" on the added line and just start typing, as in the thread view
|
|
86
|
+
await dragText(setup, "new Map()", "new Map()", "new Map()".length);
|
|
87
|
+
await typeText(setup, "Map needs an eviction story.");
|
|
88
|
+
await waitForText(setup, "● Map needs an eviction story.");
|
|
89
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
90
|
+
|
|
91
|
+
// Assert - the discussion card hangs under the line; the anchor is the marked words
|
|
92
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
93
|
+
await waitForText(setup, "● Map needs an eviction story.");
|
|
94
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("new Map()");
|
|
95
|
+
|
|
96
|
+
// Act - submit with the session chord (cmd+enter, no composer open), confirm request_changes
|
|
97
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
77
98
|
await press(setup, "enter");
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
expect(stored.annotations.length).toBe(1);
|
|
82
|
-
expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
|
|
83
|
-
// inline annotation card rendered under the line
|
|
84
|
-
expect(setup.captureCharFrame()).toContain("◆ Map needs an eviction story.");
|
|
85
|
-
|
|
86
|
-
await press(setup, "enter"); // submit
|
|
87
|
-
await press(setup, "enter"); // confirm request_changes
|
|
88
|
-
await Bun.sleep(100);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
await waitForText(setup, "feedback sent");
|
|
89
102
|
const resolved = server.core.sessionGet(session.id);
|
|
103
|
+
|
|
90
104
|
expect(resolved.verdict!.feedback).toContain("new Map()");
|
|
91
105
|
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
92
106
|
});
|
|
93
107
|
|
|
94
|
-
test("
|
|
108
|
+
test("a comment reopens with its mark painted on the code", async () => {
|
|
109
|
+
// Arrange - a stored comment anchored to the added line's words
|
|
110
|
+
server.core.sessionAnnotate(session.id, {
|
|
111
|
+
id: "a_existing",
|
|
112
|
+
kind: "comment",
|
|
113
|
+
anchor: { quote: "new Map()", prefix: " private items = ", suffix: ";" },
|
|
114
|
+
body: "Existing note.",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Act
|
|
118
|
+
const setup = await renderApp();
|
|
119
|
+
|
|
120
|
+
// Assert
|
|
121
|
+
await waitForText(setup, "● Existing note.");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("option+c folds the caret's file to its band; the chevron restores its body", async () => {
|
|
125
|
+
// Arrange - the caret opens on the file's first code line
|
|
126
|
+
const setup = await renderApp();
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
await pressKey(setup, "c", { meta: true });
|
|
130
|
+
|
|
131
|
+
// Assert - the body is gone but the band (with its counts) remains
|
|
132
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("new Map()"));
|
|
133
|
+
const collapsed = setup.captureCharFrame();
|
|
134
|
+
|
|
135
|
+
expect(collapsed).toContain("src/store.ts");
|
|
136
|
+
expect(collapsed).toContain("+1");
|
|
137
|
+
|
|
138
|
+
// Act - the band's chevron unfolds it again
|
|
139
|
+
await clickText(setup, NERD.chevronRight);
|
|
140
|
+
|
|
141
|
+
// Assert
|
|
142
|
+
await waitForText(setup, "new Map()");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("curation needs full file contents; a legacy diff answers", async () => {
|
|
146
|
+
// Arrange - the session carries no artifact.files
|
|
95
147
|
const setup = await renderApp();
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
|
|
149
|
+
// Act
|
|
150
|
+
await pressKey(setup, "x", { meta: true });
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
await waitForText(setup, "hunk curation needs full file contents");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("rejecting the only change empties the curated working copy", async () => {
|
|
157
|
+
// Arrange - a diff session that carries full file contents
|
|
158
|
+
const withFiles = server.core.sessionCreate({
|
|
159
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
160
|
+
artifact: {
|
|
161
|
+
type: "diff",
|
|
162
|
+
content: PATCH,
|
|
163
|
+
meta: { title: "working tree" },
|
|
164
|
+
files: [
|
|
165
|
+
{
|
|
166
|
+
path: "src/store.ts",
|
|
167
|
+
oldContents: "export class Store {\n private items = [];\n}\n",
|
|
168
|
+
newContents: "export class Store {\n private items = new Map();\n}\n",
|
|
169
|
+
status: "modified",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
const setup = await renderApp(withFiles.id);
|
|
175
|
+
|
|
176
|
+
// Act - place the caret on the added line and reject its change
|
|
177
|
+
await clickText(setup, "new Map()");
|
|
178
|
+
await pressKey(setup, "x", { meta: true });
|
|
179
|
+
|
|
180
|
+
// Assert - the single change is gone, so the curated working copy is empty
|
|
181
|
+
await waitForText(setup, "change rejected");
|
|
182
|
+
const stored = server.core.sessionGet(withFiles.id);
|
|
183
|
+
|
|
184
|
+
expect(stored.workingCopy).toBe("");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a diff opens with the Changes column listing every changed file", async () => {
|
|
188
|
+
// Arrange - two changed files; config.ts is not in the patch text, so it can
|
|
189
|
+
// only appear here by way of the Changes column reading artifact.files
|
|
190
|
+
const withFiles = server.core.sessionCreate({
|
|
191
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
192
|
+
artifact: {
|
|
193
|
+
type: "diff",
|
|
194
|
+
content: PATCH,
|
|
195
|
+
meta: { title: "working tree" },
|
|
196
|
+
files: [
|
|
197
|
+
{ path: "src/store.ts", oldContents: "a", newContents: "b", status: "modified" },
|
|
198
|
+
{ path: "src/config.ts", oldContents: "", newContents: "c", status: "added" },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const setup = await renderReadyApp(<App home={home} sessionId={withFiles.id} />, {
|
|
203
|
+
width: 120,
|
|
204
|
+
height: 30,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await waitForText(setup, "cueloop");
|
|
208
|
+
|
|
209
|
+
// Assert - the context default opens the column for a diff, so both files show
|
|
210
|
+
await waitForText(setup, "config.ts");
|
|
211
|
+
expect(setup.captureCharFrame()).toContain("store.ts");
|
|
98
212
|
});
|
|
99
213
|
});
|
package/src/App.export.test.tsx
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier-2 wiring test for the notes-vault export: with [integrations.obsidian]
|
|
3
|
-
* configured to export on resolve, submitting a review writes the note into
|
|
4
|
-
* the vault and surfaces the path in the status line.
|
|
5
|
-
*/
|
|
1
|
+
/** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
|
|
6
2
|
|
|
7
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
9
13
|
import { tmpdir } from "node:os";
|
|
10
14
|
import { join } from "node:path";
|
|
11
15
|
import React from "react";
|
|
12
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
13
16
|
import { DaemonServer } from "@cueloop/daemon";
|
|
14
|
-
import type {
|
|
17
|
+
import type { Thread } from "@cueloop/schema";
|
|
15
18
|
import { App } from "./App";
|
|
19
|
+
import {
|
|
20
|
+
isolateUserConfig,
|
|
21
|
+
press,
|
|
22
|
+
waitForState,
|
|
23
|
+
waitForText,
|
|
24
|
+
pressKey,
|
|
25
|
+
renderReadyApp,
|
|
26
|
+
} from "./test-support";
|
|
16
27
|
|
|
17
28
|
const PLAN = `# Migration Plan
|
|
18
29
|
|
|
@@ -22,63 +33,70 @@ Move the store atomically.
|
|
|
22
33
|
let home: string;
|
|
23
34
|
let vault: string;
|
|
24
35
|
let server: DaemonServer;
|
|
25
|
-
let session:
|
|
26
|
-
let
|
|
36
|
+
let session: Thread;
|
|
37
|
+
let restoreUserConfig: () => void;
|
|
27
38
|
|
|
28
39
|
beforeEach(() => {
|
|
29
40
|
home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
|
|
30
41
|
vault = join(home, "vault");
|
|
31
42
|
mkdirSync(vault);
|
|
32
43
|
const configPath = join(home, "config.toml");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
|
|
45
|
+
writeFileSync(
|
|
46
|
+
configPath,
|
|
47
|
+
`[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`,
|
|
48
|
+
);
|
|
49
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
36
50
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
37
51
|
server.start();
|
|
38
52
|
session = server.core.sessionCreate({
|
|
39
53
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
40
|
-
artifact: {
|
|
54
|
+
artifact: {
|
|
55
|
+
type: "plan",
|
|
56
|
+
content: PLAN,
|
|
57
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
58
|
+
},
|
|
41
59
|
});
|
|
42
60
|
});
|
|
43
61
|
afterEach(() => {
|
|
44
|
-
|
|
45
|
-
else process.env.CUELOOP_CONFIG = savedConfigEnv;
|
|
62
|
+
restoreUserConfig();
|
|
46
63
|
server.stop();
|
|
47
64
|
rmSync(home, { recursive: true, force: true });
|
|
48
65
|
});
|
|
49
66
|
|
|
50
67
|
describe("obsidian export on resolve", () => {
|
|
51
68
|
test("submitting a review writes the plan into the vault and shows the path", async () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setup
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
69
|
+
// Arrange
|
|
70
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
71
|
+
width: 120,
|
|
72
|
+
height: 32,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await waitForText(setup, "cueloop");
|
|
76
|
+
|
|
77
|
+
// Act
|
|
78
|
+
await pressKey(setup, "RETURN", { meta: true }); // open the submit overlay, keep the default verdict (approve)
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
await waitForText(setup, "[Approve]");
|
|
82
|
+
|
|
83
|
+
// Act
|
|
84
|
+
await press(setup, "enter"); // submit
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
// the export is an async round-trip after resolve; the status line lands
|
|
88
|
+
// on a later render than the file write
|
|
65
89
|
const dir = join(vault, "cueloop");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await setup.renderOnce();
|
|
70
|
-
}
|
|
71
|
-
// the status line lands on a later render than the file write
|
|
72
|
-
const statusDeadline = Date.now() + 30_000;
|
|
73
|
-
while (Date.now() < statusDeadline && !setup.captureCharFrame().includes("exported to")) {
|
|
74
|
-
await Bun.sleep(25);
|
|
75
|
-
await setup.renderOnce();
|
|
76
|
-
}
|
|
90
|
+
|
|
91
|
+
await waitForState(setup, () => existsSync(dir));
|
|
92
|
+
await waitForText(setup, "exported to");
|
|
77
93
|
|
|
78
94
|
const notes = readdirSync(dir);
|
|
95
|
+
|
|
79
96
|
expect(notes.length).toBe(1);
|
|
80
97
|
expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
|
|
81
98
|
const written = readFileSync(join(dir, notes[0]!), "utf8");
|
|
99
|
+
|
|
82
100
|
expect(written).toContain("source: cueloop");
|
|
83
101
|
expect(written).toContain(`session: ${session.id}`);
|
|
84
102
|
expect(written).toContain("verdict: approve");
|