@cueloop/client 0.1.0-alpha.4 → 0.1.0-alpha.40
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/package.json +32 -24
- package/src/App.autoclose.test.tsx +137 -0
- package/src/App.diff.test.tsx +158 -22
- package/src/App.export.test.tsx +44 -37
- package/src/App.plan-review.test.tsx +632 -0
- package/src/App.readonly.test.tsx +60 -36
- package/src/App.rendering.test.tsx +132 -0
- package/src/App.review-panel.test.tsx +136 -0
- package/src/App.submit-confirm.test.tsx +220 -0
- package/src/App.test.tsx +128 -69
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +288 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +38 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +30 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +228 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +83 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +59 -0
- package/src/components/ConfirmCard.stories.tsx +56 -0
- package/src/components/ConfirmCard.tsx +135 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +355 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +62 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +389 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +78 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +101 -0
- package/src/components/ReviewRail.stories.tsx +220 -0
- package/src/components/ReviewRail.tsx +316 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +141 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +108 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1829 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +21 -0
- package/src/components/agent-launcher.tsx +123 -0
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +48 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +64 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +98 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +136 -0
- package/src/components/stories-app.tsx +123 -0
- package/src/components/stories.test.tsx +62 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +69 -0
- package/src/components/syntax-highlight.ts +92 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +36 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +430 -31
- package/src/config.ts +304 -26
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +143 -0
- package/src/editor.ts +123 -9
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +27 -0
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +30 -0
- package/src/intent-dispatch.test.ts +492 -0
- package/src/intent-dispatch.ts +428 -0
- package/src/key-bindings.test.ts +152 -0
- package/src/key-bindings.ts +424 -0
- package/src/keymap.test.ts +622 -0
- package/src/keymap.ts +339 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +24 -9
- package/src/serve.ts +7 -7
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +309 -0
- package/src/session-controller.ts +900 -0
- package/src/share.ts +113 -0
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +187 -0
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +36 -15
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +34 -23
- package/src/view-plan.test.ts +294 -0
- package/src/view-plan.ts +408 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +70 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
package/src/config.test.ts
CHANGED
|
@@ -2,57 +2,302 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_KEYS,
|
|
7
|
+
DEFAULT_QUICK_ACTIONS,
|
|
8
|
+
actionFor,
|
|
9
|
+
loadConfig,
|
|
10
|
+
persistAuthorName,
|
|
11
|
+
persistReviewState,
|
|
12
|
+
persistActions,
|
|
13
|
+
persistReviewWidth,
|
|
14
|
+
persistTheme,
|
|
15
|
+
quickActionBody,
|
|
16
|
+
resolveQuickAction,
|
|
17
|
+
} from "./config";
|
|
18
|
+
import { REVIEW_DEFAULT_WIDTH, REVIEW_MAX_WIDTH } from "./review-panel";
|
|
6
19
|
import { DARK } from "./theme";
|
|
20
|
+
import { themeForName } from "./theme-presets";
|
|
7
21
|
|
|
8
22
|
describe("loadConfig", () => {
|
|
9
23
|
test("defaults when no file exists", () => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
// Act
|
|
25
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(config.keys["comment"]).toEqual(["c"]);
|
|
29
|
+
expect(config.theme.accent).toBe(DARK.accent);
|
|
13
30
|
});
|
|
14
31
|
|
|
15
32
|
test("user config rebinds actions and overrides theme tokens", () => {
|
|
33
|
+
// Arrange
|
|
16
34
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-"));
|
|
17
35
|
const path = join(dir, "config.toml");
|
|
18
36
|
writeFileSync(
|
|
19
37
|
path,
|
|
20
38
|
`[keys]\ncomment = "a"\nsubmit = ["return", "S"]\n\n[theme]\naccent = "#ff0000"\n`,
|
|
21
39
|
);
|
|
40
|
+
|
|
22
41
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
expect(
|
|
28
|
-
expect(
|
|
42
|
+
// Act
|
|
43
|
+
const config = loadConfig({ userConfigPath: path });
|
|
44
|
+
|
|
45
|
+
// Assert
|
|
46
|
+
expect(config.keys["comment"]).toEqual(["a"]);
|
|
47
|
+
expect(config.keys["submit"]).toEqual(["return", "S"]);
|
|
48
|
+
expect(config.keys["cut"]).toEqual(["x"]); // untouched defaults survive
|
|
49
|
+
expect(config.theme.accent).toBe("#ff0000");
|
|
50
|
+
expect(config.theme.green).toBe(DARK.green);
|
|
29
51
|
} finally {
|
|
30
52
|
rmSync(dir, { recursive: true, force: true });
|
|
31
53
|
}
|
|
32
54
|
});
|
|
33
55
|
|
|
34
56
|
test("repo config layers over user config", () => {
|
|
57
|
+
// Arrange
|
|
35
58
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg2-"));
|
|
36
59
|
const user = join(dir, "user.toml");
|
|
37
60
|
const repoRoot = join(dir, "repo");
|
|
38
61
|
writeFileSync(user, `[keys]\ncomment = "a"\n`);
|
|
39
62
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
40
63
|
writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[keys]\ncomment = "z"\n`);
|
|
64
|
+
|
|
41
65
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
// Act
|
|
67
|
+
const config = loadConfig({ userConfigPath: user, repoRoot });
|
|
68
|
+
|
|
69
|
+
// Assert
|
|
70
|
+
expect(config.keys["comment"]).toEqual(["z"]);
|
|
44
71
|
} finally {
|
|
45
72
|
rmSync(dir, { recursive: true, force: true });
|
|
46
73
|
}
|
|
47
74
|
});
|
|
48
75
|
|
|
49
76
|
test("broken config never blocks a review", () => {
|
|
77
|
+
// Arrange
|
|
50
78
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg3-"));
|
|
51
79
|
const path = join(dir, "config.toml");
|
|
52
80
|
writeFileSync(path, "not [valid toml");
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Act
|
|
84
|
+
const config = loadConfig({ userConfigPath: path });
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
expect(config.keys["comment"]).toEqual(["c"]);
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("[ui] parses auto_close and the editor override", () => {
|
|
94
|
+
// Arrange
|
|
95
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg4-"));
|
|
96
|
+
const path = join(dir, "config.toml");
|
|
97
|
+
writeFileSync(path, `[ui]\nauto_close = 3\neditor = "code --wait"\n`);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Act
|
|
101
|
+
const config = loadConfig({ userConfigPath: path });
|
|
102
|
+
|
|
103
|
+
// Assert
|
|
104
|
+
expect(config.ui.autoClose).toBe(3);
|
|
105
|
+
expect(config.ui.editor).toBe("code --wait");
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("[ui] editor defaults to undefined and ignores a blank value", () => {
|
|
112
|
+
// Arrange
|
|
113
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg5-"));
|
|
114
|
+
const path = join(dir, "config.toml");
|
|
115
|
+
writeFileSync(path, `[ui]\neditor = " "\n`);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Assert
|
|
119
|
+
expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.editor).toBeUndefined();
|
|
120
|
+
expect(loadConfig({ userConfigPath: path }).ui.editor).toBeUndefined();
|
|
121
|
+
} finally {
|
|
122
|
+
rmSync(dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("[ui] review panel defaults to an expanded rail at the default width", () => {
|
|
127
|
+
// Act
|
|
128
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
expect(config.ui.reviewState).toBe("expanded");
|
|
132
|
+
expect(config.ui.reviewWidth).toBe(REVIEW_DEFAULT_WIDTH);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("[ui] parses review_width (clamped) and review_state", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg6-"));
|
|
138
|
+
const path = join(dir, "config.toml");
|
|
139
|
+
writeFileSync(path, `[ui]\nreview_width = 999\nreview_state = "compact"\n`);
|
|
140
|
+
|
|
53
141
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
142
|
+
// Act
|
|
143
|
+
const config = loadConfig({ userConfigPath: path });
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
expect(config.ui.reviewWidth).toBe(REVIEW_MAX_WIDTH); // out-of-range width clamps
|
|
147
|
+
expect(config.ui.reviewState).toBe("compact");
|
|
148
|
+
} finally {
|
|
149
|
+
rmSync(dir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("[ui] ignores an unknown review_state and a non-numeric width", () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg7-"));
|
|
156
|
+
const path = join(dir, "config.toml");
|
|
157
|
+
writeFileSync(path, `[ui]\nreview_width = "wide"\nreview_state = "sideways"\n`);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Act
|
|
161
|
+
const config = loadConfig({ userConfigPath: path });
|
|
162
|
+
|
|
163
|
+
// Assert
|
|
164
|
+
expect(config.ui.reviewWidth).toBe(REVIEW_DEFAULT_WIDTH);
|
|
165
|
+
expect(config.ui.reviewState).toBe("expanded");
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("[ui] theme defaults to the branded cueloop preset", () => {
|
|
172
|
+
// Act
|
|
173
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
174
|
+
|
|
175
|
+
// Assert
|
|
176
|
+
expect(config.ui.theme).toBe("cueloop");
|
|
177
|
+
expect(config.theme.accent).toBe(DARK.accent);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("[ui] theme selects a named preset as the token base", () => {
|
|
181
|
+
// Arrange
|
|
182
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-"));
|
|
183
|
+
const path = join(dir, "config.toml");
|
|
184
|
+
writeFileSync(path, `[ui]\ntheme = "nord"\n`);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Act
|
|
188
|
+
const config = loadConfig({ userConfigPath: path });
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
expect(config.ui.theme).toBe("nord");
|
|
192
|
+
expect(config.theme).toEqual(themeForName("nord"));
|
|
193
|
+
} finally {
|
|
194
|
+
rmSync(dir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("[theme] token overrides layer on top of the selected preset", () => {
|
|
199
|
+
// Arrange
|
|
200
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme2-"));
|
|
201
|
+
const path = join(dir, "config.toml");
|
|
202
|
+
writeFileSync(path, `[ui]\ntheme = "nord"\n\n[theme]\naccent = "#ff0000"\n`);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// Act
|
|
206
|
+
const config = loadConfig({ userConfigPath: path });
|
|
207
|
+
|
|
208
|
+
// Assert
|
|
209
|
+
expect(config.theme.accent).toBe("#ff0000"); // override wins
|
|
210
|
+
expect(config.theme.background).toBe(themeForName("nord").background); // preset base survives
|
|
211
|
+
expect(config.themeOverrides).toEqual({ accent: "#ff0000" }); // deltas exposed for live re-compose
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(dir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("a later file's preset keeps an earlier file's [theme] override", () => {
|
|
218
|
+
// Arrange - user overrides accent; repo picks a preset in a separate file
|
|
219
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-x-"));
|
|
220
|
+
const user = join(dir, "user.toml");
|
|
221
|
+
const repoRoot = join(dir, "repo");
|
|
222
|
+
writeFileSync(user, `[theme]\naccent = "#ff0000"\n`);
|
|
223
|
+
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
224
|
+
writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[ui]\ntheme = "nord"\n`);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Act
|
|
228
|
+
const config = loadConfig({ userConfigPath: user, repoRoot });
|
|
229
|
+
|
|
230
|
+
// Assert - the preset is the base, but the earlier override still wins its token
|
|
231
|
+
expect(config.ui.theme).toBe("nord");
|
|
232
|
+
expect(config.theme.background).toBe(themeForName("nord").background);
|
|
233
|
+
expect(config.theme.accent).toBe("#ff0000");
|
|
234
|
+
} finally {
|
|
235
|
+
rmSync(dir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("[ui] ignores an unknown theme name and keeps the default", () => {
|
|
240
|
+
// Arrange
|
|
241
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme3-"));
|
|
242
|
+
const path = join(dir, "config.toml");
|
|
243
|
+
writeFileSync(path, `[ui]\ntheme = "solarized-galaxy"\n`);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Act
|
|
247
|
+
const config = loadConfig({ userConfigPath: path });
|
|
248
|
+
|
|
249
|
+
// Assert
|
|
250
|
+
expect(config.ui.theme).toBe("cueloop");
|
|
251
|
+
expect(config.theme.accent).toBe(DARK.accent);
|
|
252
|
+
} finally {
|
|
253
|
+
rmSync(dir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("persistTheme round-trips through the config file", () => {
|
|
258
|
+
// Arrange
|
|
259
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme4-"));
|
|
260
|
+
const path = join(dir, "config.toml");
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Act
|
|
264
|
+
persistTheme("tokyo-night", path);
|
|
265
|
+
|
|
266
|
+
// Assert
|
|
267
|
+
expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("tokyo-night");
|
|
268
|
+
|
|
269
|
+
// a second write replaces the key in place
|
|
270
|
+
// Act
|
|
271
|
+
persistTheme("gruvbox-dark", path);
|
|
272
|
+
|
|
273
|
+
// Assert
|
|
274
|
+
expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("gruvbox-dark");
|
|
275
|
+
} finally {
|
|
276
|
+
rmSync(dir, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("persistReviewWidth and persistReviewState round-trip through the config file", () => {
|
|
281
|
+
// Arrange
|
|
282
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg8-"));
|
|
283
|
+
const path = join(dir, "config.toml");
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Act
|
|
287
|
+
persistReviewWidth(42, path);
|
|
288
|
+
persistReviewState("hidden", path);
|
|
289
|
+
|
|
290
|
+
// Assert
|
|
291
|
+
const config = loadConfig({ userConfigPath: path });
|
|
292
|
+
expect(config.ui.reviewWidth).toBe(42);
|
|
293
|
+
expect(config.ui.reviewState).toBe("hidden");
|
|
294
|
+
|
|
295
|
+
// a second write replaces the key in place rather than appending a duplicate
|
|
296
|
+
// Act
|
|
297
|
+
persistReviewWidth(30, path);
|
|
298
|
+
|
|
299
|
+
// Assert
|
|
300
|
+
expect(loadConfig({ userConfigPath: path }).ui.reviewWidth).toBe(30);
|
|
56
301
|
} finally {
|
|
57
302
|
rmSync(dir, { recursive: true, force: true });
|
|
58
303
|
}
|
|
@@ -61,56 +306,210 @@ describe("loadConfig", () => {
|
|
|
61
306
|
|
|
62
307
|
describe("integrations.obsidian config", () => {
|
|
63
308
|
test("defaults when the section is absent", () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
309
|
+
// Act
|
|
310
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
311
|
+
|
|
312
|
+
// Assert
|
|
313
|
+
expect(config.integrations.obsidian.vault).toBeUndefined();
|
|
314
|
+
expect(config.integrations.obsidian.folder).toBe("cueloop");
|
|
315
|
+
expect(config.integrations.obsidian.filenameFormat).toBe("{YYYY}-{MM}-{DD} - {title}");
|
|
316
|
+
expect(config.integrations.obsidian.separator).toBe("space");
|
|
317
|
+
expect(config.integrations.obsidian.exportOn).toBe("manual");
|
|
70
318
|
});
|
|
71
319
|
|
|
72
320
|
test("user config sets the section; invalid enum values are ignored", () => {
|
|
321
|
+
// Arrange
|
|
73
322
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs-"));
|
|
74
323
|
const path = join(dir, "config.toml");
|
|
75
324
|
writeFileSync(
|
|
76
325
|
path,
|
|
77
326
|
`[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approve"\nseparator = "comma"\n`,
|
|
78
327
|
);
|
|
328
|
+
|
|
79
329
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
expect(
|
|
85
|
-
expect(
|
|
330
|
+
// Act
|
|
331
|
+
const config = loadConfig({ userConfigPath: path });
|
|
332
|
+
|
|
333
|
+
// Assert
|
|
334
|
+
expect(config.integrations.obsidian.vault).toBe("/notes/vault");
|
|
335
|
+
expect(config.integrations.obsidian.folder).toBe("plans");
|
|
336
|
+
expect(config.integrations.obsidian.exportOn).toBe("approve");
|
|
337
|
+
expect(config.integrations.obsidian.separator).toBe("space"); // invalid value falls back
|
|
338
|
+
expect(config.keys["comment"]).toEqual(["c"]); // other sections untouched
|
|
86
339
|
} finally {
|
|
87
340
|
rmSync(dir, { recursive: true, force: true });
|
|
88
341
|
}
|
|
89
342
|
});
|
|
90
343
|
|
|
91
344
|
test("repo config layers over user config for the section", () => {
|
|
345
|
+
// Arrange
|
|
92
346
|
const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs2-"));
|
|
93
347
|
const user = join(dir, "user.toml");
|
|
94
348
|
const repoRoot = join(dir, "repo");
|
|
95
349
|
writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "resolve"\n`);
|
|
96
350
|
Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
|
|
97
|
-
writeFileSync(
|
|
351
|
+
writeFileSync(
|
|
352
|
+
join(repoRoot, ".cueloop", "config.toml"),
|
|
353
|
+
`[integrations.obsidian]\nfolder = "repo-plans"\n`,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
// Act
|
|
358
|
+
const config = loadConfig({ userConfigPath: user, repoRoot });
|
|
359
|
+
|
|
360
|
+
// Assert
|
|
361
|
+
expect(config.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
|
|
362
|
+
expect(config.integrations.obsidian.exportOn).toBe("resolve");
|
|
363
|
+
expect(config.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
|
|
364
|
+
} finally {
|
|
365
|
+
rmSync(dir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("quick actions ([[actions]])", () => {
|
|
371
|
+
test("absent config keeps the built-in defaults", () => {
|
|
372
|
+
// Act
|
|
373
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
374
|
+
|
|
375
|
+
// Assert
|
|
376
|
+
expect(config.actions).toEqual(DEFAULT_QUICK_ACTIONS);
|
|
377
|
+
expect(config.actions).toHaveLength(7);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("configured actions replace the defaults, with optional metadata", () => {
|
|
381
|
+
// Arrange
|
|
382
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-actions-"));
|
|
383
|
+
const path = join(dir, "config.toml");
|
|
384
|
+
writeFileSync(
|
|
385
|
+
path,
|
|
386
|
+
`[[actions]]\nprompt = "Add a benchmark"\n\n[[actions]]\nprompt = "Guard the edge case"\nmetadata = "null and empty input"\n`,
|
|
387
|
+
);
|
|
388
|
+
|
|
98
389
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
390
|
+
// Act
|
|
391
|
+
const config = loadConfig({ userConfigPath: path });
|
|
392
|
+
|
|
393
|
+
// Assert
|
|
394
|
+
expect(config.actions).toEqual([
|
|
395
|
+
{ prompt: "Add a benchmark" },
|
|
396
|
+
{ prompt: "Guard the edge case", metadata: "null and empty input" },
|
|
397
|
+
]);
|
|
398
|
+
} finally {
|
|
399
|
+
rmSync(dir, { recursive: true, force: true });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("a table missing prompt is skipped; the rest replace the defaults", () => {
|
|
404
|
+
// Arrange
|
|
405
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-actions2-"));
|
|
406
|
+
const path = join(dir, "config.toml");
|
|
407
|
+
writeFileSync(
|
|
408
|
+
path,
|
|
409
|
+
`[[actions]]\nmetadata = "orphan without a prompt"\n\n[[actions]]\nprompt = "Keep this one"\n`,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
// Act
|
|
414
|
+
const config = loadConfig({ userConfigPath: path });
|
|
415
|
+
|
|
416
|
+
// Assert
|
|
417
|
+
expect(config.actions).toEqual([{ prompt: "Keep this one" }]);
|
|
103
418
|
} finally {
|
|
104
419
|
rmSync(dir, { recursive: true, force: true });
|
|
105
420
|
}
|
|
106
421
|
});
|
|
107
422
|
});
|
|
108
423
|
|
|
424
|
+
describe("persistActions", () => {
|
|
425
|
+
test("round-trips the action set through [[actions]] and replaces prior ones", () => {
|
|
426
|
+
// Arrange
|
|
427
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-persist-actions-"));
|
|
428
|
+
const path = join(dir, "config.toml");
|
|
429
|
+
writeFileSync(path, `[[actions]]\nprompt = "old one"\n\n[ui]\nauto_close = 3\n`);
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Act
|
|
433
|
+
persistActions(
|
|
434
|
+
[{ prompt: "Zoom out" }, { prompt: "Prototype", metadata: "skip tests and polish" }],
|
|
435
|
+
path,
|
|
436
|
+
);
|
|
437
|
+
const config = loadConfig({ userConfigPath: path });
|
|
438
|
+
|
|
439
|
+
// Assert - the old action is gone, the new set is read back, and [ui] survives
|
|
440
|
+
expect(config.actions).toEqual([
|
|
441
|
+
{ prompt: "Zoom out" },
|
|
442
|
+
{ prompt: "Prototype", metadata: "skip tests and polish" },
|
|
443
|
+
]);
|
|
444
|
+
expect(config.ui.autoClose).toBe(3);
|
|
445
|
+
} finally {
|
|
446
|
+
rmSync(dir, { recursive: true, force: true });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("quickActionBody", () => {
|
|
452
|
+
test("appends the system prompt below the prompt when set", () => {
|
|
453
|
+
// Assert
|
|
454
|
+
expect(quickActionBody({ prompt: "Prototype this", metadata: "skip tests" })).toBe(
|
|
455
|
+
"Prototype this\n\nskip tests",
|
|
456
|
+
);
|
|
457
|
+
expect(quickActionBody({ prompt: "Out of scope" })).toBe("Out of scope");
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("resolveQuickAction", () => {
|
|
462
|
+
test("resolves by 1-based index and by case-insensitive prompt", () => {
|
|
463
|
+
// Assert
|
|
464
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "1")).toEqual(DEFAULT_QUICK_ACTIONS[0]!);
|
|
465
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "out of scope")).toEqual(
|
|
466
|
+
DEFAULT_QUICK_ACTIONS[2]!,
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("returns undefined for an out-of-range index or an unknown name", () => {
|
|
471
|
+
// Assert
|
|
472
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "0")).toBeUndefined();
|
|
473
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "99")).toBeUndefined();
|
|
474
|
+
expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "no such action")).toBeUndefined();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
109
478
|
describe("actionFor", () => {
|
|
110
479
|
test("resolves keys and shifted keys to actions", () => {
|
|
480
|
+
// Assert
|
|
111
481
|
expect(actionFor(DEFAULT_KEYS, "j", false)).toBe("down");
|
|
112
482
|
expect(actionFor(DEFAULT_KEYS, "g", true)).toBe("bottom"); // G
|
|
113
483
|
expect(actionFor(DEFAULT_KEYS, "return", false)).toBe("submit");
|
|
114
484
|
expect(actionFor(DEFAULT_KEYS, "zz", false)).toBeUndefined();
|
|
115
485
|
});
|
|
116
486
|
});
|
|
487
|
+
|
|
488
|
+
describe("[authors] rename map", () => {
|
|
489
|
+
test("loads quoted fingerprint keys", () => {
|
|
490
|
+
// Arrange
|
|
491
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-authors-"));
|
|
492
|
+
const path = join(dir, "config.toml");
|
|
493
|
+
writeFileSync(path, `[authors]\n"SHA256:abc+def/gh" = "Alex"\n`);
|
|
494
|
+
|
|
495
|
+
// Act
|
|
496
|
+
const config = loadConfig({ userConfigPath: path });
|
|
497
|
+
|
|
498
|
+
// Assert
|
|
499
|
+
expect(config.authors["SHA256:abc+def/gh"]).toBe("Alex");
|
|
500
|
+
rmSync(dir, { recursive: true, force: true });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("persistAuthorName round-trips and updates a fingerprint id in place", () => {
|
|
504
|
+
// Arrange
|
|
505
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-authors2-"));
|
|
506
|
+
const path = join(dir, "config.toml");
|
|
507
|
+
|
|
508
|
+
// Act / Assert
|
|
509
|
+
persistAuthorName("SHA256:abc+def/gh", "Alex", path);
|
|
510
|
+
expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alex");
|
|
511
|
+
persistAuthorName("SHA256:abc+def/gh", "Alexa", path);
|
|
512
|
+
expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alexa");
|
|
513
|
+
rmSync(dir, { recursive: true, force: true });
|
|
514
|
+
});
|
|
515
|
+
});
|