Skip to content

fix(service-datasource)!: POST /datasources/:name/test answers an unknown name 404 RESOURCE_NOT_FOUND - #27574

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-27510-datasource-test-unknown-404
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-27510-datasource-test-unknown-404

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Requested by Sales · project thread

Fixes #27510

Clause-②: no (narrowing) — POST /api/v1/datasources/:name/test and the in-process ExternalDatasourceService.testConnection(name) refuse a name that resolves no saved datasource with 404 RESOURCE_NOT_FOUND, which they answered 200 { ok: false } before. Nothing is widened.
Metadata-protocol: none
Skills-docs: none — no content/docs page or skills/** file states what POST /api/v1/datasources/:name/test answers (grep for the route, test_connection and testConnection over content/docs and skills/ found none; data-modeling/external-datasources.mdx names only GET /api/v1/datasources); the RESOURCE_NOT_FOUND example in protocol/kernel/error-handling.mdx is GET /api/v1/datasources/:name, whose answer this door now matches.

维护者速读

  • 改了什么:POST /api/v1/datasources/NAME/test 遇到不存在的数据源名,现在返回 404 RESOURCE_NOT_FOUND(与 GET /datasources/NAME 同码同文案),不再返回 200 {ok:false}。
  • 为什么改:原来"数据源不存在"和"数据源连不上"返回一模一样,管理工具和脚本无法区分;其它按名字访问的接口早已统一为 404。
  • 风险与回滚:风险低,只影响查无此名的请求;已存在但连不上的数据源仍是 200 {ok:false},主数据源 default 即使没有存储记录也照常测试。回滚即 revert 本 PR。
  • 先看:① external-datasource-service.ts 的 testConnection 开头新增的存在性检查;② 新测试 saved-datasource-test-unknown-name.test.ts;③ 两个旧测试里补上的数据源记录。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/saved-datasource-test-unknown-name.test.ts,预期 5 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • ExternalDatasourceService.testConnection(name) first resolves the saved record through getDatasource. This is the same stored-record read that the sibling by-name doors make: both plugins wire it to metadata.get('datasource', name). When nothing is stored under that name, it throws a 404 / RESOURCE_NOT_FOUND error in the words GET /datasources/NAME uses. The admin route's existing relay in badRequest answers that error as 404, so the route handler is unchanged.
  • The primary datasource (default) is not looked up. It may have no stored record, and its live driver answers for it, as before.
  • Every other outcome is unchanged. A saved datasource that fails its probe still gets 200 { ok: false, error }, and one that is up still gets 200 { ok: true, … }.
  • Two docblocks in admin-routes.ts now list the Test door among the by-name doors that answer 404.
  • Changeset: @objectstack/service-datasource patch.

Tests

  • New: src/__tests__/saved-datasource-test-unknown-name.test.ts. It runs through the real HTTP door over the real composition: ObjectQL, the connection service and driver factory, the admin service, the ExternalDatasourceServicePlugin wiring, and the admin routes on Hono. It has 5 cases:
    • An unknown name answers 404 RESOURCE_NOT_FOUND, names the datasource and has no data.
    • That answer has the same status, code and message as GET /datasources/NAME for the same name.
    • Control: a saved datasource with no live driver keeps 200 { ok: false, error }.
    • Control: a saved datasource that is up keeps 200 { ok: true }.
    • Control: the primary datasource with no stored record is still tested through its live driver and answers 200 { ok: true, latencyMs }.
  • Red first. The new file ran at base d4ff1a025f before the fix and gave Tests 2 failed | 3 passed (5). The two failures were the unknown-name cases, which received status: 200, success: true.
  • Fixture triage (two existing files). Five existing pins tested "a saved datasource with no live driver" under a name that had no stored record. Two of them did this over HTTP (/ghost/test), so they pinned this defect directly. Their assertions are unchanged. Each fixture now stores that datasource's record:
    • primary-datasource-test-and-sync.test.ts: ghost was added to RECORDS, and the health-check unit's getDatasource returns a record.
    • saved-datasource-test-after-failed-connect.test.ts: ghost is seeded into the records map, and the verdict units' getDatasource returns a record.
  • Package suite on the final head 1ae90cf405. pnpm --filter @objectstack/service-datasource test gave Test Files 59 passed (59) and Tests 989 passed (989). typecheck (tsc --noEmit, which covers the three test files: --listFiles counts 3/3) exited 0. eslint --no-inline-config packages/services/service-datasource exited 0.
Ablation (scripts/ablation-replace.mjs, wrap mode)

The subject is imported from src by a relative import, so no dist is involved.

  • A1 replaced the existence guard with if (false ...). The new file went red with 2 failed | 3 passed (5): both unknown-name cases failed. The file was restored with blob 4750f4f6e7fb equal to HEAD, and git diff HEAD was empty.
  • A2 dropped the primary-datasource exemption. The new file went red with 1 failed | 4 passed (5): "the primary datasource with no stored record is still tested through its live driver" failed. The file was restored with blob equal to HEAD and an empty git diff HEAD.

Acceptance notes

  • Out of scope, unchanged. GET /datasources/NAME/remote-tables and POST /datasources/NAME/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR. The code path is assertFederationSource: getDatasource returns undefined, isFederationSource(undefined) is false, and the door throws notAFederationSourceError. This is a separate discrepancy, named in the claim. This PR did not measure it over HTTP.
  • A host that wires no metadata service now answers every non-primary name 404 at Test. GET /datasources/NAME already does the same in that host, because both read the same store.
  • datasource-route-ledger.ts is unchanged. Its POST /:name/test note does not state what the route answers.

Verification

The derived gate list comes from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1ae90cf405. Each command ran with its exit code captured before any pipe, and the record was reconciled with --ran: 62 derived, 61 run green (exit 0), 1 NOT MEASURED, 0 UNRUN.

  • NOT MEASURED: pnpm check:dual-build-cjs-loads exited 3 with PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. It needs a full-repo build, so the reading is left to CI.
  • Dogfood battery (owed by the (narrowing) arm, claim amendment 6498154139), on b09ea62c76: pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood test under the os-verify-lock, VERDICT command-exit 0, Test Files 350 passed | 3 skipped (353), Tests 2817 passed | 20 skipped (2837).
  • Overlap: none of the 30 open PRs touches any file in this diff, so no merge-tree was needed.
The 61 green families (exit 0)
  • node scripts/check-adr-0087-registration.mjs --base origin/main
  • node scripts/check-adr-0087-registration.mjs --self-test
  • node scripts/check-changeset-no-major.mjs --base origin/main
  • node scripts/check-closing-keyword-parity.mjs
  • node scripts/check-closing-keyword-parity.mjs --self-test
  • node scripts/check-comment-mask-adoption.mjs
  • node scripts/check-comment-mask-adoption.mjs --self-test
  • node scripts/check-comment-mask-corpus.mjs
  • node scripts/check-dts-emitted.mjs --self-test
  • node scripts/check-empty-changeset.mjs --base origin/main
  • node scripts/check-empty-changeset.mjs --self-test
  • node scripts/check-issue-citations.mjs
  • node scripts/check-plugin-teardown-shape.mjs
  • node scripts/check-plugin-teardown-shape.mjs --self-test
  • node scripts/check-tenant-audit-census.mjs
  • node scripts/check-tenant-audit-census.mjs --self-test
  • node scripts/ci/assert-turbo-remote-cache.mjs --self-test
  • node scripts/ci/plan-test-core-shards.mjs --self-test
  • node scripts/ci/prune-turbo-cache.mjs --self-test
  • node scripts/docs-audit/check-affected-docs.mjs
  • node scripts/docs-audit/check-drift-comment.mjs
  • node scripts/pm/release-rehearsal-clone.mjs --self-test
  • node scripts/release-pending-publish.mjs --self-test
  • pnpm --filter @objectstack/spec run check:duration-unit-keys
  • pnpm check:changeset-gate-self-tests
  • pnpm check:ci-filter-parity
  • pnpm check:closed-ceremony-text
  • pnpm check:cross-package-test-inputs
  • pnpm check:dispatcher-error-vocabulary
  • pnpm check:doc-authoring
  • pnpm check:driver-memory-census
  • pnpm check:dts-closure
  • pnpm check:engine-double-contract
  • pnpm check:error-status-conformance
  • pnpm check:gitlink-declared
  • pnpm check:issue-citations
  • pnpm check:keyed-text-bounds
  • pnpm check:lean-entry-closure
  • pnpm check:logger-receiver-detach
  • pnpm check:nul-bytes
  • pnpm check:objectql-double-limit
  • pnpm check:org-identifier
  • pnpm check:page-declaration-shape
  • pnpm check:platform-object-tenancy-census
  • pnpm check:pm-changeset-deadline-census
  • pnpm check:published-files
  • pnpm check:query-options-erasure
  • pnpm check:refd-timer-probe
  • pnpm check:registry-log-declared
  • pnpm check:rest-log-spy-declared
  • pnpm check:route-envelope
  • pnpm check:slot-lookup
  • pnpm check:sourcemap-no-sources-content
  • pnpm check:system-context-census
  • pnpm check:test-source-alias
  • pnpm check:tier-file-adoption
  • pnpm check:type-check-coverage
  • pnpm check:type-check-debt
  • pnpm check:undeclared-dep-imports
  • pnpm check:watch-hint-literal
  • pnpm check:where-matcher

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw

…own name (red)

An unknown saved name answers 200 { ok: false } at the Test door while
GET /datasources/:name answers the same name 404 RESOURCE_NOT_FOUND.
Controls: an existing datasource that fails its probe keeps { ok: false },
an existing one that is up answers ok, and the primary datasource with no
stored record is still tested through its live driver.

Claude-Session: https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
Co-authored-by: Claude <noreply@anthropic.com>
…ces/:name/test

Test resolved no saved record before probing, so a name no datasource has
answered 200 { ok: false } with the engine's driver-missing sentence, the
answer a saved datasource that is down gets. testConnection now reads the
stored record first (getDatasource, the same metadata read the sibling
:name doors make) and throws the 404 RESOURCE_NOT_FOUND envelope in GET's
words, which the route relays. The primary datasource is not looked up: it
may have no stored record and its live driver answers for it.

Fixtures that tested a saved datasource with no live driver now store that
datasource's record.

Claude-Session: https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 7 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/api/error-catalog.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/api/error-handling-client.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/automation/webhooks.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/data-modeling/drivers.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))
  • content/docs/kernel/cluster.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/permissions/sso.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/protocol/kernel/error-handling.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError), /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))
  • content/docs/ui/print-pages.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/releases/v17/17-1.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-5.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError))
  • content/docs/releases/v17/17-6.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-7.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))
  • content/docs/releases/v18/18-2.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError), /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1ed33609731b0bc3b2da81c97242e9c8582d003d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4046c96620112657b99fe797c308ed670c0d5c2d — the merge of head b09ea62c764f4d63a128b14220f87b00f4254be3 into base 1ed33609731b0bc3b2da81c97242e9c8582d003d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4046c96620112657b99fe797c308ed670c0d5c2d && git checkout 4046c96620112657b99fe797c308ed670c0d5c2d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ed33609731b0bc3b2da81c97242e9c8582d003d b09ea62c764f4d63a128b14220f87b00f4254be3 && git checkout -B drift-repro 1ed33609731b0bc3b2da81c97242e9c8582d003d && git merge --no-ff b09ea62c764f4d63a128b14220f87b00f4254be3

node scripts/docs-audit/affected-docs.mjs --json 1ed33609731b0bc3b2da81c97242e9c8582d003d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1ed33609731b0bc3b2da81c97242e9c8582d003d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…owing

Clause-2 moves from no to no (narrowing): a request answered 200 is now
refused 404. Adds the BREAKING banner (before / now / unchanged / remedy)
and the ADR-0087 not-required (no-migration-prescription) marker.

Claude-Session: https://claude.ai/code/session_01L6bhgXcN9HaZYSEUdJWEjw
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(service-datasource): POST /datasources/:name/test answers an unknown name 404 RESOURCE_NOT_FOUND fix(service-datasource)!: POST /datasources/:name/test answers an unknown name 404 RESOURCE_NOT_FOUND Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Contract-review-claim: session_01L6bhgXcN9HaZYSEUdJWEjw · head b09ea62 · 2026-10-29T14:41Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b09ea62c764f4d63a128b14220f87b00f4254be3
Local-runs: none
Scope: packages/services/service-datasource/src/external-datasource-service.ts, packages/services/service-datasource/src/admin-routes.ts, packages/services/service-datasource/src/tests/saved-datasource-test-unknown-name.test.ts, packages/services/service-datasource/src/tests/primary-datasource-test-and-sync.test.ts, packages/services/service-datasource/src/tests/saved-datasource-test-after-failed-connect.test.ts + packages/services/service-datasource/src/plugin.ts (liveDriver, getDatasource wiring), packages/services/service-datasource/src/datasource-admin-plugin.ts (getDatasourceRecord, servedOrigin), packages/services/service-datasource/src/datasource-admin-service.ts (datasourceNotFoundError, getDatasource), packages/services/service-datasource/src/datasource-connection-service.ts (DEFAULT_DATASOURCE_NAME, registeredDriverName, attemptConnect), packages/services/service-datasource/src/datasource-route-ledger.ts and its conformance test, packages/services/service-datasource/src/index.ts and package.json exports, packages/services/service-datasource/src/tests/datasource-unknown-name.test.ts, packages/plugins/plugin-hono-server/src/adapter.ts (readRouteParams) with hono 4.13.12 request.js, packages/core/src/fallbacks/memory-metadata.ts, packages/runtime/src/app-plugin.ts and default-datasource-plugin.ts (registerInMemory('datasource', …)), ui/packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx (the console caller), precedent commit 6279efef6f (.changeset/26641-explain-unknown-user-refused.md).

Face: narrowing (Clause-②: no (narrowing) on the PR body and the changeset). Inputs: card #27510 body, triage 6495190501, claim 6496102898, amendment 6498154139, os-dev-reports 6498107334 and 6499695437; PR #27574 title, body, file list; git diff origin/main...b09ea62c read in the detached main tree (merge-base 881af5d2de); the head's check-runs read at 2026-10-29T14:42Z: 52 runs, all completed, 0 failures (success or skipped; the two skipped re-runs of Auto Label and Check PR Size at 14:40Z follow the body edit and sit beside their earlier success runs). PR author objectstack-fleet[bot] (relay write): no off-relay note owed.

① Derived judgments

  1. The narrowing, as the tree does it. ExternalDatasourceService.testConnection(name) now opens with if (name !== DEFAULT_DATASOURCE_NAME && !(await this.config.getDatasource(name))) throw datasourceNotFoundError(name), placed BEFORE the method's try. config.getDatasource is the required member the plugin wires to metadata()?.get('datasource', n) (plugin.ts:181), the same read getDatasourceRecord makes for the sibling doors (datasource-admin-plugin.ts:413). The thrown error carries status 404 + code 'RESOURCE_NOT_FOUND' + the message Datasource "NAME" does not exist., and badRequest (admin-routes.ts:651) relays exactly that pair as 404 RESOURCE_NOT_FOUND. That is the same status, code and words GET /datasources/:name writes inline (admin-routes.ts:749) and the admin service's own datasourceNotFoundError (datasource-admin-service.ts:346) throws for PATCH, DELETE and migrate-credential. Right: the narrowing is exactly "a non-primary name with no stored record: 200 {ok:false} → 404 RESOURCE_NOT_FOUND".
  2. Nothing else that answered 200 is now refused. Every path after the guard is byte-identical to main: a stored datasource with a live driver is probed (checkHealth or introspect), one with no live driver and a failed verdict answers the verdict's reason, one with no live driver and no verdict falls to introspect and its engine sentence; every one of those still returns {ok:false} inside the try, never throws. The primary name default skips the lookup entirely, so a default with or without a stored record is probed as before. Right.
  3. Public surface of @objectstack/service-datasource. exports map (. and ./contracts) and src/index.ts are unchanged on the head; the new datasourceNotFoundError in external-datasource-service.ts is module-private (no export), so no helper is published. The one published surface that moves is the in-process ExternalDatasourceService.testConnection (class exported from .): it gains a throw path for a name with no record, which is a narrowing of its accept set and is named verbatim in the Clause-② line. Its return type is untouched. Right, and the declaration covers it.
  4. Host with no metadata service. metadata() is undefined ⇒ getDatasource resolves undefined ⇒ every non-default name answers 404 at Test. GET /datasources/:name and the list already answer 404 / empty in that host (getDatasourceRecord and listDatasourceRecords read the same optional service). The door now agrees with its siblings there too. Right; see ③.5 for the one shade.
  5. Code-declared datasource that is not a stored record. In first-party hosts it does not arise: AppPlugin registers every defineStack({ datasources }) entry with metadata.registerInMemory('datasource', name, …) (app-plugin.ts:1328-1338) and DefaultDatasourcePlugin registers default the same way (default-datasource-plugin.ts:288); the kernel's memory fallback implements registerInMemory (memory-metadata.ts) so the standalone boot path also lists them. A metadata service without registerInMemory leaves code datasources out of the list and out of GET /:name already; Test now joins them. Right: "exists to the engine but has no record" is exactly what the sibling doors already call unknown.
  6. Fault vs absence. The guard sits outside the try, so a metadata.get that THROWS propagates out of testConnection as a plain error and badRequest answers it 400 EXTERNAL_DATASOURCE_ERROR, not 404. A store fault is never read as "does not exist", matching the fix(plugin-security)!: security explain refuses a userId that no sys_user record has with 404 RECORD_NOT_FOUND #26801 precedent's stance and GET /:name's own 400 DATASOURCE_ADMIN_ERROR on a throwing store. Right. Noted, not a defect: a store fault now refuses Test for a datasource whose live driver is up, where before Test never consulted the store; that is the price of the lookup and the siblings pay it too.
  7. Order of checks. requireDatasourceAdmin (401/403) and resolve (503 when the service is absent) run before the lookup, as at GET /:name; an unauthenticated caller learns nothing about which names exist. Right.
  8. Route handler and ledger. admin-routes.ts changes two docblocks only; the handler at :756 is unchanged, as the PR says. datasource-route-ledger.ts is unchanged; its POST /:name/test row states the route's purpose and caller, not its answer, and its conformance test checks presence, not status. Right.
  9. Primary name. DEFAULT_DATASOURCE_NAME is the host-reserved 'default', the only name connect registers asDefault; registeredDriverName resolves the live driver by that same name test, so the exemption and the probe agree on which datasource is primary. Right.

② Semver level

  • .changeset/27510-datasource-test-unknown-name-404.md: '@objectstack/service-datasource': patch, title with !, Clause-②: no (narrowing) — … identical to the PR body's line, Metadata-protocol: none, adr-0087: not-required (no-migration-prescription) marker, **BREAKING** accept-set narrowing … banner with Before / Now / Unchanged / Who sees it / The remedy. This is the fix(plugin-security)!: security explain refuses a userId that no sys_user record has with 404 RECORD_NOT_FOUND #26801 shape (6279efef6f, .changeset/26641-explain-unknown-user-refused.md: patch, !, same marker text pattern, same banner sections) applied to this door. Matches.
  • The level is right for what the diff publishes: no schema key, export or type moves; the request and the { ok, latencyMs, tableCount, error } answer keep their shape; Metadata-protocol: none holds (no packages/spec file, no metadata-protocol surface in the diff). A declared narrowing at patch with the banner is the repo's precedent and check-changeset-no-major / check-adr-0087-registration / Check Changeset are green on the head.
  • The Unchanged bullet's claim "every other outcome of testConnection is still returned, never thrown" is true of the tree (①.2). The Who sees it bullet names the platform-settings caller; see ③.5 for the host-without-registerInMemory shade it does not name (no action required).

③ Boundary flags

  1. File-surface extension (two fixtures). Amendment 6498154139 adds primary-datasource-test-and-sync.test.ts and saved-datasource-test-after-failed-connect.test.ts as fixture-only edits. Read: in the first, ghost is added to RECORDS (never connected, so its Test keeps the "no live driver" subject; the file's list pin at :185 looks up default by name, unaffected) and the health-check unit's getDatasource returns { name }; in the second, boot() returns records, the HTTP pin seeds ghost before POST /ghost/test, and the verdict units' getDatasource returns { name }. Every assertion is unchanged. The edits are forced by the fix (those pins drove /ghost/test with no record and so pinned the defect) and are exactly the amendment's description. Answered: accepted.
  2. remote-tables / object-draft answer an unknown name 400. Read: assertFederationSource (external-datasource-service.ts:604-606) throws the primary refusal for default, then getDatasource; an undefined record falls into isFederationSource(undefined) === false and the not-a-federation-source error (400). It is a real fifth instance of the rest: three doors answer an unknown resource as empty or bad input (publish-drafts 200, flow runs 200 [], datasource PATCH 400) where their siblings answer 404 #25486 family, outside this card's ruling (the triage names only the Test door). Escalated, not blocking: the dev marked it "noted, not filed"; the seat should file it as its own card (dedupe words in the report) so the family is closed, since this PR's new docblock at admin-routes.ts:86-89 now says "every datasource-admin door that takes :name answers it 404 … and so does POST /datasources/:name/test", which is true as written (the two 400 doors are external-datasource doors) but leaves the reader to notice the carve-out.
  3. datasource-route-ledger.ts unchanged. Right under the claim's own condition (①.8). Answered.
  4. Absent-metadata host. Answered at ①.4: Test now agrees with GET /:name in that host. Answered.
  5. Shade for the record. A host whose metadata service lacks registerInMemory holds no record for code-declared datasources; Test previously probed them by live driver and now answers 404, as list and GET /:name already did. No first-party metadata service lacks the member (MetadataManager, memory fallback). Not a finding; the declaration's "a name that resolves no saved datasource" covers it literally.
  6. Dev deviations. "No Skills-docs line" is superseded: the PR body carries Skills-docs: none — … with the grep it rests on; the changeset carries none, as the fix(plugin-security)!: security explain refuses a userId that no sys_user record has with 404 RECORD_NOT_FOUND #26801 changeset carried none. "Commit trailers use the model-free pair" is a process note outside this review's contract face; recorded, no ruling.
  7. open_questions: empty in both reports. PR author is objectstack-fleet[bot]; no off-relay login to name.
  8. Console caller. DatasourceResourcePage.tsx:311-319: api() throws on a non-2xx, so a 404 surfaces as NAME: Datasource "NAME" does not exist. in the error toast instead of the former "no registered driver" text; the console only tests names it listed, so this is reached only by a concurrent delete. No consumer change owed.

④ Design judgment

  • external-datasource-service.ts — holds. The lookup is the sibling's lookup (①.1), the exemption is the reserved name (①.9), the guard sits outside the try so a store fault stays a fault (①.6), and every other outcome is byte-identical (①.2). Probes: default with a record → not looked up, probed by registeredDriverName → unchanged; default without a record → same; a stored datasource whose driver is down (no live driver, failed verdict) → verdict reason, 200 {ok:false}; stored, no live driver, no verdict → engine sentence, 200 {ok:false}; stored and up → 200 {ok:true}; a name in odd casing → the store's key is the key, identical at GET /:name, so both doors agree (404 or found); URL-encoded name → Hono's param() percent-decodes (tryDecodeURIComponent, hono 4.13.12 request.js:50) and readRouteParams passes it through, so no%20such is looked up as no such at both doors; a metadata read that throws → 400, not 404; the in-process caller gets a throw where it got {ok:false} — declared in the Clause-② line, and no in-process caller exists outside the package (grep over packages/ and apps/). No must-fix. Should-fix (non-blocking): datasourceNotFoundError is now a second private copy of the admin service's helper with the same words; the new test holds the two doors' answers equal so drift is caught, but one shared helper (e.g. beside importNameRefusedError's pattern, or exported from one module to the other) would make the 404 envelope one fact instead of a pinned pair. Carry forward, not a condition of this PR.
  • admin-routes.ts — holds. Docblock-only; both sentences are true of the tree (badRequest relays the pair; the throwers now include testConnection). The handler is unchanged, so the relay is the one tested at datasource-unknown-name.test.ts for the four siblings.
  • saved-datasource-test-unknown-name.test.ts — holds against the ruling. Real composition (ObjectQL, connection service, driver factory, admin service, ExternalDatasourceServicePlugin.init, admin routes on Hono), entitled caller, both services reading the one records map. The five pins are the triage's two clauses plus the three controls, and the parity pin compares status, code and message with GET /:name for the same name. Red-first and ablations A1/A2 are reported; this record relies on the head's green Test Core, not on the report.
  • primary-datasource-test-and-sync.test.ts, saved-datasource-test-after-failed-connect.test.ts — hold. Fixture records only; assertions unchanged; each edit is commented with the reason and the pinning file (③.1).

Implemented-by: claude/issue-27510-datasource-test-unknown-404
Reviewed-by: subagent of session_01L6bhgXcN9HaZYSEUdJWEjw (domain:server#1 contract reviewer)

VERDICT: PASS

Reviewed 2026-10-29T14:47Z.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw

维护者速读(席位终稿)

  • 改了什么:POST /api/v1/datasources/NAME/test 遇到不存在的数据源名,现在返回 404 RESOURCE_NOT_FOUND(与 GET /datasources/NAME 同码同文案),不再返回 200 {ok:false}。
  • 为什么改:datasources: POST /datasources/NAME/test answers an unknown name 200 {ok:false} where every sibling NAME route answers 404 (the #25486 family) #27510。「数据源不存在」和「数据源连不上」原来返回一模一样,管理工具和脚本无法区分;其它按名字访问的接口早已统一为 404。
  • 风险与回滚:低。只收窄「查无此名」这一种请求(Clause-② 收窄)。已存在但连不上的数据源仍是 200 {ok:false},主数据源 default 没有存储记录也照常测试。回滚就是还原这一个 PR。
  • 先看:packages/services/service-datasource/src/external-datasource-service.ts 的 testConnection 开头新增的存在性检查。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/saved-datasource-test-unknown-name.test.ts,预期 5 个用例全绿。
  • 需要你做的:无。Tier S,契约复核 PASS 在案(6499920933),席位落地。

席位意见:接受。认领时席位把声明写成了 no,后经认领修正 6498154139 改为收窄(先例 #26801),标题、changeset 和 dogfood 已一并补齐;复核一轮通过,无必修项。

席位核对 — ACCEPT · domain:server#1 · head b09ea62 · 2026-10-29T14:54Z

  • Shape: ExternalDatasourceService.testConnection(name) now opens with an existence guard, outside the method's try: name !== DEFAULT_DATASOURCE_NAME && !(await this.config.getDatasource(name)) → datasourceNotFoundError(name) (404, RESOURCE_NOT_FOUND, GET's words). getDatasource is the same metadata.get('datasource', name) read the sibling doors make. The existing badRequest relay answers it 404, so the route handler is unchanged. Every path after the guard is byte-identical to main.
  • Premise: claim 6496102898 with amendment 6498154139 holds; Fixes #27510; triage 6495190501.
  • Gates: 52 check-runs on this head: 36 success, 16 skipped, 0 failed, 0 pending. check-expected-skips --pr 27574 (fresh origin/main): VERDICT … OK — 16 skipped check-run(s), every one in main's roster (exit 0). mergeable_state: clean. Dev (os-dev-reports 6498107334, 6499695437):
    • red-first: 2 failed / 3 passed;
    • two ablation legs, each red and restored to the HEAD blob;
    • package 59 files / 989 tests;
    • dispatch-gates --ran 61 exit 0, 1 NOT-MEASURED (check:dual-build-cjs-loads, full build; CI green);
    • dogfood battery exit 0 (350 files passed).
  • Contract review: owed (narrowing face), served: PASS 6499920933 on this head, CONTRACT_REVIEW_TIER, no must-fix.
  • Prose read against the diff: changeset @objectstack/service-datasource: patch, **BREAKING** banner, ADR-0087 marker not-required (no-migration-prescription), matching the fix(plugin-security)!: security explain refuses a userId that no sys_user record has with 404 RECORD_NOT_FOUND #26801 precedent. The two admin-routes.ts docblocks list the Test door among the by-name 404 doors.
  • Accepted deviations: the two fixture-only test files are added to the file surface (amendment 6498154139). Their pins drove /ghost/test and pinned this defect; their assertions are unchanged. The reviewer's should-fix (a second private copy of datasourceNotFoundError) is carried to datasources: GET /datasources/NAME/remote-tables and POST /datasources/NAME/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR where GET /datasources/NAME answers 404 (the #25486 / #27510 family) #27618, where sharing one helper is the natural fix.
  • Declaration: Clause-②: no (narrowing), Metadata-protocol: none, Skills-docs: none (no page states what the Test door answers). The title carries !.
  • Out of scope: GET /:name/remote-tables and POST /:name/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR (measured over HTTP): filed as datasources: GET /datasources/NAME/remote-tables and POST /datasources/NAME/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR where GET /datasources/NAME answers 404 (the #25486 / #27510 family) #27618.
  • Landing: Tier S, NOT governed, 254 changed lines ≤ 3000 → pr_ready + automerge_enable via the relay now.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 14:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit e270dab Oct 29, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27510-datasource-test-unknown-404 branch October 29, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

datasources: POST /datasources/NAME/test answers an unknown name 200 {ok:false} where every sibling NAME route answers 404 (the #25486 family)

1 participant