@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +42 -34
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +259 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +330 -161
  16. package/src/App.prototype-markdown.test.tsx +128 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +264 -0
  23. package/src/App.test.tsx +402 -126
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1402 -1060
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +774 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +577 -0
  35. package/src/app-view-model.ts +168 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/client-extension-registry.test.tsx +61 -0
  39. package/src/client-extension-registry.ts +159 -0
  40. package/src/clipboard.test.ts +23 -0
  41. package/src/clipboard.ts +33 -0
  42. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  43. package/src/components/AnnotatableFileView.test.tsx +102 -0
  44. package/src/components/AnnotatableFileView.tsx +134 -0
  45. package/src/components/AnnotationCards.stories.tsx +86 -0
  46. package/src/components/AnnotationCards.tsx +341 -0
  47. package/src/components/AnsiScreen.stories.tsx +12 -0
  48. package/src/components/AnsiScreen.tsx +28 -0
  49. package/src/components/AppHeader.stories.tsx +50 -0
  50. package/src/components/AppHeader.tsx +83 -0
  51. package/src/components/AppShell.brand.test.tsx +58 -0
  52. package/src/components/AppShell.extensions.test.tsx +191 -0
  53. package/src/components/AppShell.stories.tsx +89 -0
  54. package/src/components/AppShell.tsx +362 -0
  55. package/src/components/AppShell.zoom.test.tsx +135 -0
  56. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  57. package/src/components/BareWorkbenchFileView.tsx +64 -0
  58. package/src/components/Breadcrumb.stories.tsx +19 -0
  59. package/src/components/Breadcrumb.tsx +45 -0
  60. package/src/components/ChangesColumn.stories.tsx +44 -0
  61. package/src/components/ChangesColumn.tsx +199 -0
  62. package/src/components/ChangesFileTree.test.tsx +61 -0
  63. package/src/components/CodeBlock.stories.tsx +42 -0
  64. package/src/components/CodeBlock.tsx +84 -0
  65. package/src/components/CompletionOverlay.stories.tsx +37 -0
  66. package/src/components/CompletionOverlay.tsx +77 -0
  67. package/src/components/ConfirmCard.stories.tsx +37 -0
  68. package/src/components/ConfirmCard.tsx +150 -0
  69. package/src/components/ConfirmDialog.stories.tsx +21 -0
  70. package/src/components/ConfirmDialog.tsx +72 -0
  71. package/src/components/DiffContentView.scroll.test.tsx +149 -0
  72. package/src/components/DiffContentView.split.test.tsx +140 -0
  73. package/src/components/DiffContentView.stories.tsx +80 -0
  74. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  75. package/src/components/DiffContentView.tsx +906 -0
  76. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  77. package/src/components/DiscussionMarkerRail.tsx +186 -0
  78. package/src/components/EditorGrid.stories.tsx +59 -0
  79. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  80. package/src/components/EditorGrid.tsx +426 -0
  81. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  82. package/src/components/ExtensionZone.stories.tsx +28 -0
  83. package/src/components/ExtensionZone.tsx +145 -0
  84. package/src/components/FileContentsView.stories.tsx +16 -0
  85. package/src/components/FileContentsView.test.tsx +29 -0
  86. package/src/components/FileContentsView.tsx +98 -0
  87. package/src/components/GridTabContent.stories.tsx +59 -0
  88. package/src/components/GridTabContent.tsx +182 -0
  89. package/src/components/HandleChipsInput.stories.tsx +24 -0
  90. package/src/components/HandleChipsInput.test.tsx +35 -0
  91. package/src/components/HandleChipsInput.tsx +145 -0
  92. package/src/components/KeybindsDialog.stories.tsx +31 -0
  93. package/src/components/KeybindsDialog.tsx +103 -0
  94. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  95. package/src/components/MarkdownGridBlock.tsx +72 -0
  96. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  97. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  98. package/src/components/MarkdownThreadEditor.tsx +162 -0
  99. package/src/components/MenuBar.stories.tsx +28 -0
  100. package/src/components/MenuBar.tsx +44 -0
  101. package/src/components/NavModeHint.stories.tsx +40 -0
  102. package/src/components/NavModeHint.tsx +31 -0
  103. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  104. package/src/components/OverlayScrollbar.test.tsx +139 -0
  105. package/src/components/OverlayScrollbar.tsx +129 -0
  106. package/src/components/PaneGrid.stories.tsx +84 -0
  107. package/src/components/PaneGrid.tsx +128 -0
  108. package/src/components/PanelColumn.stories.tsx +25 -0
  109. package/src/components/PanelColumn.tsx +98 -0
  110. package/src/components/ProjectTreeView.stories.tsx +25 -0
  111. package/src/components/ProjectTreeView.test.tsx +75 -0
  112. package/src/components/ProjectTreeView.tsx +125 -0
  113. package/src/components/PromptDialog.stories.tsx +36 -0
  114. package/src/components/PromptDialog.test.tsx +61 -0
  115. package/src/components/PromptDialog.tsx +99 -0
  116. package/src/components/PrototypeContentView.stories.tsx +19 -0
  117. package/src/components/PrototypeContentView.tsx +446 -0
  118. package/src/components/RootOverlay.stories.tsx +45 -0
  119. package/src/components/RootOverlay.tsx +48 -0
  120. package/src/components/ScrollArea.stories.tsx +20 -0
  121. package/src/components/ScrollArea.tsx +44 -0
  122. package/src/components/SettingsDialog.stories.tsx +117 -0
  123. package/src/components/SettingsDialog.tsx +186 -0
  124. package/src/components/SettingsRows.stories.tsx +18 -0
  125. package/src/components/SettingsRows.tsx +112 -0
  126. package/src/components/ShareDialog.stories.tsx +121 -0
  127. package/src/components/ShareDialog.tsx +552 -0
  128. package/src/components/ShellHeader.stories.tsx +50 -0
  129. package/src/components/ShellHeader.tsx +137 -0
  130. package/src/components/SlashComposer.stories.tsx +37 -0
  131. package/src/components/SlashComposer.tsx +87 -0
  132. package/src/components/ThreadFooter.stories.tsx +30 -0
  133. package/src/components/ThreadFooter.test.tsx +47 -0
  134. package/src/components/ThreadFooter.tsx +76 -0
  135. package/src/components/ThreadTree.stories.tsx +46 -0
  136. package/src/components/ThreadTree.tsx +255 -0
  137. package/src/components/ThreadView.selection.test.tsx +951 -0
  138. package/src/components/ThreadView.slash.test.tsx +146 -0
  139. package/src/components/ThreadView.stories.tsx +26 -0
  140. package/src/components/ThreadView.tsx +525 -0
  141. package/src/components/Toast.stories.tsx +19 -0
  142. package/src/components/Toast.tsx +52 -0
  143. package/src/components/Tooltip.stories.tsx +26 -0
  144. package/src/components/Tooltip.tsx +72 -0
  145. package/src/components/TreePane.stories.tsx +106 -0
  146. package/src/components/TreePane.tsx +157 -0
  147. package/src/components/WalkWizard.stories.tsx +100 -0
  148. package/src/components/WalkWizard.tsx +150 -0
  149. package/src/components/WelcomePlayground.stories.tsx +12 -0
  150. package/src/components/WelcomePlayground.test.tsx +49 -0
  151. package/src/components/WelcomePlayground.tsx +139 -0
  152. package/src/components/WorkspacePanels.stories.tsx +36 -0
  153. package/src/components/WorkspacePanels.tsx +116 -0
  154. package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
  155. package/src/components/diff-content-view-layout.test.ts +198 -0
  156. package/src/components/diff-content-view-layout.ts +84 -0
  157. package/src/components/editor-grid.test.ts +86 -0
  158. package/src/components/editor-grid.ts +138 -0
  159. package/src/components/file-tree.test.ts +60 -0
  160. package/src/components/file-tree.ts +112 -0
  161. package/src/components/menu-control.stories.tsx +35 -0
  162. package/src/components/menu-control.tsx +43 -0
  163. package/src/components/pages/Workspace.stories.tsx +181 -0
  164. package/src/components/primitives/Button.stories.tsx +32 -0
  165. package/src/components/primitives/Button.tsx +59 -0
  166. package/src/components/primitives/Card.stories.tsx +25 -0
  167. package/src/components/primitives/Card.tsx +63 -0
  168. package/src/components/primitives/Dialog.stories.tsx +24 -0
  169. package/src/components/primitives/Dialog.test.tsx +37 -0
  170. package/src/components/primitives/Dialog.tsx +71 -0
  171. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  172. package/src/components/primitives/DialogActions.tsx +39 -0
  173. package/src/components/primitives/IconButton.stories.tsx +36 -0
  174. package/src/components/primitives/IconButton.tsx +62 -0
  175. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  176. package/src/components/primitives/StatusBar.tsx +23 -0
  177. package/src/components/primitives/Switch.stories.tsx +17 -0
  178. package/src/components/primitives/Switch.tsx +32 -0
  179. package/src/components/primitives/Tabs.stories.tsx +29 -0
  180. package/src/components/primitives/Tabs.tsx +102 -0
  181. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  182. package/src/components/primitives/Toolbar.tsx +14 -0
  183. package/src/components/primitives/Tree.stories.tsx +124 -0
  184. package/src/components/primitives/Tree.tsx +100 -0
  185. package/src/components/primitives/frame.test.ts +12 -0
  186. package/src/components/primitives/frame.ts +17 -0
  187. package/src/components/primitives/icons.ts +61 -0
  188. package/src/components/primitives/tree-model.test.ts +99 -0
  189. package/src/components/primitives/tree-model.ts +137 -0
  190. package/src/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/components/quick-actions-editor.test.tsx +117 -0
  192. package/src/components/quick-actions-editor.tsx +226 -0
  193. package/src/components/share-dialog-store.ts +58 -0
  194. package/src/components/stories-app.tsx +193 -0
  195. package/src/components/stories.test.tsx +74 -0
  196. package/src/components/story-fixtures.ts +102 -0
  197. package/src/components/story-tree.test.ts +108 -0
  198. package/src/components/story.ts +159 -0
  199. package/src/components/syntax-highlight.ts +133 -0
  200. package/src/components/terminal-pane.test.tsx +40 -0
  201. package/src/components/terminal-pane.ts +199 -0
  202. package/src/components/theme-context.stories.tsx +46 -0
  203. package/src/components/theme-context.tsx +37 -0
  204. package/src/components/thread-tree.test.ts +134 -0
  205. package/src/components/thread-tree.ts +109 -0
  206. package/src/components/truncate-text.ts +7 -0
  207. package/src/components/truncate-title.test.ts +31 -0
  208. package/src/components/truncate-title.ts +15 -0
  209. package/src/config-identity.test.ts +78 -0
  210. package/src/config.test.ts +579 -34
  211. package/src/config.ts +533 -57
  212. package/src/diff-fold.test.ts +78 -0
  213. package/src/diff-fold.ts +126 -0
  214. package/src/diff-hunk-curate.test.ts +315 -0
  215. package/src/diff-hunk-curate.ts +153 -0
  216. package/src/diff-intraline.test.ts +238 -0
  217. package/src/diff-intraline.ts +236 -0
  218. package/src/diff-syntax.test.ts +112 -0
  219. package/src/diff-syntax.ts +179 -0
  220. package/src/discussions.ts +79 -0
  221. package/src/editor.test.ts +149 -0
  222. package/src/editor.ts +139 -9
  223. package/src/ghostty-terminal.test.ts +85 -0
  224. package/src/ghostty-terminal.ts +199 -0
  225. package/src/github-identity.test.ts +58 -0
  226. package/src/github-identity.ts +41 -0
  227. package/src/herdr.test.ts +9 -34
  228. package/src/herdr.ts +25 -21
  229. package/src/index.ts +28 -0
  230. package/src/integrations.test.ts +41 -0
  231. package/src/integrations.ts +32 -0
  232. package/src/intent-dispatch.test.ts +724 -0
  233. package/src/intent-dispatch.ts +690 -0
  234. package/src/key-bindings.test.ts +156 -0
  235. package/src/key-bindings.ts +459 -0
  236. package/src/keymap.test.ts +456 -115
  237. package/src/keymap.ts +315 -81
  238. package/src/kitty-image.test.ts +66 -0
  239. package/src/kitty-image.ts +138 -0
  240. package/src/launch-layout.test.ts +67 -0
  241. package/src/launch-layout.ts +47 -0
  242. package/src/mark-runs.ts +121 -0
  243. package/src/markdown-editor-syntax-style.ts +50 -0
  244. package/src/markdown-grid.test.ts +101 -0
  245. package/src/markdown-grid.ts +203 -0
  246. package/src/markdown-highlight.test.ts +61 -0
  247. package/src/markdown-highlight.ts +122 -0
  248. package/src/pasted-image.test.ts +67 -0
  249. package/src/pasted-image.ts +60 -0
  250. package/src/perf/otlp-trace.test.ts +116 -0
  251. package/src/perf/otlp-trace.ts +130 -0
  252. package/src/perf/perf-report.ts +17 -0
  253. package/src/perf/perf-timings.test.ts +18 -0
  254. package/src/perf/perf-timings.ts +38 -0
  255. package/src/prototype-browser.test.ts +81 -0
  256. package/src/prototype-browser.ts +329 -0
  257. package/src/prototype-pixels.tsx +35 -0
  258. package/src/pty.test.ts +81 -0
  259. package/src/pty.ts +245 -0
  260. package/src/ready-signal.test.ts +85 -0
  261. package/src/ready-signal.ts +52 -0
  262. package/src/run.ts +85 -12
  263. package/src/serve.test.ts +24 -12
  264. package/src/serve.ts +52 -10
  265. package/src/share.ts +237 -0
  266. package/src/skills.test.ts +74 -0
  267. package/src/skills.ts +75 -0
  268. package/src/slash-palette.test.ts +103 -0
  269. package/src/slash-palette.ts +129 -0
  270. package/src/split-diff.test.ts +110 -0
  271. package/src/split-diff.ts +149 -0
  272. package/src/test-support.test.ts +264 -0
  273. package/src/test-support.ts +369 -0
  274. package/src/theme-presets.test.ts +51 -0
  275. package/src/theme-presets.ts +219 -0
  276. package/src/theme.ts +88 -19
  277. package/src/thread-chords.test.ts +79 -0
  278. package/src/thread-chords.ts +163 -0
  279. package/src/thread-code-syntax.ts +45 -0
  280. package/src/thread-controller.curate.test.ts +476 -0
  281. package/src/thread-controller.derivation.test.ts +62 -0
  282. package/src/thread-controller.live-diff.test.ts +322 -0
  283. package/src/thread-controller.optimistic.test.ts +69 -0
  284. package/src/thread-controller.share.test.ts +561 -0
  285. package/src/thread-controller.switch-cache.test.ts +176 -0
  286. package/src/thread-controller.tree.test.ts +261 -0
  287. package/src/thread-controller.ts +2050 -0
  288. package/src/thread-selection.test.ts +210 -0
  289. package/src/thread-selection.ts +243 -0
  290. package/src/tree-view.test.ts +128 -0
  291. package/src/tree-view.ts +153 -0
  292. package/src/use-annotation-surface.tsx +975 -0
  293. package/src/use-changes-workbench.ts +190 -0
  294. package/src/use-frame-measure.ts +41 -0
  295. package/src/use-remember-layout.ts +31 -0
  296. package/src/use-repo-changes.ts +87 -0
  297. package/src/use-settings-dialog.tsx +364 -0
  298. package/src/use-terminal-virtualizer.ts +259 -0
  299. package/src/use-thread-body-editing.tsx +58 -0
  300. package/src/version.ts +5 -0
  301. package/src/view-diff.test.ts +279 -9
  302. package/src/view-diff.ts +460 -24
  303. package/src/view-plan.regression.test.ts +60 -0
  304. package/src/view-plan.test.ts +545 -0
  305. package/src/view-plan.ts +1019 -0
  306. package/src/visual/contrast.test.ts +35 -0
  307. package/src/visual/contrast.ts +33 -0
  308. package/src/walk.test.ts +121 -0
  309. package/src/walk.ts +74 -0
  310. package/src/workbench-snapshot.test.ts +99 -0
  311. package/src/workbench-snapshot.ts +59 -0
  312. package/src/session-controller.ts +0 -398
  313. package/src/syntax.test.ts +0 -41
  314. package/src/syntax.ts +0 -88
  315. package/src/view.test.ts +0 -157
  316. package/src/view.ts +0 -364
@@ -1,116 +1,661 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { DEFAULT_KEYS, actionFor, loadConfig } from "./config";
5
+ import {
6
+ DEFAULT_KEYS,
7
+ DEFAULT_QUICK_ACTIONS,
8
+ actionFor,
9
+ loadConfig,
10
+ persistAuthorName,
11
+ persistActions,
12
+ persistLayout,
13
+ persistPins,
14
+ persistReviewSkill,
15
+ persistReviewWorkspace,
16
+ persistTheme,
17
+ quickActionBody,
18
+ resolveQuickAction,
19
+ } from "./config";
6
20
  import { DARK } from "./theme";
21
+ import { themeForName } from "./theme-presets";
7
22
 
8
23
  describe("loadConfig", () => {
24
+ test("review defaults use the bundled skill in an isolated worktree", () => {
25
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
26
+
27
+ expect(config.review).toEqual({ skill: "code-review", workspace: "worktree" });
28
+ });
29
+
30
+ test("review settings load and persist without changing other tables", () => {
31
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-review-config-"));
32
+ const path = join(dir, "config.toml");
33
+
34
+ writeFileSync(path, '[ui]\ntheme = "cueloop"\n\n[review]\nskill = "my-review"\n');
35
+ try {
36
+ persistReviewSkill("code-review", path);
37
+ persistReviewWorkspace("current", path);
38
+
39
+ expect(loadConfig({ userConfigPath: path }).review).toEqual({
40
+ skill: "code-review",
41
+ workspace: "current",
42
+ });
43
+ expect(readFileSync(path, "utf8")).toContain('[ui]\ntheme = "cueloop"');
44
+ } finally {
45
+ rmSync(dir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("review settings update a table whose header has a comment", () => {
50
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-review-commented-config-"));
51
+ const path = join(dir, "config.toml");
52
+
53
+ writeFileSync(path, '[review] # preferred settings\nskill = "my-review"\n');
54
+ try {
55
+ persistReviewSkill("code-review", path);
56
+
57
+ const text = readFileSync(path, "utf8");
58
+
59
+ expect(loadConfig({ userConfigPath: path }).review.skill).toBe("code-review");
60
+ expect(text.match(/^\[review\]/gm)).toHaveLength(1);
61
+ expect(text).toContain("[review] # preferred settings");
62
+ } finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test("review settings update a CRLF table without duplicating it", () => {
68
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-review-crlf-config-"));
69
+ const path = join(dir, "config.toml");
70
+
71
+ writeFileSync(path, '[review]\r\nskill = "code-review"\r\nworkspace = "worktree"\r\n');
72
+ try {
73
+ persistReviewWorkspace("current", path);
74
+
75
+ const text = readFileSync(path, "utf8");
76
+
77
+ expect(loadConfig({ userConfigPath: path }).review.workspace).toBe("current");
78
+ expect(text.match(/^\[review\]/gm)).toHaveLength(1);
79
+ } finally {
80
+ rmSync(dir, { recursive: true, force: true });
81
+ }
82
+ });
9
83
  test("defaults when no file exists", () => {
10
- const c = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
11
- expect(c.keys["comment"]).toEqual(["c"]);
12
- expect(c.theme.accent).toBe(DARK.accent);
84
+ // Act
85
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
86
+
87
+ // Assert
88
+ expect(config.keys["comment"]).toEqual(["c"]);
89
+ expect(config.theme.accent).toBe(DARK.accent);
13
90
  });
14
91
 
15
92
  test("user config rebinds actions and overrides theme tokens", () => {
93
+ // Arrange
16
94
  const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-"));
17
95
  const path = join(dir, "config.toml");
96
+
18
97
  writeFileSync(
19
98
  path,
20
99
  `[keys]\ncomment = "a"\nsubmit = ["return", "S"]\n\n[theme]\naccent = "#ff0000"\n`,
21
100
  );
101
+
22
102
  try {
23
- const c = loadConfig({ userConfigPath: path });
24
- expect(c.keys["comment"]).toEqual(["a"]);
25
- expect(c.keys["submit"]).toEqual(["return", "S"]);
26
- expect(c.keys["cut"]).toEqual(["x"]); // untouched defaults survive
27
- expect(c.theme.accent).toBe("#ff0000");
28
- expect(c.theme.green).toBe(DARK.green);
103
+ // Act
104
+ const config = loadConfig({ userConfigPath: path });
105
+
106
+ // Assert
107
+ expect(config.keys["comment"]).toEqual(["a"]);
108
+ expect(config.keys["submit"]).toEqual(["return", "S"]);
109
+ expect(config.keys["cut"]).toEqual(["x"]); // untouched defaults survive
110
+ expect(config.theme.accent).toBe("#ff0000");
111
+ expect(config.theme.green).toBe(DARK.green);
29
112
  } finally {
30
113
  rmSync(dir, { recursive: true, force: true });
31
114
  }
32
115
  });
33
116
 
34
117
  test("repo config layers over user config", () => {
118
+ // Arrange
35
119
  const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg2-"));
36
120
  const user = join(dir, "user.toml");
37
121
  const repoRoot = join(dir, "repo");
122
+
38
123
  writeFileSync(user, `[keys]\ncomment = "a"\n`);
39
124
  Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
40
125
  writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[keys]\ncomment = "z"\n`);
126
+
41
127
  try {
42
- const c = loadConfig({ userConfigPath: user, repoRoot });
43
- expect(c.keys["comment"]).toEqual(["z"]);
128
+ // Act
129
+ const config = loadConfig({ userConfigPath: user, repoRoot });
130
+
131
+ // Assert
132
+ expect(config.keys["comment"]).toEqual(["z"]);
44
133
  } finally {
45
134
  rmSync(dir, { recursive: true, force: true });
46
135
  }
47
136
  });
48
137
 
49
138
  test("broken config never blocks a review", () => {
139
+ // Arrange
50
140
  const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg3-"));
51
141
  const path = join(dir, "config.toml");
142
+
52
143
  writeFileSync(path, "not [valid toml");
144
+
145
+ try {
146
+ // Act
147
+ const config = loadConfig({ userConfigPath: path });
148
+
149
+ // Assert
150
+ expect(config.keys["comment"]).toEqual(["c"]);
151
+ } finally {
152
+ rmSync(dir, { recursive: true, force: true });
153
+ }
154
+ });
155
+
156
+ test("[ui] parses auto_close and the editor override", () => {
157
+ // Arrange
158
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg4-"));
159
+ const path = join(dir, "config.toml");
160
+
161
+ writeFileSync(path, `[ui]\nauto_close = 3\neditor = "code --wait"\n`);
162
+
163
+ try {
164
+ // Act
165
+ const config = loadConfig({ userConfigPath: path });
166
+
167
+ // Assert
168
+ expect(config.ui.autoClose).toBe(3);
169
+ expect(config.ui.editor).toBe("code --wait");
170
+ } finally {
171
+ rmSync(dir, { recursive: true, force: true });
172
+ }
173
+ });
174
+
175
+ test("[ui] editor defaults to undefined and ignores a blank value", () => {
176
+ // Arrange
177
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg5-"));
178
+ const path = join(dir, "config.toml");
179
+
180
+ writeFileSync(path, `[ui]\neditor = " "\n`);
181
+
182
+ try {
183
+ // Assert
184
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.editor).toBeUndefined();
185
+ expect(loadConfig({ userConfigPath: path }).ui.editor).toBeUndefined();
186
+ } finally {
187
+ rmSync(dir, { recursive: true, force: true });
188
+ }
189
+ });
190
+
191
+ test("[ui] diff_view defaults to split and parses stacked", () => {
192
+ // Arrange
193
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-diff-"));
194
+ const path = join(dir, "config.toml");
195
+
196
+ writeFileSync(path, `[ui]\ndiff_view = "stacked"\n`);
197
+
198
+ try {
199
+ // Assert
200
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.diffView).toBe("split");
201
+ expect(loadConfig({ userConfigPath: path }).ui.diffView).toBe("stacked");
202
+ } finally {
203
+ rmSync(dir, { recursive: true, force: true });
204
+ }
205
+ });
206
+
207
+ test("[ui] default_message defaults to approve and parses changes_requested", () => {
208
+ // Arrange
209
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-message-"));
210
+ const path = join(dir, "config.toml");
211
+
212
+ writeFileSync(path, `[ui]\ndefault_message = "changes_requested"\n`);
213
+
214
+ try {
215
+ // Assert
216
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.defaultMessage).toBe(
217
+ "approved",
218
+ );
219
+ expect(loadConfig({ userConfigPath: path }).ui.defaultMessage).toBe("changes_requested");
220
+ } finally {
221
+ rmSync(dir, { recursive: true, force: true });
222
+ }
223
+ });
224
+
225
+ test("[skills] path overrides the default and a malformed value never discards the config", () => {
226
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-skills-"));
227
+ const path = join(dir, "config.toml");
228
+
229
+ try {
230
+ writeFileSync(path, `[skills]\npath = "/custom/skills"\n`);
231
+ expect(loadConfig({ userConfigPath: path }).skillsPath).toBe("/custom/skills");
232
+
233
+ // a wrong-typed path falls back to the default without dropping the sibling [keys] setting
234
+ writeFileSync(path, `[skills]\npath = 123\n\n[keys]\ncomment = "z"\n`);
235
+ const config = loadConfig({ userConfigPath: path });
236
+
237
+ expect(config.keys["comment"]).toEqual(["z"]);
238
+ expect(config.skillsPath).toContain(".agents");
239
+ } finally {
240
+ rmSync(dir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test('[ui] diff_view loads the pre-rename "unified" value as "stacked"', () => {
245
+ // Arrange - an upgrade must not flip a user who had persisted the old spelling
246
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-diff-"));
247
+ const path = join(dir, "config.toml");
248
+
249
+ writeFileSync(path, `[ui]\ndiff_view = "unified"\n`);
250
+
251
+ try {
252
+ // Assert
253
+ expect(loadConfig({ userConfigPath: path }).ui.diffView).toBe("stacked");
254
+ } finally {
255
+ rmSync(dir, { recursive: true, force: true });
256
+ }
257
+ });
258
+
259
+ test("[ui] theme defaults to the branded cueloop preset", () => {
260
+ // Act
261
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
262
+
263
+ // Assert
264
+ expect(config.ui.theme).toBe("cueloop");
265
+ expect(config.theme.accent).toBe(DARK.accent);
266
+ });
267
+
268
+ test("[ui] theme selects a named preset as the token base", () => {
269
+ // Arrange
270
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-"));
271
+ const path = join(dir, "config.toml");
272
+
273
+ writeFileSync(path, `[ui]\ntheme = "nord"\n`);
274
+
275
+ try {
276
+ // Act
277
+ const config = loadConfig({ userConfigPath: path });
278
+
279
+ // Assert
280
+ expect(config.ui.theme).toBe("nord");
281
+ expect(config.theme).toEqual(themeForName("nord"));
282
+ } finally {
283
+ rmSync(dir, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ test("[theme] token overrides layer on top of the selected preset", () => {
288
+ // Arrange
289
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme2-"));
290
+ const path = join(dir, "config.toml");
291
+
292
+ writeFileSync(path, `[ui]\ntheme = "nord"\n\n[theme]\naccent = "#ff0000"\n`);
293
+
294
+ try {
295
+ // Act
296
+ const config = loadConfig({ userConfigPath: path });
297
+
298
+ // Assert
299
+ expect(config.theme.accent).toBe("#ff0000"); // override wins
300
+ expect(config.theme.background).toBe(themeForName("nord").background); // preset base survives
301
+ expect(config.themeOverrides).toEqual({ accent: "#ff0000" }); // deltas exposed for live re-compose
302
+ } finally {
303
+ rmSync(dir, { recursive: true, force: true });
304
+ }
305
+ });
306
+
307
+ test("a later file's preset keeps an earlier file's [theme] override", () => {
308
+ // Arrange - user overrides accent; repo picks a preset in a separate file
309
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme-x-"));
310
+ const user = join(dir, "user.toml");
311
+ const repoRoot = join(dir, "repo");
312
+
313
+ writeFileSync(user, `[theme]\naccent = "#ff0000"\n`);
314
+ Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
315
+ writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[ui]\ntheme = "nord"\n`);
316
+
317
+ try {
318
+ // Act
319
+ const config = loadConfig({ userConfigPath: user, repoRoot });
320
+
321
+ // Assert - the preset is the base, but the earlier override still wins its token
322
+ expect(config.ui.theme).toBe("nord");
323
+ expect(config.theme.background).toBe(themeForName("nord").background);
324
+ expect(config.theme.accent).toBe("#ff0000");
325
+ } finally {
326
+ rmSync(dir, { recursive: true, force: true });
327
+ }
328
+ });
329
+
330
+ test("[ui] ignores an unknown theme name and keeps the default", () => {
331
+ // Arrange
332
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme3-"));
333
+ const path = join(dir, "config.toml");
334
+
335
+ writeFileSync(path, `[ui]\ntheme = "solarized-galaxy"\n`);
336
+
337
+ try {
338
+ // Act
339
+ const config = loadConfig({ userConfigPath: path });
340
+
341
+ // Assert
342
+ expect(config.ui.theme).toBe("cueloop");
343
+ expect(config.theme.accent).toBe(DARK.accent);
344
+ } finally {
345
+ rmSync(dir, { recursive: true, force: true });
346
+ }
347
+ });
348
+
349
+ test("persistTheme round-trips through the config file", () => {
350
+ // Arrange
351
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-theme4-"));
352
+ const path = join(dir, "config.toml");
353
+
354
+ try {
355
+ // Act
356
+ persistTheme("tokyo-night", path);
357
+
358
+ // Assert
359
+ expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("tokyo-night");
360
+
361
+ // a second write replaces the key in place
362
+ // Act
363
+ persistTheme("gruvbox-dark", path);
364
+
365
+ // Assert
366
+ expect(loadConfig({ userConfigPath: path }).ui.theme).toBe("gruvbox-dark");
367
+ } finally {
368
+ rmSync(dir, { recursive: true, force: true });
369
+ }
370
+ });
371
+
372
+ test("persistPins round-trips the pinned-thread ids through the config file", () => {
373
+ // Arrange
374
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-pins-"));
375
+ const path = join(dir, "config.toml");
376
+
377
+ try {
378
+ // Act
379
+ persistPins(["s_one", "s_two"], path);
380
+
381
+ // Assert
382
+ expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual(["s_one", "s_two"]);
383
+
384
+ // unpinning rewrites the list in place, including down to empty
385
+ // Act
386
+ persistPins(["s_two"], path);
387
+
388
+ // Assert
389
+ expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual(["s_two"]);
390
+
391
+ // Act
392
+ persistPins([], path);
393
+
394
+ // Assert
395
+ expect(loadConfig({ userConfigPath: path }).ui.pins).toEqual([]);
396
+ } finally {
397
+ rmSync(dir, { recursive: true, force: true });
398
+ }
399
+ });
400
+
401
+ test("persistLayout round-trips the remembered pane composition through the config file", () => {
402
+ // Arrange
403
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-layout-"));
404
+ const path = join(dir, "config.toml");
405
+
53
406
  try {
54
- const c = loadConfig({ userConfigPath: path });
55
- expect(c.keys["comment"]).toEqual(["c"]);
407
+ // Act
408
+ persistLayout({ threads: false, rightSidebar: "project", zoomChanges: false }, path);
409
+
410
+ // Assert
411
+ expect(loadConfig({ userConfigPath: path }).ui.layout).toEqual({
412
+ threads: false,
413
+ rightSidebar: "project",
414
+ zoomChanges: false,
415
+ });
416
+
417
+ // Act
418
+ persistLayout({ threads: true, rightSidebar: "off", zoomChanges: true }, path);
419
+
420
+ // Assert
421
+ expect(loadConfig({ userConfigPath: path }).ui.layout).toEqual({
422
+ threads: true,
423
+ rightSidebar: "off",
424
+ zoomChanges: true,
425
+ });
56
426
  } finally {
57
427
  rmSync(dir, { recursive: true, force: true });
58
428
  }
59
429
  });
430
+
431
+ test("layout is unset until the user changes one", () => {
432
+ // Assert
433
+ expect(loadConfig({ userConfigPath: "/nonexistent/config.toml" }).ui.layout).toBeUndefined();
434
+ });
60
435
  });
61
436
 
62
437
  describe("integrations.obsidian config", () => {
63
438
  test("defaults when the section is absent", () => {
64
- const c = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
65
- expect(c.integrations.obsidian.vault).toBeUndefined();
66
- expect(c.integrations.obsidian.folder).toBe("cueloop");
67
- expect(c.integrations.obsidian.filenameFormat).toBe("{YYYY}-{MM}-{DD} - {title}");
68
- expect(c.integrations.obsidian.separator).toBe("space");
69
- expect(c.integrations.obsidian.exportOn).toBe("manual");
439
+ // Act
440
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
441
+
442
+ // Assert
443
+ expect(config.integrations.obsidian.vault).toBeUndefined();
444
+ expect(config.integrations.obsidian.folder).toBe("cueloop");
445
+ expect(config.integrations.obsidian.filenameFormat).toBe("{YYYY}-{MM}-{DD} - {title}");
446
+ expect(config.integrations.obsidian.separator).toBe("space");
447
+ expect(config.integrations.obsidian.exportOn).toBe("manual");
70
448
  });
71
449
 
72
450
  test("user config sets the section; invalid enum values are ignored", () => {
451
+ // Arrange
73
452
  const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs-"));
74
453
  const path = join(dir, "config.toml");
454
+
75
455
  writeFileSync(
76
456
  path,
77
- `[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approve"\nseparator = "comma"\n`,
457
+ `[integrations.obsidian]\nvault = "/notes/vault"\nfolder = "plans"\nexportOn = "approved"\nseparator = "comma"\n`,
78
458
  );
459
+
79
460
  try {
80
- const c = loadConfig({ userConfigPath: path });
81
- expect(c.integrations.obsidian.vault).toBe("/notes/vault");
82
- expect(c.integrations.obsidian.folder).toBe("plans");
83
- expect(c.integrations.obsidian.exportOn).toBe("approve");
84
- expect(c.integrations.obsidian.separator).toBe("space"); // invalid value falls back
85
- expect(c.keys["comment"]).toEqual(["c"]); // other sections untouched
461
+ // Act
462
+ const config = loadConfig({ userConfigPath: path });
463
+
464
+ // Assert
465
+ expect(config.integrations.obsidian.vault).toBe("/notes/vault");
466
+ expect(config.integrations.obsidian.folder).toBe("plans");
467
+ expect(config.integrations.obsidian.exportOn).toBe("approved");
468
+ expect(config.integrations.obsidian.separator).toBe("space"); // invalid value falls back
469
+ expect(config.keys["comment"]).toEqual(["c"]); // other sections untouched
86
470
  } finally {
87
471
  rmSync(dir, { recursive: true, force: true });
88
472
  }
89
473
  });
90
474
 
91
475
  test("repo config layers over user config for the section", () => {
476
+ // Arrange
92
477
  const dir = mkdtempSync(join(tmpdir(), "cueloop-cfg-obs2-"));
93
478
  const user = join(dir, "user.toml");
94
479
  const repoRoot = join(dir, "repo");
95
- writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "resolve"\n`);
480
+
481
+ writeFileSync(user, `[integrations.obsidian]\nvault = "/user/vault"\nexportOn = "message"\n`);
96
482
  Bun.spawnSync(["mkdir", "-p", join(repoRoot, ".cueloop")]);
97
- writeFileSync(join(repoRoot, ".cueloop", "config.toml"), `[integrations.obsidian]\nfolder = "repo-plans"\n`);
483
+ writeFileSync(
484
+ join(repoRoot, ".cueloop", "config.toml"),
485
+ `[integrations.obsidian]\nfolder = "repo-plans"\n`,
486
+ );
487
+
98
488
  try {
99
- const c = loadConfig({ userConfigPath: user, repoRoot });
100
- expect(c.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
101
- expect(c.integrations.obsidian.exportOn).toBe("resolve");
102
- expect(c.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
489
+ // Act
490
+ const config = loadConfig({ userConfigPath: user, repoRoot });
491
+
492
+ // Assert
493
+ expect(config.integrations.obsidian.vault).toBe("/user/vault"); // user layer survives
494
+ expect(config.integrations.obsidian.exportOn).toBe("message");
495
+ expect(config.integrations.obsidian.folder).toBe("repo-plans"); // repo layer wins
103
496
  } finally {
104
497
  rmSync(dir, { recursive: true, force: true });
105
498
  }
106
499
  });
107
500
  });
108
501
 
502
+ describe("quick actions ([[actions]])", () => {
503
+ test("absent config keeps the built-in defaults", () => {
504
+ // Act
505
+ const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
506
+
507
+ // Assert
508
+ expect(config.actions).toEqual(DEFAULT_QUICK_ACTIONS);
509
+ expect(config.actions).toHaveLength(8);
510
+ });
511
+
512
+ test("configured actions replace the defaults, with optional metadata", () => {
513
+ // Arrange
514
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-actions-"));
515
+ const path = join(dir, "config.toml");
516
+
517
+ writeFileSync(
518
+ path,
519
+ `[[actions]]\nprompt = "Add a benchmark"\n\n[[actions]]\nprompt = "Guard the edge case"\nmetadata = "null and empty input"\n`,
520
+ );
521
+
522
+ try {
523
+ // Act
524
+ const config = loadConfig({ userConfigPath: path });
525
+
526
+ // Assert
527
+ expect(config.actions).toEqual([
528
+ { prompt: "Add a benchmark" },
529
+ { prompt: "Guard the edge case", metadata: "null and empty input" },
530
+ ]);
531
+ } finally {
532
+ rmSync(dir, { recursive: true, force: true });
533
+ }
534
+ });
535
+
536
+ test("a table missing prompt is skipped; the rest replace the defaults", () => {
537
+ // Arrange
538
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-actions2-"));
539
+ const path = join(dir, "config.toml");
540
+
541
+ writeFileSync(
542
+ path,
543
+ `[[actions]]\nmetadata = "orphan without a prompt"\n\n[[actions]]\nprompt = "Keep this one"\n`,
544
+ );
545
+
546
+ try {
547
+ // Act
548
+ const config = loadConfig({ userConfigPath: path });
549
+
550
+ // Assert
551
+ expect(config.actions).toEqual([{ prompt: "Keep this one" }]);
552
+ } finally {
553
+ rmSync(dir, { recursive: true, force: true });
554
+ }
555
+ });
556
+ });
557
+
558
+ describe("persistActions", () => {
559
+ test("round-trips the action set through [[actions]] and replaces prior ones", () => {
560
+ // Arrange
561
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-persist-actions-"));
562
+ const path = join(dir, "config.toml");
563
+
564
+ writeFileSync(path, `[[actions]]\nprompt = "old one"\n\n[ui]\nauto_close = 3\n`);
565
+
566
+ try {
567
+ // Act
568
+ persistActions(
569
+ [{ prompt: "Zoom out" }, { prompt: "Prototype", metadata: "skip tests and polish" }],
570
+ path,
571
+ );
572
+ const config = loadConfig({ userConfigPath: path });
573
+
574
+ // Assert - the old action is gone, the new set is read back, and [ui] survives
575
+ expect(config.actions).toEqual([
576
+ { prompt: "Zoom out" },
577
+ { prompt: "Prototype", metadata: "skip tests and polish" },
578
+ ]);
579
+ expect(config.ui.autoClose).toBe(3);
580
+ } finally {
581
+ rmSync(dir, { recursive: true, force: true });
582
+ }
583
+ });
584
+ });
585
+
586
+ describe("quickActionBody", () => {
587
+ test("appends the system prompt below the prompt when set", () => {
588
+ // Assert
589
+ expect(quickActionBody({ prompt: "Prototype this", metadata: "skip tests" })).toBe(
590
+ "Prototype this\n\nskip tests",
591
+ );
592
+ expect(quickActionBody({ prompt: "Out of scope" })).toBe("Out of scope");
593
+ });
594
+ });
595
+
596
+ describe("resolveQuickAction", () => {
597
+ test("resolves by 1-based index and by case-insensitive prompt", () => {
598
+ // Assert
599
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "1")).toEqual(DEFAULT_QUICK_ACTIONS[0]!);
600
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "out of scope")).toEqual(
601
+ DEFAULT_QUICK_ACTIONS[2]!,
602
+ );
603
+ });
604
+
605
+ test("returns undefined for an out-of-range index or an unknown name", () => {
606
+ // Assert
607
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "0")).toBeUndefined();
608
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "99")).toBeUndefined();
609
+ expect(resolveQuickAction(DEFAULT_QUICK_ACTIONS, "no such action")).toBeUndefined();
610
+ });
611
+ });
612
+
109
613
  describe("actionFor", () => {
110
614
  test("resolves keys and shifted keys to actions", () => {
615
+ // Assert
111
616
  expect(actionFor(DEFAULT_KEYS, "j", false)).toBe("down");
112
617
  expect(actionFor(DEFAULT_KEYS, "g", true)).toBe("bottom"); // G
113
618
  expect(actionFor(DEFAULT_KEYS, "return", false)).toBe("submit");
114
619
  expect(actionFor(DEFAULT_KEYS, "zz", false)).toBeUndefined();
115
620
  });
116
621
  });
622
+
623
+ describe("[authors] rename map", () => {
624
+ test("loads quoted fingerprint keys", () => {
625
+ // Arrange
626
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-authors-"));
627
+ const path = join(dir, "config.toml");
628
+
629
+ writeFileSync(path, `[authors]\n"SHA256:abc+def/gh" = "Alex"\n`);
630
+
631
+ // Act
632
+ const config = loadConfig({ userConfigPath: path });
633
+
634
+ // Assert
635
+ expect(config.authors["SHA256:abc+def/gh"]).toBe("Alex");
636
+ rmSync(dir, { recursive: true, force: true });
637
+ });
638
+
639
+ test("persistAuthorName round-trips and updates a fingerprint id in place", () => {
640
+ // Arrange
641
+ const dir = mkdtempSync(join(tmpdir(), "cueloop-authors2-"));
642
+ const path = join(dir, "config.toml");
643
+
644
+ // Act / Assert
645
+ persistAuthorName("SHA256:abc+def/gh", "Alex", path);
646
+ expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alex");
647
+ persistAuthorName("SHA256:abc+def/gh", "Alexa", path);
648
+ expect(loadConfig({ userConfigPath: path }).authors["SHA256:abc+def/gh"]).toBe("Alexa");
649
+ rmSync(dir, { recursive: true, force: true });
650
+ });
651
+ });
652
+
653
+ describe("the /lgtm default quick action", () => {
654
+ test("expands to a terse approving note", () => {
655
+ const lgtm = DEFAULT_QUICK_ACTIONS.find((action) => action.prompt.toLowerCase() === "lgtm");
656
+
657
+ expect(lgtm).toBeDefined();
658
+ // the reference is /lgtm, so the label is the prompt; it expands like every action (label, then note)
659
+ expect(quickActionBody(lgtm!)).toBe("LGTM\n\nThis looks good to me.");
660
+ });
661
+ });