gemiterm 2.0.0

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 (184) hide show
  1. package/.env +2 -0
  2. package/.github/workflows/manual-release.yml +34 -0
  3. package/.github/workflows/release.yml +198 -0
  4. package/.github/workflows/test.yml +29 -0
  5. package/.opencode/commands/opsx-apply.md +152 -0
  6. package/.opencode/commands/opsx-archive.md +157 -0
  7. package/.opencode/commands/opsx-explore.md +169 -0
  8. package/.opencode/commands/opsx-propose.md +104 -0
  9. package/.opencode/commands/opsx-sync.md +140 -0
  10. package/.opencode/commands/search-intelligently.md +9 -0
  11. package/.opencode/opencode.json +20 -0
  12. package/.opencode/skills/intellisearch/.version +1 -0
  13. package/.opencode/skills/intellisearch/SKILL.md +84 -0
  14. package/.opencode/skills/intellisearch/references/brave-search.md +67 -0
  15. package/.opencode/skills/intellisearch/references/ddg-search.md +79 -0
  16. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +246 -0
  17. package/.opencode/skills/intellisearch/references/examples.md +97 -0
  18. package/.opencode/skills/intellisearch/references/gh-cli.md +143 -0
  19. package/.opencode/skills/intellisearch/references/google-search.md +40 -0
  20. package/.opencode/skills/intellisearch/references/search-workflow.md +143 -0
  21. package/.opencode/skills/intellisearch/references/workflow.md +115 -0
  22. package/.opencode/skills/openspec-apply-change/SKILL.md +159 -0
  23. package/.opencode/skills/openspec-archive-change/SKILL.md +117 -0
  24. package/.opencode/skills/openspec-explore/SKILL.md +287 -0
  25. package/.opencode/skills/openspec-propose/SKILL.md +111 -0
  26. package/.opencode/skills/openspec-sync-specs/SKILL.md +147 -0
  27. package/.serena/project.yml +133 -0
  28. package/AGENTS.md +136 -0
  29. package/CHANGELOG.md +3 -0
  30. package/README.md +245 -0
  31. package/bun.lock +105 -0
  32. package/docs/INSTALL.md +185 -0
  33. package/docs/PLAYWRIGHT_CLI_API.md +306 -0
  34. package/docs/python-release-for-reference.md +175 -0
  35. package/examples/sample_json_export.json +32 -0
  36. package/examples/sample_markdown_export.md +221 -0
  37. package/examples/usage_examples.sh +222 -0
  38. package/install.ps1 +264 -0
  39. package/install.sh +275 -0
  40. package/openspec/MAESTRO_MIGRATION.md +180 -0
  41. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +2 -0
  42. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +194 -0
  43. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +52 -0
  44. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +133 -0
  45. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +95 -0
  46. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +2 -0
  47. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +28 -0
  48. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +22 -0
  49. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +35 -0
  50. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +28 -0
  51. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +2 -0
  52. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +323 -0
  53. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +122 -0
  54. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +29 -0
  55. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +172 -0
  56. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +2 -0
  57. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +241 -0
  58. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +41 -0
  59. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +205 -0
  60. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +97 -0
  61. package/openspec/changes/commander-cli-parser/.openspec.yaml +2 -0
  62. package/openspec/changes/commander-cli-parser/design.md +107 -0
  63. package/openspec/changes/commander-cli-parser/proposal.md +31 -0
  64. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +48 -0
  65. package/openspec/changes/commander-cli-parser/tasks.md +33 -0
  66. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +2 -0
  67. package/openspec/changes/cross-platform-build-and-ci/design.md +213 -0
  68. package/openspec/changes/cross-platform-build-and-ci/proposal.md +50 -0
  69. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +335 -0
  70. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +95 -0
  71. package/openspec/changes/cross-platform-build-and-ci/tasks.md +164 -0
  72. package/openspec/config.yaml +20 -0
  73. package/openspec/specs/auth/spec.md +394 -0
  74. package/openspec/specs/cli/spec.md +108 -0
  75. package/openspec/specs/commands/spec.md +409 -0
  76. package/openspec/specs/configuration/spec.md +130 -0
  77. package/openspec/specs/conversations/spec.md +139 -0
  78. package/openspec/specs/domain-model/spec.md +128 -0
  79. package/openspec/specs/formatters/spec.md +158 -0
  80. package/openspec/specs/install-browser/spec.md +146 -0
  81. package/openspec/specs/interactive-prompt-loop/spec.md +41 -0
  82. package/openspec/specs/logger/spec.md +71 -0
  83. package/openspec/specs/mediator/spec.md +72 -0
  84. package/openspec/specs/multi-profile-conversations/spec.md +145 -0
  85. package/openspec/specs/path-utils/spec.md +101 -0
  86. package/openspec/specs/profiles/spec.md +94 -0
  87. package/openspec/specs/storage/spec.md +198 -0
  88. package/openspec/specs/testing/spec.md +237 -0
  89. package/openspec/specs/v2-installer/spec.md +205 -0
  90. package/openspec/specs/validators/spec.md +65 -0
  91. package/package.json +45 -0
  92. package/scripts/build.ts +88 -0
  93. package/scripts/clean-build.ps1 +4 -0
  94. package/scripts/clean-build.sh +5 -0
  95. package/scripts/install-browser.ps1 +5 -0
  96. package/scripts/install-browser.sh +6 -0
  97. package/scripts/lint-path-mediation.ps1 +65 -0
  98. package/scripts/lint-path-mediation.sh +40 -0
  99. package/src/cli/.gitkeep +0 -0
  100. package/src/cli/command-registry.ts +62 -0
  101. package/src/cli/commands/auth-command.ts +179 -0
  102. package/src/cli/commands/continue-command.ts +158 -0
  103. package/src/cli/commands/delete-command.ts +163 -0
  104. package/src/cli/commands/export-all-command.ts +243 -0
  105. package/src/cli/commands/export-command.ts +156 -0
  106. package/src/cli/commands/fetch-command.ts +176 -0
  107. package/src/cli/commands/help.ts +45 -0
  108. package/src/cli/commands/install-browser-command.ts +33 -0
  109. package/src/cli/commands/list-command.ts +226 -0
  110. package/src/cli/commands/new-command.ts +147 -0
  111. package/src/cli/commands/profile-command.ts +226 -0
  112. package/src/cli/commands/status-command.ts +59 -0
  113. package/src/cli/index.ts +213 -0
  114. package/src/cli/utils/interactive-prompt.ts +64 -0
  115. package/src/core/.gitkeep +0 -0
  116. package/src/core/command-handlers.ts +240 -0
  117. package/src/core/errors.ts +41 -0
  118. package/src/core/mediator.ts +58 -0
  119. package/src/core/query-handlers.ts +163 -0
  120. package/src/core/types.ts +43 -0
  121. package/src/infrastructure/.gitkeep +0 -0
  122. package/src/infrastructure/cli-table.ts +45 -0
  123. package/src/infrastructure/config.ts +52 -0
  124. package/src/infrastructure/formatters.ts +130 -0
  125. package/src/infrastructure/io.ts +161 -0
  126. package/src/infrastructure/logger.ts +41 -0
  127. package/src/infrastructure/path-utils.ts +157 -0
  128. package/src/infrastructure/storage.ts +209 -0
  129. package/src/infrastructure/validators.ts +28 -0
  130. package/src/services/.gitkeep +0 -0
  131. package/src/services/auth-service.ts +135 -0
  132. package/src/services/cookie-monitor.ts +159 -0
  133. package/src/services/cookie-storage-service.ts +74 -0
  134. package/src/services/gemini-client-wrapper.ts +239 -0
  135. package/src/services/install-browser-service.ts +205 -0
  136. package/src/services/playwright-cli-driver.ts +269 -0
  137. package/src/services/profile-auth-manager.ts +65 -0
  138. package/src/services/profile-service.ts +121 -0
  139. package/tests/cli/auth-command.test.ts +220 -0
  140. package/tests/cli/command-registry.test.ts +67 -0
  141. package/tests/cli/continue-command.test.ts +100 -0
  142. package/tests/cli/delete-command.test.ts +128 -0
  143. package/tests/cli/export-all-command.test.ts +375 -0
  144. package/tests/cli/install-browser-command.test.ts +48 -0
  145. package/tests/cli/list-command.test.ts +179 -0
  146. package/tests/cli/new-command.test.ts +139 -0
  147. package/tests/cli/status-command.test.ts +106 -0
  148. package/tests/core/query-handlers.test.ts +187 -0
  149. package/tests/fixtures/auth-fixtures.ts +98 -0
  150. package/tests/fixtures/chat-fixtures.ts +87 -0
  151. package/tests/infrastructure/cli-table.test.ts +107 -0
  152. package/tests/infrastructure/formatters.test.ts +202 -0
  153. package/tests/infrastructure/io.test.ts +197 -0
  154. package/tests/infrastructure/logger.test.ts +74 -0
  155. package/tests/infrastructure/storage.test.ts +335 -0
  156. package/tests/infrastructure/validators.test.ts +67 -0
  157. package/tests/integration/commands/auth.test.ts +186 -0
  158. package/tests/integration/commands/continue.test.ts +130 -0
  159. package/tests/integration/commands/delete.test.ts +140 -0
  160. package/tests/integration/commands/export.test.ts +266 -0
  161. package/tests/integration/commands/fetch.test.ts +238 -0
  162. package/tests/integration/commands/list.test.ts +281 -0
  163. package/tests/integration/commands/profile.test.ts +258 -0
  164. package/tests/integration/commands/status.test.ts +223 -0
  165. package/tests/integration/installer-script-shape.test.ts +67 -0
  166. package/tests/parity/compare-outputs.ts +359 -0
  167. package/tests/parity/test-commands-parity.ps1 +82 -0
  168. package/tests/parity/test-commands-parity.sh +61 -0
  169. package/tests/services/auth-service.test.ts +315 -0
  170. package/tests/services/cookie-monitor.test.ts +229 -0
  171. package/tests/services/cookie-storage-service.test.ts +230 -0
  172. package/tests/services/gemini-client-wrapper.test.ts +688 -0
  173. package/tests/services/install-browser-service.test.ts +83 -0
  174. package/tests/services/playwright-cli-driver.test.ts +468 -0
  175. package/tests/services/profile-auth-manager.test.ts +334 -0
  176. package/tests/services/profile-service.test.ts +224 -0
  177. package/tests/setup.ts +95 -0
  178. package/tests/smoke/smoke.test.ts +52 -0
  179. package/tests/unit/config.test.ts +159 -0
  180. package/tests/unit/errors.test.ts +119 -0
  181. package/tests/unit/formatters.test.ts +316 -0
  182. package/tests/unit/mediator.test.ts +152 -0
  183. package/tests/unit/path-utils.test.ts +289 -0
  184. package/tsconfig.json +16 -0
@@ -0,0 +1,238 @@
1
+ import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
2
+ import { FetchCommand } from "../../../src/cli/commands/fetch-command.ts";
3
+ import { Mediator } from "../../../src/core/mediator.ts";
4
+ import { QUERY_TYPES, type FetchChatQueryResult } from "../../../src/core/query-handlers.ts";
5
+ import type { CliCommandContext } from "../../../src/cli/command-registry.ts";
6
+ import { setupTestConfig, teardownTestConfig } from "../../setup.ts";
7
+ import { createMockMessageHistory } from "../../fixtures/chat-fixtures.ts";
8
+ import * as configModule from "../../../src/infrastructure/config.ts";
9
+ import { existsSync, unlinkSync, readFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ describe("fetch command integration", () => {
15
+ let command: FetchCommand;
16
+ let context: CliCommandContext;
17
+ let logSpy: ReturnType<typeof spyOn>;
18
+ let originalEnv: Record<string, string | undefined>;
19
+ let mediatorSendSpy: ReturnType<typeof spyOn>;
20
+
21
+ beforeEach(() => {
22
+ command = new FetchCommand();
23
+ context = { verbose: false, mediator: new Mediator() };
24
+ logSpy = spyOn(console, "log").mockImplementation(() => {});
25
+ originalEnv = {
26
+ GEMITERM_CONFIG_DIR: process.env.GEMITERM_CONFIG_DIR,
27
+ };
28
+ const configDir = setupTestConfig("fetch-integration");
29
+ spyOn(configModule, "getConfigDir").mockReturnValue(configDir);
30
+ spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
31
+
32
+ const mockMessages = createMockMessageHistory({ count: 4 });
33
+ mediatorSendSpy = spyOn(context.mediator, "send").mockResolvedValue({ messages: mockMessages });
34
+ });
35
+
36
+ afterEach(() => {
37
+ mock.restore();
38
+ logSpy.mockRestore();
39
+ teardownTestConfig(originalEnv);
40
+ });
41
+
42
+ describe("command metadata", () => {
43
+ test("has correct name and description", () => {
44
+ expect(command.name).toBe("fetch");
45
+ expect(command.description).toBe("Fetch and display a conversation");
46
+ });
47
+ });
48
+
49
+ describe("--help flag", () => {
50
+ test("--help shows usage information", async () => {
51
+ await command.execute(["--help"], context);
52
+
53
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
54
+ expect(output).toContain("Usage: gemiterm fetch");
55
+ expect(output).toContain("--format");
56
+ expect(output).toContain("--path");
57
+ expect(output).toContain("--help");
58
+ });
59
+
60
+ test("-h shows usage information", async () => {
61
+ await command.execute(["-h"], context);
62
+
63
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
64
+ expect(output).toContain("Usage: gemiterm fetch");
65
+ });
66
+
67
+ test("help does not send query to mediator", async () => {
68
+ await command.execute(["--help"], context);
69
+
70
+ expect(mediatorSendSpy).not.toHaveBeenCalled();
71
+ });
72
+ });
73
+
74
+ describe("fetch with conversation id", () => {
75
+ test("sends fetch-chat query to mediator with correct id", async () => {
76
+ await command.execute(["conv-abc123"], context);
77
+
78
+ expect(mediatorSendSpy).toHaveBeenCalledTimes(1);
79
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
80
+ expect(sentQuery.type).toBe(QUERY_TYPES.FETCH_CHAT);
81
+ expect(sentQuery.payload.conversationId).toBe("conv-abc123");
82
+ });
83
+
84
+ test("returns formatted text output by default", async () => {
85
+ await command.execute(["conv-abc123"], context);
86
+
87
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
88
+ expect(output).toContain("Conversation:");
89
+ expect(output).toContain("conv-abc123");
90
+ expect(output).toContain("User:");
91
+ expect(output).toContain("Model:");
92
+ });
93
+
94
+ test("displays message content in text output", async () => {
95
+ const mockMessages = createMockMessageHistory({
96
+ count: 2,
97
+ contents: ["Hello!", "Hi there!"],
98
+ });
99
+ mediatorSendSpy.mockResolvedValue({ messages: mockMessages });
100
+
101
+ await command.execute(["conv-xyz"], context);
102
+
103
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
104
+ expect(output).toContain("Hello!");
105
+ expect(output).toContain("Hi there!");
106
+ });
107
+
108
+ test("shows 'No messages found' when conversation has no messages", async () => {
109
+ mediatorSendSpy.mockResolvedValue({ messages: [] });
110
+
111
+ await command.execute(["conv-empty"], context);
112
+
113
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
114
+ expect(output).toContain("No messages found");
115
+ });
116
+ });
117
+
118
+ describe("--format json option", () => {
119
+ test("outputs valid JSON when --format json", async () => {
120
+ const mockMessages = createMockMessageHistory({ count: 2 });
121
+ mediatorSendSpy.mockResolvedValue({ messages: mockMessages });
122
+
123
+ await command.execute(["conv-abc123", "--format", "json"], context);
124
+
125
+ const jsonOutput = logSpy.mock.calls.map((c) => c[0]).join("\n");
126
+ const parsed = JSON.parse(jsonOutput);
127
+ expect(parsed).toHaveProperty("conversationId", "conv-abc123");
128
+ expect(parsed).toHaveProperty("messages");
129
+ expect(parsed.messages).toHaveLength(2);
130
+ });
131
+
132
+ test("-f json also outputs valid JSON", async () => {
133
+ const mockMessages = createMockMessageHistory({ count: 3 });
134
+ mediatorSendSpy.mockResolvedValue({ messages: mockMessages });
135
+
136
+ await command.execute(["conv-abc123", "-f", "json"], context);
137
+
138
+ const jsonOutput = logSpy.mock.calls.map((c) => c[0]).join("\n");
139
+ const parsed = JSON.parse(jsonOutput);
140
+ expect(parsed.messages).toHaveLength(3);
141
+ });
142
+
143
+ test("JSON output contains message roles and content", async () => {
144
+ const mockMessages = createMockMessageHistory({
145
+ count: 2,
146
+ roles: ["user", "model"],
147
+ contents: ["What is TypeScript?", "TypeScript is a typed superset of JavaScript."],
148
+ });
149
+ mediatorSendSpy.mockResolvedValue({ messages: mockMessages });
150
+
151
+ await command.execute(["conv-abc123", "--format", "json"], context);
152
+
153
+ const jsonOutput = logSpy.mock.calls.map((c) => c[0]).join("\n");
154
+ const parsed = JSON.parse(jsonOutput);
155
+ expect(parsed.messages[0].role).toBe("user");
156
+ expect(parsed.messages[0].content).toBe("What is TypeScript?");
157
+ expect(parsed.messages[1].role).toBe("model");
158
+ });
159
+ });
160
+
161
+ describe("--path option", () => {
162
+ test("--path writes text output to file", async () => {
163
+ const outputPath = join(tmpdir(), `fetch-test-${Date.now()}.txt`);
164
+
165
+ try {
166
+ await command.execute(["conv-abc123", "--path", outputPath], context);
167
+
168
+ expect(existsSync(outputPath)).toBe(true);
169
+ const content = readFileSync(outputPath, "utf-8");
170
+ expect(content).toContain("Conversation:");
171
+ expect(content).toContain("conv-abc123");
172
+ } finally {
173
+ if (existsSync(outputPath)) unlinkSync(outputPath);
174
+ }
175
+ });
176
+
177
+ test("-p writes text output to file", async () => {
178
+ const outputPath = join(tmpdir(), `fetch-test-short-${Date.now()}.txt`);
179
+
180
+ try {
181
+ await command.execute(["conv-abc123", "-p", outputPath], context);
182
+
183
+ expect(existsSync(outputPath)).toBe(true);
184
+ const content = readFileSync(outputPath, "utf-8");
185
+ expect(content).toContain("Conversation:");
186
+ } finally {
187
+ if (existsSync(outputPath)) unlinkSync(outputPath);
188
+ }
189
+ });
190
+
191
+ test("--path with json format writes JSON to file", async () => {
192
+ const outputPath = join(tmpdir(), `fetch-test-json-${Date.now()}.json`);
193
+
194
+ try {
195
+ await command.execute(["conv-abc123", "--format", "json", "--path", outputPath], context);
196
+
197
+ expect(existsSync(outputPath)).toBe(true);
198
+ const content = readFileSync(outputPath, "utf-8");
199
+ const parsed = JSON.parse(content);
200
+ expect(parsed).toHaveProperty("conversationId");
201
+ expect(parsed).toHaveProperty("messages");
202
+ } finally {
203
+ if (existsSync(outputPath)) unlinkSync(outputPath);
204
+ }
205
+ });
206
+ });
207
+
208
+ describe("no conversation id", () => {
209
+ test("does not send fetch-chat query when no id is provided", async () => {
210
+ const mockListExecute = mock(() => Promise.resolve());
211
+ const mockListHandler = { name: "list", description: "", execute: mockListExecute };
212
+ const mockRegistry = {
213
+ registerAllCommands: mock(() => {}),
214
+ getHandler: mock(() => mockListHandler),
215
+ };
216
+ spyOn(command as any, "invokeListCommand").mockImplementation(() => {});
217
+
218
+ await command.execute([], context);
219
+
220
+ expect(mediatorSendSpy).not.toHaveBeenCalled();
221
+ expect((command as any).invokeListCommand).toHaveBeenCalledTimes(1);
222
+ });
223
+ });
224
+
225
+ describe("error handling", () => {
226
+ test("propagates mediator errors", async () => {
227
+ mediatorSendSpy.mockRejectedValue(new Error("Conversation not found"));
228
+
229
+ await expect(command.execute(["conv-invalid"], context)).rejects.toThrow("Conversation not found");
230
+ });
231
+
232
+ test("propagates network errors", async () => {
233
+ mediatorSendSpy.mockRejectedValue(new Error("Network error"));
234
+
235
+ await expect(command.execute(["conv-abc123"], context)).rejects.toThrow("Network error");
236
+ });
237
+ });
238
+ });
@@ -0,0 +1,281 @@
1
+ import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
2
+ import { ListCommand } from "../../../src/cli/commands/list-command.ts";
3
+ import { Mediator } from "../../../src/core/mediator.ts";
4
+ import { QUERY_TYPES, type ListChatsQueryPayload, type ListChatsQueryResult } from "../../../src/core/query-handlers.ts";
5
+ import type { CliCommandContext } from "../../../src/cli/command-registry.ts";
6
+ import { setupTestConfig, teardownTestConfig } from "../../setup.ts";
7
+ import { createMockChatList } from "../../fixtures/chat-fixtures.ts";
8
+ import * as configModule from "../../../src/infrastructure/config.ts";
9
+
10
+ describe("list command integration", () => {
11
+ let command: ListCommand;
12
+ let context: CliCommandContext;
13
+ let logSpy: ReturnType<typeof spyOn>;
14
+ let originalEnv: Record<string, string | undefined>;
15
+ let mediatorSendSpy: ReturnType<typeof spyOn>;
16
+
17
+ beforeEach(() => {
18
+ command = new ListCommand();
19
+ context = { verbose: false, mediator: new Mediator() };
20
+ logSpy = spyOn(console, "log").mockImplementation(() => {});
21
+ originalEnv = {
22
+ GEMITERM_CONFIG_DIR: process.env.GEMITERM_CONFIG_DIR,
23
+ };
24
+ const configDir = setupTestConfig("list-integration");
25
+ spyOn(configModule, "getConfigDir").mockReturnValue(configDir);
26
+ spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
27
+
28
+ const mockChats = createMockChatList({ count: 5 });
29
+ mediatorSendSpy = spyOn(context.mediator, "send").mockResolvedValue({ chats: mockChats });
30
+ });
31
+
32
+ afterEach(() => {
33
+ mock.restore();
34
+ logSpy.mockRestore();
35
+ teardownTestConfig(originalEnv);
36
+ });
37
+
38
+ describe("command metadata", () => {
39
+ test("has correct name and description", () => {
40
+ expect(command.name).toBe("list");
41
+ expect(command.description).toBe("List conversations");
42
+ });
43
+ });
44
+
45
+ describe("--help flag", () => {
46
+ test("--help shows usage information", async () => {
47
+ await command.execute(["--help"], context);
48
+
49
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
50
+ expect(output).toContain("Usage: gemiterm list");
51
+ expect(output).toContain("--limit");
52
+ expect(output).toContain("--search");
53
+ expect(output).toContain("--format");
54
+ });
55
+
56
+ test("-h shows usage information", async () => {
57
+ await command.execute(["-h"], context);
58
+
59
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
60
+ expect(output).toContain("Usage: gemiterm list");
61
+ });
62
+
63
+ test("help does not send query to mediator", async () => {
64
+ await command.execute(["--help"], context);
65
+
66
+ expect(mediatorSendSpy).not.toHaveBeenCalled();
67
+ });
68
+ });
69
+
70
+ describe("list with mock data", () => {
71
+ test("sends list-chats query to mediator", async () => {
72
+ await command.execute([], context);
73
+
74
+ expect(mediatorSendSpy).toHaveBeenCalledTimes(1);
75
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
76
+ expect(sentQuery.type).toBe(QUERY_TYPES.LIST_CHATS);
77
+ });
78
+
79
+ test("returns formatted table output by default", async () => {
80
+ await command.execute([], context);
81
+
82
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
83
+ expect(output).toContain("ID");
84
+ expect(output).toContain("TITLE");
85
+ expect(output).toContain("DATE");
86
+ expect(output).toContain("Total:");
87
+ });
88
+
89
+ test("displays chat IDs in output", async () => {
90
+ const mockChats = createMockChatList({ count: 3 });
91
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
92
+
93
+ await command.execute([], context);
94
+
95
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
96
+ for (const chat of mockChats) {
97
+ expect(output).toContain(chat.id);
98
+ }
99
+ });
100
+
101
+ test("displays empty message when no chats", async () => {
102
+ mediatorSendSpy.mockResolvedValue({ chats: [] });
103
+
104
+ await command.execute([], context);
105
+
106
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
107
+ expect(output).toContain("No conversations found");
108
+ });
109
+ });
110
+
111
+ describe("--limit option", () => {
112
+ test("--limit 2 passes limit to mediator query", async () => {
113
+ await command.execute(["--limit", "2"], context);
114
+
115
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
116
+ expect(sentQuery.payload.limit).toBe(2);
117
+ });
118
+
119
+ test("-n 5 passes limit to mediator query", async () => {
120
+ await command.execute(["-n", "5"], context);
121
+
122
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
123
+ expect(sentQuery.payload.limit).toBe(5);
124
+ });
125
+
126
+ test("limits output to specified number of results", async () => {
127
+ const mockChats = createMockChatList({ count: 10 });
128
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
129
+
130
+ await command.execute(["--limit", "3"], context);
131
+
132
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
133
+ const lines = output.split("\n");
134
+ const dataLines = lines.filter((l) => l.includes("\u2502") && !l.includes("Total:"));
135
+ expect(dataLines.length).toBeLessThanOrEqual(4);
136
+ });
137
+ });
138
+
139
+ describe("--format json option", () => {
140
+ test("outputs valid JSON when --format json", async () => {
141
+ const mockChats = createMockChatList({ count: 3 });
142
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
143
+
144
+ await command.execute(["--format", "json"], context);
145
+
146
+ const jsonOutput = logSpy.mock.calls.map((c) => c[0]).join("\n");
147
+ const parsed = JSON.parse(jsonOutput);
148
+ expect(parsed).toHaveProperty("chats");
149
+ expect(parsed.chats).toHaveLength(3);
150
+ });
151
+
152
+ test("-f json also outputs valid JSON", async () => {
153
+ const mockChats = createMockChatList({ count: 2 });
154
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
155
+
156
+ await command.execute(["-f", "json"], context);
157
+
158
+ const jsonOutput = logSpy.mock.calls.map((c) => c[0]).join("\n");
159
+ const parsed = JSON.parse(jsonOutput);
160
+ expect(parsed.chats).toHaveLength(2);
161
+ });
162
+
163
+ test("JSON output contains chat ids and titles", async () => {
164
+ const mockChats = createMockChatList({ count: 2, ids: ["id-one", "id-two"], titles: ["First", "Second"] });
165
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
166
+
167
+ await command.execute(["--format", "json"], context);
168
+
169
+ const jsonOutput = logSpy.mock.calls.map((c) => c[0]).join("\n");
170
+ const parsed = JSON.parse(jsonOutput);
171
+ expect(parsed.chats[0].id).toBe("id-one");
172
+ expect(parsed.chats[0].title).toBe("First");
173
+ });
174
+ });
175
+
176
+ describe("--search option", () => {
177
+ test("--search passes search term to mediator query", async () => {
178
+ await command.execute(["--search", "TypeScript"], context);
179
+
180
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
181
+ expect(sentQuery.payload.search).toBe("TypeScript");
182
+ });
183
+
184
+ test("-s passes search term to mediator query", async () => {
185
+ await command.execute(["-s", "Bun"], context);
186
+
187
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
188
+ expect(sentQuery.payload.search).toBe("Bun");
189
+ });
190
+ });
191
+
192
+ describe("--all flag", () => {
193
+ test("--all sends query without limit", async () => {
194
+ await command.execute(["--all"], context);
195
+
196
+ const sentQuery = mediatorSendSpy.mock.calls[0][0] as any;
197
+ expect(sentQuery.payload.limit).toBeUndefined();
198
+ });
199
+ });
200
+
201
+ describe("sort options", () => {
202
+ test("default sort is recent", async () => {
203
+ await command.execute([], context);
204
+
205
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
206
+ expect(output).toContain("Total:");
207
+ });
208
+
209
+ test("--sort oldest is accepted", async () => {
210
+ await command.execute(["--sort", "oldest"], context);
211
+
212
+ expect(mediatorSendSpy).toHaveBeenCalledTimes(1);
213
+ });
214
+
215
+ test("--sort alpha is accepted", async () => {
216
+ await command.execute(["--sort", "alpha"], context);
217
+
218
+ expect(mediatorSendSpy).toHaveBeenCalledTimes(1);
219
+ });
220
+ });
221
+
222
+ describe("--all-profiles flag", () => {
223
+ test("renders Profile column when --all-profiles is set", async () => {
224
+ const mockChats = [
225
+ { id: "conv-1", title: "Chat 1", isPinned: false, timestamp: Date.now(), profile: "work" },
226
+ { id: "conv-2", title: "Chat 2", isPinned: false, timestamp: Date.now(), profile: "personal" },
227
+ ];
228
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
229
+
230
+ await command.execute(["--all-profiles"], context);
231
+
232
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
233
+ expect(output).toContain("PROFILE");
234
+ expect(output).toContain("work");
235
+ expect(output).toContain("personal");
236
+ });
237
+
238
+ test("omits Profile column when --all-profiles is not set", async () => {
239
+ const mockChats = [
240
+ { id: "conv-1", title: "Chat 1", isPinned: false, timestamp: Date.now(), profile: "work" },
241
+ ];
242
+ mediatorSendSpy.mockResolvedValue({ chats: mockChats });
243
+
244
+ await command.execute([], context);
245
+
246
+ const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
247
+ expect(output).not.toContain("PROFILE");
248
+ });
249
+
250
+ test("JSON output includes profile field only when --all-profiles is set", async () => {
251
+ const mockChatsWithProfile = [
252
+ { id: "conv-1", title: "Chat 1", isPinned: false, timestamp: Date.now(), profile: "work" },
253
+ ];
254
+ mediatorSendSpy.mockResolvedValue({ chats: mockChatsWithProfile });
255
+
256
+ await command.execute(["--all-profiles", "--format", "json"], context);
257
+ const outputWithFlag = logSpy.mock.calls.map((c) => c[0]).join("\n");
258
+ const parsedWithFlag = JSON.parse(outputWithFlag);
259
+ expect(parsedWithFlag.chats[0]).toHaveProperty("profile");
260
+ expect(parsedWithFlag.chats[0].profile).toBe("work");
261
+
262
+ const mockChatsWithoutProfile = [
263
+ { id: "conv-1", title: "Chat 1", isPinned: false, timestamp: Date.now() },
264
+ ];
265
+ logSpy.mockClear();
266
+ mediatorSendSpy.mockResolvedValue({ chats: mockChatsWithoutProfile });
267
+ await command.execute(["--format", "json"], context);
268
+ const outputWithoutFlag = logSpy.mock.calls.map((c) => c[0]).join("\n");
269
+ const parsedWithoutFlag = JSON.parse(outputWithoutFlag);
270
+ expect(parsedWithoutFlag.chats[0]).not.toHaveProperty("profile");
271
+ });
272
+ });
273
+
274
+ describe("error handling", () => {
275
+ test("propagates mediator errors", async () => {
276
+ mediatorSendSpy.mockRejectedValue(new Error("Network error"));
277
+
278
+ await expect(command.execute([], context)).rejects.toThrow("Network error");
279
+ });
280
+ });
281
+ });