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,315 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
AuthService,
|
|
4
|
-
AuthServiceTimeoutError,
|
|
5
|
-
} from "../../src/services/auth-service.ts";
|
|
6
|
-
import { Logger } from "../../src/infrastructure/logger.ts";
|
|
7
|
-
import type { Cookie } from "../../src/core/types.ts";
|
|
8
|
-
import type { CookieStorage } from "../../src/infrastructure/storage.ts";
|
|
9
|
-
|
|
10
|
-
function createMockDriver() {
|
|
11
|
-
return {
|
|
12
|
-
openHeaded: mock(async (_url: string, _profile: string, _session?: string) => {}),
|
|
13
|
-
closeSession: mock(async (_session: string) => {}),
|
|
14
|
-
closeAll: mock(async () => {}),
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function createMockCookieMonitor() {
|
|
19
|
-
return {
|
|
20
|
-
start: mock(
|
|
21
|
-
async (
|
|
22
|
-
_session: string,
|
|
23
|
-
_onCookiesFound: (cookies: Cookie[]) => void,
|
|
24
|
-
_timeoutMs?: number,
|
|
25
|
-
) => {},
|
|
26
|
-
),
|
|
27
|
-
stop: mock(() => {}),
|
|
28
|
-
isRunning: false,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function createMockCookieStorage() {
|
|
33
|
-
return {
|
|
34
|
-
save: mock((_profileName: string, _cookies: Cookie[]) => {}),
|
|
35
|
-
load: mock((_profileName: string) => [] as Cookie[]),
|
|
36
|
-
delete: mock((_profileName: string) => {}),
|
|
37
|
-
list: mock(() => [] as string[]),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createTestLogger(): Logger {
|
|
42
|
-
const logger = new Logger("test");
|
|
43
|
-
Logger.setVerbose(true);
|
|
44
|
-
return logger;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function makeAuthCookies(): Cookie[] {
|
|
48
|
-
const farFuture = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
|
|
49
|
-
return [
|
|
50
|
-
{
|
|
51
|
-
name: "__Secure-1PSID",
|
|
52
|
-
value: "test-psid-value",
|
|
53
|
-
domain: ".google.com",
|
|
54
|
-
path: "/",
|
|
55
|
-
expires: -1,
|
|
56
|
-
httpOnly: true,
|
|
57
|
-
secure: true,
|
|
58
|
-
sameSite: "None",
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: "__Secure-1PSIDTS",
|
|
62
|
-
value: "test-psidts-value",
|
|
63
|
-
domain: ".google.com",
|
|
64
|
-
path: "/",
|
|
65
|
-
expires: farFuture,
|
|
66
|
-
httpOnly: true,
|
|
67
|
-
secure: true,
|
|
68
|
-
sameSite: "Lax",
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function buildService(
|
|
74
|
-
driver: ReturnType<typeof createMockDriver>,
|
|
75
|
-
cookieMonitor: ReturnType<typeof createMockCookieMonitor>,
|
|
76
|
-
cookieStorage: ReturnType<typeof createMockCookieStorage>,
|
|
77
|
-
logger: Logger,
|
|
78
|
-
) {
|
|
79
|
-
return new AuthService({
|
|
80
|
-
driver: driver as never,
|
|
81
|
-
cookieMonitor: cookieMonitor as never,
|
|
82
|
-
cookieStorage: cookieStorage as never,
|
|
83
|
-
logger,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
describe("AuthService", () => {
|
|
88
|
-
let driver: ReturnType<typeof createMockDriver>;
|
|
89
|
-
let cookieMonitor: ReturnType<typeof createMockCookieMonitor>;
|
|
90
|
-
let cookieStorage: ReturnType<typeof createMockCookieStorage>;
|
|
91
|
-
let logger: Logger;
|
|
92
|
-
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
driver = createMockDriver();
|
|
95
|
-
cookieMonitor = createMockCookieMonitor();
|
|
96
|
-
cookieStorage = createMockCookieStorage();
|
|
97
|
-
logger = createTestLogger();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
afterEach(() => {
|
|
101
|
-
mock.restore();
|
|
102
|
-
Logger.setVerbose(false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("authenticate", () => {
|
|
106
|
-
test("completes full auth flow and returns cookies", async () => {
|
|
107
|
-
const authCookies = makeAuthCookies();
|
|
108
|
-
cookieMonitor.start.mockImplementationOnce(
|
|
109
|
-
async (_session, callback) => {
|
|
110
|
-
callback(authCookies);
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
115
|
-
|
|
116
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
117
|
-
const result = await svc.authenticate("test-profile");
|
|
118
|
-
|
|
119
|
-
expect(result.cookies).toHaveLength(2);
|
|
120
|
-
expect(result.cookies[0]!.name).toBe("__Secure-1PSID");
|
|
121
|
-
expect(result.expiresAt).not.toBeNull();
|
|
122
|
-
expect(driver.openHeaded).toHaveBeenCalledTimes(1);
|
|
123
|
-
expect(cookieMonitor.start).toHaveBeenCalledTimes(1);
|
|
124
|
-
expect(cookieStorage.save).toHaveBeenCalledTimes(1);
|
|
125
|
-
expect(driver.closeSession).toHaveBeenCalledTimes(1);
|
|
126
|
-
expect(driver.closeSession).toHaveBeenCalledWith("test-profile");
|
|
127
|
-
expect(driver.closeAll).not.toHaveBeenCalled();
|
|
128
|
-
logSpy.mockRestore();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("rejects on timeout", async () => {
|
|
132
|
-
cookieMonitor.start.mockImplementationOnce(
|
|
133
|
-
async () => {
|
|
134
|
-
// never invokes callback -> hard timeout fires
|
|
135
|
-
},
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
139
|
-
|
|
140
|
-
await expect(svc.waitForLogin("default", 50)).rejects.toBeInstanceOf(
|
|
141
|
-
AuthServiceTimeoutError,
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("calls closeBrowser in finally even when waitForLogin throws", async () => {
|
|
146
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
147
|
-
const spy = spyOn(svc, "waitForLogin").mockRejectedValueOnce(new Error("boom"));
|
|
148
|
-
|
|
149
|
-
await expect(svc.authenticate("test-profile")).rejects.toThrow("boom");
|
|
150
|
-
expect(driver.closeSession).toHaveBeenCalledWith("test-profile");
|
|
151
|
-
spy.mockRestore();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("throws on invalid profile name", async () => {
|
|
155
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
156
|
-
await expect(svc.authenticate("bad name!")).rejects.toThrow("invalid characters");
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe("notifyUser", () => {
|
|
161
|
-
test("prints the URL and a hint about auto-detection", () => {
|
|
162
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
163
|
-
|
|
164
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
165
|
-
svc.notifyUser("default");
|
|
166
|
-
|
|
167
|
-
const all = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
168
|
-
expect(all).toContain("Opening headed browser");
|
|
169
|
-
expect(all).toContain("https://gemini.google.com/app");
|
|
170
|
-
expect(all).toContain("auto-detect");
|
|
171
|
-
logSpy.mockRestore();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe("launchBrowser", () => {
|
|
176
|
-
test("calls openHeaded with app URL, profile name as both profile and session", async () => {
|
|
177
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
178
|
-
|
|
179
|
-
await svc.launchBrowser("my-profile");
|
|
180
|
-
|
|
181
|
-
expect(driver.openHeaded).toHaveBeenCalledTimes(1);
|
|
182
|
-
expect(driver.openHeaded).toHaveBeenCalledWith(
|
|
183
|
-
"https://gemini.google.com/app",
|
|
184
|
-
"my-profile",
|
|
185
|
-
"my-profile",
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("waitForLogin", () => {
|
|
191
|
-
test("resolves with cookies when monitor callback fires", async () => {
|
|
192
|
-
const authCookies = makeAuthCookies();
|
|
193
|
-
cookieMonitor.start.mockImplementationOnce(
|
|
194
|
-
async (_session, callback) => {
|
|
195
|
-
callback(authCookies);
|
|
196
|
-
},
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
200
|
-
const cookies = await svc.waitForLogin("default", 60_000);
|
|
201
|
-
expect(cookies).toHaveLength(2);
|
|
202
|
-
expect(cookies[0]!.name).toBe("__Secure-1PSID");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("rejects with AuthServiceTimeoutError on hard timeout", async () => {
|
|
206
|
-
cookieMonitor.start.mockImplementationOnce(async () => {});
|
|
207
|
-
|
|
208
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
209
|
-
|
|
210
|
-
await expect(svc.waitForLogin("default", 50)).rejects.toBeInstanceOf(
|
|
211
|
-
AuthServiceTimeoutError,
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("stops cookie monitor on timeout", async () => {
|
|
216
|
-
cookieMonitor.start.mockImplementationOnce(async () => {});
|
|
217
|
-
|
|
218
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
await svc.waitForLogin("default", 50);
|
|
222
|
-
} catch {
|
|
223
|
-
// expected
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
expect(cookieMonitor.stop).toHaveBeenCalled();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test("does not double-resolve when timeout and callback race", async () => {
|
|
230
|
-
cookieMonitor.start.mockImplementationOnce(
|
|
231
|
-
async (_session, callback) => {
|
|
232
|
-
// schedule callback AND let the timeout race
|
|
233
|
-
setTimeout(() => callback(makeAuthCookies()), 5);
|
|
234
|
-
},
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
238
|
-
const cookies = await svc.waitForLogin("default", 200);
|
|
239
|
-
expect(cookies).toHaveLength(2);
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
describe("extractCookies", () => {
|
|
244
|
-
test("saves cookies via cookieStorage", async () => {
|
|
245
|
-
const cookies = makeAuthCookies();
|
|
246
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
247
|
-
|
|
248
|
-
await svc.extractCookies("default", cookies);
|
|
249
|
-
|
|
250
|
-
expect(cookieStorage.save).toHaveBeenCalledTimes(1);
|
|
251
|
-
expect(cookieStorage.save).toHaveBeenCalledWith("default", cookies);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe("confirmAuthSuccess", () => {
|
|
256
|
-
test("prints success messages, expiry, and __Secure-1PSID check", () => {
|
|
257
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
258
|
-
|
|
259
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
260
|
-
svc.confirmAuthSuccess(2, new Date("2026-12-31T00:00:00Z"), makeAuthCookies());
|
|
261
|
-
|
|
262
|
-
const all = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
263
|
-
expect(all).toContain("Login auto-detected");
|
|
264
|
-
expect(all).toContain("Authentication successful");
|
|
265
|
-
expect(all).toContain("2 cookies");
|
|
266
|
-
expect(all).toContain("Session expires");
|
|
267
|
-
expect(all).toContain("__Secure-1PSID");
|
|
268
|
-
logSpy.mockRestore();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("prints success without expiry when expiresAt is null", () => {
|
|
272
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
273
|
-
|
|
274
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
275
|
-
svc.confirmAuthSuccess(1, null, []);
|
|
276
|
-
|
|
277
|
-
const all = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
278
|
-
expect(all).toContain("Authentication successful");
|
|
279
|
-
expect(all).toContain("1 cookies");
|
|
280
|
-
expect(all).not.toContain("Session expires");
|
|
281
|
-
logSpy.mockRestore();
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("indicates missing __Secure-1PSID cookie when absent", () => {
|
|
285
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
286
|
-
|
|
287
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
288
|
-
svc.confirmAuthSuccess(0, null, []);
|
|
289
|
-
|
|
290
|
-
const all = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
|
291
|
-
expect(all).toContain("__Secure-1PSID");
|
|
292
|
-
expect(all).toContain("❌");
|
|
293
|
-
logSpy.mockRestore();
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe("closeBrowser", () => {
|
|
298
|
-
test("calls closeSession on the driver with the profile name", async () => {
|
|
299
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
300
|
-
|
|
301
|
-
await svc.closeBrowser("default");
|
|
302
|
-
|
|
303
|
-
expect(driver.closeSession).toHaveBeenCalledTimes(1);
|
|
304
|
-
expect(driver.closeSession).toHaveBeenCalledWith("default");
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("does not throw when closeSession fails", async () => {
|
|
308
|
-
driver.closeSession.mockRejectedValueOnce(new Error("browser already closed"));
|
|
309
|
-
|
|
310
|
-
const svc = buildService(driver, cookieMonitor, cookieStorage, logger);
|
|
311
|
-
|
|
312
|
-
await expect(svc.closeBrowser("default")).resolves.toBeUndefined();
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
});
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
CookieMonitor,
|
|
4
|
-
CookieMonitorTimeoutError,
|
|
5
|
-
} from "../../src/services/cookie-monitor.ts";
|
|
6
|
-
import { Logger } from "../../src/infrastructure/logger.ts";
|
|
7
|
-
import type { Cookie } from "../../src/core/types.ts";
|
|
8
|
-
|
|
9
|
-
function createMockDriver() {
|
|
10
|
-
return {
|
|
11
|
-
evalJs: mock(async (_session: string, _expression: string) => "false"),
|
|
12
|
-
cookieListFromState: mock(async (_session: string) => [] as Cookie[]),
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function createTestLogger(): Logger {
|
|
17
|
-
const logger = new Logger("test");
|
|
18
|
-
Logger.setVerbose(true);
|
|
19
|
-
return logger;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const authCookies: Cookie[] = [
|
|
23
|
-
{
|
|
24
|
-
name: "__Secure-1PSID",
|
|
25
|
-
value: "psid-val",
|
|
26
|
-
domain: ".google.com",
|
|
27
|
-
path: "/",
|
|
28
|
-
expires: -1,
|
|
29
|
-
httpOnly: true,
|
|
30
|
-
secure: true,
|
|
31
|
-
sameSite: "None",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: "__Secure-1PSIDTS",
|
|
35
|
-
value: "ts-val",
|
|
36
|
-
domain: ".google.com",
|
|
37
|
-
path: "/",
|
|
38
|
-
expires: -1,
|
|
39
|
-
httpOnly: true,
|
|
40
|
-
secure: true,
|
|
41
|
-
sameSite: "Lax",
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
describe("CookieMonitor", () => {
|
|
46
|
-
let driver: ReturnType<typeof createMockDriver>;
|
|
47
|
-
let logger: Logger;
|
|
48
|
-
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
driver = createMockDriver();
|
|
51
|
-
logger = createTestLogger();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
afterEach(() => {
|
|
55
|
-
mock.restore();
|
|
56
|
-
Logger.setVerbose(false);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("checkLoggedIn", () => {
|
|
60
|
-
test("returns true when eval returns 'true' (sign-out link present)", async () => {
|
|
61
|
-
driver.evalJs.mockResolvedValueOnce("true");
|
|
62
|
-
|
|
63
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
64
|
-
const result = await monitor.checkLoggedIn("sess1");
|
|
65
|
-
expect(result).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("returns false when eval returns 'false' (no sign-out link)", async () => {
|
|
69
|
-
driver.evalJs.mockResolvedValueOnce("false");
|
|
70
|
-
|
|
71
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
72
|
-
const result = await monitor.checkLoggedIn("sess1");
|
|
73
|
-
expect(result).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("returns true when eval returns 'true' with surrounding whitespace", async () => {
|
|
77
|
-
driver.evalJs.mockResolvedValueOnce(" true \n");
|
|
78
|
-
|
|
79
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
80
|
-
const result = await monitor.checkLoggedIn("sess1");
|
|
81
|
-
expect(result).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("returns false when eval throws", async () => {
|
|
85
|
-
driver.evalJs.mockRejectedValueOnce(new Error("browser closed"));
|
|
86
|
-
|
|
87
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
88
|
-
const result = await monitor.checkLoggedIn("sess1");
|
|
89
|
-
expect(result).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("checkCookies", () => {
|
|
94
|
-
test("returns auth cookies when both required cookies present", async () => {
|
|
95
|
-
driver.cookieListFromState.mockResolvedValueOnce(authCookies);
|
|
96
|
-
|
|
97
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
98
|
-
const result = await monitor.checkCookies("sess1");
|
|
99
|
-
expect(result).toHaveLength(2);
|
|
100
|
-
expect(result.map((c) => c.name)).toEqual(["__Secure-1PSID", "__Secure-1PSIDTS"]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("returns empty array when only one cookie present", async () => {
|
|
104
|
-
driver.cookieListFromState.mockResolvedValueOnce([authCookies[0]!]);
|
|
105
|
-
|
|
106
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
107
|
-
const result = await monitor.checkCookies("sess1");
|
|
108
|
-
expect(result).toEqual([]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("returns empty array when no cookies present", async () => {
|
|
112
|
-
driver.cookieListFromState.mockResolvedValueOnce([]);
|
|
113
|
-
|
|
114
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
115
|
-
const result = await monitor.checkCookies("sess1");
|
|
116
|
-
expect(result).toEqual([]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("returns empty array when cookieListFromState throws", async () => {
|
|
120
|
-
driver.cookieListFromState.mockRejectedValueOnce(new Error("session error"));
|
|
121
|
-
|
|
122
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
123
|
-
const result = await monitor.checkCookies("sess1");
|
|
124
|
-
expect(result).toEqual([]);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("start / stop lifecycle", () => {
|
|
129
|
-
test("calls onCookiesFound once interval ticks", async () => {
|
|
130
|
-
driver.evalJs.mockResolvedValue("true");
|
|
131
|
-
driver.cookieListFromState.mockResolvedValue(authCookies);
|
|
132
|
-
|
|
133
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
134
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
135
|
-
|
|
136
|
-
await monitor.start("sess1", callback, 10_000);
|
|
137
|
-
await new Promise((r) => setTimeout(r, 2100));
|
|
138
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
139
|
-
expect(monitor.isRunning).toBe(false);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("stop() prevents further polling", async () => {
|
|
143
|
-
driver.evalJs.mockResolvedValue("false");
|
|
144
|
-
|
|
145
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
146
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
147
|
-
|
|
148
|
-
const startPromise = monitor.start("sess1", callback, 10_000);
|
|
149
|
-
monitor.stop();
|
|
150
|
-
await startPromise;
|
|
151
|
-
|
|
152
|
-
expect(callback).toHaveBeenCalledTimes(0);
|
|
153
|
-
expect(monitor.isRunning).toBe(false);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("does not call onCookiesFound if eval returns 'false' (not logged in)", async () => {
|
|
157
|
-
driver.evalJs.mockResolvedValue("false");
|
|
158
|
-
|
|
159
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
160
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
161
|
-
|
|
162
|
-
const startPromise = monitor.start("sess1", callback, 10_000);
|
|
163
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
164
|
-
monitor.stop();
|
|
165
|
-
await startPromise;
|
|
166
|
-
|
|
167
|
-
expect(callback).toHaveBeenCalledTimes(0);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("stop() is idempotent", () => {
|
|
171
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
172
|
-
monitor.stop();
|
|
173
|
-
monitor.stop();
|
|
174
|
-
monitor.stop();
|
|
175
|
-
expect(monitor.isRunning).toBe(false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("does not call driver.evalJs immediately at start", async () => {
|
|
179
|
-
driver.evalJs.mockResolvedValue("false");
|
|
180
|
-
driver.cookieListFromState.mockResolvedValue([]);
|
|
181
|
-
|
|
182
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
183
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
184
|
-
|
|
185
|
-
await monitor.start("sess1", callback, 10_000);
|
|
186
|
-
expect(driver.evalJs).not.toHaveBeenCalled();
|
|
187
|
-
monitor.stop();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("calls driver.evalJs once after POLL_INTERVAL_MS", async () => {
|
|
191
|
-
driver.evalJs.mockResolvedValue("false");
|
|
192
|
-
driver.cookieListFromState.mockResolvedValue([]);
|
|
193
|
-
|
|
194
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
195
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
196
|
-
|
|
197
|
-
await monitor.start("sess1", callback, 10_000);
|
|
198
|
-
await new Promise((r) => setTimeout(r, 2100));
|
|
199
|
-
expect(driver.evalJs.mock.calls.length).toBe(1);
|
|
200
|
-
monitor.stop();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("swallows repeated eval throws without rejecting", async () => {
|
|
204
|
-
driver.evalJs.mockRejectedValue(new Error("session gone"));
|
|
205
|
-
driver.cookieListFromState.mockResolvedValue([]);
|
|
206
|
-
|
|
207
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
208
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
209
|
-
|
|
210
|
-
await expect(monitor.start("sess1", callback, 10_000)).resolves.toBeUndefined();
|
|
211
|
-
await new Promise((r) => setTimeout(r, 2500));
|
|
212
|
-
expect(callback).toHaveBeenCalledTimes(0);
|
|
213
|
-
monitor.stop();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("swallows repeated cookieListFromState throws without rejecting", async () => {
|
|
217
|
-
driver.evalJs.mockResolvedValue("true");
|
|
218
|
-
driver.cookieListFromState.mockRejectedValue(new Error("cookies unreachable"));
|
|
219
|
-
|
|
220
|
-
const monitor = new CookieMonitor({ driver: driver as never, logger });
|
|
221
|
-
const callback = mock((_cookies: Cookie[]) => {});
|
|
222
|
-
|
|
223
|
-
await expect(monitor.start("sess1", callback, 10_000)).resolves.toBeUndefined();
|
|
224
|
-
await new Promise((r) => setTimeout(r, 2500));
|
|
225
|
-
expect(callback).toHaveBeenCalledTimes(0);
|
|
226
|
-
monitor.stop();
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
});
|