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.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/index.ts +19 -44
- package/src/infrastructure/cli-parser.ts +77 -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,334 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { ProfileAuthManager } from "../../src/services/profile-auth-manager.ts";
|
|
6
|
-
import { ProfileManager, CookieStorage } from "../../src/infrastructure/storage.ts";
|
|
7
|
-
import { CookieStorageService } from "../../src/services/cookie-storage-service.ts";
|
|
8
|
-
import { Logger } from "../../src/infrastructure/logger.ts";
|
|
9
|
-
import type { Cookie } from "../../src/core/types.ts";
|
|
10
|
-
import type { ProfileManager as ProfileManagerType } from "../../src/infrastructure/storage.ts";
|
|
11
|
-
import type { IGeminiClientService } from "../../src/core/command-handlers.ts";
|
|
12
|
-
|
|
13
|
-
/*
|
|
14
|
-
The 8 tests in `describe('findProfileForConversation')` previously asserted the BUGGY
|
|
15
|
-
'first active profile' behavior; they have been updated to assert the CORRECT
|
|
16
|
-
per-profile-lookup behavior. See `openspec/changes/command-spec-conformance/proposal.md`
|
|
17
|
-
for context.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const TEST_DIR = join(tmpdir(), "gemiterm-test-profile-auth-manager");
|
|
21
|
-
|
|
22
|
-
const logger = new Logger("test");
|
|
23
|
-
|
|
24
|
-
function makeValidCookies(): Cookie[] {
|
|
25
|
-
const farFuture = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
|
|
26
|
-
return [
|
|
27
|
-
{
|
|
28
|
-
name: "__Secure-1PSID",
|
|
29
|
-
value: "test-psid-value",
|
|
30
|
-
domain: ".google.com",
|
|
31
|
-
path: "/",
|
|
32
|
-
expires: farFuture,
|
|
33
|
-
httpOnly: true,
|
|
34
|
-
secure: true,
|
|
35
|
-
sameSite: "Lax",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: "__Secure-1PSIDTS",
|
|
39
|
-
value: "test-psidts-value",
|
|
40
|
-
domain: ".google.com",
|
|
41
|
-
path: "/",
|
|
42
|
-
expires: farFuture,
|
|
43
|
-
httpOnly: true,
|
|
44
|
-
secure: true,
|
|
45
|
-
sameSite: "Lax",
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function makeExpiredCookies(): Cookie[] {
|
|
51
|
-
const past = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60;
|
|
52
|
-
return [
|
|
53
|
-
{
|
|
54
|
-
name: "__Secure-1PSID",
|
|
55
|
-
value: "expired-psid-value",
|
|
56
|
-
domain: ".google.com",
|
|
57
|
-
path: "/",
|
|
58
|
-
expires: past,
|
|
59
|
-
httpOnly: true,
|
|
60
|
-
secure: true,
|
|
61
|
-
sameSite: "Lax",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: "__Secure-1PSIDTS",
|
|
65
|
-
value: "expired-psidts-value",
|
|
66
|
-
domain: ".google.com",
|
|
67
|
-
path: "/",
|
|
68
|
-
expires: past,
|
|
69
|
-
httpOnly: true,
|
|
70
|
-
secure: true,
|
|
71
|
-
sameSite: "Lax",
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function createManager(
|
|
77
|
-
profileManager: ProfileManagerType,
|
|
78
|
-
geminiClient?: IGeminiClientService,
|
|
79
|
-
): ProfileAuthManager {
|
|
80
|
-
const cookieStorage = new CookieStorageService({
|
|
81
|
-
cookieStorage: new CookieStorage(),
|
|
82
|
-
logger,
|
|
83
|
-
});
|
|
84
|
-
return new ProfileAuthManager({
|
|
85
|
-
profileManager,
|
|
86
|
-
cookieStorageService: cookieStorage,
|
|
87
|
-
logger,
|
|
88
|
-
geminiClient: geminiClient ?? {
|
|
89
|
-
async deleteChat() { return; },
|
|
90
|
-
async sendMessage() { return ""; },
|
|
91
|
-
async startNewChat() { return { response: "", conversationId: "" }; },
|
|
92
|
-
async profileHasConversation() { return false; },
|
|
93
|
-
forProfile() { return this as unknown as IGeminiClientService; },
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
beforeEach(() => {
|
|
99
|
-
process.env.GEMITERM_CONFIG_DIR = TEST_DIR;
|
|
100
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
afterEach(() => {
|
|
104
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
105
|
-
delete process.env.GEMITERM_CONFIG_DIR;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("ProfileAuthManager", () => {
|
|
109
|
-
describe("ensureAuthenticated", () => {
|
|
110
|
-
test("returns cookies for a profile with valid session", () => {
|
|
111
|
-
const storage = new CookieStorage();
|
|
112
|
-
const manager = new ProfileManager(storage);
|
|
113
|
-
manager.create("default");
|
|
114
|
-
storage.save("default", makeValidCookies());
|
|
115
|
-
|
|
116
|
-
const mgr = createManager(manager);
|
|
117
|
-
const cookies = mgr.ensureAuthenticated("default");
|
|
118
|
-
|
|
119
|
-
expect(cookies.secure_1psid).toBe("test-psid-value");
|
|
120
|
-
expect(cookies.secure_1psidts).toBe("test-psidts-value");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("throws AuthenticationError when no valid cookies", () => {
|
|
124
|
-
const storage = new CookieStorage();
|
|
125
|
-
const manager = new ProfileManager(storage);
|
|
126
|
-
manager.create("default");
|
|
127
|
-
|
|
128
|
-
const mgr = createManager(manager);
|
|
129
|
-
|
|
130
|
-
expect(() => mgr.ensureAuthenticated("default")).toThrow("No valid session");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("throws AuthenticationError with expired cookies", () => {
|
|
134
|
-
const storage = new CookieStorage();
|
|
135
|
-
const manager = new ProfileManager(storage);
|
|
136
|
-
manager.create("default");
|
|
137
|
-
storage.save("default", makeExpiredCookies());
|
|
138
|
-
|
|
139
|
-
const mgr = createManager(manager);
|
|
140
|
-
|
|
141
|
-
expect(() => mgr.ensureAuthenticated("default")).toThrow("No valid session");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("throws on invalid profile name", () => {
|
|
145
|
-
const storage = new CookieStorage();
|
|
146
|
-
const manager = new ProfileManager(storage);
|
|
147
|
-
const mgr = createManager(manager);
|
|
148
|
-
|
|
149
|
-
expect(() => mgr.ensureAuthenticated("bad name!")).toThrow("invalid characters");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("uses default profile when none specified", () => {
|
|
153
|
-
const storage = new CookieStorage();
|
|
154
|
-
const manager = new ProfileManager(storage);
|
|
155
|
-
manager.create("default");
|
|
156
|
-
storage.save("default", makeValidCookies());
|
|
157
|
-
|
|
158
|
-
const markerDir = join(TEST_DIR, "config");
|
|
159
|
-
mkdirSync(markerDir, { recursive: true });
|
|
160
|
-
writeFileSync(join(markerDir, "default-profile"), "default", "utf-8");
|
|
161
|
-
|
|
162
|
-
const mgr = createManager(manager);
|
|
163
|
-
const cookies = mgr.ensureAuthenticated();
|
|
164
|
-
|
|
165
|
-
expect(cookies.secure_1psid).toBe("test-psid-value");
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe("getActiveProfiles", () => {
|
|
170
|
-
test("returns profiles with valid cookies", () => {
|
|
171
|
-
const storage = new CookieStorage();
|
|
172
|
-
const manager = new ProfileManager(storage);
|
|
173
|
-
manager.create("active");
|
|
174
|
-
manager.create("expired");
|
|
175
|
-
storage.save("active", makeValidCookies());
|
|
176
|
-
storage.save("expired", makeExpiredCookies());
|
|
177
|
-
|
|
178
|
-
const mgr = createManager(manager);
|
|
179
|
-
const active = mgr.getActiveProfiles();
|
|
180
|
-
|
|
181
|
-
expect(active).toContain("active");
|
|
182
|
-
expect(active).not.toContain("expired");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("returns empty array when no profiles have valid cookies", () => {
|
|
186
|
-
const storage = new CookieStorage();
|
|
187
|
-
const manager = new ProfileManager(storage);
|
|
188
|
-
manager.create("p1");
|
|
189
|
-
storage.save("p1", makeExpiredCookies());
|
|
190
|
-
|
|
191
|
-
const mgr = createManager(manager);
|
|
192
|
-
const active = mgr.getActiveProfiles();
|
|
193
|
-
|
|
194
|
-
expect(active).toEqual([]);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("returns empty array when no profiles exist", () => {
|
|
198
|
-
const storage = new CookieStorage();
|
|
199
|
-
const manager = new ProfileManager(storage);
|
|
200
|
-
const mgr = createManager(manager);
|
|
201
|
-
const active = mgr.getActiveProfiles();
|
|
202
|
-
|
|
203
|
-
expect(active).toEqual([]);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe("findProfileForConversation", () => {
|
|
208
|
-
test("returns the profile that owns the conversation", async () => {
|
|
209
|
-
const storage = new CookieStorage();
|
|
210
|
-
const manager = new ProfileManager(storage);
|
|
211
|
-
manager.create("work");
|
|
212
|
-
manager.create("personal");
|
|
213
|
-
storage.save("work", makeValidCookies());
|
|
214
|
-
storage.save("personal", makeValidCookies());
|
|
215
|
-
|
|
216
|
-
const mockGeminiClient = {
|
|
217
|
-
async deleteChat() { return; },
|
|
218
|
-
async sendMessage() { return ""; },
|
|
219
|
-
async startNewChat() { return { response: "", conversationId: "" }; },
|
|
220
|
-
async profileHasConversation(profileName: string) {
|
|
221
|
-
return profileName === "work";
|
|
222
|
-
},
|
|
223
|
-
forProfile() { return this as unknown as IGeminiClientService; },
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const mgr = createManager(manager, mockGeminiClient as unknown as IGeminiClientService);
|
|
227
|
-
const result = await mgr.findProfileForConversation("conv-123");
|
|
228
|
-
|
|
229
|
-
expect(result).toBe("work");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("returns null when no profile owns the conversation", async () => {
|
|
233
|
-
const storage = new CookieStorage();
|
|
234
|
-
const manager = new ProfileManager(storage);
|
|
235
|
-
manager.create("p1");
|
|
236
|
-
storage.save("p1", makeExpiredCookies());
|
|
237
|
-
|
|
238
|
-
const mockGeminiClient = {
|
|
239
|
-
async deleteChat() { return; },
|
|
240
|
-
async sendMessage() { return ""; },
|
|
241
|
-
async startNewChat() { return { response: "", conversationId: "" }; },
|
|
242
|
-
async profileHasConversation() { return false; },
|
|
243
|
-
forProfile() { return this as unknown as IGeminiClientService; },
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const mgr = createManager(manager, mockGeminiClient as unknown as IGeminiClientService);
|
|
247
|
-
const result = await mgr.findProfileForConversation("conv-456");
|
|
248
|
-
|
|
249
|
-
expect(result).toBeNull();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("returns null when no profiles exist", async () => {
|
|
253
|
-
const storage = new CookieStorage();
|
|
254
|
-
const manager = new ProfileManager(storage);
|
|
255
|
-
const mgr = createManager(manager);
|
|
256
|
-
|
|
257
|
-
const result = await mgr.findProfileForConversation("conv-789");
|
|
258
|
-
|
|
259
|
-
expect(result).toBeNull();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
test("returns null when conversation is not in any profile", async () => {
|
|
263
|
-
const storage = new CookieStorage();
|
|
264
|
-
const manager = new ProfileManager(storage);
|
|
265
|
-
manager.create("work");
|
|
266
|
-
manager.create("personal");
|
|
267
|
-
storage.save("work", makeValidCookies());
|
|
268
|
-
storage.save("personal", makeValidCookies());
|
|
269
|
-
|
|
270
|
-
const mockGeminiClient = {
|
|
271
|
-
async deleteChat() { return; },
|
|
272
|
-
async sendMessage() { return ""; },
|
|
273
|
-
async startNewChat() { return { response: "", conversationId: "" }; },
|
|
274
|
-
async profileHasConversation() { return false; },
|
|
275
|
-
forProfile() { return this as unknown as IGeminiClientService; },
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const mgr = createManager(manager, mockGeminiClient as unknown as IGeminiClientService);
|
|
279
|
-
const result = await mgr.findProfileForConversation("conv-999");
|
|
280
|
-
|
|
281
|
-
expect(result).toBeNull();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("returns first profile in list order when multiple profiles report ownership", async () => {
|
|
285
|
-
const storage = new CookieStorage();
|
|
286
|
-
const manager = new ProfileManager(storage);
|
|
287
|
-
manager.create("profile1");
|
|
288
|
-
manager.create("profile2");
|
|
289
|
-
manager.create("profile3");
|
|
290
|
-
storage.save("profile1", makeValidCookies());
|
|
291
|
-
storage.save("profile2", makeValidCookies());
|
|
292
|
-
storage.save("profile3", makeValidCookies());
|
|
293
|
-
|
|
294
|
-
const mockGeminiClient = {
|
|
295
|
-
async deleteChat() { return; },
|
|
296
|
-
async sendMessage() { return ""; },
|
|
297
|
-
async startNewChat() { return { response: "", conversationId: "" }; },
|
|
298
|
-
async profileHasConversation(profileName: string) {
|
|
299
|
-
return profileName === "profile1" || profileName === "profile3";
|
|
300
|
-
},
|
|
301
|
-
forProfile() { return this as unknown as IGeminiClientService; },
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const mgr = createManager(manager, mockGeminiClient as unknown as IGeminiClientService);
|
|
305
|
-
const result = await mgr.findProfileForConversation("conv-shared");
|
|
306
|
-
|
|
307
|
-
expect(result).toBe("profile1");
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test("passes the conversationId argument to the lookup helper", async () => {
|
|
311
|
-
const storage = new CookieStorage();
|
|
312
|
-
const manager = new ProfileManager(storage);
|
|
313
|
-
manager.create("work");
|
|
314
|
-
storage.save("work", makeValidCookies());
|
|
315
|
-
|
|
316
|
-
const calls: Array<[string, string]> = [];
|
|
317
|
-
const mockGeminiClient = {
|
|
318
|
-
async deleteChat() { return; },
|
|
319
|
-
async sendMessage() { return ""; },
|
|
320
|
-
async startNewChat() { return { response: "", conversationId: "" }; },
|
|
321
|
-
async profileHasConversation(profileName: string, conversationId: string) {
|
|
322
|
-
calls.push([profileName, conversationId]);
|
|
323
|
-
return false;
|
|
324
|
-
},
|
|
325
|
-
forProfile() { return this as unknown as IGeminiClientService; },
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
const mgr = createManager(manager, mockGeminiClient as unknown as IGeminiClientService);
|
|
329
|
-
await mgr.findProfileForConversation("abc-123");
|
|
330
|
-
|
|
331
|
-
expect(calls).toContainEqual(["work", "abc-123"]);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
});
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { ProfileService } from "../../src/services/profile-service.ts";
|
|
6
|
-
import { ProfileManager, CookieStorage } from "../../src/infrastructure/storage.ts";
|
|
7
|
-
import { Logger } from "../../src/infrastructure/logger.ts";
|
|
8
|
-
import type { Cookie } from "../../src/core/types.ts";
|
|
9
|
-
|
|
10
|
-
const TEST_DIR = join(tmpdir(), "gemiterm-test-profile-service");
|
|
11
|
-
|
|
12
|
-
const logger = new Logger("test");
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
process.env.GEMITERM_CONFIG_DIR = TEST_DIR;
|
|
16
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
21
|
-
delete process.env.GEMITERM_CONFIG_DIR;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function makeValidCookies(): Cookie[] {
|
|
25
|
-
const farFuture = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
|
|
26
|
-
return [
|
|
27
|
-
{
|
|
28
|
-
name: "__Secure-1PSID",
|
|
29
|
-
value: "test-psid-value",
|
|
30
|
-
domain: ".google.com",
|
|
31
|
-
path: "/",
|
|
32
|
-
expires: farFuture,
|
|
33
|
-
httpOnly: true,
|
|
34
|
-
secure: true,
|
|
35
|
-
sameSite: "Lax",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: "__Secure-1PSIDTS",
|
|
39
|
-
value: "test-psidts-value",
|
|
40
|
-
domain: ".google.com",
|
|
41
|
-
path: "/",
|
|
42
|
-
expires: farFuture,
|
|
43
|
-
httpOnly: true,
|
|
44
|
-
secure: true,
|
|
45
|
-
sameSite: "Lax",
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function setupAuthenticatedProfile(profileName = "default"): ProfileService {
|
|
51
|
-
const storage = new CookieStorage();
|
|
52
|
-
const manager = new ProfileManager(storage);
|
|
53
|
-
storage.save(profileName, makeValidCookies());
|
|
54
|
-
|
|
55
|
-
const markerDir = join(TEST_DIR, "config");
|
|
56
|
-
mkdirSync(markerDir, { recursive: true });
|
|
57
|
-
writeFileSync(join(markerDir, "default-profile"), profileName, "utf-8");
|
|
58
|
-
|
|
59
|
-
return new ProfileService(manager, logger);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe("ProfileService", () => {
|
|
63
|
-
describe("authenticate", () => {
|
|
64
|
-
test("authenticates with valid cookies", async () => {
|
|
65
|
-
const svc = setupAuthenticatedProfile();
|
|
66
|
-
const result = await svc.authenticate();
|
|
67
|
-
|
|
68
|
-
expect(result.cookies).toHaveLength(2);
|
|
69
|
-
expect(result.cookies[0].name).toBe("__Secure-1PSID");
|
|
70
|
-
expect(result.cookies[0].value).toBe("test-psid-value");
|
|
71
|
-
expect(result.expiresAt).not.toBeNull();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("throws AuthenticationError when no valid cookies exist", async () => {
|
|
75
|
-
const storage = new CookieStorage();
|
|
76
|
-
const manager = new ProfileManager(storage);
|
|
77
|
-
const svc = new ProfileService(manager, logger);
|
|
78
|
-
|
|
79
|
-
await expect(svc.authenticate("default")).rejects.toThrow("No valid session");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("creates profile if it does not exist and authenticates", async () => {
|
|
83
|
-
const storage = new CookieStorage();
|
|
84
|
-
const manager = new ProfileManager(storage);
|
|
85
|
-
storage.save("default", makeValidCookies());
|
|
86
|
-
|
|
87
|
-
const markerDir = join(TEST_DIR, "config");
|
|
88
|
-
mkdirSync(markerDir, { recursive: true });
|
|
89
|
-
writeFileSync(join(markerDir, "default-profile"), "default", "utf-8");
|
|
90
|
-
|
|
91
|
-
const svc = new ProfileService(manager, logger);
|
|
92
|
-
const result = await svc.authenticate("default");
|
|
93
|
-
|
|
94
|
-
expect(result.cookies).toHaveLength(2);
|
|
95
|
-
expect(result.cookies[0].name).toBe("__Secure-1PSID");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("throws on invalid profile name", async () => {
|
|
99
|
-
const storage = new CookieStorage();
|
|
100
|
-
const manager = new ProfileManager(storage);
|
|
101
|
-
const svc = new ProfileService(manager, logger);
|
|
102
|
-
|
|
103
|
-
await expect(svc.authenticate("bad name!")).rejects.toThrow("invalid characters");
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("getProfileStatuses", () => {
|
|
108
|
-
test("returns statuses for all profiles", async () => {
|
|
109
|
-
const storage = new CookieStorage();
|
|
110
|
-
const manager = new ProfileManager(storage);
|
|
111
|
-
storage.save("active", makeValidCookies());
|
|
112
|
-
|
|
113
|
-
const svc = new ProfileService(manager, logger);
|
|
114
|
-
const statuses = await svc.getProfileStatuses();
|
|
115
|
-
|
|
116
|
-
expect(statuses.length).toBeGreaterThan(0);
|
|
117
|
-
const active = statuses.find((s) => s.name === "active");
|
|
118
|
-
expect(active).toBeDefined();
|
|
119
|
-
expect(active!.isActive).toBe(true);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("returns empty array when no profiles exist", async () => {
|
|
123
|
-
const storage = new CookieStorage();
|
|
124
|
-
const manager = new ProfileManager(storage);
|
|
125
|
-
const svc = new ProfileService(manager, logger);
|
|
126
|
-
|
|
127
|
-
const statuses = await svc.getProfileStatuses();
|
|
128
|
-
expect(statuses).toEqual([]);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("getAuthStatus", () => {
|
|
133
|
-
test("returns authenticated when default profile has valid cookies", async () => {
|
|
134
|
-
const svc = setupAuthenticatedProfile();
|
|
135
|
-
const status = await svc.getAuthStatus();
|
|
136
|
-
|
|
137
|
-
expect(status.authenticated).toBe(true);
|
|
138
|
-
expect(status.profileName).toBe("default");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("returns not authenticated when default has no cookies", async () => {
|
|
142
|
-
const storage = new CookieStorage();
|
|
143
|
-
const manager = new ProfileManager(storage);
|
|
144
|
-
const svc = new ProfileService(manager, logger);
|
|
145
|
-
|
|
146
|
-
const status = await svc.getAuthStatus();
|
|
147
|
-
expect(status.authenticated).toBe(false);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe("deleteProfile", () => {
|
|
152
|
-
test("deletes an existing profile", async () => {
|
|
153
|
-
const storage = new CookieStorage();
|
|
154
|
-
const manager = new ProfileManager(storage);
|
|
155
|
-
manager.create("to-delete");
|
|
156
|
-
storage.save("to-delete", makeValidCookies());
|
|
157
|
-
|
|
158
|
-
const svc = new ProfileService(manager, logger);
|
|
159
|
-
await svc.deleteProfile("to-delete");
|
|
160
|
-
|
|
161
|
-
expect(manager.list()).not.toContain("to-delete");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("throws on non-existent profile", async () => {
|
|
165
|
-
const storage = new CookieStorage();
|
|
166
|
-
const manager = new ProfileManager(storage);
|
|
167
|
-
const svc = new ProfileService(manager, logger);
|
|
168
|
-
|
|
169
|
-
await expect(svc.deleteProfile("ghost")).rejects.toThrow("does not exist");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("throws on invalid profile name", async () => {
|
|
173
|
-
const storage = new CookieStorage();
|
|
174
|
-
const manager = new ProfileManager(storage);
|
|
175
|
-
const svc = new ProfileService(manager, logger);
|
|
176
|
-
|
|
177
|
-
await expect(svc.deleteProfile("bad name!")).rejects.toThrow("invalid characters");
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe("renameProfile", () => {
|
|
182
|
-
test("renames an existing profile", async () => {
|
|
183
|
-
const storage = new CookieStorage();
|
|
184
|
-
const manager = new ProfileManager(storage);
|
|
185
|
-
manager.create("old-name");
|
|
186
|
-
|
|
187
|
-
const svc = new ProfileService(manager, logger);
|
|
188
|
-
await svc.renameProfile("old-name", "new-name");
|
|
189
|
-
|
|
190
|
-
expect(manager.list()).toContain("new-name");
|
|
191
|
-
expect(manager.list()).not.toContain("old-name");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("throws on invalid names", async () => {
|
|
195
|
-
const storage = new CookieStorage();
|
|
196
|
-
const manager = new ProfileManager(storage);
|
|
197
|
-
const svc = new ProfileService(manager, logger);
|
|
198
|
-
|
|
199
|
-
await expect(svc.renameProfile("old", "new!")).rejects.toThrow("invalid characters");
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe("setDefaultProfile", () => {
|
|
204
|
-
test("sets the default profile", async () => {
|
|
205
|
-
const storage = new CookieStorage();
|
|
206
|
-
const manager = new ProfileManager(storage);
|
|
207
|
-
manager.create("p1");
|
|
208
|
-
manager.create("p2");
|
|
209
|
-
|
|
210
|
-
const svc = new ProfileService(manager, logger);
|
|
211
|
-
await svc.setDefaultProfile("p2");
|
|
212
|
-
|
|
213
|
-
expect(manager.getDefault()).toBe("p2");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("throws on invalid profile name", async () => {
|
|
217
|
-
const storage = new CookieStorage();
|
|
218
|
-
const manager = new ProfileManager(storage);
|
|
219
|
-
const svc = new ProfileService(manager, logger);
|
|
220
|
-
|
|
221
|
-
await expect(svc.setDefaultProfile("bad!")).rejects.toThrow("invalid characters");
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
});
|
package/tests/setup.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, rmSync, writeFileSync, mkdir } from "node:fs";
|
|
2
|
-
import { resolve, join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
|
-
|
|
6
|
-
const mkdirAsync = promisify(mkdir);
|
|
7
|
-
|
|
8
|
-
let testConfigDir: string | null = null;
|
|
9
|
-
|
|
10
|
-
export function createTestConfigDir(prefix = "gemiterm-test"): string {
|
|
11
|
-
testConfigDir = resolve(
|
|
12
|
-
join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
13
|
-
);
|
|
14
|
-
mkdirSync(testConfigDir, { recursive: true });
|
|
15
|
-
return testConfigDir;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function createTestConfigDirAsync(prefix = "gemiterm-test"): Promise<string> {
|
|
19
|
-
testConfigDir = resolve(
|
|
20
|
-
join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
21
|
-
);
|
|
22
|
-
await mkdirAsync(testConfigDir, { recursive: true });
|
|
23
|
-
return testConfigDir;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function cleanupTestConfigDir(): void {
|
|
27
|
-
if (testConfigDir) {
|
|
28
|
-
try {
|
|
29
|
-
rmSync(testConfigDir, { recursive: true, force: true });
|
|
30
|
-
} catch {
|
|
31
|
-
// temp dir cleanup is best-effort
|
|
32
|
-
}
|
|
33
|
-
testConfigDir = null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getTestConfigDir(): string {
|
|
38
|
-
if (!testConfigDir) {
|
|
39
|
-
throw new Error("Test config dir not initialized. Call createTestConfigDir() first.");
|
|
40
|
-
}
|
|
41
|
-
return testConfigDir;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function setupTestConfig(prefix?: string): string {
|
|
45
|
-
const dir = createTestConfigDir(prefix);
|
|
46
|
-
process.env.GEMITERM_CONFIG_DIR = dir;
|
|
47
|
-
return dir;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function teardownTestConfig(originalEnv?: Record<string, string | undefined>): void {
|
|
51
|
-
cleanupTestConfigDir();
|
|
52
|
-
if (originalEnv) {
|
|
53
|
-
if (originalEnv.GEMITERM_CONFIG_DIR !== undefined) {
|
|
54
|
-
process.env.GEMITERM_CONFIG_DIR = originalEnv.GEMITERM_CONFIG_DIR;
|
|
55
|
-
} else {
|
|
56
|
-
delete process.env.GEMITERM_CONFIG_DIR;
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
59
|
-
delete process.env.GEMITERM_CONFIG_DIR;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createMockStorageStateFile(
|
|
64
|
-
profileName: string,
|
|
65
|
-
cookies: Array<{
|
|
66
|
-
name: string;
|
|
67
|
-
value: string;
|
|
68
|
-
domain?: string;
|
|
69
|
-
path?: string;
|
|
70
|
-
expires?: number;
|
|
71
|
-
httpOnly?: boolean;
|
|
72
|
-
secure?: boolean;
|
|
73
|
-
sameSite?: string;
|
|
74
|
-
}>,
|
|
75
|
-
configDir?: string,
|
|
76
|
-
): string {
|
|
77
|
-
const dir = configDir || getTestConfigDir();
|
|
78
|
-
const profileDir = join(dir, "profiles", profileName);
|
|
79
|
-
mkdirSync(profileDir, { recursive: true });
|
|
80
|
-
const content = JSON.stringify({
|
|
81
|
-
cookies: cookies.map((c) => ({
|
|
82
|
-
name: c.name,
|
|
83
|
-
value: c.value,
|
|
84
|
-
domain: c.domain ?? ".google.com",
|
|
85
|
-
path: c.path ?? "/",
|
|
86
|
-
expires: c.expires ?? -1,
|
|
87
|
-
httpOnly: c.httpOnly ?? true,
|
|
88
|
-
secure: c.secure ?? true,
|
|
89
|
-
sameSite: c.sameSite ?? "None",
|
|
90
|
-
})),
|
|
91
|
-
});
|
|
92
|
-
const filePath = join(profileDir, "storage_state.json");
|
|
93
|
-
writeFileSync(filePath, content, "utf-8");
|
|
94
|
-
return filePath;
|
|
95
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, afterAll } from "bun:test";
|
|
2
|
-
import { spawn, Subprocess } from "bun";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const CLI_ENTRY = resolve(import.meta.dir, "..", "..", "src", "cli", "index.ts");
|
|
6
|
-
|
|
7
|
-
interface SpawnResult {
|
|
8
|
-
exitCode: number;
|
|
9
|
-
stdout: string;
|
|
10
|
-
stderr: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function runCli(args: string[]): Promise<SpawnResult> {
|
|
14
|
-
const proc: Subprocess = spawn({
|
|
15
|
-
cmd: ["bun", CLI_ENTRY, ...args],
|
|
16
|
-
stdout: "pipe",
|
|
17
|
-
stderr: "pipe",
|
|
18
|
-
env: { ...process.env, GEMITERM_CONFIG_DIR: "" },
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
22
|
-
new Response(proc.stdout).text(),
|
|
23
|
-
new Response(proc.stderr).text(),
|
|
24
|
-
proc.exited,
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
return { exitCode: exitCode ?? -1, stdout, stderr };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("Smoke Tests", () => {
|
|
31
|
-
test("--help displays usage information and exits 0", async () => {
|
|
32
|
-
const result = await runCli(["--help"]);
|
|
33
|
-
|
|
34
|
-
expect(result.exitCode).toBe(0);
|
|
35
|
-
expect(result.stdout.length).toBeGreaterThan(0);
|
|
36
|
-
expect(result.stdout.toLowerCase()).toContain("gemiterm");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("--version prints version string and exits 0", async () => {
|
|
40
|
-
const result = await runCli(["--version"]);
|
|
41
|
-
|
|
42
|
-
expect(result.exitCode).toBe(0);
|
|
43
|
-
expect(result.stdout).toContain("gemiterm v");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("status runs without crashing", async () => {
|
|
47
|
-
const result = await runCli(["status"]);
|
|
48
|
-
|
|
49
|
-
expect(result.exitCode).toBeGreaterThanOrEqual(0);
|
|
50
|
-
expect(result.stdout.length).toBeGreaterThan(0);
|
|
51
|
-
});
|
|
52
|
-
});
|