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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/bin/mod8.js +2 -0
- package/dist/cli.js +302 -0
- package/dist/commands/addProvider.js +105 -0
- package/dist/commands/all.js +158 -0
- package/dist/commands/chat.js +855 -0
- package/dist/commands/config.js +29 -0
- package/dist/commands/devAuthStatus.js +34 -0
- package/dist/commands/devHostAsk.js +51 -0
- package/dist/commands/devHostSystem.js +15 -0
- package/dist/commands/devResolve.js +54 -0
- package/dist/commands/devSimulate.js +235 -0
- package/dist/commands/devWorkAsk.js +55 -0
- package/dist/commands/intentRouting.js +280 -0
- package/dist/commands/keys.js +55 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/login.js +147 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/prompt.js +63 -0
- package/dist/commands/providers.js +30 -0
- package/dist/commands/verify.js +5 -0
- package/dist/input/compose.js +37 -0
- package/dist/input/files.js +49 -0
- package/dist/input/stdin.js +14 -0
- package/dist/providers/anthropic.js +115 -0
- package/dist/providers/displayName.js +25 -0
- package/dist/providers/errorHints.js +175 -0
- package/dist/providers/generic.js +331 -0
- package/dist/providers/genericChat.js +265 -0
- package/dist/providers/google.js +63 -0
- package/dist/providers/hostSystem.js +173 -0
- package/dist/providers/index.js +38 -0
- package/dist/providers/mock.js +87 -0
- package/dist/providers/modelResolution.js +42 -0
- package/dist/providers/openai.js +75 -0
- package/dist/providers/pricing.js +47 -0
- package/dist/providers/proxy.js +148 -0
- package/dist/providers/registry.js +196 -0
- package/dist/providers/types.js +1 -0
- package/dist/providers/workSystem.js +33 -0
- package/dist/storage/auth.js +65 -0
- package/dist/storage/config.js +35 -0
- package/dist/storage/keys.js +59 -0
- package/dist/storage/providers.js +337 -0
- package/dist/storage/sessions.js +150 -0
- package/dist/types.js +9 -0
- package/dist/util/debug.js +79 -0
- package/dist/util/errors.js +157 -0
- package/dist/util/prompt.js +111 -0
- package/dist/util/secrets.js +110 -0
- package/dist/util/text.js +53 -0
- package/dist/util/time.js +25 -0
- package/dist/verify/runner.js +437 -0
- package/package.json +69 -0
- package/specs/all-mode.yaml +44 -0
- package/specs/behavior/auto-fallback.yaml +49 -0
- package/specs/behavior/bare-name-routing.yaml +223 -0
- package/specs/behavior/bare-paste-confirm.yaml +125 -0
- package/specs/behavior/env-var-respected.yaml +108 -0
- package/specs/behavior/error-fidelity.yaml +92 -0
- package/specs/behavior/error-hints.yaml +160 -0
- package/specs/behavior/fresh-vs-resume.yaml +94 -0
- package/specs/behavior/fuzzy-match.yaml +208 -0
- package/specs/behavior/host-self-knowledge-fresh.yaml +66 -0
- package/specs/behavior/intent-no-mismatch.yaml +115 -0
- package/specs/behavior/login-logout.yaml +97 -0
- package/specs/behavior/no-model-allowlist.yaml +80 -0
- package/specs/behavior/paste-key.yaml +342 -0
- package/specs/behavior/provider-switching.yaml +186 -0
- package/specs/behavior/providers-json-respected.yaml +106 -0
- package/specs/behavior/self-knowledge.yaml +119 -0
- package/specs/behavior/stress-session.yaml +226 -0
- package/specs/behavior/switch-back-when-failing.yaml +90 -0
- package/specs/behavior/work-character.yaml +109 -0
- package/specs/chat-meta.yaml +349 -0
- package/specs/chat-startup.yaml +148 -0
- package/specs/chat.yaml +91 -0
- package/specs/config.yaml +42 -0
- package/specs/install.yaml +112 -0
- package/specs/keys.yaml +81 -0
- package/specs/one-shot.yaml +65 -0
- package/specs/pipe-and-files.yaml +40 -0
- package/specs/providers.yaml +172 -0
- 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'
|