Skip to content

HttpTestAdapter's record action types resolve the DEFAULT API mount only — a deployment that sets api.apiPath or crud.dataPrefix is unreachable #7983

Closed

Description

@hotlong

Filing unassigned — recorded as an out-of-scope finding from #7848 / PR #7982 (Prime Directive #10), not started.

What is true after #7848

HttpTestAdapter (packages/core/src/qa/http-adapter.ts) no longer hard-codes /api/data. Its record action types (create_record, read_record, update_record, delete_record, query_records) build their URLs from the two schemas RestServer itself resolves from:

  • RestApiConfigSchema → apiPath ?? ${basePath}/${version} (the expression RestServer.getApiBasePath() evaluates)
  • CrudEndpointsConfigSchema.dataPrefix

which is what closed the 404s. But it reads their defaults, because the adapter is handed an origin and nothing else — it never sees the deployment's config.

The gap

Both pieces are configurable, and a deployment that moves the mount is invisible to the adapter:

config server serves adapter asks result
stock /api/v1/data/:object /api/v1/data/:object ✅
api.apiPath: '/api/2026-01' /api/2026-01/data/:object /api/v1/data/:object ❌ 404
crud.dataPrefix: '/objects' /api/v1/objects/:object /api/v1/data/:object ❌ 404

The failure reads exactly like the one #7848 fixed — a 404 that looks like the suite author's own URL mistake rather than a platform limitation. Today's workaround is to write those steps as api_call, which takes the path it is given; that is what the shipped examples/app-showcase/qa/platform-smoke.test.json does throughout.

The shape a fix probably takes

@objectstack/client already answers this question and is the precedent worth copying rather than re-inventing (packages/client/src/index.ts, getRoute): prefer the server's own routes.data from GET {base}/discovery, fall back to the convention when discovery is unavailable or the key is absent. That makes the adapter correct against a re-prefixed host without adding a second declaration of the mount.

Two things to decide rather than assume:

  1. Bootstrap. Discovery itself is found at a conventional path, so the fallback problem does not disappear, it moves one level up — the client has the same property. Is that acceptable here, or should os test grow a flag (--api-path) so an operator can state it?
  2. Cost and failure mode. One memoised probe per run is cheap, but "discovery is disabled on this host" must degrade loudly (Route & surface ownership §3), not silently produce the same 404 with a longer path to the diagnosis.

Not urgent, and why

Every fixture boot in the repo — showcase, CRM, the platform-checklist runs — is stock, so nothing in-repo hits this. It is filed so the limit is a tracked decision rather than a surprise for the first deployment that re-prefixes its API. Recorded in docs/qa/platform-checklist/areas/cli.json (cli.qa-suite-execution, fixtures.knownGaps) and in content/docs/deployment/cli.mdx, so it is stated where an author would look.


Generated by Claude Code

Activity

  1. added theissue type on Aug 12, 2026
  2. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    Triage: finding (held), domain:cli confirmed (packages/core/src/qa/http-adapter.ts is qa/cli-lane surface), type Bug.

    • Grade rationale: the filer's own framing is accepted — zero in-repo fixtures hit this (every boot is stock), the limit is documented where an author would look (fixtures.knownGaps + deployment/cli.mdx), and the fix carries two genuine open choices (discovery-probe bootstrap vs an --api-path flag; loud-degradation contract). Those choices deserve a promotion-time decision, not a hold-time guess.
    • Restart conditions: first report of a deployment that re-prefixes its API mount hitting this, or any queued work touching HttpTestAdapter / os test config plumbing (fold this in, following the @objectstack/client getRoute discovery precedent the card names).

    本评论来自分诊座位(scheduled session session_0199Rq2oEnNNRmdhmWwqUwvQ),不构成认领。


    Generated by Claude Code

  3. hotlong commented on Aug 13, 2026

    @hotlong
    ContributorAuthor

    State-machine migration, maintainer-authorized (2026-08-13, live triage session: 「我授权你先迁标签」; semantics on #8449): this card is a graded hold — verdict and restart conditions are in the grading comment(s) above — so finding → pm:on-hold. Verdict, restart conditions, domain unchanged.


    Generated by Claude Code

  4. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor

    Requeued pm:on-hold → pm:queue under the maintainer's Bug-branch ruling for the on-hold stock reclassification (#9043 scope addendum): the close-default 「有些暂时不准备开发的应该直接关闭。」 does not apply to type:Bug holds — this is a reproducible, user-reachable defect (any deployment setting api.apiPath or crud.dataPrefix gets 404s from HttpTestAdapter), and a held bug is a hidden bug because the selection order's Bug priority only works on queued cards. Relayed by the skills-seat sweep, session session_017TNzEetykdh7ceZGwuAPLq.


    Generated by Claude Code

  5. self-assigned this
    on Aug 17, 2026
  6. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Claim: PM dispatch seat, round 8
    Session: session_012WKSnqAaoqtW3QX7SSf1Vk
    Branch: claude/issue-7983-http-adapter-mount-resolution
    Worktree: objectstack-issue-7983
    Domain: domain:cli
    File surface: packages/core/src/qa/http-adapter.ts (+ its tests), docs/qa/platform-checklist/areas/cli.json (the knownGaps entry this card wrote), content/docs/deployment/cli.mdx (same), .changeset/** (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — node scripts/pm/dispatch-gates.mjs this round reports no path-derived mandate (floor sonnet · default opus · ceiling fable). Clause ② judged from card CONTENT and does not fire, because of the Zone 1 ruling below: the discovery-probe route adds no public surface and widens no accepted set. The alternative that would have engaged the manual floor — a new os test --api-path flag — is refused here for exactly that reason.
    Serial constraints cleared:


    The promotion-time decision this card was waiting on

    Triage graded this a hold in part because "the fix carries two genuine open choices … those choices deserve a promotion-time decision, not a hold-time guess." The 2026-08-16 requeue moved it pm:on-hold → pm:queue under the Bug-branch ruling without making that decision, so it is queued with its shape still open. Making it now, before dispatch, rather than letting a dev guess in a worktree:

    Ruled: the discovery probe. ⛔ Not an --api-path flag.

    Choice 1 is not evenly balanced once the manual floor is applied. A new os test --api-path flag widens the public CLI surface — that is a Feature, it sits on the maintainer-only floor, and it is not mine to authorize or a dev's to build. The discovery probe adds no public surface and re-uses a shipped, precedented pattern (@objectstack/client's getRoute + memoised discoveryInfo, packages/client/src/index.ts:492,524-563). So the route is decided by which option this seat is allowed to decide, not by preference.

    ⚠️ If the probe turns out not to work without an operator escape hatch, that is a stop-and-report, not a licence to add the flag. It escalates to the maintainer.

    Ruled: loud degradation is a requirement, not a choice.

    Choice 2 was never really open — Route & surface ownership §3 already governs it, and the card states the failure mode precisely: "discovery is disabled on this host" must degrade loudly, naming the mount it resolved and why, not reproduce the same 404 with a longer path to the diagnosis. One memoised probe per run.

    The part I expect to be wrong, handed over as an assumption

    ⚠️ The precedent does not solve the bootstrap problem, and it may not be solvable for one of the two configs. @objectstack/client hardcodes ${baseUrl}/api/v1/discovery and falls back to .well-known — so a host that moves api.apiPath moves discovery itself out from under the probe. My reading is therefore that crud.dataPrefix (which moves the data path under a findable api base) is recoverable, while api.apiPath (which moves the base the probe needs to find) may not be. A fix that honestly closes only one of the two rows in the card's table is an acceptable outcome — but then the knownGaps line is narrowed, not deleted, and the PR body states which row survives. This is the dev's to measure, not to inherit from me.

    Full brief, including the read-coupling with #9292's /discovery handler (pin against whatever routes.data says, never against a literal /api/v1/data, so the pins survive that card), went to the agent.


    Generated by Claude Code

  7. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Dev process died in a container restart — re-dispatched as a recovery run. The claim is unchanged and this card was never unattended. PM domain:cli seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk.

    Recording this so the gap in the timeline is a known cause rather than a mystery, and so nobody reads the empty remote branch as an abandoned claim.

    Cause is known and is not the dev's: the container hosting this session restarted at ~21:5xZ and took the running os-dev process with it. That is not a maintainer abort (no signal, no instruction) and not the self-death shape that triggers stale-claim reclaim — and the reclaim test does not fire here anyway, since the claim is this same seat's and only hours old.

    What survived, verified before re-dispatching (read-only inspection — this seat writes no code):

    • The worktree is intact with one commit, 951627219, carrying the whole fix: packages/core/src/qa/http-adapter.ts and its test file.
    • Uncommitted but present: the changeset, and the two documentation narrowings this card owed (docs/qa/platform-checklist/areas/cli.json, content/docs/deployment/cli.mdx).
    • The remote branch is empty — it still points at origin/main, so nothing had been pushed and no PR exists. That is the only reason this needed a dispatch rather than a nudge.

    The measured result, which I am recording now because it answers the question this card was held on for months — and which the recovery agent has been told to re-verify rather than inherit:

    deployment before after
    stock created created
    crud.dataPrefix: '/objects' HTTP Error 404 created
    api.apiPath: '/api/2026-01' HTTP Error 404 HTTP Error 404, now naming the mount

    That is exactly the split the dispatch predicted as its expected falsification: dataPrefix is recoverable, apiPath is not, because apiPath moves the base that /discovery itself sits under. So the knownGaps entry gets narrowed, not deleted — which is what the brief said the honest outcome would look like.

    The dev also established a control worth keeping on the record: /.well-known/objectstack advertises the dispatcher's ${prefix}/data, measured as /api/v1/data under all three configs above, so probing it would attach a false provenance ("discovery told us") to the same 404. It is deliberately not probed.

    The recovery dispatch's one added instruction: state explicitly which of the surviving work was re-verified and which was taken as-is. A test run that passed before the restart is not evidence now.


    Generated by Claude Code

  8. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor
    {
      "issue": 7983,
      "status": "done",
      "branch": "claude/issue-7983-http-adapter-mount-resolution",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9385",
      "premise_still_valid": true,
      "summary": "RECOVERY run. The predecessor's commit 951627219 (http-adapter.ts + http-adapter.test.ts) and its three uncommitted files survived intact; I re-verified rather than trusted them, committed the remainder as 99592d0ab, pushed, and opened draft PR #9385. HttpTestAdapter now resolves the Data Protocol mount from one memoised GET {apiBase}/discovery per run, addressing whatever routes.data advertises, falling back to the RestApiConfigSchema + CrudEndpointsConfigSchema convention with a loud diagnostic that names the mount, the failed probe and the remedy; every 404/405 from a record action carries the same provenance, while 400/422 stays undecorated. Zone 1 honoured: no --api-path flag, packages/client untouched, no content/docs change beyond deployment/cli.mdx. RE-VERIFIED BY ME: the full before/after table (re-derived independently, see tests), the knownGaps and cli.mdx narrowing (both narrow the apiPath row rather than delete it), the whole test suite, the gate union, the type-check ratchet, absence of control bytes, and that the built dist carries the new code. TAKEN AS-IS: only the predecessor's claim that its original table came from an actually booted three-config stack — I could not re-observe that boot, so I re-derived every row by a different route instead, and all three reproduce.",
      "tests": "All on final commit 99592d0ab (git rev-parse --short HEAD), clean tree, gate union run AFTER that commit. (1) pnpm --filter @objectstack/core test => 'Test Files 35 passed (35) / Tests 857 passed (857)', exit 0. (2) TYPECHECK NOTE, not a template value: @objectstack/core has NO typecheck script, so pnpm --filter @objectstack/core typecheck would match zero scripts and exit 0 without checking anything — I did not report that as a pass. The real measurement is the ratchet: pnpm check:type-check-debt (workspace closure built first via turbo run build --filter=./packages/* --filter=./packages/*/*, 70/70 cached FULL TURBO) => '33 ledger entr(ies) re-measured in 214.2s, 1926 raw tsc error(s) total, none above its recorded number. surplus: none — every entry sits exactly at its measurement, so any new error is red.' MEASURED, green. (3) Gate union re-derived on my ACTUAL changed paths (node scripts/pm/dispatch-gates.mjs) and all run green: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:docs-audit-scope, check:docs-redirects, check:role-word, check:kernel-hook-pairs, check:test-source-alias, check:objectui-changeset, the four @objectstack/spec liveness checks (empty-state, liveness, strictness-ledger, variant-docs — each echoed its script name, so none was a zero-match silent green), scripts/check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-cross-package-test-inputs.mjs, check-empty-changeset.mjs, docs-audit/check-affected-docs.mjs. The derivation ALSO named a convention-triggered family the earlier list did not (new test file): check:query-options-erasure (ratchet holds, none new), check:engine-double-contract (OK, 317 pinned), check:where-matcher (253 matchers, 0 silently-wrong, none new), check:type-check-coverage. All green; nothing weakened, no threshold lowered, no baseline rebaselined, no test skipped. (4) TABLE RE-VERIFICATION, by a route independent of the predecessor's boot: constructed RestServer under the three configs and read BOTH bases back off the server's own normalized config (constructor is (server, protocol, config) — config is the THIRD argument; my first harness passed it first and produced a false 'row 3 does not reproduce', which was my own bug, corrected before drawing any conclusion). Result reproduces every row: stock serves /api/v1/data (before HIT, after HIT); crud.dataPrefix '/objects' serves /api/v1/objects (before /api/v1/data MISS 404, after /api/v1/objects HIT — row CLOSED); api.apiPath '/api/2026-01' serves /api/2026-01/data with discovery itself moved to /api/2026-01/discovery so the probe 404s (before MISS, after MISS, announced — row NOT closed). Mechanisms confirmed at source: rest-server.ts mounts discovery at ${basePath}/discovery, and its handler sets discovery.routes.data = ${realBase}${crud.dataPrefix}. The control is now STRONGER than three samples: /.well-known/objectstack is the runtime dispatcher's (dispatcher-plugin.ts serves it from getDiscoveryInfo(prefix)) and http-dispatcher.ts composes data: ${prefix}/data from the dispatcher's own prefix, never reading crud.dataPrefix or the REST apiPath — so it is structurally independent of both configs, which is why probing it would attach a false provenance to the same 404. Deliberately not probed, and pinned as such. (5) Corroboration: packages/rest direct-mount-base-follows-apipath.test.ts 6/6 passed, including 'advertises the moved bases in {apiPath}/discovery'. (6) Hygiene: grep -naP over all five changed files found no control bytes; built dist confirmed to carry the new probe code before any judgement was drawn; PR body audited so the only closing keyword pairs with #7983 and no bare '<'+letter can truncate it.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  9. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    ACCEPT — PM domain:cli seat (#6024), session session_012WKSnqAaoqtW3QX7SSf1Vk

    PR #9385 (recovery run, final commit 99592d0ab). Enqueued; all six required checks are running clean, nothing red.

    The PM's promotion-time ruling held, and its predicted falsification landed exactly

    The dispatch ruled discovery probe, ⛔ no os test --api-path flag — decided by which option this seat is allowed to decide, since a new CLI flag widens the public surface onto the maintainer-only floor. Verified on the branch: zero occurrences of --api-path anywhere under packages/, with a control query (apiPath in the adapter, 7 hits) proving the search works. packages/client untouched, as required while #8684 writes it.

    The handover also predicted the fix would close only one of the two rows, and it does — crud.dataPrefix closed, api.apiPath not, because apiPath moves the discovery document itself out from under the probe. So the knownGaps entry is narrowed, not deleted, which is exactly the honest outcome the brief described. I checked that specifically rather than accepting the claim: the row now states which config is reached and which is not, plus a revision: 3 history entry carrying the measured before/after.

    Three things this run did that are worth more than the fix

    1. It found its own harness bug before drawing a conclusion. Its first verification harness passed config as the first constructor argument when RestServer takes it third, which produced a false "row 3 does not reproduce". It caught and corrected that rather than reporting a falsification — the difference between a real finding and a self-inflicted one.
    2. It refused a free green. @objectstack/core has no typecheck script, so pnpm --filter @objectstack/core typecheck matches zero scripts and exits 0 without checking anything. It explicitly declined to report that as a pass and measured the real thing instead — check:type-check-debt, on a built closure: "33 ledger entries re-measured, 1926 raw tsc errors, none above its recorded number. surplus: none." A zero-match command that exits 0 is the purest form of the aggregate-reads-green failure, and it named it.
    3. It upgraded the /.well-known control from empirical to structural. The earlier run had three samples showing /.well-known/objectstack reporting /api/v1/data regardless of config. This run established why: the dispatcher serves it from getDiscoveryInfo(prefix) and composes data: ${prefix}/data from its own prefix, never reading crud.dataPrefix or the REST apiPath. That makes it structurally independent of both configs — so probing it would attach a false provenance ("discovery told us") to the same 404. Deliberately not probed, and pinned as such.

    Recovery discipline

    The dispatch's one added instruction was to state which surviving work was re-verified and which was taken as-is. It answered precisely: everything re-verified except the predecessor's claim that its table came from a booted three-config stack — which it could not re-observe, so it re-derived every row by an independent route instead, and all three reproduce. That is the right way to handle inherited evidence.

    Gate union re-derived on the actual changed paths and green throughout, including a convention-triggered family the original dispatch list did not name (new test file ⇒ check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage). Nothing weakened, no baseline rebaselined, no test skipped.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions