mod8-cli 0.2.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 (86) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +239 -0
  4. package/bin/mod8.js +2 -0
  5. package/dist/cli.js +302 -0
  6. package/dist/commands/addProvider.js +105 -0
  7. package/dist/commands/all.js +158 -0
  8. package/dist/commands/chat.js +855 -0
  9. package/dist/commands/config.js +29 -0
  10. package/dist/commands/devAuthStatus.js +34 -0
  11. package/dist/commands/devHostAsk.js +51 -0
  12. package/dist/commands/devHostSystem.js +15 -0
  13. package/dist/commands/devResolve.js +54 -0
  14. package/dist/commands/devSimulate.js +235 -0
  15. package/dist/commands/devWorkAsk.js +55 -0
  16. package/dist/commands/intentRouting.js +280 -0
  17. package/dist/commands/keys.js +55 -0
  18. package/dist/commands/list.js +27 -0
  19. package/dist/commands/login.js +147 -0
  20. package/dist/commands/logout.js +17 -0
  21. package/dist/commands/prompt.js +63 -0
  22. package/dist/commands/providers.js +30 -0
  23. package/dist/commands/verify.js +5 -0
  24. package/dist/input/compose.js +37 -0
  25. package/dist/input/files.js +49 -0
  26. package/dist/input/stdin.js +14 -0
  27. package/dist/providers/anthropic.js +115 -0
  28. package/dist/providers/displayName.js +25 -0
  29. package/dist/providers/errorHints.js +175 -0
  30. package/dist/providers/generic.js +331 -0
  31. package/dist/providers/genericChat.js +265 -0
  32. package/dist/providers/google.js +63 -0
  33. package/dist/providers/hostSystem.js +173 -0
  34. package/dist/providers/index.js +38 -0
  35. package/dist/providers/mock.js +87 -0
  36. package/dist/providers/modelResolution.js +42 -0
  37. package/dist/providers/openai.js +75 -0
  38. package/dist/providers/pricing.js +47 -0
  39. package/dist/providers/proxy.js +148 -0
  40. package/dist/providers/registry.js +196 -0
  41. package/dist/providers/types.js +1 -0
  42. package/dist/providers/workSystem.js +33 -0
  43. package/dist/storage/auth.js +65 -0
  44. package/dist/storage/config.js +35 -0
  45. package/dist/storage/keys.js +59 -0
  46. package/dist/storage/providers.js +337 -0
  47. package/dist/storage/sessions.js +150 -0
  48. package/dist/types.js +9 -0
  49. package/dist/util/debug.js +79 -0
  50. package/dist/util/errors.js +157 -0
  51. package/dist/util/prompt.js +111 -0
  52. package/dist/util/secrets.js +110 -0
  53. package/dist/util/text.js +53 -0
  54. package/dist/util/time.js +25 -0
  55. package/dist/verify/runner.js +437 -0
  56. package/package.json +69 -0
  57. package/specs/all-mode.yaml +44 -0
  58. package/specs/behavior/auto-fallback.yaml +49 -0
  59. package/specs/behavior/bare-name-routing.yaml +223 -0
  60. package/specs/behavior/bare-paste-confirm.yaml +125 -0
  61. package/specs/behavior/env-var-respected.yaml +108 -0
  62. package/specs/behavior/error-fidelity.yaml +92 -0
  63. package/specs/behavior/error-hints.yaml +160 -0
  64. package/specs/behavior/fresh-vs-resume.yaml +94 -0
  65. package/specs/behavior/fuzzy-match.yaml +208 -0
  66. package/specs/behavior/host-self-knowledge-fresh.yaml +66 -0
  67. package/specs/behavior/intent-no-mismatch.yaml +115 -0
  68. package/specs/behavior/login-logout.yaml +97 -0
  69. package/specs/behavior/no-model-allowlist.yaml +80 -0
  70. package/specs/behavior/paste-key.yaml +342 -0
  71. package/specs/behavior/provider-switching.yaml +186 -0
  72. package/specs/behavior/providers-json-respected.yaml +106 -0
  73. package/specs/behavior/self-knowledge.yaml +119 -0
  74. package/specs/behavior/stress-session.yaml +226 -0
  75. package/specs/behavior/switch-back-when-failing.yaml +90 -0
  76. package/specs/behavior/work-character.yaml +109 -0
  77. package/specs/chat-meta.yaml +349 -0
  78. package/specs/chat-startup.yaml +148 -0
  79. package/specs/chat.yaml +91 -0
  80. package/specs/config.yaml +42 -0
  81. package/specs/install.yaml +112 -0
  82. package/specs/keys.yaml +81 -0
  83. package/specs/one-shot.yaml +65 -0
  84. package/specs/pipe-and-files.yaml +40 -0
  85. package/specs/providers.yaml +172 -0
  86. package/specs/sessions.yaml +115 -0
@@ -0,0 +1,223 @@
1
+ name: behavior — bare name & greeting routing
2
+ description: |
3
+ Pins the fix for "user names a provider but mod8 only lectures" — the
4
+ intent router now catches:
5
+ - bare provider name ("codex")
6
+ - first word + remainder ("codex tell me a joke")
7
+ - greeting + name ("hi codex", "hey gpt how are you")
8
+ …all locally, before the host LLM gets a chance to lecture.
9
+
10
+ Strict resolution (id or configured display-name only) for bare/first-word
11
+ prevents false positives like "haiku" or "claude alone is great". Greeting
12
+ matching is more permissive because the greeting itself signals intent.
13
+
14
+ tests:
15
+ # --- bare name ---
16
+
17
+ - name: 'bare "codex" (configured display name) routes to openai'
18
+ setup:
19
+ - shell: |
20
+ mkdir -p $MOD8_CONFIG_DIR
21
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
22
+ {
23
+ "openai": {
24
+ "apiKey": "sk-fake",
25
+ "apiType": "openai-compat",
26
+ "name": "codex",
27
+ "baseUrl": "https://api.openai.com/v1",
28
+ "defaultModel": "gpt-4o",
29
+ "color": "#10B981"
30
+ }
31
+ }
32
+ JSON
33
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
34
+ shell: 'mod8 dev:resolve "codex"'
35
+ expect:
36
+ stdout_contains: "route id=codex resolved=openai"
37
+
38
+ - name: 'bare "Codex" (capitalized) is case-insensitive'
39
+ setup:
40
+ - shell: |
41
+ mkdir -p $MOD8_CONFIG_DIR
42
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
43
+ {
44
+ "openai": {
45
+ "apiKey": "sk-fake",
46
+ "apiType": "openai-compat",
47
+ "name": "codex",
48
+ "baseUrl": "https://api.openai.com/v1",
49
+ "defaultModel": "gpt-4o",
50
+ "color": "#10B981"
51
+ }
52
+ }
53
+ JSON
54
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
55
+ shell: 'mod8 dev:resolve "Codex"'
56
+ expect:
57
+ stdout_contains: "route id=codex resolved=openai"
58
+
59
+ - name: 'bare built-in id "deepseek" routes (even when not configured)'
60
+ shell: 'mod8 dev:resolve "deepseek"'
61
+ expect:
62
+ stdout_contains: "route id=deepseek resolved=deepseek"
63
+
64
+ # --- first-word + rest ---
65
+
66
+ - name: '"codex tell me a joke" routes + carries rest="tell me a joke"'
67
+ setup:
68
+ - shell: |
69
+ mkdir -p $MOD8_CONFIG_DIR
70
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
71
+ {
72
+ "openai": {
73
+ "apiKey": "sk-fake",
74
+ "apiType": "openai-compat",
75
+ "name": "codex",
76
+ "baseUrl": "https://api.openai.com/v1",
77
+ "defaultModel": "gpt-4o",
78
+ "color": "#10B981"
79
+ }
80
+ }
81
+ JSON
82
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
83
+ shell: 'mod8 dev:resolve "codex tell me a joke"'
84
+ expect:
85
+ stdout_contains: "route id=codex resolved=openai"
86
+ stdout_matches: 'rest="tell me a joke"'
87
+
88
+ - name: '"codex codex" switches once, sends "codex" as rest (no double-route)'
89
+ setup:
90
+ - shell: |
91
+ mkdir -p $MOD8_CONFIG_DIR
92
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
93
+ {
94
+ "openai": {
95
+ "apiKey": "sk-fake",
96
+ "apiType": "openai-compat",
97
+ "name": "codex",
98
+ "baseUrl": "https://api.openai.com/v1",
99
+ "defaultModel": "gpt-4o",
100
+ "color": "#10B981"
101
+ }
102
+ }
103
+ JSON
104
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
105
+ shell: 'mod8 dev:resolve "codex codex"'
106
+ expect:
107
+ stdout_contains: "route id=codex resolved=openai"
108
+ stdout_matches: 'rest="codex"'
109
+
110
+ # --- greeting + name (full resolution including synonyms) ---
111
+
112
+ - name: '"hi codex" routes + forwards "hi" as the first message'
113
+ setup:
114
+ - shell: |
115
+ mkdir -p $MOD8_CONFIG_DIR
116
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
117
+ {
118
+ "openai": {
119
+ "apiKey": "sk-fake",
120
+ "apiType": "openai-compat",
121
+ "name": "codex",
122
+ "baseUrl": "https://api.openai.com/v1",
123
+ "defaultModel": "gpt-4o",
124
+ "color": "#10B981"
125
+ }
126
+ }
127
+ JSON
128
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
129
+ shell: 'mod8 dev:resolve "hi codex"'
130
+ expect:
131
+ stdout_contains: "route id=codex resolved=openai"
132
+ stdout_matches: 'rest="hi"'
133
+
134
+ - name: '"hey codex how are you" routes + forwards "hey how are you"'
135
+ setup:
136
+ - shell: |
137
+ mkdir -p $MOD8_CONFIG_DIR
138
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
139
+ {
140
+ "openai": {
141
+ "apiKey": "sk-fake",
142
+ "apiType": "openai-compat",
143
+ "name": "codex",
144
+ "baseUrl": "https://api.openai.com/v1",
145
+ "defaultModel": "gpt-4o",
146
+ "color": "#10B981"
147
+ }
148
+ }
149
+ JSON
150
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
151
+ shell: 'mod8 dev:resolve "hey codex how are you"'
152
+ expect:
153
+ stdout_contains: "route id=codex resolved=openai"
154
+ stdout_matches: 'rest="hey how are you"'
155
+
156
+ - name: '"yo gpt" routes via greeting + synonym (gpt → openai)'
157
+ shell: 'mod8 dev:resolve "yo gpt"'
158
+ expect:
159
+ stdout_contains: "route id=gpt resolved=openai"
160
+ stdout_matches: 'rest="yo"'
161
+
162
+ - name: '"hello claude" routes via greeting + synonym (claude → anthropic)'
163
+ shell: 'mod8 dev:resolve "hello claude"'
164
+ expect:
165
+ stdout_contains: "route id=claude resolved=anthropic"
166
+
167
+ # --- false-positive avoidance ---
168
+
169
+ - name: 'bare "haiku" does NOT route (synonym, but bare-mode is strict)'
170
+ shell: 'mod8 dev:resolve "haiku"'
171
+ expect:
172
+ stdout_contains: "none"
173
+
174
+ - name: 'bare "claude" routes to anthropic (high-confidence brand alias)'
175
+ shell: 'mod8 dev:resolve "claude"'
176
+ expect:
177
+ stdout_contains: "route id=claude resolved=anthropic"
178
+
179
+ - name: 'bare "gpt" routes to openai (high-confidence brand alias)'
180
+ shell: 'mod8 dev:resolve "gpt"'
181
+ expect:
182
+ stdout_contains: "route id=gpt resolved=openai"
183
+
184
+ - name: 'bare "grok" routes to xai (high-confidence brand alias)'
185
+ shell: 'mod8 dev:resolve "grok"'
186
+ expect:
187
+ stdout_contains: "route id=grok resolved=xai"
188
+
189
+ - name: '"claude please" routes + carries rest="please"'
190
+ shell: 'mod8 dev:resolve "claude please"'
191
+ expect:
192
+ stdout_contains: "route id=claude resolved=anthropic"
193
+ stdout_matches: 'rest="please"'
194
+
195
+ - name: '"gpt help me with this" routes + carries rest'
196
+ shell: 'mod8 dev:resolve "gpt help me with this"'
197
+ expect:
198
+ stdout_contains: "route id=gpt resolved=openai"
199
+ stdout_matches: 'rest="help me with this"'
200
+
201
+ - name: '"hello world" does NOT route ("world" is not a provider)'
202
+ shell: 'mod8 dev:resolve "hello world"'
203
+ expect:
204
+ stdout_contains: "none"
205
+
206
+ - name: '"hi there" does NOT route ("there" is not a provider)'
207
+ shell: 'mod8 dev:resolve "hi there"'
208
+ expect:
209
+ stdout_contains: "none"
210
+
211
+ - name: 'multi-word non-route message stays plain ("write me a haiku")'
212
+ shell: 'mod8 dev:resolve "write me a haiku"'
213
+ expect:
214
+ stdout_contains: "none"
215
+
216
+ # --- priority: host-back still wins over bare-name ---
217
+
218
+ - name: 'bare "mod8" goes to host-back, not provider routing'
219
+ shell: 'mod8 dev:resolve "mod8"'
220
+ expect:
221
+ stdout_contains: "host-back"
222
+ stdout_omits:
223
+ - "route id="
@@ -0,0 +1,125 @@
1
+ name: behavior — bare-paste auto-detect (no consent first)
2
+ description: |
3
+ When the user pastes a recognizable API key into chat WITHOUT first saying
4
+ "add a key", mod8 should NOT pass the message to the host LLM (which then
5
+ lectures about `mod8 keys set <id>`). Instead:
6
+
7
+ 1. Detect the key locally (sk-ant-/sk-or-/AIza/etc.).
8
+ 2. Mask it for display.
9
+ 3. Cache the raw key in memory only (NEVER session JSON).
10
+ 4. Ask "Save it as `<id>`? (yes / no)".
11
+ 5. Affirmative → save to providers.json, confirm.
12
+ Negative → discard, acknowledge.
13
+ Anything else → discard, fall through to normal dispatch (no trap).
14
+
15
+ Specs cover the happy path, the broadened paste-key intent regex (which
16
+ now accepts "this/that/these/those/it/them" as articles and the bare-
17
+ pronoun forms "save this", "use it", etc.), and the cancellation paths.
18
+
19
+ tests:
20
+ - name: 'bare paste of anthropic key → asks "save as anthropic?"'
21
+ shell: |
22
+ mod8 dev:simulate <<'INPUTS'
23
+ sk-ant-api03-FAKE-TEST-AAAAAAAAAAAAAAAAAAAAAAAA
24
+ yes
25
+ INPUTS
26
+ echo "---providers.json---"
27
+ cat $MOD8_CONFIG_DIR/providers.json
28
+ expect:
29
+ stdout_contains:
30
+ # Step 1: bare paste detected, key masked in stdout, pending state armed.
31
+ - 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
32
+ # Step 2: user confirms with "yes" → key is saved.
33
+ - 'step=2 input="yes" mode=host provider=host action=paste-saved id=anthropic'
34
+ # And the full key DOES land in providers.json.
35
+ - '"apiKey": "sk-ant-api03-FAKE-TEST-AAAAAAAAAAAAAAAAAAAAAAAA"'
36
+ stdout_omits:
37
+ # The raw key must never appear in stdout (only providers.json).
38
+ - 'step=1 input="sk-ant-api03-FAKE'
39
+
40
+ - name: '"save it" / "save this" / "use it" all trigger save in pendingKey state'
41
+ shell: |
42
+ mod8 dev:simulate <<'INPUTS'
43
+ sk-ant-api03-FAKE-KEY-1-AAAAAAAAAAAAAAAAAAAA
44
+ save it
45
+ INPUTS
46
+ echo "---providers.json---"
47
+ cat $MOD8_CONFIG_DIR/providers.json
48
+ expect:
49
+ stdout_contains:
50
+ - 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
51
+ - 'step=2 input="save it" mode=host provider=host action=paste-saved id=anthropic'
52
+ - '"apiKey": "sk-ant-api03-FAKE-KEY-1-AAAAAAAAAAAAAAAAAAAA"'
53
+
54
+ - name: 'broadened intent — "add this key!" triggers consent (was rejected before)'
55
+ shell: |
56
+ mod8 dev:simulate <<'INPUTS'
57
+ add this key!
58
+ INPUTS
59
+ expect:
60
+ stdout_contains:
61
+ - 'step=1 input="add this key!" mode=host provider=host action=paste-consent'
62
+
63
+ - name: '"no" cancels the bare paste and discards the key'
64
+ shell: |
65
+ mod8 dev:simulate <<'INPUTS'
66
+ AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA
67
+ no
68
+ INPUTS
69
+ echo "---providers.json---"
70
+ cat $MOD8_CONFIG_DIR/providers.json 2>/dev/null || echo "no providers.json"
71
+ expect:
72
+ stdout_contains:
73
+ - 'step=1 input="AIzaSy…AAAA" mode=host provider=host action=paste-pending id=google'
74
+ - 'step=2 input="no" mode=host provider=host action=paste-cancelled'
75
+ - 'no providers.json'
76
+
77
+ - name: 'unrelated input cancels paste and falls through (no trap)'
78
+ setup:
79
+ - shell: |
80
+ mkdir -p $MOD8_CONFIG_DIR
81
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
82
+ {
83
+ "openai": {"apiKey":"x","apiType":"openai-compat","name":"codex","baseUrl":"https://api.openai.com/v1","defaultModel":"gpt-4o","color":"#10B981"}
84
+ }
85
+ JSON
86
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
87
+ shell: |
88
+ mod8 dev:simulate <<'INPUTS'
89
+ sk-ant-api03-FAKE-2-AAAAAAAAAAAAAAAAAAAAAAAAAAAA
90
+ use codex
91
+ INPUTS
92
+ expect:
93
+ stdout_contains:
94
+ - 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
95
+ # "use codex" is NOT an affirmative → cancel + fall through to normal
96
+ # routing (which switches to codex).
97
+ - 'step=2 input="use codex" mode=work provider=openai action=route'
98
+
99
+ - name: 'broadened article list — "let me add this anthropic key" triggers consent'
100
+ shell: |
101
+ mod8 dev:simulate <<'INPUTS'
102
+ let me add this anthropic key
103
+ INPUTS
104
+ expect:
105
+ stdout_contains:
106
+ - 'step=1 input="let me add this anthropic key" mode=host provider=host action=paste-consent'
107
+
108
+ - name: 'multiple bare pastes back-to-back each get saved'
109
+ shell: |
110
+ mod8 dev:simulate <<'INPUTS'
111
+ sk-ant-api03-CLAUDE-FAKE-AAAAAAAAAAAAAAAAAAAAAAAA
112
+ yes
113
+ AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA
114
+ save it
115
+ INPUTS
116
+ echo "---providers.json---"
117
+ cat $MOD8_CONFIG_DIR/providers.json
118
+ expect:
119
+ stdout_contains:
120
+ - 'step=1 input="sk-ant-…AAAA" mode=host provider=host action=paste-pending id=anthropic'
121
+ - 'step=2 input="yes" mode=host provider=host action=paste-saved id=anthropic'
122
+ - 'step=3 input="AIzaSy…AAAA" mode=host provider=host action=paste-pending id=google'
123
+ - 'step=4 input="save it" mode=host provider=host action=paste-saved id=google'
124
+ - '"apiKey": "sk-ant-api03-CLAUDE-FAKE-AAAAAAAAAAAAAAAAAAAAAAAA"'
125
+ - '"apiKey": "AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA"'
@@ -0,0 +1,108 @@
1
+ name: behavior — MOD8_<ID>_MODEL env var is respected
2
+ description: |
3
+ Bug: the env var advertised in the "model not available" error message
4
+ was silently ignored. The dead code in src/providers/google.ts honored
5
+ MOD8_GOOGLE_MODEL but no one imports it; the live path in generic.ts
6
+ used `opts.model ?? entry.defaultModel` and never read the env.
7
+
8
+ Fix: every provider call goes through `resolveModel()` which checks
9
+ opts.model → MOD8_<ID>_MODEL → providers.json defaultModel. These specs
10
+ prove the env var beats the providers.json default (no allowlist is
11
+ consulted along the way; whatever string the env sets is what the SDK
12
+ receives).
13
+
14
+ tests:
15
+ - name: 'MOD8_GOOGLE_MODEL beats providers.json default'
16
+ setup:
17
+ - shell: |
18
+ mkdir -p $MOD8_CONFIG_DIR
19
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
20
+ {
21
+ "google": {
22
+ "apiKey": "x",
23
+ "apiType": "gemini",
24
+ "name": "Google (Gemini)",
25
+ "defaultModel": "gemini-2.0-flash",
26
+ "color": "#06B6D4"
27
+ }
28
+ }
29
+ JSON
30
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
31
+ shell: 'MOD8_GOOGLE_MODEL=gemini-2.5-flash mod8 dev:resolve-model google'
32
+ expect:
33
+ stdout_contains:
34
+ - 'model="gemini-2.5-flash"'
35
+ - 'source=env'
36
+ - 'envVar=MOD8_GOOGLE_MODEL'
37
+
38
+ - name: 'env var passes through unchanged — even an arbitrary string'
39
+ setup:
40
+ - shell: |
41
+ mkdir -p $MOD8_CONFIG_DIR
42
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
43
+ {
44
+ "google": {"apiKey":"x","apiType":"gemini","name":"Google (Gemini)","defaultModel":"x","color":"#06B6D4"}
45
+ }
46
+ JSON
47
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
48
+ shell: 'MOD8_GOOGLE_MODEL=this-could-be-any-string-mod8-does-not-allowlist mod8 dev:resolve-model google'
49
+ expect:
50
+ stdout_contains:
51
+ - 'model="this-could-be-any-string-mod8-does-not-allowlist"'
52
+ - 'source=env'
53
+
54
+ - name: 'env var name is built from id — works for non-google providers'
55
+ setup:
56
+ - shell: |
57
+ mkdir -p $MOD8_CONFIG_DIR
58
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
59
+ {
60
+ "anthropic": {"apiKey":"x","apiType":"anthropic","name":"Anthropic","defaultModel":"claude-sonnet-4-6","color":"#A78BFA"}
61
+ }
62
+ JSON
63
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
64
+ shell: 'MOD8_ANTHROPIC_MODEL=claude-opus-4-7 mod8 dev:resolve-model anthropic'
65
+ expect:
66
+ stdout_contains:
67
+ - 'model="claude-opus-4-7"'
68
+ - 'source=env'
69
+ - 'envVar=MOD8_ANTHROPIC_MODEL'
70
+
71
+ - name: 'empty env var is ignored, falls through to providers.json'
72
+ setup:
73
+ - shell: |
74
+ mkdir -p $MOD8_CONFIG_DIR
75
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
76
+ {
77
+ "google": {"apiKey":"x","apiType":"gemini","name":"Google (Gemini)","defaultModel":"gemini-2.0-flash","color":"#06B6D4"}
78
+ }
79
+ JSON
80
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
81
+ shell: 'MOD8_GOOGLE_MODEL= mod8 dev:resolve-model google'
82
+ expect:
83
+ stdout_contains:
84
+ - 'model="gemini-2.0-flash"'
85
+ - 'source=providers.json'
86
+
87
+ - name: 'env-resolved model lands in the SDK URL the provider is about to hit'
88
+ setup:
89
+ - shell: |
90
+ mkdir -p $MOD8_CONFIG_DIR
91
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
92
+ {
93
+ "google": {"apiKey":"AIzaSy-FAKE-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAAA","apiType":"gemini","name":"Google (Gemini)","defaultModel":"gemini-2.0-flash","color":"#06B6D4"}
94
+ }
95
+ JSON
96
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
97
+ # No network — dev:debug-call runs the same resolution + URL builder as
98
+ # the actual provider call path. The URL we print here is identical
99
+ # to what the SDK will request.
100
+ shell: 'MOD8_GOOGLE_MODEL=gemini-2.5-flash mod8 dev:debug-call google'
101
+ expect:
102
+ stdout_contains:
103
+ - 'providerId=google'
104
+ - 'model="gemini-2.5-flash"'
105
+ - 'modelSource=env'
106
+ - 'models/gemini-2.5-flash:streamGenerateContent'
107
+ # API key is masked — never the raw value.
108
+ - 'key=AIzaSy…AAAA'
@@ -0,0 +1,92 @@
1
+ name: behavior — provider error message fidelity (verbatim quote)
2
+ description: |
3
+ Bug: when a provider returned a specific error, mod8 dropped the message
4
+ and printed something generic ("model not available — set MOD8_..." for
5
+ what was actually a 400 / API_KEY_INVALID; "free-tier quota / billing"
6
+ for what was actually a 403 project block).
7
+
8
+ Fix: every error preserves the provider's raw human-readable message in
9
+ the short summary, with prefix noise (`[GoogleGenerativeAI Error]:`,
10
+ `Error fetching from <url>:`, structured-detail JSON tail) stripped.
11
+ The user can copy the quoted message and search for it.
12
+
13
+ These specs feed synthetic SDK-shape errors through dev:explain-error
14
+ and assert the verbatim quote survives + the diagnosis matches reality.
15
+
16
+ tests:
17
+ - name: 'Google "no longer available" model error → kind=model + verbatim quote'
18
+ shell: |
19
+ mod8 dev:explain-error google "[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent: [404 Not Found] The model gemini-pro-vision is no longer available."
20
+ expect:
21
+ stdout_contains:
22
+ - 'kind=model'
23
+ - 'no longer available'
24
+ # The quoted raw text — survives extractRawMessage and stays
25
+ # search-friendly. Must NOT include the SDK wrapper prefix.
26
+ - "'The model gemini-pro-vision is no longer available.'"
27
+ stdout_omits:
28
+ - 'rate limited'
29
+ - 'set MOD8_GOOGLE_MODEL'
30
+ - 'GoogleGenerativeAI Error'
31
+
32
+ - name: 'Google "API key not valid" → kind=auth (NOT kind=model)'
33
+ # The exact misclassification the user reported: URL contains "models/<name>"
34
+ # AND message contains "valid", and the old `model.*invalid` regex matched.
35
+ shell: |
36
+ mod8 dev:explain-error google "[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent: [400 Bad Request] API key not valid. Please pass a valid API key."
37
+ expect:
38
+ stdout_contains:
39
+ - 'kind=auth'
40
+ - "'API key not valid. Please pass a valid API key.'"
41
+ stdout_omits:
42
+ - 'kind=model'
43
+ - 'set MOD8_GOOGLE_MODEL'
44
+
45
+ - name: '403 project denied → kind=forbidden + verbatim quote (no fake billing diagnosis)'
46
+ shell: |
47
+ mod8 dev:explain-error google "[403 Forbidden] Your project has been denied access. Please contact support."
48
+ expect:
49
+ stdout_contains:
50
+ - 'kind=forbidden'
51
+ - "'Your project has been denied access. Please contact support.'"
52
+ stdout_omits:
53
+ - 'kind=no-credit'
54
+ - 'free-tier quota'
55
+ - 'Top up at'
56
+
57
+ - name: '429 with retry-after preserves the delay AND quotes the raw message'
58
+ shell: |
59
+ mod8 dev:explain-error anthropic "rate_limit_exceeded: please retry after 30 seconds"
60
+ expect:
61
+ stdout_contains:
62
+ - 'kind=rate-limit'
63
+ - 'retry in 30s'
64
+ - "'rate_limit_exceeded: please retry after 30 seconds'"
65
+
66
+ - name: 'unknown shape → kind=other, raw preserved (no fabricated diagnosis)'
67
+ shell: |
68
+ mod8 dev:explain-error openai "Some completely surprising message we have not seen before"
69
+ expect:
70
+ stdout_contains:
71
+ - 'kind=other'
72
+ - 'Some completely surprising message we have not seen before'
73
+ stdout_omits:
74
+ - 'kind=auth'
75
+ - 'kind=forbidden'
76
+ - 'kind=rate-limit'
77
+ - 'kind=model'
78
+
79
+ - name: 'extractRawMessage strips Google SDK wrapper prefix'
80
+ # Specifically: "[GoogleGenerativeAI Error]: Error fetching from <url>: "
81
+ # must be removed so the user sees just the meaningful summary.
82
+ shell: |
83
+ mod8 dev:explain-error google "[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent: [400 Bad Request] API key not valid. Please pass a valid API key. [{\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\",\"reason\":\"API_KEY_INVALID\"}]"
84
+ expect:
85
+ stdout_contains:
86
+ - "'API key not valid. Please pass a valid API key.'"
87
+ stdout_omits:
88
+ # The wrapper noise must NOT survive into the user-facing quote.
89
+ - 'GoogleGenerativeAI Error'
90
+ - 'Error fetching from'
91
+ - '@type'
92
+ - 'type.googleapis.com/google.rpc.ErrorInfo'