Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions docs/qa/platform-checklist/areas/access-security.json

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions docs/qa/platform-checklist/areas/ai.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
"title": "MCP stdio auto-start fails closed without OS_MCP_STDIO_API_KEY; with a member-bound key, reads AND aggregates honor RLS/FLS and revocation bites on the next call",
"since": "v16",
"status": "active",
"revision": 8,
"revision": 9,
"priority": "P0",
"surface": "cli",
"personas": ["restricted member API key (for the RLS half — on showcase, a contributor-bound member)", "a second restricted member API key for the FLS half (on showcase, a client_liaison-bound member — see fixtures.requires)", "admin API key (for the full-row comparison)"],
Expand All @@ -257,7 +257,7 @@
"run the same two reads over REST as the same member (Bearer the same key) and as the admin key; compare. On `os start` the stdio transport answers tool calls before the HTTP listener is up, so poll GET /api/v1/health until 200 before the first REST leg; a refused connection there is the boot still coming up, not a result",
"aggregate parity: over MCP stdio as the member, aggregate_records the OWD-private object with a count grouped by a readable field ({ aggregations: [{ function: 'count', alias: 'n' }], groupBy: ['<readable field>'] }); run the SAME aggregate over REST as the member and as the admin; compare the per-group counts",
"FLS aggregate fail-closed: over MCP stdio as the member, aggregate_records with an aggregation (or a groupBy) naming the field the member cannot read (the masked field above); capture the refusal and confirm no numeric result is returned",
"revoke the member key while the stdio session is up (as the admin, PATCH /api/v1/data/sys_api_key/<key id> {\"revoked\": true}); issue one more read on the SAME session; capture the tool result and parse its error envelope (code and status, not only the prose)",
"revoke the member key while the stdio session is up (as the admin from a SIGNED-IN SESSION, PATCH /api/v1/data/sys_api_key/<key id> {\"revoked\": true}, or revoke it on Setup's API Keys page. Never with the admin's API key: a sys_api_key write from a principal an API key admitted is refused 403 PERMISSION_DENIED 'Changing an API key requires a signed-in session…' and nothing is written, packages/core/src/security/api-key-management-credential.ts); issue one more read on the SAME session; capture the tool result and parse its error envelope (code and status, not only the prose)",
"start once more with stdio enabled and the key revoked in the previous step; capture the refusal to start (the lifecycle arm of the up-front admission)",
"start once more with stdio NOT enabled and no key set, again with `--log-level info` (the not-auto-started line is logged at INFO, like the principal-bound line of step 2); confirm a clean boot (the key is only demanded when stdio is on)"
],
Expand Down Expand Up @@ -359,6 +359,12 @@
"date": "2026-10-28",
"change": "steps[4] waits for the HTTP listener, and automated.ref names two corroborating files. Checklist-accuracy findings of the 18.2.0 wave 2 (QA runs #26481, priority:P0, and #26468, Tier 1 half A, both at 756220c4; wave anchor #25807, slice B). (1) #26481: on `os start` stdio answered tool calls before the HTTP listener was up, so the REST comparison leg met a refused connection; the step now polls health 200 first. (2) #26468: plugin.test.ts's 'Transport not auto-started' cases and plugin-execution-context.test.ts (generic 'no longer valid' refusal) corroborate without scoring; the ref names both, and its statement that no packages/mcp test asserts API_KEY_REVOKED still holds. Re-read at base fd246c22 (grep API_KEY_REVOKED over packages/mcp/src: no hit; plugin-execution-context.test.ts, its /no longer valid/ case). No clause changed",
"ref": "#25807"
},
{
"revision": 9,
"date": "2026-11-01",
"change": "QA run #28545 (W3-C1R) finding: steps[7]'s revoke 'as the admin' failed when the admin used an API key, which answers 403 PERMISSION_DENIED 'Changing an API key requires a signed-in session' since #27692 (api-key-management-credential.ts: every non-read sys_api_key operation from an API-key credential is refused). The step now says to revoke from the admin's signed-in session, or Setup's API Keys page, and names that refusal. No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054",
"ref": "#28545"
}
]
},
Expand Down
20 changes: 14 additions & 6 deletions docs/qa/platform-checklist/areas/api-backend.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@
"title": "Data-API query contract: every filter operator gives known answers; $-params, select, sort, expand honored; malformed input 400s with the exact code",
"since": "v15",
"status": "active",
"revision": 8,
"revision": 9,
"priority": "P0",
"surface": "api",
"personas": [
Expand Down Expand Up @@ -478,7 +478,8 @@
}
],
"negative": [
"silent WIDENING is the catastrophic failure mode (#3899): a malformed query body must never degrade into an unfiltered 200 full read. A mistyped clause (e.g. {\"limit\": \"ten\"}) answers 400 VALIDATION_FAILED at the request schema; an UNDECLARED top-level key (e.g. {\"not_a_query_key\": {…}}) is lowered as an implicit field filter and answers 400 INVALID_FIELD. ⛔ Do not probe with {\"filter\": {…}}: `filter` is a DECLARED transport spelling of `where` and is served. Verify by asserting each probe's status and code AND that no response carries the full table"
"silent WIDENING is the catastrophic failure mode (#3899): a malformed query body must never degrade into an unfiltered 200 full read. A mistyped clause (e.g. {\"limit\": \"ten\"}) answers 400 VALIDATION_FAILED at the request schema; an UNDECLARED top-level key (e.g. {\"not_a_query_key\": {…}}) is lowered as an implicit field filter and answers 400 INVALID_FIELD. ⛔ Do not probe with {\"filter\": {…}}: `filter` is a DECLARED transport spelling of `where` and is served. Verify by asserting each probe's status and code AND that no response carries the full table",
"#26644 — an empty projection or a count flag that is neither true nor false is refused, never read as the absent default (every column, or a reported total). GET /api/v1/data/showcase_account?$select= (and ?$select=%20, ?$select=, with a lone comma, ?select= and ?fields=) answers 400 VALIDATION_ERROR, \"Query parameter '$select' names no field\" naming the spelling sent; GET ?$top=2&$count=maybe (and ?$count=1, ?count=) answers 400 VALIDATION_ERROR, \"Query parameter '$count' must be true or false\"; POST /api/v1/data/showcase_account/query {\"fields\": []} (and {\"select\": \"\"}) answers 400 VALIDATION_FAILED at the body schema, with fields[0] on the key, and {\"limit\": 2, \"count\": \"maybe\"} answers 400 VALIDATION_FAILED; GET /api/v1/data/showcase_account/ID?select= answers 400 VALIDATION_ERROR naming select. No probe returns rows: a 200 carrying every column or a total is the widening this contract closed (packages/metadata-protocol/src/protocol.ts projectionFieldsValue and countFlagValue; door pins packages/rest/src/data-empty-projection-count-door.test.ts and packages/runtime/src/domains/data-empty-projection-count.test.ts)"
],
"variants": [
"op:eq",
Expand Down Expand Up @@ -579,7 +580,8 @@
"change": "automated.ref names the HTTP pin for acceptance[4] and says what two named pins do and do not reach; the variants gain the six AST_OPERATOR_MAP operators the list omitted; steps[1] names the comparand-less null spellings. Checklist-accuracy findings of the 18.2.0 wave 2 (QA run #26468, release-18.2.0-w2-tier1-a, at 756220c4; wave anchor #25807, slice A): showcase-expand-crud-gate.dogfood.test.ts drives acceptance[4] over HTTP and was missing (so 'no clause has a pin at its oracle's layer' was false); request-schema-gate.conformance.test.ts has no collection-operator case; engine-filter-array-lowering asserts the engine's INVALID_FILTER, not the POST door's code; and AST_OPERATOR_MAP serves icontains, ilike, is_empty/is_not_empty and before/after beyond the list. Re-read at base e4ba846f (packages/spec/src/data/filter.zod.ts AST_OPERATOR_MAP). acceptance[8] now asks a verdict of six more rows",
"ref": "#25807"
},
{ "revision": 8, "date": "2026-10-31", "change": "steps[1] names the prefix-form compound AST and steps[2] the case contract of the substring operators. Checklist-accuracy findings of QA run #27409 (18.3.0 sweep, wave 3, Tier 1 half A, at c4c515ad; sweep anchor #27377): the infix compound [cond, 'and', cond] answered 400 while the step showed only the single-condition form, and the substring operators matched case-exactly on the SQL driver while the step gave no case note. Re-read at base 488b3ed0: packages/spec/src/data/filter.zod.ts lowerFilterAST and the CONTAINS / NOT_CONTAINS / STARTS_WITH / ENDS_WITH / ICONTAINS descriptions. No clause changed", "ref": "#27409" }
{ "revision": 8, "date": "2026-10-31", "change": "steps[1] names the prefix-form compound AST and steps[2] the case contract of the substring operators. Checklist-accuracy findings of QA run #27409 (18.3.0 sweep, wave 3, Tier 1 half A, at c4c515ad; sweep anchor #27377): the infix compound [cond, 'and', cond] answered 400 while the step showed only the single-condition form, and the substring operators matched case-exactly on the SQL driver while the step gave no case note. Re-read at base 488b3ed0: packages/spec/src/data/filter.zod.ts lowerFilterAST and the CONTAINS / NOT_CONTAINS / STARTS_WITH / ENDS_WITH / ICONTAINS descriptions. No clause changed", "ref": "#27409" },
{ "revision": 9, "date": "2026-11-01", "change": "QA run #28545 (W3-C1R) finding: the #26644 contract (an empty projection or an unrecognised $count answers 400) had no acceptance or negative row, so its recheck rested on a dispatcher note. negative[1] adds it, spelled from source: protocol.ts projectionFieldsValue / countFlagValue (400 VALIDATION_ERROR naming the spelling) and the body schema's 400 VALIDATION_FAILED, with the probe set of data-empty-projection-count-door.test.ts. #28545 observed the #26644 fix holding. A negative is added; nothing is removed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", "ref": "#28545" }
]
},
{
Expand Down Expand Up @@ -1676,7 +1678,7 @@
"title": "A disabled packaged action is refused 409 ACTION_DISABLED at BOTH dispatch doors — after the capability gate (a 403 is never an oracle for the switch), before the param contract and the record load, durable across restart",
"since": "v17",
"status": "active",
"revision": 10,
"revision": 11,
"priority": "P0",
"surface": "api",
"personas": [
Expand Down Expand Up @@ -1744,9 +1746,9 @@
"evidence": "post-restart traces + the row read"
},
{
"clause": "a disabled `type: flow` action is refused by its OWN switch, ahead of the type branch — the refusal is ACTION_DISABLED for the action's name, independent of anything the target flow's own ledger row says",
"clause": "a disabled `type: flow` action is refused by its OWN switch, ahead of the type branch — the refusal is 409 ACTION_DISABLED on the action's own route (POST /api/v1/actions/showcase_task/showcase_bulk_reassign) with the turned-off sentence of acceptance[0], which names no action (describeDisabled ignores the name, packages/objectql/src/action-activation.ts), independent of anything the target flow's own ledger row says, and the target flow is left untouched",
"oracle": "api",
"verify": "step-8: 409 ACTION_DISABLED naming showcase_bulk_reassign; the automation service is never consulted for the run",
"verify": "step-8 on /actions/showcase_task/showcase_bulk_reassign: status 409, body.error.code ACTION_DISABLED and body.error.message the turned-off sentence (first sentence \"This action is turned off for this deployment.\", the Setup › Packaged automation remedy). Do not look for the action's name in it: the sentence names none, and the live pin's expectDisabledSentence ignores the name too. The automation service is never consulted for the run, so showcase_reassign_wizard records no run from it",
"evidence": "the trace"
}
],
Expand Down Expand Up @@ -1841,6 +1843,12 @@
"date": "2026-10-30",
"change": "acceptance[0]'s clause and verify respelled for #27842: the disabled-action sentence is now written for the end user who clicked (what happened, that an administrator can turn the action on again under Setup › Packaged automation, or use another action) and names no system table and no ADR; the engine's describeDisabledAction writes the ledger table, ADR-0126 §8 and the ACTION_DISABLED code to its log line instead. The code and the 409 are unchanged, so every other clause stands; the clause's 'naming the ledger' was the only text that turned false. The dogfood pin's sentence helper and packaged-activation-ledger-reach.dogfood.test.ts's /ACTION_DISABLED|is disabled/ are flipped in the same change (the latter now reads the code alone)",
"ref": "#27842"
},
{
"revision": 11,
"date": "2026-11-01",
"change": "QA run #28545 (W3-C1R) finding: acceptance[6] (clause and verify) expected ACTION_DISABLED 'naming showcase_bulk_reassign', but since #27842 the sentence names no action (action-activation.ts describeDisabled returns describeTurnedOffPackagedItem('action'), and packaged-action-disabled-dispatch-live.dogfood.test.ts expectDisabledSentence ignores its action argument). The clause now asks 409 ACTION_DISABLED with the turned-off sentence on the /actions/showcase_task/showcase_bulk_reassign route, and the target flow untouched. The status, the code and the own-switch ordering are kept; only the expectation that could never hold is replaced. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054",
"ref": "#28545"
}
]
},
Expand Down
16 changes: 11 additions & 5 deletions docs/qa/platform-checklist/areas/approvals.json
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@
"title": "A non-admin reaches the full Approvals Inbox from the account app's Approvals entry",
"since": "v17",
"status": "active",
"revision": 7,
"revision": 8,
"priority": "P0",
"surface": "browser",
"personas": ["a NON-ADMIN business user who is a pending approver and has no Setup access"],
Expand All @@ -1132,10 +1132,10 @@
]
},
"steps": [
"build the console from this checkout (`pnpm console:build`) and keep its ready line; boot the showcase on its own port and DB (dogfood §0)",
"build the console from this checkout (`pnpm console:build`), or take the build the dispatcher made of this checkout at the run SHA, and keep its ready line; boot the showcase on its own port and DB (dogfood §0); then compare the entry chunk the served /_console/ page loads with packages/console/dist's (acceptance[0])",
"grep the BUILT bundle for the registration key: `grep -o 'approvals:inbox' packages/console/dist/assets/index-*.js` — the entry chunk must carry the register call, because the unit pin (#7231) stubs the page at its module boundary and does not assert main.tsx performs the side-effect import",
"sign in as the non-admin persona; GET /api/v1/meta/app and record which apps the server returns for this session",
"open the account app and click the Approvals entry in the Inbox group; screenshot",
"open the account app from the avatar (user) menu's Profile item — it opens /apps/account/component/account/profile_card, which QA run #28545 read as /apps/com.objectstack.account/component/account/profile_card. Not from the app switcher: the account app declares hidden: true (packages/platform-objects/src/apps/account.app.ts), so for this persona the switcher offers only Showcase, Back to home and All apps. Then click the Approvals entry in the Inbox group; screenshot",
"read the resulting URL and the rendered surface (tabs, list rows, keyboard hint bar)",
"open a request from its inbox row — it navigates to the request's served record page, /apps/APP/sys_approval_request/record/ID — and read the rendered action set; execute one decision and re-read the request",
"cold-load the same request via ?request= on this component route and read the URL it lands on",
Expand All @@ -1145,8 +1145,8 @@
{
"clause": "the registration survives BUNDLING — the built dist, not just source, contains the approvals:inbox component registration",
"oracle": "build",
"verify": "`pnpm console:build` ran in this checkout at the run's framework SHA and printed its ready line (`✓ @objectstack/console dist ready (… KB) from ui/apps/console`) AND the built entry chunk contains the `approvals:inbox` register call; a source-only grep does not satisfy this clause",
"evidence": "the framework SHA + the console:build ready line + the grep hit with surrounding bundle context"
"verify": "`pnpm console:build` ran in this checkout at the run's framework SHA and printed its ready line (`✓ @objectstack/console dist ready (… KB) from ui/apps/console`) — run by the runner, or by the dispatcher that prepared the checkout (RUNNER.md: one container builds one tree at a time), whose ready line the run record then cites — AND the server serves that build: the entry chunk the served /_console/ page loads (index-HASH.js) is the one in packages/console/dist, same name and same bytes (QA run #28545 compared index-RFCp05oY.js) — AND that built entry chunk contains the `approvals:inbox` register call; a source-only grep does not satisfy this clause",
"evidence": "the framework SHA + the console:build ready line (and who ran it) + the served-entry comparison + the grep hit with surrounding bundle context"
},
{
"clause": "the entry renders the FULL inbox — tabs (en: My Pending / Submitted by me / All; zh-CN: 待我审批 / 我发起的 / 全部) and the request list — not a component-not-found empty state",
Expand Down Expand Up @@ -1228,6 +1228,12 @@
"date": "2026-10-29",
"change": "fixtures.requires[2] no longer says a persona-created invoice without `owner` lands and reads 404 to its creator: the showcase contributor set now carries the ownership check on insert as well as update (invoice_owner_on_create beside invoice_owner_immutable, examples/app-showcase/src/security/permission-sets.ts), so such a create is refused 403 PERMISSION_DENIED with nothing written. The requirement that the persona own the invoice is unchanged",
"ref": "claude/harden-invoice-owner-check-on-create"
},
{
"revision": 8,
"date": "2026-11-01",
"change": "Two QA run #28545 (W3-C1R) findings. steps[3] did not say how to reach the account app: it is hidden from the app switcher (account.app.ts hidden: true), which offered the persona only Showcase / Back to home / All apps, and is entered from the avatar menu's Profile item (AppHeader.tsx, /apps/account/component/account/profile_card, which #28545 read under the com.objectstack.account segment). acceptance[0] asked the runner to run pnpm console:build itself; it now also accepts the dispatcher's build of the same checkout at the run SHA, and adds a served-entry check (the chunk /_console/ loads is the dist's), so the clause still proves the bundle under test is this checkout's. steps[0] and the evidence follow. No clause weakened: the build, SHA and grep requirements stand, and one check is added. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054",
"ref": "#28545"
}
]
},
Expand Down
Loading
Loading