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.
Files changed (150) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/install-skills-command.ts +35 -0
  5. package/src/cli/index.ts +19 -44
  6. package/src/infrastructure/cli-parser.ts +77 -0
  7. package/src/services/skills-cli-driver.ts +58 -0
  8. package/src/services/skills-service.ts +52 -0
  9. package/.env +0 -2
  10. package/.github/workflows/manual-release.yml +0 -34
  11. package/.github/workflows/release.yml +0 -198
  12. package/.github/workflows/test.yml +0 -29
  13. package/.opencode/commands/opsx-apply.md +0 -152
  14. package/.opencode/commands/opsx-archive.md +0 -157
  15. package/.opencode/commands/opsx-explore.md +0 -169
  16. package/.opencode/commands/opsx-propose.md +0 -104
  17. package/.opencode/commands/opsx-sync.md +0 -140
  18. package/.opencode/commands/search-intelligently.md +0 -9
  19. package/.opencode/opencode.json +0 -20
  20. package/.opencode/skills/intellisearch/.version +0 -1
  21. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  22. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  23. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  24. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  25. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  26. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  27. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  28. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  29. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  30. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  31. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  32. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  33. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  34. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  35. package/.serena/project.yml +0 -133
  36. package/AGENTS.md +0 -136
  37. package/CHANGELOG.md +0 -3
  38. package/bun.lock +0 -105
  39. package/docs/INSTALL.md +0 -185
  40. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  41. package/docs/python-release-for-reference.md +0 -175
  42. package/examples/sample_json_export.json +0 -32
  43. package/examples/sample_markdown_export.md +0 -221
  44. package/examples/usage_examples.sh +0 -222
  45. package/install.ps1 +0 -264
  46. package/install.sh +0 -275
  47. package/openspec/MAESTRO_MIGRATION.md +0 -180
  48. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  49. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  50. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  51. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  52. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  53. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  55. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  56. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  57. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  58. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  60. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  61. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  62. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  63. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  65. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  66. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  67. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  68. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  69. package/openspec/changes/commander-cli-parser/design.md +0 -107
  70. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  71. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  72. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  73. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  74. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  75. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  76. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  77. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  78. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  79. package/openspec/config.yaml +0 -20
  80. package/openspec/specs/auth/spec.md +0 -394
  81. package/openspec/specs/cli/spec.md +0 -108
  82. package/openspec/specs/commands/spec.md +0 -409
  83. package/openspec/specs/configuration/spec.md +0 -130
  84. package/openspec/specs/conversations/spec.md +0 -139
  85. package/openspec/specs/domain-model/spec.md +0 -128
  86. package/openspec/specs/formatters/spec.md +0 -158
  87. package/openspec/specs/install-browser/spec.md +0 -146
  88. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  89. package/openspec/specs/logger/spec.md +0 -71
  90. package/openspec/specs/mediator/spec.md +0 -72
  91. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  92. package/openspec/specs/path-utils/spec.md +0 -101
  93. package/openspec/specs/profiles/spec.md +0 -94
  94. package/openspec/specs/storage/spec.md +0 -198
  95. package/openspec/specs/testing/spec.md +0 -237
  96. package/openspec/specs/v2-installer/spec.md +0 -205
  97. package/openspec/specs/validators/spec.md +0 -65
  98. package/scripts/build.ts +0 -88
  99. package/scripts/clean-build.ps1 +0 -4
  100. package/scripts/clean-build.sh +0 -5
  101. package/scripts/install-browser.ps1 +0 -5
  102. package/scripts/install-browser.sh +0 -6
  103. package/scripts/lint-path-mediation.ps1 +0 -65
  104. package/scripts/lint-path-mediation.sh +0 -40
  105. package/tests/cli/auth-command.test.ts +0 -220
  106. package/tests/cli/command-registry.test.ts +0 -67
  107. package/tests/cli/continue-command.test.ts +0 -100
  108. package/tests/cli/delete-command.test.ts +0 -128
  109. package/tests/cli/export-all-command.test.ts +0 -375
  110. package/tests/cli/install-browser-command.test.ts +0 -48
  111. package/tests/cli/list-command.test.ts +0 -179
  112. package/tests/cli/new-command.test.ts +0 -139
  113. package/tests/cli/status-command.test.ts +0 -106
  114. package/tests/core/query-handlers.test.ts +0 -187
  115. package/tests/fixtures/auth-fixtures.ts +0 -98
  116. package/tests/fixtures/chat-fixtures.ts +0 -87
  117. package/tests/infrastructure/cli-table.test.ts +0 -107
  118. package/tests/infrastructure/formatters.test.ts +0 -202
  119. package/tests/infrastructure/io.test.ts +0 -197
  120. package/tests/infrastructure/logger.test.ts +0 -74
  121. package/tests/infrastructure/storage.test.ts +0 -335
  122. package/tests/infrastructure/validators.test.ts +0 -67
  123. package/tests/integration/commands/auth.test.ts +0 -186
  124. package/tests/integration/commands/continue.test.ts +0 -130
  125. package/tests/integration/commands/delete.test.ts +0 -140
  126. package/tests/integration/commands/export.test.ts +0 -266
  127. package/tests/integration/commands/fetch.test.ts +0 -238
  128. package/tests/integration/commands/list.test.ts +0 -281
  129. package/tests/integration/commands/profile.test.ts +0 -258
  130. package/tests/integration/commands/status.test.ts +0 -223
  131. package/tests/integration/installer-script-shape.test.ts +0 -67
  132. package/tests/parity/compare-outputs.ts +0 -359
  133. package/tests/parity/test-commands-parity.ps1 +0 -82
  134. package/tests/parity/test-commands-parity.sh +0 -61
  135. package/tests/services/auth-service.test.ts +0 -315
  136. package/tests/services/cookie-monitor.test.ts +0 -229
  137. package/tests/services/cookie-storage-service.test.ts +0 -230
  138. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  139. package/tests/services/install-browser-service.test.ts +0 -83
  140. package/tests/services/playwright-cli-driver.test.ts +0 -468
  141. package/tests/services/profile-auth-manager.test.ts +0 -334
  142. package/tests/services/profile-service.test.ts +0 -224
  143. package/tests/setup.ts +0 -95
  144. package/tests/smoke/smoke.test.ts +0 -52
  145. package/tests/unit/config.test.ts +0 -159
  146. package/tests/unit/errors.test.ts +0 -119
  147. package/tests/unit/formatters.test.ts +0 -316
  148. package/tests/unit/mediator.test.ts +0 -152
  149. package/tests/unit/path-utils.test.ts +0 -289
  150. package/tsconfig.json +0 -16
@@ -1,198 +0,0 @@
1
- ## Purpose
2
-
3
- The cookie and profile persistence layer. It reads and writes the per-profile `storage_state.json` cookie file, manages the lifecycle of profile directories (create, delete, rename, set-default), and computes profile freshness from cookie expiry. Two classes are exposed: `CookieStorage` (raw cookie I/O) and `ProfileManager` (high-level profile lifecycle and status).
4
-
5
- ## Requirements
6
-
7
- ### Requirement: CookieStorage.save
8
- The `CookieStorage` class MUST expose a `save(profileName, cookies)` method that writes the given cookie array into the profile's `storage_state.json` file as a JSON object with shape `{ cookies: Cookie[] }`. The method MUST create the profile directory (and any parents) before writing.
9
-
10
- #### Scenario: Save then load round-trips cookies
11
- - **WHEN** `save("test-profile", cookies)` is called and then `load("test-profile")` is called
12
- - **THEN** the loaded cookie array has the same names and values as the saved array
13
-
14
- #### Scenario: Save creates the profile directory
15
- - **WHEN** `save("new-profile", cookies)` is called and the profile directory does not exist
16
- - **THEN** the directory `<configDir>/profiles/new-profile` is created and contains `storage_state.json`
17
-
18
- #### Scenario: Save overwrites prior cookies
19
- - **WHEN** `save("profile", cookiesA)` is called followed by `save("profile", cookiesB)`
20
- - **THEN** `load("profile")` returns cookiesB (cookiesA is replaced)
21
-
22
- ### Requirement: CookieStorage.load
23
- The `CookieStorage` class MUST expose a `load(profileName)` method that reads the `storage_state.json` file for the profile and returns the `cookies` array. If the file does not exist, the method MUST throw an `Error` whose message includes the substring `No storage state found` and mentions `gemiterm auth`.
24
-
25
- #### Scenario: Load existing cookies
26
- - **WHEN** the profile has a previously-saved `storage_state.json`
27
- - **THEN** `load(name)` returns the cookie array
28
-
29
- #### Scenario: Load missing profile throws
30
- - **WHEN** no `storage_state.json` exists for the profile
31
- - **THEN** `load(name)` throws an error whose message contains `No storage state found`
32
-
33
- ### Requirement: CookieStorage.delete
34
- The `CookieStorage` class MUST expose a `delete(profileName)` method that removes the profile's directory recursively. If the directory does not exist, the method MUST be a no-op (no throw).
35
-
36
- #### Scenario: Delete removes the directory
37
- - **WHEN** a profile directory exists
38
- - **THEN** `delete(name)` removes the directory and any files within it
39
-
40
- #### Scenario: Delete on missing profile is a no-op
41
- - **WHEN** the profile directory does not exist
42
- - **THEN** `delete(name)` does not throw
43
-
44
- ### Requirement: CookieStorage.list
45
- The `CookieStorage` class MUST expose a `list()` method that returns the names of all stored profile directories. The return type is `string[]` and the order matches the underlying `listProfiles()` (sorted alphabetically).
46
-
47
- #### Scenario: Empty when no profiles
48
- - **WHEN** no profile directories exist
49
- - **THEN** `list()` returns `[]`
50
-
51
- #### Scenario: Lists saved profile names
52
- - **WHEN** profiles `alpha` and `beta` have been saved
53
- - **THEN** `list()` returns a list that includes both `"alpha"` and `"beta"`
54
-
55
- ### Requirement: ProfileManager.create
56
- The `ProfileManager` class MUST expose a `create(profileName)` method that creates the profile directory. If the profile directory already exists, the method MUST throw an `Error` whose message mentions `already exists`. If the profile is the first one (no other profiles exist), the method MUST additionally call `setDefaultProfileName(profileName)`. Otherwise, the default profile is unchanged.
57
-
58
- #### Scenario: Create a new profile
59
- - **WHEN** `create("new-profile")` is called
60
- - **THEN** the directory `<configDir>/profiles/new-profile` exists
61
-
62
- #### Scenario: First created profile is default
63
- - **WHEN** no profiles exist and `create("first")` is called
64
- - **THEN** `getDefault()` returns `"first"`
65
-
66
- #### Scenario: Subsequent create does not change default
67
- - **WHEN** profile `"first"` already exists and is default, and `create("second")` is called
68
- - **THEN** `getDefault()` still returns `"first"`
69
-
70
- #### Scenario: Duplicate create throws
71
- - **WHEN** profile `"dup"` already exists
72
- - **THEN** `create("dup")` throws an error whose message contains `already exists`
73
-
74
- ### Requirement: ProfileManager.delete
75
- The `ProfileManager` class MUST expose a `delete(name)` method that removes the profile's cookies (via `CookieStorage.delete`) and, if the deleted profile was the default, reassigns the default to the first remaining profile. If no profiles remain, the default marker file is removed.
76
-
77
- #### Scenario: Delete removes the profile
78
- - **WHEN** profile `"p1"` exists
79
- - **THEN** `delete("p1")` removes it and `list()` no longer contains `"p1"`
80
-
81
- #### Scenario: Deleting the default resets to remaining profile
82
- - **WHEN** profiles `"p1"` and `"p2"` exist with `"p1"` as default
83
- - **THEN** `delete("p1")` reassigns the default to `"p2"`
84
-
85
- #### Scenario: Deleting the only profile clears the default marker
86
- - **WHEN** a single profile `"solo"` exists and is the default
87
- - **THEN** `delete("solo")` removes the profile and the default marker file is gone (so `getDefault()` returns the literal string `"default"`)
88
-
89
- ### Requirement: ProfileManager.rename
90
- The `ProfileManager` class MUST expose a `rename(oldName, newName)` method that renames the profile directory on disk. If the source profile does not exist, the method MUST throw an `Error` whose message contains `does not exist`. If the destination profile already exists, the method MUST throw an `Error` whose message contains `already exists`. If the renamed profile was the default, the default MUST be updated to the new name.
91
-
92
- #### Scenario: Rename moves the directory
93
- - **WHEN** profile `"old-name"` exists
94
- - **THEN** `rename("old-name", "new-name")` moves the directory and `list()` reflects the new name
95
-
96
- #### Scenario: Renaming the default updates the marker
97
- - **WHEN** profile `"default-profile"` is the default
98
- - **THEN** `rename("default-profile", "renamed")` updates the marker so `getDefault()` returns `"renamed"`
99
-
100
- #### Scenario: Rename with missing source throws
101
- - **WHEN** profile `"nope"` does not exist
102
- - **THEN** `rename("nope", "dest")` throws an error whose message contains `does not exist`
103
-
104
- #### Scenario: Rename with existing destination throws
105
- - **WHEN** profiles `"a"` and `"b"` both exist
106
- - **THEN** `rename("a", "b")` throws an error whose message contains `already exists`
107
-
108
- ### Requirement: ProfileManager.setDefault and getDefault
109
- The `ProfileManager` class MUST expose `setDefault(name)` and `getDefault()` methods. `setDefault` MUST write the supplied name to the default marker and MUST throw an `Error` whose message contains `does not exist` if no profile directory with that name exists. `getDefault` MUST return the current default profile name (as resolved by the marker file, or `"default"` if no marker).
110
-
111
- #### Scenario: Set default on existing profile
112
- - **WHEN** profiles `"p1"` and `"p2"` exist
113
- - **THEN** `setDefault("p2")` makes `"p2"` the default and `getDefault()` returns `"p2"`
114
-
115
- #### Scenario: Set default on missing profile throws
116
- - **WHEN** profile `"ghost"` does not exist
117
- - **THEN** `setDefault("ghost")` throws an error whose message contains `does not exist`
118
-
119
- ### Requirement: ProfileManager.list
120
- The `ProfileManager` class MUST expose a `list()` method that returns the sorted list of profile directory names (delegating to `listProfiles`).
121
-
122
- #### Scenario: List returns profile names
123
- - **WHEN** multiple profiles have been created
124
- - **THEN** `list()` returns their names
125
-
126
- ### Requirement: ProfileManager.getStatus
127
- The `ProfileManager` class MUST expose a `getStatus(name)` method returning a `ProfileStatus` object. The method MUST report `exists: false` and `isActive: false` (with `expiresAt: null`) when the profile's storage file does not exist. When the file exists, the method MUST attempt to load the cookies and compute `isActive` from cookie validity and freshness (see Requirement: Freshness and Validity). If loading throws, the method MUST return `exists: true`, `isActive: false`, and `expiresAt: null`. The `isDefault` field MUST reflect whether `name` equals the current default profile name.
128
-
129
- #### Scenario: Status for a valid active profile
130
- - **WHEN** a profile has fresh `__Secure-1PSID` and `__Secure-1PSIDTS` cookies
131
- - **THEN** `getStatus(name)` returns `exists: true`, `isActive: true`, a non-null `expiresAt`, and the correct `isDefault`
132
-
133
- #### Scenario: Status for an expired profile
134
- - **WHEN** a profile's cookies are expired
135
- - **THEN** `getStatus(name)` returns `exists: true`, `isActive: false`
136
-
137
- #### Scenario: Status for a missing profile
138
- - **WHEN** no storage file exists for the profile
139
- - **THEN** `getStatus(name)` returns `exists: false`, `isActive: false`, `expiresAt: null`
140
-
141
- #### Scenario: Status reports isDefault
142
- - **WHEN** the profile is the current default
143
- - **THEN** `getStatus(name).isDefault` is `true`
144
-
145
- ### Requirement: ProfileManager.getAllStatuses
146
- The `ProfileManager` class MUST expose a `getAllStatuses()` method that returns a `ProfileStatus[]` array covering every known profile. The method MUST call `ensureConfigDir()` first (so the profiles directory exists) and MUST populate `isDefault` on every entry based on the current default profile name.
147
-
148
- #### Scenario: Returns one status per profile
149
- - **WHEN** profiles `active` (fresh cookies) and `expired` (stale cookies) exist
150
- - **THEN** `getAllStatuses()` returns a 2-element array, with `active.isActive === true` and `expired.isActive === false`
151
-
152
- ### Requirement: ProfileManager.hasValidCookies
153
- The `ProfileManager` class MUST expose a `hasValidCookies(profileName)` method that returns `true` iff the profile has both `__Secure-1PSID` and `__Secure-1PSIDTS` cookies AND the cookies are still fresh (see Requirement: Freshness and Validity). If the storage file is missing or unreadable, the method MUST return `false` (no throw).
154
-
155
- #### Scenario: Fresh cookies
156
- - **WHEN** a profile has fresh cookies
157
- - **THEN** `hasValidCookies(name)` returns `true`
158
-
159
- #### Scenario: Expired cookies
160
- - **WHEN** a profile's cookies have expired
161
- - **THEN** `hasValidCookies(name)` returns `false`
162
-
163
- #### Scenario: Missing profile
164
- - **WHEN** no storage file exists for the profile
165
- - **THEN** `hasValidCookies(name)` returns `false` (does not throw)
166
-
167
- ### Requirement: ProfileManager.loadCookiesForApi
168
- The `ProfileManager` class MUST expose a `loadCookiesForApi(profileName)` method that returns `{ secure1psid: string; secure1psidts: string | null }`. The method MUST throw an error mentioning `expired` if the cookies are not fresh. The method MUST throw an error mentioning `__Secure-1PSID` (or `No storage state found` if the file is missing) if the required `__Secure-1PSID` cookie is absent. When successful, the returned `secure1psidts` is the cookie value, or `null` if the cookie is absent.
169
-
170
- #### Scenario: Returns cookie values
171
- - **WHEN** a profile has both required cookies and they are fresh
172
- - **THEN** `loadCookiesForApi(name)` returns `{ secure1psid: "<psid>", secure1psidts: "<psidts>" }`
173
-
174
- #### Scenario: Throws on expired cookies
175
- - **WHEN** a profile's cookies are not fresh
176
- - **THEN** `loadCookiesForApi(name)` throws an error whose message contains `expired`
177
-
178
- #### Scenario: Throws on missing profile
179
- - **WHEN** no storage file exists for the profile
180
- - **THEN** `loadCookiesForApi(name)` throws an error whose message contains `No storage state found`
181
-
182
- ### Requirement: Freshness and Validity
183
- A profile's cookies are considered valid and fresh when ALL of the following are true: (a) the cookie set includes both `__Secure-1PSID` and `__Secure-1PSIDTS`, (b) the `__Secure-1PSIDTS` cookie has an `expires` value greater than 0, and (c) the resulting expiry timestamp (cookie `expires` in milliseconds) is later than `now + 7 days` (the freshness threshold). The system MUST use these rules consistently in `hasValidCookies`, `getStatus`, and `loadCookiesForApi`.
184
-
185
- #### Scenario: Freshness window uses 7-day threshold
186
- - **WHEN** a profile's `__Secure-1PSIDTS` cookie expires more than 7 days from now
187
- - **THEN** `hasValidCookies` and `getStatus` both report the profile as active
188
-
189
- #### Scenario: Cookies inside the 7-day window are not fresh
190
- - **WHEN** a profile's `__Secure-1PSIDTS` cookie expires within 7 days from now (or has already passed)
191
- - **THEN** `hasValidCookies` returns `false` and `getStatus` reports `isActive: false`
192
-
193
- ### Requirement: Cookie JSON On-Disk Layout
194
- Cookies MUST be persisted to `<profilesDir>/<name>/storage_state.json` as a JSON object of the form `{ "cookies": <Cookie[]> }`. The file MUST be UTF-8 encoded. The `expires` field on each cookie is a Unix-seconds numeric timestamp.
195
-
196
- #### Scenario: Storage file is parseable JSON with cookies array
197
- - **WHEN** `CookieStorage.save(name, cookies)` completes
198
- - **THEN** the file at `<profilesDir>/<name>/storage_state.json` is valid JSON whose top-level `cookies` field is the saved array
@@ -1,237 +0,0 @@
1
- ## Purpose
2
-
3
- The test infrastructure that supports the `gemiterm` test suite. It provides a global setup module for creating and tearing down per-test config directories, fixture factories for cookies, storage states, and chat histories, parity-comparison infrastructure that diffs the Python and Bun CLI outputs, smoke tests that exercise the compiled CLI binary, and the `package.json` test scripts that run the unit, integration, parity, and smoke test suites. Together this infrastructure lets the test suite run hermetically against temporary directories and verifies that the Bun rewrite matches the Python reference behavior.
4
-
5
- ## Requirements
6
-
7
- ### Requirement: Global Test Setup
8
-
9
- The system MUST provide `tests/setup.ts` as a global test-setup module. The module MUST expose `createTestConfigDir(prefix?)`, `createTestConfigDirAsync(prefix?)`, `cleanupTestConfigDir()`, `getTestConfigDir()`, `setupTestConfig(prefix?)`, `teardownTestConfig(originalEnv?)`, and `createMockStorageStateFile(profileName, cookies, configDir?)`. The default `prefix` MUST be `"gemiterm-test"`. `createTestConfigDir` and `createTestConfigDirAsync` MUST create a unique directory under `os.tmpdir()` whose name combines the prefix, a millisecond timestamp, and a random suffix. `cleanupTestConfigDir` MUST recursively remove the current test config directory (best-effort) and reset the cached value to `null`. `getTestConfigDir` MUST throw `Test config dir not initialized. Call createTestConfigDir() first.` when no directory has been created. `setupTestConfig` MUST create a test directory and MUST set `process.env.GEMITERM_CONFIG_DIR` to its path. `teardownTestConfig` MUST clean up the directory and MUST restore the previous value of `GEMITERM_CONFIG_DIR` from the `originalEnv` argument (deleting it entirely if it was unset, restoring it if it was set). `createMockStorageStateFile(profileName, cookies, configDir?)` MUST create a profile directory `<dir>/profiles/<profileName>`, MUST write a `storage_state.json` file inside it containing `{ cookies: Cookie[] }` where each cookie has `domain = ".google.com"`, `path = "/"`, `expires = -1`, `httpOnly = true`, `secure = true`, `sameSite = "None"` as defaults, and MUST return the file path.
10
-
11
- #### Scenario: createTestConfigDir creates a unique directory under tmpdir
12
- - **WHEN** `createTestConfigDir()` is called twice in succession
13
- - **THEN** the second call returns a different path from the first and both directories exist on disk
14
-
15
- #### Scenario: createTestConfigDir uses the provided prefix
16
- - **WHEN** `createTestConfigDir("my-prefix")` is called
17
- - **THEN** the resulting directory's basename starts with `my-prefix-`
18
-
19
- #### Scenario: cleanupTestConfigDir removes the directory and resets state
20
- - **WHEN** `createTestConfigDir()` is called and then `cleanupTestConfigDir()` is called
21
- - **THEN** the directory no longer exists on disk and a subsequent `getTestConfigDir()` throws `Test config dir not initialized.`
22
-
23
- #### Scenario: getTestConfigDir throws when uninitialized
24
- - **WHEN** `getTestConfigDir()` is called before `createTestConfigDir()` (or after cleanup)
25
- - **THEN** it throws an `Error` whose message contains `Test config dir not initialized.`
26
-
27
- #### Scenario: setupTestConfig sets GEMITERM_CONFIG_DIR
28
- - **WHEN** `setupTestConfig()` is called
29
- - **THEN** `process.env.GEMITERM_CONFIG_DIR` equals the returned directory path
30
-
31
- #### Scenario: teardownTestConfig restores the original env
32
- - **WHEN** `teardownTestConfig({ GEMITERM_CONFIG_DIR: "/old/path" })` is called after `setupTestConfig()`
33
- - **THEN** `process.env.GEMITERM_CONFIG_DIR` equals `"/old/path"` and the test directory is removed
34
-
35
- #### Scenario: teardownTestConfig deletes the env var when it was originally unset
36
- - **WHEN** `teardownTestConfig({ GEMITERM_CONFIG_DIR: undefined })` is called after `setupTestConfig()`
37
- - **THEN** `process.env.GEMITERM_CONFIG_DIR` is deleted from the environment
38
-
39
- #### Scenario: createMockStorageStateFile writes a valid storage_state.json
40
- - **WHEN** `createMockStorageStateFile("p1", [{ name: "NID", value: "v" }], dir)` is called
41
- - **THEN** the file `<dir>/profiles/p1/storage_state.json` exists, contains a JSON object with a `cookies` array, and the first cookie has `name === "NID"`, `domain === ".google.com"`, `path === "/"`, `expires === -1`, `httpOnly === true`, `secure === true`, `sameSite === "None"`
42
-
43
- #### Scenario: createMockStorageStateFile uses getTestConfigDir by default
44
- - **WHEN** `createMockStorageStateFile("p1", cookies)` is called without a `configDir`
45
- - **THEN** the file is written under the path returned by `getTestConfigDir()`
46
-
47
- ### Requirement: Auth Fixtures
48
-
49
- The system MUST provide `tests/fixtures/auth-fixtures.ts` exporting `createMockCookies(options?)`, `createMockStorageState(options?)`, `createExpiredStorageState()`, and `mockProfileDir(options?)`. The module MUST define internal constants `MOCK_COOKIE_DEFAULTS` (with `domain = ".google.com"`, `path = "/"`, `httpOnly = true`, `secure = true`, `sameSite = "None"`), `FUTURE_EXPIRY` (a Unix timestamp roughly 30 days in the future), and `PAST_EXPIRY` (a Unix timestamp roughly 30 days in the past). `createMockCookies` MUST accept `{ count?, names?, expiry?, values? }` where `count` defaults to 4, `names` defaults to `["__Secure-1PSID", "__Secure-1PSIDTS", "__Secure-1PSIDCC", "NID"]`, `expiry` defaults to `FUTURE_EXPIRY`, and `values` defaults to `{}`. The function MUST return an array of `Cookie` objects each built from `MOCK_COOKIE_DEFAULTS` plus the given name, value, and expiry. `createMockStorageState` MUST return `{ cookies: Cookie[] }` (calling `createMockCookies` internally with the supplied cookies/expiry). `createExpiredStorageState` MUST return a state object whose cookies all use `PAST_EXPIRY`. `mockProfileDir` MUST accept `{ profileName?, configDir?, cookies?, expired? }` (defaults: `profileName = "test-profile"`, `expired = false`) and MUST write a `storage_state.json` file under `<dir>/profiles/<profileName>` using `createMockStorageState` or `createExpiredStorageState` depending on `expired`, returning the profile directory path.
50
-
51
- #### Scenario: createMockCookies returns 4 default future-expiry cookies
52
- - **WHEN** `createMockCookies()` is called with no arguments
53
- - **THEN** the returned array has 4 cookies named `__Secure-1PSID`, `__Secure-1PSIDTS`, `__Secure-1PSIDCC`, `NID` in that order, each with `expires === FUTURE_EXPIRY` and the `MOCK_COOKIE_DEFAULTS` fields
54
-
55
- #### Scenario: createMockCookies honors custom count and names
56
- - **WHEN** `createMockCookies({ count: 2, names: ["A", "B"] })` is called
57
- - **THEN** the returned array has 2 cookies named `A` and `B`
58
-
59
- #### Scenario: createMockCookies honors custom values
60
- - **WHEN** `createMockCookies({ count: 1, names: ["X"], values: { X: "specific" } })` is called
61
- - **THEN** the cookie's `value` is `"specific"`
62
-
63
- #### Scenario: createMockStorageState wraps cookies in the expected shape
64
- - **WHEN** `createMockStorageState({ cookies })` is called
65
- - **THEN** the returned object is `{ cookies }` where `cookies` is the same array
66
-
67
- #### Scenario: createExpiredStorageState uses PAST_EXPIRY
68
- - **WHEN** `createExpiredStorageState()` is called
69
- - **THEN** every cookie in the returned state's `cookies` array has `expires === PAST_EXPIRY`
70
-
71
- #### Scenario: mockProfileDir creates the profile directory and storage_state.json
72
- - **WHEN** `mockProfileDir({ profileName: "alice" })` is called
73
- - **THEN** the directory `<configDir>/profiles/alice` exists and contains a valid `storage_state.json`
74
-
75
- #### Scenario: mockProfileDir with expired writes past-expiry cookies
76
- - **WHEN** `mockProfileDir({ profileName: "alice", expired: true })` is called
77
- - **THEN** the cookies in the resulting `storage_state.json` all have `expires === PAST_EXPIRY`
78
-
79
- #### Scenario: mockProfileDir creates a fresh test config dir when none exists
80
- - **WHEN** `mockProfileDir({ profileName: "alice" })` is called and `getTestConfigDir()` would throw
81
- - **THEN** `mockProfileDir` calls `createTestConfigDir()` internally and the profile directory is written under that new directory
82
-
83
- ### Requirement: Chat Fixtures
84
-
85
- The system MUST provide `tests/fixtures/chat-fixtures.ts` exporting `createMockChatList(options?)`, `createMockMessageHistory(options?)`, and `createMockConversation(options?)`. `createMockChatList` MUST accept `{ count?, ids?, titles?, pinnedIndices?, baseTimestamp? }` where `count` defaults to 3, `pinnedIndices` defaults to `[]`, and `baseTimestamp` defaults to `Date.now()`. It MUST return an array of `ChatInfo` objects whose `id`/`title` defaults are taken from fixed default lists (`conv-abc123`/`"Chat about TypeScript"`, `conv-def456`/`"Pinned conversation"`, etc.) and whose `timestamp` equals `baseTimestamp - i * 86400000`. `createMockMessageHistory` MUST accept `{ count?, conversationId?, roles?, contents? }` where `count` defaults to 4, `roles` defaults to `["user", "model", "user", "model"]`, and the default contents are `["Hello, Gemini!", "Hi there! How can I help you today?", "Tell me about TypeScript.", "TypeScript is a typed superset of JavaScript."]`. It MUST return an array of `Message` objects, attaching `conversationId` to each only when supplied. `createMockConversation` MUST accept `{ id?, title?, messages?, messageCount? }` (defaults: `id = "conv-abc123"`, `title = "Chat about TypeScript"`, `messageCount = 4`) and MUST return a `Conversation` whose `messages` come from `createMockMessageHistory` (or the supplied `messages`).
86
-
87
- #### Scenario: createMockChatList returns the requested number of chats
88
- - **WHEN** `createMockChatList({ count: 5 })` is called
89
- - **THEN** the returned array has length 5
90
-
91
- #### Scenario: createMockChatList honors custom ids and titles
92
- - **WHEN** `createMockChatList({ ids: ["x", "y"], titles: ["X", "Y"] })` is called
93
- - **THEN** the resulting chats have `id === "x"` and `id === "y"` with corresponding titles
94
-
95
- #### Scenario: createMockChatList applies pinnedIndices
96
- - **WHEN** `createMockChatList({ count: 3, pinnedIndices: [0, 2] })` is called
97
- - **THEN** the first and third chats have `isPinned === true` and the second has `isPinned === false`
98
-
99
- #### Scenario: createMockChatList timestamps are baseTimestamp minus i days in ms
100
- - **WHEN** `createMockChatList({ count: 3, baseTimestamp: 1_000_000_000_000 })` is called
101
- - **THEN** chat `i` has `timestamp === 1_000_000_000_000 - i * 86_400_000`
102
-
103
- #### Scenario: createMockMessageHistory defaults to 4 user/model messages
104
- - **WHEN** `createMockMessageHistory()` is called
105
- - **THEN** the returned array has 4 messages with roles cycling through `user, model, user, model`
106
-
107
- #### Scenario: createMockMessageHistory attaches conversationId when supplied
108
- - **WHEN** `createMockMessageHistory({ count: 2, conversationId: "conv-x" })` is called
109
- - **THEN** both messages have `conversationId === "conv-x"`
110
-
111
- #### Scenario: createMockMessageHistory omits conversationId when not supplied
112
- - **WHEN** `createMockMessageHistory({ count: 1 })` is called
113
- - **THEN** the resulting message has no `conversationId` field
114
-
115
- #### Scenario: createMockConversation defaults id and title
116
- - **WHEN** `createMockConversation()` is called
117
- - **THEN** the returned `Conversation` has `id === "conv-abc123"`, `title === "Chat about TypeScript"`, and 4 messages
118
-
119
- #### Scenario: createMockConversation accepts an explicit messages array
120
- - **WHEN** `createMockConversation({ messages: [...] })` is called
121
- - **THEN** the returned `Conversation` has exactly that `messages` array
122
-
123
- ### Requirement: Test Scripts
124
-
125
- `package.json` MUST define a `scripts` section with the keys `test`, `test:unit`, `test:integration`, `test:parity`, `test:smoke`, and `test:all`. `test` MUST be `bun test`. `test:unit` MUST run `bun test tests/unit`. `test:integration` MUST run `bun test tests/integration`. `test:parity` MUST run `bun test tests/parity`. `test:smoke` MUST run `bun test tests/smoke`. `test:all` MUST be `bun test` (an alias for `test`).
126
-
127
- #### Scenario: bun test runs all tests
128
- - **WHEN** `bun test` is executed
129
- - **THEN** all `*.test.ts` files under `tests/` are executed
130
-
131
- #### Scenario: test:unit only runs tests/unit
132
- - **WHEN** `bun run test:unit` is executed
133
- - **THEN** only files under `tests/unit/` are loaded by the test runner
134
-
135
- #### Scenario: test:integration only runs tests/integration
136
- - **WHEN** `bun run test:integration` is executed
137
- - **THEN** only files under `tests/integration/` are loaded by the test runner
138
-
139
- #### Scenario: test:parity only runs tests/parity
140
- - **WHEN** `bun run test:parity` is executed
141
- - **THEN** only files under `tests/parity/` are loaded by the test runner
142
-
143
- #### Scenario: test:smoke only runs tests/smoke
144
- - **WHEN** `bun run test:smoke` is executed
145
- - **THEN** only files under `tests/smoke/` are loaded by the test runner
146
-
147
- #### Scenario: test:all is an alias for bun test
148
- - **WHEN** `bun run test:all` is executed
149
- - **THEN** it runs `bun test` with no path restriction
150
-
151
- ### Requirement: Parity Infrastructure
152
-
153
- The system MUST provide `tests/parity/compare-outputs.ts` as a parity-comparison module that runs the same command against the Python `gemiterm` CLI and the Bun `gemiterm` CLI and diffs their `stdout`, `stderr`, and exit code. The Python CLI MUST be invoked as `${GEMITERM_PYTHON_CLI:-gemiterm} <args>` (default name `gemiterm`). The Bun CLI MUST be invoked as `bun <repo>/src/cli/index.ts <args>`. Each invocation MUST set `GEMITERM_CONFIG_DIR` to a temporary per-run directory. The module MUST normalize the outputs before comparison: version strings (`gemiterm vX.Y.Z`), ISO timestamps (`<timestamp>`), `/tmp/<dir>` paths (`<tmpdir>`), `C:\Users\<user>` (`<userdir>`), and `/home/<user>` (`<homedir>`). The module MUST export `runParityComparison(commands?, configDir?)` returning a `ParityReport` and, when run directly (`import.meta.main`), MUST print a formatted report and exit with code 1 if any command failed and 0 otherwise. The module MUST define a default command set `DEFAULT_TEST_COMMANDS` that includes `--help`, `--version`, help for each subcommand, and basic `status` / `list` invocations.
154
-
155
- #### Scenario: Default command set is a non-empty list
156
- - **WHEN** `runParityComparison()` is called with no arguments
157
- - **THEN** it iterates over `DEFAULT_TEST_COMMANDS`, which MUST contain at least one command and MUST include the strings `"--help"`, `"--version"`, and a help invocation for at least one subcommand
158
-
159
- #### Scenario: Version strings are normalized before diff
160
- - **WHEN** the Python CLI prints `gemiterm v1.2.3` and the Bun CLI prints `gemiterm v2.0.0`
161
- - **THEN** the parity comparison MUST consider their outputs equivalent (both normalized to `gemiterm vX.Y.Z`)
162
-
163
- #### Scenario: ISO timestamps are normalized
164
- - **WHEN** either CLI prints a string matching `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.]\d+Z`
165
- - **THEN** the string is replaced with `<timestamp>` before comparison
166
-
167
- #### Scenario: User-specific paths are normalized
168
- - **WHEN** either CLI prints a path matching `/tmp/<dir>`, `C:\Users\<user>`, or `/home/<user>`
169
- - **THEN** that path is replaced with `<tmpdir>`, `<userdir>`, or `<homedir>` respectively before comparison
170
-
171
- #### Scenario: When Python CLI is unavailable the result is recorded as skipped
172
- - **WHEN** the `gemiterm` binary is not on `PATH` and `GEMITERM_PYTHON_CLI` is not set
173
- - **THEN** the parity result for that command MUST mark `python === null` and the summary MUST count the command as `skipped`
174
-
175
- #### Scenario: Parity module exits non-zero on failed comparisons
176
- - **WHEN** the module is run directly via `bun tests/parity/compare-outputs.ts` and at least one command has discrepancies
177
- - **THEN** the process exits with code 1
178
-
179
- #### Scenario: Parity module exits zero when no failures
180
- - **WHEN** the module is run directly via `bun tests/parity/compare-outputs.ts` and no command has discrepancies and no commands were skipped
181
- - **THEN** the process exits with code 0
182
-
183
- ### Requirement: Parity Test Scripts
184
-
185
- The system MUST provide two parity-runner scripts: `tests/parity/test-commands-parity.sh` (POSIX shell) and `tests/parity/test-commands-parity.ps1` (PowerShell). Both scripts MUST verify that `bun` is on `PATH` (exiting with an error if not), MUST resolve `tests/parity/compare-outputs.ts` relative to their own location, MUST set a default `PythonCli` of `gemiterm` (overridable via `GEMITERM_PYTHON_CLI`), MUST define a default command list including help, version, and help for each of the user-facing commands, MUST create a `reports/parity/` directory (using a `REPORT_DIR` env override when set), MUST pass the command list to `compare-outputs.ts` via its `--commands` flag (comma-joined), MUST write a timestamped report file `parity-<YYYYMMDD-HHMMSS>.txt` containing the runner's combined stdout/stderr, and MUST exit with code 0 on success or 1 on failure. The PowerShell script MUST additionally propagate `$LASTEXITCODE` to its own exit code in the `catch` branch. Both scripts MUST accept an optional command list to override the defaults; the shell script accepts the commands as positional arguments, the PowerShell script accepts them as a `-Commands` parameter.
186
-
187
- #### Scenario: POSIX parity script writes a report under reports/parity
188
- - **WHEN** `tests/parity/test-commands-parity.sh` is run with `bun` on `PATH`
189
- - **THEN** a file `reports/parity/parity-<timestamp>.txt` is created containing the runner output
190
-
191
- #### Scenario: PowerShell parity script writes a report under reports/parity
192
- - **WHEN** `tests/parity/test-commands-parity.ps1` is run with `bun` on `PATH`
193
- - **THEN** a file `reports\parity\parity-<timestamp>.txt` is created containing the runner output
194
-
195
- #### Scenario: Both scripts default the Python CLI to "gemiterm"
196
- - **WHEN** either script is run without setting `GEMITERM_PYTHON_CLI`
197
- - **THEN** the script passes `gemiterm` (or the literal default) to `compare-outputs.ts` as the Python CLI name
198
-
199
- #### Scenario: PowerShell script propagates the comparison exit code
200
- - **WHEN** `tests/parity/compare-outputs.ts` exits with a non-zero code
201
- - **THEN** the PowerShell script exits with that same non-zero code from its `catch` block
202
-
203
- #### Scenario: Scripts skip the comparison when the Python CLI is unavailable
204
- - **WHEN** `gemiterm` is not on `PATH` and `GEMITERM_PYTHON_CLI` is not set
205
- - **THEN** every command in the default list is reported as `SKIPPED` in the parity report (no failures)
206
-
207
- ### Requirement: Parity Default Command List
208
-
209
- The parity test scripts MUST include `--help`, `--version`, `auth --help`, `status --help`, `list --help`, `fetch --help`, `continue --help`, `new --help`, `delete --help`, `export --help`, `export-all --help`, `profile --help`, `status`, `list`, `list --limit 5`, `list --format json`, and `auth` in their default command list. Together this covers all 9 user-facing non-help subcommands (`auth`, `status`, `list`, `fetch`, `continue`, `new`, `delete`, `export`, `export-all`) plus the help and version checks and the integration of `list --limit 5` and `list --format json`.
210
-
211
- #### Scenario: Default list contains help and version checks
212
- - **WHEN** either parity script is run with defaults
213
- - **THEN** the command list contains `--help` and `--version`
214
-
215
- #### Scenario: Default list contains a help invocation for every subcommand
216
- - **WHEN** either parity script is run with defaults
217
- - **THEN** the command list contains `<cmd> --help` for each of `auth`, `status`, `list`, `fetch`, `continue`, `new`, `delete`, `export`, `export-all`, `profile`
218
-
219
- #### Scenario: Default list contains a non-help invocation for the three most-used subcommands
220
- - **WHEN** either parity script is run with defaults
221
- - **THEN** the command list contains `status`, `list`, `list --limit 5`, and `list --format json`
222
-
223
- ### Requirement: Smoke Tests
224
-
225
- The system MUST provide `tests/smoke/smoke.test.ts` that runs the compiled CLI binary `src/cli/index.ts` via `bun` against `tests/fixtures` and asserts basic correctness. The smoke test suite MUST set `GEMITERM_CONFIG_DIR` to an empty string for the spawned process. The suite MUST contain a test for `--help` that asserts exit code `0`, non-empty `stdout`, and a `stdout` value that contains the substring `gemiterm` (case-insensitive). The suite MUST contain a test for `--version` that asserts exit code `0` and that `stdout` contains the substring `gemiterm v`. The suite MUST contain a test for `status` that asserts exit code `0` and non-empty `stdout`.
226
-
227
- #### Scenario: --help smoke test passes
228
- - **WHEN** the `--help` smoke test runs the CLI with `["--help"]`
229
- - **THEN** the process exits with code 0, `stdout` is non-empty, and `stdout.toLowerCase().includes("gemiterm")` is true
230
-
231
- #### Scenario: --version smoke test passes
232
- - **WHEN** the `--version` smoke test runs the CLI with `["--version"]`
233
- - **THEN** the process exits with code 0 and `stdout.includes("gemiterm v")` is true
234
-
235
- #### Scenario: status smoke test passes
236
- - **WHEN** the `status` smoke test runs the CLI with `["status"]` and `GEMITERM_CONFIG_DIR=""`
237
- - **THEN** the process exits with code 0 and `stdout` is non-empty