Skip to content

test(e2e,showcase): 执行器收官验收 —— showcase 端点回迁 + 真实 boot 探针(#5040 E8) - #5230

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5112-acceptance
Aug 4, 2026
Merged

test(e2e,showcase): 执行器收官验收 —— showcase 端点回迁 + 真实 boot 探针(#5040 E8)#5230
os-zhuang merged 3 commits into
mainfrom
claude/issue-5112-acceptance

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5112
Part of #5040(E8 —— 收尾件)

这一单做了什么

#4936 把 showcase 的两条 apis: 声明注释掉,理由是当时端点面全链路零执行:没有挂载、没有匹配器、每个键(包括 authRequired)解析通过而不生效。E1–E7 建成执行器、把整面硬拒收窄为逐端点 publish 门之后,那条理由不复存在。本单按原意恢复这两条 —— 同名、同 target、同 authRequired、同 cacheTtl —— 只做 ADR-0121 D1 要求的一处修改:

- '/api/v1/showcase/tasks'            + '/api/v1/apps/showcase/tasks'
- '/api/v1/showcase/inquiries/purge'  + '/api/v1/apps/showcase/inquiries/purge'

匿名面没有增加:两条历史声明本来就都是 authRequired: true。一个例子不该长出它从来没有过的公开面 —— 匿名分支改由一份一次性 fixture 承担(见下)。

RED-first(证据,不是说法)

两份 e2e 的断言先于 showcase 回迁写就并运行。pre-restore 状态下:

 FAIL  ... > GET /meta/api returns both restored declarations
 FAIL  ... > object_operation endpoint > answers 200 for an authenticated caller
 FAIL  ... > object_operation endpoint > carries the declared cacheTtl as a Cache-Control header
 FAIL  ... > object_operation endpoint > DENIES an anonymous caller with 401 — authRequired finally gates
 FAIL  ... > object_operation endpoint > never lets an error answer carry a cache directive
 FAIL  ... > flow endpoint > answers 200 for an authenticated caller and runs the janitor flow
 FAIL  ... > flow endpoint > DENIES an anonymous caller with 401
 FAIL  ... > /openapi.json > documents the object_operation endpoint under its declared path
 FAIL  ... > /openapi.json > documents the flow endpoint under its declared path
 Test Files  1 failed (1)
      Tests  9 failed | 5 passed (14)

九条全部是 #4936 记录的那个裸 404。通过的 5 条正是两个状态下都必须成立的:挂载点下未声明路径的裸 404、挂载点外的裸 404、方法不匹配、以及「不宣告运行时不服务的东西」。回迁后:

 Test Files  2 passed (2)
      Tests  22 passed (22)

两份 e2e,分工是刻意的

showcase-declarative-endpoints.dogfood.test.ts —— 走真实 artifact 摄入路径(MetadataPlugin + local-file,createStandaloneStack 用的同一条),所以这次 boot 本身就再证明了一遍 E7 的 publish 门接受回迁后的声明。覆盖:匹配命中执行、finddata 与内建 /data 路由逐字节相同、匿名 401、cacheTtl 只随成功答案上线、挂载点下未声明路径与挂载点外裸 404 byte-identical/meta/api/openapi.json 描述的正是挂载的东西。

#5112 的验收注记,断言 components.schemas 与内建 $ref 解析(存量 #5168),只断言端点 path 条目。

declarative-endpoint-policy.dogfood.test.ts + fixture —— ADR-0121 D6 的匿名分支。省略 authRequired 拒绝匿名(升级指南「omission is SAFE」这句话第一次被测量而不是被相信)、显式 false 服务匿名、已装配预算耗尽后 429 且 Retry-After 真的在线上、429 不带缓存指令、预算是逐端点的。外加 E7b(#5189/#5203)的直写拒绝探针:绕过 publish 直接往 store 写一条 authRequired: false 且无已装配预算的条目,两种调用者都拿 404。

真实 boot 探针(原始输出,已裁剪)

pnpm dev:showcase -- --fresh -p 39720 --seed-admin,47 plugins,29 flows,130 seed rows。

########## P1  GET /api/v1/apps/showcase/tasks  (authed)
HTTP/1.1 200 OK
cache-control: private, max-age=30
content-type: application/json
  body keys: ['success', 'data']    data keys: ['object', 'records', 'total', 'hasMore']
  total: 10  records: 10   first record title: Audit current IA

########## P2  GET /api/v1/apps/showcase/tasks  (ANONYMOUS)
HTTP/1.1 401 Unauthorized
  {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}

########## P3  POST /api/v1/apps/showcase/inquiries/purge  (authed)
HTTP/1.1 200 OK
  {"success":true,"data":{"success":false,"error":"Node 'purge' failed: delete_record(showcase_inquiry) failed:
   Delete requires an ID or options.multi=true","durationMs":9,"summary":{"selected":1,"acted":0,...}}}
   ↑ 流本身的存量缺陷,已立 #5225;对照组见下

########## P4  POST purge (ANONYMOUS)                     HTTP/1.1 401 Unauthorized
########## P5  GET /api/v1/apps/showcase/no-such-endpoint  HTTP/1.1 404  {"error":"Not found"}
########## P6  GET /api/v1/no-such-route-e8                HTTP/1.1 404  {"error":"Not found"}
          byte-identical to P5: YES
########## P7  POST /api/v1/apps/showcase/tasks (方法不匹配)
HTTP/1.1 404 Not Found   {"error":"Not found"}   byte-identical to bare 404: YES

########## CONTROL: 同一条流走内建 automation 路由
POST /api/v1/automation/showcase_inquiry_purge/trigger → HTTP/1.1 200 OK
  success: False   error: Node 'purge' failed: delete_record(...) Delete requires an ID or options.multi=true
  ↑ 与 P3 逐字相同 —— 执行器是忠实的,坏的是流

########## P8  GET /api/v1/meta/api
  - showcase_task_feed        GET  /api/v1/apps/showcase/tasks            authRequired=True  cacheTtl=30
  - showcase_inquiry_purge_api POST /api/v1/apps/showcase/inquiries/purge authRequired=True  cacheTtl=None

########## P9  GET /api/v1/openapi.json(仅端点 path 条目;#5168 的 schemas 不断言)
   GET  /api/v1/apps/showcase/tasks
     operationId: showcase_task_feed | summary: Task feed
     security: [{"bearerAuth": []}] | responses: ['200', '401']
   POST /api/v1/apps/showcase/inquiries/purge
     operationId: showcase_inquiry_purge_api | summary: Purge closed inquiries
     security: [{"bearerAuth": []}] | responses: ['200', '401']
  total paths in doc: 201

没有 429 的真实 boot 探针,因为回迁的两条都没有声明 rateLimit —— 而给一个已发布示例加一条它历史上没有的限流(必然连带一条匿名端点)正是不该做的事。429 + Retry-After 由 fixture 那份 e2e 在同样真实的 boot 上证明。

P7 的说明:声明式端点走的是 setFallbackHandler(Hono notFound),不是注册路由,所以方法不匹配是 404 而不是 405 + Allow —— 与「挂载点不改变任何人的未匹配答案」是同一件事。405 + Allow 的对照在已注册路由上仍然成立。

升级文档安全复核 —— PASS

docs/protocol-upgrade-guide.mddeclarative-apis-endpoints-live 条目,逐条对着实现读:

文档的说法 实现 判定
path 移到 /api/v1/apps/{manifest.namespace}/{subpath} namespaceGate + DEFAULT_RUNTIME_PREFIX + APP_ENDPOINT_SEGMENT
namespace 必须显式,无 manifest.id 推导回落 NAMESPACE_RE,无 derive 分支(Q1=A)
authRequired 缺省 true,省略是安全的 e2e 实测:省略键的端点对匿名返回 401 ✅ 已测量
只有显式 false 打开匿名 e2e 实测:显式 false 的端点匿名 200 ✅ 已测量
D6 判据是 rateLimit.enabled === true,不是键存在 policyGate:authRequired === false && !armed
「写了窗口和配额却不写 enabled 会得到匿名且完全不计量的端点」 实测反证:临时插入该形状,objectstack validate 判红并给出处方 ✅ 见下
验收 (3):objectstack validate 证明没有 17.x 不能执行的形状 五道门全部在场(target / mapping / policy / uniqueness / namespace)
验收 (4):匿名请求 session-only 端点返回 401 而不是数据 P2 / P4

反证的原始输出:

✗ defineStack validation failed (1 issue):
✗ apis.2.rateLimit: Endpoint 'e8_doc_review_probe' (apis[2]) declares `authRequired: false`
  without an ARMED rate limit. ... declare `rateLimit: { enabled: true, windowMs: 60000,
  maxRequests: 100 }`. `enabled` is not `true` — and it DEFAULTS to `false`, so writing only
  `windowMs` / `maxRequests` declares a budget that meters nothing and the endpoint would be
  anonymous AND unmetered. ...

指令在场、准确、可执行 —— 一个没有其它上下文的 AI 维护者照着 grep authRequired: false、照着报错处方改,就能落地。未编辑生成文件。

两条精度备注(不构成 FAIL,建议走 registry 修):

  1. 条目把 /api/v1 写成字面量,而门确实硬编码 DEFAULT_RUNTIME_PREFIX —— 重设了 dispatcher prefix 的部署无法发布任何端点。这是 17.x 立项:建设声明式 ApiEndpoint 执行器(挂载 + matchEndpoint + authRequired/cacheTtl/inputMapping/outputMapping 逐键接线) #5040 §7-8 的已知开放问题,文档里没有点出这个前提。
  2. grep 指引只覆盖源码里的 apis: 块。已经以 sys_metadata 行存在的 api 条目(pre-17 的 Studio 写入)没有东西可 grep —— 而那正是 声明式端点的两个机器可读面会说谎:runtime-authored api 行在 /meta/api 与 /openapi.json 里在场,匹配器却永远看不见(真实 boot 实测) #5224 那条路径。

如实报告的三件事

多租户交错验收(E5b)—— 无法表达,不是没做。 端点步骤的多租户分支判据是 isMultiTenantHost(),即 kernel-resolver 服务是否注册。全仓 grep:该服务在本仓只有消费者没有提供者(http-dispatcher.tsrest-api-plugin.tscurrent-user-endpoints.ts 都是 getService),提供方在 cloud 分发里。bootStack({ multiTenant: true }) 给的是多组织(tenancy posture),单内核,isMultiTenantHost() 恒为 false。所以「按请求自己的环境解析」与「环境解析不出 → 拒绝服务(404 + warn)」两条在本仓的任何真实 boot 上都不可达。顺带查出:那条 decline 分支的提示语在全仓只出现在 dispatcher-plugin.ts 源码里,没有任何测试引用它 —— 该分支目前零覆盖。 建议作为 E 系列的收尾 followup 单独派发(需要在 packages/runtime 里用桩 kernelResolver 驱动)。

composed-stack(设计 §7-7)—— 仍然开放。 composeStacks 在本仓只有 packages/spec 自己的定义与测试引用,examples/ 与任何 first-party 启动路径都不组合 stack。所以「组合栈的端点加载」在示例层无法探针,E7 的结论(composeStacks 不重新解析)未被本单改变。

覆盖真相。 E 系列没有留下 istanbul/c8 豁免或 coverage 配置条目;它留下的是散在源码里的「结构性不可达」断言,而这些断言在 E7 翻转 + 本单回迁之后已经不成立了。这些文件都在本单文件面之外(#4604),仅列出供 PM 派发:

顺手发现的、已单独立项的

验证(真实输出)

example-showcase verify   validate ✓(仅存量 warning)· tsc --noEmit clean · 11 files / 87 tests passed
dogfood 新增两份 e2e      Test Files 2 passed (2) / Tests 22 passed (22)
dogfood 全量             Test Files 85 passed | 1 skipped (86) / Tests 505 passed | 3 skipped (508)
dogfood typecheck        tsc --noEmit clean
pnpm build               Tasks 71 successful, 71 total(artifact 内 apis 两条,路径已在 carve-out 内)
eslint                   clean(六个改动文件)

现场已清:dev server 全部按 PID 停止,--fresh 的 OS_HOME 临时目录随退出自动删除,e2e 自己的 tmpdir 在 afterAllrmSync


🤖 Generated with Claude Code

https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd


Generated by Claude Code

claude added 3 commits August 4, 2026 10:47
#4936 把 showcase 的两条 `apis:` 声明注释掉,理由是当时端点面全链路零执行:没有
挂载、没有匹配器、每个键(包括 `authRequired`)解析通过而不生效。E1–E7 建成执行器
并把整面硬拒收窄为逐端点 publish 门之后,那条理由不复存在。本单把两条声明按原意
恢复 —— 同名、同 target、同 `authRequired`、同 `cacheTtl` —— 只做 ADR-0121 D1
要求的一处修改:路径迁入本应用的命名空间保留区 `/api/v1/apps/showcase/…`。

- examples/app-showcase:恢复 TaskFeedEndpoint / InquiryPurgeEndpoint;coverage
  的 `apis` 从 waived 翻回 demonstrated(理由改写为「由真实 boot 测量」而不是
  「声明即证明」—— 这正是 #4936 抓到的那类假覆盖);gap-fill 的反向断言改回正向,
  并新增命名空间与「零匿名面」两条静态钉子。
- packages/qa/dogfood:两个真实 boot e2e。showcase 一份走真实 artifact 摄入路径
  (MetadataPlugin + local-file),证明匹配命中执行、find 的 data 与内建 /data
  路由逐字节相同、匿名 401、cacheTtl 只随成功答案、挂载点下未声明路径与挂载点外
  的裸 404 完全一致、/meta/api 与 /openapi.json 描述的正是挂载的东西;fixture
  一份补上 ADR-0121 D6 的匿名分支 —— 省略 authRequired 拒绝匿名、显式 false 服务
  匿名、已装配预算耗尽后 429 且 Retry-After 真的在线上。

RED-first:两份断言先于 showcase 回迁写就并运行,pre-restore 状态下 9 条失败(全部
是 #4936 记录的那个裸 404),回迁后 14 + 7 全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
E7b(#5189/#5203)在 main 上落地之后,「一次绕过 publish 的直写不会变成活路由」
从待办 TODO 变成可以真实断言的事:往 store 里直接写一条 `authRequired: false`
且没有已装配预算的 api 条目(D6 最危险的那个形状),两种调用者都必须拿到 404 ——
不是「因为匿名所以 401」,而是压根没有这条路由。

同时把 fixture 里那条「故意省略 authRequired」的声明改标 `ApiEndpointInput`:
`ApiEndpoint` 是 schema 的输出类型,`.default(true)` 已被物化,该键在上面是必填,
所以用它做注解的 TS 作者根本写不出升级指南推荐的那个安全形状(另立 #5227 记录)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
@vercel

vercel Bot commented Aug 4, 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)
objectstack Ignored Ignored Aug 4, 2026 11:10am

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/dogfood.

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

  • content/docs/permissions/authorization.mdx (via packages/qa/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa/dogfood)

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 marked this pull request as ready for review August 4, 2026 11:11
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 11:11
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 9f41ee6 Aug 4, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5112-acceptance branch August 4, 2026 11:25
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…bjectstack-ai#5244) (objectstack-ai#5395)

* docs(examples): showcase 的 cacheTtl 注释写成 private —— 运行时发的就是它,且它是安全规则 (objectstack-ai#5244)

`examples/app-showcase/src/system/apis/index.ts` 里 `TaskFeedEndpoint.cacheTtl`
上方的注释把响应头写成 `Cache-Control: public, max-age=30`,运行时发的是
`private, max-age=30`(`packages/runtime/src/endpoint-policy.ts` 的
`computeCacheControl`;PR objectstack-ai#5230 的真实 boot 探针 P1 也打印了实测值)。

`private` 在这条链上不是调优选择而是安全规则:任何一条响应都可能按调用者被 RLS
裁剪,共享缓存绝不能存下来再发给别人。而这份文件是声明式端点唯一的一手示例,是 AI
作者最可能整段抄走的那份 —— 抄走 `public` 正好得出该规则要挡住的推断。

因此除了把 `public` 改成 `private`,注释还点明它不是本示例的调优选择(在
`authRequired: false` 上同样成立),并**引用** `computeCacheControl` 的文档块而不是
复述第二套规则;顺带把 `cacheTtl: 0` 的语义(`no-store`,不是「不发头」)一并指过去。

注释的后半句(只随成功答案上线、不随 401/429/5xx、GET-only)本来就是对的,保留。

零行为变更:不动运行时,不动声明本身(`cacheTtl: 30` 是对的)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w

* docs(examples): 把「every ttl」收紧成「every positive ttl」(objectstack-ai#5244)

自查时发现上一版注释里「the runtime emits it for every ttl」本身就不准:`private`
只出现在正 ttl 上,`cacheTtl: 0` 走的是 `no-store`,并不带 `private`。这正是本 PR 要
消灭的那一类失真,故就地收紧为「every positive ttl」,与紧随其后的 `cacheTtl: 0`
说明自洽。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…able until the E7 flip" comment family + cover the multi-tenant decline branch (objectstack-ai#5399) (objectstack-ai#5404)

The objectstack-ai#5040 E7 publish flip landed (packages/spec/src/api/endpoint-publish-gate.ts
opens with "This module is that flip"), and E8 moved endpoints back into the
OpenAPI document. Thirteen comments across runtime/metadata/rest still asserted
the pre-flip world -- "Structurally unreachable today", "Nothing calls this yet",
"Today it emits nothing" -- which is exactly the objectstack-ai#5078 defect: a comment that
contradicts the code in front of it.

Each site is rewritten to describe current reality and cite the authority
(the publish gate, or the real-boot probe in
packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts, whose
/openapi.json case disproves "emits nothing" directly). Surrounding still-true
prose is preserved. Pure comment change: the only non-comment line touched is a
descriptive `note:` string in the route ledger.

Also adds dispatcher-plugin.multi-tenant-endpoint.integration.test.ts, covering
the E5b branch "multi-tenant resolution finds no environment -> decline + warn",
which had zero test references repo-wide because the kernel-resolver PROVIDER
ships in the cloud distribution. A stub resolver drives a real boot over a real
socket: a placed request executes on its own tenant kernel; an unplaced one gets
the transport's bare 404, the warn, and no probe of any declaration.

The host kernel deliberately declares the same path too. Without that, the
decline cases pass vacuously -- verified by deleting the branch and re-running:
with tenant-only declarations only the warn assertion moved, because the step
declined a second time for an unrelated reason. With the host copy in place,
deleting the branch turns both decline cases red with "expected 200 to be 404",
the cross-tenant answer the branch exists to prevent.

Out of scope, filed as objectstack-ai#5400: the sibling `else` branch still logs `debug` when
an adapter exposes no setFallbackHandler. Its own comment scheduled a move to
`warn` "when that flip lands" -- it has landed, so declared endpoints are now
silently unservable there -- but changing the level is a behavior change, so
this commit only makes the comment truthful and names the tracking issue.

Refs objectstack-ai#5231, objectstack-ai#5040, objectstack-ai#5078, objectstack-ai#5230, objectstack-ai#5385


Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E8(#5040 执行器):验收 —— showcase 端点回迁 /apps/showcase/… + RED-first e2e + 真实 boot 探针 + 升级文档安全复核

2 participants