gemiterm 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/index.ts +19 -44
- package/src/infrastructure/cli-parser.ts +77 -0
- package/src/services/skills-cli-driver.ts +58 -0
- package/src/services/skills-service.ts +52 -0
- package/.env +0 -2
- package/.github/workflows/manual-release.yml +0 -34
- package/.github/workflows/release.yml +0 -198
- package/.github/workflows/test.yml +0 -29
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/commands/search-intelligently.md +0 -9
- package/.opencode/opencode.json +0 -20
- package/.opencode/skills/intellisearch/.version +0 -1
- package/.opencode/skills/intellisearch/SKILL.md +0 -84
- package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
- package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
- package/.opencode/skills/intellisearch/references/examples.md +0 -97
- package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
- package/.opencode/skills/intellisearch/references/google-search.md +0 -40
- package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
- package/.opencode/skills/intellisearch/references/workflow.md +0 -115
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.serena/project.yml +0 -133
- package/AGENTS.md +0 -136
- package/CHANGELOG.md +0 -3
- package/bun.lock +0 -105
- package/docs/INSTALL.md +0 -185
- package/docs/PLAYWRIGHT_CLI_API.md +0 -306
- package/docs/python-release-for-reference.md +0 -175
- package/examples/sample_json_export.json +0 -32
- package/examples/sample_markdown_export.md +0 -221
- package/examples/usage_examples.sh +0 -222
- package/install.ps1 +0 -264
- package/install.sh +0 -275
- package/openspec/MAESTRO_MIGRATION.md +0 -180
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
- package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
- package/openspec/changes/commander-cli-parser/design.md +0 -107
- package/openspec/changes/commander-cli-parser/proposal.md +0 -31
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
- package/openspec/changes/commander-cli-parser/tasks.md +0 -33
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
- package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
- package/openspec/config.yaml +0 -20
- package/openspec/specs/auth/spec.md +0 -394
- package/openspec/specs/cli/spec.md +0 -108
- package/openspec/specs/commands/spec.md +0 -409
- package/openspec/specs/configuration/spec.md +0 -130
- package/openspec/specs/conversations/spec.md +0 -139
- package/openspec/specs/domain-model/spec.md +0 -128
- package/openspec/specs/formatters/spec.md +0 -158
- package/openspec/specs/install-browser/spec.md +0 -146
- package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
- package/openspec/specs/logger/spec.md +0 -71
- package/openspec/specs/mediator/spec.md +0 -72
- package/openspec/specs/multi-profile-conversations/spec.md +0 -145
- package/openspec/specs/path-utils/spec.md +0 -101
- package/openspec/specs/profiles/spec.md +0 -94
- package/openspec/specs/storage/spec.md +0 -198
- package/openspec/specs/testing/spec.md +0 -237
- package/openspec/specs/v2-installer/spec.md +0 -205
- package/openspec/specs/validators/spec.md +0 -65
- package/scripts/build.ts +0 -88
- package/scripts/clean-build.ps1 +0 -4
- package/scripts/clean-build.sh +0 -5
- package/scripts/install-browser.ps1 +0 -5
- package/scripts/install-browser.sh +0 -6
- package/scripts/lint-path-mediation.ps1 +0 -65
- package/scripts/lint-path-mediation.sh +0 -40
- package/tests/cli/auth-command.test.ts +0 -220
- package/tests/cli/command-registry.test.ts +0 -67
- package/tests/cli/continue-command.test.ts +0 -100
- package/tests/cli/delete-command.test.ts +0 -128
- package/tests/cli/export-all-command.test.ts +0 -375
- package/tests/cli/install-browser-command.test.ts +0 -48
- package/tests/cli/list-command.test.ts +0 -179
- package/tests/cli/new-command.test.ts +0 -139
- package/tests/cli/status-command.test.ts +0 -106
- package/tests/core/query-handlers.test.ts +0 -187
- package/tests/fixtures/auth-fixtures.ts +0 -98
- package/tests/fixtures/chat-fixtures.ts +0 -87
- package/tests/infrastructure/cli-table.test.ts +0 -107
- package/tests/infrastructure/formatters.test.ts +0 -202
- package/tests/infrastructure/io.test.ts +0 -197
- package/tests/infrastructure/logger.test.ts +0 -74
- package/tests/infrastructure/storage.test.ts +0 -335
- package/tests/infrastructure/validators.test.ts +0 -67
- package/tests/integration/commands/auth.test.ts +0 -186
- package/tests/integration/commands/continue.test.ts +0 -130
- package/tests/integration/commands/delete.test.ts +0 -140
- package/tests/integration/commands/export.test.ts +0 -266
- package/tests/integration/commands/fetch.test.ts +0 -238
- package/tests/integration/commands/list.test.ts +0 -281
- package/tests/integration/commands/profile.test.ts +0 -258
- package/tests/integration/commands/status.test.ts +0 -223
- package/tests/integration/installer-script-shape.test.ts +0 -67
- package/tests/parity/compare-outputs.ts +0 -359
- package/tests/parity/test-commands-parity.ps1 +0 -82
- package/tests/parity/test-commands-parity.sh +0 -61
- package/tests/services/auth-service.test.ts +0 -315
- package/tests/services/cookie-monitor.test.ts +0 -229
- package/tests/services/cookie-storage-service.test.ts +0 -230
- package/tests/services/gemini-client-wrapper.test.ts +0 -688
- package/tests/services/install-browser-service.test.ts +0 -83
- package/tests/services/playwright-cli-driver.test.ts +0 -468
- package/tests/services/profile-auth-manager.test.ts +0 -334
- package/tests/services/profile-service.test.ts +0 -224
- package/tests/setup.ts +0 -95
- package/tests/smoke/smoke.test.ts +0 -52
- package/tests/unit/config.test.ts +0 -159
- package/tests/unit/errors.test.ts +0 -119
- package/tests/unit/formatters.test.ts +0 -316
- package/tests/unit/mediator.test.ts +0 -152
- package/tests/unit/path-utils.test.ts +0 -289
- package/tsconfig.json +0 -16
|
@@ -1,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
|