Skip to content

Commit 13f5360

Browse files
test: spike a virtual object on a plugin driver through steps a to g (#27330)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiVkDMsTP545Q58eNLit6dXy)_ Fixes #27276 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只加两份测试,零产品代码。一份在 runtime 里用插件发布的驱动起完整栈(REST、鉴权、权限集、共享),把卡上 (a)–(e)、(g) 逐步钉在驱动实际收到的调用上;一份在 plugin-gantt 里证 (f) 甘特树、锁和拖拽写回。 - 为什么改:#27276 要实测「虚拟对象挂插件驱动」这条路今天哪步通、哪步不通,作为 #27261 的依据。七步都走得通,但第五步 (e) 要把对象设成 `public_read_write`;留在平台默认共享模型时,持有编辑权的成员也改不了任何外部行,现状钉在 (e3)。 - 风险与回滚:只有测试文件,不发布(两个包都只发 dist,构建产物里零命中、正控命中);回滚即删两文件。runtime 那份起整套内核,单文件约 30 秒。 - 先看:`packages/runtime/src/plugin-driver-external-object.spike.test.ts` 的 (e3) 段(默认共享下的缺口)和 (c) 段(驱动收到的 DriverQuery 原样);再看 `ui/packages/plugin-gantt/src/ObjectGantt.pluginDriverTree.test.tsx`。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/plugin-driver-external-object.spike.test.ts`,预期 `Tests 12 passed (12)`。 - 需要你做的:无,席位落地。 ## What changes Two test files, no product code. - `packages/runtime/src/plugin-driver-external-object.spike.test.ts` (12 tests). A plugin publishes `driver.spike_tree`, an in-memory stand-in for a remote system that records every call it receives and refuses every method or operator outside the spike by name. `defineStack` declares a datasource on it (`schemaMode: 'external'`, `validation: { onMismatch: 'fail', checkOnBoot: false }`, `allowWrites: true`, no `introspectSchema` on the driver) and two external objects: `spike_node` (a tree with `parent`, `depends_on`, a `current_user` formula `locked`, a `beforeUpdate` hook with a `PLAN_LOCKED` refusal, a gantt list view) and `spike_owned` (left on the platform's default sharing model). The kernel boots the real REST, auth, security and sharing plugins over it, and each step is asserted on what reached the driver, not on what the engine says it sent. - `ui/packages/plugin-gantt/src/ObjectGantt.pluginDriverTree.test.tsx` (4 tests). The real `GanttView` over the records that read serves: the tree, the formula lock on the rendered bars, and the write a pointer drag produces. ## Why #27276 asks, before any platform change in #27261, which of the seven steps a virtual object on a plugin driver passes today. Each step is now a test; the one that does not pass is pinned as it behaves, so the change that lifts it turns that test red on purpose. ## Tests | Step | Verdict | What the test pins | |---|---|---| | (a) | passes | `driver: 'spike_tree'` with `config: {}` resolves to the plugin's `driver.spike_tree` service; the engine routes `spike_node` to that instance; the connection service reports `already-registered` and leaves it alone; the boot calls `connect` once. | | (b) | passes | The boot completes with `onMismatch: 'fail'` and `checkOnBoot: false` on a driver with no `introspectSchema`, and no `syncSchema` is sent. A later `validateAll()` reports `spike_node` `ok: false`, so the opt-out is what lets it boot. | | (c) | passes | The plan view's filter and sort, sent as the console sends them, arrive at `driver.find` as `{ where: { archived: false }, orderBy: [{ field: 'start_date', order: 'asc' }], limit: 50, object: 'spike_node' }`. No `fields`, no `offset`. A separate `count` with the same `where` follows. | | (d) | passes | The formula over `current_user.id` is computed engine-side per caller: the same row reads `locked: false` for the planner and `locked: true` for the administrator. | | (e) | passes on `sharingModel: 'public_read_write'` | A hook refusal answers 409 `RESOURCE_CONFLICT`, `declaredCode: 'PLAN_LOCKED'`, with the catalogue message, and `update` is never sent. An allowed write reaches `driver.update` last with the patch, the hook's stamp, and the engine's `id`, `updated_at`, `updated_by`; the returned row is served with the formula recomputed. | | (e3) | does not pass on the default sharing model | A member whose permission set grants `allowEdit` gets 403 `PERMISSION_DENIED` on every row of `spike_owned`: the sharing check reads `owner_id` and the ownership floor reads `created_by`, and no remote row carries either. | | (f) | passes (unit) | The parent renders as a summary bar, the children as task bars, the dependency as a link; the formula lock removes the resize grips and blocks the drag; dragging the unlocked bar calls `dataSource.update('spike_node', 'n_cut', { start_date, end_date })`. The adapter's `PATCH /data/:object/:id` is pinned in `packages/client/src/client.test.ts:2177`. | | (g) | passes | A user whose permission sets grant nothing on `spike_node` gets 403 on the list read with zero driver calls, and 403 on a PATCH after one by-id `findOne` and no write. | ## Acceptance notes - **(e3), the one step that does not pass.** Smallest platform change: an `os validate` warning (a warning, since a `defineStack` refusal would narrow it) for an external object left on an owner-keyed sharing model with no owner column mapped, naming both ways out (`public_read_write` plus a hook, or a `columnMap` to an owner column). Left for the seat to file as a sub-card of #27261. - Read cost the spike measured, for #27277: every PATCH reads the pre-image from the remote three times by id, and every list read is a `find` plus a separate `count`. - Shape notes for the guide in #27278: `object` rides inside the DriverQuery although the type omits it, and `driver.update` receives the audit stamps `updated_at` and `updated_by` plus `id`, so a driver's writable column set has to accept them or drop them. - A denied PATCH still costs the remote one by-id read before the 403. ## Verification Head `680ce37b`, branched from `b4458075`; `main` has since moved 11 commits, none in the paths this diff reaches, and the gate derivation on current `main` with this diff applied is the same 53 commands. Local runs on a shared box, so seconds are shared-box seconds. <details> <summary>Spike tests: 12/12 and 4/4</summary> ``` $ pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/plugin-driver-external-object.spike.test.ts Test Files 1 passed (1) Tests 12 passed (12) Duration 29.86s $ (cd ui/packages/plugin-gantt && npx vitest run --root ../.. packages/plugin-gantt/src/ObjectGantt.pluginDriverTree.test.tsx) Test Files 1 passed (1) Tests 4 passed (4) ``` </details> <details> <summary>Package suites and type-checks</summary> ``` $ pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=3 (at 4426989; the runtime file is unchanged since) Test Files 469 passed (469) Tests 7641 passed | 19 skipped (7660) $ pnpm --filter @objectstack/runtime typecheck (tsc + check:test-typecheck) exit 0 $ pnpm exec turbo run type-check --filter=@object-ui/plugin-gantt Tasks: 48 successful, 48 total (at 4426989) $ (cd ui/packages/plugin-gantt && npx tsc -p tsconfig.test.json) exit 0 (at 680ce37) $ (cd ui/packages/plugin-gantt && pnpm test) (at 4426989) Test Files 102 passed | 1 skipped (103) Tests 772 passed | 114 skipped (886) $ eslint (root config, --no-inline-config) on both files exit 0, no warnings ``` `680ce37b` changed only the gantt test's schema annotation (typed as `ObjectGanttHandoff`, so `type: 'object-gantt'`); its file and the test-program type-check were rerun there. </details> <details> <summary>Derived gates: 53 of 53, all exit 0</summary> Second pass at `680ce37b`. `node scripts/pm/dispatch-gates.mjs --ran` on the recorded exit codes: 53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). The first pass at `fbd9e598` had five non-zero exits. Two were this diff and are now green: `check:slot-lookup` (two lookups typed `any`; now typed with their contracts) and `check:where-matcher` (the lift pulled in a module-level declaration named in the matcher's message text; the text no longer names it). Two were exit 3, prerequisite not met (`check-plugin-teardown-shape --self-test` needed its pinned fixture fetched; `check:dual-build-cjs-loads` needed built `dist/`), and both pass on rerun. `pnpm --dir ui exec vitest run scripts/` failed on 31 unresolved `@objectstack/*` imports before `dist/` was built plus one 15s timeout under load; rerun after the build: 174 passed, 2 skipped (176 files, 4990 tests). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (Root eslint run on both files, exit 0.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. (Branched at `b4458075`; `main` has moved 11 commits since, none in these paths.) - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Nothing publishes: `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (The 维护者速读 block is Chinese by the seat's PR-body rule.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_017ihL2fiCyHoxhseMhHUuhN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 16d151b commit 13f5360

2 files changed

Lines changed: 751 additions & 0 deletions

File tree

0 commit comments

Comments
 (0)