Skip to content

test(seed): guard that seeds stay inside the app's own object graph (#1258) - #1260

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-1258-seed-interactions
Aug 23, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-1258-seed-interactions

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #1258 — and it deliberately does NOT populate the Activity tab, because measurement says that cannot be done from this repo. The card's premise is falsified; this PR ships only the guard that keeps the next author from paying for the same measurement.

What was measured

The card asked for "a handful of sys_activity rows for the demo leads/accounts". I authored exactly that — three completed interaction rows for the seeded lead Lisa Thompson (CloudFirst Inc) — built it, wiped .objectstack/data, booted, and read the result back out of the running server.

The rows load. pnpm build is silent about a seed naming a platform table, the boot banner counts them (Seeds: app.objectstack.hotcrm 345 rows, up from 342), and every column arrives — readonly ones included. That answers the open question in the card: sys_activity is seedable in the narrow sense. Specifically:

  • seeds write under { isSystem: true }, which exempts the static readonly strip — and sys_activity declares every field readonly;
  • type: 'completed' is accepted (the value the renderer maps to feed kind task);
  • a cel value such as daysAgo(n) resolves normally;
  • mode: 'upsert' is accepted even though the object is managedBy: 'append-only'.

The rows attach to nothing. sys_activity.record_id is Field.text(), not a lookup, and the seed loader resolves a natural key only for lookup / master_detail fields — every other field is stored verbatim. So record_id: 'Lisa Thompson' is stored as that literal string, while the seeded lead's id is a runtime nanoid (tgIjpNhjlfmWU8YF) that does not exist until first boot.

The consequence, using the query record:activity actually issues — read out of the shipped console bundle, not inferred (sys_activity, { $filter: { object_name, record_id }, $orderby: { timestamp: 'desc' } }):

filter { object_name: 'crm_lead', record_id: 'tgIjpNhjlfmWU8YF' }  →  0 rows
filter { object_name: 'crm_lead', record_id: 'Lisa Thompson'    }  →  3 rows

The tab stays empty and the seed book looks populated. This is the same rule the ownership note in src/data/index.ts already records for owner_id: 'Dev Admin', one column over: a seed cannot name an id that does not exist yet, and reference resolution is the only exemption from that.

Two further facts, in case the id problem ever looks solvable locally:

  • sys_activity is lifecycle.class: 'telemetry' with retention: { maxAge: '14d' } and day-shard rotation — rows live in sys_activity__r<YYYYMMDD> behind a view — so a demo narrative older than a fortnight is reaped by design. The daysAgo(40) probe row is outside the retention window on the day it lands.
  • Ownership/visibility is not the obstacle here, unlike the crm_forecast precedent the card pointed at: sys_activity declares no owner_id at all, and the seeded rows read back fine over REST as the dev admin.

Why no fourth writer

Interaction rows have exactly two writer sites in src/ — the log_call / log_meeting factory in src/actions/global.actions.ts and send_email in src/actions/contact.actions.ts. Nothing else writes sys_activity. A hook on crm_event that also wrote the pointer row would give every action-created event two rows: the two-producer conflict #702 ruled against, one object over. The card explicitly ruled that out in advance, and the measurement agrees with it.

What this PR contains

One guard block in test/seed-consistency.test.ts: every seed dataset must target an object this app declares. Seed.object is a plain z.string(), so naming a platform sys_* table type-checks, builds, validates and loads — the failure is invisible at every gate and only shows up as an empty tab. The guard closes that whole class rather than the single sys_activity case, and the measurement above is written into the block comment so the next author inherits it instead of re-deriving it.

If a future platform release makes an ActivityPointer target seedable, this test should be deleted deliberately — not widened.

Verification

Run at 86fd1e49 (the final commit on this branch):

  • pnpm typecheck — exit 0, tsc --noEmit clean
  • pnpm validate — exit 0 (pre-existing colSpan / props warnings on views this PR does not touch)
  • pnpm build — ✓ Build complete (952ms)
  • pnpm test — Test Files 120 passed (120) / Tests 2841 passed | 2 skipped (2843)

Reverse verification of the new guard, predicted direction red, observed red: re-registering the sys_activity seed made exactly one test fail — expected [ 'sys_activity' ] to deeply equal [] — with the other 36 in the file still green, so the guard is specific rather than a blanket break. The mutation was confirmed on disk by grepping the injected import, the array entry and the seed file's own sys_activity line (all non-zero) before the run, and the restore leg was confirmed the same way (file absent, 0 mentions in index.ts); the script carried a trap … EXIT INT TERM. No rebuild leg applies: this test imports ../src/data/index and ../objectstack.config as source through vitest's transform, not across a package exports → dist/ boundary.

Note on tooling: scripts/pm/os-verify-lock.sh refuses on this macOS host — flock is absent, and the entry point reports VERDICT lock-unusable itself — so the heavy commands above ran unserialised. Declaring it rather than hand-rolling a substitute lock.

skip-changeset: this PR ships nothing to users — one test file, no metadata, no runtime behaviour.

Generated by Claude Code

…1258)

`Seed.object` is a plain string, so a seed naming a platform `sys_*` table
type-checks, builds, validates and loads — and cannot produce a usable row.

Measured on 17.1.0 by seeding three `sys_activity` rows for a demo lead: the
rows land (boot counts 345 rows, up from 342) with every readonly column
written, because seeds write under `{ isSystem: true }`. But `record_id` is
`Field.text()`, and the loader resolves natural keys only for lookup /
master_detail fields, so it stores the literal string while the lead's id is a
runtime nanoid. The query `record:activity` actually issues —
`{ object_name: 'crm_lead', record_id: <id> }` — returns 0 rows for the real id
and 3 for the literal key. The tab stays empty; the seed book looks populated.

Same rule as the `owner_id` note in src/data/index.ts, one column over: a seed
cannot name an id that does not exist yet, and reference resolution is the only
exemption.

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

vercel Bot commented Aug 23, 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)
hotcrm Ignored Ignored Aug 23, 2026 12:10pm

Request Review

@os-zhuang os-zhuang added the skip-changeset PR ships nothing to users (pure CI/docs chore) — changeset gate waived label Aug 23, 2026
@github-actions github-actions Bot added the ci/cd CI plumbing and the verification pipeline label Aug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 13:40
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit 5eee1bd Aug 23, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline skip-changeset PR ships nothing to users (pure CI/docs chore) — changeset gate waived

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant