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,335 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdirSync, rmSync, existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { CookieStorage, ProfileManager } from "../../src/infrastructure/storage.ts";
|
|
6
|
-
import type { Cookie } from "../../src/core/types.ts";
|
|
7
|
-
|
|
8
|
-
const TEST_DIR = join(tmpdir(), "gemiterm-test-storage");
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
process.env.GEMITERM_CONFIG_DIR = TEST_DIR;
|
|
12
|
-
mkdirSync(TEST_DIR, { recursive: true });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
17
|
-
delete process.env.GEMITERM_CONFIG_DIR;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function makeValidCookies(): Cookie[] {
|
|
21
|
-
const farFuture = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
|
|
22
|
-
return [
|
|
23
|
-
{
|
|
24
|
-
name: "__Secure-1PSID",
|
|
25
|
-
value: "test-psid-value",
|
|
26
|
-
domain: ".google.com",
|
|
27
|
-
path: "/",
|
|
28
|
-
expires: farFuture,
|
|
29
|
-
httpOnly: true,
|
|
30
|
-
secure: true,
|
|
31
|
-
sameSite: "Lax",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "__Secure-1PSIDTS",
|
|
35
|
-
value: "test-psidts-value",
|
|
36
|
-
domain: ".google.com",
|
|
37
|
-
path: "/",
|
|
38
|
-
expires: farFuture,
|
|
39
|
-
httpOnly: true,
|
|
40
|
-
secure: true,
|
|
41
|
-
sameSite: "Lax",
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makeExpiredCookies(): Cookie[] {
|
|
47
|
-
const past = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60;
|
|
48
|
-
return [
|
|
49
|
-
{
|
|
50
|
-
name: "__Secure-1PSID",
|
|
51
|
-
value: "expired-psid",
|
|
52
|
-
domain: ".google.com",
|
|
53
|
-
path: "/",
|
|
54
|
-
expires: past,
|
|
55
|
-
httpOnly: true,
|
|
56
|
-
secure: true,
|
|
57
|
-
sameSite: "Lax",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: "__Secure-1PSIDTS",
|
|
61
|
-
value: "expired-psidts",
|
|
62
|
-
domain: ".google.com",
|
|
63
|
-
path: "/",
|
|
64
|
-
expires: past,
|
|
65
|
-
httpOnly: true,
|
|
66
|
-
secure: true,
|
|
67
|
-
sameSite: "Lax",
|
|
68
|
-
},
|
|
69
|
-
];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
describe("CookieStorage", () => {
|
|
73
|
-
let storage: CookieStorage;
|
|
74
|
-
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
storage = new CookieStorage();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("saves and loads cookies for a profile", () => {
|
|
80
|
-
const cookies = makeValidCookies();
|
|
81
|
-
storage.save("test-profile", cookies);
|
|
82
|
-
|
|
83
|
-
const loaded = storage.load("test-profile");
|
|
84
|
-
expect(loaded).toHaveLength(2);
|
|
85
|
-
expect(loaded[0].name).toBe("__Secure-1PSID");
|
|
86
|
-
expect(loaded[1].name).toBe("__Secure-1PSIDTS");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("throws when loading non-existent profile", () => {
|
|
90
|
-
expect(() => storage.load("nonexistent")).toThrow("No storage state found");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("deletes profile directory", () => {
|
|
94
|
-
storage.save("to-delete", makeValidCookies());
|
|
95
|
-
expect(existsSync(join(TEST_DIR, "profiles", "to-delete"))).toBe(true);
|
|
96
|
-
|
|
97
|
-
storage.delete("to-delete");
|
|
98
|
-
expect(existsSync(join(TEST_DIR, "profiles", "to-delete"))).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("delete is a no-op for non-existent profile", () => {
|
|
102
|
-
expect(() => storage.delete("nonexistent")).not.toThrow();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("list returns empty array when no profiles exist", () => {
|
|
106
|
-
expect(storage.list()).toEqual([]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("list returns saved profile names", () => {
|
|
110
|
-
storage.save("alpha", makeValidCookies());
|
|
111
|
-
storage.save("beta", makeValidCookies());
|
|
112
|
-
|
|
113
|
-
const names = storage.list();
|
|
114
|
-
expect(names).toContain("alpha");
|
|
115
|
-
expect(names).toContain("beta");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("overwrites existing cookies on save", () => {
|
|
119
|
-
storage.save("overwrite", makeValidCookies());
|
|
120
|
-
const newCookies = [
|
|
121
|
-
{ ...makeValidCookies()[0], value: "new-value" },
|
|
122
|
-
{ ...makeValidCookies()[1], value: "new-ts-value" },
|
|
123
|
-
];
|
|
124
|
-
storage.save("overwrite", newCookies);
|
|
125
|
-
|
|
126
|
-
const loaded = storage.load("overwrite");
|
|
127
|
-
expect(loaded[0].value).toBe("new-value");
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("ProfileManager", () => {
|
|
132
|
-
let manager: ProfileManager;
|
|
133
|
-
|
|
134
|
-
beforeEach(() => {
|
|
135
|
-
manager = new ProfileManager();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("create creates profile directory", () => {
|
|
139
|
-
manager.create("new-profile");
|
|
140
|
-
expect(existsSync(join(TEST_DIR, "profiles", "new-profile"))).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("create sets first profile as default", () => {
|
|
144
|
-
manager.create("first");
|
|
145
|
-
expect(manager.getDefault()).toBe("first");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("create does not change default if profiles already exist", () => {
|
|
149
|
-
manager.create("first");
|
|
150
|
-
manager.create("second");
|
|
151
|
-
expect(manager.getDefault()).toBe("first");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("create throws if profile already exists", () => {
|
|
155
|
-
manager.create("dup");
|
|
156
|
-
expect(() => manager.create("dup")).toThrow("already exists");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("delete removes profile directory and resets default", () => {
|
|
160
|
-
manager.create("p1");
|
|
161
|
-
manager.create("p2");
|
|
162
|
-
manager.setDefault("p1");
|
|
163
|
-
|
|
164
|
-
manager.delete("p1");
|
|
165
|
-
expect(manager.getDefault()).toBe("p2");
|
|
166
|
-
expect(manager.list()).not.toContain("p1");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("delete resets default to remaining profile", () => {
|
|
170
|
-
manager.create("solo");
|
|
171
|
-
manager.delete("solo");
|
|
172
|
-
expect(manager.list()).toEqual([]);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("rename moves profile directory", () => {
|
|
176
|
-
manager.create("old-name");
|
|
177
|
-
manager.rename("old-name", "new-name");
|
|
178
|
-
|
|
179
|
-
expect(manager.list()).toContain("new-name");
|
|
180
|
-
expect(manager.list()).not.toContain("old-name");
|
|
181
|
-
expect(existsSync(join(TEST_DIR, "profiles", "new-name"))).toBe(true);
|
|
182
|
-
expect(existsSync(join(TEST_DIR, "profiles", "old-name"))).toBe(false);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("rename updates default if renamed profile was default", () => {
|
|
186
|
-
manager.create("default-profile");
|
|
187
|
-
manager.rename("default-profile", "renamed");
|
|
188
|
-
|
|
189
|
-
expect(manager.getDefault()).toBe("renamed");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("rename throws if source does not exist", () => {
|
|
193
|
-
expect(() => manager.rename("nope", "dest")).toThrow("does not exist");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("rename throws if destination already exists", () => {
|
|
197
|
-
manager.create("a");
|
|
198
|
-
manager.create("b");
|
|
199
|
-
expect(() => manager.rename("a", "b")).toThrow("already exists");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("setDefault throws if profile does not exist", () => {
|
|
203
|
-
expect(() => manager.setDefault("ghost")).toThrow("does not exist");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("setDefault sets the default profile name", () => {
|
|
207
|
-
manager.create("p1");
|
|
208
|
-
manager.create("p2");
|
|
209
|
-
manager.setDefault("p2");
|
|
210
|
-
expect(manager.getDefault()).toBe("p2");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("getStatus returns correct data for existing valid profile", () => {
|
|
214
|
-
const storage = new CookieStorage();
|
|
215
|
-
const mgr = new ProfileManager(storage);
|
|
216
|
-
storage.save("active", makeValidCookies());
|
|
217
|
-
|
|
218
|
-
const status = mgr.getStatus("active");
|
|
219
|
-
expect(status.exists).toBe(true);
|
|
220
|
-
expect(status.isActive).toBe(true);
|
|
221
|
-
expect(status.expiresAt).not.toBeNull();
|
|
222
|
-
expect(status.isDefault).toBe(false);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test("getStatus returns inactive for expired cookies", () => {
|
|
226
|
-
const storage = new CookieStorage();
|
|
227
|
-
const mgr = new ProfileManager(storage);
|
|
228
|
-
storage.save("expired", makeExpiredCookies());
|
|
229
|
-
|
|
230
|
-
const status = mgr.getStatus("expired");
|
|
231
|
-
expect(status.exists).toBe(true);
|
|
232
|
-
expect(status.isActive).toBe(false);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("getStatus returns active for session cookies (expires: -1)", () => {
|
|
236
|
-
const storage = new CookieStorage();
|
|
237
|
-
const mgr = new ProfileManager(storage);
|
|
238
|
-
const sessionCookies: Cookie[] = [
|
|
239
|
-
{
|
|
240
|
-
name: "__Secure-1PSID",
|
|
241
|
-
value: "psid",
|
|
242
|
-
domain: ".google.com",
|
|
243
|
-
path: "/",
|
|
244
|
-
expires: -1,
|
|
245
|
-
httpOnly: true,
|
|
246
|
-
secure: true,
|
|
247
|
-
sameSite: "Lax",
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
name: "__Secure-1PSIDTS",
|
|
251
|
-
value: "psidts",
|
|
252
|
-
domain: ".google.com",
|
|
253
|
-
path: "/",
|
|
254
|
-
expires: -1,
|
|
255
|
-
httpOnly: true,
|
|
256
|
-
secure: true,
|
|
257
|
-
sameSite: "Lax",
|
|
258
|
-
},
|
|
259
|
-
];
|
|
260
|
-
storage.save("session", sessionCookies);
|
|
261
|
-
|
|
262
|
-
const status = mgr.getStatus("session");
|
|
263
|
-
expect(status.exists).toBe(true);
|
|
264
|
-
expect(status.isActive).toBe(true);
|
|
265
|
-
expect(status.expiresAt).toBeNull();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("getStatus returns not exists for missing profile", () => {
|
|
269
|
-
const status = manager.getStatus("missing");
|
|
270
|
-
expect(status.exists).toBe(false);
|
|
271
|
-
expect(status.isActive).toBe(false);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("getStatus marks the default profile", () => {
|
|
275
|
-
manager.create("def");
|
|
276
|
-
const status = manager.getStatus("def");
|
|
277
|
-
expect(status.isDefault).toBe(true);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("getAllStatuses returns all profile statuses", () => {
|
|
281
|
-
const storage = new CookieStorage();
|
|
282
|
-
const mgr = new ProfileManager(storage);
|
|
283
|
-
storage.save("active", makeValidCookies());
|
|
284
|
-
storage.save("expired", makeExpiredCookies());
|
|
285
|
-
|
|
286
|
-
const statuses = mgr.getAllStatuses();
|
|
287
|
-
expect(statuses).toHaveLength(2);
|
|
288
|
-
const active = statuses.find((s) => s.name === "active")!;
|
|
289
|
-
const expired = statuses.find((s) => s.name === "expired")!;
|
|
290
|
-
expect(active.isActive).toBe(true);
|
|
291
|
-
expect(expired.isActive).toBe(false);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("hasValidCookies returns true for fresh cookies", () => {
|
|
295
|
-
const storage = new CookieStorage();
|
|
296
|
-
const mgr = new ProfileManager(storage);
|
|
297
|
-
storage.save("fresh", makeValidCookies());
|
|
298
|
-
|
|
299
|
-
expect(mgr.hasValidCookies("fresh")).toBe(true);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("hasValidCookies returns false for expired cookies", () => {
|
|
303
|
-
const storage = new CookieStorage();
|
|
304
|
-
const mgr = new ProfileManager(storage);
|
|
305
|
-
storage.save("stale", makeExpiredCookies());
|
|
306
|
-
|
|
307
|
-
expect(mgr.hasValidCookies("stale")).toBe(false);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test("hasValidCookies returns false for missing profile", () => {
|
|
311
|
-
expect(manager.hasValidCookies("nope")).toBe(false);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test("loadCookiesForApi returns cookie values", () => {
|
|
315
|
-
const storage = new CookieStorage();
|
|
316
|
-
const mgr = new ProfileManager(storage);
|
|
317
|
-
storage.save("api-test", makeValidCookies());
|
|
318
|
-
|
|
319
|
-
const result = mgr.loadCookiesForApi("api-test");
|
|
320
|
-
expect(result.secure1psid).toBe("test-psid-value");
|
|
321
|
-
expect(result.secure1psidts).toBe("test-psidts-value");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
test("loadCookiesForApi throws for expired cookies", () => {
|
|
325
|
-
const storage = new CookieStorage();
|
|
326
|
-
const mgr = new ProfileManager(storage);
|
|
327
|
-
storage.save("expired-api", makeExpiredCookies());
|
|
328
|
-
|
|
329
|
-
expect(() => mgr.loadCookiesForApi("expired-api")).toThrow("expired");
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("loadCookiesForApi throws for missing profile", () => {
|
|
333
|
-
expect(() => manager.loadCookiesForApi("ghost")).toThrow("No storage state found");
|
|
334
|
-
});
|
|
335
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
validateConversationId,
|
|
4
|
-
parseIsoDate,
|
|
5
|
-
validateProfileName,
|
|
6
|
-
} from "../../src/infrastructure/validators.ts";
|
|
7
|
-
import { GemitermError } from "../../src/core/errors.ts";
|
|
8
|
-
|
|
9
|
-
describe("validateConversationId", () => {
|
|
10
|
-
test("does not throw for a valid non-empty id", () => {
|
|
11
|
-
expect(() => validateConversationId("abc123")).not.toThrow();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("throws for empty string", () => {
|
|
15
|
-
expect(() => validateConversationId("")).toThrow(GemitermError);
|
|
16
|
-
expect(() => validateConversationId("")).toThrow("Conversation ID must not be empty");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("throws for whitespace-only string", () => {
|
|
20
|
-
expect(() => validateConversationId(" ")).toThrow(GemitermError);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("parseIsoDate", () => {
|
|
25
|
-
test("returns timestamp for valid ISO date", () => {
|
|
26
|
-
const ts = parseIsoDate("2025-01-15T10:30:00.000Z", "createdAt");
|
|
27
|
-
expect(typeof ts).toBe("number");
|
|
28
|
-
expect(ts).toBe(new Date("2025-01-15T10:30:00.000Z").getTime());
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("throws for invalid date string", () => {
|
|
32
|
-
expect(() => parseIsoDate("not-a-date", "field")).toThrow(GemitermError);
|
|
33
|
-
expect(() => parseIsoDate("not-a-date", "field")).toThrow("Invalid ISO date for 'field'");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("includes field name in error message", () => {
|
|
37
|
-
expect(() => parseIsoDate("bad", "updatedAt")).toThrow("'updatedAt'");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("validateProfileName", () => {
|
|
42
|
-
test("does not throw for valid names", () => {
|
|
43
|
-
expect(() => validateProfileName("default")).not.toThrow();
|
|
44
|
-
expect(() => validateProfileName("my-profile")).not.toThrow();
|
|
45
|
-
expect(() => validateProfileName("profile_2")).not.toThrow();
|
|
46
|
-
expect(() => validateProfileName("ABC123")).not.toThrow();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("throws for empty string", () => {
|
|
50
|
-
expect(() => validateProfileName("")).toThrow(GemitermError);
|
|
51
|
-
expect(() => validateProfileName("")).toThrow("Profile name must not be empty");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("throws for whitespace-only string", () => {
|
|
55
|
-
expect(() => validateProfileName(" ")).toThrow(GemitermError);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("throws for name with spaces", () => {
|
|
59
|
-
expect(() => validateProfileName("my profile")).toThrow(GemitermError);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("throws for name with special characters", () => {
|
|
63
|
-
expect(() => validateProfileName("profile@bad")).toThrow(GemitermError);
|
|
64
|
-
expect(() => validateProfileName("pro/ject")).toThrow(GemitermError);
|
|
65
|
-
expect(() => validateProfileName("pro.file")).toThrow(GemitermError);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import { AuthCommand } from "../../../src/cli/commands/auth-command.ts";
|
|
3
|
-
import { Mediator } from "../../../src/core/mediator.ts";
|
|
4
|
-
import type { CliCommandContext } from "../../../src/cli/command-registry.ts";
|
|
5
|
-
import { setupTestConfig, teardownTestConfig } from "../../setup.ts";
|
|
6
|
-
import { createMockCookies, mockProfileDir } from "../../fixtures/auth-fixtures.ts";
|
|
7
|
-
import * as configModule from "../../../src/infrastructure/config.ts";
|
|
8
|
-
|
|
9
|
-
describe("auth command integration", () => {
|
|
10
|
-
let command: AuthCommand;
|
|
11
|
-
let context: CliCommandContext;
|
|
12
|
-
let logSpy: ReturnType<typeof spyOn>;
|
|
13
|
-
let originalEnv: Record<string, string | undefined>;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
command = new AuthCommand();
|
|
17
|
-
context = { verbose: false, mediator: new Mediator() };
|
|
18
|
-
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
19
|
-
originalEnv = {
|
|
20
|
-
GEMITERM_CONFIG_DIR: process.env.GEMITERM_CONFIG_DIR,
|
|
21
|
-
};
|
|
22
|
-
const configDir = setupTestConfig("auth-integration");
|
|
23
|
-
spyOn(configModule, "getConfigDir").mockReturnValue(configDir);
|
|
24
|
-
spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
|
|
25
|
-
spyOn(configModule, "getDefaultProfileName").mockReturnValue("default");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
mock.restore();
|
|
30
|
-
logSpy.mockRestore();
|
|
31
|
-
teardownTestConfig(originalEnv);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe("command metadata", () => {
|
|
35
|
-
test("has correct name and description", () => {
|
|
36
|
-
expect(command.name).toBe("auth");
|
|
37
|
-
expect(command.description).toBe("Authenticate with Google Gemini");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("auth flow with mock browser", () => {
|
|
42
|
-
test("creates first profile and authenticates when no profiles exist", async () => {
|
|
43
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
44
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
45
|
-
|
|
46
|
-
await command.execute([], context);
|
|
47
|
-
|
|
48
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
49
|
-
expect.anything(),
|
|
50
|
-
"default",
|
|
51
|
-
expect.anything(),
|
|
52
|
-
true,
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("authenticates with the only profile when one exists", async () => {
|
|
57
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["solo"]);
|
|
58
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
59
|
-
|
|
60
|
-
await command.execute([], context);
|
|
61
|
-
|
|
62
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
63
|
-
expect.anything(),
|
|
64
|
-
"solo",
|
|
65
|
-
expect.anything(),
|
|
66
|
-
false,
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("propagates authentication errors", async () => {
|
|
71
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["default"]);
|
|
72
|
-
spyOn(command as any, "authenticateWithProfile").mockRejectedValue(
|
|
73
|
-
new Error("Browser launch failed"),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await expect(command.execute([], context)).rejects.toThrow("Browser launch failed");
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe("profile selection menu", () => {
|
|
81
|
-
test("displays profile menu when multiple profiles exist", async () => {
|
|
82
|
-
const profiles = ["work", "personal", "dev"];
|
|
83
|
-
|
|
84
|
-
spyOn(configModule, "listProfiles").mockReturnValue(profiles);
|
|
85
|
-
const menuSpy = spyOn(command as any, "showProfileMenu").mockResolvedValue(null);
|
|
86
|
-
|
|
87
|
-
await command.execute([], context);
|
|
88
|
-
|
|
89
|
-
expect(menuSpy).toHaveBeenCalledWith(profiles, expect.anything());
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("menu shows all profile options", async () => {
|
|
93
|
-
const profiles = ["alpha", "beta"];
|
|
94
|
-
spyOn(command as any, "promptInput").mockResolvedValue("X");
|
|
95
|
-
|
|
96
|
-
const mockProfileManager = {
|
|
97
|
-
getStatus: mock((name: string) => ({
|
|
98
|
-
name,
|
|
99
|
-
exists: true,
|
|
100
|
-
isActive: true,
|
|
101
|
-
expiresAt: null,
|
|
102
|
-
isDefault: name === "alpha",
|
|
103
|
-
})),
|
|
104
|
-
create: mock(() => {}),
|
|
105
|
-
delete: mock(() => {}),
|
|
106
|
-
rename: mock(() => {}),
|
|
107
|
-
setDefault: mock(() => {}),
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
await (command as any).showProfileMenu(profiles, mockProfileManager);
|
|
111
|
-
|
|
112
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
113
|
-
expect(output).toContain("[A]");
|
|
114
|
-
expect(output).toContain("Add new profile");
|
|
115
|
-
expect(output).toContain("[D]");
|
|
116
|
-
expect(output).toContain("Delete profile");
|
|
117
|
-
expect(output).toContain("[S]");
|
|
118
|
-
expect(output).toContain("Set default");
|
|
119
|
-
expect(output).toContain("[R]");
|
|
120
|
-
expect(output).toContain("Rename profile");
|
|
121
|
-
expect(output).toContain("[X]");
|
|
122
|
-
expect(output).toContain("Exit");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("selecting a profile from menu triggers authentication", async () => {
|
|
126
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["p1", "p2"]);
|
|
127
|
-
spyOn(command as any, "showProfileMenu").mockResolvedValue({
|
|
128
|
-
type: "auth",
|
|
129
|
-
profileName: "p2",
|
|
130
|
-
});
|
|
131
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
132
|
-
|
|
133
|
-
await command.execute([], context);
|
|
134
|
-
|
|
135
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
136
|
-
expect.anything(),
|
|
137
|
-
"p2",
|
|
138
|
-
expect.anything(),
|
|
139
|
-
false,
|
|
140
|
-
);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("exiting menu continues with default profile", async () => {
|
|
144
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["p1", "p2"]);
|
|
145
|
-
spyOn(command as any, "showProfileMenu").mockResolvedValue(null);
|
|
146
|
-
|
|
147
|
-
await command.execute([], context);
|
|
148
|
-
|
|
149
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
150
|
-
expect(output).toContain("Continuing with current default");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("adding profile from menu triggers auth with new name", async () => {
|
|
154
|
-
const profiles = ["existing"];
|
|
155
|
-
const newProfileName = "brand-new";
|
|
156
|
-
|
|
157
|
-
spyOn(configModule, "listProfiles").mockReturnValue(profiles);
|
|
158
|
-
|
|
159
|
-
const mockProfileManager = {
|
|
160
|
-
create: mock(() => {}),
|
|
161
|
-
getStatus: mock(() => ({
|
|
162
|
-
name: "existing",
|
|
163
|
-
exists: true,
|
|
164
|
-
isActive: true,
|
|
165
|
-
expiresAt: null,
|
|
166
|
-
isDefault: true,
|
|
167
|
-
})),
|
|
168
|
-
delete: mock(() => {}),
|
|
169
|
-
rename: mock(() => {}),
|
|
170
|
-
setDefault: mock(() => {}),
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
spyOn(command as any, "promptInput").mockImplementation(
|
|
174
|
-
(prompt: string) => {
|
|
175
|
-
if (prompt.includes("Select")) return Promise.resolve("A");
|
|
176
|
-
if (prompt.includes("Enter profile name")) return Promise.resolve(newProfileName);
|
|
177
|
-
return Promise.resolve("");
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const result = await (command as any).showProfileMenu(profiles, mockProfileManager);
|
|
182
|
-
|
|
183
|
-
expect(result).toEqual({ type: "auth", profileName: newProfileName });
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
});
|