Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/24769-settings-test-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/service-settings': patch
---

fix(service-settings): Setup's AI "Test embedder" and Knowledge "Test connection" buttons are answered, and their answers say they checked the form only

Clause-②: no

- **Before.** Both buttons answered `400 SETTINGS_ACTION_FAILED` with "No handler registered for action 'test_embedder' in 'ai'." and "No handler registered for action 'test' in 'knowledge'." The manifests declared both buttons and the package exported both handlers (`aiTestEmbedderActionHandler`, `knowledgeTestActionHandler`), but `SettingsServicePlugin`'s default `actionHandlers` map did not register them.
- **Now.** The default map registers both, beside `mail.test`, `sms.test`, `storage.test` and `ai.test`. A host that passes its own `actionHandlers` still replaces the whole default map, and a plugin can still replace any one handler with `SettingsService.registerAction`.
- **What each button checks.** Both are form checks and send nothing over the network. "Test embedder" checks that a provider is chosen, that an API key is set (Ollama excepted) and that `custom` / `azure` have a Base URL. "Test connection" checks that a managed `libsql://` Turso URL has an auth token and that RAGFlow has a Base URL and an API key. The in-memory adapter answers with a warning that there is no external service to probe.
- **Answer text.** A passing "Test embedder" now ends "This checks the form only — no embedding request was sent." Every passing "Test connection" on Turso or RAGFlow now says "This checks the form only — no connection was attempted." The old "Mount @objectstack/embedder-openai + a knowledge adapter to exercise live calls" and "Mount @objectstack/knowledge-ragflow to exercise live calls" are gone: neither package registers these actions, so mounting it never made either check live.
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,35 @@ describe('aiTestActionHandler — live-call hint carries the cloud boundary (#11
});
}

// Contrast control, so the assertions above cannot be satisfied by blanket-
// rewording every hint in this file: the embedder hint names
// `@objectstack/embedder-openai`, which IS built in this repo (8 path hits
// under `git ls-tree -r --name-only HEAD | grep -cF /embedder-openai/`, against
// 0 for `/service-ai/`), so that instruction is followable as written and stays
// a plain mount line. Asserted here, deliberately not edited — commit 99ccbb9c8 fences
// this site out by name.
it('leaves the embedder hint a plain mount line — its package IS built here', async () => {
const r = await runTestEmbedder({ embedder_provider: 'openai', embedder_api_key: 'sk-test' });
// The embedder hint used to stand here as this block's contrast control — a
// plain "Mount @objectstack/embedder-openai + a knowledge adapter to exercise
// live calls", kept because that package IS built in this repo. Followable,
// but measured false for the button it answered (#24769): no package in this
// repo registers `ai/test_embedder`, so mounting it never made "Test
// embedder" live. Its replacement is pinned with the embedder handler below;
// the three branches above are still exercised one by one, so a blanket
// rewording cannot stand in for them.
});

/**
* #24769 — "Test embedder" is now answered (the handler is in the plugin's
* default map), and what it answers is a form check: no embedding request is
* sent and no package in this repo replaces it with a live probe. A green
* answer must say so, and must not name a package whose mounting would make the
* check live.
*/
describe('aiTestEmbedderActionHandler — a form check says it is one (#24769)', () => {
it('states that no embedding request was sent, and promises no live call', async () => {
const r = await runTestEmbedder({
embedder_provider: 'openai',
embedder_api_key: 'sk-test',
embedder_model: 'text-embedding-3-small',
});
expect(r.ok).toBe(true);
expect(String(r.message)).toContain(
'Mount @objectstack/embedder-openai + a knowledge adapter to exercise live calls',
);
expect(r.severity).toBe('info');
const message = String(r.message);
expect(message).toContain('text-embedding-3-small');
expect(message).toMatch(/no embedding request was sent/);
expect(message).not.toMatch(/to exercise live calls/);
});
});
20 changes: 13 additions & 7 deletions packages/services/service-settings/src/manifests/ai.manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,18 @@ export const aiTestActionHandler: SettingsActionHandler = async ({ values, paylo
};

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,28 @@ describe('knowledgeTestActionHandler', () => {
expect(message).not.toMatch(/Mount @objectstack\/knowledge-turso to exercise live calls/);
});

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

it('rejects ragflow without base URL', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,24 @@ const manifest = {
export const knowledgeSettingsManifest = manifest as unknown as SettingsManifest;

/**
* Built-in fallback handler for `knowledge/test`. The real probe with
* a live `healthCheck()` round-trip lives in `@objectstack/service-knowledge`
* and overrides this stub at runtime (mirrors the AI / storage patterns).
* The sentence every answer on a remote adapter carries, so a green "Test
* connection" is never read as a connection that succeeded.
*/
const FORM_CHECK_ONLY = 'This checks the form only — no connection was attempted.';

/**
* Handler for `knowledge/test` ("Test connection"), registered by
* `SettingsServicePlugin`'s default `actionHandlers` map (#24769).
*
* This fallback only validates form completeness so the button is
* usable when no knowledge adapter plugin is mounted.
* It checks form completeness only — a Turso auth token beside a managed
* `libsql://` URL, a RAGFlow Base URL and API key — and opens NO connection.
* Nothing in this repository replaces it with a live probe:
* `@objectstack/service-knowledge`, `@objectstack/knowledge-memory` and
* `@objectstack/knowledge-ragflow` neither read this namespace nor register
* `knowledge/test` (a plugin that did would call
* `SettingsService.registerAction`, which replaces this handler). So every
* answer on a remote adapter says no connection was attempted, and none names
* a package whose mounting would make this check live — none does.
*/
export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values, payload }) => {
const overrides = extractOverrides(payload);
Expand All @@ -163,7 +175,9 @@ export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values
return {
ok: true,
severity: 'info',
message: 'No URL configured — adapter will reuse the tenant\'s primary libSQL connection at runtime.',
message:
'No URL configured — adapter will reuse the tenant\'s primary libSQL connection at runtime. ' +
FORM_CHECK_ONLY,
};
}
const u = String(url);
Expand All @@ -178,7 +192,7 @@ export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values
ok: true,
severity: 'info',
message:
`Turso adapter configured (${u}). Live calls need @objectstack/knowledge-turso, ` +
`Turso adapter configured (${u}). ${FORM_CHECK_ONLY} Live calls need @objectstack/knowledge-turso, ` +
'which this platform does not ship: it is built in the ObjectStack Cloud monorepo ' +
'and published on its own version track, so check that it offers a release ' +
'compatible with your platform version before mounting it.',
Expand All @@ -195,7 +209,7 @@ export const knowledgeTestActionHandler: SettingsActionHandler = async ({ values
return {
ok: true,
severity: 'info',
message: `RAGFlow adapter configured (${merged.ragflow_base_url}). Mount @objectstack/knowledge-ragflow to exercise live calls.`,
message: `RAGFlow adapter configured (${merged.ragflow_base_url}). ${FORM_CHECK_ONLY}`,
};
}

Expand Down
Loading
Loading