fix(showcase): seed assignees/owners that resolve to a real user - #7789
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31516733138 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31516767645 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31518430706 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31517427201 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #7746
The defect
The showcase seeded
showcase_task.assigneeandshowcase_project.owneras bare emails —ada@/linus@/grace@/sam@example.com— that are nosys_user. Both fields areField.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}']), andRecipientResolver(ADR-0030 P1) resolves an email-shaped recipient againstsys_userand, 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 asys_inbox_messagewhoseuser_idwas 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:
admin@objectos.aiplugin-authdev-admin seed (NODE_ENV=development)phone.demo@example.comusr_showcase_phone_demoseed-approval-demo.tsonkernel:bootstrappedauditor.demo@example.comusr_showcase_auditor_demoThe persona constants move out of
seed-approval-demo.tsinto a newsrc/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
showcase_invoice.owneris 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 asada@example.comand then sees only their own invoices —showcase-invoice-seed-isolation.dogfood.test.tsdoes 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.leadis display-only — it reaches no notify recipient — so it keeps its email.Regression guard
test/inert-wirings.test.tsgains a §5 that derives the recipient fields from the flows themselves (start-nodeobjectName×{record.<field>}inside arecipientsvalue, 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:
Verification — the issue's own reproduction, on a real boot
pnpm devon 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):
Steps 2-3 — reassign through the notify path, then read
sys_inbox_messagestraight from the datastore (the rows are invisible over REST precisely because inbox reads are user-scoped — the issue's point):The control also reproduces the mechanism in the server log:
Also green: showcase suite
20 files / 190 tests,tsc --noEmit,objectstack validate(warnings pre-existing), ESLint on changed files, andshowcase-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.
resolveEmaillogs a warning and returns the raw email, the inbox channel writes it asuser_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:266assertsuser_id === 'grace@example.com'), so closing it is a deliberate platform change inpackages/services, out of scope here per the issue's own split.Release
No changeset: this touches only
examples/app-showcase/**, and@objectstack/example-showcaseisprivateso it releases nothing. Perpr-automation.yml's own prescription (examples/, tests-only, and the like -> apply the 'skip-changeset' label. <<< PREFERRED) andcheck-empty-changeset.mjs(a PR may not add an empty-frontmatter changeset), the correct route is theskip-changesetlabel — please apply it.Generated by Claude Code