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.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/continue-command.ts +75 -1
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/commands/new-command.ts +62 -0
- package/src/cli/index.ts +19 -44
- package/src/cli/utils/long-arg-guard.ts +28 -0
- package/src/cli/utils/prompt-file.ts +26 -0
- package/src/infrastructure/cli-parser.ts +77 -0
- package/src/infrastructure/io.ts +9 -0
- package/src/services/skills-cli-driver.ts +58 -0
- package/src/services/skills-service.ts +52 -0
- package/.env +0 -2
- package/.github/workflows/manual-release.yml +0 -34
- package/.github/workflows/release.yml +0 -198
- package/.github/workflows/test.yml +0 -29
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/commands/search-intelligently.md +0 -9
- package/.opencode/opencode.json +0 -20
- package/.opencode/skills/intellisearch/.version +0 -1
- package/.opencode/skills/intellisearch/SKILL.md +0 -84
- package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
- package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
- package/.opencode/skills/intellisearch/references/examples.md +0 -97
- package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
- package/.opencode/skills/intellisearch/references/google-search.md +0 -40
- package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
- package/.opencode/skills/intellisearch/references/workflow.md +0 -115
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.serena/project.yml +0 -133
- package/AGENTS.md +0 -136
- package/CHANGELOG.md +0 -3
- package/bun.lock +0 -105
- package/docs/INSTALL.md +0 -185
- package/docs/PLAYWRIGHT_CLI_API.md +0 -306
- package/docs/python-release-for-reference.md +0 -175
- package/examples/sample_json_export.json +0 -32
- package/examples/sample_markdown_export.md +0 -221
- package/examples/usage_examples.sh +0 -222
- package/install.ps1 +0 -264
- package/install.sh +0 -275
- package/openspec/MAESTRO_MIGRATION.md +0 -180
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
- package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
- package/openspec/changes/commander-cli-parser/design.md +0 -107
- package/openspec/changes/commander-cli-parser/proposal.md +0 -31
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
- package/openspec/changes/commander-cli-parser/tasks.md +0 -33
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
- package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
- package/openspec/config.yaml +0 -20
- package/openspec/specs/auth/spec.md +0 -394
- package/openspec/specs/cli/spec.md +0 -108
- package/openspec/specs/commands/spec.md +0 -409
- package/openspec/specs/configuration/spec.md +0 -130
- package/openspec/specs/conversations/spec.md +0 -139
- package/openspec/specs/domain-model/spec.md +0 -128
- package/openspec/specs/formatters/spec.md +0 -158
- package/openspec/specs/install-browser/spec.md +0 -146
- package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
- package/openspec/specs/logger/spec.md +0 -71
- package/openspec/specs/mediator/spec.md +0 -72
- package/openspec/specs/multi-profile-conversations/spec.md +0 -145
- package/openspec/specs/path-utils/spec.md +0 -101
- package/openspec/specs/profiles/spec.md +0 -94
- package/openspec/specs/storage/spec.md +0 -198
- package/openspec/specs/testing/spec.md +0 -237
- package/openspec/specs/v2-installer/spec.md +0 -205
- package/openspec/specs/validators/spec.md +0 -65
- package/scripts/build.ts +0 -88
- package/scripts/clean-build.ps1 +0 -4
- package/scripts/clean-build.sh +0 -5
- package/scripts/install-browser.ps1 +0 -5
- package/scripts/install-browser.sh +0 -6
- package/scripts/lint-path-mediation.ps1 +0 -65
- package/scripts/lint-path-mediation.sh +0 -40
- package/tests/cli/auth-command.test.ts +0 -220
- package/tests/cli/command-registry.test.ts +0 -67
- package/tests/cli/continue-command.test.ts +0 -100
- package/tests/cli/delete-command.test.ts +0 -128
- package/tests/cli/export-all-command.test.ts +0 -375
- package/tests/cli/install-browser-command.test.ts +0 -48
- package/tests/cli/list-command.test.ts +0 -179
- package/tests/cli/new-command.test.ts +0 -139
- package/tests/cli/status-command.test.ts +0 -106
- package/tests/core/query-handlers.test.ts +0 -187
- package/tests/fixtures/auth-fixtures.ts +0 -98
- package/tests/fixtures/chat-fixtures.ts +0 -87
- package/tests/infrastructure/cli-table.test.ts +0 -107
- package/tests/infrastructure/formatters.test.ts +0 -202
- package/tests/infrastructure/io.test.ts +0 -197
- package/tests/infrastructure/logger.test.ts +0 -74
- package/tests/infrastructure/storage.test.ts +0 -335
- package/tests/infrastructure/validators.test.ts +0 -67
- package/tests/integration/commands/auth.test.ts +0 -186
- package/tests/integration/commands/continue.test.ts +0 -130
- package/tests/integration/commands/delete.test.ts +0 -140
- package/tests/integration/commands/export.test.ts +0 -266
- package/tests/integration/commands/fetch.test.ts +0 -238
- package/tests/integration/commands/list.test.ts +0 -281
- package/tests/integration/commands/profile.test.ts +0 -258
- package/tests/integration/commands/status.test.ts +0 -223
- package/tests/integration/installer-script-shape.test.ts +0 -67
- package/tests/parity/compare-outputs.ts +0 -359
- package/tests/parity/test-commands-parity.ps1 +0 -82
- package/tests/parity/test-commands-parity.sh +0 -61
- package/tests/services/auth-service.test.ts +0 -315
- package/tests/services/cookie-monitor.test.ts +0 -229
- package/tests/services/cookie-storage-service.test.ts +0 -230
- package/tests/services/gemini-client-wrapper.test.ts +0 -688
- package/tests/services/install-browser-service.test.ts +0 -83
- package/tests/services/playwright-cli-driver.test.ts +0 -468
- package/tests/services/profile-auth-manager.test.ts +0 -334
- package/tests/services/profile-service.test.ts +0 -224
- package/tests/setup.ts +0 -95
- package/tests/smoke/smoke.test.ts +0 -52
- package/tests/unit/config.test.ts +0 -159
- package/tests/unit/errors.test.ts +0 -119
- package/tests/unit/formatters.test.ts +0 -316
- package/tests/unit/mediator.test.ts +0 -152
- package/tests/unit/path-utils.test.ts +0 -289
- 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
|
-
});
|