gemiterm 2.0.0 → 2.1.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 (150) 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/install-skills-command.ts +35 -0
  5. package/src/cli/index.ts +19 -44
  6. package/src/infrastructure/cli-parser.ts +77 -0
  7. package/src/services/skills-cli-driver.ts +58 -0
  8. package/src/services/skills-service.ts +52 -0
  9. package/.env +0 -2
  10. package/.github/workflows/manual-release.yml +0 -34
  11. package/.github/workflows/release.yml +0 -198
  12. package/.github/workflows/test.yml +0 -29
  13. package/.opencode/commands/opsx-apply.md +0 -152
  14. package/.opencode/commands/opsx-archive.md +0 -157
  15. package/.opencode/commands/opsx-explore.md +0 -169
  16. package/.opencode/commands/opsx-propose.md +0 -104
  17. package/.opencode/commands/opsx-sync.md +0 -140
  18. package/.opencode/commands/search-intelligently.md +0 -9
  19. package/.opencode/opencode.json +0 -20
  20. package/.opencode/skills/intellisearch/.version +0 -1
  21. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  22. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  23. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  24. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  25. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  26. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  27. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  28. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  29. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  30. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  31. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  32. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  33. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  34. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  35. package/.serena/project.yml +0 -133
  36. package/AGENTS.md +0 -136
  37. package/CHANGELOG.md +0 -3
  38. package/bun.lock +0 -105
  39. package/docs/INSTALL.md +0 -185
  40. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  41. package/docs/python-release-for-reference.md +0 -175
  42. package/examples/sample_json_export.json +0 -32
  43. package/examples/sample_markdown_export.md +0 -221
  44. package/examples/usage_examples.sh +0 -222
  45. package/install.ps1 +0 -264
  46. package/install.sh +0 -275
  47. package/openspec/MAESTRO_MIGRATION.md +0 -180
  48. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  49. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  50. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  51. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  52. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  53. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  55. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  56. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  57. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  58. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  60. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  61. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  62. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  63. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  65. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  66. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  67. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  68. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  69. package/openspec/changes/commander-cli-parser/design.md +0 -107
  70. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  71. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  72. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  73. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  74. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  75. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  76. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  77. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  78. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  79. package/openspec/config.yaml +0 -20
  80. package/openspec/specs/auth/spec.md +0 -394
  81. package/openspec/specs/cli/spec.md +0 -108
  82. package/openspec/specs/commands/spec.md +0 -409
  83. package/openspec/specs/configuration/spec.md +0 -130
  84. package/openspec/specs/conversations/spec.md +0 -139
  85. package/openspec/specs/domain-model/spec.md +0 -128
  86. package/openspec/specs/formatters/spec.md +0 -158
  87. package/openspec/specs/install-browser/spec.md +0 -146
  88. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  89. package/openspec/specs/logger/spec.md +0 -71
  90. package/openspec/specs/mediator/spec.md +0 -72
  91. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  92. package/openspec/specs/path-utils/spec.md +0 -101
  93. package/openspec/specs/profiles/spec.md +0 -94
  94. package/openspec/specs/storage/spec.md +0 -198
  95. package/openspec/specs/testing/spec.md +0 -237
  96. package/openspec/specs/v2-installer/spec.md +0 -205
  97. package/openspec/specs/validators/spec.md +0 -65
  98. package/scripts/build.ts +0 -88
  99. package/scripts/clean-build.ps1 +0 -4
  100. package/scripts/clean-build.sh +0 -5
  101. package/scripts/install-browser.ps1 +0 -5
  102. package/scripts/install-browser.sh +0 -6
  103. package/scripts/lint-path-mediation.ps1 +0 -65
  104. package/scripts/lint-path-mediation.sh +0 -40
  105. package/tests/cli/auth-command.test.ts +0 -220
  106. package/tests/cli/command-registry.test.ts +0 -67
  107. package/tests/cli/continue-command.test.ts +0 -100
  108. package/tests/cli/delete-command.test.ts +0 -128
  109. package/tests/cli/export-all-command.test.ts +0 -375
  110. package/tests/cli/install-browser-command.test.ts +0 -48
  111. package/tests/cli/list-command.test.ts +0 -179
  112. package/tests/cli/new-command.test.ts +0 -139
  113. package/tests/cli/status-command.test.ts +0 -106
  114. package/tests/core/query-handlers.test.ts +0 -187
  115. package/tests/fixtures/auth-fixtures.ts +0 -98
  116. package/tests/fixtures/chat-fixtures.ts +0 -87
  117. package/tests/infrastructure/cli-table.test.ts +0 -107
  118. package/tests/infrastructure/formatters.test.ts +0 -202
  119. package/tests/infrastructure/io.test.ts +0 -197
  120. package/tests/infrastructure/logger.test.ts +0 -74
  121. package/tests/infrastructure/storage.test.ts +0 -335
  122. package/tests/infrastructure/validators.test.ts +0 -67
  123. package/tests/integration/commands/auth.test.ts +0 -186
  124. package/tests/integration/commands/continue.test.ts +0 -130
  125. package/tests/integration/commands/delete.test.ts +0 -140
  126. package/tests/integration/commands/export.test.ts +0 -266
  127. package/tests/integration/commands/fetch.test.ts +0 -238
  128. package/tests/integration/commands/list.test.ts +0 -281
  129. package/tests/integration/commands/profile.test.ts +0 -258
  130. package/tests/integration/commands/status.test.ts +0 -223
  131. package/tests/integration/installer-script-shape.test.ts +0 -67
  132. package/tests/parity/compare-outputs.ts +0 -359
  133. package/tests/parity/test-commands-parity.ps1 +0 -82
  134. package/tests/parity/test-commands-parity.sh +0 -61
  135. package/tests/services/auth-service.test.ts +0 -315
  136. package/tests/services/cookie-monitor.test.ts +0 -229
  137. package/tests/services/cookie-storage-service.test.ts +0 -230
  138. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  139. package/tests/services/install-browser-service.test.ts +0 -83
  140. package/tests/services/playwright-cli-driver.test.ts +0 -468
  141. package/tests/services/profile-auth-manager.test.ts +0 -334
  142. package/tests/services/profile-service.test.ts +0 -224
  143. package/tests/setup.ts +0 -95
  144. package/tests/smoke/smoke.test.ts +0 -52
  145. package/tests/unit/config.test.ts +0 -159
  146. package/tests/unit/errors.test.ts +0 -119
  147. package/tests/unit/formatters.test.ts +0 -316
  148. package/tests/unit/mediator.test.ts +0 -152
  149. package/tests/unit/path-utils.test.ts +0 -289
  150. 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
- });