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,97 @@
1
+ name: behavior — mod8 login / logout / proxy routing
2
+ description: |
3
+ Tests the CLI ↔ web bridge introduced in 0.2.0.
4
+
5
+ - `mod8 logout` on a clean machine is a no-op (says "not logged in").
6
+ - Writing ~/.config/mod8/auth.json manually flips the routing decision:
7
+ built-in provider ids (anthropic/openai/google/deepseek) route through
8
+ the proxy; custom ids fall back to local providers.json.
9
+ - `mod8 logout` deletes auth.json and the routing flips back to local.
10
+ - The full secret key never appears in any CLI stdout (masking rule).
11
+
12
+ The login command itself drives an interactive paste + a real proxy ping,
13
+ so we don't exercise it here. Instead we test the predicate that powers
14
+ it: auth.json round-trip + `dev:auth-status` output.
15
+
16
+ tests:
17
+ - name: 'fresh machine — auth-status reports local mode'
18
+ shell: 'mod8 dev:auth-status'
19
+ expect:
20
+ stdout_contains:
21
+ - 'authed=false'
22
+ - 'mode=local'
23
+ stdout_omits:
24
+ - 'route id=anthropic proxy=true'
25
+
26
+ - name: 'logout on a clean machine is a quiet no-op'
27
+ shell: 'mod8 logout'
28
+ expect:
29
+ stdout_contains:
30
+ - 'Not logged in'
31
+
32
+ - name: 'writing auth.json flips routing to proxy for the 4 built-ins'
33
+ setup:
34
+ - shell: |
35
+ mkdir -p $MOD8_CONFIG_DIR
36
+ cat > $MOD8_CONFIG_DIR/auth.json <<JSON
37
+ {
38
+ "mod8Key": "sk-mod8-FAKE-TEST-KEY-DOES-NOT-WORK-ANYWHERE",
39
+ "proxyUrl": "https://example.invalid",
40
+ "email": "test@example.com"
41
+ }
42
+ JSON
43
+ chmod 600 $MOD8_CONFIG_DIR/auth.json
44
+ shell: 'mod8 dev:auth-status'
45
+ expect:
46
+ stdout_contains:
47
+ - 'authed=true'
48
+ - 'mode=proxy'
49
+ - 'email=test@example.com'
50
+ - 'proxyUrl=https://example.invalid'
51
+ - 'route id=anthropic proxy=true'
52
+ - 'route id=openai proxy=true'
53
+ - 'route id=google proxy=true'
54
+ - 'route id=deepseek proxy=true'
55
+ - 'route id=mistral proxy=false'
56
+ - 'route id=custom-foo proxy=false'
57
+ stdout_omits:
58
+ # Never leak the full plaintext key — the masked variant is fine.
59
+ - 'sk-mod8-FAKE-TEST-KEY-DOES-NOT-WORK-ANYWHERE'
60
+
61
+ - name: '`mod8 logout` after a login deletes auth.json + flips back to local'
62
+ setup:
63
+ - shell: |
64
+ mkdir -p $MOD8_CONFIG_DIR
65
+ cat > $MOD8_CONFIG_DIR/auth.json <<JSON
66
+ {"mod8Key":"sk-mod8-x","proxyUrl":"https://example.invalid"}
67
+ JSON
68
+ chmod 600 $MOD8_CONFIG_DIR/auth.json
69
+ shell: |
70
+ mod8 logout
71
+ echo "---after---"
72
+ mod8 dev:auth-status
73
+ echo "---file---"
74
+ ls $MOD8_CONFIG_DIR/auth.json 2>&1 || echo "auth.json deleted"
75
+ expect:
76
+ stdout_contains:
77
+ - 'Logged out'
78
+ - 'authed=false'
79
+ - 'mode=local'
80
+ - 'auth.json deleted'
81
+
82
+ - name: 'MOD8_PROXY_URL env overrides auth.json proxyUrl'
83
+ setup:
84
+ - shell: |
85
+ mkdir -p $MOD8_CONFIG_DIR
86
+ cat > $MOD8_CONFIG_DIR/auth.json <<JSON
87
+ {"mod8Key":"sk-mod8-x","proxyUrl":"https://from-file.example"}
88
+ JSON
89
+ chmod 600 $MOD8_CONFIG_DIR/auth.json
90
+ shell: 'MOD8_PROXY_URL=https://from-env.example mod8 dev:auth-status'
91
+ expect:
92
+ stdout_contains:
93
+ # dev:auth-status prints the proxyUrl from auth.json verbatim; the
94
+ # env override is consumed downstream at request time. Test the
95
+ # stored value here; the chat/proxy code path picks the env one
96
+ # via `effectiveProxyUrl`.
97
+ - 'proxyUrl=https://from-file.example'
@@ -0,0 +1,80 @@
1
+ name: behavior — no model allowlist; Google's response is what mod8 surfaces
2
+ description: |
3
+ The user reported "mod8 is rejecting gemini-2.5-flash without ever calling
4
+ Google." Forensics: there's no allowlist; the symptom was misclassified
5
+ errors making it LOOK like rejection. These specs lock in the no-allowlist
6
+ contract:
7
+
8
+ 1. dev:resolve-model produces whatever the user wrote, no scrubbing
9
+ 2. With debug logging on, the SDK URL for a fake model name shows it
10
+ reached the network — proving mod8 didn't pre-reject
11
+ 3. The provider's own response (whatever it is) is what mod8 surfaces
12
+ in errors — never replaced with a generic "model not available"
13
+
14
+ tests:
15
+ - name: 'unknown-but-user-supplied model passes through dev:resolve-model'
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": "definitely-not-a-real-model-yet",
26
+ "color": "#06B6D4"
27
+ }
28
+ }
29
+ JSON
30
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
31
+ shell: 'mod8 dev:resolve-model google'
32
+ expect:
33
+ stdout_contains:
34
+ - 'model="definitely-not-a-real-model-yet"'
35
+ stdout_omits:
36
+ # NEVER swap to a built-in default.
37
+ - 'gemini-2.0-flash'
38
+ - 'gemini-1'
39
+
40
+ - name: 'unknown model name reaches the Google SDK URL (proven without network)'
41
+ setup:
42
+ - shell: |
43
+ mkdir -p $MOD8_CONFIG_DIR
44
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
45
+ {
46
+ "google": {
47
+ "apiKey": "AIzaSy-FAKE-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAAA",
48
+ "apiType": "gemini",
49
+ "name": "Google (Gemini)",
50
+ "defaultModel": "definitely-not-a-real-model-yet",
51
+ "color": "#06B6D4"
52
+ }
53
+ }
54
+ JSON
55
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
56
+ # dev:debug-call runs the SAME resolution + URL-construction logic as
57
+ # the real provider call, just without making the network request.
58
+ # If mod8 had an allowlist, the model name would be swapped here too.
59
+ shell: 'mod8 dev:debug-call google'
60
+ expect:
61
+ stdout_contains:
62
+ - 'model="definitely-not-a-real-model-yet"'
63
+ - 'models/definitely-not-a-real-model-yet:streamGenerateContent'
64
+ stdout_omits:
65
+ # NEVER swap the user's model.
66
+ - 'gemini-2.0-flash'
67
+ # Old misclassification language must not appear.
68
+ - 'model not available'
69
+
70
+ - name: 'classifyError no longer over-matches "models/<name>" in URLs'
71
+ # The exact regression: SDK URL "models/X" + "API key not valid" was
72
+ # matching the model-not-available regex. Now classified as auth.
73
+ shell: |
74
+ 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."
75
+ expect:
76
+ stdout_contains:
77
+ - 'kind=auth'
78
+ stdout_omits:
79
+ - 'kind=model'
80
+ - 'set MOD8_GOOGLE_MODEL to override'
@@ -0,0 +1,342 @@
1
+ name: behavior — inline paste-key flow
2
+ description: |
3
+ Tests that the user can add an API key by pasting it inline in the chat,
4
+ without leaving the REPL. The contract:
5
+ - "add a key" / "i want to paste a key" / "let me add gemini" → consent
6
+ line is shown, next message is treated as a key paste.
7
+ - Pasted key → masked in the transcript (and dev:simulate stdout), full
8
+ key written to providers.json, masked or absent from session JSON.
9
+ - Non-key on the next turn → friendly rejection, no save, no trap.
10
+ - "save my work" / "let's add a feature" → NOT a paste-key intent
11
+ (false-positive guard).
12
+ - "mod8" while awaiting a key → clean cancel via parseHostBack.
13
+
14
+ All exercised through `mod8 dev:simulate`, which runs the same routing
15
+ state machine the chat REPL uses (no Ink, no LLM).
16
+
17
+ tests:
18
+ - name: '"i want to paste a key" → consent + next-turn key is saved'
19
+ shell: |
20
+ mod8 dev:simulate <<'INPUTS'
21
+ i want to paste a key
22
+ sk-ant-api03-FAKE-TEST-KEY-DOES-NOT-WORK-AAAAAAAAAAAAAAAA
23
+ INPUTS
24
+ echo "---providers.json---"
25
+ cat $MOD8_CONFIG_DIR/providers.json
26
+ expect:
27
+ stdout_contains:
28
+ - 'step=1 input="i want to paste a key" mode=host provider=host action=paste-consent'
29
+ # The pasted key is masked in stdout — never the full secret.
30
+ - 'step=2 input="sk-ant-…AAAA" mode=host provider=host action=paste-saved id=anthropic'
31
+ # And the full key DOES land in providers.json.
32
+ - '"apiKey": "sk-ant-api03-FAKE-TEST-KEY-DOES-NOT-WORK-AAAAAAAAAAAAAAAA"'
33
+ stdout_omits:
34
+ # The raw key must NEVER appear in stdout (only providers.json).
35
+ # The "apiKey" line above is the only allowed occurrence — checked
36
+ # separately by `stdout_contains`.
37
+ - 'step=2 input="sk-ant-api03-FAKE'
38
+
39
+ - name: '"add a key" + invalid pattern → friendly rejection, no save'
40
+ shell: |
41
+ mod8 dev:simulate <<'INPUTS'
42
+ add a key
43
+ hello not a key
44
+ INPUTS
45
+ echo "---providers.json---"
46
+ cat $MOD8_CONFIG_DIR/providers.json 2>/dev/null || echo "no providers.json"
47
+ expect:
48
+ stdout_contains:
49
+ - 'step=1 input="add a key" mode=host provider=host action=paste-consent'
50
+ - 'step=2 input="hello not a key" mode=host provider=host action=paste-rejected'
51
+ - 'no providers.json'
52
+
53
+ - name: '"let me add gemini" → consent target=google + AIza key saves to google'
54
+ shell: |
55
+ mod8 dev:simulate <<'INPUTS'
56
+ let me add gemini
57
+ AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA
58
+ INPUTS
59
+ echo "---providers.json---"
60
+ cat $MOD8_CONFIG_DIR/providers.json
61
+ expect:
62
+ stdout_contains:
63
+ - 'step=1 input="let me add gemini" mode=host provider=host action=paste-consent target=google'
64
+ - 'step=2 input="AIzaSy…AAAA" mode=host provider=host action=paste-saved id=google'
65
+ - '"apiKey": "AIzaSyFAKE-TEST-NOT-REAL-AAAAAAAAAAAAAAAAAAAAAA"'
66
+ - '"name": "Google (Gemini)"'
67
+
68
+ - name: '"save my work" / "lets add a feature" → NOT paste-key (false-positive guard)'
69
+ shell: |
70
+ mod8 dev:simulate <<'INPUTS'
71
+ save my work
72
+ lets add a feature
73
+ can you set the timer
74
+ put in a code review
75
+ INPUTS
76
+ expect:
77
+ stdout_contains:
78
+ # All of these should fall through to the LLM (action=send), NOT trap
79
+ # the user in a paste-key consent flow.
80
+ - 'step=1 input="save my work" mode=host provider=host action=send'
81
+ - 'step=2 input="lets add a feature" mode=host provider=host action=send'
82
+ - 'step=3 input="can you set the timer" mode=host provider=host action=send'
83
+ - 'step=4 input="put in a code review" mode=host provider=host action=send'
84
+ stdout_omits:
85
+ - 'paste-consent'
86
+
87
+ - name: '"mod8" while awaiting key → clean cancel via host-back-noop'
88
+ shell: |
89
+ mod8 dev:simulate <<'INPUTS'
90
+ add a key
91
+ mod8
92
+ hello
93
+ INPUTS
94
+ expect:
95
+ stdout_contains:
96
+ - 'step=1 input="add a key" mode=host provider=host action=paste-consent'
97
+ # "mod8" cancels the paste cleanly (already in host → no-op). The
98
+ # awaiting state is cleared so the next message is a normal turn.
99
+ - 'step=2 input="mod8" mode=host provider=host action=host-back-noop'
100
+ - 'step=3 input="hello" mode=host provider=host action=send'
101
+ stdout_omits:
102
+ - 'paste-rejected'
103
+ - 'paste-saved'
104
+
105
+ - name: 'multiple paste flows in one session — each saves the right provider'
106
+ shell: |
107
+ mod8 dev:simulate <<'INPUTS'
108
+ add a key
109
+ sk-ant-api03-CLAUDE-KEY-AAAAAAAAAAAAAAAAAAAAAAAA
110
+ paste my groq key
111
+ gsk_FAKETESTAAAAAAAAAAAAAAAAAAAAAAAAAAAA
112
+ let me add xai
113
+ xai-FAKETESTAAAAAAAAAAAAAAAAAAAAAAAAAAAA
114
+ INPUTS
115
+ echo "---ids---"
116
+ node -e "const fs = require('fs'); const p = JSON.parse(fs.readFileSync(process.env.MOD8_CONFIG_DIR + '/providers.json', 'utf8')); console.log(Object.keys(p).sort().join(','));"
117
+ expect:
118
+ stdout_contains:
119
+ - 'action=paste-saved id=anthropic'
120
+ - 'action=paste-saved id=groq'
121
+ - 'action=paste-saved id=xai'
122
+ - 'anthropic,groq,xai'
123
+ stdout_omits:
124
+ # No raw key prefixes leaked.
125
+ - 'sk-ant-api03-CLAUDE'
126
+ - 'gsk_FAKETEST'
127
+ - 'xai-FAKETEST'
128
+
129
+ - name: 'unknown key prefix → rejection (not silent acceptance)'
130
+ shell: |
131
+ mod8 dev:simulate <<'INPUTS'
132
+ i want to add a key
133
+ this-is-some-random-string-without-a-known-prefix-AAAAAAAA
134
+ INPUTS
135
+ echo "---providers.json---"
136
+ cat $MOD8_CONFIG_DIR/providers.json 2>/dev/null || echo "no providers.json"
137
+ expect:
138
+ stdout_contains:
139
+ - 'action=paste-consent'
140
+ - 'action=paste-rejected'
141
+ - 'no providers.json'
142
+
143
+ - name: 'out-of-band key paste (no consent) — masked in stdout, NOT saved'
144
+ shell: |
145
+ mod8 dev:simulate <<'INPUTS'
146
+ here is my key sk-ant-api03-OUT-OF-BAND-FAKE-AAAAAAAAAAAAAA
147
+ INPUTS
148
+ echo "---providers.json---"
149
+ cat $MOD8_CONFIG_DIR/providers.json 2>/dev/null || echo "no providers.json"
150
+ expect:
151
+ stdout_contains:
152
+ # Falls through as a plain message; key is masked in dev:simulate stdout
153
+ # so it never lands in scrollback / spec output.
154
+ - 'sk-ant-…AAAA'
155
+ - 'no providers.json'
156
+ stdout_omits:
157
+ - 'sk-ant-api03-OUT-OF-BAND'
158
+
159
+ - name: 'change/update/replace/rotate/swap verbs trigger consent (was missing — host LLM lectured instead)'
160
+ setup:
161
+ - shell: |
162
+ mkdir -p $MOD8_CONFIG_DIR
163
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
164
+ {
165
+ "google": {"apiKey":"x","apiType":"gemini","name":"Google (Gemini)","defaultModel":"gemini-2.5-flash","color":"#06B6D4"},
166
+ "anthropic": {"apiKey":"x","apiType":"anthropic","name":"Anthropic (Claude)","defaultModel":"claude-sonnet-4-6","color":"#A78BFA"},
167
+ "openai": {"apiKey":"x","apiType":"openai-compat","name":"codex","baseUrl":"https://api.openai.com/v1","defaultModel":"gpt-4o","color":"#10B981"}
168
+ }
169
+ JSON
170
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
171
+ shell: |
172
+ for msg in "lets change the google key" "update my anthropic key" "replace the openai key" "rotate google key" "swap out the gemini key" "renew the anthropic key" "switch the gemini key"; do
173
+ echo "--- $msg ---"
174
+ mod8 dev:simulate <<EOF
175
+ $msg
176
+ EOF
177
+ done
178
+ expect:
179
+ stdout_contains:
180
+ - 'input="lets change the google key" mode=host provider=host action=paste-consent target=google'
181
+ - 'input="update my anthropic key" mode=host provider=host action=paste-consent target=anthropic'
182
+ - 'input="replace the openai key" mode=host provider=host action=paste-consent target=openai'
183
+ - 'input="rotate google key" mode=host provider=host action=paste-consent target=google'
184
+ - 'input="swap out the gemini key" mode=host provider=host action=paste-consent target=google'
185
+ - 'input="renew the anthropic key" mode=host provider=host action=paste-consent target=anthropic'
186
+ - 'input="switch the gemini key" mode=host provider=host action=paste-consent target=google'
187
+ stdout_omits:
188
+ - 'action=send'
189
+
190
+ - name: 'typo-tolerant key noun — "kew"/"kee"/"keey" still trigger consent'
191
+ setup:
192
+ - shell: |
193
+ mkdir -p $MOD8_CONFIG_DIR
194
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
195
+ {
196
+ "google": {"apiKey":"x","apiType":"gemini","name":"Google (Gemini)","defaultModel":"gemini-2.5-flash","color":"#06B6D4"},
197
+ "anthropic": {"apiKey":"x","apiType":"anthropic","name":"Anthropic (Claude)","defaultModel":"claude-sonnet-4-6","color":"#A78BFA"},
198
+ "openai": {"apiKey":"x","apiType":"openai-compat","name":"codex","baseUrl":"https://api.openai.com/v1","defaultModel":"gpt-4o","color":"#10B981"}
199
+ }
200
+ JSON
201
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
202
+ shell: |
203
+ for msg in "i want to change the google kew" "change the google kew" "update my anthropic kee" "rotate the openai keey" "change my kew" "rotate the kee"; do
204
+ echo "--- $msg ---"
205
+ mod8 dev:simulate <<EOF
206
+ $msg
207
+ EOF
208
+ done
209
+ expect:
210
+ stdout_contains:
211
+ - 'input="i want to change the google kew" mode=host provider=host action=paste-consent target=google'
212
+ - 'input="change the google kew" mode=host provider=host action=paste-consent target=google'
213
+ - 'input="update my anthropic kee" mode=host provider=host action=paste-consent target=anthropic'
214
+ - 'input="rotate the openai keey" mode=host provider=host action=paste-consent target=openai'
215
+ # No provider — generic consent (no target= field).
216
+ - 'input="change my kew" mode=host provider=host action=paste-consent rest=""'
217
+ - 'input="rotate the kee" mode=host provider=host action=paste-consent rest=""'
218
+ stdout_omits:
219
+ - 'action=send'
220
+
221
+ - name: 'typo guards — non-key trailing words DO NOT trigger consent'
222
+ shell: |
223
+ for msg in "change google account password" "rotate the logs" "change the file" "update the readme" "swap out the cables"; do
224
+ echo "--- $msg ---"
225
+ mod8 dev:simulate <<EOF
226
+ $msg
227
+ EOF
228
+ done
229
+ expect:
230
+ # All five must fall through to LLM (action=send) — none of the
231
+ # trailing words fuzzy-matches "key" / "credentials" / "secret".
232
+ stdout_contains:
233
+ - 'input="change google account password" mode=host provider=host action=send'
234
+ - 'input="rotate the logs" mode=host provider=host action=send'
235
+ - 'input="change the file" mode=host provider=host action=send'
236
+ - 'input="update the readme" mode=host provider=host action=send'
237
+ - 'input="swap out the cables" mode=host provider=host action=send'
238
+ stdout_omits:
239
+ - 'paste-consent'
240
+
241
+ - name: 'change/update/etc DO NOT collide with provider routing'
242
+ setup:
243
+ - shell: |
244
+ mkdir -p $MOD8_CONFIG_DIR
245
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
246
+ {
247
+ "openai": {"apiKey":"x","apiType":"openai-compat","name":"codex","baseUrl":"https://api.openai.com/v1","defaultModel":"gpt-4o","color":"#10B981"}
248
+ }
249
+ JSON
250
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
251
+ shell: |
252
+ mod8 dev:simulate <<'INPUTS'
253
+ switch to codex
254
+ switch back
255
+ use codex
256
+ INPUTS
257
+ expect:
258
+ stdout_contains:
259
+ # "switch to codex" still routes (not paste-consent)
260
+ - 'step=1 input="switch to codex" mode=work provider=openai action=route'
261
+ # "switch back" is host-back (not paste)
262
+ - 'step=2 input="switch back" mode=host provider=host action=host-back'
263
+ # "use codex" still routes
264
+ - 'step=3 input="use codex" mode=work provider=openai action=route'
265
+
266
+ - name: 'inline paste-key PRESERVES existing defaultModel (regression: was clobbering)'
267
+ setup:
268
+ - shell: |
269
+ mkdir -p $MOD8_CONFIG_DIR
270
+ # User has google configured with a CUSTOM model (gemini-2.5-pro,
271
+ # not the registry default). When they paste a new key inline,
272
+ # mod8 used to overwrite the entry wholesale with the template
273
+ # defaults — silently downgrading them to gemini-2.0-flash.
274
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
275
+ {
276
+ "google": {
277
+ "apiKey": "OLD-KEY",
278
+ "apiType": "gemini",
279
+ "name": "Google (Gemini)",
280
+ "defaultModel": "gemini-2.5-pro",
281
+ "color": "#06B6D4"
282
+ }
283
+ }
284
+ JSON
285
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
286
+ shell: |
287
+ mod8 dev:simulate <<'INPUTS'
288
+ add a key
289
+ AIzaSy-FAKE-NEW-KEY-NOT-REAL-AAAAAAAAAAAAAAAAAA
290
+ INPUTS
291
+ echo "---providers.json---"
292
+ cat $MOD8_CONFIG_DIR/providers.json
293
+ expect:
294
+ stdout_contains:
295
+ - 'paste-saved id=google'
296
+ # New key reached disk.
297
+ - '"apiKey": "AIzaSy-FAKE-NEW-KEY-NOT-REAL-AAAAAAAAAAAAAAAAAA"'
298
+ # User's custom default model survived.
299
+ - '"defaultModel": "gemini-2.5-pro"'
300
+ stdout_omits:
301
+ # The bug we're fixing: entry got reset to the template default.
302
+ - '"defaultModel": "gemini-2.0-flash"'
303
+ - '"defaultModel": "gemini-2.5-flash"'
304
+
305
+ - name: 'inline paste-key on FRESH provider uses template default (no existing entry to preserve)'
306
+ shell: |
307
+ mod8 dev:simulate <<'INPUTS'
308
+ add a key
309
+ AIzaSy-FAKE-FRESH-KEY-NOT-REAL-AAAAAAAAAAAAAAA
310
+ INPUTS
311
+ echo "---providers.json---"
312
+ cat $MOD8_CONFIG_DIR/providers.json
313
+ expect:
314
+ stdout_contains:
315
+ - 'paste-saved id=google'
316
+ # No prior entry → use the (updated) template default.
317
+ - '"defaultModel": "gemini-2.5-flash"'
318
+
319
+ - name: 'paste-key intent recognized in many phrasings'
320
+ shell: |
321
+ mod8 dev:simulate <<'INPUTS'
322
+ add a key
323
+ paste my key
324
+ save my api key
325
+ register a key
326
+ i want to add a key
327
+ i wanna paste a key
328
+ i'd like to register a key
329
+ let me add a key
330
+ lets save a key
331
+ put in a key
332
+ INPUTS
333
+ expect:
334
+ # Every line should produce paste-consent + leave us awaiting; the very
335
+ # next line is the next phrasing, which is NOT a key, so it gets
336
+ # paste-rejected. We only assert that consent fires for every odd step.
337
+ stdout_contains:
338
+ - 'step=1 input="add a key" mode=host provider=host action=paste-consent'
339
+ - 'step=3 input="save my api key" mode=host provider=host action=paste-consent'
340
+ - 'step=5 input="i want to add a key" mode=host provider=host action=paste-consent'
341
+ - 'step=7 input="i''d like to register a key" mode=host provider=host action=paste-consent'
342
+ - 'step=9 input="lets save a key" mode=host provider=host action=paste-consent'