Skip to content

fix(runtime,cloud-connection): multi-tenant seed replay covers every source, not just the first (#3453)#3478

Merged
os-zhuang merged 1 commit into
mainfrom
claude/competent-dubinsky-3b3f42
Jul 25, 2026
Merged

fix(runtime,cloud-connection): multi-tenant seed replay covers every source, not just the first (#3453)#3478
os-zhuang merged 1 commit into
mainfrom
claude/competent-dubinsky-3b3f42

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What & why

Fixes #3453. In multi-tenant deployments (enterprise @objectstack/organizations) a brand-new org gets its own private copy of demo data by replaying the kernel's seed-datasets list on the sys_organization insert. That list is supposed to hold the union of every seed source — every config-declared app plus every marketplace package — but two framework traps (the same pair #3444 fixed for seed-summary) silently shrank it to just the first source:

  1. No .kernel handle. The standard PluginContext (packages/core/src/kernel.ts:76) exposes getService/registerService but has no .kernel property, so (ctx as any).kernel?.getService('seed-datasets') always read undefined. Each source then saw "nothing registered" and overwrote the list with only its own datasets.
  2. Duplicate-register throw. registerService (kernel.ts:195) throws on a duplicate name, so the second source's re-register was swallowed by the surrounding try/catch — its datasets (and, for a config app, its replayer) silently lost.

Net effect: with two config apps, or a config app + marketplace packages, a new org replayed only the first app's seeds.

The fix — register-once-then-mutate (pattern from #3444's recordSeedOutcome)

New packages/runtime/src/seed-datasets.ts helper:

  • mergeSeedDatasets(ctx, datasets) — registers one shared array the first time and mutates it in place for every later source, reading through the context's own resolver first (ctx.getService), kernel second. Never re-registers → no duplicate-register throw; always accumulates → no clobber.
  • readSeedDatasets(ctx) — reads the live shared array.
  • registerSeedReplayerOnce(ctx, replayer) — registers the per-org replayer once; later config apps reuse it.

Wired into both seams the issue names:

  • packages/runtime/src/app-plugin.ts — the config-app seed path now mergeSeedDatasets(...) into the shared array, and the per-org replayer reads readSeedDatasets(ctx) at invoke time instead of the captured merged snapshot, so an org created after a later app/marketplace install still replays their seeds.
  • packages/cloud-connection/src/marketplace-install-local-plugin.ts — the marketplace merge now routes through the runtime helper via a lazy import('@objectstack/runtime') + typeof-guard (mirroring the existing recordSeedSummary), with an equivalent inline fallback so the two test files that mock @objectstack/runtime without the helper don't crash.

Testing

packages/runtime/src/seed-datasets.test.ts (7 tests):

  • Helper units against a faithful kernel fake (getService throws on miss, registerService throws on dupe, no .kernel — mirrors seed-summary.test.ts): reads the live service despite no .kernel (defect ①); a second source accumulates with no duplicate-register throw (defect ②); the live array grows for a captured reference; registerSeedReplayerOnce registers once then reuses.
  • Seam: boots the real AppPlugin twice + a marketplace-style merge and invokes the registered seed-replayer, asserting it replays the full union [crm_account, crm_contact, hot_lead] in source order, tenant-scoped.

Red-proof: against the pre-fix app-plugin.ts the seam test fails exactly as #3453 describes — the replayer drops crm_contact (the second config app's seeds).

Results:

  • @objectstack/runtime — 44 passed (app-plugin, app-plugin.seed, seed-datasets, seed-summary)
  • @objectstack/cloud-connection — 18 passed (all marketplace-install-local-*, incl. the two that mock the runtime module and exercise the fallback)
  • forced build (DTS type-check) green for both packages

Scope note

This repo only carries seam-level coverage. True multi-tenant end-to-end coverage requires the enterprise @objectstack/organizations plugin (which drives the sys_organization-insert replay), and that lives in the cloud repo — out of tree here.

🤖 Generated with Claude Code

…source, not just the first (#3453)

In multi-tenant mode a new org replays the kernel's `seed-datasets` list on the
`sys_organization` insert, so that list must hold the union of every seed source
(every config app + every marketplace package). Two framework traps — the same
pair #3444 fixed for seed-summary — shrank it to just the first source:

- the standard PluginContext has no `.kernel` handle, so
  `(ctx as any).kernel?.getService('seed-datasets')` always read undefined and
  each source clobbered the list with only its own datasets; and
- `registerService` throws on a duplicate name, so the second source's
  re-register was swallowed and its datasets (and, for a config app, its
  replayer) were silently lost.

`seed-datasets` is now a single shared array, registered once and mutated in
place by every source via a new `mergeSeedDatasets` helper that reads through the
context's own resolver first. AppPlugin's per-org replayer reads that live list
at invoke time — not a captured snapshot — and is itself registered once and
reused by later config apps, so a new org replays the full union.

Seam-level unit tests only (accumulation across app + marketplace sources; the
replayer reads the live union); true multi-tenant e2e needs the enterprise
`@objectstack/organizations` plugin, which lives in the cloud repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 25, 2026 1:31am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/runtime.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 810a3a2 into main Jul 25, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/competent-dubinsky-3b3f42 branch July 25, 2026 02:16
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

1 participant