@onmars/lunar-core 0.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/LICENSE +21 -0
- package/README.md +13 -0
- package/package.json +32 -0
- package/src/__tests__/clear-command.test.ts +214 -0
- package/src/__tests__/command-handler.test.ts +169 -0
- package/src/__tests__/compact-command.test.ts +80 -0
- package/src/__tests__/config-command.test.ts +240 -0
- package/src/__tests__/config-loader.test.ts +1512 -0
- package/src/__tests__/config.test.ts +429 -0
- package/src/__tests__/cron-command.test.ts +418 -0
- package/src/__tests__/cron-parser.test.ts +259 -0
- package/src/__tests__/daemon.test.ts +346 -0
- package/src/__tests__/dedup.test.ts +404 -0
- package/src/__tests__/e2e-sanitization.ts +168 -0
- package/src/__tests__/e2e-skill-loader.test.ts +176 -0
- package/src/__tests__/fixtures/AGENTS.md +4 -0
- package/src/__tests__/fixtures/IDENTITY.md +2 -0
- package/src/__tests__/fixtures/SOUL.md +3 -0
- package/src/__tests__/fixtures/moons/athena/IDENTITY.md +2 -0
- package/src/__tests__/fixtures/moons/athena/SOUL.md +3 -0
- package/src/__tests__/fixtures/moons/hermes/SOUL.md +3 -0
- package/src/__tests__/fixtures/skills/brain/SKILL.md +6 -0
- package/src/__tests__/fixtures/skills/empty/SKILL.md +3 -0
- package/src/__tests__/fixtures/skills/multiline/SKILL.md +7 -0
- package/src/__tests__/fixtures/skills/no-desc/SKILL.md +5 -0
- package/src/__tests__/fixtures/skills/notion/SKILL.md +6 -0
- package/src/__tests__/fixtures/skills/quoted/SKILL.md +6 -0
- package/src/__tests__/hook-runner.test.ts +1689 -0
- package/src/__tests__/input-sanitization.test.ts +367 -0
- package/src/__tests__/logger.test.ts +163 -0
- package/src/__tests__/memory-orchestrator.test.ts +552 -0
- package/src/__tests__/model-catalog.test.ts +215 -0
- package/src/__tests__/model-command.test.ts +185 -0
- package/src/__tests__/moon-loader.test.ts +398 -0
- package/src/__tests__/ping-command.test.ts +85 -0
- package/src/__tests__/plugin.test.ts +258 -0
- package/src/__tests__/remind-command.test.ts +368 -0
- package/src/__tests__/reset-command.test.ts +92 -0
- package/src/__tests__/router.test.ts +1246 -0
- package/src/__tests__/scheduler.test.ts +469 -0
- package/src/__tests__/security.test.ts +214 -0
- package/src/__tests__/session-meta.test.ts +101 -0
- package/src/__tests__/session-tracker.test.ts +389 -0
- package/src/__tests__/session.test.ts +241 -0
- package/src/__tests__/skill-loader.test.ts +153 -0
- package/src/__tests__/status-command.test.ts +153 -0
- package/src/__tests__/stop-command.test.ts +60 -0
- package/src/__tests__/think-command.test.ts +146 -0
- package/src/__tests__/usage-api.test.ts +222 -0
- package/src/__tests__/usage-command-api-fail.test.ts +48 -0
- package/src/__tests__/usage-command-no-oauth.test.ts +48 -0
- package/src/__tests__/usage-command.test.ts +173 -0
- package/src/__tests__/whoami-command.test.ts +124 -0
- package/src/index.ts +122 -0
- package/src/lib/command-handler.ts +135 -0
- package/src/lib/commands/clear.ts +69 -0
- package/src/lib/commands/compact.ts +14 -0
- package/src/lib/commands/config-show.ts +49 -0
- package/src/lib/commands/cron.ts +118 -0
- package/src/lib/commands/help.ts +26 -0
- package/src/lib/commands/model.ts +71 -0
- package/src/lib/commands/ping.ts +24 -0
- package/src/lib/commands/remind.ts +75 -0
- package/src/lib/commands/status.ts +118 -0
- package/src/lib/commands/stop.ts +18 -0
- package/src/lib/commands/think.ts +42 -0
- package/src/lib/commands/usage.ts +56 -0
- package/src/lib/commands/whoami.ts +23 -0
- package/src/lib/config-loader.ts +1449 -0
- package/src/lib/config.ts +202 -0
- package/src/lib/cron-parser.ts +388 -0
- package/src/lib/daemon.ts +216 -0
- package/src/lib/dedup.ts +414 -0
- package/src/lib/hook-runner.ts +1270 -0
- package/src/lib/logger.ts +55 -0
- package/src/lib/memory-orchestrator.ts +415 -0
- package/src/lib/model-catalog.ts +240 -0
- package/src/lib/moon-loader.ts +291 -0
- package/src/lib/plugin.ts +148 -0
- package/src/lib/router.ts +1135 -0
- package/src/lib/scheduler.ts +422 -0
- package/src/lib/security.ts +259 -0
- package/src/lib/session-tracker.ts +222 -0
- package/src/lib/session.ts +158 -0
- package/src/lib/skill-loader.ts +166 -0
- package/src/lib/usage-api.ts +145 -0
- package/src/types/agent.ts +86 -0
- package/src/types/channel.ts +93 -0
- package/src/types/index.ts +32 -0
- package/src/types/memory.ts +92 -0
- package/src/types/moon.ts +56 -0
- package/src/types/voice.ts +74 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Catalog — Test Suite
|
|
3
|
+
*
|
|
4
|
+
* Spec: provides model metadata (context window, pricing, display names)
|
|
5
|
+
* for any model ID, with graceful fallback for unknown models.
|
|
6
|
+
*
|
|
7
|
+
* Key contracts:
|
|
8
|
+
* - Exact match by model ID
|
|
9
|
+
* - Normalized match (strip date suffix)
|
|
10
|
+
* - Prefix match (partial model names)
|
|
11
|
+
* - Unknown models get sensible defaults
|
|
12
|
+
* - Cost calculation is accurate per pricing
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, test } from 'bun:test'
|
|
15
|
+
import {
|
|
16
|
+
calculateCost,
|
|
17
|
+
getContextWindowSize,
|
|
18
|
+
getModelDisplayName,
|
|
19
|
+
getModelInfo,
|
|
20
|
+
listModels,
|
|
21
|
+
} from '../lib/model-catalog'
|
|
22
|
+
|
|
23
|
+
// ═══════════════════════════════════════════════════════════
|
|
24
|
+
// Model lookup
|
|
25
|
+
// ═══════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
describe('getModelInfo', () => {
|
|
28
|
+
test('exact match returns correct info', () => {
|
|
29
|
+
const info = getModelInfo('claude-sonnet-4-6')
|
|
30
|
+
expect(info.displayName).toBe('Claude Sonnet 4.6')
|
|
31
|
+
expect(info.contextWindow).toBe(200_000)
|
|
32
|
+
expect(info.inputPricePerM).toBe(3.0)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('date suffix is stripped for matching', () => {
|
|
36
|
+
const info = getModelInfo('claude-sonnet-4-5-20250514')
|
|
37
|
+
expect(info.displayName).toBe('Claude Sonnet 4.5')
|
|
38
|
+
expect(info.contextWindow).toBe(200_000)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('another date suffix variant', () => {
|
|
42
|
+
const info = getModelInfo('claude-opus-4-6-20260301')
|
|
43
|
+
expect(info.displayName).toBe('Claude Opus 4.6')
|
|
44
|
+
expect(info.maxOutput).toBe(32_768)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('prefix match for extended model IDs', () => {
|
|
48
|
+
const info = getModelInfo('claude-haiku-4-5-something-extra')
|
|
49
|
+
// Should match 'claude-haiku-4-5' prefix
|
|
50
|
+
expect(info.displayName).toBe('Claude Haiku 4.5')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('unknown model returns defaults with model ID as name', () => {
|
|
54
|
+
const info = getModelInfo('gpt-5-turbo')
|
|
55
|
+
expect(info.displayName).toBe('gpt-5-turbo')
|
|
56
|
+
expect(info.contextWindow).toBe(200_000) // safe default
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('opus models have higher pricing', () => {
|
|
60
|
+
const sonnet = getModelInfo('claude-sonnet-4-6')
|
|
61
|
+
const opus = getModelInfo('claude-opus-4-6')
|
|
62
|
+
expect(opus.inputPricePerM).toBeGreaterThan(sonnet.inputPricePerM)
|
|
63
|
+
expect(opus.outputPricePerM).toBeGreaterThan(sonnet.outputPricePerM)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('haiku models have lower pricing', () => {
|
|
67
|
+
const sonnet = getModelInfo('claude-sonnet-4-6')
|
|
68
|
+
const haiku = getModelInfo('claude-haiku-4-5')
|
|
69
|
+
expect(haiku.inputPricePerM).toBeLessThan(sonnet.inputPricePerM)
|
|
70
|
+
expect(haiku.outputPricePerM).toBeLessThan(sonnet.outputPricePerM)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// ═══════════════════════════════════════════════════════════
|
|
75
|
+
// Convenience accessors
|
|
76
|
+
// ═══════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
describe('getModelDisplayName', () => {
|
|
79
|
+
test('returns human-readable name', () => {
|
|
80
|
+
expect(getModelDisplayName('claude-sonnet-4-6')).toBe('Claude Sonnet 4.6')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('returns model ID for unknown models', () => {
|
|
84
|
+
expect(getModelDisplayName('mystery-model')).toBe('mystery-model')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('getContextWindowSize', () => {
|
|
89
|
+
test('returns context window for known model', () => {
|
|
90
|
+
expect(getContextWindowSize('claude-opus-4-6')).toBe(200_000)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('returns default 200K for unknown model', () => {
|
|
94
|
+
expect(getContextWindowSize('unknown-model')).toBe(200_000)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// ═══════════════════════════════════════════════════════════
|
|
99
|
+
// Cost calculation
|
|
100
|
+
// ═══════════════════════════════════════════════════════════
|
|
101
|
+
|
|
102
|
+
describe('calculateCost', () => {
|
|
103
|
+
test('basic input/output cost for Sonnet', () => {
|
|
104
|
+
// Sonnet: $3/M input, $15/M output
|
|
105
|
+
const cost = calculateCost('claude-sonnet-4-6', {
|
|
106
|
+
inputTokens: 1_000_000,
|
|
107
|
+
outputTokens: 1_000_000,
|
|
108
|
+
})
|
|
109
|
+
expect(cost).toBeCloseTo(18.0, 2) // $3 + $15
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('cost with cache tokens', () => {
|
|
113
|
+
// Cache tokens reduce the regular input cost
|
|
114
|
+
// 100K total input, 50K cache read, 20K cache write
|
|
115
|
+
// Regular input = 100K - 50K - 20K = 30K
|
|
116
|
+
const cost = calculateCost('claude-sonnet-4-6', {
|
|
117
|
+
inputTokens: 100_000,
|
|
118
|
+
outputTokens: 50_000,
|
|
119
|
+
cacheReadTokens: 50_000,
|
|
120
|
+
cacheWriteTokens: 20_000,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Regular: 30K * $3/M = $0.09
|
|
124
|
+
// Output: 50K * $15/M = $0.75
|
|
125
|
+
// Cache read: 50K * $0.3/M = $0.015
|
|
126
|
+
// Cache write: 20K * $3.75/M = $0.075
|
|
127
|
+
const expected = 0.09 + 0.75 + 0.015 + 0.075
|
|
128
|
+
expect(cost).toBeCloseTo(expected, 4)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('zero tokens = zero cost', () => {
|
|
132
|
+
const cost = calculateCost('claude-sonnet-4-6', {
|
|
133
|
+
inputTokens: 0,
|
|
134
|
+
outputTokens: 0,
|
|
135
|
+
})
|
|
136
|
+
expect(cost).toBe(0)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('opus is significantly more expensive', () => {
|
|
140
|
+
const tokens = { inputTokens: 100_000, outputTokens: 10_000 }
|
|
141
|
+
const sonnetCost = calculateCost('claude-sonnet-4-6', tokens)
|
|
142
|
+
const opusCost = calculateCost('claude-opus-4-6', tokens)
|
|
143
|
+
expect(opusCost).toBeGreaterThan(sonnetCost * 3)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// ═══════════════════════════════════════════════════════════
|
|
148
|
+
// Catalog enumeration
|
|
149
|
+
// ═══════════════════════════════════════════════════════════
|
|
150
|
+
|
|
151
|
+
describe('listModels', () => {
|
|
152
|
+
test('returns all known models', () => {
|
|
153
|
+
const models = listModels()
|
|
154
|
+
expect(models.length).toBeGreaterThanOrEqual(7)
|
|
155
|
+
expect(models).toContain('claude-sonnet-4-6')
|
|
156
|
+
expect(models).toContain('claude-opus-4-6')
|
|
157
|
+
expect(models).toContain('claude-haiku-4-5')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ═══════════════════════════════════════════════════════════
|
|
162
|
+
// Extended context [1m] variants
|
|
163
|
+
// ═══════════════════════════════════════════════════════════
|
|
164
|
+
|
|
165
|
+
describe('1M context variants', () => {
|
|
166
|
+
test('exact match for opus [1m] variant', () => {
|
|
167
|
+
const info = getModelInfo('claude-opus-4-6[1m]')
|
|
168
|
+
expect(info.displayName).toBe('Claude Opus 4.6 (1M)')
|
|
169
|
+
expect(info.contextWindow).toBe(1_000_000)
|
|
170
|
+
expect(info.maxOutput).toBe(128_000)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('exact match for sonnet [1m] variant', () => {
|
|
174
|
+
const info = getModelInfo('claude-sonnet-4-6[1m]')
|
|
175
|
+
expect(info.displayName).toBe('Claude Sonnet 4.6 (1M)')
|
|
176
|
+
expect(info.contextWindow).toBe(1_000_000)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('[1m] variant with date suffix is normalized correctly', () => {
|
|
180
|
+
const info = getModelInfo('claude-opus-4-6-20260301[1m]')
|
|
181
|
+
expect(info.displayName).toBe('Claude Opus 4.6 (1M)')
|
|
182
|
+
expect(info.contextWindow).toBe(1_000_000)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('[1m] variant has premium pricing (2x input)', () => {
|
|
186
|
+
const standard = getModelInfo('claude-opus-4-6')
|
|
187
|
+
const extended = getModelInfo('claude-opus-4-6[1m]')
|
|
188
|
+
expect(extended.inputPricePerM).toBe(standard.inputPricePerM * 2)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('[1m] variant has premium pricing (1.5x output)', () => {
|
|
192
|
+
const standard = getModelInfo('claude-opus-4-6')
|
|
193
|
+
const extended = getModelInfo('claude-opus-4-6[1m]')
|
|
194
|
+
expect(extended.outputPricePerM).toBe(standard.outputPricePerM * 1.5)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('cost calculation uses [1m] premium pricing', () => {
|
|
198
|
+
const tokens = { inputTokens: 500_000, outputTokens: 10_000 }
|
|
199
|
+
const standardCost = calculateCost('claude-opus-4-6', tokens)
|
|
200
|
+
const extendedCost = calculateCost('claude-opus-4-6[1m]', tokens)
|
|
201
|
+
expect(extendedCost).toBeGreaterThan(standardCost)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('[1m] variants are listed in catalog', () => {
|
|
205
|
+
const models = listModels()
|
|
206
|
+
expect(models).toContain('claude-opus-4-6[1m]')
|
|
207
|
+
expect(models).toContain('claude-sonnet-4-6[1m]')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('regular date suffix normalization still works', () => {
|
|
211
|
+
// Ensure the [1m] changes didn't break normal normalization
|
|
212
|
+
const info = getModelInfo('claude-sonnet-4-5-20250514')
|
|
213
|
+
expect(info.displayName).toBe('Claude Sonnet 4.5')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /model Command — Test Suite
|
|
3
|
+
*
|
|
4
|
+
* Spec: view or change the active model at runtime.
|
|
5
|
+
*
|
|
6
|
+
* Key contracts:
|
|
7
|
+
* - No args: shows current model + available aliases
|
|
8
|
+
* - With alias (opus, sonnet, haiku, sonnet-4.5): sets model override
|
|
9
|
+
* - With full model ID: sets model override directly
|
|
10
|
+
* - /model clear or /model reset: removes override
|
|
11
|
+
* - When updateSessionMeta is not available: returns error
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from 'bun:test'
|
|
14
|
+
import type { CommandContext } from '../lib/command-handler'
|
|
15
|
+
import { modelCommand } from '../lib/commands/model'
|
|
16
|
+
import { SessionTracker } from '../lib/session-tracker'
|
|
17
|
+
|
|
18
|
+
function makeCtx(overrides?: Partial<CommandContext>): CommandContext {
|
|
19
|
+
return {
|
|
20
|
+
sessionKey: 'test:channel-1',
|
|
21
|
+
message: {
|
|
22
|
+
id: 'msg-1',
|
|
23
|
+
channelId: 'channel-1',
|
|
24
|
+
sender: { id: 'user-1', name: 'Test User' },
|
|
25
|
+
text: '/model',
|
|
26
|
+
timestamp: new Date(),
|
|
27
|
+
},
|
|
28
|
+
tracker: new SessionTracker(),
|
|
29
|
+
daemonStartedAt: Date.now(),
|
|
30
|
+
agent: { id: 'claude', name: 'Claude Code (CLI)' },
|
|
31
|
+
clearSession: async () => ({ sessionCleared: true, hooksRan: false }),
|
|
32
|
+
...overrides,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ═══════════════════════════════════════════════════════════
|
|
37
|
+
// No args — show current model
|
|
38
|
+
// ═══════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
describe('/model no args', () => {
|
|
41
|
+
test('shows "Current Model" header', () => {
|
|
42
|
+
const result = modelCommand('', makeCtx())
|
|
43
|
+
expect(result).toContain('Current Model')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('shows available aliases (opus, sonnet, haiku)', () => {
|
|
47
|
+
const result = modelCommand('', makeCtx())
|
|
48
|
+
expect(result).toContain('Available aliases')
|
|
49
|
+
expect(result).toContain('opus')
|
|
50
|
+
expect(result).toContain('sonnet')
|
|
51
|
+
expect(result).toContain('haiku')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('shows current model from channelPersona.model', () => {
|
|
55
|
+
const ctx = makeCtx({
|
|
56
|
+
channelPersona: { model: 'claude-opus-4-6' } as any,
|
|
57
|
+
})
|
|
58
|
+
const result = modelCommand('', ctx)
|
|
59
|
+
expect(result).toContain('claude-opus-4-6')
|
|
60
|
+
expect(result).toContain('Claude Opus 4.6')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('falls back to moon.model when no channelPersona.model', () => {
|
|
64
|
+
const ctx = makeCtx({
|
|
65
|
+
moon: { model: 'claude-haiku-4-5', name: 'TestMoon', systemPrompt: '', files: {} } as any,
|
|
66
|
+
})
|
|
67
|
+
const result = modelCommand('', ctx)
|
|
68
|
+
expect(result).toContain('claude-haiku-4-5')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('shows "default" when neither channelPersona nor moon has model', () => {
|
|
72
|
+
const result = modelCommand('', makeCtx())
|
|
73
|
+
expect(result).toContain('default')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('shows override hint when session has modelOverride', () => {
|
|
77
|
+
const ctx = makeCtx({
|
|
78
|
+
getSessionMeta: () => ({ modelOverride: 'claude-opus-4-6' }),
|
|
79
|
+
})
|
|
80
|
+
const result = modelCommand('', ctx)
|
|
81
|
+
expect(result).toContain('Override active')
|
|
82
|
+
expect(result).toContain('/model clear')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ═══════════════════════════════════════════════════════════
|
|
87
|
+
// Set model with alias
|
|
88
|
+
// ═══════════════════════════════════════════════════════════
|
|
89
|
+
|
|
90
|
+
describe('/model set alias', () => {
|
|
91
|
+
test('/model opus sets claude-opus-4-6', () => {
|
|
92
|
+
let captured: unknown = null
|
|
93
|
+
const ctx = makeCtx({
|
|
94
|
+
updateSessionMeta: (u) => { captured = u },
|
|
95
|
+
})
|
|
96
|
+
modelCommand('opus', ctx)
|
|
97
|
+
expect(captured).toEqual({ modelOverride: 'claude-opus-4-6' })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('/model sonnet sets claude-sonnet-4-6', () => {
|
|
101
|
+
let captured: unknown = null
|
|
102
|
+
const ctx = makeCtx({
|
|
103
|
+
updateSessionMeta: (u) => { captured = u },
|
|
104
|
+
})
|
|
105
|
+
modelCommand('sonnet', ctx)
|
|
106
|
+
expect(captured).toEqual({ modelOverride: 'claude-sonnet-4-6' })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('/model haiku sets claude-haiku-4-5', () => {
|
|
110
|
+
let captured: unknown = null
|
|
111
|
+
const ctx = makeCtx({
|
|
112
|
+
updateSessionMeta: (u) => { captured = u },
|
|
113
|
+
})
|
|
114
|
+
modelCommand('haiku', ctx)
|
|
115
|
+
expect(captured).toEqual({ modelOverride: 'claude-haiku-4-5' })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('/model with full ID sets it directly', () => {
|
|
119
|
+
let captured: unknown = null
|
|
120
|
+
const ctx = makeCtx({
|
|
121
|
+
updateSessionMeta: (u) => { captured = u },
|
|
122
|
+
})
|
|
123
|
+
modelCommand('claude-opus-4-6', ctx)
|
|
124
|
+
expect(captured).toEqual({ modelOverride: 'claude-opus-4-6' })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('shows model display name in response after setting', () => {
|
|
128
|
+
const ctx = makeCtx({
|
|
129
|
+
updateSessionMeta: () => {},
|
|
130
|
+
})
|
|
131
|
+
const result = modelCommand('opus', ctx)
|
|
132
|
+
expect(result).toContain('Claude Opus 4.6')
|
|
133
|
+
expect(result).toContain('claude-opus-4-6')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ═══════════════════════════════════════════════════════════
|
|
138
|
+
// Clear / Reset
|
|
139
|
+
// ═══════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
describe('/model clear and reset', () => {
|
|
142
|
+
test('/model clear calls updateSessionMeta with modelOverride null', () => {
|
|
143
|
+
let captured: unknown = null
|
|
144
|
+
const ctx = makeCtx({
|
|
145
|
+
updateSessionMeta: (u) => { captured = u },
|
|
146
|
+
})
|
|
147
|
+
modelCommand('clear', ctx)
|
|
148
|
+
expect(captured).toEqual({ modelOverride: null })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('/model reset also clears', () => {
|
|
152
|
+
let captured: unknown = null
|
|
153
|
+
const ctx = makeCtx({
|
|
154
|
+
updateSessionMeta: (u) => { captured = u },
|
|
155
|
+
})
|
|
156
|
+
modelCommand('reset', ctx)
|
|
157
|
+
expect(captured).toEqual({ modelOverride: null })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('/model clear shows reverted model', () => {
|
|
161
|
+
const ctx = makeCtx({
|
|
162
|
+
updateSessionMeta: () => {},
|
|
163
|
+
channelPersona: { model: 'claude-sonnet-4-6' } as any,
|
|
164
|
+
})
|
|
165
|
+
const result = modelCommand('clear', ctx)
|
|
166
|
+
expect(result).toContain('override cleared')
|
|
167
|
+
expect(result).toContain('claude-sonnet-4-6')
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// ═══════════════════════════════════════════════════════════
|
|
172
|
+
// Error: updateSessionMeta not available
|
|
173
|
+
// ═══════════════════════════════════════════════════════════
|
|
174
|
+
|
|
175
|
+
describe('/model without updateSessionMeta', () => {
|
|
176
|
+
test('returns "not available" when setting model without updateSessionMeta', () => {
|
|
177
|
+
const result = modelCommand('opus', makeCtx())
|
|
178
|
+
expect(result).toContain('not available')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('returns "not available" when clearing without updateSessionMeta', () => {
|
|
182
|
+
const result = modelCommand('clear', makeCtx())
|
|
183
|
+
expect(result).toContain('not available')
|
|
184
|
+
})
|
|
185
|
+
})
|