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,159 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
getDefaultProfileName,
|
|
6
|
-
setDefaultProfileName,
|
|
7
|
-
listProfiles,
|
|
8
|
-
ensureConfigDir,
|
|
9
|
-
} from "../../src/infrastructure/config.ts";
|
|
10
|
-
import {
|
|
11
|
-
setupTestConfig,
|
|
12
|
-
teardownTestConfig,
|
|
13
|
-
} from "../setup.ts";
|
|
14
|
-
|
|
15
|
-
describe("config", () => {
|
|
16
|
-
let configDir: string;
|
|
17
|
-
let originalConfigDir: string | undefined;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
|
|
21
|
-
configDir = setupTestConfig();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
teardownTestConfig({ GEMITERM_CONFIG_DIR: originalConfigDir });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("getDefaultProfileName", () => {
|
|
29
|
-
test("returns 'default' when no marker file exists", () => {
|
|
30
|
-
expect(getDefaultProfileName()).toBe("default");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("returns the name from the marker file when it exists", () => {
|
|
34
|
-
const profilesDir = join(configDir, "profiles");
|
|
35
|
-
mkdirSync(profilesDir, { recursive: true });
|
|
36
|
-
setDefaultProfileName("work");
|
|
37
|
-
expect(getDefaultProfileName()).toBe("work");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("trims whitespace from marker file content", () => {
|
|
41
|
-
const profilesDir = join(configDir, "profiles");
|
|
42
|
-
mkdirSync(profilesDir, { recursive: true });
|
|
43
|
-
const markerPath = join(profilesDir, ".default");
|
|
44
|
-
writeFileSync(markerPath, " my-profile \n", "utf-8");
|
|
45
|
-
expect(getDefaultProfileName()).toBe("my-profile");
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("setDefaultProfileName", () => {
|
|
50
|
-
test("creates marker file with the given name", () => {
|
|
51
|
-
const markerPath = join(configDir, "profiles", ".default");
|
|
52
|
-
expect(existsSync(markerPath)).toBe(false);
|
|
53
|
-
|
|
54
|
-
setDefaultProfileName("personal");
|
|
55
|
-
|
|
56
|
-
expect(existsSync(markerPath)).toBe(true);
|
|
57
|
-
const content = readFileSync(markerPath, "utf-8");
|
|
58
|
-
expect(content).toBe("personal");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("creates profiles directory if it does not exist", () => {
|
|
62
|
-
const profilesDir = join(configDir, "profiles");
|
|
63
|
-
expect(existsSync(profilesDir)).toBe(false);
|
|
64
|
-
|
|
65
|
-
setDefaultProfileName("work");
|
|
66
|
-
|
|
67
|
-
expect(existsSync(profilesDir)).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("overwrites existing marker file", () => {
|
|
71
|
-
setDefaultProfileName("first");
|
|
72
|
-
expect(getDefaultProfileName()).toBe("first");
|
|
73
|
-
|
|
74
|
-
setDefaultProfileName("second");
|
|
75
|
-
expect(getDefaultProfileName()).toBe("second");
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe("listProfiles", () => {
|
|
80
|
-
test("returns empty array when profiles directory does not exist", () => {
|
|
81
|
-
const profilesDir = join(configDir, "profiles");
|
|
82
|
-
expect(existsSync(profilesDir)).toBe(false);
|
|
83
|
-
|
|
84
|
-
const profiles = listProfiles();
|
|
85
|
-
|
|
86
|
-
expect(profiles).toEqual([]);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("returns empty array when profiles directory exists but is empty", () => {
|
|
90
|
-
mkdirSync(join(configDir, "profiles"), { recursive: true });
|
|
91
|
-
|
|
92
|
-
const profiles = listProfiles();
|
|
93
|
-
|
|
94
|
-
expect(profiles).toEqual([]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("returns sorted list of profile directories", () => {
|
|
98
|
-
const profilesDir = join(configDir, "profiles");
|
|
99
|
-
mkdirSync(join(profilesDir, "charlie"), { recursive: true });
|
|
100
|
-
mkdirSync(join(profilesDir, "alpha"), { recursive: true });
|
|
101
|
-
mkdirSync(join(profilesDir, "bravo"), { recursive: true });
|
|
102
|
-
|
|
103
|
-
const profiles = listProfiles();
|
|
104
|
-
|
|
105
|
-
expect(profiles).toEqual(["alpha", "bravo", "charlie"]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("excludes the default marker file (.default) from results", () => {
|
|
109
|
-
const profilesDir = join(configDir, "profiles");
|
|
110
|
-
mkdirSync(join(profilesDir, "work"), { recursive: true });
|
|
111
|
-
mkdirSync(join(profilesDir, "personal"), { recursive: true });
|
|
112
|
-
writeFileSync(join(profilesDir, ".default"), "work", "utf-8");
|
|
113
|
-
|
|
114
|
-
const profiles = listProfiles();
|
|
115
|
-
|
|
116
|
-
expect(profiles).toEqual(["personal", "work"]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("excludes files (only includes directories)", () => {
|
|
120
|
-
const profilesDir = join(configDir, "profiles");
|
|
121
|
-
mkdirSync(join(profilesDir, "valid-profile"), { recursive: true });
|
|
122
|
-
writeFileSync(join(profilesDir, "not-a-profile.txt"), "data", "utf-8");
|
|
123
|
-
|
|
124
|
-
const profiles = listProfiles();
|
|
125
|
-
|
|
126
|
-
expect(profiles).toEqual(["valid-profile"]);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("ensureConfigDir", () => {
|
|
131
|
-
test("creates config directory when it does not exist", () => {
|
|
132
|
-
rmSync(configDir, { recursive: true, force: true });
|
|
133
|
-
|
|
134
|
-
ensureConfigDir();
|
|
135
|
-
|
|
136
|
-
expect(existsSync(configDir)).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("creates profiles directory inside config dir", () => {
|
|
140
|
-
const profilesDir = join(configDir, "profiles");
|
|
141
|
-
|
|
142
|
-
ensureConfigDir();
|
|
143
|
-
|
|
144
|
-
expect(existsSync(profilesDir)).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("returns the config directory path", () => {
|
|
148
|
-
const result = ensureConfigDir();
|
|
149
|
-
|
|
150
|
-
expect(result).toBe(configDir);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("does not throw when directories already exist", () => {
|
|
154
|
-
mkdirSync(join(configDir, "profiles"), { recursive: true });
|
|
155
|
-
|
|
156
|
-
expect(() => ensureConfigDir()).not.toThrow();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
GemitermError,
|
|
4
|
-
AuthenticationError,
|
|
5
|
-
CookieExpiredError,
|
|
6
|
-
GeminiAPIError,
|
|
7
|
-
ConversationNotFoundError,
|
|
8
|
-
ConversationPendingError,
|
|
9
|
-
} from "../../src/core/errors.ts";
|
|
10
|
-
|
|
11
|
-
describe("errors", () => {
|
|
12
|
-
test("all error classes extend GemitermError", () => {
|
|
13
|
-
expect(new AuthenticationError()).toBeInstanceOf(GemitermError);
|
|
14
|
-
expect(new CookieExpiredError()).toBeInstanceOf(GemitermError);
|
|
15
|
-
expect(new GeminiAPIError("test")).toBeInstanceOf(GemitermError);
|
|
16
|
-
expect(new ConversationNotFoundError("abc")).toBeInstanceOf(GemitermError);
|
|
17
|
-
expect(new ConversationPendingError()).toBeInstanceOf(GemitermError);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("all error classes extend Error", () => {
|
|
21
|
-
expect(new GemitermError("x")).toBeInstanceOf(Error);
|
|
22
|
-
expect(new AuthenticationError()).toBeInstanceOf(Error);
|
|
23
|
-
expect(new CookieExpiredError()).toBeInstanceOf(Error);
|
|
24
|
-
expect(new GeminiAPIError("x")).toBeInstanceOf(Error);
|
|
25
|
-
expect(new ConversationNotFoundError("x")).toBeInstanceOf(Error);
|
|
26
|
-
expect(new ConversationPendingError()).toBeInstanceOf(Error);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("GemitermError", () => {
|
|
30
|
-
test("preserves the provided message", () => {
|
|
31
|
-
const err = new GemitermError("something went wrong");
|
|
32
|
-
expect(err.message).toBe("something went wrong");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("has name set to 'GemitermError'", () => {
|
|
36
|
-
expect(new GemitermError("").name).toBe("GemitermError");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("AuthenticationError", () => {
|
|
41
|
-
test("uses default message when none provided", () => {
|
|
42
|
-
const err = new AuthenticationError();
|
|
43
|
-
expect(err.message).toBe(
|
|
44
|
-
"Not authenticated. Please run 'gemiterm login' first.",
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("preserves custom message", () => {
|
|
49
|
-
const err = new AuthenticationError("custom auth failure");
|
|
50
|
-
expect(err.message).toBe("custom auth failure");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("has name set to 'AuthenticationError'", () => {
|
|
54
|
-
expect(new AuthenticationError().name).toBe("AuthenticationError");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("CookieExpiredError", () => {
|
|
59
|
-
test("uses default message when none provided", () => {
|
|
60
|
-
const err = new CookieExpiredError();
|
|
61
|
-
expect(err.message).toBe(
|
|
62
|
-
"Session has expired. Please run 'gemiterm login' again.",
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("preserves custom message", () => {
|
|
67
|
-
const err = new CookieExpiredError("token expired at 12:00");
|
|
68
|
-
expect(err.message).toBe("token expired at 12:00");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("has name set to 'CookieExpiredError'", () => {
|
|
72
|
-
expect(new CookieExpiredError().name).toBe("CookieExpiredError");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("GeminiAPIError", () => {
|
|
77
|
-
test("preserves the provided message", () => {
|
|
78
|
-
const err = new GeminiAPIError("rate limit exceeded");
|
|
79
|
-
expect(err.message).toBe("rate limit exceeded");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("has name set to 'GeminiAPIError'", () => {
|
|
83
|
-
expect(new GeminiAPIError("").name).toBe("GeminiAPIError");
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("ConversationNotFoundError", () => {
|
|
88
|
-
test("includes the conversation ID in the message", () => {
|
|
89
|
-
const err = new ConversationNotFoundError("conv-123");
|
|
90
|
-
expect(err.message).toBe("Conversation 'conv-123' not found.");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("has name set to 'ConversationNotFoundError'", () => {
|
|
94
|
-
expect(new ConversationNotFoundError("x").name).toBe(
|
|
95
|
-
"ConversationNotFoundError",
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("ConversationPendingError", () => {
|
|
101
|
-
test("uses default message when none provided", () => {
|
|
102
|
-
const err = new ConversationPendingError();
|
|
103
|
-
expect(err.message).toBe(
|
|
104
|
-
"Conversation operation is still pending.",
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("preserves custom message", () => {
|
|
109
|
-
const err = new ConversationPendingError("still generating");
|
|
110
|
-
expect(err.message).toBe("still generating");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("has name set to 'ConversationPendingError'", () => {
|
|
114
|
-
expect(new ConversationPendingError().name).toBe(
|
|
115
|
-
"ConversationPendingError",
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
formatChatAsMarkdown,
|
|
4
|
-
formatChatAsJson,
|
|
5
|
-
formatProfileTable,
|
|
6
|
-
formatChatList,
|
|
7
|
-
} from "../../src/infrastructure/formatters.ts";
|
|
8
|
-
import type { Message, ChatInfo, ProfileStatus } from "../../src/core/types.ts";
|
|
9
|
-
|
|
10
|
-
describe("formatters", () => {
|
|
11
|
-
describe("formatChatAsMarkdown", () => {
|
|
12
|
-
const messages: Message[] = [
|
|
13
|
-
{ role: "user", content: "Hello Gemini" },
|
|
14
|
-
{ role: "model", content: "Hello! How can I help you?" },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
test("produces markdown with title heading", () => {
|
|
18
|
-
const result = formatChatAsMarkdown(messages, "Test Chat");
|
|
19
|
-
expect(result).toContain("# Test Chat");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("separates title heading with blank line", () => {
|
|
23
|
-
const result = formatChatAsMarkdown(messages, "Test Chat");
|
|
24
|
-
expect(result).toContain("# Test Chat\n\n");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("labels user messages as **You:**", () => {
|
|
28
|
-
const result = formatChatAsMarkdown(messages, "Test Chat");
|
|
29
|
-
expect(result).toContain("**You:**");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("labels model messages as **Gemini:**", () => {
|
|
33
|
-
const result = formatChatAsMarkdown(messages, "Test Chat");
|
|
34
|
-
expect(result).toContain("**Gemini:**");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("includes message content after label", () => {
|
|
38
|
-
const result = formatChatAsMarkdown(messages, "Test Chat");
|
|
39
|
-
expect(result).toContain("**You:**\n\nHello Gemini");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("separates messages with horizontal rule", () => {
|
|
43
|
-
const result = formatChatAsMarkdown(messages, "Test Chat");
|
|
44
|
-
expect(result).toContain("---");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("handles empty messages array", () => {
|
|
48
|
-
const result = formatChatAsMarkdown([], "Empty Chat");
|
|
49
|
-
expect(result).toContain("# Empty Chat");
|
|
50
|
-
expect(result).not.toContain("**You:**");
|
|
51
|
-
expect(result).not.toContain("**Gemini:**");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("handles single message", () => {
|
|
55
|
-
const single: Message[] = [{ role: "user", content: "Solo message" }];
|
|
56
|
-
const result = formatChatAsMarkdown(single, "Solo");
|
|
57
|
-
expect(result).toContain("**You:**");
|
|
58
|
-
expect(result).toContain("Solo message");
|
|
59
|
-
expect(result).toContain("---");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("includes metadata when includeMetadata and conversationId are provided", () => {
|
|
63
|
-
const result = formatChatAsMarkdown(messages, "Test", "conv-123", true);
|
|
64
|
-
expect(result).toContain("> Conversation ID: conv-123");
|
|
65
|
-
expect(result).toContain("> Messages: 2");
|
|
66
|
-
expect(result).toContain("> Exported:");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("excludes metadata when includeMetadata is false", () => {
|
|
70
|
-
const result = formatChatAsMarkdown(messages, "Test", "conv-123", false);
|
|
71
|
-
expect(result).not.toContain("> Conversation ID:");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("excludes metadata when includeMetadata is undefined", () => {
|
|
75
|
-
const result = formatChatAsMarkdown(messages, "Test", "conv-123");
|
|
76
|
-
expect(result).not.toContain("> Conversation ID:");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("excludes metadata when conversationId is missing", () => {
|
|
80
|
-
const result = formatChatAsMarkdown(messages, "Test", undefined, true);
|
|
81
|
-
expect(result).not.toContain("> Conversation ID:");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("formatChatAsJson", () => {
|
|
86
|
-
const messages: Message[] = [
|
|
87
|
-
{ role: "user", content: "Hello" },
|
|
88
|
-
{ role: "model", content: "Hi there" },
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
test("produces valid JSON", () => {
|
|
92
|
-
const result = formatChatAsJson(messages, "conv-1");
|
|
93
|
-
expect(() => JSON.parse(result)).not.toThrow();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("includes conversationId in output", () => {
|
|
97
|
-
const result = formatChatAsJson(messages, "conv-1");
|
|
98
|
-
const parsed = JSON.parse(result);
|
|
99
|
-
expect(parsed.conversationId).toBe("conv-1");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("includes all messages in output", () => {
|
|
103
|
-
const result = formatChatAsJson(messages, "conv-1");
|
|
104
|
-
const parsed = JSON.parse(result);
|
|
105
|
-
expect(parsed.messages).toHaveLength(2);
|
|
106
|
-
expect(parsed.messages[0].role).toBe("user");
|
|
107
|
-
expect(parsed.messages[1].role).toBe("model");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("preserves message content", () => {
|
|
111
|
-
const result = formatChatAsJson(messages, "conv-1");
|
|
112
|
-
const parsed = JSON.parse(result);
|
|
113
|
-
expect(parsed.messages[0].content).toBe("Hello");
|
|
114
|
-
expect(parsed.messages[1].content).toBe("Hi there");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("pretty-prints JSON with 2-space indentation", () => {
|
|
118
|
-
const result = formatChatAsJson(messages, "conv-1");
|
|
119
|
-
expect(result).toContain(" \"conversationId\"");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("handles empty messages array", () => {
|
|
123
|
-
const result = formatChatAsJson([], "conv-empty");
|
|
124
|
-
const parsed = JSON.parse(result);
|
|
125
|
-
expect(parsed.messages).toEqual([]);
|
|
126
|
-
expect(parsed.conversationId).toBe("conv-empty");
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("formatProfileTable", () => {
|
|
131
|
-
test("returns 'No profiles found' message for empty array", () => {
|
|
132
|
-
const result = formatProfileTable([]);
|
|
133
|
-
expect(result).toContain("No profiles found");
|
|
134
|
-
expect(result).toContain("gemiterm login");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("includes header row with column names", () => {
|
|
138
|
-
const statuses: ProfileStatus[] = [
|
|
139
|
-
{ name: "default", exists: true, isActive: true, expiresAt: null, isDefault: false },
|
|
140
|
-
];
|
|
141
|
-
const result = formatProfileTable(statuses);
|
|
142
|
-
expect(result).toContain("NAME");
|
|
143
|
-
expect(result).toContain("ACTIVE");
|
|
144
|
-
expect(result).toContain("EXPIRES");
|
|
145
|
-
expect(result).toContain("DEFAULT");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("includes separator row", () => {
|
|
149
|
-
const statuses: ProfileStatus[] = [
|
|
150
|
-
{ name: "default", exists: true, isActive: true, expiresAt: null, isDefault: false },
|
|
151
|
-
];
|
|
152
|
-
const result = formatProfileTable(statuses);
|
|
153
|
-
const lines = result.split("\n");
|
|
154
|
-
const headerIdx = lines.findIndex((l) => l.includes("NAME"));
|
|
155
|
-
expect(headerIdx).toBeGreaterThanOrEqual(0);
|
|
156
|
-
expect(lines[headerIdx + 1]).toMatch(/[\u2500-\u257f]/);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("shows profile name", () => {
|
|
160
|
-
const statuses: ProfileStatus[] = [
|
|
161
|
-
{ name: "work", exists: true, isActive: true, expiresAt: null, isDefault: false },
|
|
162
|
-
];
|
|
163
|
-
const result = formatProfileTable(statuses);
|
|
164
|
-
expect(result).toContain("work");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("marks default profile with asterisk", () => {
|
|
168
|
-
const statuses: ProfileStatus[] = [
|
|
169
|
-
{ name: "default", exists: true, isActive: true, expiresAt: null, isDefault: true },
|
|
170
|
-
];
|
|
171
|
-
const result = formatProfileTable(statuses);
|
|
172
|
-
expect(result).toContain("default");
|
|
173
|
-
expect(result).toContain("Yes");
|
|
174
|
-
expect(result).toContain("* = default profile");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("shows 'Session' for active profile with null expiresAt (session cookie)", () => {
|
|
178
|
-
const statuses: ProfileStatus[] = [
|
|
179
|
-
{ name: "default", exists: true, isActive: true, expiresAt: null, isDefault: false },
|
|
180
|
-
];
|
|
181
|
-
const result = formatProfileTable(statuses);
|
|
182
|
-
expect(result).toContain("Session");
|
|
183
|
-
expect(result).not.toContain("N/A");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("shows N/A for expiresAt when inactive and null", () => {
|
|
187
|
-
const statuses: ProfileStatus[] = [
|
|
188
|
-
{ name: "default", exists: true, isActive: false, expiresAt: null, isDefault: false },
|
|
189
|
-
];
|
|
190
|
-
const result = formatProfileTable(statuses);
|
|
191
|
-
expect(result).toContain("N/A");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("shows dash for profile that does not exist", () => {
|
|
195
|
-
const statuses: ProfileStatus[] = [
|
|
196
|
-
{ name: "missing", exists: false, isActive: false, expiresAt: null, isDefault: false },
|
|
197
|
-
];
|
|
198
|
-
const result = formatProfileTable(statuses);
|
|
199
|
-
const lines = result.split("\n");
|
|
200
|
-
const dataLine = lines.find((l) => l.includes("missing"));
|
|
201
|
-
expect(dataLine).toBeTruthy();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test("handles multiple profiles", () => {
|
|
205
|
-
const statuses: ProfileStatus[] = [
|
|
206
|
-
{ name: "default", exists: true, isActive: true, expiresAt: "2026-12-31T00:00:00Z", isDefault: true },
|
|
207
|
-
{ name: "work", exists: true, isActive: false, expiresAt: "2026-06-30T00:00:00Z", isDefault: false },
|
|
208
|
-
];
|
|
209
|
-
const result = formatProfileTable(statuses);
|
|
210
|
-
expect(result).toContain("default");
|
|
211
|
-
expect(result).toContain("work");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("formats expiration date", () => {
|
|
215
|
-
const statuses: ProfileStatus[] = [
|
|
216
|
-
{ name: "default", exists: true, isActive: true, expiresAt: "2026-12-31T00:00:00Z", isDefault: false },
|
|
217
|
-
];
|
|
218
|
-
const result = formatProfileTable(statuses);
|
|
219
|
-
expect(result).toContain("2026");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("does not truncate 'Yes' in DEFAULT column for default profile", () => {
|
|
223
|
-
const statuses: ProfileStatus[] = [
|
|
224
|
-
{ name: "default", exists: true, isActive: true, expiresAt: null, isDefault: true },
|
|
225
|
-
];
|
|
226
|
-
const result = formatProfileTable(statuses);
|
|
227
|
-
expect(result).toContain("Yes");
|
|
228
|
-
expect(result).not.toContain("Ye\u2026");
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
describe("formatChatList", () => {
|
|
233
|
-
test("returns 'No conversations found' message for empty array", () => {
|
|
234
|
-
const result = formatChatList([]);
|
|
235
|
-
expect(result).toContain("No conversations found");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("includes header row with column names", () => {
|
|
239
|
-
const chats: ChatInfo[] = [
|
|
240
|
-
{ id: "abc123", title: "Test Chat", isPinned: false, timestamp: Date.now() },
|
|
241
|
-
];
|
|
242
|
-
const result = formatChatList(chats);
|
|
243
|
-
expect(result).toContain("ID");
|
|
244
|
-
expect(result).toContain("TITLE");
|
|
245
|
-
expect(result).toContain("DATE");
|
|
246
|
-
expect(result).toContain("PIN");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("includes separator row", () => {
|
|
250
|
-
const chats: ChatInfo[] = [
|
|
251
|
-
{ id: "abc123", title: "Test Chat", isPinned: false, timestamp: Date.now() },
|
|
252
|
-
];
|
|
253
|
-
const result = formatChatList(chats);
|
|
254
|
-
const lines = result.split("\n");
|
|
255
|
-
const headerIdx = lines.findIndex((l) => l.includes("ID"));
|
|
256
|
-
expect(headerIdx).toBeGreaterThanOrEqual(0);
|
|
257
|
-
expect(lines[headerIdx + 1]).toMatch(/[\u2500-\u257f]/);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test("shows chat title", () => {
|
|
261
|
-
const chats: ChatInfo[] = [
|
|
262
|
-
{ id: "abc123", title: "My Chat", isPinned: false, timestamp: Date.now() },
|
|
263
|
-
];
|
|
264
|
-
const result = formatChatList(chats);
|
|
265
|
-
expect(result).toContain("My Chat");
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
test("shows total count footer", () => {
|
|
269
|
-
const chats: ChatInfo[] = [
|
|
270
|
-
{ id: "abc123", title: "Chat 1", isPinned: false, timestamp: Date.now() },
|
|
271
|
-
{ id: "def456", title: "Chat 2", isPinned: false, timestamp: Date.now() },
|
|
272
|
-
];
|
|
273
|
-
const result = formatChatList(chats);
|
|
274
|
-
expect(result).toContain("Total: 2 conversations");
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test("uses singular 'conversation' for single item", () => {
|
|
278
|
-
const chats: ChatInfo[] = [
|
|
279
|
-
{ id: "abc123", title: "Chat 1", isPinned: false, timestamp: Date.now() },
|
|
280
|
-
];
|
|
281
|
-
const result = formatChatList(chats);
|
|
282
|
-
expect(result).toContain("Total: 1 conversation");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("truncates long titles", () => {
|
|
286
|
-
const longTitle = "A".repeat(100);
|
|
287
|
-
const chats: ChatInfo[] = [
|
|
288
|
-
{ id: "abc123", title: longTitle, isPinned: false, timestamp: Date.now() },
|
|
289
|
-
];
|
|
290
|
-
const result = formatChatList(chats);
|
|
291
|
-
expect(result).toContain("\u2026");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("handles multiple chats", () => {
|
|
295
|
-
const chats: ChatInfo[] = [
|
|
296
|
-
{ id: "abc123", title: "First", isPinned: false, timestamp: 1000000 },
|
|
297
|
-
{ id: "def456", title: "Second", isPinned: true, timestamp: 2000000 },
|
|
298
|
-
{ id: "ghi789", title: "Third", isPinned: false, timestamp: 3000000 },
|
|
299
|
-
];
|
|
300
|
-
const result = formatChatList(chats);
|
|
301
|
-
expect(result).toContain("First");
|
|
302
|
-
expect(result).toContain("Second");
|
|
303
|
-
expect(result).toContain("Third");
|
|
304
|
-
expect(result).toContain("Total: 3 conversations");
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("shows formatted date", () => {
|
|
308
|
-
const chats: ChatInfo[] = [
|
|
309
|
-
{ id: "abc123", title: "Test", isPinned: false, timestamp: Date.now() },
|
|
310
|
-
];
|
|
311
|
-
const result = formatChatList(chats);
|
|
312
|
-
const currentYear = new Date().getFullYear().toString();
|
|
313
|
-
expect(result).toContain(currentYear);
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
});
|