Skip to content

Commit c8dbf13

Browse files
committed
fix(service-settings): register the ai.test_embedder and knowledge.test handlers by default
Both buttons were declared and their handlers exported, but the default actionHandlers map never registered them, so each answered 400 'No handler registered'. Their success messages now say they check the form only and no request was sent: nothing in this repository replaces either with a live probe. Claude-Session: https://claude.ai/code/session_01UmqzD97e14iws6hmVhgZ5y Co-authored-by: Claude <noreply@anthropic.com>
1 parent a66a741 commit c8dbf13

5 files changed

Lines changed: 100 additions & 44 deletions

File tree

‎packages/services/service-settings/src/manifests/ai.manifest.test.ts‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,18 +354,35 @@ describe('aiTestActionHandler — live-call hint carries the cloud boundary (#11
354354
});
355355
}
356356

357-
// Contrast control, so the assertions above cannot be satisfied by blanket-
358-
// rewording every hint in this file: the embedder hint names
359-
// `@objectstack/embedder-openai`, which IS built in this repo (8 path hits
360-
// under `git ls-tree -r --name-only HEAD | grep -cF /embedder-openai/`, against
361-
// 0 for `/service-ai/`), so that instruction is followable as written and stays
362-
// a plain mount line. Asserted here, deliberately not edited — commit 99ccbb9c8 fences
363-
// this site out by name.
364-
it('leaves the embedder hint a plain mount line — its package IS built here', async () => {
365-
const r = await runTestEmbedder({ embedder_provider: 'openai', embedder_api_key: 'sk-test' });
357+
// The embedder hint used to stand here as this block's contrast control — a
358+
// plain "Mount @objectstack/embedder-openai + a knowledge adapter to exercise
359+
// live calls", kept because that package IS built in this repo. Followable,
360+
// but measured false for the button it answered (#24769): no package in this
361+
// repo registers `ai/test_embedder`, so mounting it never made "Test
362+
// embedder" live. Its replacement is pinned with the embedder handler below;
363+
// the three branches above are still exercised one by one, so a blanket
364+
// rewording cannot stand in for them.
365+
});
366+
367+
/**
368+
* #24769 — "Test embedder" is now answered (the handler is in the plugin's
369+
* default map), and what it answers is a form check: no embedding request is
370+
* sent and no package in this repo replaces it with a live probe. A green
371+
* answer must say so, and must not name a package whose mounting would make the
372+
* check live.
373+
*/
374+
describe('aiTestEmbedderActionHandler — a form check says it is one (#24769)', () => {
375+
it('states that no embedding request was sent, and promises no live call', async () => {
376+
const r = await runTestEmbedder({
377+
embedder_provider: 'openai',
378+
embedder_api_key: 'sk-test',
379+
embedder_model: 'text-embedding-3-small',
380+
});
366381
expect(r.ok).toBe(true);
367-
expect(String(r.message)).toContain(
368-
'Mount @objectstack/embedder-openai + a knowledge adapter to exercise live calls',
369-
);
382+
expect(r.severity).toBe('info');
383+
const message = String(r.message);
384+
expect(message).toContain('text-embedding-3-small');
385+
expect(message).toMatch(/no embedding request was sent/);
386+
expect(message).not.toMatch(/to exercise live calls/);
370387
});
371388
});

‎packages/services/service-settings/src/manifests/ai.manifest.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,18 @@ export const aiTestActionHandler: SettingsActionHandler = async ({ values, paylo
427427
};
428428

429429
/**
430-
* Built-in fallback handler for `ai/test_embedder`. Real implementation
431-
* with a live `embed()` round-trip lives in `@objectstack/service-ai` or
432-
* `@objectstack/service-knowledge` and overrides this stub at runtime.
430+
* Handler for `ai/test_embedder` ("Test embedder"), registered by
431+
* `SettingsServicePlugin`'s default `actionHandlers` map (#24769).
433432
*
434-
* This fallback validates form completeness only — no network call —
435-
* so the button is useful even when no embedder plugin is mounted.
433+
* It checks form completeness only — a provider is chosen, an API key is
434+
* present (Ollama excepted), a Base URL is present for `custom` / `azure` —
435+
* and sends NO embedding request. Nothing in this repository replaces it with
436+
* a live probe: neither `@objectstack/embedder-openai` nor
437+
* `@objectstack/service-knowledge` registers `ai/test_embedder` (a plugin that
438+
* did would call `SettingsService.registerAction`, which replaces this
439+
* handler). So its success message says what was checked and that no request
440+
* was sent, and names no package whose mounting would make this check live —
441+
* none does.
436442
*/
437443
export const aiTestEmbedderActionHandler: SettingsActionHandler = async ({ values, payload }) => {
438444
const overrides = extractOverrides(payload);
@@ -469,8 +475,8 @@ export const aiTestEmbedderActionHandler: SettingsActionHandler = async ({ value
469475
ok: true,
470476
severity: 'info',
471477
message:
472-
`${provider} embedder configured (model=${model}). ` +
473-
'Mount @objectstack/embedder-openai + a knowledge adapter to exercise live calls.',
478+
`${provider} embedder settings are complete (model=${model}). ` +
479+
'This checks the form only — no embedding request was sent.',
474480
};
475481
};
476482

‎packages/services/service-settings/src/manifests/knowledge.manifest.test.ts‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -145,20 +145,28 @@ describe('knowledgeTestActionHandler', () => {
145145
expect(message).not.toMatch(/Mount @objectstack\/knowledge-turso to exercise live calls/);
146146
});
147147

148-
// Contrast, so the assertion above cannot be satisfied by blanket-rewording every
149-
// hint: ragflow's plugin IS built here, and its hint stays the plain mount line.
150-
it('ragflow live-call hint stays a plain mount line (its plugin is built here)', async () => {
151-
const r = await knowledgeTestActionHandler({
152-
values: {
153-
adapter: 'ragflow',
154-
ragflow_base_url: 'http://localhost:9380',
155-
ragflow_api_key: 'k',
156-
},
157-
} as any);
158-
expect(r.ok).toBe(true);
159-
expect(String(r.message)).toContain(
160-
'Mount @objectstack/knowledge-ragflow to exercise live calls',
161-
);
148+
// #24769 — "Test connection" is now answered (the handler is in the plugin's
149+
// default map), and what it answers is a form check: no package in this repo
150+
// reads this namespace or registers `knowledge/test`, so nothing replaces it
151+
// with a live probe. Every green answer on a remote adapter must say no
152+
// connection was attempted. Ragflow's hint used to be "Mount
153+
// @objectstack/knowledge-ragflow to exercise live calls" — followable (the
154+
// plugin IS built here) but false for this button, which mounting it never
155+
// made live; no answer may carry that promise.
156+
it('every green answer on a remote adapter says no connection was attempted', async () => {
157+
const remote: Array<[string, Record<string, unknown>]> = [
158+
['turso, no URL', { adapter: 'turso' }],
159+
['turso, local file', { adapter: 'turso', turso_url: 'file:./knowledge.db' }],
160+
['turso, managed', { adapter: 'turso', turso_url: 'libsql://x.turso.io', turso_auth_token: 't' }],
161+
['ragflow', { adapter: 'ragflow', ragflow_base_url: 'http://localhost:9380', ragflow_api_key: 'k' }],
162+
];
163+
for (const [label, values] of remote) {
164+
const r = await knowledgeTestActionHandler({ values } as any);
165+
expect(r.ok, label).toBe(true);
166+
const message = String(r.message);
167+
expect(message, label).toMatch(/no connection was attempted/);
168+
expect(message, label).not.toMatch(/to exercise live calls/);
169+
}
162170
});
163171

164172
it('rejects ragflow without base URL', async () => {

‎packages/services/service-settings/src/manifests/knowledge.manifest.ts‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,24 @@ const manifest = {
137137
export const knowledgeSettingsManifest = manifest as unknown as SettingsManifest;
138138

139139
/**
140-
* Built-in fallback handler for `knowledge/test`. The real probe with
141-
* a live `healthCheck()` round-trip lives in `@objectstack/service-knowledge`
142-
* and overrides this stub at runtime (mirrors the AI / storage patterns).
140+
* The sentence every answer on a remote adapter carries, so a green "Test
141+
* connection" is never read as a connection that succeeded.
142+
*/
143+
const FORM_CHECK_ONLY = 'This checks the form only — no connection was attempted.';
144+
145+
/**
146+
* Handler for `knowledge/test` ("Test connection"), registered by
147+
* `SettingsServicePlugin`'s default `actionHandlers` map (#24769).
143148
*
144-
* This fallback only validates form completeness so the button is
145-
* usable when no knowledge adapter plugin is mounted.
149+
* It checks form completeness only — a Turso auth token beside a managed
150+
* `libsql://` URL, a RAGFlow Base URL and API key — and opens NO connection.
151+
* Nothing in this repository replaces it with a live probe:
152+
* `@objectstack/service-knowledge`, `@objectstack/knowledge-memory` and
153+
* `@objectstack/knowledge-ragflow` neither read this namespace nor register
154+
* `knowledge/test` (a plugin that did would call
155+
* `SettingsService.registerAction`, which replaces this handler). So every
156+
* answer on a remote adapter says no connection was attempted, and none names
157+
* a package whose mounting would make this check live — none does.
146158
*/
147159
export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values, payload }) => {
148160
const overrides = extractOverrides(payload);
@@ -163,7 +175,9 @@ export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values
163175
return {
164176
ok: true,
165177
severity: 'info',
166-
message: 'No URL configured — adapter will reuse the tenant\'s primary libSQL connection at runtime.',
178+
message:
179+
'No URL configured — adapter will reuse the tenant\'s primary libSQL connection at runtime. ' +
180+
FORM_CHECK_ONLY,
167181
};
168182
}
169183
const u = String(url);
@@ -178,7 +192,7 @@ export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values
178192
ok: true,
179193
severity: 'info',
180194
message:
181-
`Turso adapter configured (${u}). Live calls need @objectstack/knowledge-turso, ` +
195+
`Turso adapter configured (${u}). ${FORM_CHECK_ONLY} Live calls need @objectstack/knowledge-turso, ` +
182196
'which this platform does not ship: it is built in the ObjectStack Cloud monorepo ' +
183197
'and published on its own version track, so check that it offers a release ' +
184198
'compatible with your platform version before mounting it.',
@@ -195,7 +209,7 @@ export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values
195209
return {
196210
ok: true,
197211
severity: 'info',
198-
message: `RAGFlow adapter configured (${merged.ragflow_base_url}). Mount @objectstack/knowledge-ragflow to exercise live calls.`,
212+
message: `RAGFlow adapter configured (${merged.ragflow_base_url}). ${FORM_CHECK_ONLY}`,
199213
};
200214
}
201215

‎packages/services/service-settings/src/settings-service-plugin.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import {
4646
smsTestActionHandler,
4747
storageTestActionHandler,
4848
aiTestActionHandler,
49+
aiTestEmbedderActionHandler,
50+
knowledgeTestActionHandler,
4951
} from './manifests/index.js';
5052
import { settingsBuiltinTranslations } from './translations/index.js';
5153

@@ -79,8 +81,12 @@ export interface SettingsServicePluginOptions {
7981
env?: Record<string, string | undefined>;
8082
/**
8183
* Action handlers to register at boot, keyed by namespace and action
82-
* id. The bundled `mail.test` handler is registered automatically
83-
* unless this object is provided.
84+
* id. When omitted, the bundled handler of every `action_button` the
85+
* builtin manifests declare is registered (`mail.test`, `sms.test`,
86+
* `storage.test`, `ai.test`, `ai.test_embedder`, `knowledge.test`;
87+
* `reset` is served by the service for every namespace). Providing this
88+
* object replaces that whole default map. A plugin may still replace any
89+
* one handler later with `SettingsService.registerAction`.
8490
*/
8591
actionHandlers?: Record<string, Record<string, import('./settings-service.types.js').SettingsActionHandler>>;
8692
}
@@ -121,11 +127,16 @@ export class SettingsServicePlugin implements Plugin {
121127
this.opts = {
122128
...opts,
123129
manifests: opts.manifests ?? builtinSettingsManifests,
130+
// One entry per `action_button` a builtin manifest declares against the
131+
// settings action route — a declared button with no entry here answers
132+
// "No handler registered" (#24769). `settings-default-action-handlers.pin.test.ts`
133+
// enumerates the manifests and holds this map to them.
124134
actionHandlers: opts.actionHandlers ?? {
125135
mail: { test: mailTestActionHandler },
126136
sms: { test: smsTestActionHandler },
127137
storage: { test: storageTestActionHandler },
128-
ai: { test: aiTestActionHandler },
138+
ai: { test: aiTestActionHandler, test_embedder: aiTestEmbedderActionHandler },
139+
knowledge: { test: knowledgeTestActionHandler },
129140
},
130141
};
131142
}

0 commit comments

Comments
 (0)