@objectivex666/dsh-settings-search 1.10.0 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectivex666/dsh-settings-search",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance. Installable via `dsh plugin add`.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -55,7 +55,7 @@
55
55
  "node": ">=18"
56
56
  },
57
57
  "scripts": {
58
- "test": "node scripts/check.mjs"
58
+ "test": "node scripts/check.mjs && node --test scripts/ai.test.mjs"
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public"
@@ -0,0 +1,379 @@
1
+ import assert from 'node:assert/strict'
2
+ import { readFileSync } from 'node:fs'
3
+ import test from 'node:test'
4
+ import vm from 'node:vm'
5
+
6
+ const source = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
7
+ const hook = ' exports.apply = apply'
8
+ assert.equal(source.split(hook).length, 2)
9
+ // Expose closure helpers only in the VM; the shipped module has no test exports.
10
+ const instrumented = source.replace(hook, `
11
+ Object.assign(exports, {
12
+ aiConfig, setAiConfig, aiConfigReady, ollamaChatUrl,
13
+ requestUserAi, requestAiKeywords, testAiService, LOGS,
14
+ modelsUrl, fetchAiModels,
15
+ readIndexCache, serializeHarvest,
16
+ })
17
+ ${hook}`)
18
+ const storage = () => {
19
+ const values = new Map()
20
+ return {
21
+ getItem: (key) => values.get(key) ?? null,
22
+ setItem: (key, value) => values.set(key, String(value)),
23
+ removeItem: (key) => values.delete(key),
24
+ }
25
+ }
26
+ const json = (data, status = 200) => ({
27
+ ok: status >= 200 && status < 300,
28
+ status,
29
+ json: async () => data,
30
+ })
31
+ const plain = (value) => JSON.parse(JSON.stringify(value))
32
+ const ollama = { provider: 'ollama', baseUrl: '', model: 'local-model:7b', apiKey: '' }
33
+ const openai = { provider: 'openai', baseUrl: 'https://example.test/v1', model: 'cloud-model', apiKey: 'test-secret' }
34
+ const anthropic = { ...openai, provider: 'anthropic', baseUrl: 'https://example.test' }
35
+ const modelHook = ' const model = createModel(ctx)'
36
+
37
+ const reply = (provider, content = '{"keywords":["Language"]}') => provider === 'ollama'
38
+ ? { message: { role: 'assistant', content }, done: true }
39
+ : provider === 'anthropic'
40
+ ? { content: [{ type: 'text', text: content }] }
41
+ : { choices: [{ message: { role: 'assistant', content } }] }
42
+
43
+ function load(fetchImpl = async () => json(reply('ollama'))) {
44
+ let handoff
45
+ const calls = []
46
+ const sandbox = {
47
+ window: { __ModuleLoader__: { load: (value) => { handoff = value } }, dispatchEvent() {} },
48
+ localStorage: storage(),
49
+ sessionStorage: storage(),
50
+ CustomEvent: class { constructor(type) { this.type = type } },
51
+ URL,
52
+ AbortController,
53
+ setTimeout,
54
+ clearTimeout,
55
+ fetch: async (url, options) => {
56
+ const call = { url, ...options, body: options.body ? JSON.parse(options.body) : undefined }
57
+ calls.push(call)
58
+ return fetchImpl(call)
59
+ },
60
+ }
61
+ const instrumentedModel = instrumented.replace(modelHook, modelHook + '\n exports.__indexTestModel = model')
62
+ vm.runInNewContext(instrumentedModel, sandbox, { filename: 'lib/client.js' })
63
+ const api = handoff.factory(() => ({ createElement: () => null }))
64
+ return { api, calls, sandbox }
65
+ }
66
+
67
+ test('persistent settings index reuses matching fingerprints and drops invalid rows', () => {
68
+ const { api, sandbox } = load()
69
+ const records = {
70
+ 'settings.general.item': {
71
+ row1: { title: 'Enable Workshop card', idx: 1, text: 'Enable Workshop card and save.', inner: [] },
72
+ broken: { idx: 2 },
73
+ },
74
+ }
75
+ assert.equal(Object.keys(api.serializeHarvest(new Map())).length, 0)
76
+ sandbox.localStorage.setItem('dsh-settings-search:index:v1', JSON.stringify({
77
+ version: 1,
78
+ fingerprint: 'manager:test',
79
+ updatedAt: 1,
80
+ harvest: records,
81
+ }))
82
+ const cached = api.readIndexCache()
83
+ assert.equal(cached.fingerprint, 'manager:test')
84
+ assert.equal(cached.harvest.get('settings.general.item')?.get('row1')?.title, 'Enable Workshop card')
85
+ assert.equal(cached.harvest.get('settings.general.item')?.has('broken'), false)
86
+
87
+ const ctx = {
88
+ effect: (fn) => fn(),
89
+ get: () => null,
90
+ inject: () => {},
91
+ locale: {
92
+ register: () => {},
93
+ bind: () => () => '',
94
+ subscribe: () => () => {},
95
+ },
96
+ slots: {
97
+ register: (name, id, options) => ({ name, id, options }),
98
+ inject: (_slot, registrar) => registrar(),
99
+ entries: (slot) => slot === 'settings.general.item'
100
+ ? [{ options: { id: 'row1', order: 1 } }]
101
+ : [],
102
+ subscribe: () => () => {},
103
+ },
104
+ }
105
+ api.apply(ctx)
106
+ const model = api.__indexTestModel
107
+ assert.equal(model.indexFingerprintReady(), true)
108
+ assert.equal(model.applyIndexFingerprint('manager:test'), false)
109
+ assert.ok(model.readDeep().some((item) => item.label === 'Enable Workshop card'))
110
+ assert.equal(model.applyIndexFingerprint('manager:changed'), true)
111
+ assert.equal(model.readDeep().some((item) => item.label === 'Enable Workshop card'), false)
112
+ assert.equal(model.persistIndex('manager:changed'), false)
113
+ })
114
+
115
+ test('readIndexCache rejects malformed persisted index payloads', () => {
116
+ const { api, sandbox } = load()
117
+ for (const value of ['{', '{"version":1}', 'null']) {
118
+ sandbox.localStorage.setItem('dsh-settings-search:index:v1', value)
119
+ assert.equal(api.readIndexCache(), null)
120
+ }
121
+ })
122
+
123
+ test('Ollama configuration persists and requires a model but no key', () => {
124
+ const { api, sandbox } = load()
125
+ api.setAiConfig({ ...ollama, model: ' local-model:7b ' })
126
+ assert.deepEqual(plain(api.aiConfig()), { ...ollama, baseUrl: 'http://127.0.0.1:11434' })
127
+ assert.equal(api.aiConfigReady(api.aiConfig()), true)
128
+ assert.equal(api.aiConfigReady({ ...ollama, model: ' ' }), false)
129
+ assert.equal(sandbox.localStorage.getItem('dsh-settings-search:ai-provider'), 'ollama')
130
+ assert.equal(sandbox.sessionStorage.getItem('dsh-settings-search:ai-api-key'), null)
131
+ })
132
+
133
+ test('legacy providers still require keys and unknown provider falls back to OpenAI', () => {
134
+ const { api } = load()
135
+ for (const cfg of [openai, anthropic]) {
136
+ assert.equal(api.aiConfigReady(cfg), true)
137
+ assert.equal(api.aiConfigReady({ ...cfg, apiKey: ' ' }), false)
138
+ }
139
+ api.setAiConfig({ ...openai, provider: 'unknown' })
140
+ assert.equal(api.aiConfig().provider, 'openai')
141
+ })
142
+
143
+ test('Ollama accepts root, native, compatibility, proxy and IPv6 base URLs', () => {
144
+ const { api } = load()
145
+ for (const suffix of ['', '/', '/api', '/api/', '/api/chat', '/api/chat/', '/v1', '/v1/chat/completions']) {
146
+ assert.equal(api.ollamaChatUrl(`http://localhost:11434${suffix}`), 'http://localhost:11434/api/chat')
147
+ }
148
+ assert.equal(api.ollamaChatUrl('http://[::1]:11434/'), 'http://[::1]:11434/api/chat')
149
+ assert.equal(api.ollamaChatUrl('https://example.test/ollama/v1/'), 'https://example.test/ollama/api/chat')
150
+ })
151
+
152
+ test('users discover selectable models from each supported API type', async () => {
153
+ const cases = [
154
+ {
155
+ cfg: ollama,
156
+ payload: { models: [{ name: 'qwen3:8b' }, { name: 'llama3.1:8b' }, { name: 'qwen3:8b' }] },
157
+ url: 'http://127.0.0.1:11434/api/tags',
158
+ headers: {},
159
+ models: ['qwen3:8b', 'llama3.1:8b'],
160
+ },
161
+ {
162
+ cfg: openai,
163
+ payload: { data: [{ id: 'gpt-4o-mini' }, { id: 'gpt-4.1-mini' }] },
164
+ url: 'https://example.test/v1/models',
165
+ headers: { authorization: `Bearer ${openai.apiKey}` },
166
+ models: ['gpt-4o-mini', 'gpt-4.1-mini'],
167
+ },
168
+ {
169
+ cfg: anthropic,
170
+ payload: { data: [{ id: 'claude-sonnet-4-20250514' }] },
171
+ url: 'https://example.test/v1/models',
172
+ headers: {
173
+ 'x-api-key': anthropic.apiKey,
174
+ 'anthropic-version': '2023-06-01',
175
+ 'anthropic-dangerous-direct-browser-access': 'true',
176
+ },
177
+ models: ['claude-sonnet-4-20250514'],
178
+ },
179
+ ]
180
+ for (const expected of cases) {
181
+ const { api, calls } = load(async () => json(expected.payload))
182
+ assert.deepEqual(plain(await api.fetchAiModels(expected.cfg)), { ok: true, models: expected.models, code: 'ok', detail: '' })
183
+ const [call] = calls
184
+ assert.equal(call.url, expected.url)
185
+ assert.equal(call.method, 'GET')
186
+ assert.equal(call.redirect, 'error')
187
+ assert.deepEqual(plain(call.headers), expected.headers)
188
+ }
189
+ })
190
+
191
+ test('model discovery rejects unsafe or incomplete sources before fetching', async () => {
192
+ const { api, calls } = load(async () => json({ data: [{ id: 'should-not-load' }] }))
193
+ assert.equal(api.modelsUrl('ollama', 'javascript:alert(1)'), '')
194
+ assert.deepEqual(plain(await api.fetchAiModels({ ...ollama, baseUrl: 'file:///models' })), { ok: false, code: 'unsafe-url', models: [], detail: '' })
195
+ assert.deepEqual(plain(await api.fetchAiModels({ ...openai, apiKey: '' })), { ok: false, code: 'need-config', models: [], detail: '' })
196
+ assert.equal(calls.length, 0)
197
+ })
198
+
199
+ test('model discovery reports provider failures without inventing choices', async () => {
200
+ const { api, calls } = load(async () => json({ error: 'denied' }, 401))
201
+ assert.deepEqual(plain(await api.fetchAiModels(ollama)), { ok: false, code: 'auth', models: [], detail: 'HTTP 401' })
202
+ assert.equal(calls.length, 1)
203
+ })
204
+
205
+ test('Ollama search uses native non-streaming JSON and never sends a stored cloud key', async () => {
206
+ const { api, calls, sandbox } = load(async () => json(reply('ollama', '{"keywords":["Language","Language"," Theme ",42]}')))
207
+ api.setAiConfig({ ...ollama, apiKey: 'test-secret' })
208
+ const keywords = await api.requestUserAi('unmatched intent', 'Language\nTheme')
209
+ assert.deepEqual(plain(keywords), ['Language', 'Theme'])
210
+ const [call] = calls
211
+ assert.equal(call.url, 'http://127.0.0.1:11434/api/chat')
212
+ assert.equal(call.method, 'POST')
213
+ assert.equal(call.redirect, 'error')
214
+ assert.deepEqual(plain(call.headers), { 'content-type': 'application/json' })
215
+ assert.equal(call.body.model, ollama.model)
216
+ assert.equal(call.body.stream, false)
217
+ assert.equal(call.body.format, 'json')
218
+ assert.equal(call.body.options.temperature, 0)
219
+ assert.equal(call.body.options.num_predict, 1024)
220
+ assert.equal(call.body.messages[0].role, 'system')
221
+ assert.match(call.body.messages[1].content, /unmatched intent/)
222
+ assert.match(call.body.messages[1].content, /Language\nTheme/)
223
+ assert.equal(JSON.stringify(call.body).includes('test-secret'), false)
224
+ assert.equal(JSON.stringify(api.LOGS).includes('test-secret'), false)
225
+ assert.equal(sandbox.localStorage.getItem('dsh-settings-search:ai-api-key'), null)
226
+ })
227
+
228
+ test('Ollama connectivity test works without an API key and limits generation', async () => {
229
+ const { api, calls } = load()
230
+ assert.deepEqual(plain(await api.testAiService(ollama)), { ok: true })
231
+ assert.deepEqual(plain(calls[0].body), {
232
+ model: ollama.model,
233
+ messages: [{ role: 'user', content: 'ping' }],
234
+ stream: false,
235
+ options: { temperature: 0, num_predict: 1 },
236
+ })
237
+ assert.equal(calls[0].headers.authorization, undefined)
238
+ })
239
+
240
+ for (const cfg of [openai, anthropic]) {
241
+ test(`${cfg.provider} search and test preserve protocol, authentication and redirect protection`, async () => {
242
+ const { api, calls } = load(async () => json(reply(cfg.provider)))
243
+ api.setAiConfig(cfg)
244
+ assert.deepEqual(plain(await api.requestUserAi('intent', 'Language')), ['Language'])
245
+ assert.deepEqual(plain(await api.testAiService(cfg)), { ok: true })
246
+ for (const call of calls) {
247
+ assert.equal(call.redirect, 'error')
248
+ assert.equal(call.body.model, cfg.model)
249
+ assert.equal(call.body.stream, undefined)
250
+ assert.equal(call.body.options, undefined)
251
+ if (cfg.provider === 'anthropic') {
252
+ assert.equal(call.url, 'https://example.test/v1/messages')
253
+ assert.equal(call.headers['x-api-key'], cfg.apiKey)
254
+ assert.equal(call.headers['anthropic-version'], '2023-06-01')
255
+ assert.equal(call.headers['anthropic-dangerous-direct-browser-access'], 'true')
256
+ assert.equal(call.headers.authorization, undefined)
257
+ } else {
258
+ assert.equal(call.url, 'https://example.test/v1/chat/completions')
259
+ assert.equal(call.headers.authorization, `Bearer ${cfg.apiKey}`)
260
+ }
261
+ }
262
+ assert.equal(calls[1].body.max_tokens, 1)
263
+ assert.equal(calls[1].body.messages.length, 1)
264
+ if (cfg.provider === 'anthropic') assert.equal(typeof calls[0].body.system, 'string')
265
+ else assert.equal(calls[0].body.messages[0].role, 'system')
266
+ })
267
+ }
268
+
269
+ test('incomplete configuration never sends requests', async () => {
270
+ const { api, calls } = load()
271
+ for (const cfg of [{ ...ollama, model: '' }, { ...openai, apiKey: '' }, { ...anthropic, apiKey: '' }]) {
272
+ api.setAiConfig(cfg)
273
+ assert.deepEqual(plain(await api.requestUserAi('intent', 'Language')), [])
274
+ assert.equal((await api.testAiService(cfg)).code, 'need-config')
275
+ }
276
+ assert.equal(calls.length, 0)
277
+ })
278
+
279
+ test('unsafe URLs are rejected before search and connectivity requests', async () => {
280
+ const { api, calls } = load()
281
+ for (const baseUrl of ['javascript:alert(1)', 'file:///tmp/model', 'http://example.test', 'https://user:pass@example.test']) {
282
+ const cfg = { ...ollama, baseUrl }
283
+ api.setAiConfig(cfg)
284
+ assert.deepEqual(plain(await api.requestUserAi('intent', 'Language')), [])
285
+ assert.equal((await api.testAiService(cfg)).code, 'unsafe-url')
286
+ }
287
+ assert.equal(calls.length, 0)
288
+ })
289
+
290
+ for (const [status, code] of [[400, 'bad-request'], [401, 'auth'], [403, 'forbidden'], [404, 'not-found'], [429, 'rate-limit'], [500, 'server']]) {
291
+ test(`Ollama HTTP ${status} produces ${code}`, async () => {
292
+ const { api } = load(async () => json({ error: 'test error' }, status))
293
+ assert.equal((await api.testAiService(ollama)).code, code)
294
+ api.setAiConfig(ollama)
295
+ await assert.rejects(api.requestUserAi('intent', 'Language'), { message: `HTTP ${status}` })
296
+ })
297
+ }
298
+
299
+ test('malformed responses and native error payloads cannot pass connectivity checks', async () => {
300
+ for (const data of [null, {}, { message: {} }, { message: { content: 123 } }, { error: 'bad model', ...reply('ollama') }]) {
301
+ const { api } = load(async () => json(data))
302
+ assert.equal((await api.testAiService(ollama)).code, 'bad-response')
303
+ }
304
+ const { api } = load(async () => ({ ok: true, json: async () => { throw new SyntaxError('invalid JSON') } }))
305
+ assert.equal((await api.testAiService(ollama)).code, 'bad-response')
306
+ })
307
+
308
+ test('network failure is reported without claiming connection success', async () => {
309
+ const { api } = load(async () => { throw new TypeError('Failed to fetch') })
310
+ assert.equal((await api.testAiService(ollama)).code, 'network')
311
+ })
312
+
313
+ const abortedFetch = ({ signal }) => new Promise((_, reject) => {
314
+ const abort = () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' }))
315
+ if (signal.aborted) abort()
316
+ else signal.addEventListener('abort', abort, { once: true })
317
+ })
318
+
319
+ for (const cfg of [ollama, openai, anthropic]) {
320
+ test(`${cfg.provider} timeout aborts the request and uses its provider deadline`, async () => {
321
+ const { api, sandbox, calls } = load(abortedFetch)
322
+ let expire
323
+ let cleared = false
324
+ sandbox.setTimeout = (callback, ms) => {
325
+ assert.equal(ms, cfg.provider === 'ollama' ? 120000 : 12000)
326
+ expire = callback
327
+ return 1
328
+ }
329
+ sandbox.clearTimeout = () => { cleared = true }
330
+ const result = api.testAiService(cfg)
331
+ expire()
332
+ assert.equal((await result).code, 'timeout')
333
+ assert.equal(calls[0].signal.aborted, true)
334
+ assert.equal(cleared, true)
335
+ })
336
+ }
337
+
338
+ test('search and connectivity propagate cancellation, including pre-aborted signals', async () => {
339
+ const { api, calls } = load(abortedFetch)
340
+ api.setAiConfig(ollama)
341
+ const controller = new AbortController()
342
+ const search = api.requestUserAi('intent', 'Language', controller.signal)
343
+ controller.abort()
344
+ await assert.rejects(search, { name: 'AbortError' })
345
+ assert.equal(calls[0].signal.aborted, true)
346
+ assert.equal((await api.testAiService(ollama, controller.signal)).code, 'cancelled')
347
+ })
348
+
349
+ test('a successful request clears its timer and detaches the caller abort listener', async () => {
350
+ const { api, calls, sandbox } = load()
351
+ const cleared = []
352
+ sandbox.setTimeout = () => 7
353
+ sandbox.clearTimeout = (timer) => cleared.push(timer)
354
+ const controller = new AbortController()
355
+ api.setAiConfig(ollama)
356
+ await api.requestUserAi('intent', 'Language', controller.signal)
357
+ controller.abort()
358
+ assert.equal(calls[0].signal.aborted, false)
359
+ assert.deepEqual(cleared, [7])
360
+ })
361
+
362
+ test('cache respects settings labels and is invalidated when provider configuration changes', async () => {
363
+ let provider = 'ollama'
364
+ const { api, calls } = load(async () => json(reply(provider)))
365
+ api.setAiConfig(ollama)
366
+ await api.requestAiKeywords('intent', 'Language')
367
+ await api.requestAiKeywords('intent', 'Language')
368
+ assert.equal(calls.length, 1)
369
+ await api.requestAiKeywords('intent', 'Theme')
370
+ assert.equal(calls.length, 2)
371
+ api.setAiConfig({ ...ollama, model: 'another-model' })
372
+ await api.requestAiKeywords('intent', 'Language')
373
+ assert.equal(calls.length, 3)
374
+ provider = 'openai'
375
+ api.setAiConfig(openai)
376
+ await api.requestAiKeywords('intent', 'Language')
377
+ assert.equal(calls.length, 4)
378
+ assert.equal(calls[3].headers.authorization, `Bearer ${openai.apiKey}`)
379
+ })