Skip to content

fix(showcase): seed assignees/owners that resolve to a real user - #7789

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-7746-showcase-seed-assignees
Aug 11, 2026
Merged

fix(showcase): seed assignees/owners that resolve to a real user#7789
huangyiirene merged 1 commit into
mainfrom
claude/issue-7746-showcase-seed-assignees

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #7746

The defect

The showcase seeded showcase_task.assignee and showcase_project.owner as bare emails — ada@ / linus@ / grace@ / sam@example.com — that are no sys_user. Both fields are Field.text, so nothing rejected them, and every view rendered happily.

The cost is one layer down. Those fields are read as notify recipients by the flows (recipients: ['{record.assignee}'], ['{record.owner}']), and RecipientResolver (ADR-0030 P1) resolves an email-shaped recipient against sys_user and, on a miss, keeps the string verbatim as the recipient id (packages/services/service-messaging/src/recipient-resolver.ts:206). So the stock reassignment demo did not fail loudly — it persisted a sys_inbox_message whose user_id was a literal email address: a row no authenticated user can read. The QA run that found this (#7690) had to sign up its own personas to exercise the notify path at all.

The fix

Those fields now name the identities the app actually provisions on a dev boot — exactly three rows:

email id created by
admin@objectos.ai (better-auth id) plugin-auth dev-admin seed (NODE_ENV=development)
phone.demo@example.com usr_showcase_phone_demo seed-approval-demo.ts on kernel:bootstrapped
auditor.demo@example.com usr_showcase_auditor_demo same

The persona constants move out of seed-approval-demo.ts into a new src/security/demo-personas.ts, so the seed and the bootstrap that creates the rows read one registry and cannot drift apart. Three distinct values keep the Kanban / gallery / list spread intact, so the seed's "feed every view" principle is unaffected.

Trade-offs, stated

  • These personas hold no better-auth credential, so they cannot sign in. That is enough to make a recipient resolve and the inbox row addressable to a real user id — which is the whole of this defect — but reading the row as that persona still needs the sign-up the showcase deliberately leaves to the operator.
  • All three are dev-only. In a real deployment a fresh showcase has no users at all, so nothing could resolve. That is a property of the environment, not of the seed, and no seed value can repair it.
  • showcase_invoice.owner is knowingly left alone. It genuinely reaches a notify recipient too (showcase_invoice_lifecycle{record.owner}), so it is the same defect class. But those emails are the fixture for the ADR-0055 controlled-by-parent isolation demo, whose whole point is that an operator signs up as ada@example.com and then sees only their own invoices — showcase-invoice-seed-isolation.dogfood.test.ts does exactly that and pins the seeded owners. Repointing invoices at credential-less personas would delete that demo rather than fix it. Flagged on the issue rather than guessed at here.
  • showcase_team.lead is display-only — it reaches no notify recipient — so it keeps its email.

Regression guard

test/inert-wirings.test.ts gains a §5 that derives the recipient fields from the flows themselves (start-node objectName × {record.<field>} inside a recipients value, including notify nodes nested in branch bodies), so a new notify node is covered automatically. It fails when a seeded recipient is an email the app never provisions, and separately pins that the derivation is non-vacuous and that every recipient names a declared field.

Negative control — reverting one assignee to the old value fails as intended:

FAIL  seeded notify recipients resolve to a real user (#7746) >
      no seeded recipient value is an email the app never provisions
AssertionError: seeded notify recipient(s) are not a provisioned sys_user —
  the inbox row would be unreadable: showcase_task.assignee='ada@example.com' ('Design system')

Verification — the issue's own reproduction, on a real boot

pnpm dev on a wiped datastore (full plugin set: automation, triggers, messaging, approvals), then the three steps from the issue.

Step 1 — seed ran; every recipient resolves (0 unresolvable, was 15):

OK   Audit current IA       -> auditor.demo@example.com
OK   Build homepage         -> admin@objectos.ai
OK   Content backlog        -> phone.demo@example.com
...  (10 tasks + 5 projects)
=> unresolvable recipients: 0

Steps 2-3 — reassign through the notify path, then read sys_inbox_message straight from the datastore (the rows are invisible over REST precisely because inbox reads are user-scoped — the issue's point):

('usr_showcase_auditor_demo', 'New task assigned: Ingest pipeline')   <- after the fix, real user id
('usr_showcase_phone_demo',   'New task assigned: Ingest pipeline')   <- after the fix, real user id
('ada@example.com',           'New task assigned: Warehouse schema')  <- CONTROL: old value, raw email

The control also reproduces the mechanism in the server log:

WARN [recipients] no 'sys_user' matched email 'ada@example.com'; keeping verbatim

Also green: showcase suite 20 files / 190 tests, tsc --noEmit, objectstack validate (warnings pre-existing), ESLint on changed files, and showcase-invoice-seed-isolation.dogfood.test.ts (4 tests) — the one I deliberately did not disturb.

Reported, not fixed — the deeper gap

The notify/assignment path never validates that a recipient resolves before persisting the row. resolveEmail logs a warning and returns the raw email, the inbox channel writes it as user_id, and the write succeeds. Seed data is only the loudest way to reach it — any user-entered assignee does the same. Two existing tests already pin the behaviour (examples/app-showcase/test/approval-resume-relation-expand.test.ts:266 asserts user_id === 'grace@example.com'), so closing it is a deliberate platform change in packages/services, out of scope here per the issue's own split.

Release

No changeset: this touches only examples/app-showcase/**, and @objectstack/example-showcase is private so it releases nothing. Per pr-automation.yml's own prescription (examples/, tests-only, and the like -> apply the 'skip-changeset' label. <<< PREFERRED) and check-empty-changeset.mjs (a PR may not add an empty-frontmatter changeset), the correct route is the skip-changeset label — please apply it.


Generated by Claude Code

The showcase seeded `showcase_task.assignee` and `showcase_project.owner`
as bare emails — `ada@`/`linus@`/`grace@`/`sam@example.com` — that are no
`sys_user`. Both fields are read as notify RECIPIENTS by the flows, and
`RecipientResolver` (ADR-0030 P1) resolves an email-shaped recipient
against `sys_user` and, on a MISS, keeps the string VERBATIM as the
recipient id. So the stock reassignment demo did not fail loudly: it
persisted a `sys_inbox_message` whose `user_id` was a literal email
address — a row no authenticated user can read. The QA run that found
this (#7690) had to sign up its own personas to test notify at all.

Those fields now name the identities the app actually provisions on a dev
boot: the `plugin-auth` dev admin plus the two personas
`seed-approval-demo.ts` creates on `kernel:bootstrapped`. The persona
constants move to `src/security/demo-personas.ts` so the seed and the
bootstrap that creates the rows read one registry and cannot drift.
Three distinct values keep the Kanban/gallery/list spread intact.

`showcase_invoice.owner` deliberately keeps its `ada@`-style emails: it is
the fixture for the ADR-0055 controlled-by-parent isolation demo, where an
operator SIGNS UP as those emails. The provisioned personas hold no
credential, so repointing invoices at them would delete that demo rather
than fix it — reported on the issue instead of guessed at here.

Guarded by `test/inert-wirings.test.ts` §5, which derives the recipient
fields from the flows themselves (so a new notify node is covered
automatically) and fails when a seeded recipient is an email the app never
provisions.

Verified on a real `pnpm dev` boot of the showcase: all 10 task assignees
and 5 project owners resolve to `sys_user` rows, and a reassignment writes
`sys_inbox_message.user_id = usr_showcase_auditor_demo` (a real id). The
old value still reproduces the defect — `WARN [recipients] no 'sys_user'
matched email 'ada@example.com'; keeping verbatim`, row keyed to the raw
address.

Refs #7690
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 3:00pm

Request Review

@huangyiirene huangyiirene added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 11, 2026 — with Claude
@huangyiirene
huangyiirene marked this pull request as ready for review August 11, 2026 16:58
@huangyiirene
huangyiirene added this pull request to the merge queue Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31516733138 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 62 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31516767645 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Checkout repository(日志不可读,点进 job 看)

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 62 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31518430706 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/lint-startup-registry-verdict.corpus.test.ts�[2m > �[22mstartup open-vocabulary verdicts across packages/ (#4776)�[2m > �[22mno package records a verdict the boot can sti
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 2 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 66 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31517427201 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    �[41m�[1m FAIL �[22m�[49m �[30m�[46m isolated �[49m�[39m test/semantic-roles.dogfood.test.ts�[2m [ test/semantic-roles.dogfood.test.ts ]�[22m
    
  • Test Core (3/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m src/security/operation-private-keys.pin.test.ts�[2m > �[22mthe `__` operation-private-key convention has one owner (#7284)�[2m > �[22mis declared in exactly one file, and tha
    

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 3 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 67 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: showcase seed assigns tasks to non-user emails — a stock reassignment lands an inbox row nobody can read

2 participants