gemiterm 2.0.0 → 2.1.1

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 (155) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/continue-command.ts +75 -1
  5. package/src/cli/commands/install-skills-command.ts +35 -0
  6. package/src/cli/commands/new-command.ts +62 -0
  7. package/src/cli/index.ts +19 -44
  8. package/src/cli/utils/long-arg-guard.ts +28 -0
  9. package/src/cli/utils/prompt-file.ts +26 -0
  10. package/src/infrastructure/cli-parser.ts +77 -0
  11. package/src/infrastructure/io.ts +9 -0
  12. package/src/services/skills-cli-driver.ts +58 -0
  13. package/src/services/skills-service.ts +52 -0
  14. package/.env +0 -2
  15. package/.github/workflows/manual-release.yml +0 -34
  16. package/.github/workflows/release.yml +0 -198
  17. package/.github/workflows/test.yml +0 -29
  18. package/.opencode/commands/opsx-apply.md +0 -152
  19. package/.opencode/commands/opsx-archive.md +0 -157
  20. package/.opencode/commands/opsx-explore.md +0 -169
  21. package/.opencode/commands/opsx-propose.md +0 -104
  22. package/.opencode/commands/opsx-sync.md +0 -140
  23. package/.opencode/commands/search-intelligently.md +0 -9
  24. package/.opencode/opencode.json +0 -20
  25. package/.opencode/skills/intellisearch/.version +0 -1
  26. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  27. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  28. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  29. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  30. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  31. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  32. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  33. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  34. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  35. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  36. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  37. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  38. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  39. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  40. package/.serena/project.yml +0 -133
  41. package/AGENTS.md +0 -136
  42. package/CHANGELOG.md +0 -3
  43. package/bun.lock +0 -105
  44. package/docs/INSTALL.md +0 -185
  45. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  46. package/docs/python-release-for-reference.md +0 -175
  47. package/examples/sample_json_export.json +0 -32
  48. package/examples/sample_markdown_export.md +0 -221
  49. package/examples/usage_examples.sh +0 -222
  50. package/install.ps1 +0 -264
  51. package/install.sh +0 -275
  52. package/openspec/MAESTRO_MIGRATION.md +0 -180
  53. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  55. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  56. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  57. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  58. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  60. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  61. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  62. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  63. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  65. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  66. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  67. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  68. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  69. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  70. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  71. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  72. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  73. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  74. package/openspec/changes/commander-cli-parser/design.md +0 -107
  75. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  76. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  77. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  78. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  79. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  80. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  81. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  82. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  83. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  84. package/openspec/config.yaml +0 -20
  85. package/openspec/specs/auth/spec.md +0 -394
  86. package/openspec/specs/cli/spec.md +0 -108
  87. package/openspec/specs/commands/spec.md +0 -409
  88. package/openspec/specs/configuration/spec.md +0 -130
  89. package/openspec/specs/conversations/spec.md +0 -139
  90. package/openspec/specs/domain-model/spec.md +0 -128
  91. package/openspec/specs/formatters/spec.md +0 -158
  92. package/openspec/specs/install-browser/spec.md +0 -146
  93. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  94. package/openspec/specs/logger/spec.md +0 -71
  95. package/openspec/specs/mediator/spec.md +0 -72
  96. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  97. package/openspec/specs/path-utils/spec.md +0 -101
  98. package/openspec/specs/profiles/spec.md +0 -94
  99. package/openspec/specs/storage/spec.md +0 -198
  100. package/openspec/specs/testing/spec.md +0 -237
  101. package/openspec/specs/v2-installer/spec.md +0 -205
  102. package/openspec/specs/validators/spec.md +0 -65
  103. package/scripts/build.ts +0 -88
  104. package/scripts/clean-build.ps1 +0 -4
  105. package/scripts/clean-build.sh +0 -5
  106. package/scripts/install-browser.ps1 +0 -5
  107. package/scripts/install-browser.sh +0 -6
  108. package/scripts/lint-path-mediation.ps1 +0 -65
  109. package/scripts/lint-path-mediation.sh +0 -40
  110. package/tests/cli/auth-command.test.ts +0 -220
  111. package/tests/cli/command-registry.test.ts +0 -67
  112. package/tests/cli/continue-command.test.ts +0 -100
  113. package/tests/cli/delete-command.test.ts +0 -128
  114. package/tests/cli/export-all-command.test.ts +0 -375
  115. package/tests/cli/install-browser-command.test.ts +0 -48
  116. package/tests/cli/list-command.test.ts +0 -179
  117. package/tests/cli/new-command.test.ts +0 -139
  118. package/tests/cli/status-command.test.ts +0 -106
  119. package/tests/core/query-handlers.test.ts +0 -187
  120. package/tests/fixtures/auth-fixtures.ts +0 -98
  121. package/tests/fixtures/chat-fixtures.ts +0 -87
  122. package/tests/infrastructure/cli-table.test.ts +0 -107
  123. package/tests/infrastructure/formatters.test.ts +0 -202
  124. package/tests/infrastructure/io.test.ts +0 -197
  125. package/tests/infrastructure/logger.test.ts +0 -74
  126. package/tests/infrastructure/storage.test.ts +0 -335
  127. package/tests/infrastructure/validators.test.ts +0 -67
  128. package/tests/integration/commands/auth.test.ts +0 -186
  129. package/tests/integration/commands/continue.test.ts +0 -130
  130. package/tests/integration/commands/delete.test.ts +0 -140
  131. package/tests/integration/commands/export.test.ts +0 -266
  132. package/tests/integration/commands/fetch.test.ts +0 -238
  133. package/tests/integration/commands/list.test.ts +0 -281
  134. package/tests/integration/commands/profile.test.ts +0 -258
  135. package/tests/integration/commands/status.test.ts +0 -223
  136. package/tests/integration/installer-script-shape.test.ts +0 -67
  137. package/tests/parity/compare-outputs.ts +0 -359
  138. package/tests/parity/test-commands-parity.ps1 +0 -82
  139. package/tests/parity/test-commands-parity.sh +0 -61
  140. package/tests/services/auth-service.test.ts +0 -315
  141. package/tests/services/cookie-monitor.test.ts +0 -229
  142. package/tests/services/cookie-storage-service.test.ts +0 -230
  143. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  144. package/tests/services/install-browser-service.test.ts +0 -83
  145. package/tests/services/playwright-cli-driver.test.ts +0 -468
  146. package/tests/services/profile-auth-manager.test.ts +0 -334
  147. package/tests/services/profile-service.test.ts +0 -224
  148. package/tests/setup.ts +0 -95
  149. package/tests/smoke/smoke.test.ts +0 -52
  150. package/tests/unit/config.test.ts +0 -159
  151. package/tests/unit/errors.test.ts +0 -119
  152. package/tests/unit/formatters.test.ts +0 -316
  153. package/tests/unit/mediator.test.ts +0 -152
  154. package/tests/unit/path-utils.test.ts +0 -289
  155. package/tsconfig.json +0 -16
@@ -1,688 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
2
- import { Logger } from "../../src/infrastructure/logger.ts";
3
- import { CookieStorageService } from "../../src/services/cookie-storage-service.ts";
4
- import type { CookieStorage } from "../../src/infrastructure/storage.ts";
5
- import type { Cookie } from "../../src/core/types.ts";
6
-
7
- interface RawChatInfo {
8
- cid: string;
9
- title: string;
10
- is_pinned: boolean;
11
- timestamp: number;
12
- }
13
-
14
- interface RawChatTurn {
15
- role: "user" | "model";
16
- text: string;
17
- }
18
-
19
- interface RawChatHistory {
20
- cid: string;
21
- turns: RawChatTurn[];
22
- }
23
-
24
- interface RawModelOutput {
25
- text: { toString(): string };
26
- rcid: string;
27
- }
28
-
29
- interface RawAvailableModel {
30
- model_id: string;
31
- model_name: string;
32
- display_name: string;
33
- }
34
-
35
- interface RawChatSession {
36
- sendMessage(opts: { prompt: string }): Promise<RawModelOutput>;
37
- readHistory(limit?: number): Promise<RawChatHistory | null>;
38
- cid: string;
39
- rcid: string;
40
- }
41
-
42
- interface RawGeminiClient {
43
- init(opts?: {
44
- timeout?: number;
45
- autoClose?: boolean;
46
- closeDelay?: number;
47
- autoRefresh?: boolean;
48
- refreshInterval?: number;
49
- verbose?: boolean;
50
- watchdogTimeout?: number;
51
- }): Promise<void>;
52
- listChats(): RawChatInfo[] | null;
53
- readChat(cid: string, limit?: number): Promise<RawChatHistory | null>;
54
- startChat(opts?: { metadata?: (string | null)[] | null; cid?: string; rid?: string; rcid?: string }): RawChatSession;
55
- deleteChat(cid: string): Promise<void>;
56
- listModels(): RawAvailableModel[] | null;
57
- }
58
-
59
- class MockAuthError extends Error {
60
- name = "AuthError" as const;
61
- }
62
-
63
- class MockTimeoutError extends Error {
64
- name = "TimeoutError" as const;
65
- }
66
-
67
- class MockUsageLimitExceeded extends Error {
68
- name = "UsageLimitExceeded" as const;
69
- }
70
-
71
- class MockTemporarilyBlocked extends Error {
72
- name = "TemporarilyBlocked" as const;
73
- }
74
-
75
- class MockModelInvalid extends Error {
76
- name = "ModelInvalid" as const;
77
- }
78
-
79
- class MockAPIError extends Error {
80
- name = "APIError" as const;
81
- }
82
-
83
- class MockGeminiError extends Error {
84
- name = "GeminiError" as const;
85
- }
86
-
87
- let mockClientInstances: RawGeminiClient[];
88
- let mockClientConstructorCallCount: number;
89
- let mockOverrides: {
90
- listChats?: RawChatInfo[] | null;
91
- readChat?: (cid: string) => RawChatHistory | null;
92
- startChat?: (opts?: { cid?: string }) => RawChatSession;
93
- deleteChat?: (cid: string) => Promise<void>;
94
- listModels?: RawAvailableModel[] | null;
95
- initImplementation?: (client: RawGeminiClient) => void;
96
- listChatsImplementation?: () => never;
97
- } | undefined;
98
-
99
- function createMockChatInfo(overrides?: Partial<RawChatInfo>): RawChatInfo {
100
- return { cid: "cid1", title: "Test Chat", is_pinned: false, timestamp: 1700000000000, ...overrides };
101
- }
102
-
103
- function createMockChatHistory(turns: RawChatTurn[]): RawChatHistory {
104
- return { cid: "conv1", turns };
105
- }
106
-
107
- function createMockChatTurn(role: "user" | "model", text: string): RawChatTurn {
108
- return { role, text };
109
- }
110
-
111
- function createMockModelOutput(text: string, rcid = "rcid1"): RawModelOutput {
112
- return { text: { toString: () => text }, rcid };
113
- }
114
-
115
- function createMockAvailableModel(overrides?: Partial<RawAvailableModel>): RawAvailableModel {
116
- return {
117
- model_id: "gemini-2.5",
118
- model_name: "Gemini 2.5",
119
- display_name: "Gemini 2.5 Flash",
120
- ...overrides,
121
- };
122
- }
123
-
124
- function createMockCookieStorage(profileCookies: Record<string, { secure_1psid: string; secure_1psidts: string | null }> = {}): CookieStorage {
125
- const storage: CookieStorage = {
126
- save: () => {},
127
- load: (profileName: string): Cookie[] => {
128
- const cookies = profileCookies[profileName] ?? { secure_1psid: "", secure_1psidts: null };
129
- const farFuture = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
130
- const result: Cookie[] = [];
131
- if (cookies.secure_1psid) {
132
- result.push({ name: "__Secure-1PSID", value: cookies.secure_1psid, domain: ".google.com", path: "/", expires: farFuture, httpOnly: true, secure: true, sameSite: "Lax" as const });
133
- }
134
- if (cookies.secure_1psidts) {
135
- result.push({ name: "__Secure-1PSIDTS", value: cookies.secure_1psidts, domain: ".google.com", path: "/", expires: farFuture, httpOnly: true, secure: true, sameSite: "Lax" as const });
136
- }
137
- return result;
138
- },
139
- delete: () => {},
140
- list: () => Object.keys(profileCookies),
141
- };
142
- return storage;
143
- }
144
-
145
- function installGeminiReverseMock(overrides?: typeof mockOverrides) {
146
- mockClientInstances = [];
147
- mockClientConstructorCallCount = 0;
148
- mockOverrides = overrides;
149
-
150
- const mockGeminiClientFactory = function(config?: { secure_1psid?: string; secure_1psidts?: string | null }) {
151
- mockClientConstructorCallCount++;
152
- const instance: RawGeminiClient & { secure_1psid?: string; secure_1psidts?: string | null } = {
153
- init: mock(async function(this: RawGeminiClient) {
154
- if (mockOverrides?.initImplementation) {
155
- mockOverrides.initImplementation(instance);
156
- }
157
- }),
158
- listChats: mock(function(this: RawGeminiClient) {
159
- if (mockOverrides?.listChatsImplementation) {
160
- throw mockOverrides.listChatsImplementation();
161
- }
162
- return mockOverrides?.listChats ?? null;
163
- }),
164
- readChat: mock(async function(this: RawGeminiClient, cid: string) {
165
- return mockOverrides?.readChat?.(cid) ?? null;
166
- }),
167
- startChat: mock(function(this: RawGeminiClient, opts?: { cid?: string }) {
168
- const cid = opts?.cid ?? "new-cid";
169
- const session: RawChatSession = {
170
- sendMessage: mock(async function(this: RawChatSession, _opts: { prompt: string }) {
171
- return createMockModelOutput("response text", cid);
172
- }),
173
- readHistory: mock(async function(this: RawChatSession, _limit?: number) {
174
- return null;
175
- }),
176
- cid,
177
- rcid: "rcid-" + cid,
178
- };
179
- if (mockOverrides?.startChat) {
180
- return mockOverrides.startChat(opts) ?? session;
181
- }
182
- return session;
183
- }),
184
- deleteChat: mock(async function(this: RawGeminiClient, cid: string) {
185
- if (mockOverrides?.deleteChat) {
186
- await mockOverrides.deleteChat(cid);
187
- }
188
- }),
189
- listModels: mock(() => mockOverrides?.listModels ?? null),
190
- };
191
- instance.secure_1psid = config?.secure_1psid;
192
- instance.secure_1psidts = config?.secure_1psidts;
193
- mockClientInstances.push(instance);
194
- return instance;
195
- };
196
-
197
- const mockModule = {
198
- get GeminiClient() { return mockGeminiClientFactory; },
199
- AuthError: MockAuthError,
200
- TimeoutError: MockTimeoutError,
201
- UsageLimitExceeded: MockUsageLimitExceeded,
202
- TemporarilyBlocked: MockTemporarilyBlocked,
203
- ModelInvalid: MockModelInvalid,
204
- APIError: MockAPIError,
205
- GeminiError: MockGeminiError,
206
- ChatInfo: class {},
207
- ChatHistory: class {},
208
- ChatTurn: class {},
209
- ModelOutput: class {},
210
- AvailableModel: class {},
211
- ChatSession: class {},
212
- };
213
-
214
- mock.module("gemini-reverse", () => mockModule);
215
- }
216
-
217
- describe("GeminiClientService", () => {
218
- let logger: Logger;
219
- let cookieStorageService: CookieStorageService;
220
-
221
- beforeEach(async () => {
222
- logger = new Logger("test");
223
- const storage = createMockCookieStorage();
224
- cookieStorageService = new CookieStorageService({ cookieStorage: storage, logger });
225
- });
226
-
227
- afterEach(() => {
228
- mock.restore();
229
- });
230
-
231
- describe("listChats", () => {
232
- test("maps cid to id and is_pinned to isPinned", async () => {
233
- installGeminiReverseMock({
234
- listChats: [
235
- createMockChatInfo({ cid: "chat-1", title: "First Chat", is_pinned: true, timestamp: 1700000000000 }),
236
- createMockChatInfo({ cid: "chat-2", title: "Second Chat", is_pinned: false, timestamp: 1699000000000 }),
237
- ],
238
- });
239
-
240
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
241
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
242
-
243
- const chats = await service.listChats();
244
-
245
- expect(chats[0].id).toBe("chat-1");
246
- expect(chats[0].title).toBe("First Chat");
247
- expect(chats[0].isPinned).toBe(true);
248
- expect(chats[1].id).toBe("chat-2");
249
- expect(chats[1].isPinned).toBe(false);
250
- });
251
-
252
- test("applies search filter", async () => {
253
- installGeminiReverseMock({
254
- listChats: [
255
- createMockChatInfo({ cid: "1", title: "Python Help" }),
256
- createMockChatInfo({ cid: "2", title: "JavaScript Help" }),
257
- createMockChatInfo({ cid: "3", title: "Python Tips" }),
258
- ],
259
- });
260
-
261
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
262
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
263
-
264
- const chats = await service.listChats({ search: "python" });
265
-
266
- expect(chats).toHaveLength(2);
267
- expect(chats[0].title).toBe("Python Help");
268
- expect(chats[1].title).toBe("Python Tips");
269
- });
270
-
271
- test("applies limit and offset", async () => {
272
- installGeminiReverseMock({
273
- listChats: [
274
- createMockChatInfo({ cid: "1", title: "Chat 1", timestamp: 1700000000 }),
275
- createMockChatInfo({ cid: "2", title: "Chat 2", timestamp: 1699000000 }),
276
- createMockChatInfo({ cid: "3", title: "Chat 3", timestamp: 1698000000 }),
277
- ],
278
- });
279
-
280
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
281
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
282
-
283
- const chats = await service.listChats({ limit: 1, offset: 1 });
284
-
285
- expect(chats).toHaveLength(1);
286
- expect(chats[0].id).toBe("2");
287
- });
288
-
289
- test("sorts by timestamp descending", async () => {
290
- installGeminiReverseMock({
291
- listChats: [
292
- createMockChatInfo({ cid: "1", title: "Older", timestamp: 1000000000 }),
293
- createMockChatInfo({ cid: "2", title: "Newer", timestamp: 2000000000 }),
294
- createMockChatInfo({ cid: "3", title: "Middle", timestamp: 1500000000 }),
295
- ],
296
- });
297
-
298
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
299
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
300
-
301
- const chats = await service.listChats();
302
-
303
- expect(chats[0].title).toBe("Newer");
304
- expect(chats[1].title).toBe("Middle");
305
- expect(chats[2].title).toBe("Older");
306
- });
307
-
308
- test("converts timestamp from seconds to milliseconds", async () => {
309
- installGeminiReverseMock({
310
- listChats: [
311
- createMockChatInfo({ cid: "1", title: "Chat", timestamp: 1700000000 }),
312
- ],
313
- });
314
-
315
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
316
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
317
-
318
- const chats = await service.listChats();
319
-
320
- expect(chats[0].timestamp).toBe(1700000000000);
321
- });
322
-
323
- test("timestamp produces valid date (not epoch 1970)", async () => {
324
- installGeminiReverseMock({
325
- listChats: [
326
- createMockChatInfo({ cid: "1", title: "Chat", timestamp: 1700000000 }),
327
- ],
328
- });
329
-
330
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
331
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
332
-
333
- const chats = await service.listChats();
334
-
335
- const chatDate = new Date(chats[0].timestamp);
336
- expect(chatDate.getFullYear()).toBeGreaterThan(2000);
337
- });
338
-
339
- test("attaches profile when forProfile was used", async () => {
340
- const profileCookies: Record<string, { secure_1psid: string; secure_1psidts: string | null }> = {
341
- work: { secure_1psid: "work-sid", secure_1psidts: null },
342
- };
343
- const storage = createMockCookieStorage(profileCookies);
344
- const css = new CookieStorageService({ cookieStorage: storage, logger });
345
-
346
- installGeminiReverseMock({
347
- listChats: [createMockChatInfo({ cid: "1", title: "Profile Chat" })],
348
- });
349
-
350
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
351
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger, css);
352
- const profileService = service.forProfile("work");
353
-
354
- const chats = await profileService.listChats();
355
-
356
- expect(chats[0].profile).toBe("work");
357
- });
358
-
359
- test("returns empty array when listChats returns null", async () => {
360
- installGeminiReverseMock({ listChats: null });
361
-
362
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
363
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
364
-
365
- const chats = await service.listChats();
366
-
367
- expect(chats).toEqual([]);
368
- });
369
- });
370
-
371
- describe("fetchChat", () => {
372
- test("flattens turns text correctly", async () => {
373
- installGeminiReverseMock({
374
- readChat: (_cid: string) =>
375
- createMockChatHistory([
376
- createMockChatTurn("user", "Hello"),
377
- createMockChatTurn("model", "Hi there!"),
378
- ]),
379
- });
380
-
381
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
382
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
383
-
384
- const messages = await service.fetchChat("conv-123");
385
-
386
- expect(messages).toHaveLength(2);
387
- expect(messages[0].role).toBe("user");
388
- expect(messages[0].content).toBe("Hello");
389
- expect(messages[1].role).toBe("model");
390
- expect(messages[1].content).toBe("Hi there!");
391
- });
392
-
393
- test("returns empty array when readChat returns null", async () => {
394
- installGeminiReverseMock({ readChat: () => null });
395
-
396
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
397
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
398
-
399
- const messages = await service.fetchChat("nonexistent");
400
-
401
- expect(messages).toEqual([]);
402
- });
403
- });
404
-
405
- describe("sendMessage", () => {
406
- test("returns output.text", async () => {
407
- installGeminiReverseMock({
408
- startChat: (opts) => {
409
- const cid = opts?.cid ?? "conv-1";
410
- return {
411
- sendMessage: mock(async () => createMockModelOutput("model response", cid)),
412
- readHistory: mock(async () => null),
413
- cid,
414
- rcid: "rcid-1",
415
- };
416
- },
417
- });
418
-
419
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
420
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
421
-
422
- const result = await service.sendMessage("conv-1", "hello");
423
-
424
- expect(result).toBe("model response");
425
- });
426
- });
427
-
428
- describe("startNewChat", () => {
429
- test("returns output.text and ChatSession.cid", async () => {
430
- installGeminiReverseMock({});
431
-
432
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
433
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
434
-
435
- const result = await service.startNewChat("Hello Gemini");
436
-
437
- expect(result.response).toBe("response text");
438
- expect(result.conversationId).toBe("new-cid");
439
- });
440
- });
441
-
442
- describe("deleteChat", () => {
443
- test("calls client.deleteChat with cid", async () => {
444
- let deletedCid: string | null = null;
445
- installGeminiReverseMock({
446
- deleteChat: async (cid: string) => {
447
- deletedCid = cid;
448
- },
449
- });
450
-
451
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
452
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
453
-
454
- await service.deleteChat("conv-to-delete");
455
-
456
- expect(deletedCid).toBe("conv-to-delete");
457
- });
458
- });
459
-
460
- describe("listModels", () => {
461
- test("returns display_name when present", async () => {
462
- installGeminiReverseMock({
463
- listModels: [
464
- createMockAvailableModel({ model_id: "gemini-2.5", model_name: "Gemini 2.5", display_name: "Gemini 2.5 Flash" }),
465
- ],
466
- });
467
-
468
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
469
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
470
-
471
- const models = await service.listModels();
472
-
473
- expect(models).toEqual(["Gemini 2.5 Flash"]);
474
- });
475
-
476
- test("falls back to model_name when display_name is missing", async () => {
477
- installGeminiReverseMock({
478
- listModels: [
479
- createMockAvailableModel({ model_id: "gemini-2.5", model_name: "Gemini 2.5", display_name: "" }),
480
- ],
481
- });
482
-
483
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
484
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
485
-
486
- const models = await service.listModels();
487
-
488
- expect(models).toEqual(["Gemini 2.5"]);
489
- });
490
-
491
- test("falls back to model_id when display_name and model_name are missing", async () => {
492
- installGeminiReverseMock({
493
- listModels: [
494
- createMockAvailableModel({ model_id: "gemini-ultra", model_name: "", display_name: "" }),
495
- ],
496
- });
497
-
498
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
499
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
500
-
501
- const models = await service.listModels();
502
-
503
- expect(models).toEqual(["gemini-ultra"]);
504
- });
505
-
506
- test("returns empty array when listModels returns null", async () => {
507
- installGeminiReverseMock({ listModels: null });
508
-
509
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
510
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
511
-
512
- const models = await service.listModels();
513
-
514
- expect(models).toEqual([]);
515
- });
516
- });
517
-
518
- describe("error translations", () => {
519
- test("AuthError -> AuthenticationError", async () => {
520
- installGeminiReverseMock({
521
- listChatsImplementation: () => {
522
- throw new MockAuthError("auth failure");
523
- },
524
- });
525
-
526
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
527
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
528
-
529
- await expect(service.listChats()).rejects.toThrow("Session expired or invalid");
530
- });
531
-
532
- test("TimeoutError -> GeminiAPIError", async () => {
533
- installGeminiReverseMock({
534
- listChatsImplementation: () => {
535
- throw new MockTimeoutError("timeout");
536
- },
537
- });
538
-
539
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
540
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
541
-
542
- await expect(service.listChats()).rejects.toThrow("Request to Gemini timed out");
543
- });
544
-
545
- test("UsageLimitExceeded -> GeminiAPIError", async () => {
546
- installGeminiReverseMock({
547
- listChatsImplementation: () => {
548
- throw new MockUsageLimitExceeded("usage exceeded");
549
- },
550
- });
551
-
552
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
553
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
554
-
555
- await expect(service.listChats()).rejects.toThrow("Gemini usage limit reached");
556
- });
557
-
558
- test("TemporarilyBlocked -> GeminiAPIError", async () => {
559
- installGeminiReverseMock({
560
- listChatsImplementation: () => {
561
- throw new MockTemporarilyBlocked("blocked");
562
- },
563
- });
564
-
565
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
566
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
567
-
568
- await expect(service.listChats()).rejects.toThrow("Temporarily blocked by Gemini");
569
- });
570
-
571
- test("ModelInvalid -> GeminiAPIError", async () => {
572
- installGeminiReverseMock({
573
- listChatsImplementation: () => {
574
- throw new MockModelInvalid("model invalid");
575
- },
576
- });
577
-
578
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
579
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
580
-
581
- await expect(service.listChats()).rejects.toThrow("Model is invalid or unavailable");
582
- });
583
-
584
- test("APIError -> GeminiAPIError", async () => {
585
- installGeminiReverseMock({
586
- listChatsImplementation: () => {
587
- throw new MockAPIError("api error occurred");
588
- },
589
- });
590
-
591
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
592
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
593
-
594
- await expect(service.listChats()).rejects.toThrow("api error occurred");
595
- });
596
-
597
- test("GeminiError -> GeminiAPIError", async () => {
598
- installGeminiReverseMock({
599
- listChatsImplementation: () => {
600
- throw new MockGeminiError("generic gemini error");
601
- },
602
- });
603
-
604
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
605
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
606
-
607
- await expect(service.listChats()).rejects.toThrow("generic gemini error");
608
- });
609
- });
610
-
611
- describe("forProfile", () => {
612
- test("creates a brand-new GeminiClient instance", async () => {
613
- const profileCookies: Record<string, { secure_1psid: string; secure_1psidts: string | null }> = {
614
- work: { secure_1psid: "work-sid", secure_1psidts: "work-ts" },
615
- };
616
- const storage = createMockCookieStorage(profileCookies);
617
- const css = new CookieStorageService({ cookieStorage: storage, logger });
618
-
619
- installGeminiReverseMock({ listChats: [createMockChatInfo({ cid: "1", title: "Work Chat" })] });
620
-
621
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
622
- const service = new GeminiClientService({ secure1psid: "main-sid" }, logger, css);
623
- const callCountBefore = mockClientConstructorCallCount;
624
-
625
- service.forProfile("work");
626
- const newCallCount = mockClientConstructorCallCount;
627
-
628
- expect(newCallCount - callCountBefore).toBe(1);
629
- });
630
-
631
- test("new instance listChats is scoped to profile cookies", async () => {
632
- let receivedSid = "";
633
- const profileCookies: Record<string, { secure_1psid: string; secure_1psidts: string | null }> = {
634
- work: { secure_1psid: "work-sid", secure_1psidts: null },
635
- };
636
- const storage = createMockCookieStorage(profileCookies);
637
- const css = new CookieStorageService({ cookieStorage: storage, logger });
638
-
639
- installGeminiReverseMock({
640
- initImplementation: (client) => {
641
- receivedSid = (client as unknown as { secure_1psid?: string }).secure_1psid ?? "";
642
- },
643
- listChats: [createMockChatInfo({ cid: "work-1", title: "Work Chat" })],
644
- });
645
-
646
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
647
- const service = new GeminiClientService({ secure1psid: "main-sid" }, logger, css);
648
-
649
- const profileService = service.forProfile("work");
650
- await profileService.listChats();
651
-
652
- expect(receivedSid).toBe("work-sid");
653
- });
654
- });
655
-
656
- describe("init() idempotency", () => {
657
- test("calls client.init exactly once even when called multiple times", async () => {
658
- let initCallCount = 0;
659
- installGeminiReverseMock({
660
- initImplementation: () => {
661
- initCallCount++;
662
- },
663
- listChats: [createMockChatInfo()],
664
- });
665
-
666
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
667
- const service = new GeminiClientService({ secure1psid: "testsid" }, logger);
668
-
669
- await service.listChats();
670
- await service.listChats();
671
- await service.listChats();
672
-
673
- expect(initCallCount).toBe(1);
674
- });
675
- });
676
-
677
- describe("empty-cookies factory case", () => {
678
- test("init resolves without throwing when secure1psid is empty string", async () => {
679
- installGeminiReverseMock({});
680
-
681
- const { GeminiClientService } = await import("../../src/services/gemini-client-wrapper.ts");
682
- const service = new GeminiClientService({ secure1psid: "" }, logger);
683
-
684
- await service.listChats();
685
- expect(service.isAuthenticated()).toBe(true);
686
- });
687
- });
688
- });