Repository navigation
Commit c5f4a06
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg4i9JiKjEjwtuVzd4gKy9hB)_
## 维护者速读
- 改了什么:实时界面测试(Live
E2E)以前只用管理员登录,现在多了一个「能看不能改」的普通成员身份,并新增一条场景:成员打开管理员的询价记录时,编辑框只读并写明「你没有权限编辑这条记录」、没有保存按钮,记录页上没有「编辑」按钮也没有双击编辑;管理员对照组能编辑同一条记录并保存成功。
- 为什么改:权限类缺陷(#27138、#27146、#27147)是 Console
和服务端之间最常见的合并后才发现的问题,管理员什么都能过,所以原来的实时测试看不到这一类。
- 风险与回滚:只加测试文件和 nightly 列表里的一项,不改产品代码、不改种子数据、不进 CI 必跑车道;回滚即 revert 本 PR。
-
先看:`ui/e2e/live/read-only-member-record.spec.ts`(三步断言)、`ui/e2e/live/member.ts`(管理员建成员并登录)、`ui/package.json`
的 `test:e2e:live:nightly` 一行。
- 两分钟验证:起实时栈后在 `ui/` 跑 `pnpm exec playwright test
--config=playwright.live.config.ts read-only-member-record.spec.ts`,预期 1
passed(本地约 38 秒)。
- 需要你做的:无,席位落地。
Fixes #27552
Clause-②: no
Metadata-protocol: none
## What changes
- **A second, non-admin identity for the live harness**
(`ui/e2e/live/member.ts`). `signInMember(adminRequest, permissionSet)`
has the admin create a member holding one of the showcase's own
permission sets through `POST /api/v1/auth/admin/create-user` (password,
`mustChangePassword: false`, `permissionSet`), signs it in, and returns
a storageState with the same two halves `global-setup.ts` writes for the
admin (the API-origin cookie and the APP-origin `auth-session-token`).
One member per set at a fixed address, so a re-run against a long-lived
backend reuses it (`USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` is accepted).
The state is returned, not written to disk, so no path needs rooting.
- **A new live spec** (`ui/e2e/live/read-only-member-record.spec.ts`).
The member holds `showcase_manager`: on `showcase_inquiry` (OWD
`private`) it reads org-wide and writes only its own rows. The admin
creates an inquiry, so the member may read it and may not write it: a
row verdict, the shape #27138 had. In order:
1. premise through the server: the member's GET answers 200, its PATCH
answers 403 `PERMISSION_DENIED`;
2. as the member, `?form=ID` opens the edit dialog with
`closed-affordance-notice` visible, `name`/`email`/`company`/`message`
disabled and no submit button in `modal-form-footer`; the record page
under it has no Edit button and no inline-edit pencils; the member's
browser sent no non-GET request to the object;
3. control, as the admin: the same record page offers Edit and the
pencils (the same locators step 2 finds absent), the same form has no
notice and an enabled field, and a save through the form answers 2xx and
the record reads back the new value.
- **Nightly only** (`ui/package.json`): the spec joins
`test:e2e:live:nightly`. Per the card's promotion rule it moves to
`test:e2e:live:ci` only after 3/3 consecutive green on the live pair
plus 5/5 of the enlarged allowlist, inside #27550's 2-minute budget.
## Tests
Live stack in this container: this tree's built CLI serving
`examples/app-showcase` through `ui/e2e/live/ci/start-backend.sh`
(`objectstack dev --seed-admin --fresh`, port 4127), the Console from
`ui/apps/console` on vite (port 5327) proxying to it.
<details><summary>Spec runs: green 3/3 consecutive, about 38 s each on
the vite dev server</summary>
```
run 1 exit 0 1 passed (39.9s)
run 2 exit 0 1 passed (39.3s)
run 3 exit 0 1 passed (38.3s)
```
Runs 2 and 3 took the member-already-exists path.
</details>
<details><summary>The spec can fail: the row write verdict removed, the
spec goes red</summary>
Mutation through `scripts/ablation-replace.mjs` on
`ui/packages/permissions/src/RecordFieldAccessScope.tsx` (vite serves
source, so no build was needed): the anchor `answer?.writable === false
? 'record-not-writable'` replaced so the projection's `writable: false`
is ignored, which is the #27138 shape (the dialog offers editable fields
on a row the server will refuse).
```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 948fb29 -> 93c6ea3bfde0
✘ … › as the member: a read-only form with a named notice, no edit affordance, no write sent
Error: expect(locator).toBeVisible() failed
Locator: getByRole('dialog').getByTestId('closed-affordance-notice')
Error: element(s) not found
ablation-replace: ok restored: blob == HEAD (948fb29) and `git diff HEAD` is empty
```
</details>
<details><summary>What was measured before writing the spec</summary>
- `POST /api/v1/auth/sign-up/email` on the dev server answers 403
`SELF_REGISTRATION_CLOSED` (audience posture `invite_only`), so a
sign-up step in the global setup cannot make the member; the admin's
create-user door can.
- As the `showcase_manager` member: GET of an admin-owned inquiry 200,
PATCH 403 `PERMISSION_DENIED` ("You do not have access to this
record…"); PATCH of a project 403 (object-level, from the `everyone`
baseline's read-only grant).
- Console as the member on that inquiry: header shows `More actions` and
`Timeline` only (no Edit), the `⋯` menu holds Refresh / Add to favorites
/ Share (no Delete), no inline-edit pencils; `?form=ID` draws the dialog
read-only with "You don't have permission to edit this record. The
fields are read-only." and Cancel / Close only. As the admin: Edit, six
pencils, Delete in the menu, the dialog editable with Update.
</details>
- `pnpm --dir ui run type-check:e2e` exit 0; `tsc --listFiles` includes
both new files.
- `pnpm --dir ui exec vitest run
scripts/__tests__/live-e2e-storage-state-roots-9519.test.ts
scripts/__tests__/check-test-path-roots.test.ts
scripts/__tests__/e2e-type-check.test.ts`: 3 files, 43 tests passed.
- `pnpm --dir ui exec eslint e2e/live/member.ts
e2e/live/read-only-member-record.spec.ts` exit 0; `node
ui/scripts/check-test-path-roots.mjs` OK.
<details><summary>Derived gates on head 3937f50: 31 derived, 31 run,
all exit 0, plus check-sdui-manifest exit 0</summary>
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran FILE`: 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.
```
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-osv-exemptions.mjs :: exit 0
node scripts/check-osv-exemptions.mjs --self-test :: exit 0
node scripts/osv-base-relative.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm check:changeset-fixed :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:manifest-repository-directory :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:override-consistency :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:turbo-task-graph :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-export-contract-resolve :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:workspace-manifest-cycles :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
```
</details>
## Acceptance notes
- The card offered two ways to make the member: a seeded member in
`examples/app-showcase`, or a sign-up step in `global-setup.ts` with a
second storageState. Sign-up is closed on the dev server (measured
above), and a seed change is not needed: the admin create-user door with
the showcase's existing `showcase_manager` set gives the member and the
read-not-write row. So neither `global-setup.ts` nor the showcase
changed. The member is made by the one spec that needs it rather than by
the global setup, so the required `Live E2E UI` lane, which shares the
global setup, pays nothing for it while the spec is nightly-only. It
also keeps this PR off `global-setup.ts`, which sibling #27551 may
touch. When the spec is promoted, moving `signInMember` into the global
setup is a one-call change if a second spec wants the identity.
- `ui/e2e/live/member.ts` is a new file beside the spec, under the
claimed `ui/e2e/live/` surface; it is the "second identity" half of the
card's Task 1 kept reusable for the next access scenario (#27146 /
#27147 shapes).
- Scope chosen for the record: a ROW verdict (object allows edit, this
row refuses), because that is the #27138 escape. The object-level
refusal (the member's read-only grant on `showcase_project`) was
measured and left out of the spec; the existing unit suites already pin
the object-level form lock (objectui#11000).
- Durations: about 38 s per run against the vite dev server here,
including the cold compile of three page loads. That alone would take a
third of #27550's 2-minute budget, so the promotion decision should
re-measure it on the CI lane's `vite preview` build.
- No published package changes (`ui/package.json` is the private
workspace root), so no changeset.
- Harness PR step skipped: per the dispatch order, this PR carries no
assignee and no review request.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbDmdfNUqBtfbQtu5T)_
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ca167f6 commit c5f4a06
3 files changed
Lines changed: 229 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
115 | | - | |
| 115 | + | |
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
| |||
0 commit comments