16c82739db1c12dff004d75a62c03793453e2876
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
825fd41ee4 |
fix(auth): 修复刷新令牌链路三层断点,并把 121 处裸异常收敛为携带 HTTP 语义的异常
用户在微信开发者工具里看到首页全站 401,追下去发现「刷新」这条路整条是断的,
且断在三个互不相干的地方 —— 每修好一层,下一层才显形:
1) 前端漏传 userId → /auth/refresh 直接 400(上一笔
|
||
|
|
2162612892 |
fix(service-appointment): 状态冲突/越权改为 4xx,修并发下 500 与弱断言漏判
CI E2E (live stack) 在 main@63c18d8 上失败:test_appointment_concurrent_safety 并发「用户取消」与「运营确认」得到 [200, 500]。根因是 ServiceAppointmentService 四处状态/权限守卫抛裸 RuntimeException,GlobalExceptionHandler 无对应映射, 只能兜底成 HTTP 500 —— 「预约不存在」「状态不允许」「越权」被报成服务端故障。 改动: - ServiceAppointmentService:四处守卫改用 BusinessException + ApiResponseCode (不存在→404 NOT_FOUND,越权→403 FORBIDDEN,状态冲突→409 REDUNDANT_OPERATION)。 - ReferralService.confirmReferral:同一家族的四条守卫同样收敛(预约/报名的 不存在、越权、非转介类型)。 - tests/e2e/service_appointment_test.py:把 3 处 `!= 200` 的弱断言收紧为 4xx。 弱断言会把 500 当成「越权被拒」放行 —— 这正是该缺陷长期未被发现的原因, 此前只有并发用例的 `< 500` 断言咬了它一口。 - ServiceAppointmentOwnerGuardTest:新增「终态不可再确认/取消」「预约不存在」 用例,并把越权断言从 RuntimeException 收紧到具体业务码(403)。 验证: - 定向后端测试 35 passed(ServiceAppointment* / MatchmakerReferralPhase13Test)。 - 本机真实栈(重建 backend 镜像):service_appointment_test.py 8/8、全量 E2E 65 passed。 - 负向注入(把守卫改回裸 RuntimeException):2 例精准变红,工作区哈希前后一致。 |
||
|
|
797b1c46cf |
refactor(storage): 对象存储从 MinIO 换成 SeaweedFS(钉 4.47)
动机:MinIO 的 GitHub 仓库已于 2026-02 归档,不再产出安全补丁,已累积多个 CVE 且不修;本项目存身份证 / 实名材料 / 用户照片,无人修 CVE 才是真正驱动因素 (Docker Hub 下架只是表象,quay.io 仍在提供)。 变更: - 三套 compose(dev / staging / prod)的 minio 服务替换为 seaweedfs 4.47: 覆盖 entrypoint 渲染 -s3.config 注入 identity(S3 网关默认不鉴权,必须显式开启), -volume.max=100 解除第二个 bucket 写入时的 InternalError。 - 新增一次性 storage-init 容器,经 filer 接口预建 public / private 桶 (SeaweedFS PutObject 到不存在的 bucket 直接失败;全仓库原无任何 createBucket)。 - S3Config 显式开启 path-style:AWS SDK v1 默认 virtual-host 会把 bucket 拼进 主机名,容器名在 DNS 中不存在必然失败(本机有 http_proxy 时易误判为 502 拒绝)。 - ci.yml:镜像校验与起栈步骤改为 seaweedfs + storage-init。 - 运维脚本 health-check.sh / run-uat.sh 的 MinIO 探针改为 SeaweedFS (S3 网关开启鉴权后匿名根请求返回 403,200/403 均算存活)。 注:path-style 与建桶属换存储的必要项;presigned URL 的 host 须对外可达这一 既有缺陷(原 minio:9000 直发客户端不可达)留待下一轮「改缺陷」处理。 |
||
|
|
aa1114338e |
新增对象存储真实链路探针(判据,非 pytest 用例)
背景:E2E 全套 65 例对对象存储零覆盖,而 generatePresignedUrl 只是本地 签名、不访问存储,因此"签名接口返回 200"无法证明对象真能存进去。 实证(2026-09-22):MinIO 的 /data 下只有 .minio.sys,无任何 bucket; mc cp 到 private 报 `Bucket 'private' does not exist`。全仓库零建桶代码、 零初始化脚本 —— 真实上传链路在当前配置下必然失败,而所有测试仍全绿。 本探针覆盖 PhotoService 的真实路径并要求字节级一致:服务端直传、 presigned PUT(客户端直传)、presigned GET,并同时验证 path / virtual-host 两种 addressing style。支持 --no-create-bucket 负向模式,用于证明判据 有判别力而非恒绿。 不在 CI 运行:依赖 boto3 + requests,不进 tests/e2e/requirements.txt。 |
||
|
|
b60c1ffb71 | docs: 数据保留 36 个月政策定稿 + 09-20 收尾状态 —— STATUS 版本演进表重排至 rc.20,隐私政策 §3.3/PIA §8 修订,UAT V1.1 用例 46 条(9 角色×7 路径),E2E 65/65 本机模拟 CI 登记 | ||
|
|
e666ee6f8a |
feat(e2e): Phase 23 Task 2.1 —— 活动互选 8 场景 + 服务预约 8 场景真实实现(16 个真实用例)
背景:`tests/e2e/activity_mutual_consent_test.py` 与 `service_appointment_test.py` 此前是 **诚实占位**(仅 docstring、0 用例,指针指向 run_v11_tests.py),计划 Task 2.1 要求把 文件头保留的场景清单逐条实现为真实 HTTP 用例。本提交完成该任务。 ═══ 新增真实用例 16 个(本机活栈 pytest tests/e2e 49 → 65 passed)═══ activity_mutual_consent_test.py(8,覆盖 INV-014/015/006 + AGENTS.md #19/#20) 1 建立互选记录不建 Match(INV-014) 2 单方确认不建 Match(INV-015) 3 双方确认建 Match Episode 且双方可见 4 已有 active Match 时再次双确认必须复用(#19) 5 曾 Unmatch 后重新双确认开启新 Episode:episode +1(#20) 6 一方拒绝不建 Match,对方随后确认也不建 7 超时:已过期记录不得被推动成双确认(含「未过期必须生效」对照,防假绿) 8 单向意向不泄露(INV-006,含「B 确认后必须看到 accepted」反证) service_appointment_test.py(8) 1 提交预约;2 幂等键重放不得产生第二条;3 运营确认 submitted→confirmed(用户侧同态可见) 4 并发「用户取消 × 运营确认」不得 5xx、最终状态必须收敛;5 我的列表只含本人 6 待确认可取消;7 归属隔离(他人不可读详情/不可取消) 8 运营侧状态闸门:已完成预约不得再被取消 ═══ 两个幂等 seed(E2E 前置数据,本机与 CI 通用)═══ - seed_e2e_activity.sql:organization + 两个 activity。 必须建的原因是硬约束而非选择:`activity_mutual_connection_consents.activity_id` 有 **FK 到 activities(id)**(实测约束名 …_activity_id_fkey),而 activities.organizer_org_id 又 FK 到 organizations(id) 且 NOT NULL —— 全新库里三张表全空,互选无活动可挂。 - seed_e2e_service_station.sql:service_location + **站点-运营关联**。 关联必需:运营确认受数据范围约束(`assertOperatorScope` → `canAccessByOperator`), super_admin **不豁免**,实测 e2e_admin 确认他站预约返回 403。 E2E 因此改用 seed 中该站点关联的 e2e_station。 两个 seed 由测试首用时幂等执行(docker exec psql),无需改 CI 步骤。 ═══ 负向注入(Task 2.1 Step 3)═══ ③ 撤销 service_station_operator 的 service_appointment.confirm → 恰 2 红(test_operator_can_confirm / test_operator_cannot_cancel_completed,403),其余 6 绿; ④ 触发器强制新建互选记录的对方侧 accepted → 3 红(both_confirm_creates_match_episode / timeout_handled / single_interest_not_leaked),其余 5 绿。 两处均已还原并复跑全绿(65 passed),确认无残留(无 e2e* 触发器、权限计数回到 1)。 **两次失败的注入(记录以免后人重复踩)**: - 删除站点→运营关联:**无效** —— 测试的 seed fixture 在用例前幂等重放 seed 会自愈该前置条件; - 改 user_b_status 列默认值:**无效** —— `createMutualConsent` 在 Java 侧显式写 pending, DB 默认值根本不参与(侧面证明写入路径的权威性)。 ═══ 计划文档校正(审阅产物)═══ 按实测推翻 4 处失实断言,登记 7 项新发现(详见计划文档「附 A / 附 B」): - 推翻:matchmaker_services「无表/需扩权」(实为 V009 已建表且 mapper 齐备); 门禁 14/14(实为 18/18);「tests/e2e 现为占位」(实为仅 2 个文件); E2E 基线 49(实为 65)。 - 新发现:① `POST /operator/organizations` 必然 500(createOrganization 不赋 id + IdType.INPUT + 无 DB 默认值;且未校验 chk_org_type)→ 连带「无法新建活动」; ② `createMutualConsent` 从不写 expires_at → 互选永不过期(即批 3 P16-T13,配置与 V041 部分索引空转);③ 用户侧取消预约不校验状态(已确认/已完成也能自取消), 与运营侧明确拒绝 completed 口径不一致 → 需产品裁定; ④ E2E 环境 organizations/activities/service_locations 均为空表(无迁移种子); ⑤ 运营数据范围 super_admin 不豁免(预期行为,勿误判); ⑥ 本机复跑会遇到 Redis 登录锁定键(auth:operator_login_lock:*); ⑦ e2e_* 账号因 ON CONFLICT DO NOTHING 重跑 seed 不覆盖旧口令。 ═══ 验证 ═══ - `pytest tests/e2e` → **65 passed**(含新增 16);`node scripts/run-ui-contracts.mjs` → **18/18 PASS**,退出 0; - 本机环境处置(不属代码改动):把 6 个 e2e_* 账号口令对齐到 CI 口令、清除 Redis 登录锁定键。 - **未验证**:微信开发者工具、CI 上的首次 E2E 实测绿(Task 2.3 Step 3,本机无 Docker Hub 出口)。 |
||
|
|
f96dcb8aa6 |
fix(admin): 管理端入口收敛到 /admin/ 子路径,staging 端口 13521,修复生产 /admin/ 假成功
承接 rc.15 的管理端端口收敛(3000 → 13520),本轮把「入口」也收敛到一处: - staging admin 发布端口 3001 → 13521(保持「生产 + 1」错开),同步 tests/uat/run-uat.sh 的默认 UAT_ADMIN_URL。 - 管理端产物改为 /admin/ 子路径部署:vite 构建 base=/admin/(dev 仍为 /,开发与 排查习惯不变)、router history base 跟随 import.meta.env.BASE_URL、三处容器化 入口(生产 nginx / 本地整套栈 nginx / admin-web 容器内 nginx)统一为「不剥离 前缀」转发,形态一致。 - 修复生产 nginx /admin/ 的假成功:原为 alias /usr/share/nginx/html/,而那正是 nginx:alpine 纯净镜像自带的默认页目录(管理端产物不在该容器内),访问返回 200 + nginx 欢迎页 —— 浏览器不报错、状态码正常,只是打开的不是管理端,比 404 更难发现。改为 upstream admin_web + proxy_pass,并以真实两跳容器实测: /admin/ → 200 且为管理端标题与 /admin/assets/*、/admin/dashboard → 200(SPA 兜底)、/admin/brand/logo-mark.png → 200、根路径 → 显式 404(用 location = / 钉住,不依赖「本配置恰好没有 server 级 root」这一偶然事实)。 - 品牌资源引用统一走新增的 src/brand.ts(public/ 资源不受 vite base 影响,原 5 处 写死根路径,子路径部署下会裂图);favicon 由仓库内并不存在的 /favicon.svg 改为 存在的品牌标。 - 补全 tests/uat/run-uat.sh 被截断的尾部:原文件停在 `if [ -`,bash -n 不通过、 脚本根本无法运行;顺带把此前「定义了却无人消费」的 ADMIN_URL 接到管理端可达性 实测上,并补结果汇总与非零退出码。 - ui-tests:ui-document-baseline-contracts 新增两类一致性断言(staging 发布端口 ⇄ run-uat.sh 默认入口、子路径部署的五处约束);admin-reference-contracts 的资产 断言适配 brand 模块(断言「组件经 brand 模块引用」+「brand 模块指向正式资产」)。 6 条新增断言全部经负向注入验证(注入对应缺陷 → 守卫变红且信息指向正确对象)。 验证:UI 门控 14/14 绿;vue-tsc EXIT=0;vite build EXIT=0 且产物资源均为 /admin/ 前缀;三份 nginx 配置 nginx -t 通过。 注:docs/STATUS.md 含此前的在途文档改动(非本次工作,且其中引用了已被删除的 plans 文件),故未纳入本提交;本轮的口径更新已写入该文件待一并处理。 |
||
|
|
af5412646b |
test(e2e): 运营登录只用 seed 账号;补幂等 E2E 与两处机械守卫
1) 修复一处从未暴露的 CI 缺陷 库中 operator 账号没有任何迁移种子,DemoDataSeeder 也不创建运营账号 (只统计数量),全新库唯一的账号来源是 seed_e2e_operators.sql。 而 conftest 的 admin_token 写死 username="admin" —— 那是某台开发机上 手工创建的遗留账号:本机有它所以本地一直跑绿,CI 全新库没有它 → 该 fixture 必然 401 → e2e job 从未真正通过过。 同一文件里「外加迁移 seed 的 admin」这句注释也是纯粹的虚构。 改用 seed 创建的 e2e_admin(已绑定 super_admin,语义等价)。 2) fixture_account_guard_test.py(2 用例) 机械拦住「fixture 依赖本机手工数据」:从 seed SQL 解析账号清单, 扫描 conftest 的所有登录用户名,任何一个不在 seed 内即变红; 另一条锁住推理前提(迁移里确实没有 operator 种子)。 两条都带「解析失败即失败」断言 —— 守卫自身静默失效是本项目反复踩过的坑。 3) idempotency_test.py(7 用例) 同键重放响应逐字节相同、同键重复提交只产生一条记录、body 字段幂等未回归、 不同键相互独立、同键作用域按 actor 隔离、5 线程并发同键业务恰好落库一次、 无键重复 Like 不得返回 500。 4) conftest.ApiClient.call 支持 headers 覆盖,供幂等用例携带自定义键。 |
||
|
|
cbb4f64fdc |
fix(security): stop operator identity from reaching user endpoints; guard path rules
Continues the HTTP-semantics work. Fixing 401/403 exposed a worse defect on the
same axis: who is allowed to reach which endpoint.
Root cause chain:
- PlatformPrincipal has two implementations (OperatorPrincipal / UserPrincipal),
chosen by JwtAuthFilter from the token type.
- `@AuthenticationPrincipal UserPrincipal` against an OperatorPrincipal is a type
mismatch, and Spring Security returns null (errorOnInvalidType defaults to
false) instead of failing.
- 244 uses of that annotation repo-wide; only 48 null-check. The rest NPE -> 500
when reached with an operator identity.
- The fallback rule was `.authenticated()`, which an operator token satisfies, so
such requests did reach user-facing controllers.
Adding ~196 null checks would be mechanical, easy to miss, and does nothing to
constrain future code. The mismatch is between identity type and endpoint, so it
is fixed once, at the authentication layer: the fallback becomes
`.hasRole("USER")` (operator -> ROLE_OPERATOR, user -> ROLE_USER, per JwtAuthFilter).
Operator requests to user endpoints now get 403 before any controller runs.
Trap worth recording: operator endpoints are not all under /operator/ —
/counseling/services/operator/queue, /content/operator/articles,
/activities/operator/checkin/scan, /service-appointments/operator/list and others
sit deeper inside business paths. The first attempt covered them with
`requestMatchers("/**/operator/**")`, which fails at runtime:
PatternParseException: No more pattern data allowed after {*...} or ** pattern element
Spring's PathPattern permits `**` only as the final element. Worse, the symptom is
a 500 from the whole filter chain (Tomcat HTML error page, not our JSON), and E2E
went from 37 passed to 33 failed — an error far removed from its cause. Now the
nine prefixes are listed explicitly, each ending in /**.
Because explicit enumeration can be forgotten as endpoints are added, and because
the failure mode is "operators locked out of their own admin module" (invisible to
user-side tests), SecurityPathCoverageTest parses the OPERATOR rule prefixes out of
SecurityConfig source, walks every *Controller.java, joins class-level and
method-level mapping paths, and asserts every path containing /operator/ is
covered (/auth/operator/** excluded by design). Two things that would silently
neuter it, both handled: it must parse only the .hasRole("OPERATOR") chain — pulling
in the permitAll or fallback rules would mark everything covered — and the test
working directory is the repo root, not the Maven module root.
Verification: negatively validated by removing /content/operator/** -> exactly one
test turns red listing the six affected ContentController endpoints; restored.
E2E 40 passed (37 + 3 identity-isolation cases). Backend regression 664 / 0
failures / 0 errors / 3 skipped.
|
||
|
|
7682abd85c |
fix(api): give failures real HTTP semantics; fix 401/403 confusion and CORS default
Phase 23B ended with an E2E assertion changed to expect "HTTP 200 + body.code == BAD_REQUEST", documented at the time as "this project's convention". That description was accurate about the code and wrong about the intent: it described a defect, and I made the test accommodate it instead of fixing it. Auditing it found the convention was already broken, not merely risky: - ~100 sites (105, across 30 controllers) do `return ApiResponse.error(...)` / `forbidden()` / `notFound()` with no exception and no `@ResponseStatus`, so the HTTP status is always 200. Gateways and APM cannot separate 4xx from 5xx; callers cannot tell "fix your request" (400) from "don't retry" (403) from "check state first" (409) from "retry later" (429). - miniapp/utils/api.js already had 401/403/429 branches that could never be reached, since the backend never returned those codes. The concrete damage: an expired token returned 403, so `onSessionExpired` never fired and users were stuck with no way back to the login flow. - SecurityConfig had no `authenticationEntryPoint`, so Spring's default `Http403ForbiddenEntryPoint` returned 403 for unauthenticated calls, with a framework body that has no `code` field at all. - CorsConfig defaulted `allowed-origins` to `*` together with `allowCredentials(true)`, while its comment claimed "localhost only by default". Comment contradicting code is worse than no comment: it reads as safe. - Only 4 files / 15 assertions in the whole backend suite touched HTTP status, which is how this survived. Approach: one definition, two applications, zero controller changes. - ApiResponseCode gains an `httpStatus` field — the single source of truth. The constructor requires it, so a new code cannot be added without one. - GlobalExceptionHandler reads status from the enum instead of hardcoding `@ResponseStatus` per handler. - New ApiResponseStatusAdvice (ResponseBodyAdvice) fills in the status just before the body is written, covering all ~100 direct-return sites without touching them. Two constraints, both noted in code: only downgrade when the current status is still 2xx (the advice also sees exception-handler results, and must not override an already-correct 401/403/409), and never alter the body. - SecurityConfig: entry point returns 401 with a uniform ApiResponse body; access denied handler returns 403 likewise. - CorsConfig: default changed to empty (deny). Both clients are same-origin anyway — nginx proxies /api/ and Vite proxies /api — so nothing depends on it. - miniapp api.js: 401/403/429/409 branches now prefer the server's `body.message`, otherwise "账号已被封禁" would be flattened to "无权限访问". Implementation trap worth remembering: `ServerHttpResponse` has `setStatusCode` but no getter, so reading the current status requires casting to `ServletServerHttpResponse`. Verification: 37 E2E passed (33 + 4 new contract cases), including a live-stack check that a controller's direct `notFound()` now yields 404 — that path throws no exception and never reaches the exception handler, so if the advice were inactive it would silently return 200 while the enum unit tests stayed green. Backend regression 662 / 0 failures / 0 errors / 3 skipped (+6 new guards). The first regression run had 3 failures, all tests asserting the old wrong behavior (anonymous access expected 403; outsiders reading a conversation expected 200). Fixed the assertions, not the implementation. Where 404 is used instead of 403 for "invisible", that is deliberate — it prevents probing whether another user's conversation exists — and the substantive assertions (e.g. that an unauthorized response must not leak message bodies) were kept. Not done, needs a product decision: third-party credential issuance, whether rate limiting is actually active, publishing OpenAPI externally, per-caller audit and quotas, pagination/idempotency coverage, desensitization boundaries. |
||
|
|
730dddde30 |
test(e2e): Phase 23B — rebuild E2E infrastructure; fix 6 real prod defects it exposed
Phase 23 closed as PARTIAL: fake coverage was gone, but the E2E layer itself
was still empty — no conftest.py anywhere, ~15 fixtures that never existed,
6 files crashing at import on os.environ["E2E_PASSWORD"], and CI never ran E2E.
STATUS's "10 E2E files / ~80+ cases ✅" described files that had never executed.
Infrastructure delivered:
- tests/e2e/conftest.py: ApiClient (trust_env=False — a host http_proxy turns
localhost calls into 502), operator_token(username) / new_user / admin_token
fixtures, and a session probe that calls pytest.exit(4) when the stack is
unreachable. NOT skip: skip would launder "did not run" into "passed".
- pytest.ini, tests/e2e/requirements.txt, and tests/e2e/sql/
seed_e2e_operators.sql — migrations contain NO operator seed at all, so a
fresh database has zero logins; the seed creates 6 e2e accounts with role
bindings, idempotent, password injected via `psql -v pwd`.
- CI job `e2e`: compose up -> poll /health/live (timeout = fail) -> seed ->
pytest -> always teardown. Gate D-23-D is now closed.
- 8 E2E files migrated off import-time env reads; tautological assertions
(`x == t or not x`), a counselor token standing in for content_editor, and a
bare `status_code == 200` claiming to prove INV-020 were replaced with real
permission/behavior assertions.
Six real production defects found only by running against real PostgreSQL
(H2 has zero coverage for all of them):
- D-23B-A (Critical): stale V042__add_appointment_operator_register_permission.sql
left in target/classes; src had replaced it with V042__add_service_location_
announcements.sql. Packing without clean put two V042 in the jar -> Flyway
"Found more than one migration with version 042" -> app would not start.
Guard added: testBuildOutputHasNoDuplicateMigrationVersions scans build output
(src is clean, so only this catches it).
- D-23B-B (High): non-UUID path variable reached the DB -> 500 + leaked
`invalid input syntax for type uuid`. Now rejected at the controller.
- D-23B-C (High): 6 E2E files raised KeyError at import -> never collected.
- D-23B-E (Critical): permissions = 48, role_permissions for super_admin = 18,
matchmaker = 0. V015's "grant all to super_admin" was a point-in-time snapshot;
the 30 permissions added later were never granted. On a fresh stack an admin
cannot create a service station and matchmakers can do nothing. V044 restores
super_admin to its documented definition (not an expansion); the matchmaker
permission set needs a product decision and is left as a residual.
- D-23B-I (Medium): IllegalArgumentException (e.g. invalid activity type) was
unmapped -> 500. Now BAD_REQUEST.
- D-23B-J (High): `operator does not exist: uuid = character varying` — Java
ids are String, columns are uuid, PostgreSQL does not implicitly cast in
comparisons. Fixed at the source with JDBC `stringtype=unspecified` (docker
profile); production datasource comes from deploy env and needs the same.
One judgement retracted: an initial read suggested service_appointment.register
was missing. It is not — the endpoint uses service.assisted_registration, seeded
in V029, and the code deliberately avoids a duplicate permission. Nothing was
changed on that basis.
Verification: 33 E2E passed against a live stack; unreachable stack and a
missing operator password both exit 4 (never skip/pass); backend regression
656 / 0 failures / 0 errors / 3 skipped (baseline 655, +1 guard, negatively
validated by injecting a duplicate V042 -> exactly one test turned red).
|
||
|
|
ae1bd2b734 |
test(phase23): KEEP-VERIFY — remove 44 pass shells + fake INV tests, add shell guard
Phase 23 "可信系统测试重建 (REFACTOR)" re-audit. STATUS claimed "10 E2E
files / ~80+ cases" and "INV-011~020 ✅"; measurement showed the opposite.
D-23-A (Critical): V11InvariantsTest's 14 cases asserted only Constants
strings. INV-016 "Banner target types" actually asserted ARTICLE_STATUS_*;
INV-017 "home 8 entries" declared local arrays then asserted
firstRow.length == 4 (tautology, never called the home service);
INV-018/019/020 used appointment/article/account status constants as
stand-ins for permission and data-scope checks. Rewritten to 12 cases:
4 real service-behavior guards (INV-013 via real CounselingService.getDetail
— Matchmaker sees null userRequestSummary, with an assigned-Counselor
counter-proof so global sanitization cannot fake a pass; INV-016 reads the
production ALLOWED_TARGET_TYPES whitelist and asserts no external/http/url/
web entry; INV-017 calls real getHome and pins all 8 codes + order;
INV-019 asserts content_editor reads null), plus assertBehaviorTestExists
turning "行为验证见 XxxTest" pointers into executable Class.forName contracts.
D-23-B (High): 44 `pass` shells across 6 files in tests/e2e (4 files were
100% shells). Gate forbids skip/pass shells. Rewritten in place as honest
placeholders with the full scenario lists preserved.
D-23-C (High, residual): the pytest E2E suite is not executable at all —
no conftest.py anywhere, ~15 fixtures (api_client/verified_user/
banner_factory/...) do not exist. Only run_v11_tests.py really runs
(16 cases, needs a live stack + E2E_PASSWORD).
D-23-D (High, residual, NOT fixed): CI does not chain E2E as the plan
requires. Needs an infrastructure decision on how CI provisions a live
stack; not implemented rather than written unverified.
D-23-E (Medium): the CI migration job claimed to "验证 migration SQL 语法"
but only checked file non-emptiness, while the real syntax/baseline checks
live in MigrationValidationTest (already run by the backend job). Replaced
with a real sequence check (naming / non-empty / unique versions / no
gaps); verified by extracting the exact inline script and running it.
New gate: scripts/check-no-empty-tests.py (AST scan, blocks pass-only and
unconditionally skipped tests) + a CI test-shell-guard job. Verified both
ways: 0 findings on the real tree, 2 precise findings on injected shells.
Negative validation: 3 injected production defects (external_url in the
banner whitelist, swapped home entry order, isMatchmaker returning the full
record) turned exactly inv016/inv017/inv013 red, 9 others green, zero
unrelated failures; md5sum -c confirmed byte-identical restore.
Full regression: 655 tests / 652 passed / 0 failures / 0 errors / 3 skipped
(baseline 657; -2 because 14 vacuous cases were merged into 12). The 3
skips carry explicit reasons (2x @EnabledIfEnvironmentVariable(CI=true),
1x @Disabled "需要完整数据库连接") — none is a shell.
Phase 23 is PARTIALLY closed: fake coverage is gone, but the UAT gate
requires the end-to-end chain to actually execute, so "失败为零可进入 UAT"
is NOT claimed.
|
||
|
|
ac2327b499 | chore: checkpoint before switching to main | ||
|
|
5b51d5b3f1 | feat: complete phase 02 RBAC and data scope recovery | ||
|
|
b46b48eb33 | chore: establish V1.1 recovery baseline |