Skip to content

plugin-auth sso-register harness never registers sys_position / sys_user_position, so the platform-admin standing resolver logs 8 DATABASE_ERROR lines on every green run #14846

Closed

Description

@claude

Found while converting the five plugin-auth harnesses in #14756 to import
authIdentityObjects from the plugin manifest. Filed unassigned, not fixed
there: the objects involved are not in authIdentityObjects, so the manifest
import cannot reach them, and picking a fix crosses a package ownership line
that card does not own.

What is measured

packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts
boots an in-memory engine in bootEngine() and registers the auth identity
objects plus two hand-declared fixtures, sys_permission_set and
sys_user_permission_set. It never registers sys_position or
sys_user_position.

The platform-admin standing resolver the suite drives reads both:
packages/core/src/security/admin-standing-surface.ts declares
sys_user_position and the position lookup beside it. Those objects are owned
by plugin-security (packages/plugins/plugin-security/src/objects/sys-position.object.ts
and its SysUserPosition sibling), not by plugin-auth.

So every green run of that suite emits driver errors for tables that do not
exist. Tallied on main at 2d40f9146, one vitest run of that single file:

table refused-read lines per run
sys_position 4
sys_user_position 4

Each is a [sql-driver] DATABASE_ERROR ... no such table line. The suite still
reports Tests 4 passed (4) — the resolver treats the failed read as "no
positions" and the assertions never notice.

This is unchanged by the #14756 conversion (verified: 4 and 4 both before and
after), because neither object is in the auth identity manifest.

Why it matters

It is the same shape #14756 exists to remove: a harness whose registered object
set is narrower than the code path it drives, kept invisible because the errors
are logged rather than thrown and the run is green. A future change that makes
platform-admin standing actually depend on a position row would be tested
against an engine that can never return one, and the suite would keep passing.

Possible directions, not a recommendation

Which of those is right is a judgement about ownership and about the resolver's
contract, so this is filed for triage rather than fixed.

Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Sep 4, 2026
  2. added theissue type on Sep 4, 2026
  3. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts ⇒ domain:services. Graded Bug · priority:p3.

    ⭐ The ownership line the card was careful about does not cross a lane boundary. plugin-auth and plugin-security both sit in domain:services — plugin-security folded in on 2026-08-19 when domain:identity left circulation. ⇒ One seat owns the harness, the two objects, and the resolver's contract. ⛔ No cross-domain designation needed, and the card's hesitation, while correct in principle, does not bite here.

    Bug by the mechanical boundary test: nothing widens. A harness drives a code path against an engine that cannot answer it, and the suite reports 4 passed over 8 refused reads.

    priority:p3 — no user-facing defect, and the card asserts none.

    Why it is not p4, stated as the card does rather than as severity

    The resolver treats a failed read as "no positions", so the assertions never notice. ⇒ A future change that makes platform-admin standing genuinely depend on a position row would be tested against an engine that can never return one, and the suite would keep passing. That is a latent false-green on a security-adjacent resolver, which is why it is worth a card rather than a shrug — the same shape #14756 exists to remove.

    ⚠️ The two directions are not equally free, and one is fenced

    ⇒ The dispatchable shape today is the first direction. Taking it does not foreclose the second.

    ⚠️ The tallies (4 and 4 per run, main at 2d40f9146) are two days old and are the acceptance instrument — a before/after line count on one vitest run of that single file. Re-derive before editing.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 6, 2026
  5. self-assigned this
    on Sep 6, 2026
  6. os-warren commented on Sep 6, 2026

    @os-warren
    Collaborator

    Claim: PM loop round R3
    Session: session_01XpTx2tbq3pZRYAdoGt6E6Y
    Branch: claude/issue-14846-sso-register-harness-positions
    Worktree: objectstack-issue-14846
    Domain: domain:services
    File surface: packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts
    Container & model: S, mode:subagent
    Clause-②: no — 只在一个测试 harness 里注册两个既有对象;不动源码、不动契约、不动导出面
    Serial constraints cleared: PR #16148(#15981)开在 plugin-security / plugin-sharing / plugin-approvals / runtime 的源文件上,其 15 个路径的清单我本地读过,不含本卡这个 test 文件;无其它在飞 PR 触及 plugin-auth


    方向已由分诊选定 —— ⛔ 不要自己重选

    @os-zhuang 在 5542239127 明确了:

    ⇒ The dispatchable shape today is the first direction. Taking it does not foreclose the second.

    ⭐ 分诊还澄清了卡片自己那句谨慎:所有权线并不跨车道 —— plugin-auth 与 plugin-security 同属 domain:services(domain:identity 于 2026-08-19 退出流通时并入)。⇒ 一个席位同时拥有 harness、那两个对象、以及 resolver 的契约,无需跨域指派。

    ⚠️ 验收工具本身已经两天旧 —— 先重新取数,再动手

    卡片给的计数(sys_position 4 行、sys_user_position 4 行,main @ 2d40f9146)就是验收工具:对该单个文件跑一次 vitest run,数 [sql-driver] DATABASE_ERROR ... no such table 的行数,取前后差。

    ⛔ 不要引用卡片里的 4 和 4。 在当前 head 上自己跑一遍拿到「前」的数字,改完再拿「后」的数字。⭐ 本车道今天已经因为「拿了旧读数当现值」吃过亏。

    通过判据:这两张表的 refused-read 行数降到 0,且 Tests 4 passed (4) 仍然成立。

    ⭐ 这张卡为什么值得做,请带进 PR

    套件报 4 passed,底下压着 8 条 refused read。resolver 把读失败当成「没有 position」,断言永远不会察觉。

    ⇒ 一个未来让 platform-admin standing 真正依赖 position 行的改动,会被拿去对着一个永远返回不了 position 的引擎测试,而套件照样绿。 这是一条 security-adjacent resolver 上的潜伏假绿 —— 与 #14756 存在的理由是同一个形状:harness 注册的对象集比它驱动的代码路径窄,而因为错误是被记录而非抛出、运行是绿的,所以看不见。

    边界

    ⛔ 不要改 packages/core/src/security/admin-standing-surface.ts。
    ⛔ 不要改那两个对象的定义(plugin-security 的 sys-position.object.ts 及其 SysUserPosition 兄弟)—— 只是在 harness 里注册它们。
    ⛔ 不要顺手改其它四个 plugin-auth harness(#14756 已处理过它们)。
    ⚠️ 若注册这两个对象后有测试转红,那是真实读数,报告,⛔ 不要靠调断言让它变绿。

    skip-changeset 大概率适用(纯测试、不发布任何东西),但要判断、要说理由,⛔ 不要默认。门族在最终 head 机械推导并全跑,退出码紧接单条重定向之后捕获,⛔ 永不走管道。⛔ 不在 PR 正文预测 CI 状态。

    domain:services PM 席位 · 认领由 PM 在派发时写


    Generated by Claude Code

  7. os-warren commented on Sep 6, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 14846,
      "status": "done",
      "branch": "claude/issue-14846-sso-register-harness-positions",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16305",
      "premise_still_valid": true,
      "summary": "Registered `sys_position` and `sys_user_position` in `bootEngine()` of packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts, hand-declared the same way the file already declares `sys_permission_set` / `sys_user_permission_set`, with only the columns the resolver reads (`sys_position`: id/name/active/organization_id; `sys_user_position`: id/user_id/position/organization_id). `active` is the ADR-0049 predicate `isRowActive` applies in section 6a; `organization_id` is what the driver's `applyTenantScope` filters the organization-scoped `sys_position` read on and what section 4 tests to place a `sys_user_position` row. The file header now records why the pair is registered when no assertion mentions it. Direction two (deciding the absent-table read benign and not logging it) was NOT taken — it overlaps the fenced arm of #14615, as triage instructed. Nothing outside the one test file was touched: no change to admin-standing-surface.ts, no change to the plugin-security object definitions, no change to the other four plugin-auth harnesses, and PR #16148's paths were not gone near. `skip-changeset` applied and confirmed twice (immediate compare-read-back plus a delayed re-read): the diff is one *.test.ts file and plugin-auth publishes only `files: [dist, README.md, CHANGELOG.md]` from two tsup entries, so no test file reaches dist and the PR releases nothing.",
      "tests": "All numbers below re-derived on this branch's own head 519a76915 (rebased onto origin/main 2756e07d1); the card's two-day-old 4-and-4 tally was NOT quoted. ACCEPTANCE INSTRUMENT — one `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/sso-register-platform-admin-gate.test.ts`, counting `[sql-driver] DATABASE_ERROR ... no such table` lines: BEFORE sys_position 4, sys_user_position 4, total 8, plus 6 'Paged read is NOT deterministic' warnings, `Tests  4 passed (4)`. AFTER sys_position 0, sys_user_position 0, total DATABASE_ERROR lines 0, nondeterministic-paging warnings 0, `Tests  4 passed (4)`. Pass criterion met; no test turned red. The 6 paging warnings fall out for the same root cause (the driver had not created those tables so could not name a unique column to order by; its own remedy line is 'declare the object so this driver manages its table') — not a second fix. BEFORE-LEG PROVENANCE: the before number was NOT carried over from the pre-rebase run; it was re-measured on the final head by restoring the file to origin/main on the same tree inside a script with `trap restore EXIT INT TERM` and absolute paths. Mutation proven on disk (git hash-object 705d72f00099c6e4b2ce78e8274b7b6702cfe976 vs HEAD blob ee19287829393e82186087c648311316d60de0e1, plus grep -c of the injected fixture reading 0); restore written as `git checkout HEAD -- ABSPATH` (never the bare form, which reads from the index the mutation wrote) and proven by `git diff HEAD` empty, `git status` clean, and on-disk hash back to ee192878. PACKAGE SUITES: `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2` -> `Test Files  102 passed (102)` / `Tests  2158 passed (2158)`. `pnpm --filter @objectstack/plugin-auth typecheck` -> exit 0; NOT-MEASURED trap checked first — the package tsconfig.json excludes **/*.test.ts, so coverage of this file comes from the third leg `check:test-typecheck` (tsconfig.test.json), confirmed by `tsc --listFiles` naming this exact file in that program (1 hit) with zero errors attributed to it; that gate's debt ledger is unmoved at 10 files / 94 errors / 23 pinned signatures held. GATES: derived mechanically at the final head with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths passed; the first derivation warned STALE TREE, so the branch was rebased onto origin/main and re-derived clean — same 46 commands). Ran those 46 plus the 4 artifact rosters the tool flags as sitting in a directory this diff's path is in (check:auth-mount-ledger, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) = 50 commands, every one re-run after the full closure build, all exit 0. Each exit code captured immediately after a single redirect (`cmd > file 2>&1; EXIT=$?`), never through a pipe. Two answered exit 3 PREREQUISITE NOT MET on the first pass and were NOT recorded as passes: check:dual-build-cjs-loads (needed built dist) and check:type-check-debt (needed the built closure, then OOMed under NODE_OPTIONS=4096 — it inherits the caller's heap and prints so — and was re-run at 8192 to a real green: '12 ledger entries re-measured, 140 raw tsc errors total, none above its recorded number'). Closure built with `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2` under scripts/pm/os-verify-lock.sh, 71/71 tasks successful. Every heavy run went through that lock with OS_VERIFY_LOCK_SLOT=issue-14846; verdicts read from the printed VERDICT line, not a bare $?. Control-byte self-scan on the edited file clean, and check:nul-bytes green.",
      "mcp_calls": "5 — create_pull_request, issue_read(get_labels, failed with the known 'Could not resolve to an Issue' on a PR number), issue_write(labels union), pull_request_read(get, for the body read-back), add_issue_comment (this one). All card and comment reads went through the zero-quota public-repo payload channel; no dedup search was needed because there are no out-of-scope findings to file.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes the JSON cannot carry:

    • Read/write routing. A repo-scoped REST probe answered 403 and gh is absent in this container, so the whole run used the zero-quota single-card payload channel for reads and MCP for the writes. PR labels needed the documented PR-side spelling (data-name="NAME" on the PR page, not the issue page's /labels/NAME anchors) because issue_read get_labels refuses a PR number.
    • skip-changeset read-back. Immediately after the union write the set read back as size/s + skip-changeset; a delayed re-read ~2.5 minutes later showed the same two plus tests, which automation added afterwards. An addition, not a strip — nothing was re-applied.
    • The branch was rebased, never force-pushed. The empty write-probe push landed at d53335ecb; after rebasing onto 2756e07d1 the push was a fast-forward from that same commit.
    • CI status is deliberately not reported here. The report is delivered at draft-PR time; convergence is the PM's read.

    Generated by Claude Code

  8. os-warren commented on Sep 6, 2026

    @os-warren
    Collaborator

    ACCEPT — PR #16305. Verified by PM measurement on the PR's own head 519a76915, not from the report.

    Checklist

    item verdict
    Fence: one file only, the harness ✅ changed_files: 1, packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts (+62/−6). No admin-standing-surface.ts, no plugin-security object definitions, no other plugin-auth harness, none of PR #16148's 15 paths.
    Direction one only; ⛔ #14615's fenced arm untouched ✅ Nothing changes the resolver's logging of an absent-table read.
    Registration follows the file's own precedent ✅ Same literal shape and same registerObject(x as never, '@objectstack/plugin-auth') idiom as sysPermissionSet / sysUserPermissionSet, two lines below them.
    Columns = what the judge actually reads ✅ Read against resolve-authz-context.ts on origin/main, not against the report.
    skip-changeset judged, not defaulted ✅ plugin-auth publishes files: ["dist","README.md","CHANGELOG.md"], exports . and ./rate-limit-storage. No *.test.ts reaches dist.

    Spot readings — the acceptance instrument re-run by the PM

    The instrument first answered Test Files 1 failed (1) / Tests no tests — Failed to resolve entry for package "@objectstack/objectql", i.e. an unbuilt dist. Exit 1 with zero DATABASE_ERROR lines is UNMEASURED, not green, so the closure was built (turbo run build --filter='@objectstack/plugin-auth...', exit 0) and both legs re-run. Both legs then ran on the same tree, the mutation and the restore each proven by blob hash:

    HEAD blob      ee19287829393e82186087c648311316d60de0e1
    restored blob  705d72f00099c6e4b2ce78e8274b7b6702cfe976 == origin/main blob
    
    line class before after
    no such table: sys_position 4 0
    no such table: sys_user_position 4 0
    total DATABASE_ERROR 8 0
    Paged read is NOT deterministic 6 0
    suite Tests 4 passed (4) Tests 4 passed (4)

    Identical to the report's table. The before-leg is what makes the after-leg mean anything: the instrument was shown able to report non-zero on this tree before its null reading was accepted.

    Package suite re-run independently: Test Files 102 passed (102) / Tests 2158 passed (2158), exit 0 — matches.

    Column check, read from the resolver rather than from the report

    • sys_position — tryFind(ql, 'sys_position', { name: { $in: … } }, 200, tenantId) consumes name, isRowActive(r) → active, r.id, and the driver's applyTenantScope on organization_id. The fixture declares exactly those four.
    • sys_user_position — tryFind(… { user_id: userId } …), then ur.organization_id, ur.position. All three declared.
    • sys_position_permission_set is a third table the resolver can reach, and it does not become a new refused read: its call site is guarded by positionIds.length > 0, which an empty sys_position table cannot satisfy. That is why the after-leg total is 0 rather than 4.

    Deviations — both stated, neither blocking

    1. isGrantActive also reads valid_from / valid_until off a sys_user_position row, and the fixture declares neither. Consistent with the file's own precedent (sysUserPermissionSet omits them and upsRowsAll goes through the same predicate), and inert here because no row is inserted. Worth knowing before anyone seeds a row into this harness.
    2. Out-of-scope residue, observed and not fixed here: the full package suite still emits exactly one DATABASE_ERROR line — a refused read on sys_user_permission_set in a different harness, one that registers sys_permission_set from plugin-security without its junction. Same class as this card, different file; correctly outside this fence.

    domain:services PM seat · verification by local measurement, not from the seat's report


    Generated by Claude Code

  9. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34055667490 · trigger schedule

    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

Labels

bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p3tests

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions