From 248fa5d1820adc18a034611030f903cea6eba83f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 01:51:46 +0000 Subject: [PATCH 01/11] docs(acceptance): revise the 17.0 acceptance test plan for rc.2 / current main Rebases the rc.0-era plan from upgrade/objectstack-17 onto the current tree: drops retired surfaces (AI agents #512, competitor #551), adds the activity model (#592/#670), refreshes the metadata census to the rc.2 numbers (17 objects / 26 actions / 24 flows / 5 dashboards / 10 reports), adds the profile permission matrix, four-locale i18n checks, the full-coverage flow pass, and the platform-blocker retest ledger with expected outcomes from the upstream fixes shipped in rc.1/rc.2. Refs #520 #521 #522 #524 #525 #526 #528 #508 #509 #510 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- .changeset/acceptance-17-rc2-test-plan.md | 4 + docs/acceptance-17/test-plan.md | 145 ++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 .changeset/acceptance-17-rc2-test-plan.md create mode 100644 docs/acceptance-17/test-plan.md diff --git a/.changeset/acceptance-17-rc2-test-plan.md b/.changeset/acceptance-17-rc2-test-plan.md new file mode 100644 index 00000000..80600355 --- /dev/null +++ b/.changeset/acceptance-17-rc2-test-plan.md @@ -0,0 +1,4 @@ +--- +--- + +docs: revised 17.0 GA acceptance test plan for rc.2 (docs-only, no package changes) diff --git a/docs/acceptance-17/test-plan.md b/docs/acceptance-17/test-plan.md new file mode 100644 index 00000000..0e6cfb2f --- /dev/null +++ b/docs/acceptance-17/test-plan.md @@ -0,0 +1,145 @@ +# HotCRM × ObjectStack 17.0 GA 验收测试计划(rc.2 / 当前 main) + +> 修订自 `upgrade/objectstack-17` 分支的 `docs/upgrade-17/test-plan.md`(rc.0 底本)。 +> 本版针对 **当前 main**:`@objectstack/*` **17.0.0-rc.2**(#579 升 rc.1,#663 升 rc.2), +> `engines.protocol ^17.0.0-rc.2`。任务原文以 rc.1 为目标;main 已于 2026-08-03 前进到 +> rc.2,验收以当前 main 为准,判定注明 rc.2。 + +- **环境**:远程沙箱独立容器,分支 `claude/hotcrm-17-rc1-acceptance-hj869o`;dev server + `objectstack dev -p 4001`,SQLite 库 `.objectstack/data`(全新种子);登录 + `admin@objectos.ai / admin123` +- **原则**:**只测不修** —— 发现问题建 issue(bug 标签;平台侧加 `[17.0-rc][疑似平台]`), + 不改 `src/` 代码;与既有 issue 台账去重后再立新 +- **数据隔离**:所有测试写入的记录名/主题统一加前缀 **`RC1ACC-`**(按执行者再加分段号,如 + `RC1ACC-P3-`),便于并行会话隔离与事后清理;不修改、不删除种子记录(删除测试只删自建记录) +- **证据规范**(沿用 rc.0 报告):每项判定必须附 **请求状态码 / 服务端·控制台输出 / 截图** + 之一;不接受"看起来正常"。判定值:**通过 / 失败 / 阻塞**(阻塞=被其它缺陷挡住无法执行) + +## 与 rc.0 底本的差异(2026-08-05 修订) + +| 变化 | 说明 | +|---|---| +| AI agents 已裁撤(#512) | 旧计划 §7「AI(2 agents/6 skills)」删除;skills 仍在,仅验证元数据加载 | +| competitor 模块已移除(#551) | 对象清单删去 `crm_competitor` | +| 活动模型上线(#592/#670) | 新增 `crm_event`、`crm_event_attendee` 对象、activity 仪表盘、14 个活动类 action | +| rc.0 发现的 #527/#529/#530/#531/#535 已修复关闭 | 不再复测;#520 系列见 §8 | +| 上游 #3912/#3913/#3914/#3915/#3955 已随 rc.1/rc.2 发布 | §8 阻塞项预期部分解除,逐条实证 | +| 元数据普查(`pnpm validate`,rc.2) | **17 对象/344 字段/1 app/14 视图/8 页面/5 仪表盘/10 报表/26 actions/24 flows/12 岗位/6 权限集** | + +## 0. 静态与自动化基线(先行,必须全绿) + +| 项 | 内容 | 结果判据 | +|---|---|---| +| S1 | `pnpm verify`(validate+typecheck+lint+hygiene+build+vitest) | 退出码 0(vitest 52 文件 / 1281 例) | +| S2 | `pnpm test:e2e`(Playwright headless,同 CI e2e.yml) | 全过 | +| S3 | dev server 冷启动 + 种子加载 | 无 ERROR,登录可用 | + +## 1. 对象:列表 + 详情(14 个带视图对象;只读,可并行) + +每对象:① 列表视图渲染且有种子数据 ② 打开一条详情不报错 ③ 控制台无新增报错。 + +`crm_account` `crm_contact` `crm_lead` `crm_opportunity` `crm_product` `crm_quote` +`crm_contract` `crm_case` `crm_campaign` `crm_forecast` `crm_knowledge_article` +`crm_task` `crm_event` `crm_event_attendee` + +3 个无独立视图的子对象经父记录相关列表验证:`crm_opportunity_line_item`(商机详情)、 +`crm_quote_line_item`(报价详情)、`crm_campaign_member`(市场活动详情)。 + +## 2. 对象:新建 / 编辑 / 删除(17 个对象全量;写,串行) + +每对象:新建(必填校验+保存)→ 编辑(字段回显+保存)→ 删除自建记录。重点: + +| 项 | 重点 | +|---|---| +| C1 商机 | stage→probability/expected_revenue 派生 hook;closed_won 盖章 close_date | +| C2 工单 | **多 tab 表单**(Case/SLA/Resolution)——rc.0 的 #525 场景,校验失败后切 tab 值不丢 | +| C3 线索 | 新建 + convert_lead screen flow 全程(industry 越界映射已修 #626 的回归) | +| C4 合同 | 新建 + 状态看板拖拽(rc.0 未验证项 O1,人工/Playwright 拖一次) | +| C5 行项目 | 商机/报价行项目经相关列表增删(controlled_by_parent 写权限,#547 修复回归) | +| C6 事件 | crm_event 新建 + 与会人(event_attendee)联动(#592 新功能首测) | + +## 3. Actions(26 个全量;写,串行) + +12 个业务 action + 14 个活动类 action(`log_call`/`log_meeting`/`schedule_meeting` × +lead/contact/account/opportunity/case 表面 + 全局)。每个:入口可见 → 参数弹窗 → 执行 → +数据联动正确 → 失败时有可见报错(rc.0 P3 静默吞错的回归点)。 + +| 组 | Action | +|---|---| +| 工单 | `escalate_case`、`close_case`(#521 FORBIDDEN 场景回归) | +| 线索 | `convert_lead`(screen flow)、`schedule_followup`、`create_campaign` | +| 商机 | `clone_opportunity`、`mass_update_stage`(列表批量,#508 回归)、`generate_quote` | +| 联系人 | `send_email`、`mark_primary` | +| 市场活动 | `enroll_leads` | +| 活动类 | 各对象 `log_call`/`log_meeting`/`schedule_meeting` 至少每 action 名一测,全局入口单测(#522/#509 回归);产物落 `sys_activity`/`crm_event` 并出现在时间线 | + +## 4. 仪表盘(5)与报表(10)(只读,可并行) + +- 仪表盘:`crm_overview` / `executive` / `sales` / `service` / `sales_activity`(新)。 + 每个:渲染、KPI 出数、图表出数;全局过滤器(Owner、日期范围)改变后数字变化; + `service` 仪表盘为 #520 datetime 过滤的判定现场 +- 报表:10 个全量打开、图表按度量出数;矩阵日期报表(`lead_inflow_by_month_source`、 + `pipeline_coverage_by_quarter`、`cases_opened_by_day_priority`)为 #523 粒度问题判定现场 + +## 5. Flows(24 个全量;写,串行) + +每条构造触发条件并留证(记录前缀 `RC1ACC-`)。按触发方式分组: + +- **record-change(即时可测)**:`lead_assignment`、`lead_conversion`(screen)、 + `contact_welcome`、`opportunity_won_alert`、`opportunity_stagnation`(条件版)、 + `opportunity_approval`/`opportunity_approval_on_create`(审批,岗位需有人:#652 已配)、 + `case_escalation`/`case_escalation_on_create`、`case_csat_followup`、 + `campaign_enrollment`、`campaign_completion`、`quote_generation`、`escalate_case`、 + `close_case`、`schedule_followup`、`task_urgent_alert` +- **scheduled(构造到期数据 + 触发一次调度或断言注册)**:`case_sla_monitor`、 + `contract_expiration`、`contract_renewal`、`quote_expiration`、`task_due_reminder`、 + `forecast_snapshot`、`opportunity_stagnation`(定时分支) +- **bootstrap**:`demo_bootstrap`(冷启动日志断言,#663 修复回归) + +判据:目标写入发生(任务/事件/字段变更/通知),或调度注册可见;失败必须有服务端日志佐证。 + +## 6. 【新增】Profile 权限矩阵实测(写,串行,置于 CRUD/Action 之后) + +为 5 个 profile 各建一个测试用户(用户名前缀 `RC1ACC-`):`sales_rep` / `sales_manager` / +`service_agent` / `marketing_user` / `system_admin`,逐一登录(或带 token 调 REST)实测: + +1. **对象级 CRUD**:每用户 × 17 对象,读/建/改/删与 6 个权限集声明(`src/profiles/`)一致 +2. **readScope / OWD**:own-only(如 rep 的 forecast)、org-wide、`controlled_by_parent` + 三类各取代表对象验证行集 +3. **sharing 规则**:territory/team/岗位共享(`src/sharing/`)——#549 已知缺口对照, + private 商机(`is_private`)对 org-wide 读者隐藏(#547 回归) +4. **字段级权限**:`crm_account.health_score`、`crm_case.internal_notes`、 + `crm_quote.internal_notes`、`crm_opportunity.amount` 的掩码/只读按 #547 声明生效 +5. **owner 重指派**:改 Owner 后访问是否随动 —— #548(p0,已立案)判定现场,结果记录到 + 该 issue,不重复立案 + +## 7. i18n 四语言抽查(只读,可并行) + +`en` / `zh-CN` / `es-ES` / `ja-JP` 各:主导航 + 核心对象列表/表单 + **action 标签** + +**校验错误文案**(重点,rc.0 的 P6 附带项)。已知台账:#494(--skip-i18n 隐藏 205 警告)、 +#645(34 个选项未译)、#661(列表 tab 标签无翻译面)——只验证是否恶化,不重复立案。 + +## 8. 【关键】平台阻塞 issue 复测(在 rc.2 逐条重跑复现步骤) + +上游 #3912/#3913/#3914/#3915/#3955 的修复已随 rc.1/rc.2 到达;每条按原 issue 复现步骤重跑, +在 issue 评论:**已解除(附证据,可关闭)** 或 **仍复现(注明 rc.2 + 新证据)**。 + +| issue | 内容 | 上游修复 | 预期 | +|---|---|---|---| +| #520 | datetime 时间窗过滤空集(客服仪表盘) | #3912(closed) | 解除 | +| #521 | script action 写库 FORBIDDEN | #3914(closed) | 解除 | +| #522 | 全局 action 无法派发 + 200 信封吞错 | #3913(closed) | 解除 | +| #524 | flow 型 action 死菜单 + screen flow 失败静默 | #3915(closed)+ objectui#2958/#2960 | 部分解除 | +| #525 | 多 tab 表单丢值(objectui#2959) | 待验 | 待验 | +| #526 | migrate 占用检测(#3917) | 待验 | 待验 | +| #528 | 9 个孤儿 `__search` 列(#3955 closed) | #3955 | 解除/可清理 | +| #508 | 批量 action 全路径不可执行(16.1 console) | 17.0 console | 待验 | +| #509 | 全局 body action 注册键 'global' vs '*' | #3913 | 解除 | +| #510 | 仪表盘查询无用户 token,"my" widget 不可能 | 待验 | 待验 | + +## 9. 产出 + +1. `docs/acceptance-17/test-report.md`:结论 / 结果矩阵 / 平台问题 / CRM 遗留 / 诚实记录 + (结构沿用 rc.0 报告),提交 PR(附空 frontmatter changeset;Refs 不用 Closes) +2. 新发现逐条建 issue,与 §7/§8 台账及 #664/#661/#656/#650/#620/#617 等既有 issue 去重 +3. **GA 发布时需重跑的最小回归清单**(报告附录):从本轮失败/解除项中提炼 From 6fb8b3aad3832668f789c3c378095c506a613c3d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 02:31:44 +0000 Subject: [PATCH 02/11] docs(acceptance): record the read-only acceptance results for rc.2 Baseline (pnpm verify, Playwright e2e 16/16, cold boot) plus the three read-only sweeps: 15/17 objects clean with zero application console errors, 5/5 dashboards and 10/10 reports resolving real data, and the four-locale i18n matrix. Confirms two rc.0-era blockers are lifted on rc.2 (#520 datetime window filters, #523 matrix report date granularity) and records the honest account of one hypothesis the probes disproved. Write-phase sections are marked as being filled in. Refs #520 #523 #494 #645 #661 #671 #680 #681 #682 #683 #684 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 156 ++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/acceptance-17/test-report.md diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md new file mode 100644 index 00000000..f10a2afa --- /dev/null +++ b/docs/acceptance-17/test-report.md @@ -0,0 +1,156 @@ +# HotCRM × ObjectStack 17.0 GA 验收报告(rc.2) + +- **日期**:2026-08-05 +- **被测版本**:`@objectstack/*` **17.0.0-rc.2**(`engines.protocol ^17.0.0-rc.2`)。 + 任务下达时的目标是 rc.1,但 `main` 已于 2026-08-03 由 #663 前进到 rc.2,**本轮以 rc.2 为准**, + 所有判定均注明 rc.2。rc.1 相关的上游修复(#3912/#3913/#3914/#3915/#3955)全部包含在内。 +- **被测代码**:`main` @ 5a78f88(`feat(activity): first-class events…` #670) +- **环境**:远程沙箱,`objectstack dev -p 4001`,SQLite `.objectstack/data`(全新种子); + 登录 `admin@objectos.ai`,鉴权走 `POST /api/v1/auth/sign-in/email` → Bearer token +- **测试计划**:[test-plan.md](./test-plan.md) +- **纪律**:只测不修 —— 未改动任何 `src/` 代码;新发现逐条立 issue;测试数据统一 `RC1ACC-` 前缀 + +## 结论(TL;DR) + +> 本节在全部阶段完成后定稿。 + +## 1. 自动化基线 + +| 项 | 命令 | 结果 | 证据 | +|---|---|---|---| +| S1 | `pnpm verify`(validate+typecheck+lint+hygiene+build+vitest) | ✅ 通过 | 退出码 0;vitest **52 文件 / 1280 通过 / 1 跳过**,耗时 26.24s | +| S2 | `pnpm test:e2e`(Playwright headless,同 CI `e2e.yml`) | ✅ 通过 | **16/16 通过**,耗时 1.0m,含 `pnpm build` + `objectstack start` 冷启动 | +| S3 | dev server 冷启动 + 种子 + 登录 | ✅ 通过 | `/api/v1/health` 200;启动日志 **0 条 ERROR**;登录返回 token,`/api/v1/data/crm_account` 200 | + +`pnpm validate` 元数据普查(rc.2):**17 对象 / 344 字段 / 1 应用 / 14 视图 / 8 页面 / +5 仪表盘 / 10 报表 / 26 actions / 24 flows / 12 岗位 / 6 权限集**。 + +validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风险 ×4、`crm_campaign_member` +的 basic 分组全被提升到高亮条),非本轮新增。 + +## 2. 结果矩阵 + +| 计划项 | 结果 | 说明 | +|---|---|---| +| 0. 自动化基线 | ✅ 通过 | 见 §1,全绿 | +| 1. 14 对象列表 + 详情 | ✅ 12 通过 / 2 阻塞 | 见 §3 | +| 1. 3 子对象(经父相关列表) | ✅ 通过 | 见 §3 | +| 2. 17 对象 CRUD | *(填充中)* | | +| 3. 26 actions | *(填充中)* | | +| 4. 5 仪表盘 | ✅ 通过 | 56 widget 全渲染出数,见 §4 | +| 4. 10 报表 | ✅ 通过 | 见 §4 | +| 5. 24 flows | *(填充中)* | | +| 6. Profile 权限矩阵 | *(填充中)* | | +| 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | +| 8. 阻塞 issue 复测 | *(填充中)* | 已确认解除:#520、#523,见 §6 | + +## 3. 对象:列表与详情(执行者 R1) + +**15/17 通过,2 阻塞;全站零应用层控制台错误、零 pageerror。** + +14 个带视图对象中 12 个完全通过:列表渲染有数据、详情页字段分组/相关列表/时间线正常。 +逐对象 REST 行数与截图见执行记录。 + +**阻塞 2 项**:`crm_event`、`crm_event_attendee` —— REST 200 但种子 **0 行**,列表空状态 +渲染正确,详情页无记录可开故无法测试。对应既有 issue **#671**(活动模型缺演示种子, +#592 的后续),不重复立案;详情页由写阶段自建记录后补测。 + +3 个子对象经父记录相关列表验证通过:`crm_opportunity_line_item`(74 行,商机详情 +Products 列表)、`crm_quote_line_item`(16 行)、`crm_campaign_member`(51 行,分页 1/3)。 + +**环境噪声(非应用缺陷,已排除)**:Sentry 遥测被沙箱代理拦截(`ERR_TUNNEL_CONNECTION_FAILED`)。 + +**新发现(低)**: +- `GET /assets/crm-favicon.ico` → 404,应用品牌声明的 favicon 未被服务(打包疏漏,无功能影响) +- 商机详情右侧 Products 汇总侧栏显示原始记录 ID(如 `-PBylY-NARjgK6dS`)而非显示名 + +## 4. 仪表盘与报表(执行者 R2) + +**5/5 仪表盘(共 56 widget)、10/10 报表全部通过。** 所有 widget 经 +`POST /api/v1/analytics/dataset/query` 取数,全部 200,无应用控制台错误。 + +**关键:所有为空的 widget 均经 REST 反查证实"数据本就如此",不是渲染或查询缺陷** —— +`sales_activity` 的事件类 widget 为空是因 `crm_event` 0 行(#671);SLA 达标率 0.0% 是因 +3 条违约工单均未关闭而该仪表盘按 `is_closed:true` 取数,逻辑自洽。 + +**全局过滤器实测通过**(2 个仪表盘,前后截图):CRM 总览日期范围 This quarter→This year, +营收 610,000→1,290,000、成交数 7→10、赢单 4→8;客服仪表盘 Priority→Critical, +未结工单 30→7。Owner 过滤器经捕获的查询载荷证明已注入(种子数据仅单一 owner, +故数字不变属预期)。 + +**报表**:10 个全部渲染、度量解析正确、坐标轴有数。`customer_churn_signals` 的两个空区块 +经反查证实为数据正确(所有客户 21 天内均有活动)。 + +**低优先观察**:`forecast_metrics` 的 owner 标签显示 "—"(种子 `crm_forecast.owner` 为 null); +一个报表图表的坐标轴显示原始度量名 `total_amount`。 + +## 5. i18n 四语言(执行者 R3) + +**zh-CN 完整,ja-JP / es-ES 有实质缺口,校验文案四语言均正确翻译。** + +locale 切换路径(实测发现,非猜测):头像菜单 → Preferences → Language;控制台用短码, +服务端映射(`GET /api/v1/i18n/translations/zh` → `{"locale":"zh-CN"}`); +`GET /api/v1/i18n/locales` 返回 en/zh-CN/ja-JP/es-ES,与 `src/translations/crm.translation.ts` 一致。 + +| 检查项 | zh-CN | ja-JP | es-ES | +|---|---|---|---| +| 主导航 | ✅ 全译 | ❌ 23 个条目全英文 | ❌ 同左 | +| 列表视图 tab 标签 | ✅ | ✅ | ✅ | +| 列表列头 | ✅ | ⚠ 缺 Health Score / Next Renewal Date | ⚠ 同左 | +| 详情页 tab / 分组标题 | ✅ | ❌ 英文 | ❌ 英文 | +| Action 按钮标签 | ✅ | ✅ | ✅ | +| **校验错误文案** | ✅ 主题不能为空 | ✅ 件名は必須です | ✅ Asunto es obligatorio | +| Action 参数弹窗 | ⚠ 标题英文 | ⚠ 标题英文 | ⚠ 标题英文 | + +**量化**:`objectstack lint` 关闭 `--skip-i18n` 后 **710 条 i18n 警告**(ja-JP 355 + es-ES 355, +zh-CN 0),远超 #494 记录的 205 条。缺失选项键 **169 个/语言**,远超 #645 记录的 34 个。 +根因:`ja-JP.ts` / `es-ES.ts` 按 `en.ts` 的形状编写(~926 行),从未同步 `zh-CN.ts`(1345 行) +后来新增的导航映射、`pages`、11 个 `_sections` 中的 10 个、CS 字段与工单状态/优先级选项集。 + +**既有 issue 复核结果**: +- **#661 未复现,疑似已修** —— 列表视图 tab 标签在 zh/ja/es 三语言下均已翻译(已在 issue 评论,建议 owner 确认后关闭) +- **#645 确认仍在**(ja/es),计数已更新至 169/语言(已评论) +- **#494 确认仍在**,计数已更新至 710(已评论),并发现 lint **完全没有 `_sections` 检查**这一盲区 + +**本轮新立 issue**:#680(action 参数弹窗标题不翻译)、#681(语言选择不持久化,刷新即回退 +英文,使非英语 locale 实际只能演示)、#682(控制台 chrome 层 i18n 缺陷:lookup 占位符泄露 +原始 API 名 `crm_account`、视图/工具栏字符串、en 文案误用顿号、es 性数不一致)、 +#683(创建表单分节 tab 无翻译面 + lint `_sections` 盲区)。 + +## 6. 平台阻塞 issue 复测 + +> 本节在复测阶段完成后定稿。已取得的确认: + +- **#520(datetime 时间窗过滤返回空集)—— 已解除。** rc.0 下客服仪表盘全空;rc.2 下 + 38 条种子工单全部出数:未结 30 / 紧急 7 / 平均解决 45.0h / SLA 违约 3,30 天日粒度 + 时间序列有数,与 REST 直查结果一致。上游 objectstack#3912 已关闭。 +- **#523(矩阵报表日期粒度缺失)—— 已解除。** `lead_inflow_by_month_source` 正确按月分桶 + (2026-02…2026-08),`pipeline_coverage_by_quarter` 按季(2026-Q3/Q4), + `cases_opened_by_day_priority` 按日(2026-07-06…2026-08-04),均按时间正序且同期记录 + 聚合入同一列,rc.0 的"逐原始日期爆列"现象消失。 + +## 7. CRM 侧遗留与新发现 + +> 本节在全部阶段完成后定稿。已立案: + +- **#684**(本轮新立,p1)记录变更流漏了 `runAs: 'system'`:10/21 个流文件已为**定时流** + 加上该声明并附相同注释,但**记录变更流被系统写入触发时同样没有用户**这一情形被漏掉。 + 单次启动+种子+一轮 CRUD 写入即产生 **73 条 `[runAs]` 拒绝警告与 12 次运行失败** + (`case_escalation` 38/3、`opportunity_approval` 23/9)。影响:种子加载、流触发流、 + 未来的集成/webhook 写入都会静默丢失这些自动化。 + +## 8. 诚实记录(测试过程说明) + +- **一次被证伪的假设**:dev 日志出现 `SqliteError: … table crm_forecast has no column named + name`(REST 未捕获错误),我最初推断为"未声明字段被直送 SQL 层"。随后用对照探针证伪: + `POST /api/v1/data/crm_account` 带虚构字段返回干净的 **400 `INVALID_FIELD`**, + `POST /api/v1/data/crm_forecast` 带 `name` 同样返回 **400**(该对象用 + `nameField: 'display_title'`,不声明 `name`)。即**单条 REST 插入路径的校验是正确的**, + 该 SqliteError 走的是另一条路径。归属与复现留给写阶段的精确请求记录,未据此立案。 +- 上述探针创建的 2 条记录(1 客户、1 预测)已 `DELETE` 并经 GET 404 验证清除。 +- `display_title` 是公式字段,写入时被拒为 `Unknown field` —— 报错措辞对只读计算字段有误导性 + (低优先观察项,未单独立案)。 + +## 附录 A:GA 发布时需重跑的最小回归清单 + +> 本节在全部阶段完成后定稿。 From 41c212796c44b58db0bcc75cf12255b56a057298 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 02:34:55 +0000 Subject: [PATCH 03/11] docs(acceptance): add the parallel task-package split for the write phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The write phases run serially in the orchestrating session; this documents the alternative delivery the maintainer asked for — five packages that can each be driven from a separate chat, with per-package record prefixes, a shared-server do-not-do list, and the physical-isolation fallback (own port + own DB file). Carries the environment facts this run had to discover: the auth endpoint is /api/v1/auth/sign-in/email (the obvious /auth/login returns 500), and the three entrypoints default to three different SQLite files, so a migrate plan aimed at the dev server's database needs an explicit --database-url. Refs #507 #508 #509 #510 #515 #520 #521 #522 #523 #524 #525 #526 #528 #548 #549 #671 #684 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/parallel-task-packages.md | 115 +++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/acceptance-17/parallel-task-packages.md diff --git a/docs/acceptance-17/parallel-task-packages.md b/docs/acceptance-17/parallel-task-packages.md new file mode 100644 index 00000000..dca12cfa --- /dev/null +++ b/docs/acceptance-17/parallel-task-packages.md @@ -0,0 +1,115 @@ +# rc.2 验收:可并行执行的任务包 + +本轮验收由单一会话串行编排(写数据的阶段互斥执行)。本文件是**备选投放方式**: +每个任务包自带数据隔离约定,可分别开一个 chat 并行跑,互不干扰。 + +## 通用前置(每个包都要做) + +```bash +cd /home/user/hotcrm +# dev server 若未运行: +pnpm dev # 端口 4001;等 /api/v1/health 返回 200(约 30s) + +# 鉴权(注意:不是 /auth/login,那个路径会返回 500) +TOKEN=$(curl -s -X POST http://localhost:4001/api/v1/auth/sign-in/email \ + -H 'Content-Type: application/json' \ + -d '{"email":"admin@objectos.ai","password":"admin123"}' | node -pe 'JSON.parse(require("fs").readFileSync(0)).token') +# 之后所有请求带:-H "Authorization: Bearer $TOKEN" +``` + +浏览器测试:Playwright,`executablePath: '/opt/pw-browsers/chromium'`;用 +`context.request.post` 打同一鉴权端点,让 cookie 落到浏览器上下文。 + +## 数据隔离总则 + +| 约定 | 说明 | +|---|---| +| **记录前缀** | 每个包用**自己的前缀**(下表 P1–P5),写进记录的名称/主题/标题字段 | +| **禁止改种子** | 不修改、不删除任何种子记录;只删自己建的 | +| **台账** | 每包维护 `LEDGER.md`(对象 + id + 名称),结束时按台账清理并记录 DELETE 状态码 | +| **共享 dev server** | 所有包共用 4001 的同一个库。**不要重启 server、不要 `demo:reset`、不要 `os migrate apply`** —— 会毁掉其它包的在途数据 | +| **只读包例外** | P5 只读,可与任意包同时跑 | + +> 若要真正物理隔离,改用各自的库与端口: +> `pnpm exec objectstack dev -p 40NN -d file:/tmp/rc2-PN/data.db`(首启会自动灌种子)。 +> 代价是每包一次冷启动(约 30s)+ 各自的种子基线数字不同。 + +--- + +## P1 — 对象 CRUD 全量(17 对象) + +- **前缀**:`RC1ACC-P1-` +- **写入**:是 +- **范围**:17 个对象各跑 Create→Read→Update→Delete;REST 为主证据,6 个核心对象 + (account/opportunity/case/lead/contract/event)加测 UI 表单 +- **重点**: + - 商机 hook:stage=proposal + amount=10000 → probability/expected_revenue 派生; + 改 closed_won **必须带 `win_reason`**(否则 400,e2e 已固化该规则) + - 工单多 tab 表单(#525 现场):记录当前 tab 结构与必填字段所在 tab;若因 #515 已把 + `description` 移到第一个 tab 而**无法构造**跨 tab 校验失败,如实说明并给出当前布局证据 + - `crm_event` / `crm_event_attendee` 种子为 0 行(#671),需自建记录才能测详情页 + - 合同看板拖拽:必须用 `mouse.move/down/up` 真实鼠标事件(合成事件 dnd 库不吃); + 若确实无法自动化,写"不可自动化验证",不得判通过或失败 + +## P2 — Actions 全量(26 个)+ 相关阻塞项复测 + +- **前缀**:`RC1ACC-P2-` +- **写入**:是 +- **范围**:12 个业务 action + 14 个活动类 action(log_call/log_meeting/schedule_meeting × + lead/contact/account/opportunity/case + 全局入口) +- **同时复测**:#521(script action 写库 FORBIDDEN)、#522/#509(全局 action 派发 + + 200 信封吞错)、#508(批量 action 三条调用路径)、#524(flow 型 action) +- **判据**:入口可见 → 参数弹窗 → 执行 → 数据联动正确 → **失败时 UI 必须有可见报错** + (rc.0 的静默吞错是重点回归点:HTTP 200 但内层 `{"success":false}` 且弹窗直接关闭) + +## P3 — Flows 全量(24 条) + +- **前缀**:`RC1ACC-P3-` +- **写入**:是 +- **范围**:按触发方式分三组(record-change 16 条 / scheduled 7 条 / bootstrap 1 条), + 逐条构造触发条件并留服务端日志证据 +- **重点**:已立案的 **#684** —— 7 条记录变更流未声明 `runAs: 'system'`,被系统写入触发时 + 数据操作被拒。**必须验证用户会话驱动的写入路径是否正常**,这决定 #684 的严重度; + 结论请回填到 #684 的评论 +- **注意**:`case_escalation` 有自触发重入警告(与已关闭的 #507 同形),留意是否复发 + +## P4 — Profile 权限矩阵(5 profiles) + +- **前缀**:`RC1ACC-P4-`(含测试用户名) +- **写入**:是(建用户 + 建记录) +- **范围**:sales_rep / sales_manager / service_agent / marketing_user / system_admin + 各建一个用户,实测对象级 CRUD、readScope/OWD、sharing 规则、字段级权限 +- **建用户方式**:`POST /api/v1/auth/admin/create-user` + `sys_user_position` 绑定 + (profile 绑定即 position 绑定,权限集名 = position 名),之后**必须触发 sharing 规则重算**; + `system_admin` 无对应 position,断言请用 dev-admin 种子账号 +- **已知缺口(不要重复立案)**:#548(owner 重指派不移动访问权)、#549(territory/team 共享 + 的客户,其报价/任务/合同相关列表 rep 读不到 —— 这是**现状即预期**,per #549 台账) +- **易错点**:17.0 的 `allowExport` 反转 —— 即便 system_admin 对 crm_quote/campaign/task + 也会得到 `EXPORT_NOT_PERMITTED`;唯一被掩码的字段是 `crm_case.internal_notes`(对 sales_rep) + +## P5 — 只读复核(可与任何包并行) + +- **前缀**:无(不写数据) +- **范围**:#510(分析查询路径解析不到用户 token,"我的…" widget 不可能)、 + #528(孤儿 `__search` 列)、#520/#523 的二次确认 +- **已知**:全新 rc.2 库上 PRAGMA 扫描得到 **5 个 `__search` 列** + (crm_event / crm_opportunity / crm_opportunity_line_item / crm_quote_line_item / crm_task), + 且都是 rc.2 运行时自建、非旧版遗留 +- **注意**:`os migrate plan` 默认打到 `standalone.db`,**不是** dev server 用的 `dev.db`; + 要复测 #528 必须 `--database-url file:/home/user/hotcrm/.objectstack/data/dev.db`。 + **只跑 `plan`,绝不跑 `apply`**(#526 未验;且会破坏其它包的数据) + +--- + +## 证据规范(所有包一致) + +每项判定必须附以下之一,不接受"看起来正常": + +1. **HTTP 状态码 + 响应体**(REST 路径的首选证据) +2. **服务端日志行**(`/tmp/.../scratchpad/devserver.log`,按时间戳定位) +3. **截图**(UI 路径必须有) + +判定值:**通过 / 失败 / 阻塞**(阻塞 = 被其它缺陷挡住,无法执行到判定点)。 + +宣告"缺失/损坏/不可达"这类严重结论前,必须用截图或服务端权威响应复核 —— +导航后立即做 DOM 查询会因 React 未水合返回空结果,rc.0 那轮就因此产生过误判。 From bda07b27a6023ee2c1024d72b805c21f461bc933 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 03:12:47 +0000 Subject: [PATCH 04/11] docs(acceptance): record the object CRUD write phase and the six defects it found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REST-layer CRUD passes on all 17 objects with zero leftovers; the console New form for crm_lead is unusable because the renderer ignores visibleOn while enforcing required (#688). Also files the SQL-layer validation path (#689), the null nameField formula (#690), the 403-for-a-missing-parent shape (#691), the lookup redisplay (#692) and the mis-addressed cascade delete errors (#693). Two rc.0-era records are overturned and stated as such: there is no contract status board on main, and kanban drag IS automatable with the real mouse API. #525 does not reproduce — cross-tab values survive a failed submit. The honest-record section now carries the full arc of one hypothesis that was disproved and then reinstated on further evidence, since stopping at either intermediate step would have published a wrong conclusion. Refs #515 #520 #523 #525 #547 #671 #684 #688 #689 #690 #691 #692 #693 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 75 +++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index f10a2afa..75ad79ea 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -35,14 +35,14 @@ validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风 | 0. 自动化基线 | ✅ 通过 | 见 §1,全绿 | | 1. 14 对象列表 + 详情 | ✅ 12 通过 / 2 阻塞 | 见 §3 | | 1. 3 子对象(经父相关列表) | ✅ 通过 | 见 §3 | -| 2. 17 对象 CRUD | *(填充中)* | | +| 2. 17 对象 CRUD | ✅ REST 17/17 通过 / ❌ UI 1 项失败 | 见 §6;`crm_lead` UI 新建完全不可用(#688) | | 3. 26 actions | *(填充中)* | | | 4. 5 仪表盘 | ✅ 通过 | 56 widget 全渲染出数,见 §4 | | 4. 10 报表 | ✅ 通过 | 见 §4 | | 5. 24 flows | *(填充中)* | | | 6. Profile 权限矩阵 | *(填充中)* | | | 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | -| 8. 阻塞 issue 复测 | *(填充中)* | 已确认解除:#520、#523,见 §6 | +| 8. 阻塞 issue 复测 | *(填充中)* | 已确认解除:#520、#523,见 §7 | ## 3. 对象:列表与详情(执行者 R1) @@ -117,7 +117,42 @@ zh-CN 0),远超 #494 记录的 205 条。缺失选项键 **169 个/语言**,远 原始 API 名 `crm_account`、视图/工具栏字符串、en 文案误用顿号、es 性数不一致)、 #683(创建表单分节 tab 无翻译面 + lint `_sections` 盲区)。 -## 6. 平台阻塞 issue 复测 +## 6. 对象 CRUD 与定向检查(执行者 W1) + +**REST 层 17/17 对象全量 C-R-U-D 通过**,无一对象在增删改查任一环节失败。 +测试数据全部 `RC1ACC-W1-` 前缀,结束时 30 条台账记录 + 3 条 hook 派生任务全部删除, +**零残留、未触碰任何种子记录**。 + +UI 表单已验证可用:客户、商机、工单、事件(均创建成功);**线索 UI 新建失败**(#688)。 + +| 定向检查 | 结果 | 关键证据 | +|---|---|---| +| C1 商机 hook 派生 | ✅ 通过 | `proposal/10000` → probability=60、expected_revenue=6000;无 `win_reason` 关闭被 **400** 拒绝;补齐后 → probability=100、expected_revenue=10000、close_date 盖章 | +| C2 多 tab 表单(#525) | ✅ 通过 | **无数据丢失**,详见下文 | +| C3 线索转化(industry 映射) | ✅ 通过 | 两轮转化,`nonprofit` 与 `media` 均干净落到 `crm_account.industry`,rc.0 的枚举越界未复发 | +| C4 看板拖拽 | ✅ 通过 | 真实鼠标 API 触发真实 PATCH,hook 正确重算 | +| C5 行项目与 rollup(#547) | ✅ 通过 | 报价/商机 rollup 均正确;`controlled_by_parent` 写权限无阻碍 | +| C6 事件/与会人 | ✅ 通过 | 两个详情页渲染完好,R1 的阻塞纯属无种子数据(#671) | + +**C2 —— #525 的结论是"不复现"**:`crm_case` 的原始复现路径已不可构造(#515 把 5 个必填 +字段全移到第一个 tab)。W1 转而发现 `crm_contract` 仍存在真正的跨 tab 必填布局 +(Parties: 客户/主要联系人/状态;Terms: 期限/起止日期/合同额),在那里跑了真复现: +**其它 tab 上已填的值在校验失败后全部存活**(哨兵字符串完好;lookup 值也存活—— +第二次提交只报 `Status is required`,不再重报客户/联系人),失败提交零网络写入,tab 头未错乱。 +残留问题是 lookup 控件重挂载后显示回 "Select..." 但值仍在(#692)。 + +**C4 的两点修正**(推翻 rc.0 的记录):① `crm_contract` **根本没有状态看板** —— +只有 Grid/Calendar/Gantt/Timeline,该前提在当前 main 不存在;② 看板拖拽**可以自动化验证**, +在 `pipeline_kanban` 上用真实鼠标 API(mouse.down + 30 步插值 move + up)触发了真实 +`PATCH` 200,读回 `stage=needs_analysis / probability=40 / expected_revenue=10000`。 +rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「不可自动化」结论应予撤回。 + +**新发现(已立案)**:#688(p0,`visibleOn` 被无视致 `crm_lead` UI 完全无法创建)、 +#689(未声明字段由 SQL 失败兜底,hook 已先执行,完整 INSERT 连值落日志)、 +#690(`crm_forecast.display_title` 全行为 null)、#691(缺失 master-detail 父记录返回 403 而非 400)、 +#692(lookup 显示回退)、#693(级联删除报错指向错误对象)。 + +## 7. 平台阻塞 issue 复测 > 本节在复测阶段完成后定稿。已取得的确认: @@ -129,7 +164,7 @@ zh-CN 0),远超 #494 记录的 205 条。缺失选项键 **169 个/语言**,远 `cases_opened_by_day_priority` 按日(2026-07-06…2026-08-04),均按时间正序且同期记录 聚合入同一列,rc.0 的"逐原始日期爆列"现象消失。 -## 7. CRM 侧遗留与新发现 +## 8. CRM 侧遗留与新发现 > 本节在全部阶段完成后定稿。已立案: @@ -139,17 +174,31 @@ zh-CN 0),远超 #494 记录的 205 条。缺失选项键 **169 个/语言**,远 (`case_escalation` 38/3、`opportunity_approval` 23/9)。影响:种子加载、流触发流、 未来的集成/webhook 写入都会静默丢失这些自动化。 -## 8. 诚实记录(测试过程说明) - -- **一次被证伪的假设**:dev 日志出现 `SqliteError: … table crm_forecast has no column named - name`(REST 未捕获错误),我最初推断为"未声明字段被直送 SQL 层"。随后用对照探针证伪: - `POST /api/v1/data/crm_account` 带虚构字段返回干净的 **400 `INVALID_FIELD`**, - `POST /api/v1/data/crm_forecast` 带 `name` 同样返回 **400**(该对象用 - `nameField: 'display_title'`,不声明 `name`)。即**单条 REST 插入路径的校验是正确的**, - 该 SqliteError 走的是另一条路径。归属与复现留给写阶段的精确请求记录,未据此立案。 -- 上述探针创建的 2 条记录(1 客户、1 预测)已 `DELETE` 并经 GET 404 验证清除。 +## 9. 诚实记录(测试过程说明) + +- **一个假设先被证伪、随后又被翻转过来 —— 完整经过如下**,因为中途任一步停手都会写出错误结论: + 1. dev 日志出现 `SqliteError: … table crm_forecast has no column named name` + 并以 `[REST] Unhandled error` 抛出,我推断"未声明字段被直送 SQL 层"。 + 2. 用对照探针**证伪**:`POST /api/v1/data/crm_account` 带虚构字段返回干净的 + **400 `INVALID_FIELD`**;`crm_forecast` 带 `name` 同样 **400**。看上去校验是正确的, + 我据此撤回了假设,并明确不立案。 + 3. 但随后核对 W1 的精确请求记录发现:那次 400 的时间戳是 02:26:22.917,SqliteError 是 + .994 —— **同一次请求的两个层次,不是两次请求**。回查我自己那次探针的日志, + 同样留下了完整的 `SqliteError` + `[REST] Unhandled error`。 + 4. **结论**:原假设方向是对的,但症状判断错了 —— 客户端拿到的 400 **是 SQL 失败被翻译后的产物**, + 而非 schema 校验的结果。佐证:失败的 INSERT 里含有调用方从未提交的 `period_label` + (由 `forecast.hook.ts:89` 派生),证明 **beforeInsert hook 在请求被判定非法之前就已执行**。 + 已按此立案 **#689**(含 hook 副作用暴露、日志噪声、字段值明文落日志三点后果)。 +- 教训:一次"干净的 400"不足以证明校验发生在正确的层次;必须同时看服务端日志。 +- 上述探针创建的 2 条记录(1 客户、1 预测)已 `DELETE` 并经 GET 404 验证清除; + W1 的 30 条台账记录 + 3 条 hook 派生任务同样已清理,全库零残留。 +- **未予断言的一点**:被拒绝的那次 INSERT 中自动编号取值为 `ACC-000003`,而该编号已被种子 + 客户占用。这暗示自动编号是插入时计算而非预留,但我没有隔离验证,因此**没有**把它写成缺陷, + 只在 #689 里留了一句提示。 - `display_title` 是公式字段,写入时被拒为 `Unknown field` —— 报错措辞对只读计算字段有误导性 (低优先观察项,未单独立案)。 +- **rc.0 报告的两处记录经本轮推翻**,已在 §6 注明:合同状态看板在当前 main 并不存在; + 看板拖拽**可以**自动化验证(rc.0 结论「不可自动化」应撤回)。 ## 附录 A:GA 发布时需重跑的最小回归清单 From 7f453c98336f321fea1471b0a57227be687c6379 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 03:37:46 +0000 Subject: [PATCH 05/11] docs(acceptance): finalize the rc.2 report as a partial run with a GA regression checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep stopped early when the session budget ran out, so the report now says so plainly: five of eight planned sections executed, three (full actions, full flows, profile permission matrix) did not, and the six remaining blocker retests are listed as unverified rather than silently absent. Adds the GA minimal-regression checklist the run was asked for, marking each item as either verified-this-round or still-unverified so the next session can tell what it is confirming versus testing for the first time. Records the teardown honestly, including the leftover W2 test records and the two defects that blocked their deletion — one filed as #696, the other left unfiled because it plausibly duplicates #548 and was not isolated. Refs #494 #520 #521 #522 #523 #525 #548 #645 #661 #684 #688 #696 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 106 +++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index 75ad79ea..bba5201e 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -12,7 +12,27 @@ ## 结论(TL;DR) -> 本节在全部阶段完成后定稿。 +> ⚠ **本轮为部分完成**。计划 8 节中 5 节已执行完毕,3 节(actions 全量、flows 全量、 +> profile 权限矩阵)因会话额度耗尽**未执行**。已完成部分的结论如下,未完成部分见 §10。 + +- **平台侧:rc.0 的三个升级阻断项已解除两个,第三个未验。** #520(datetime 时间窗过滤空集) + 与 #523(矩阵报表日期粒度)在 rc.2 上均取得确凿解除证据;#521/#522/#508/#509/#510 + 的复测尚未执行。 +- **基线全绿**:`pnpm verify` 退出码 0(vitest 1280 通过)、Playwright e2e 16/16、 + 冷启动 0 ERROR。 +- **数据层健康**:REST 层 **17/17 对象全量 CRUD 通过**,商机 hook 派生、行项目 rollup、 + 线索转化(含 rc.0 失败的 industry 映射)、看板拖拽均正确。 +- **UI 层有一个 p0 缺陷**:console 表单无视 `visibleOn` 却强制其 `required`, + 导致 **`crm_lead` 在界面上完全无法创建**(#688)——这是应用的头号演示路径。 +- **分析层健康**:5 仪表盘 56 个 widget、10 报表全部出数;所有空值均经 REST 反查证实 + 为数据本身如此,而非查询缺陷。 +- **i18n 分化明显**:zh-CN 完整;ja-JP / es-ES 各缺 355 条,导航与详情页分节标题仍是英文; + 四语言的校验文案均已正确翻译。 +- **本轮共立 13 个新 issue**(#680–#684、#688–#693、#696),另给 5 个既有 issue + 回填 rc.2 实测数据(#494/#525/#645/#661 及 #684 关联的 #507)。 + +**对 GA 的建议**:在 §11 的最小回归清单跑通、且 #688 修复前,不要认为 UI 层已通过验收。 +剩余三节的测试必须补齐后才能给出完整的 GA 放行判断。 ## 1. 自动化基线 @@ -200,6 +220,86 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 - **rc.0 报告的两处记录经本轮推翻**,已在 §6 注明:合同状态看板在当前 main 并不存在; 看板拖拽**可以**自动化验证(rc.0 结论「不可自动化」应撤回)。 -## 附录 A:GA 发布时需重跑的最小回归清单 +## 10. 未完成的阶段(会话额度耗尽,需另起会话补齐) -> 本节在全部阶段完成后定稿。 +本轮在 W2 阶段开始后不久因额度耗尽终止。以下三节**未执行**,不得视为通过: + +| 未执行项 | 计划位置 | 备注 | +|---|---|---| +| **26 个 actions 全量** | §3 | W2 已建 16 条记录后被终止,无有效结论 | +| **24 条 flows 全量** | §5 | 含 #684 的关键待验点:用户会话驱动的写入路径是否正常 | +| **5 profile 权限矩阵** | §6 | 权限期望矩阵已备妥(见并行任务包 P4) | +| **阻塞 issue 复测 6 条** | §8 | #521 #522 #509 #508 #524 #510 未复测 | +| **#526 / #528 CLI 复测** | §8 | 需独立端口+独立库隔离验证,刻意排在最后未及执行 | + +补齐方式见 [parallel-task-packages.md](./parallel-task-packages.md) 的 P2/P3/P4/P5 四个包, +每包自带记录前缀与隔离约定,可并行投放。 + +### 终止时的环境状态(诚实记录) + +- W2 被终止时留下了测试记录,已尽力清理:campaign member 8 条、部分线索/联系人/市场活动 + 已删除(200)。**仍有残留**:6 条 `crm_event`、2 条 `crm_case`、2 条 `crm_opportunity`、 + 2 条 `crm_contact`、5 条 `crm_lead`、1 条 `crm_account`,均带 `RC1ACC-W2-` 前缀。 + 该库是容器内的临时 SQLite(`.objectstack/data/dev.db`,已 gitignore),随容器回收, + 不影响仓库与其它环境。 +- **清理受阻本身暴露了两个问题**,其一已立案: + - **#696**(已立案):报名过市场活动的线索/联系人无法删除 —— 级联把 campaign_member + 的引用置空后违反其自身校验规则。删掉 campaign member 后线索即可删除(400 → 200), + 机制已证实。 + - **未立案的观察**:admin 对部分记录删除返回 **403 FORBIDDEN + "insufficient privileges to delete"**,尤其是活动类 action 创建的 `crm_event` + (W1 用 REST 自建的 event 删除是 200,差别在创建路径)。 + **未立案的原因**:这很可能是既有 #548(app `owner` lookup 与平台 `owner_id` 不一致) + 的症状,在未做隔离验证前立新案有重复风险。补齐 W2/W4 时应优先验证这一条, + 并把结论回填到 #548 或据此另立。 + +## 11. 附录 A:GA 发布时需重跑的最小回归清单 + +从本轮结果提炼。**打 ★ 的是本轮已验证通过、GA 时只需确认未回归的项;打 ⚠ 的是本轮未验证或 +已知失败,GA 前必须真正跑通。** + +### A. 自动化闸门(必须全绿,约 3 分钟) + +1. ★ `pnpm verify` —— 退出码 0,vitest 52 文件 / 1280 通过 +2. ★ `pnpm test:e2e` —— 16/16(含冷启动 + 种子) + +### B. 平台阻塞项回归(rc.0 的三个升级阻断项) + +3. ★ **#520** 客服仪表盘出数:未结工单、紧急数、SLA 违约数非零,30 天时间序列有数 + —— datetime 时间窗过滤的判定现场 +4. ★ **#523** 三个矩阵报表的日期列按月/季/日正确分桶且有序,无"逐原始日期爆列" +5. ⚠ **#521** 带写库 body 的 script action(如 `close_case`)执行后**写入确实落库** +6. ⚠ **#522 / #509** 全局(无 objectName)action 可派发,且失败时返回非 200、UI 有可见报错 +7. ⚠ **#508** `mass_update_stage` 三条调用路径(REST `selectedIds` / 选择栏 / 工具栏) + +### C. 核心业务链路(每条都有真实数据效果判据) + +8. ★ 商机生命周期:`proposal/10000` → probability=60、expected_revenue=6000; + 无 `win_reason` 关闭被 400 拒绝;补齐后 probability=100、close_date 盖章 +9. ★ 线索转化 screen flow 端到端:线索标记 converted,客户/联系人(按需商机)真实生成, + **industry 枚举跨对象映射不报错**(rc.0 的失败点) +10. ★ 行项目 rollup:报价/商机行项目增改后父记录金额重算(注意 rollup 是 **async**, + 勿在同一 tick 断言) +11. ⚠ 24 条 flows 全量触发 —— 尤其 **#684**:确认记录变更流在**用户会话驱动**的写入下 + 正常执行(系统驱动下已确认被拒) +12. ⚠ 5 个 profile 的对象级 CRUD、readScope、sharing 与字段级权限与声明一致 + +### D. UI 可用性(本轮发现的重灾区) + +13. ⚠ **#688(p0)** 从界面新建线索必须能成功 —— 当前完全不可用,是本清单最高优先级 +14. ★ 多 tab 表单校验失败后,其它 tab 已填值不丢(在 `crm_contract` 上测, + `crm_case` 已无跨 tab 必填布局) +15. ★ 看板拖拽触发真实 PATCH 且 hook 重算(用真实鼠标 API,合成事件无效) +16. ⚠ 删除链路:报名过市场活动的线索可删除(#696);admin 能删除 action 创建的记录 + +### E. i18n(抽查即可) + +17. ⚠ 四语言的 **action 标签与校验文案**(本轮:标签与文案均正确, + 但参数弹窗标题仍是英文 #680) +18. ⚠ `objectstack lint` 不带 `--skip-i18n` 的警告数**不高于 710**(本轮基线), + 确认债务未继续增长 + +### 执行顺序建议 + +A → B → C → D → E。A 不绿则其余不必开始;B 中任一条失败即为**升级阻断**, +应在 GA 前解决或明确接受。 From 37a6723488b81eaf3cc64ce7a7e6beced7ca64a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 03:53:42 +0000 Subject: [PATCH 06/11] docs(acceptance): add the handoff page and commit the run's text evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The evidence lived only in the session scratchpad, which dies with the container — so a colleague picking this up on their own account would have inherited a report full of references to files they cannot open. The four executor records, the blocker-retest playbook and the profile expectation matrix are now in the repo (116KB of text); the ~200 screenshots are not, and evidence/README.md says so plainly rather than leaving dead references unexplained. HANDOFF.md is the entry point: what is done, what is not, the three commands to get running, the eight environment traps this run hit, and the one observation deliberately left unfiled because it may duplicate #548. Also replaces the four '(填充中)' placeholders in the result matrix with an explicit 未执行, so nothing unrun can be misread as in progress. Refs #520 #523 #548 #684 #688 #696 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/HANDOFF.md | 112 +++++ docs/acceptance-17/evidence/README.md | 35 ++ .../evidence/permission-matrix.md | 212 +++++++++ docs/acceptance-17/evidence/r1-objects.md | 47 ++ docs/acceptance-17/evidence/r2-analytics.md | 170 +++++++ docs/acceptance-17/evidence/r3-i18n.md | 131 ++++++ .../acceptance-17/evidence/retest-playbook.md | 424 ++++++++++++++++++ docs/acceptance-17/evidence/w1-crud.md | 379 ++++++++++++++++ docs/acceptance-17/evidence/w1-ledger.md | 56 +++ docs/acceptance-17/test-report.md | 13 +- 10 files changed, 1573 insertions(+), 6 deletions(-) create mode 100644 docs/acceptance-17/HANDOFF.md create mode 100644 docs/acceptance-17/evidence/README.md create mode 100644 docs/acceptance-17/evidence/permission-matrix.md create mode 100644 docs/acceptance-17/evidence/r1-objects.md create mode 100644 docs/acceptance-17/evidence/r2-analytics.md create mode 100644 docs/acceptance-17/evidence/r3-i18n.md create mode 100644 docs/acceptance-17/evidence/retest-playbook.md create mode 100644 docs/acceptance-17/evidence/w1-crud.md create mode 100644 docs/acceptance-17/evidence/w1-ledger.md diff --git a/docs/acceptance-17/HANDOFF.md b/docs/acceptance-17/HANDOFF.md new file mode 100644 index 00000000..f0931ef9 --- /dev/null +++ b/docs/acceptance-17/HANDOFF.md @@ -0,0 +1,112 @@ +# rc.2 验收 —— 交接说明 + +给接手这轮验收的下一位(或下一个会话)。**先读这一页,再读别的。** + +- **PR**:[#676](https://github.com/objectstack-ai/hotcrm/pull/676)(draft),分支 + `claude/hotcrm-17-rc1-acceptance-hj869o`,基于 `main` @ 5a78f88 +- **被测版本**:`@objectstack/*` **17.0.0-rc.2**(任务原文说 rc.1,但 main 已由 #663 升到 rc.2) +- **纪律**:**只测不修** —— 不改 `src/`,发现问题建 issue。上一轮 `src/` 零改动,请保持 + +## 一、这一页之外该读什么 + +| 文件 | 用途 | +|---|---| +| [test-plan.md](./test-plan.md) | 验收计划(8 节),已按 rc.2 修订 | +| [test-report.md](./test-report.md) | 报告。**§10 列出未执行项,§11 是 GA 最小回归清单** | +| [parallel-task-packages.md](./parallel-task-packages.md) | 剩余工作的 P1–P5 任务包,**直接照着投** | +| [evidence/](./evidence/) | 上一轮的执行记录与准备资产(见下) | + +## 二、进度:8 节完成 5 节 + +| 计划节 | 状态 | +|---|---| +| 0 自动化基线 | ✅ 完成(verify 1280 例、e2e 16/16、冷启动 0 ERROR) | +| 1 对象列表/详情 | ✅ 完成(15/17 通过,2 项因无种子数据阻塞) | +| 2 对象 CRUD | ✅ 完成(REST 17/17 全量通过) | +| 4 仪表盘/报表 | ✅ 完成(5 仪表盘 56 widget、10 报表全部出数) | +| 7 i18n 四语言 | ✅ 完成 | +| **3 actions 全量(26 个)** | ❌ **未执行** → 用 **P2** 包 | +| **5 flows 全量(24 条)** | ❌ **未执行** → 用 **P3** 包 | +| **6 profile 权限矩阵** | ❌ **未执行** → 用 **P4** 包 | +| **8 阻塞 issue 复测** | ⚠ **部分**:#520/#523 已确证解除;#521 #522 #509 #508 #524 #510 未测 → **P2**;#526 #528 未测 → **P5** | + +## 三、上手三步 + +```bash +cd /home/user/hotcrm # 若是新容器,先 clone 并 checkout 上述分支 +pnpm install +pnpm dev # 端口 4001,等 /api/v1/health 返回 200(约 30s) +``` + +鉴权(**这一条能省你半小时**): + +```bash +TOKEN=$(curl -s -X POST http://localhost:4001/api/v1/auth/sign-in/email \ + -H 'Content-Type: application/json' \ + -d '{"email":"admin@objectos.ai","password":"admin123"}' | node -pe 'JSON.parse(require("fs").readFileSync(0)).token') +``` + +然后照 [parallel-task-packages.md](./parallel-task-packages.md) 投 **P2 → P3 → P4 → P5**。 +写数据的包之间要么串行,要么各用各的前缀(P2 用 `RC1ACC-P2-`,依此类推)。 + +## 四、踩过的坑,别再踩一遍 + +1. **鉴权端点是 `/api/v1/auth/sign-in/email`**。直觉的 `/api/v1/auth/login` 返回 **500** + 而不是 404,极易误判成"服务挂了"。 +2. **三个入口默认三个不同的 SQLite 库**:`objectstack dev` → `dev.db`; + `objectstack start` → `objectstack.db`;migrate CLI → `standalone.db`。 + 所以 `os migrate plan` **默认根本没看 dev server 的库**;复测 #528 必须显式传 + `--database-url file:/home/user/hotcrm/.objectstack/data/dev.db`。 + 另:`migrate plan` 自称 dry-run,却会**新建** `standalone.db` 文件。 +3. **控制台 SPA 首次导航要等 8–10 秒**。等太短会读到"列表页没有 New 按钮"这类假象 —— + 上一轮踩过,是测试假象不是缺陷。 +4. **看板拖拽必须用真实鼠标 API**(`mouse.down` + 多步插值 `move` + `up`); + 合成 `dispatchEvent` 不触发 dnd 库。用真实 API 是**可以**自动化验证的 + (rc.0 报告说"不可自动化",已被本轮推翻)。 +5. **行项目 rollup 是 async**,别在写入的同一 tick 断言父记录金额。 +6. **表单弹窗是 `[role="dialog"]`,lookup 选择器会再开一层**,要 `.last()`。 +7. **删除测试数据要按子→父顺序**,而且:报名过市场活动的线索删不掉(#696), + 得先删 campaign member。 +8. **别做这三件事**:重启/重建 dev server(会毁掉别人的在途数据)、`pnpm demo:reset`、 + `os migrate apply`。#526 的验证要另起独立端口 + 独立库。 + +## 五、必须先看的两条判定 + +- **#688(p0)** console 表单无视 `visibleOn` 却强制其 `required` —— + **`crm_lead` 在界面上完全无法创建**。这是应用的头号演示路径,GA 前必须修。 +- **#684** 记录变更流缺 `runAs: 'system'`,系统驱动的写入下自动化被拒。 + **P3 包的关键任务**:验证**用户会话驱动**的写入路径是否正常 —— 这决定 #684 是 + "系统写入下自动化失效"还是更严重。结论请回填到 #684 评论。 + +## 六、一条悬而未决、**故意没立案**的观察 + +admin 删除部分记录返回 **403 FORBIDDEN "insufficient privileges to delete"**, +尤其是活动类 action 创建的 `crm_event`(而 W1 用 REST 自建的 event 删除是 200, +差别在创建路径)。 + +**没立案的原因**:它很可能是既有 **#548**(app `owner` lookup 与平台 `owner_id` 不一致) +的症状,未做隔离验证前立新案有重复风险。**做 P4 时请优先验证这一条**, +然后要么回填 #548,要么据此另立新案。 + +## 七、evidence/ 里有什么 + +| 文件 | 内容 | +|---|---| +| `retest-playbook.md` | 10 条阻塞 issue 的可执行复现手册。**注意:它的 auth 章节是错的**(猜了 `/auth/login`),用本页第三节的端点 | +| `permission-matrix.md` | 5 profile × 17 对象的权限期望矩阵 + 测试用户创建方式,**P4 的输入** | +| `r1-objects.md` | 对象列表/详情逐项结果 | +| `r2-analytics.md` | 仪表盘 widget 级 + 报表逐项结果,含 #520/#523 的判定依据 | +| `r3-i18n.md` | 四语言矩阵、lint 量化、locale 切换机制 | +| `w1-crud.md` | 17 对象 CRUD 矩阵 + C1–C6 定向检查全文 | +| `w1-ledger.md` | W1 的数据台账(已全部清理,留作格式范例) | + +**截图没有入库**:约 200 张、33MB,留在上一轮容器的 `/tmp` 里,**已随容器回收丢失**。 +报告与 issue 里按文件名引用它们,那些引用现在只表示"当时拍过",无法再打开。 +需要视觉证据的判定(尤其 #688)请在你的环境重拍。 + +## 八、上一轮结束时的环境状态 + +- 上一轮在 W2(actions)开始不久因额度耗尽终止,W2 留下的测试记录已尽力清理, + 仍有少量 `RC1ACC-W2-` 残留(6 event / 2 case / 2 opportunity / 4 contact / 5 lead / 1 account)。 + 那个库是容器内临时 SQLite,**已随容器回收**,你会拿到全新种子库。 +- PR #676 的订阅已退订(避免 Vercel bot 反复唤醒会话消耗额度)。你若要盯 CI,自行重新订阅。 diff --git a/docs/acceptance-17/evidence/README.md b/docs/acceptance-17/evidence/README.md new file mode 100644 index 00000000..e8c93d12 --- /dev/null +++ b/docs/acceptance-17/evidence/README.md @@ -0,0 +1,35 @@ +# evidence/ —— rc.2 验收的执行记录与准备资产 + +本目录是 2026-08-05 那轮验收的原始产物,供复核判定、或接手时直接复用。 +入口页是 [../HANDOFF.md](../HANDOFF.md),结论页是 [../test-report.md](../test-report.md)。 + +## 准备资产(下一轮可直接用) + +| 文件 | 内容 | 用途 | +|---|---|---| +| `retest-playbook.md` | 10 条阻塞 issue 的可执行复现手册,逐条给出 REST 命令、FIXED/仍复现 的判据、上游 issue 关联、是否写数据 | P2/P5 包的输入。**其 auth 章节有误** —— 它猜测端点是 `/auth/login`,实际是 `/api/v1/auth/sign-in/email` | +| `permission-matrix.md` | 5 profile × 17 对象的声明期望矩阵、字段级权限、sharing 规则、可 API 校验的断言清单、测试用户创建方式 | P4 包的输入 | + +## 执行记录 + +| 文件 | 执行者 | 范围 | +|---|---|---| +| `r1-objects.md` | R1(只读) | 14 个带视图对象的列表/详情 + 3 个子对象经父相关列表 | +| `r2-analytics.md` | R2(只读) | 5 仪表盘(widget 级)+ 10 报表;含 #520 与 #523 的解除判定依据 | +| `r3-i18n.md` | R3(只读) | en / zh-CN / ja-JP / es-ES 四语言矩阵、lint 量化、locale 切换机制 | +| `w1-crud.md` | W1(写) | 17 对象全量 CRUD 矩阵 + C1–C6 定向检查 + 6 条新发现的完整论证 | +| `w1-ledger.md` | W1(写) | 数据台账。记录已全部清理,保留作为台账格式范例 | + +W2(actions + 阻塞复测)在开始不久后因额度耗尽被终止,无有效结论,故无记录文件。 + +## 关于截图 + +四位执行者共产出约 **200 张截图(33MB)**,**未入库**,留在当时容器的 `/tmp` scratchpad 中, +**已随容器回收而丢失**。 + +上述记录文件、[../test-report.md](../test-report.md) 以及本轮所立的 issue 中, +仍按文件名引用这些截图(如 `shots/uif-crm_lead-02-after-submit.png`)。 +这些引用现在只说明"当时确实拍过该证据",**无法再打开**。 + +需要视觉证据的判定 —— 尤其 **#688**(`crm_lead` UI 无法创建)—— 请在接手环境重新截取。 +纯 REST 判定不受影响:状态码与响应体都完整写在记录里。 diff --git a/docs/acceptance-17/evidence/permission-matrix.md b/docs/acceptance-17/evidence/permission-matrix.md new file mode 100644 index 00000000..1fa285bf --- /dev/null +++ b/docs/acceptance-17/evidence/permission-matrix.md @@ -0,0 +1,212 @@ +# HotCRM 17.0 rc.2 — Declared Permission / Sharing Matrix (for acceptance testing) + +Sources (all read on branch `claude/hotcrm-17-rc1-acceptance-hj869o`): + +- `/home/user/hotcrm/src/profiles/sales-rep.profile.ts`, `sales-manager.profile.ts`, `service-agent.profile.ts`, `marketing-user.profile.ts`, `system-admin.profile.ts` (also `guest-portal.profile.ts`, insert-only, out of scope here) +- `/home/user/hotcrm/src/sharing/account.sharing.ts`, `case.sharing.ts`, `opportunity.sharing.ts`, `campaign.sharing.ts`, `positions.ts`, `demo-staffing.ts` +- `/home/user/hotcrm/src/objects/*.object.ts` (`sharingModel`, `owner` fields) +- `/home/user/hotcrm/test/authorization-coverage.test.ts` (declared invariants), plus `test/sharing-coverage.test.ts` and `test/demo-staffing.test.ts` for #549/#548 context +- `/home/user/hotcrm/scripts/demo-staff.ts` (test-user creation path) + +Platform semantics that the metadata relies on (as documented in-repo): + +- Permission sets are **explicit-allow only**. An object absent from a profile's `objects` map is denied at the object-CRUD gate *before* OWD/sharing/`view_all_data` are consulted. +- On a `private`-OWD object, the row baseline is **owner-only** (keyed on the platform `owner_id` column, NOT the app-authored `owner` lookup — see #548 below); `viewAllRecords: true` widens to all rows; sharing rules (`sys_record_share`) only ever widen. +- `controlled_by_parent` objects (contact, campaign_member, event_attendee, opportunity/quote line items) derive row access from their parent (ADR-0055); `readScope`/`viewAllRecords` are inert there and the coverage test forbids authoring a scope on them. +- 17.0: `allowExport` is a hard opt-in gate (unset = export DENIED even for admins; `viewAllRecords`/`modifyAllRecords` do not substitute). Export is read-derived: it never widens rows. +- Install-time binding: **a permission set whose name matches a position is bound to that position** (`test/demo-staffing.test.ts`). Plus the platform's additive `member_default` set for every org member (grants e.g. baseline crm_account read at `own` depth — see `src/sharing/demo-staffing.ts` header). + +## 1. Object-level matrix + +OWD per object (from `sharingModel` in `src/objects/*.object.ts`): + +| Object | OWD | Owner field notes | +|---|---|---| +| crm_account | private | app `owner` lookup + `renewal_owner`; platform `owner_id` is what sharing reads | +| crm_campaign | public_read | `owner` lookup | +| crm_campaign_member | controlled_by_parent (parent: crm_campaign) | no owner field (was `private` pre-#488) | +| crm_case | private | `owner` lookup | +| crm_contact | controlled_by_parent (master-detail: crm_account) | `owner` lookup (rows still follow the account) | +| crm_contract | private | `owner` lookup | +| crm_event | private | `owner` lookup (comment: #548 Option B decided, not implemented) | +| crm_event_attendee | controlled_by_parent (parent: crm_event) | — | +| crm_forecast | private | `owner` lookup | +| crm_knowledge_article | public_read | — | +| crm_lead | private | `owner` | +| crm_opportunity | private | `owner`; `is_private` checkbox honoured only by RLS (see §3) | +| crm_opportunity_line_item | controlled_by_parent (parent: crm_opportunity) | — | +| crm_product | public_read | — | +| crm_quote | private | `owner` | +| crm_quote_line_item | controlled_by_parent (parent: crm_quote) | — | +| crm_task | private | `owner` | + +Legend: `C R U D` = allowCreate/Read/Edit/Delete; `VA` = viewAllRecords; `MA` = modifyAllRecords; `own` = explicit `readScope: 'own'`; `X` = allowExport; `—` = **no grant at all (object-level 403 for this profile)**; `deny` = grant present with every flag false. + +| Object (OWD) | sales_rep | sales_manager | service_agent | marketing_user | system_admin | +|---|---|---|---|---|---| +| crm_lead (private) | CRU-, own, X | CRUD, VA+MA, X | -R--, VA, X | CRU-, VA, X | CRUD, VA+MA, X | +| crm_account (private) | CRU-, own, X | CRUD, VA+MA, X | -R--, VA, X | -R--, VA, X | CRUD, VA+MA, X | +| crm_contact (cbp) | CRU-, X (rows follow account) | CRUD, VA+MA, X | -RU-, VA, X | CRU-, VA, X | CRUD, VA+MA, X | +| crm_opportunity (private) | CRU-, own, X | CRUD, VA+MA, X (+RLS is_private) | **deny** (explicit all-false) | -R--, VA, X (+RLS is_private) | CRUD, VA+MA, X | +| crm_quote (private) | CRU-, own | CRUD, VA+MA | — | — | CRUD, VA+MA | +| crm_contract (private) | -R--, own | CRU-, VA | — | — | CRUD, VA+MA | +| crm_product (public_read) | -R--, VA | CRU-, VA | -R--, VA | — | CRUD, VA+MA | +| crm_campaign (public_read) | -R--, VA | CRU-, VA | — | CRU-, VA (+RLS update-widening) | CRUD, VA+MA | +| crm_case (private) | -R--, own, X | -R--, VA, X | CRU-, own, X | — | CRUD, VA+MA, X | +| crm_task (private) | CRUD, own | CRUD, VA+MA | CRUD, own | — | CRUD, VA+MA | +| crm_event (private) | CRUD, own | CRUD, VA+MA | CRUD, own | — | CRUD, VA+MA | +| crm_event_attendee (cbp) | CRUD | CRUD | CRUD | — | CRUD, VA+MA | +| crm_forecast (private) | -R--, own | CRU-, VA+MA | — | — | CRUD, VA+MA | +| crm_knowledge_article (public_read) | -R--, VA | -R--, VA | CRU-, VA | -R--, VA | CRUD, VA+MA | +| crm_opportunity_line_item (cbp) | CRUD (via parent) | CRUD (via parent) | — | — | CRUD, VA+MA | +| crm_quote_line_item (cbp) | CRUD (via parent) | CRUD (via parent) | — | — | CRUD, VA+MA | +| crm_campaign_member (cbp) | -R-- | -R-- | — | CRU- | CRUD, VA+MA | + +`system_admin` additionally declares `systemPermissions: [view_setup, manage_users, customize_application, view_all_data, modify_all_data, manage_profiles, manage_roles, manage_sharing]`. + +Export-bit invariant (pinned by `authorization-coverage.test.ts`): `allowExport` exists exactly where allowRead exists AND the app ships an export surface — the surface set is exactly `crm_account, crm_case, crm_contact, crm_lead, crm_opportunity`. No other object carries the bit on any set; guest_portal carries none. + +## 2. Field-level security (FLS) — complete declared list + +There are NO other FLS declarations besides these (`fields` maps in the five profiles; system_admin declares none): + +| Field | sales_rep | sales_manager | service_agent | marketing_user | +|---|---|---|---|---| +| crm_account.health_score | read-only | read+write | read-only | read-only | +| crm_account.annual_revenue | read-only | (unauthored) | (unauthored) | (unauthored) | +| crm_account.description | read+write | (unauthored) | (unauthored) | (unauthored) | +| crm_opportunity.amount | read+write | read+write | (no read on object at all) | read-only | +| crm_opportunity.probability | read+write | (unauthored) | — | (unauthored) | +| crm_quote.internal_notes | read+write (stays writable after quote sent, per quote.hook) | read+write | (no object grant) | (no object grant) | +| crm_case.internal_notes | **MASKED (readable: false, editable: false)** | read-only | read+write | (no object grant) | +| crm_case.is_sla_violated | (unauthored) | (unauthored) | read-only | — | +| crm_case.resolution_time_hours | (unauthored) | (unauthored) | read-only | — | + +Notes for the tester: + +- The only fully **masked** field in the app is `crm_case.internal_notes` for **sales_rep**. Expect it absent/nulled in rep reads of a case they otherwise can read. The coverage test guarantees no view filters/sorts on it (a masked field in a filter throws `field_predicate_denied`). +- `crm_opportunity.amount` is NOT masked for anyone who can read opportunities — the differences are editability (marketing read-only; rep/manager/admin writable). +- FLS is only authored where the same set holds allowRead on the object (test-pinned), so e.g. marketing has no case FLS because it has no case grant. + +## 3. Sharing rules and row-level security policies + +### Criteria sharing rules (9 total, all `type: 'criteria'`, all target positions) + +| # | Rule name | File | Object | Condition (CEL) | Level | Target position | +|---|---|---|---|---|---|---| +| 1 | account_team_sharing | src/sharing/account.sharing.ts | crm_account | `record.type == "customer" && record.is_active == true` | edit | sales_manager | +| 2 | north_america_territory | src/sharing/account.sharing.ts | crm_account | `record.billing_country in ["US","CA","MX"]` | edit | na_sales_team | +| 3 | europe_territory | src/sharing/account.sharing.ts | crm_account | `record.billing_country in ["UK","DE","FR","IT","ES"]` (deliberately `UK` not `GB`) | edit | eu_sales_team | +| 4 | campaign_leadership_manager | src/sharing/campaign.sharing.ts | crm_campaign | `record.status in ["planning","in_progress"] && record.is_active == true` | edit | marketing_manager | +| 5 | campaign_leadership_director | src/sharing/campaign.sharing.ts | crm_campaign | same as #4 | edit | marketing_director | +| 6 | case_escalation_sharing | src/sharing/case.sharing.ts | crm_case | `record.priority == "critical" && record.is_closed == false` | edit | service_manager | +| 7 | case_director_sharing | src/sharing/case.sharing.ts | crm_case | same as #6 | **read** | service_director | +| 8 | opportunity_sales_sharing | src/sharing/opportunity.sharing.ts | crm_opportunity | `!(record.stage in ["closed_won","closed_lost"]) && record.amount >= 100000` | read | sales_director | +| 9 | opportunity_executive_sharing | src/sharing/opportunity.sharing.ts | crm_opportunity | same as #8 | read | executive | + +Positions are FLAT (ADR-0090 D3 — no hierarchy, no roll-up), which is why every leadership rung (executive, service_director, marketing_manager, marketing_director) has its own explicit rule. Declared positions (`src/sharing/positions.ts`): executive, sales_director, sales_manager, sales_rep, service_director, service_manager, service_agent, marketing_director, marketing_manager, marketing_user, na_sales_team, eu_sales_team. The territory team positions are record groupings only — no permission set binds to them. + +Rules #4/#5 target a `public_read` object at `edit` level — read adds nothing there; what these rules actually add is WRITE access to live campaigns for marketing leadership. The `authorization-coverage` test pins that a share on a public_read object must not be read-level. + +### Row-level security policies (in `rowLevelSecurity` of profiles) + +| Policy | Set | Object | Op | Predicate (`using`) | +|---|---|---|---|---| +| opportunity_private_owner_only | sales_manager | crm_opportunity | select | `is_private == false \|\| owner == current_user.id` | +| opportunity_private_owner_only_marketing | marketing_user | crm_opportunity | select | `is_private == false \|\| owner == current_user.id` | +| marketing_campaign_updates | marketing_user | crm_campaign | update | `id != null` (all-rows widener vs member_default's owner-only-writes) | +| marketing_campaign_member_updates | marketing_user | crm_campaign_member | update | `id != null` | + +The **is_private rule**: `crm_opportunity.is_private` is settable in the opportunity form, and the ONLY thing enforcing it is these two RLS policies — carried by exactly the two sets holding `viewAllRecords` on opportunities (sales_manager, marketing_user). Compiles to `{$or:[{is_private:false},{owner:}]}`. Note the predicate keys on the app `owner` lookup, not platform `owner_id` (relevant to #548). system_admin carries **no** such policy — an admin (viewAllRecords + view_all_data) is expected to see private deals. sales_rep needs no policy (own-scope already excludes others' deals). The sharing rules #8/#9 vs the RLS interact: a sales_director share on a >=100k deal that is ALSO is_private is only filtered for sets carrying the RLS — sales_director holds no persona set here, so if a tester stands one up, check which layer wins. + +## 4. Testable expectations per profile + +Assume seed data: 9 accounts (6 NA, 2 EU (UK/DE), 1 no-territory SG); `demo_bootstrap` assigns ownership of ALL seeded records to the first user (dev admin). API base `http://localhost:4001`, cookie auth via `POST /api/v1/auth/sign-in/email`. Query via `POST /api/v1/data//query`. + +### sales_rep (e.g. RC1ACC-rep, position `sales_rep` only, no territory) + +1. `GET/query crm_forecast` → only rows where `owner_id` = self (freshly created user owning nothing: 0 rows, NOT a 403). +2. `query crm_account` → only self-owned accounts (0 for a new user with no territory position); after creating an account, exactly that one. +3. `query crm_opportunity` → own only; another user's >=100k deal must NOT appear (rep holds no leadership position). +4. `query crm_product` and `crm_knowledge_article` and `crm_campaign` → ALL rows (viewAllRecords on public_read catalogs). +5. Read an own case → `internal_notes` masked (absent/null); write to `crm_case.internal_notes` → denied/ignored (FLS). +6. Create opportunity + line items (POST crm_opportunity, crm_opportunity_line_item) → 2xx; line items on someone ELSE's opportunity → denied (parent-derived write). +7. Export: list-export/report-export on crm_opportunity → allowed but contains only own book. Export on crm_quote → denied (`EXPORT_NOT_PERMITTED`; no allowExport bit). +8. NEGATIVE: `DELETE crm_account/` → 403 (allowDelete false). +9. NEGATIVE: `PATCH crm_account.health_score` → denied (FLS read-only); `PATCH crm_account.annual_revenue` → denied. +10. NEGATIVE (with `na_sales_team` added, after rule evaluate): reads the 6 NA accounts not owned + their contacts, but the accounts' quotes/contracts/tasks/events related lists stay empty (own-only children) — **this is #549, expected, do NOT file**. + +### sales_manager (RC1ACC-mgr, position `sales_manager`) + +1. `query crm_opportunity` → ALL non-private deals org-wide (viewAllRecords), and edit/delete on any of them succeeds (modifyAllRecords). +2. `query crm_opportunity` with a deal flagged `is_private=true` owned by someone else → row HIDDEN (RLS); own private deal → visible. +3. `query crm_forecast` → every rep's snapshots; PATCH a rep's forecast committed number → 2xx. +4. Read case → `internal_notes` visible; PATCH `crm_case.internal_notes` → denied (FLS read-only for this set). +5. Via sharing rule #1 (`account_team_sharing`): after rule evaluation, holds edit share on all active customer accounts (redundant with viewAllRecords for read — verify via `sys_record_share` rows or `POST /api/v1/security/explain`). +6. Submitting an opportunity >= $100k for approval → `manager_review` node resolves to this user (non-empty approver slate). +7. NEGATIVE: `POST crm_case` → 403 (allowCreate false); `PATCH crm_case` non-internal fields → 403 (allowEdit false). +8. NEGATIVE: `DELETE crm_contract`, `DELETE crm_product`, `DELETE crm_campaign` → 403 (allowDelete false on all three). +9. NEGATIVE: quote export → `EXPORT_NOT_PERMITTED` (no allowExport on crm_quote despite full CRUD+MA — 17.0 gate). + +### service_agent (RC1ACC-agent, position `service_agent`) + +1. `query crm_account` / `crm_contact` / `crm_lead` → ALL rows (viewAllRecords context reads); PATCH contact → 2xx, PATCH account → 403 (allowEdit false). +2. `query crm_case` → own queue only; another agent's non-critical case hidden. +3. Critical open case owned by another agent → still hidden for the agent (escalation rules target service_manager/service_director positions, NOT service_agent). +4. Knowledge: create + edit own draft article → 2xx; read all published → all rows; DELETE article → 403. +5. `log_call`/`log_meeting` on a case → creates crm_event → 2xx (#592 grant). +6. Read a case → `internal_notes` readable AND writable; `is_sla_violated`, `resolution_time_hours` read-only (PATCH denied). +7. NEGATIVE: `query crm_opportunity` → 403 object-level (explicit all-false deny — before OWD is even consulted). +8. NEGATIVE: `query crm_quote`, `crm_contract`, `crm_forecast`, `crm_campaign_member` → 403 (no grant at all). +9. NEGATIVE: on a territory/org-readable account detail page, tasks/events related lists show only agent-owned rows even though the account is fully readable — **#549, expected**. + +### marketing_user (RC1ACC-mkt, position `marketing_user`) + +1. `query crm_lead`, `crm_contact` → all rows, editable; DELETE lead → 403. +2. `query crm_opportunity` → all NON-private rows (viewAllRecords + RLS); private deal owned by someone else → hidden; amount readable but PATCH amount → denied (FLS read-only + allowEdit false anyway). +3. "Add to Campaign" action: enrolls leads into a campaign the user did NOT create → 2xx (needs RLS wideners `marketing_campaign_updates` + `marketing_campaign_member_updates`; if this 403s, the RLS widening regressed). +4. PATCH a `crm_campaign_member.status` created by the enrollment flow (system-created row) → 2xx (same widener). +5. Export lead/contact/account/opportunity lists → allowed; campaign export → denied (no bit). +6. NEGATIVE: `query crm_case`, `crm_task`, `crm_quote`, `crm_product`, `crm_event`, `crm_forecast` → 403 (no grants). +7. NEGATIVE: `POST crm_account` → 403 (allowCreate false); `DELETE crm_campaign` → 403. +8. NEGATIVE: DELETE a crm_campaign_member row → 403 (delete reserved to manager/admin — actually only system_admin grants it). + +### system_admin (dev admin or RC1ACC-admin, see §5 binding caveat) + +1. Full CRUD on every one of the 17 objects, all rows, including is_private opportunities (no RLS on this set) — `query crm_opportunity` must include private deals owned by others. +2. Masked fields do NOT apply: reads `crm_case.internal_notes` etc. everywhere. +3. Export on crm_lead/account/contact/opportunity/case → allowed; export on crm_quote/campaign/task/... → `EXPORT_NOT_PERMITTED` **even for admin** (17.0: modifyAllRecords does not substitute for allowExport — expected, not a bug). +4. Setup surfaces reachable (view_setup, manage_users, manage_sharing). +5. NEGATIVE-ish: any NEW object added without a system_admin grant would 403 for admin too (explicit-allow-only); rc acceptance can spot-check one uncatalogued sys-adjacent surface if present. + +### Known-broken / do-not-refile areas + +- **#548 — two ownership columns.** Every object carries the app-authored `owner` lookup (drives "My …" views, notifications, analytics owner axis) AND the platform `owner_id` (the ONLY column the sharing service's private-OWD baseline reads). The `owner_id` migration is "decided (Option B) but not implemented" (`event.object.ts` comment). Consequence to expect: **editing the `owner` field on a record does not move row-level access** — the old owner may keep reading it and the new owner may not see it, and RLS `owner == current_user.id` (is_private policies) diverges from OWD `owner_id` behavior after a reassignment. Import mappings also still target `owner`. Attribute all reassignment-visibility anomalies to #548; do not file duplicates. +- **#549 — sharing rules do not cascade to children.** Territory/team rules are authored on `crm_account` ONLY. A rep/agent who receives an account via territory/team share reads the account + its contacts (controlled_by_parent), but quotes, contracts, tasks, events on it stay own-only, and opportunities widen only via the >=$100k leadership rules; cases only via the critical-escalation rules. Related lists that look "missing rows" on shared accounts are the pinned status quo (`test/sharing-coverage.test.ts` ACCOUNT_CHILD_COVERAGE ledger: contact=derived, opportunity=partial, case=partial, quote/contract/task/event=own_only). Open business decision — do not file duplicates. +- Also note (#621 history): territory rules filter on the flat `crm_account.billing_country` projection maintained by `account.hook.ts` — an account whose `billing_address.country` is typo'd/lowercase gets normalized (trim+uppercase); EU list uses `UK` not `GB`. + +## 5. Creating test users (RC1ACC-*) + +How the app itself does it (`scripts/demo-staff.ts` driving `src/sharing/demo-staffing.ts`, all idempotent, local-server-only guard): + +1. **Sign in as dev admin**: `POST /api/v1/auth/sign-in/email` with `admin@objectos.ai` / `admin123` (exists only under `NODE_ENV=development`, server from `pnpm dev` on port 4001). Keep the session cookie; send `Origin: http://localhost:4001` (better-auth rejects other origins; use hostname `localhost`, not `127.0.0.1` — the latter 403s INVALID_ORIGIN). +2. **Create user**: `POST /api/v1/auth/admin/create-user` with `{email, password (>=8 chars), name, mustChangePassword: false}`. This is the ONLY sanctioned path — direct `sys_user` inserts are refused by the ADR-0092 write guard and would be un-loginable anyway. It also creates the `sys_member` row (role `member`) automatically. Without `mustChangePassword: false` every subsequent call 403s PASSWORD_EXPIRED. +3. **Bind positions**: `POST /api/v1/data/sys_user_position` with `{user_id, position, organization_id}` (org id read from the admin's own `sys_member` row). **Profile binding = position binding**: a permission set whose name matches a position is bound to it at install time, so giving a user position `sales_rep` gives them the SalesRepProfile set. +4. **Re-evaluate sharing rules** (mandatory — seeded rows were written `isSystem: true`, so the grant-materialising hook skipped them): list `sys_sharing_rule` where `active = true`, then `POST /api/v1/sharing/rules//evaluate` for each (diff-based, safe to rerun). +5. Verify per user with `POST /api/v1/security/explain` and/or `POST /api/v1/data/crm_account/query` as that user. + +Recommended RC1ACC roster (mirrors DemoOrgStaffing's shape; password e.g. `rc1acc-Pass1`): + +| Email | Positions | Bound profile | +|---|---|---| +| RC1ACC-rep@objectos.ai | `sales_rep` (optionally + `na_sales_team` for territory tests) | sales_rep | +| RC1ACC-mgr@objectos.ai | `sales_manager` | sales_manager | +| RC1ACC-agent@objectos.ai | `service_agent` | service_agent | +| RC1ACC-mkt@objectos.ai | `marketing_user` | marketing_user | +| RC1ACC-admin@objectos.ai | (no CRM position — see caveat) | system_admin | + +**system_admin caveat**: `system_admin` is NOT in `CrmPositions`, so the name-match binding does not apply; the set binds to the platform admin anchor. `create-user` provisions `sys_member` role `member`, so a freshly created RC1ACC-admin will NOT hold the system_admin set — either run admin assertions as the dev-admin seed account (`admin@objectos.ai`, which demo_bootstrap makes owner of all seed records), or elevate the new user's member role through Setup → Users before testing. Verify with `POST /api/v1/security/explain` that the set actually resolved before attributing failures. + +Alternative: extend `DemoOrgStaffing` locally (scratch copy) and run `pnpm demo:staff --url http://localhost:4001` — but note the shipped table only covers rep/rep/manager, and the script's verify step asserts territory behavior you may not want for all five users; direct API calls per steps 1-4 are cleaner for RC1ACC. + +Keep test users NON-owners of seeded records (ownership stays with the dev admin) — a share proven against a record the user owns proves nothing, since private OWD already admits the owner. diff --git a/docs/acceptance-17/evidence/r1-objects.md b/docs/acceptance-17/evidence/r1-objects.md new file mode 100644 index 00000000..0bf6dfc9 --- /dev/null +++ b/docs/acceptance-17/evidence/r1-objects.md @@ -0,0 +1,47 @@ +# HotCRM 17.0.0-rc.2 Acceptance — Executor R1 + +Run: 2026-08-05 · Server http://localhost:4001 · Console `/_console/apps/app.objectstack.hotcrm/` · Auth: admin@objectos.ai (Bearer) +Routes verified: list = `/_console/apps/app.objectstack.hotcrm/`, detail = `...//record/`. +Screenshots in this directory (`/tmp/claude-0/-home-user/f9de7acc-06e5-5667-b535-06e82c336458/scratchpad/r1/`). Raw data: `raw-results.json`. + +## Environment-wide console noise (present on every page, NOT app defects) + +- `console.error: Failed to load resource: net::ERR_TUNNEL_CONNECTION_FAILED` — Sentry telemetry (`o4510356161757184.ingest.us.sentry.io/.../envelope/`) blocked by the sandbox agent proxy. Environment artifact. +- `console.error: Failed to load resource: 404` on first load only — `GET /assets/crm-favicon.ico` → 404. Minor packaging nit: the favicon declared in app branding is not served. No functional impact. + +No other console.error and zero `pageerror` across all 17 objects' list + detail pages. + +## Main objects (14) + +| object | REST status/rows (limit=5) | list render | detail render | console errors (deduped) | verdict | +|---|---|---|---|---|---| +| crm_account | 200 / 5 (total 9) | PASS, 9 rows grid + footer "9 records" (`crm_account-list.png`) | PASS — Details/Related/Attachments/Activity tabs, field groups Basic Info/Financials/Contact Info (`crm_account-detail.png`) | env noise only | 通过 — fully healthy | +| crm_contact | 200 / 5 (total 9) | PASS, grouped-by-Account grid, 9 accounts groups, footer "9 records" (`crm_contact-list.png`) | PASS — Details/Related(3)/Attachments tabs, Identity/Account & Role/Comm-Prefs groups (`crm_contact-detail.png`) | env noise only | 通过 — default view groups by account (by design) | +| crm_lead | 200 / 5 (total 21) | PASS, 21 rows (`crm_lead-list.png`) | PASS — Details/Related/Activity (`crm_lead-detail.png`) | env noise only | 通过 | +| crm_opportunity | 200 / 5 (total 23) | PASS, 10 rows — default "Open Deals" filtered view, footer "10 records" (`crm_opportunity-list.png`) | PASS — stage path, Details/Related(3)/Activity, Products side panel (`crm_opportunity-detail.png`) | env noise only | 通过 — filtered default view is intentional | +| crm_product | 200 / 5 (total 13) | PASS, 13 rows (`crm_product-list.png`) | PASS — Details/Related (`crm_product-detail.png`) | env noise only | 通过 | +| crm_quote | 200 / 5 (total 5) | PASS, 5 rows (`crm_quote-list.png`) | PASS — status path Draft→Accepted, Details/Related(3)/Attachments (`crm_quote-detail.png`) | env noise only | 通过 | +| crm_contract | 200 / 4 (total 4) | PASS, 4 rows (`crm_contract-list.png`) | PASS — status path, Details/Attachments, Contract Info/Parties/Terms & Dates groups; no Related tab (layout choice) (`crm_contract-detail.png`) | env noise only | 通过 | +| crm_case | 200 / 5 (total 38) | PASS, 38 rows, All Cases + Service Workflow/SLA Calendar/Timeline views listed (`crm_case-list.png`) | PASS — Details/Related/Activity (`crm_case-detail.png`) | env noise only | 通过 | +| crm_campaign | 200 / 5 (total 7) | PASS, 7 rows (`crm_campaign-list.png`) | PASS — status path Planning→In Progress, Details/Related(14) (`crm_campaign-detail.png`) | env noise only | 通过 | +| crm_forecast | 200 / 5 (total 8) | PASS, 8 rows (`crm_forecast-list.png`) | PASS — tab-less layout: Snapshot/Amounts/Source groups + Discussion timeline (`crm_forecast-detail.png`) | env noise only | 通过 | +| crm_knowledge_article | 200 / 4 (total 4) | PASS, 4 rows (`crm_knowledge_article-list.png`) | PASS — status path Draft→Published, Article Information + rendered rich-text Content (`crm_knowledge_article-detail.png`) | env noise only | 通过 | +| crm_task | 200 / 5 (total 7) | PASS, 7 rows (`crm_task-list.png`) | PASS — status path, Related Records/Recurrence/Progress & Effort/System groups (`crm_task-detail.png`) | env noise only | 通过 | +| crm_event | 200 / 0 (total 0) | PASS — view renders, clean "Nothing here yet" empty state; Grid/Calendar/Timeline/Kanban view tabs present (`crm_event-list.png`) | 阻塞 — no seed record to open | env noise only | 阻塞 — UI healthy, seed data absent (0 rows); detail untestable | +| crm_event_attendee | 200 / 0 (total 0) | PASS — renders empty state "Nothing here yet" (`crm_event_attendee-list.png`) | 阻塞 — no seed record to open | env noise only | 阻塞 — UI healthy, seed data absent (0 rows); detail untestable | + +## Child objects (3) — REST + parent related list + +| object | REST status/rows | parent related-list check | verdict | +|---|---|---|---| +| crm_opportunity_line_item | 200 / 5 (total 74) | Opportunity "Wayne Q1 Expansion" (UNGq93kVo03CO3nk) → Related → Products: "Opportunity Line Item 3" table with product names/qty/price/total (`opportunity_related_expanded.png`; collapsed state `opportunity_related.png`) | 通过 — note: right-hand "Products" summary side panel lists raw record IDs (e.g. `-PBylY-NARjgK6dS`) instead of display names — cosmetic | +| crm_quote_line_item | 200 / 5 (total 16) | Quote QTE-0001 (76ZW4qQ9oTHG97_Y) → Related: "Quote Line Item 3" table, product names + qty + price + total (`quote_related.png`) | 通过 | +| crm_campaign_member | 200 / 5 (total 51) | Campaign CPG-0001 (YyaCORMnaaoQ91Qd) → Related: "Campaign Member 12", Lead/Contact/Status/Response Date columns, paginated Page 1 of 3 (`campaign_related.png`) | 通过 | + +Note: the tested opportunity has no quotes attached (its "Quotes" related section is legitimately empty — REST confirms no crm_quote rows point at UNGq93kVo03CO3nk); "Open Tasks" expands to "Task 0 — No related records found", correct. + +## Summary + +- 15/17 objects 通过; crm_event and crm_event_attendee 阻塞 for detail-page testing only because the seed ships 0 rows (REST 200, empty-state list view renders correctly). +- Zero application console errors or page errors anywhere. Only environment noise: proxy-blocked Sentry telemetry and a 404 on `/assets/crm-favicon.ico` (minor packaging nit worth a look). +- Minor cosmetic finding: opportunity detail "Products" side-panel widget renders raw line-item record IDs instead of display labels. diff --git a/docs/acceptance-17/evidence/r2-analytics.md b/docs/acceptance-17/evidence/r2-analytics.md new file mode 100644 index 00000000..a9f861b0 --- /dev/null +++ b/docs/acceptance-17/evidence/r2-analytics.md @@ -0,0 +1,170 @@ +# R2 — HotCRM 17.0.0-rc.2 Acceptance: Dashboards & Reports + +- Date: 2026-08-05 · Executor: R2 (read-only) · Server: http://localhost:4001 (not restarted) +- Auth: POST /api/v1/auth/sign-in/email → Bearer token; Playwright authenticated via `context.request.post` (cookies shared with browser). +- Analytics REST path used by the UI (captured from network): **`POST /api/v1/analytics/dataset/query`** — every observed call returned **200**. +- Seed baseline (REST cross-checks, `GET /api/v1/data/?limit=1` → `total`): crm_case **38**, crm_opportunity **23**, crm_lead **21**, crm_account **9**, crm_contact **9**, crm_task **7**, crm_forecast **8**, crm_product **13**, **crm_event 0 (no seeds)**. +- Noise excluded from verdicts: Sentry `o4510356161757184.ingest.us.sentry.io` requests fail with `net::ERR_TUNNEL_CONNECTION_FAILED` (outbound proxy blocks them; unrelated to app) and `GET /assets/crm-favicon.ico` → 404 (cosmetic asset miss, appears on report/dashboard pages). +- Scripts & artifacts: `/tmp/claude-0/-home-user/f9de7acc-06e5-5667-b535-06e82c336458/scratchpad/r2/` (`dash.js`, `dash2.js`, `filters.js`, `reports.js`, `dash_results.json`, `dash2_results.json`, `filter_results.json`, `report_results.json`, PNGs referenced below). + +Verdict legend: 通过 = pass, 失败 = fail, 阻塞 = blocked. + +--- + +## SCOPE A — Dashboards + +### 1. crm_overview_dashboard (`/_console/apps/app.objectstack.hotcrm/dashboard/crm_overview_dashboard`) — 通过 +Screenshots: `dash_crm_overview_dashboard.png` (top), `dash_crm_overview_dashboard_mid.png`, `dash_crm_overview_dashboard_bottom.png` (below fold). + +| Widget | Verdict | Evidence | +|---|---|---| +| Total Revenue (metric) | 通过 | 610,000 (this quarter) | +| Active Deals (metric) | 通过 | 7 | +| Won Deals (metric) | 通过 | 4 | +| Avg Deal Size (metric) | 通过 | 152,500 | +| Revenue Trends (area) | 通过 | SVG rendered, month x-axis 2026-01…2026-08, y-axis 0–600K | +| Lead Source (donut) | 通过 | Donut with 7 legend entries (Cold Call, Content/Blog, Email Campaign, Event/Trade Show, Partner, Referral, Web) | +| Pipeline by Stage (funnel) | 通过 | Funnel with Qualification / Needs Analysis / Proposal / Negotiation segments | +| Top Products (bar) | 通过 | Bars for Service/Software/Subscription/Support, y-axis 0–120K | +| Pipeline by Owner (table) | 通过 | Row: Dev Admin · 2,910,000 · 7 · 415,714 | + +Console errors: only favicon 404 + Sentry tunnel noise. Failed app requests: none (all analytics queries 200). + +### 2. executive_dashboard — 通过 +Screenshot: `dash_executive_dashboard.png`. + +| Widget | Verdict | Evidence | +|---|---|---| +| Total Revenue (YTD) | 通过 | 1,290,000 | +| Active Accounts | 通过 | 9 (matches REST total) | +| Total Contacts | 通过 | 9 (matches REST total) | +| Open Leads | 通过 | 21 (matches REST total) | +| Revenue Trend (area) | 通过 | Month buckets 2026-01…2026-08, y 0–600K | +| Revenue by Industry (donut) | 通过 | 6 industry segments | +| Pipeline by Stage (funnel) | 通过 | 4 stage segments | +| New Accounts (bar) | 通过 | 2026-08 bucket, y 0–12 | +| Accounts by Industry (table) | 通过 | 9 data rows, e.g. Technology · 30,000,000 · 2 | + +Console errors: Sentry noise only. HTTP errors: none. + +### 3. sales_dashboard — 通过 +Screenshots: `dash_sales_dashboard.png`, `dash_sales_dashboard_mid.png`, `dash_sales_dashboard_bottom.png`. + +| Widget | Verdict | Evidence | +|---|---|---| +| Total Pipeline | 通过 | 2,910,000 | +| Closed Won (QTD) | 通过 | 610,000 | +| Open Opportunities | 通过 | 7 | +| Win Rate (12M) | 通过 | 62% | +| Deals Won (12M) | 通过 | 8 | +| Deals Lost (12M) | 通过 | 5 | +| Monthly Revenue Trend (area) | 通过 | Month buckets 2026-01…2026-08 | +| Pipeline by Forecast Category (h-bar) | 通过 | Axis 0–2.4M, bars rendered | +| Lead Source (donut) | 通过 | 7 segments | +| Open Pipeline by Owner (table) | 通过 | Dev Admin · 2,910,000 · 7 · 55% | +| Quota Attainment by Rep (table) | 通过* | "— · 1,500,000 · 820,000 · 55%" — values correct; owner label renders "—" (see Observations O2) | +| Win / Loss by Rep (table) | 通过 | Dev Admin · 8 · 5 · 13 · 62% · 1,290,000 | +| Win / Loss by Lead Source (table) | 通过 | 8 data rows, e.g. Web · 2 · 1 · 3 · 67% · 470,000 | +| Why We Lose (donut) | 通过 | 5 loss-reason segments | +| Pipeline by Stage × Lead Source (pivot) | 通过 | Cross-tab renders, e.g. Negotiation × Partner 1,200,000; row/col totals present | + +Console errors: Sentry noise only. HTTP errors: none. + +### 4. service_dashboard — 通过 (see #520 signal below) +Screenshots: `dash_service_dashboard.png`, `dash_service_dashboard_mid.png`, `dash_service_dashboard_bottom.png`. + +| Widget | Verdict | Evidence | +|---|---|---| +| Open Cases (metric) | 通过 | **30** (REST: 38 cases total, 30 with `is_closed=false` — exact match) | +| Critical Cases (metric) | 通过 | **7** (REST: 9 critical, 7 open critical) | +| Avg Resolution Time (metric) | 通过 | **45.0** h | +| SLA Violations (metric) | 通过 | **3** | +| Cases by Status (donut) | 通过 | 5 segments: Escalated / In Progress / New / Resolved / Waiting on Customer | +| Cases by Priority (pie) | 通过 | 4 segments: Critical / High / Low / Medium | +| Cases by Origin (bar) | 通过 | Chat/Email/Phone/Portal/Web bars, y 0–12 | +| Daily Case Volume (area) | 通过 | Day buckets Jul 6 → Aug 4, non-empty series | +| SLA Compliance (gauge) | 通过 | Shows 0.0% SLA Violation Rate — **data-consistent, not a bug**: widget filters `is_closed: true`, and all 3 SLA-violated cases are open (Open Cases by Priority shows Critical 42.9% ≈ 3/7); closed cases have zero violations | +| Open Cases by Priority (table) | 通过 | Medium 9 · 0.0% / Critical 7 · 42.9% / High 7 · 0.0% / Low 7 · 0.0% | + +Console errors: Sentry noise only. HTTP errors: none; analytics queries 200 (e.g. payload `{"datasetName":"case_metrics","selection":{"measures":["case_count"],"runtimeFilter":{"$and":[{"is_closed":false}...]}}}`). + +### 5. sales_activity_dashboard — 通过 (empty event widgets are seed-consistent) +Screenshots: `dash_sales_activity_dashboard.png`, `dash_sales_activity_dashboard_mid.png`, `dash_sales_activity_dashboard_bottom.png`. + +| Widget | Verdict | Evidence | +|---|---|---| +| Interactions Logged | 通过 | 0 — correct: `GET /api/v1/data/crm_event?limit=1` → `"total":0` (no event seeds) | +| Meetings Booked | 通过 | 0 (same — no crm_event seeds) | +| Customer Minutes | 通过 | 0 (same) | +| Tasks Completed | 通过 | 1 (crm_task has 7 records; 1 matches widget filter) | +| Activity by Rep (bar) | 通过 | Renders clean "No rows" empty state (correct for 0 events; no error) | +| Activity Volume by Week (area) | 通过 | "No rows" empty state | +| Activity Mix (donut) | 通过 | "No rows" empty state | +| Where the Activity Lands (bar) | 通过 | "No rows" empty state | +| Interactions on Deals | 通过 | 0 (no events) | +| Open Deals | 通过 | 10 | +| Quiet 30+/60+/90+ Days | 通过 | 0 / 0 / 0 — correct: all 9 accounts have `last_activity_date` between 2026-07-15 and 2026-08-04 (REST-verified), i.e. none quiet 30+ days | + +Console errors: Sentry noise only. HTTP errors: none. Verdict note: every widget renders and the zeros/empties are provably data-driven (REST cross-check), not query failures — no failing request or console error on this page. + +### Global-filter tests (2 dashboards, before/after) + +**crm_overview_dashboard — date range** (`filter_crm_before.png` → `filter_crm_after_year.png`): +"This quarter" → "This year" changed every dateRange-bound KPI: +- Total Revenue 610,000 → **1,290,000** +- Active Deals 7 → **10** +- Won Deals 4 → **8** +- Avg Deal Size 152,500 → **161,250** + +**crm_overview_dashboard — Owner filter** (`filter_crm_after_owner.png`): +Owner: All → Dev Admin. Numbers unchanged — expected, since REST shows a single owner (`6pQQ0XGw…`) owns all 23 opportunities. The filter is provably applied server-side: captured query payload +`{"datasetName":"opportunity_metrics","selection":{"measures":["total_amount"],"runtimeFilter":{"$and":[{"stage":"closed_won"},{"$and":[{"close_date":{"$gte":"2026-01-01","$lte":"2026-12-31"}},{"owner":"6pQQ0XGw6t294Qme6cbucdEM8giap4ew"}]}]}}}` → 200. + +**service_dashboard — Priority filter** (`filter_svc_before.png` → `filter_svc_after_priority.png`): +Priority: All → Critical changed the numbers: +- Open Cases 30 → **7** +- Avg Resolution Time 45.0 → **48.0** h +- SLA Violations 3 → 3 (all violated cases are critical — consistent with Critical row's 42.9% rate) +Captured payload: `{"datasetName":"case_metrics","selection":{"measures":["case_count"],"runtimeFilter":{"$and":[{"is_closed":false},{"priority":"critical"}]}}}` → 200. + +### #520 signal (service_dashboard datetime window filter — acceptance) +**PASS.** In rc.0 every service_dashboard widget was empty (hotcrm#520 / upstream objectstack#3912: SQLite datetime filter values coerced to epoch-ms INTEGER against ISO-TEXT columns, so the injected date window matched nothing). On rc.2 every widget shows real numbers from the 38 seeded cases: **Open Cases 30, Critical Cases 7, Avg Resolution 45.0 h, SLA Violations 3**, plus fully populated status/priority/origin charts, a 30-day Daily Case Volume series with day buckets Jul 6–Aug 4, and the Open Cases by Priority table (Medium 9 / Critical 7 / High 7 / Low 7). These exactly match the REST ground truth (38 total, 30 open, 8 closed; 9 critical). All `POST /api/v1/analytics/dataset/query` calls returned 200 with no console errors. Note the shipped design in `src/dashboards/service.dashboard.ts` deliberately omits the dashboard `dateRange` (per the in-file #460/#3912 analysis) — the read-side acceptance signal here is the widgets computing over datetime-stamped case data and returning correct non-zero numbers, which they do; additionally the `created_date`-bucketed Daily Case Volume window (last 30 days) populates correctly, showing the datetime day-bucketing path works. + +--- + +## SCOPE B — Reports (all at `/_console/apps/app.objectstack.hotcrm/report/`) + +| Report | Type | Verdict | Evidence (screenshot `report_.png`) | +|---|---|---|---| +| accounts_by_industry_type | matrix | 通过 | 9 rows × Customer/Prospect/Partner column groups, both measures populated (Total 9 accounts / 109,800,000); grand totals correct | +| cases_by_status_priority | summary + bar | 通过 | 22 rows (status × priority), both measures (Cases, Avg Resolution (h)) resolve; bar chart renders with populated axes | +| sla_performance | summary + column | 通过 | 4 priority rows + total, measures Cases / SLA Violation Rate / Avg Resolution (h) all resolve (closed-only scope → 0.0% rates, consistent with gauge); chart renders | +| cases_opened_by_day_priority | **matrix (day)** | 通过 | See #523 below — 30 day columns `2026-07-06`…`2026-08-04`, chronological, Total 38 = seeded case count | +| lead_inflow_by_month_source | **matrix (month)** | 通过 | See #523 below — month columns `2026-02`…`2026-08`, chronological; Total 20 (21 leads minus 1 never-contacted, excluded by `last_contacted_date ≠ null` filter — correct) | +| opportunities_by_stage | summary + bar | 通过 | 6 stage rows + total (5,065,000 · 68%); bar chart with labeled stage x-axis and 0–2M y-axis, no blank axes | +| won_opportunities_by_owner | summary + column | 通过 | Dev Admin · 1,290,000 + total row; column chart renders | +| pipeline_coverage_by_quarter | **matrix (quarter)** | 通过 | See #523 below — quarter columns `2026-Q3`, `2026-Q4`, ordered; per-cell amount+count, totals row 2,910,000+865,000=3,775,000 · 10 opps | +| opportunity_funnel_owner_stage | summary (2-level) + funnel | 通过 | 6 owner→stage rows + total; multi-level grouping works; funnel chart renders | +| customer_churn_signals | joined (3 blocks) | 通过 | Block 3 (Recently Lost) has data: Dev Admin · 75,000 · 1. Blocks 1–2 show explicit empty state "The dataset returned no rows for this report's scope" — **data-correct, not a failure**: REST shows all 9 accounts have `last_activity_date` ≥ 2026-07-15 (< 60 days ago), so the 60+/90+-day quiet populations are genuinely empty | + +Console/network per report: zero failing app requests and zero app console errors on all 10 (only the favicon-404 + Sentry tunnel noise documented above); all analytics queries 200. + +### #523 signal (matrix date-column granularity & ordering — acceptance) +**PASS.** All three date-columned matrix reports bucket properly and order chronologically — no "one column per raw date/timestamp" symptom: +- **lead_inflow_by_month_source**: columns are calendar months `2026-02, 2026-03, 2026-04, 2026-05, 2026-06, 2026-07, 2026-08` — exactly 7 columns for 20 contacted leads whose raw `last_contacted_date` values are distinct datetimes; multiple leads aggregate into shared month cells (e.g. 2026-05 total = 4). Screenshot `report_lead_inflow_by_month_source.png`. +- **pipeline_coverage_by_quarter**: columns are quarters `2026-Q3, 2026-Q4` (ordered), each carrying both measures; 10 open opportunities with distinct close dates collapse into 2 quarter columns. Screenshot `report_pipeline_coverage_by_quarter.png`. +- **cases_opened_by_day_priority**: columns are day buckets `2026-07-06` … `2026-08-04`, strictly chronological, with same-day cases aggregating into one column (grand-total row sums per day incl. 2/3-per-day cells; overall Total 38 = all seeded cases). Screenshot `report_cases_opened_by_day_priority.png`. + +--- + +## Observations (non-blocking) +- **O1**: `GET /assets/crm-favicon.ico` → 404 on app pages (console error noise; cosmetic). +- **O2**: Quota Attainment by Rep (sales_dashboard, `forecast_metrics`) shows owner as "—": seeded `crm_forecast` records have `owner_id` set but the `owner` field itself `null` (REST-verified), so the dimension label is empty. Data/seed modeling quirk, not a render failure; numbers are correct. +- **O3**: opportunities_by_stage chart shows the raw measure id `total_amount` as x-axis footer label instead of the friendly label — cosmetic. +- **O4**: crm_event has zero seeded records, leaving 7 of 13 sales_activity_dashboard widgets at 0/"No rows". If a populated activity dashboard is wanted for the demo, event seeds would need to be added (out of scope for this read-only run). + +## Summary +- Scope A: 5/5 dashboards 通过 (56 widgets checked; every widget renders; all zeros/empties REST-verified as data-correct). Global filters verified on 2 dashboards with before/after screenshots and captured query payloads. +- Scope B: 10/10 reports 通过; measures resolve, charts render with populated axes. +- **#520: PASS** — service_dashboard fully populated (30/7/45.0/3 over 38 seeded cases). +- **#523: PASS** — month/quarter/day matrix buckets proper and chronologically ordered. diff --git a/docs/acceptance-17/evidence/r3-i18n.md b/docs/acceptance-17/evidence/r3-i18n.md new file mode 100644 index 00000000..2e1bbcf6 --- /dev/null +++ b/docs/acceptance-17/evidence/r3-i18n.md @@ -0,0 +1,131 @@ +# R3 — i18n / locale acceptance, HotCRM 17.0.0-rc.2 + +Executor: R3. Read-only for business data — nothing created/updated/deleted; only the UI +locale was toggled (session-scoped) and it ends restored to `en` (`shots/zz-restored-en.png`). +Server: http://localhost:4001 (dev, untouched). Date: 2026-08-05. +Evidence: screenshots in `shots/`, structured text dumps in `run-log.json`, full lint output +in `lint-full.txt`, runner in `run.mjs`. + +## How the console switches locale (discovered, not guessed) + +1. UI switcher: top-right **avatar (DA) → PREFERENCES → Language** — a custom row (not a + `role=menuitem`) opening a list: English / 中文 / 日本語 / 한국어 / Deutsch / Français / + Español / Português / Русский / العربية. Found by dumping the avatar-dropdown contents + (`shots/explore-avatar-menu.png`, `shots/explore-lang-menu.png`). The row's own label + localizes (语言/言語/Idioma) once switched. +2. The console uses short codes; the server maps them to the app bundle: + `GET /api/v1/i18n/translations/zh` → `{"locale":"zh-CN","requestedLocale":"zh",…}`. + `GET /api/v1/i18n/locales` lists `en` (default), `zh-CN`, `ja-JP`, `es-ES` — exactly what + `/home/user/hotcrm/src/translations/crm.translation.ts` declares. +3. **The choice does not persist**: nothing is written to localStorage or + `sys_user_preference`; a fresh session (and any full page reload) requests + `/api/v1/i18n/translations/en` again (verified by network capture). The test runner + therefore switched once per locale and navigated exclusively client-side (sidebar/row + clicks); a `page.goto` mid-pass silently reverts the UI to English. +4. App strings (objects/fields/options/views/actions/nav/dashboards) come from the HotCRM + bundle; console chrome (Search box, New/Import/Edit/Cancel, validation scaffolding) + comes from the console's built-in translations. + +## Static baseline (src/translations/) + +- `en.ts` 951 lines; `zh-CN.ts` 1345 lines — zh is a **superset**: full `navigation` map + (all `nav_*` ids + `group_work/insights/approvals`), a `pages` section, **11 `_sections` + blocks**, case status/priority option sets, account CS fields, extra dashboard widgets. +- `ja-JP.ts` / `es-ES.ts` (~926 lines) mirror **en's shape, not zh's**: no `nav_*` labels, no + `pages`, only 1 `_sections` block, no case status/priority options, no account + tier/segment/health_score/renewal fields, missing action params (send_email, + create_campaign, mass_update_stage incl. its 7 stage options). +- `objectstack lint` with i18n checks ON (repo's `pnpm lint` passes `--skip-i18n`): + **723 warnings — 710 i18n: 355 for ja-JP + 355 for es-ES, 0 for zh-CN.** + Breakdown (both locales combined): 338 missing-option, 140 missing-field, + 68 missing-navigation, 50 missing-page, 44 missing-action, 26 missing-view, + 24 missing-widget, 20 missing-object. (Remaining 13 = duplicate activity-action name + warnings, not i18n.) + +## Matrix — check item × locale + +Verdicts: **T** translated · **EF** English-fallback · **MIX** mixed · (no raw-key leaks like +`objects.crm_account.label` were observed anywhere in the run). + +| Check item | en (baseline) | zh-CN | ja-JP | es-ES | +|---|---|---|---|---| +| ① Main nav (groups + items) | OK — `shots/en-1-nav.png` | **T** — all groups+items (`shots/zh-CN-1-nav.png`) | **MIX→EF** — groups 営業/活動 translated, "My Work/Insights/Approvals" + **all 23 item labels English** (`shots/ja-JP-1-nav.png`) | **MIX→EF** — Ventas/Actividad/Servicio/Marketing translated, "My Work/Insights/Approvals" + all items English (`shots/es-ES-1-nav.png`) | +| ② crm_account list: view tabs | OK — `shots/en-2-account-list.png` | **T** 全部客户/客户卡片/客户地图/企业客户 (`shots/zh-CN-2-account-list.png`) | **T** 全取引先/取引先カード/… (`shots/ja-JP-2-account-list.png`) | **T** Todas las Cuentas/… (`shots/es-ES-2-account-list.png`) | +| ② crm_account list: column headers | OK | **T** (all 8) | **MIX** — translated except **Health Score, Next Renewal Date** (missing fields); object description + Healthy/At Risk chips English | **MIX** — same two columns + chips + description English | +| ② crm_opportunity list: tabs + columns | OK — `shots/en-2-oppty-list.png` | **T** (`shots/zh-CN-2-oppty-list.png`) | **T** (`shots/ja-JP-2-oppty-list.png`) | **T** (`shots/es-ES-2-oppty-list.png`) | +| ③ Account detail: header + log_call/log_meeting/schedule_meeting | OK — `shots/en-3-account-detail.png` | **T** 记录通话/记录会议/安排会议; tabs+sections translated (`shots/zh-CN-3-account-detail.png`) | **MIX** — 3 action buttons translated; **tabs (Details/Related/Attachments) & section headers (Basic Information/Financials/Contact Information) + DISPLAY TITLE English** (`shots/ja-JP-3-account-detail.png`) | **MIX** — same pattern (`shots/es-ES-3-account-detail.png`) | +| ③ Case detail: header + close_case | OK — `shots/en-3-case-detail.png` | **T** 关闭工单/记录通话/编辑; status stepper, tabs, sections all translated (`shots/zh-CN-3-case-detail.png`) | **MIX** — ケースをクローズ/電話を記録 translated; tabs/sections + **status stepper values (New/In Progress/…) English** (`shots/ja-JP-3-case-detail.png`) | **MIX** — Cerrar Caso/Registrar llamada translated; same English leftovers (`shots/es-ES-3-case-detail.png`) | +| ④ crm_case New form, empty submit → validation | OK — `shots/en-4-validation.png` | **T** messages (主题不能为空 …) + toast; **but** form tabs Case/SLA/Resolution English (`shots/zh-CN-4-validation.png`) | **T** messages (件名は必須です …) + toast; status/priority **options English**; tabs English (`shots/ja-JP-4-validation.png`) | **T** messages (Asunto es obligatorio …) + toast; options English; tabs English (`shots/es-ES-4-validation.png`) | +| ⑤ log_call param dialog (opened, screenshotted, closed via Esc — never executed) | OK — `shots/en-5-param-dialog.png` | **MIX** — all 5 param labels + subtitle + 取消/确认 translated, **dialog title "Log a Call" English** (`shots/zh-CN-5-param-dialog.png`) | **MIX** — params translated, **title English** (`shots/ja-JP-5-param-dialog.png`) | **MIX** — params translated, **title English** (`shots/es-ES-5-param-dialog.png`) | + +Form was closed with Escape after capture; empty-submit writes nothing (server rejected +client-side; record count unchanged at 9 accounts / 10 opptys / same case list). + +## Matches known issue #NNN + +- **#661 (list-view tab labels hardcoded English, all non-en locales): NOT reproduced — + appears FIXED in rc.2.** View tabs are translated in zh/ja/es on both crm_account and + crm_opportunity (screenshots row ② above; also case-list tabs 全部工单/… in + `run-log.json`). Recommend closing after confirmation by the issue owner. +- **#645 (untranslated select option labels): CONFIRMED for ja-JP / es-ES, resolved for + zh-CN.** Visible instances: case Status/Priority dropdown options English in the ja/es + create form (`shots/ja-JP-4-validation.png`, `shots/es-ES-4-validation.png`), case detail + status stepper English (`shots/ja-JP-3-case-detail.png`, `shots/es-ES-3-case-detail.png`), + Health Score chips Healthy/At Risk English in ja/es lists. Current measure: lint reports + **169 missing option keys per locale** (ja, es; zh = 0) — substantially more than the 34 + recorded in the issue; scope has grown, not shrunk. +- **#494 (--skip-i18n hides warnings): CONFIRMED and count outdated.** `pnpm lint` still + passes `--skip-i18n`; running without it yields **710 i18n warnings** (355 ja + 355 es), + not the 205 recorded — the debt has grown. Additional blind spot found: lint has **no + check for `_sections`** — ja/es lack 10 of zh's 11 `_sections` blocks and visibly render + English section headers (row ③), with zero warnings emitted. + +## New findings (distinct from #661/#645/#494) + +1. **Action param-dialog title ignores the translated action label** — the page button is + localized (记录通话 / 電話を記録 / Registrar llamada) but the dialog it opens is titled + "Log a Call" in every non-en locale, while subtitle, all param labels and buttons are + localized. Evidence: `shots/zh-CN-5-param-dialog.png`, `shots/ja-JP-5-param-dialog.png`, + `shots/es-ES-5-param-dialog.png` (+ `run-log.json` `param_dialog`). Looks like the dialog + header resolves the raw action `label` instead of the `_actions..label` key. + Platform/console-level; affects every object action with params. +2. **Create-form section tabs are untranslatable and English in ALL locales, including the + otherwise-100 %-complete zh-CN** — "Case • / SLA / Resolution" on the New Case form + (`shots/zh-CN-4-validation.png`). These come from form/section metadata that has no + translation key path (zh's `_sections` translations cover the detail page, which renders + 工单信息/状态与SLA correctly, but not the create-form tab strip). +3. **Raw object API name leaks into a user-facing placeholder** — the Contact lookup on the + New Case form reads "Select crm_account first" / 请先选择crm_account / まず crm_account を + 選択してください / Seleccione primero crm_account in all four locales: the console + interpolates the object API name instead of its translated label. Evidence: all + `*-4a-case-new-form.png` / `*-4-validation.png`, `run-log.json` `form_buttons`. +4. **English validation toast joins field names with the CJK enumeration comma** — + "Please check the highlighted fields: Subject、Account、Status…" in the **en** locale + (`shots/en-4-validation.png`). Console `validation.formInvalid` joiner is hardcoded "、". +5. **Locale does not persist** — reload/new session always returns to `en`; no per-user + preference is stored (see "How the console switches locale" §3). Console-level UX gap; + makes every non-en locale effectively demo-only. +6. **Console chrome strings English in every non-en locale (zh included)**: view-mode + switcher Grid/Gallery/Kanban/Calendar/Timeline, "More actions", "Add reaction", "Clear", + "Close", footer aggregates "Sum:"/"Avg:". Evidence: `shots/zh-CN-2-account-list.png`, + `run-log.json` `case_list_toolbar`/`account_detail_buttons` per locale. +7. **Dashboard filter labels and stat-card captions hardcoded English in all locales incl. + zh** — "Owner:", "Lead Source:" filter chips (values 全部/すべて/Todo ARE translated) and + KPI sub-captions "Total Amount / Accounts / Contacts / Leads" + (`shots/zh-CN-1-nav.png`, `shots/ja-JP-1-nav.png`, `shots/es-ES-1-nav.png`). Widget titles + and descriptions themselves are correctly translated. +8. **es-ES validation grammar** — "Cuenta es obligatorio" (should inflect: *obligatoria*); + the console template concatenates without gender agreement (`shots/es-ES-4-validation.png`). + Minor, console-level. +9. **ja/es sidebar nav items all English** (34 missing `nav_*`/group keys each) and + **ja/es detail-page tabs/section headers English** — strictly these are part of the + known "es/ja lag zh-CN" debt measured under #494, but they are the two most visible + regressions for a Japanese/Spanish user and worth an explicit work item: ja-JP and es-ES + were authored against en.ts's shape and never received zh-CN's later additions + (navigation map, pages, `_sections`, CS fields, case status/priority options). + +## Cleanup / state + +- Locale restored to `en` (`shots/zz-restored-en.png`); choice is session-scoped anyway. +- No business data touched: the only form submissions were empty (rejected client-side, + nothing persisted); dialogs closed via Escape without executing any action. diff --git a/docs/acceptance-17/evidence/retest-playbook.md b/docs/acceptance-17/evidence/retest-playbook.md new file mode 100644 index 00000000..f695454c --- /dev/null +++ b/docs/acceptance-17/evidence/retest-playbook.md @@ -0,0 +1,424 @@ +# hotcrm 17.0 rc.2 acceptance — retest playbook + +Target: local hotcrm dev server at `http://localhost:4001`. + +## Common setup (auth) + +None of the ten issues states the auth endpoint explicitly. Assumed per task brief: + +```bash +BASE=http://localhost:4001 +curl -s -c /tmp/claude-0/-home-user/f9de7acc-06e5-5667-b535-06e82c336458/scratchpad/cookies.txt \ + -H 'Content-Type: application/json' \ + -X POST $BASE/api/v1/auth/login \ + -d '{"email":"admin@objectos.ai","password":"admin123"}' +# NEEDS DISCOVERY: verify this path (e.g. against server route list / login page network tab) +# before trusting 404s from the calls below. +CK="-b /tmp/claude-0/-home-user/f9de7acc-06e5-5667-b535-06e82c336458/scratchpad/cookies.txt" +``` + +All curl below assume `$BASE` and `$CK` are set. Record IDs (a case id, a lead id, an +opportunity id) must be fetched first from list endpoints (`/api/v1/data/` per +issue #510's description of the data path) — issues do not pin concrete IDs. + +--- + +## #508 — mass_update_stage is wired but cannot execute — every bulk-action invocation path is broken in console 16.1.0 + +**Labels:** bug, prio:p1, upstream:objectstack — **WRITES** (stage update on opportunities) + +Three invocation paths, all broken (verified live 2026-07-28 against 16.1.0 console): + +1. **Modal-path defect (REST-observable, RO probe):** + ```bash + curl -s $CK -o /dev/null -w '%{http_code}\n' \ + "$BASE/api/v1/meta/object/mass_update_stage" + ``` + Issue says the console resolves the action's `target` as an OBJECT name and this GET + returns **400** ("Error loading form"). This probe only mirrors the console's wrong call; + the real fix signal is in the UI paths below. +2. **Selection-bar bulk button — UI only.** Select rows on the opportunity list, click + "Mass Update Stage" in the selection bar. Broken = zero network requests + generic + "Action completed successfully" toast (pure client no-op). +3. **Header toolbar button — UI only.** Multi-select then click the list_toolbar button. + Broken = client-side rejection "This action runs on a single record — select exactly + one row"; `input.selectedIds` never delivered. + +**Direct REST invocation (the closure criterion from the issue):** +```bash +curl -s $CK -H 'Content-Type: application/json' \ + -X POST "$BASE/api/v1/actions/crm_opportunity/mass_update_stage" \ + -d '{"selectedIds":["",""],"params":{"stage":""}}' +# JSON body shape is not spelled out in the issue — selectedIds is the field named in it; +# param name for the stage needs discovery from the action metadata. +``` + +**Minimal Playwright outline (bulk-selection delivery cannot be tested via REST):** +open opportunity list → check 2+ row checkboxes → click "Mass Update Stage" in the +selection bar → assert a `POST /api/v1/actions/crm_opportunity/mass_update_stage` +request fires and its payload contains `selectedIds`. + +**FIXED:** selection-bar click POSTs `/api/v1/actions/crm_opportunity/mass_update_stage` +with `selectedIds`, AND the body's UPDATEs apply (stages actually change). Note the issue's +explicit second condition: sandbox-body UPDATEs are additionally blocked by sharing +middleware when execution context has no user identity ("FORBIDDEN: insufficient +privileges to update") — that must also be fixed (same defect as #521) for full closure. +**STILL BROKEN:** no network request on click / success toast with no request / FORBIDDEN +from the body. + +**Upstream refs:** none numbered in this issue (platform console/runtime defect, split +from hotcrm#491; app-side state lives on branch `claude/views-pages-dashboards-refs-850098` — +action now script-typed with `ctx.recordId` fallback). The FORBIDDEN half is +objectstack#3914 territory per #521. + +--- + +## #509 — Global (objectless) body actions are unreachable — runtime registers them under 'global' but the dispatcher only probes '*' + +**Labels:** bug, prio:p1, upstream:objectstack — **WRITES if fixed** (log_call/log_meeting create activity records); the broken-state probe is effectively RO + +> Note: the issue body was truncated in retrieval (cuts off inside the "Dispatch +> (`POST /api/v1/actions/…`)" section) and the issue has no comments. The dispatch half +> below is corroborated by #522's source-audit comment (same defect, 17.0-rc side). + +App ships two objectless actions: `log_call` and `log_meeting` (locations: +`record_header`, `list_item`, `record_related` — buttons render everywhere, as designed). +Runtime registration keys objectless actions under literal `'global'` +(`collectBundleActions` → `ql.registerAction`); the dispatcher probes a different key, so +dispatch never finds them. + +**Repro (REST):** +```bash +# Normal object-scoped dispatch of a global action — broken path +curl -s $CK -H 'Content-Type: application/json' \ + -X POST "$BASE/api/v1/actions/crm_case/log_call" \ + -d '{"recordId":"","params":{}}' + +# Registration-key probe — per #522's audit this ACCIDENTALLY works because it hits +# the literal 'global' registration key: +curl -s $CK -H 'Content-Type: application/json' \ + -X POST "$BASE/api/v1/actions/global/log_call" \ + -d '{"recordId":"","params":{}}' +``` + +**FIXED:** the first call executes the action body (inner `success:true`, activity/log +record written for the case). +**STILL BROKEN:** inner failure of the "not found" family (on 17.0-rc the exact shape is +documented in #522: HTTP 200, `{"success":true,"data":{"success":false,"error":"Action +'log_call' on object '*' not found"}}`), while the `/actions/global/log_call` probe +succeeds — proving the registration/dispatch key mismatch persists. + +**Upstream refs:** none visible in the retrievable text (split from hotcrm#491; +"upstream `@objectstack/runtime` bug"). The 17.0-rc twin #522 tracks it upstream as +objectstack#3913. + +--- + +## #510 — Analytics/dashboard query path resolves no user token — personal ("my …") dashboard widgets are impossible + +**Labels:** bug, prio:p1, upstream:objectstack — **RO** + +The list path (`/api/v1/data/...`) resolves `{current_user_id}`; the analytics path +(`/api/v1/analytics/...`) resolves NO user token — the literal string reaches the SQL +`WHERE`, matches nothing, widget shows 0. Empirical baseline from the issue: +`{current_user}` → 0, `{current_user_id}` → 0, no owner filter → 10,100,081. Date-macro +tokens DO resolve on the same path (so it is specifically user tokens). + +**Repro:** the issue names the path only as `/api/v1/analytics/...` — the exact +endpoint/verb/body **need discovery** (open any dashboard in the console and copy the +analytics request it makes, then vary the filter). Three-way comparison, same dataset +(crm_case / case_metrics): + +```bash +# 1) no owner filter -> expect large count +# 2) filter owner={current_user_id} -> broken: 0 ; fixed: >0, scoped to admin's rows +# 3) control via LIST path, which already resolves the token: +curl -s $CK "$BASE/api/v1/data/crm_case?filter=..." # e.g. the my_open_cases list view +``` + +**FIXED:** analytics response with `filter: { owner: '{current_user_id}', is_closed: false }` +returns the same rows the list-path "My Cases" view returns for admin (and the generated +SQL in the analytics response shows a substituted user id, not the literal token). Then, +per the issue's closure note: restore the widget on `service_dashboard` and retitle it +back to "My Open Cases by Priority". +**STILL BROKEN:** 0 rows with the token; literal `{current_user_id}` string visible in +the generated SQL. + +**Upstream refs:** none recorded — the crm.app.ts note says "Filed upstream" but no +issue link was ever captured; this hotcrm issue exists precisely to track that gap. + +--- + +## #520 — [17.0-rc] datetime 时间窗过滤返回空集(客服仪表盘全空) + +**Labels:** bug, prio:p1, upstream:objectstack — **RO** + +17.0 driver converts datetime comparison params to epoch-ms while the column stores ISO +text (`2026-07-26T00:00:00.000Z`) → comparison always false. Widget SQL: +`WHERE is_closed = ? AND created_date >= ? AND created_date <= ?`. `date`-typed fields +(close_date) are unaffected. Follow-up comment proved it also on 16.1.0 and found the rule +is directional: **`$lte` on a datetime matches 0 rows; `$gte` silently matches ALL rows** +(lower bound not applied). + +**Repro (analytics query on the `case_metrics` dataset the service_dashboard widgets use — +exact analytics endpoint needs the same discovery as #510; vary only the created_date filter):** + +| filter on `created_date` | broken (16.1/17.0-rc) | fixed | +|---|---|---| +| none | 38 rows | 38 | +| `$gte: 2026-05-01` only | 38 (unfiltered!) | rows actually ≥ bound | +| `$lte: 2026-07-30` only | **0** | ~38 | +| `$gte`+`$lte` last-30-days window | **0** | 29–30 (seed rows in window) | + +**SQL cross-check (definitive, RO):** +```bash +sqlite3 "SELECT typeof(created_date), count(*) FROM crm_case GROUP BY 1" +# broken & fixed: text|38 — column stays ISO text +sqlite3 "SELECT count(*) FROM crm_case WHERE is_closed=0 + AND created_date >= '2026-07-06T00:00:00.000Z' AND created_date <= '2026-08-05T23:59:59.999Z'" +# manual ISO binding returns rows either way; the platform query must now match it +``` +UI signal: service dashboard widgets non-zero with a `dateRange` on `created_date`. +Note: hotcrm #546 removed the `dateRange` block from `service_dashboard` on 16.1.0 and +added a CI guard failing any dashboard that windows a `datetime` field — to retest you +must re-add a dateRange (or query the analytics endpoint directly). + +**FIXED means BOTH upstreams landed:** window returns in-window rows, AND a bare +`YYYY-MM-DD` `$lte` upper bound does not drop same-day records created after 00:00. +**STILL BROKEN:** `$lte` half → 0 rows, or `$gte` half unfiltered, or same-day drop. + +**Upstream refs:** +- objectstack#3912 — this exact defect. **Closed as completed 2026-07-29, fix in the 17.0 train** → primary retest target for rc.2. +- objectstack#3777 — **open**, bug/priority:p1: bare-date `$lte` on datetime drops same-day records. Explicit precondition: #3912 alone does NOT make datetime windows safe. +- Lineage: introduced in 9.10.0 (objectstack#2034), detonated by 17.0 server-side placeholder resolution (objectstack#3582). +- Repro docs: branch `upgrade/objectstack-17`, `docs/upgrade-17/test-report.md` §3 P1. + +--- + +## #521 — [17.0-rc] script action 的 body 写库被 FORBIDDEN(admin 用户) + +**Labels:** bug, **prio:p0**, upstream:objectstack — **WRITES** (closes a case when fixed) + +Object-level script actions (close_case etc.) fail at execution: +`FORBIDDEN: insufficient privileges to update crm_case` — even as built-in admin. Audit +line claims `body executes TRUSTED (context-less engine, RLS/FLS-bypassing)` then +`[BodyRunner] sandboxed action threw ... FORBIDDEN`. Root cause (source-confirmed): +`ctx.api` falls back to a facade with no ExecutionContext/isSystem (body-runner.ts:161-184); +plugin-sharing `canEdit` rejects on `!context.userId` (sharing-service.ts:255); hook +bodies have an `isSystem:true` fallback (engine.ts:978-981) that action bodies lack. + +**Repro (issue path is UI: case detail → 关闭工单 → fill resolution → confirm; REST +equivalent of the same action dispatch):** +```bash +curl -s $CK -H 'Content-Type: application/json' \ + -X POST "$BASE/api/v1/actions/crm_case/close_case" \ + -d '{"recordId":"","params":{"resolution":"rc.2 acceptance retest"}}' +# param name for the resolution field needs discovery from the action metadata +``` + +**FIXED:** action succeeds (no FORBIDDEN anywhere in response or server log) and +`GET $BASE/api/v1/data/crm_case/` shows the case closed with the resolution set. +**STILL BROKEN:** red error in UI / response or `[BodyRunner]` server-log line containing +`FORBIDDEN: insufficient privileges to update crm_case`. +**Caution from the audit comment:** objects with public sharing or no owner field pass by +accident and mask the bug — retest on crm_case (owned + sharing-governed), not a +permissive object. + +**Upstream refs:** objectstack#3914 (filed 2026-07-29; no fix-status update in comments — +still pending as of last comment). Affects close_case / escalate_case / send_email / +mass_update_stage / create_campaign. Repro docs: `upgrade/objectstack-17` test-report §3 P2. + +--- + +## #522 — [17.0-rc] 全局(无 objectName)action 无法派发,且 UI 把失败当成功静默吞掉 + +**Labels:** bug, prio:p1, upstream:objectstack — **WRITES if fixed** (log_call writes an activity); broken-state probe effectively RO + +**Repro (exact, from issue):** +```bash +curl -s $CK -H 'Content-Type: application/json' \ + -X POST "$BASE/api/v1/actions/crm_case/log_call" \ + -d '{"recordId":"","params":{}}' +``` +Broken response — **HTTP 200** with: +```json +{"success":true,"data":{"success":false,"error":"Action 'log_call' on object '*' not found"}} +``` +Same for `export_csv`; flow-type actions through this endpoint give the same (misleading) +error. Registration-key probe (from the audit comment — accidentally works while broken): +`POST $BASE/api/v1/actions/global/log_call`. + +**FIXED (defect 1, dispatch):** inner `data.success:true` and the action body actually ran +(call logged). **STILL BROKEN:** inner `"not found on object '*'"`. +**FIXED (defect 2, envelope/UI):** a failing handler surfaces as an error to the user — +either a non-200 / outer `success:false`, or (console half, objectui#2958) the UI reads +the inner envelope and shows an error toast instead of silently closing. UI half needs a +browser check: trigger 全局 action from a record header while it still fails server-side +and assert an error is displayed (no silent modal close). + +**Upstream refs:** +- objectstack#3913 — runtime half: registration key `'global'` (app-plugin.ts:640-644) vs REST fallback lookup `'*'` (domains/actions.ts:195-204), exact-match engine (engine.ts:731-746); all handler failures wrapped into HTTP 200 via `deps.success()`. Filed 2026-07-29; no closure noted in comments. +- objectui#2958 — console only checks the outer envelope. Filed; no closure noted. +- Repro docs: `upgrade/objectstack-17` test-report §3 P3. + +--- + +## #524 — [17.0-rc] flow 型 action 菜单项无响应;screen flow 服务端失败无任何 UI 回显 + +**Labels:** bug, **prio:p0**, upstream:objectstack — **WRITES** (lead conversion creates account/contact/opportunity on success) + +Two phenomena; **phenomenon 1 is already RESOLVED CRM-side** (per 2026-07-30 comment), +issue kept open as regression-retest hook for phenomenon 2. + +**Phenomenon 1 (dead "Convert Lead" menu item) — retest is a regression check only:** +cause was legacy string `rowActions: ['convert_lead']` in the lead view (removed by #535, +merged via #537). Verified on 16.1.0: single 「转化线索」 entry, confirm dialog appears, +and network shows `POST /api/v1/automation/lead_conversion/trigger → 200 OK` with the +"Conversion Details" first screen rendering. objectui#2960 (string rowActions dispatched +as a type → silent no-op + green toast) still exists upstream but hotcrm no longer hits it. +- **REGRESSED:** an untranslated "Convert Lead" row-menu entry that clicks with zero + requests + green success toast. + +**Phenomenon 2 (screen-flow server failure with zero UI feedback) — UI-only, still blocked +on platform.** Original repro: lead list → row menu → 转化线索 → fill → Submit; server log +shows `Insert operation failed {"object":"crm_account","error":"industry must be one of: ..."}` +while the UI closes the modal with no error and lead status unchanged. +**Important caveat from the comments:** the original failure root cause (#531 industry +enum) was fixed via shared picklists (#490/#516), so the original path may no longer fail — +**you must construct another server-side failure** (any insert/validation failure inside +the flow) to retest the silent-swallow behavior. + +**Minimal Playwright outline:** +1. Arrange a flow step that will fail server-side (e.g. seed/point the conversion at data + violating a validation rule). +2. Lead list → row menu → 转化线索 → fill Conversion Details → Submit. +3. Capture server log confirming the failure occurred. +4. FIXED: UI shows an error (toast/inline), modal does not silently close, lead unchanged + is explained. STILL BROKEN: modal closes, no message, no state change, nothing in UI. + +**Upstream refs:** +- objectui#2958 — `useConsoleActionRuntime.tsx:479-494` treats `{success:false}` (no status/screen) as terminal success; open. +- objectstack#3915 — REST `/api/v1/actions` endpoint has no flow dispatch (MCP path does); open. +- objectstack#3913 — failures wrapped in 200 envelope (shared with #522); open. +- objectui#2960 — string rowActions path; still valid upstream but no longer needed for hotcrm. +- hotcrm #531 (industry enum, root cause of the original failure) — fixed by #490/#516. +- Repro docs: `upgrade/objectstack-17` test-report §3 P5. + +--- + +## #525 — [17.0-rc] 多 tab 新建表单校验失败后丢失已填值且布局错乱 + +**Labels:** bug, **prio:p0**, upstream:objectstack (assignee: yinlianghui) — **WRITES-intent** (attempts case create; while broken nothing persists — no cleanup needed unless fixed path completes a create) + +**UI-only by nature** — REST create works fine (issue states this explicitly); the defect +is the console multi-tab modal (each tab its own react-hook-form + `
` sharing a +formId; footer submits only the first form; Radix TabsContent without forceMount unmounts +and destroys values on tab switch — ModalForm.tsx:518 KNOWN LIMITATION). + +**Critical retest caveats from the comments (2026-07-31):** +- On current `main` (16.1.0, after #515) `description` (required) moved to the FIRST tab — + the "required fields spread across tabs" premise no longer exists there, and NONE of the + three phenomena reproduce on main. +- The original repro was made on `upgrade/objectstack-17` at `dd70aff0`, **79 commits + behind main** (its form lacked `description` entirely — #506). **Merge main into the + upgrade branch first, then retest**; phenomenon ① may vanish, leaving ②③ as the real + objectui scope. To exercise ②③ you need a form whose required fields genuinely span + tabs (or temporarily author one). + +**Minimal Playwright outline (17.0-rc console, post-merge):** +1. Case list → 新建 (Case / SLA / Resolution tabs). +2. Fill Case tab: subject=`MULTITAB-PROBE-RC2`, account, status, priority → Submit. +3. Assert error identifies the missing field AND its tab (① fixed) vs a bare + `description is required` with no tab hint (① broken). +4. Switch to Resolution, fill description, Submit. +5. FIXED: previously filled values retained (subject still `MULTITAB-PROBE-RC2`), single + remaining-field error, tabs render intact, messages localized (中文). + STILL BROKEN: `subject is required; description is required; status is required; + priority is required` (all values lost), then tab headers vanish / broken scrolling; + English engine messages. + +**Upstream refs:** +- objectui#2959 — the multi-tab form defect as filed; open. +- objectui#2153 — closed, but the explicit-sections path was NOT fixed in RC. +- objectstack#3918 — runtime dispatcher drops ValidationError `fields[]` and degrades to 500 (REST path unaffected); filed, no closure noted. +- Repro docs: `upgrade/objectstack-17` test-report §3 P6. + +--- + +## #526 — [17.0-rc] os migrate apply 对运行中服务正在使用的 SQLite 库缺少占用检测 + +**Labels:** bug, prio:p2, upstream:objectstack — **WRITES** (runs DDL against the dev DB; do this on a disposable copy/seed, never the shared dev DB) + +**CLI repro (no REST):** +1. Start the dev server so it holds the SQLite DB open. +2. Against the SAME database file, run `os migrate apply` including a rebuild-type change + (e.g. `replace_unique_index`). +3. Broken: no occupancy warning, no refusal — apply proceeds. Also (audit comment): + `runtime.start()` runs schema-sync DDL BEFORE the confirmation prompt. + +**FIXED:** CLI detects the DB is in use and warns or refuses unless `--force` (the +issue's requested behavior). **STILL BROKEN:** silent apply with server running. +Note the audit correction: the risk is stale prepared statements / SQLITE_BUSY, not file +inode replacement (replace_unique_index is pure index DDL; SQLite column rebuilds swap +tables inside the same file, sql-driver.ts:2816-2877) — data corruption was never actually +reproduced (preventive report). + +**Upstream refs:** objectstack#3917 (filed 2026-07-29 with corrected facts; no closure +noted). Repro docs: `upgrade/objectstack-17` test-report §3 P7. + +--- + +## #528 — [CRM] 9 个孤儿 __search 列待清理(17.0 收紧搜索伴生列供给条件后遗留) + +**Labels:** bug, metadata, prio:p2 (assignee: yinlianghui; state: reopened) — **RO** (plan/inspect ONLY — the original cleanup advice is RESCINDED) + +**⚠️ Verdict reversed in comments: DO NOT run `os migrate apply --allow-destructive`.** +The 9 `__search` columns (crm_competitor, crm_opportunity_line_item, crm_quote_line_item, +crm_task, sys_metadata, …) are NOT orphans — they are live pinyin-search companion columns +supplied by the dev runtime. `os migrate plan` misjudges them because the migrate CLI and +the dev runtime disagree on the schema view. Destructive apply would delete working search +columns (and dev would likely recreate them). Issue was auto-closed 2026-07-30 by a stray +`Closes #528` in a #542 commit message and manually reopened — the reopened state is +correct. `docs/MAINTENANCE.md` §3.1 (main) records this as a known false positive +("do not touch until #3955 lands"). + +**Retest (all read-only):** +```bash +# 1) plan against the live dev DB — broken: 9 objects flagged with DB-only __search columns +os migrate plan +# 2) column-level evidence: +sqlite3 "PRAGMA table_info(crm_task)" # broken: 34 cols incl. __search +# 3) control: a fresh DB created by migrate itself shows 33 cols, no __search, and plan +# reports 0 destructive (per the comment's two-fresh-DB experiment); a dev-runtime-built +# fresh DB shows 34 incl. __search. Also note the reported asymmetry: a brand-new DB +# immediately shows 3 relax_unique_index drift entries. +``` + +**FIXED (i.e. objectstack#3955 landed):** `os migrate plan` no longer flags the runtime's +`__search` companion columns as DB-only/destructive (CLI and runtime schema views agree), +and the fresh-DB asymmetry is gone. **STILL BROKEN:** plan still lists the 9 `__search` +columns as destructive drops. In no case apply the drop. + +**Upstream refs:** objectstack#3955 (open, blocking; includes the repro table and the +relax_unique_index asymmetry). Related process refs: hotcrm #542 (maintenance manual), +#526 (stop the server before any apply). Repro docs: `upgrade/objectstack-17` +test-report §4 L2. + +--- + +## Upstream fix-status summary (as claimed in the comments read above) + +| Upstream issue | Subject | Claimed status | +|---|---|---| +| objectstack#3912 | datetime filter params → epoch-ms vs ISO text (#520) | **Closed as completed 2026-07-29, fix in 17.0 train** — prime rc.2 retest | +| objectstack#3777 | bare-date `$lte` drops same-day datetime rows (#520 precondition) | **Open**, bug/priority:p1 | +| objectstack#3913 | global-action key mismatch + failures in 200 envelope (#509/#522/#524) | Filed, no closure noted | +| objectstack#3914 | action body ctx.api without user identity → FORBIDDEN (#521/#508) | Filed, no closure noted | +| objectstack#3915 | REST actions endpoint lacks flow dispatch (#524) | Filed, no closure noted | +| objectstack#3917 | migrate apply lacks in-use DB detection (#526) | Filed, no closure noted | +| objectstack#3918 | dispatcher drops ValidationError fields[], degrades to 500 (#525) | Filed, no closure noted | +| objectstack#3955 | migrate CLI vs dev runtime schema-view mismatch (#528) | **Open, blocking #528** | +| objectui#2958 | console reads only outer success envelope (#522/#524) | Filed, no closure noted | +| objectui#2959 | multi-tab modal form value loss/layout (#525) | Filed, no closure noted | +| objectui#2960 | string rowActions dispatched as type → silent no-op (#524) | Filed; hotcrm no longer affected (#535/#537) | +| objectui#2153 | earlier multi-tab issue | Closed, but explicit-sections path not fixed in RC | diff --git a/docs/acceptance-17/evidence/w1-crud.md b/docs/acceptance-17/evidence/w1-crud.md new file mode 100644 index 00000000..61d042e5 --- /dev/null +++ b/docs/acceptance-17/evidence/w1-crud.md @@ -0,0 +1,379 @@ +# RC1ACC-W1 — HotCRM 17.0.0-rc.2 acceptance, WRITE phase + +Executor: **W1**. Scope: full CRUD over all 17 CRM objects (REST primary, console UI secondary) plus +targeted checks C1–C6. + +- Server: `http://localhost:4001` (already running; never restarted). +- Evidence: `results.json`, `results_phase2.json` (every REST call with status + body), + `ui_forms_results.json`, `deletes.json`, `shots/*.png` (77 screenshots), `LEDGER.md`. +- Data isolation: every record prefixed `RC1ACC-W1-`. **0 leftovers**, **no seeded record touched**. + +--- + +## 1. Per-object CRUD matrix + +REST = `POST/GET/PATCH/DELETE /api/v1/data/`. "UI form" = console New form. + +| Object | POST | GET | PATCH | DELETE | UI form | Verdict | Evidence | +|---|---|---|---|---|---|---|---| +| crm_account | 201 | 200 | 200 | 200 | **201** | 通过 | `C0nPYZmo0VY1XZ1q`; UI create `xr_uuhWHBy20Yl-A`; 2nd UI attempt correctly 409 `UNIQUE_VIOLATION` on duplicate name | +| crm_contact | 201 | 200 | 200 | 200 | n/t | 通过 | `QpQV0XJFcR1WkU7A`; requires masterDetail parent `crm_account` (see NEW-4) | +| crm_lead | 201 | 200 | 200 | 200 | **failed** | 失败 (UI) / 通过 (REST) | REST `DAZbCqzw-wxQwV2T` clean; **UI New form cannot be submitted at all** → NEW-1 | +| crm_opportunity | 201 | 200 | 200 | 200 | **201** | 通过 | `x-OZsTliHqIUeSjh`; UI `M5zWCn0cfE7HgseX`; C1 + C4 both pass | +| crm_product | 201 | 200 | 200 | 200 | n/t | 通过 | `FC-tjJLj_3wvPLId` | +| crm_quote | 201 | 200 | 200 | 200 | n/t | 通过 | `61XYdhuseVfTikfL`; rollup verified (C5) | +| crm_contract | 201 | 200 | 200 | 200 | partial | 通过 (REST) / 阻塞 (UI) | `Jf7e0OOFRfm-g2gg`; UI create not completed — my driver failed to reach `Status` on the Parties tab. Validation + cross-tab behaviour fully captured (C2) | +| crm_case | 201 | 200 | 200 | 200 | **201** | 通过 | `XDtvf66Sf3ge5zZb`; UI `J2RCLvqD1bFUHv5w` | +| crm_campaign | 201 | 200 | 200 | 200 | n/t | 通过 | `KUTaiMnn-oYO9rcO` | +| crm_forecast | **400 → 201** | 200 | 200 | 200 | n/t | 通过 (with note) | First POST 400 `INVALID_FIELD` for `name` — correct, object has no `name` column (nameField is the `display_title` formula). Retry without `name` → 201 `HPj0OVb3sckgicFh`. See NEW-2 | +| crm_knowledge_article | 201 | 200 | 200 | 200 | n/t | 通过 | `eCH1gIWCmOpDDZZK` | +| crm_task | 201 | 200 | 200 | 200 | n/t | 通过 | `MI_idX5qwudf7xT2` | +| crm_event | 201 | 200 | 200 | 200 | **201** | 通过 | `5iaCvHN1DccFu0bG`; UI `jLfNMx5361zHbj_1`; C6 unblocks R1 | +| crm_event_attendee | 201 | 200 | 200 | 200 | n/t | 通过 | `oitrSpODtdT2xeLq` (EA-00001); C6 | +| crm_opportunity_line_item | 201 | 200 | 200 | 200 | n/t | 通过 | `AI0J0WMC_mc9h_5L`, `XJ1GzZGM-83omZBV`; rollup verified (C5) | +| crm_quote_line_item | 201 | 200 | 200 | 200 | n/t | 通过 | `N5F0MwLxnPObA1qC`; rollup verified (C5) | +| crm_campaign_member | 201 | 200 | 200 | 200 | n/t | 通过 | `_cDCxbMUxtUk716-` (CM-00001) | + +**REST layer: 17/17 objects pass full C-R-U-D.** No object failed a REST create, read, update or delete +(the single forecast 400 was a correct rejection of a field that does not exist). + +UI forms exercised: crm_account, crm_opportunity, crm_case, crm_event (all created successfully), +crm_lead (**blocked**, NEW-1), crm_contract (validation + tab behaviour captured; create not completed — +test-driver limitation, not a product failure). + +--- + +## 2. Targeted checks + +### C1 — opportunity hook derivation — 通过 + +| Step | Call | Result | +|---|---|---| +| Create `stage=proposal, amount=10000` | POST 201 | `probability=60`, `expected_revenue=6000`, `forecast_category=commit`, `stage_entry_date=2026-08-05` | +| Close **without** `win_reason` | PATCH **400** | `{"error":"Win Reason is required","code":"VALIDATION_FAILED","fields":[{"field":"win_reason","code":"required"}]}` | +| Close **with** `win_reason=best_fit` | PATCH 200 | `probability=100`, `expected_revenue=10000`, `forecast_category=closed`, `close_date=2026-08-05` (stamped) | + +All derivations correct. The e2e suite's claim that an unreasoned close is rejected is confirmed at the +REST layer. Probability tracks stage as the single source of truth; `close_date` is stamped on +`closed_won` as documented in `opportunity.hook.ts`. + +### C2 — multi-tab case form (#525 signal) — 通过, and the original repro is NOT constructible + +**Current crm_case New-form layout** (tabs from `role=tab`, fields by visibility per tab): + +| Tab | Fields | Required fields | +|---|---|---| +| **Case** (tab 1) | Case Number, Subject\*, Account\*, Contact, Status\*, Priority\*, Case Origin, Case Owner, Description\* | **all 5** | +| **SLA** (tab 2) | Created Date, First Response Date, SLA Due Date, Resolution Time (Hours), SLA Violated, Escalated, Escalation Reason, Parent Case | none | +| **Resolution** (tab 3) | Resolution, Internal Notes, Customer Satisfaction, Customer Feedback, Customer Signature, Closed Date, Is Closed | none | + +**Every required field lives on tab 1.** This confirms the retest playbook's note that #515 moved +`description` to tab 1. A cross-tab required-field failure therefore **cannot be constructed on +crm_case** on current main — stated explicitly as instructed, rather than forced. + +I still ran the value-preservation half of #525 on crm_case, and then found the **cross-tab layout does +still exist on crm_contract**, so I ran the real repro there: + +**crm_contract New form** — required fields genuinely split across two tabs: + +| Tab | Required fields | +|---|---| +| **Parties** (tab 1) | Account\*, Primary Contact\*, Status\* | +| **Terms** (tab 2) | Contract Term (Months)\*, Start Date\*, End Date\*, Contract Value\* | +| Signing & Documents | none | +| Notes | none | + +Experiment (screenshots `uif2-contract-before-fail` → `uif2-contract-survival`): +1. Filled `Account` + `Primary Contact` on **Parties**. +2. Filled `Special Terms` sentinel on **Notes**. +3. Submitted with the **Terms** tab required fields empty. +4. → Blocked client-side. **Zero network writes fired.** Errors: `Status is required`, + `Contract Term (Months) is required`, `Start Date is required`, `End Date is required`, + `Contract Value is required`. +5. Switched back to other tabs and re-read every value. + +**Answer to the #525 question: filled values on other tabs DO survive the failed submit — there is no +data loss.** +- `Special Terms` (text, Notes tab) = `"RC1ACC-W1-CROSSTAB-SENTINEL"` — survived. +- The same holds on crm_case: `Internal Notes` and `Resolution` both survived a failed submit + (`c2d-05-survival-check`). +- The underlying **lookup values also survived** — proven by the second submit, which reported *only* + `Status is required` and did **not** re-report `Account`/`Primary Contact`. + +One display-level caveat is recorded as NEW-5: the lookup **buttons** re-render showing `"Select..."` +after the failed submit even though the value is retained. + +### C3 — lead conversion — 通过 (rc.0 industry mismatch is FIXED) + +Ran `convert_lead` end-to-end in the UI twice. Flow: `Convert Lead` button → `Confirm Action` dialog → +`POST /api/v1/automation/lead_conversion/trigger` 200 (`status:"paused"`, screen node +`Conversion Details`) → screen submit → `.../runs//resume` 200. + +**Run 1** — lead industry `nonprofit` (label "Non-profit" — label/value divergence, exercises #626 +normalized-name matching), `createOpportunity` left false: + +| Assertion | Result | +|---|---| +| Lead marked converted | `status=converted`, `is_converted=true`, `converted_date=2026-08-05T02:55:55.774Z` | +| Account created | `XnswVU4p3NRlVnMg` "RC1ACC-W1-Navy Systems Corp", **`industry=nonprofit`** | +| Contact created | `wFyQmEQI49uFyp9v` "RC1ACC-W1-Grace Hopper" | +| Opportunity | not created (checkbox false) — correct for the input | +| Flow summary | `acted:5, skipped:3`, no errors in devserver.log | + +**Run 2** — lead industry `media` (label "Media & Entertainment"), `createOpportunity` **ticked**: + +| Assertion | Result | +|---|---| +| Lead marked converted | `is_converted=true` | +| Account | `T0zMC76xUMvcD--T` "RC1ACC-W1-Bletchley Analytics", **`industry=media`** | +| Contact | `DLYEv33yitW1UPf0` "RC1ACC-W1-Alan Turing" | +| **Opportunity** | `8uv9Mk4OrC4n_cmN` "RC1ACC-W1-Converted Opp", `amount=42000`, `stage=prospecting` | +| Flow summary | `acted:6, skipped:3` | + +**Industry mapping verdict: PASS.** Both `nonprofit` and `media` — values whose display labels differ +materially from the stored value — mapped cleanly from `crm_lead.industry` onto `crm_account.industry` +with no enum error. `crm_account` accepts the same `INDUSTRY_OPTIONS` picklist as `crm_lead`. The rc.0 +failure mode did not recur, and `devserver.log` contains no `lead_conversion` or industry errors. + +One new defect was found in the screen-flow UI along the way — see **NEW-6** (silent no-op submit). + +### C4 — kanban drag — 通过 (drag verifiable and working), but the contract board does not exist + +**Part 1 — there is no contract status board.** `crm_contract` offers exactly four visualizations in +the console: **Grid, Calendar, Gantt, Timeline** (`c4-01-contract-views`). `src/views/contract.view.ts` +declares `all_contracts`, `renewal_calendar`, `contract_gantt`, `contract_timeline` — no kanban. +`grep -rn "kanban" src/views/` shows boards only on **opportunity** (`pipeline_kanban`), **task** +(`task_board`), **quote** and **knowledge_article**. So the C4 premise is not constructible for +contracts on current main. Not a regression I can attribute — just an absent view. + +**Part 2 — drag itself is verifiable and works.** Ran the equivalent test on +`crm_opportunity/view/pipeline_kanban` (same dnd library, status-grouped board), dragging **my own** +card only — never a seeded record — with Playwright's real mouse API (`mouse.move` → `mouse.down` → +30 interpolated `mouse.move` steps → `mouse.up`). + +| Attempt | Target | PATCH fired? | Result | +|---|---|---|---| +| 1 | dropped over Closed Lost | **yes** | `PATCH /api/v1/data/crm_opportunity/M5zWCn0cfE7HgseX` **400** `{"error":"Loss Reason is required","code":"VALIDATION_FAILED"}` — server correctly refused an unreasoned close (mirrors C1) | +| 2 | Needs Analysis column | **yes** | `PATCH ...` **200** | + +Post-drag read-back of attempt 2: +`stage=needs_analysis`, `probability=40`, `expected_revenue=10000` (25000 × 40%), +`forecast_category=best_case`, `stage_entry_date=2026-08-05`. + +**Verdict: kanban drag-and-drop works, fires a real PATCH, and the opportunity hook re-derives every +dependent field correctly.** rc.0's observation stands: synthetic `dispatchEvent` is ignored by the dnd +library, but the real mouse API drives it correctly — so this **is** automatable, contrary to a +"not verifiable by automation" conclusion. + +### C5 — line items on parents (`controlled_by_parent`, #547 grants) — 通过 + +Both line-item objects create, read, update and delete cleanly with the parent lookup supplied +(`crm_opportunity`/`crm_quote` + `crm_product`). No permission error — #547's grants hold. + +**Quote rollup** (`quote_total_rollup`, afterInsert/afterUpdate/afterDelete): + +| Step | Parent quote | +|---|---| +| QLI created qty=2 × 500 | `subtotal=1000`, `total_price=1000`, `discount_amount=0` | +| QLI updated qty=2→4 | `subtotal=2000`, `total_price=2000` | + +**Opportunity rollup** (`opportunity_amount_rollup`) — tested on an **open** deal, since the hook +deliberately skips closed deals: + +| Step | Parent opportunity | +|---|---| +| Created `amount=1`, stage `qualification` | `amount=1` | +| OLI created qty=4 × 250 | `amount=1000`, `expected_revenue=250` | +| OLI updated qty=4→10 | `amount=2500`, `expected_revenue=625` | + +Both rollups are declared `async: true`, so a read issued immediately after the write can observe the +pre-rollup value; re-reading after a short delay shows the settled figure. That is documented intent, +not a defect — worth knowing for other executors writing assertions. + +Adding a line item to a **closed** opportunity correctly leaves `amount` untouched (the hook's +"a closed deal's value is settled" guard) — observed and confirmed against the hook source. + +### C6 — crm_event / crm_event_attendee — 通过 (R1's 阻塞 is now unblocked) + +Both objects had **zero** rows at start (confirms #671). Created via REST: +- `crm_event` `5iaCvHN1DccFu0bG` "RC1ACC-W1-Discovery Call" (POST 201) +- `crm_event_attendee` `oitrSpODtdT2xeLq` EA-00001 (POST 201), linked to the event + a contact + +Both **detail pages render correctly**: +- `shots/c6-02-event-detail.png` — title, Event Type / Status / Start / Assigned To highlight strip, + a working status path control (Planned → Held → Cancelled → No Show), Details + Related(1) tabs, + Event Information, Schedule, Related Records (resolved to "ACC-000002 - RC1ACC-W1-Acme Industrial"), + Owner, and the Discussion feed. +- `shots/c6-04-attendee-detail.png` — EA-00001, Event / Attendee Type / Response / Organizer strip, + Attendee panel with the contact resolved to "RC1ACC-W1-Ada Lovelace", and a Discussion feed showing + the field-history entry `Response: No Response → Accepted`. + +List pages for both objects also render (`c6-01`, `c6-03`). A `crm_event` record was additionally +created through the console **New form** (`jLfNMx5361zHbj_1`, POST 201) — the event UI form works. + +**R1 should re-run its event/event_attendee detail-page checks: the pages are sound; the blocker was +purely the absence of seed rows (#671), not a page defect.** Note both objects are back to 0 rows after +my cleanup, so R1 will need to create a row again (or #671 should be fixed with seeds). + +--- + +## 3. Suspected NEW findings + +### NEW-1 — `visibleOn` is ignored on console form fields, making crm_lead impossible to create in the UI — **HIGH** + +**Reproduction** +1. Console → HotCRM → Leads → **New**. +2. Fill First Name, Last Name, Company, Email; set Status = New. +3. Click **Create**. + +**Observed** — submit is blocked with five errors for fields that should not apply to a brand-new lead: +`Disqualification Reason is required`, `Duplicate Of is required`, `Duplicate Of Lead is required`, +`Duplicate Of Contact is required`, `Duplicate Status is required`. All five render on the form with a +red required asterisk. Screenshot: `shots/uif-crm_lead-02-after-submit.png`. Zero network writes fire. +The form even prints its own helper text **"Required when status is Unqualified"** directly above the +error **"Disqualification Reason is required"** — self-contradictory on screen. + +**There is no way to satisfy the form**: it demands the new lead be declared a duplicate of *both* an +existing lead *and* an existing contact simultaneously — mutually exclusive by design +(`duplicate_of_lead` and `duplicate_of_contact` are gated on `duplicate_of_type` being one or the other). +**crm_lead cannot be created through the console at all.** REST creation is unaffected (201). + +**Attribution: PLATFORM (console form renderer).** The CRM metadata is correct. Every one of the five +fields is declared in `src/views/lead.view.ts` with a `visibleOn` predicate that is false for a new lead: + +```ts +// src/views/lead.view.ts +{ field: 'disqualification_reason', required: true, visibleOn: 'status == "unqualified"' }, +// DUPLICATE_LINK_FIELDS: +{ field: 'duplicate_of_type', required: true, visibleOn: 'disqualification_reason == "duplicate"' }, +{ field: 'duplicate_of_lead', required: true, visibleOn: 'duplicate_of_type == "crm_lead"' }, +{ field: 'duplicate_of_contact', required: true, visibleOn: 'duplicate_of_type == "crm_contact"' }, +{ field: 'duplicate_status', required: true, visibleOn: 'disqualification_reason == "duplicate"' }, +``` + +The renderer displays these fields when their `visibleOn` is false **and** enforces their `required: true`. +The object-level counterparts use `requiredWhen` predicates (`lead.object.ts:369,375`) and are correctly +*not* enforced by the server. So: server honours the condition, form does not. + +Likely the single highest-impact UI defect in this run — it blocks the primary "new lead" path, which is +the app's headline demo flow. + +### NEW-2 — `crm_forecast.display_title` (the object's own `nameField`) evaluates to `null` for every row — **MEDIUM** + +**Reproduction** +``` +GET /api/v1/data/crm_forecast +``` +Every row — my created row **and all 8 seeded rows** — returns `display_title: null`, while +`period_label` and `period_start` are both populated: +``` +{'id':'-kN4v3de7J32S1B0','period_label':'Q3 2026','period_start':'2026-07-01','display_title':None} +{'id':'CJpIhxXM5hq7-Dbq','period_label':'Aug 2026','period_start':'2026-08-01','display_title':None} +``` +Mine: `period_label='RC1ACC-W1-Q3 2026'`, `period_start='2026-08-05'` → `display_title=None`. + +`forecast.object.ts` sets `nameField: 'display_title'` with +``expression: F`record.period_label + " (" + text(record.period_start) + ")"` ``. + +**Contrast** — other formula fields evaluate fine: +`crm_contact.full_name` = `"RC1ACC-W1-Ada Lovelace"`, `crm_lead.full_name` = `"RC1ACC-W1-Grace Hopper"`, +`crm_opportunity.days_in_stage` = `0`. + +The distinguishing feature of the failing expression is the **`text()` coercion of a `date` field**; +the working ones concatenate text or compute numerics. Suspected: `text()` over a date returns +null/undefined and poisons the whole concatenation. + +**Attribution: likely PLATFORM (formula engine `text()` on a date operand).** Consequence is +user-visible — forecast records have no resolvable record title anywhere the nameField is used. +Not fully isolated; a one-line formula unit test would settle it. + +### NEW-3 — misleading error text and object name when a cascade-blocked delete fails — **LOW** + +**Reproduction** (converted-lead chain still intact) +1. `DELETE /api/v1/data/crm_opportunity/` → + **400** `"Cannot edit a converted lead (attempted: converted_opportunity). Make changes on the converted record…"` + — deleting an *opportunity* reports a *lead* edit error. +2. `DELETE /api/v1/data/crm_account/` → + **400** `"Cannot delete contact: still referenced by 1 open opportunity(ies), 0 active quote(s), 0 active contract(s)"` + — deleting an **account** reports **"Cannot delete contact"**. The object name in the message is + simply wrong (it is the cascade child's guard message surfacing unmodified). + +All three succeeded on retry once the chain was torn down in the right order, so this is cosmetic — +but the account message names the wrong object, which will send people down the wrong path. +**Attribution: CRM (hook/guard messages in `lead.hook.ts` / `contact.hook.ts`) surfaced verbatim by the +platform's cascade layer.** + +### NEW-4 — missing required master-detail parent returns 403 PERMISSION_DENIED, not a 400 required-field error — **LOW** + +**Reproduction** +``` +POST /api/v1/data/crm_contact {"first_name":"X","last_name":"Y","email":"x@example.com"} +``` +→ **403** `{"error":"[Security] Access denied: insert on 'crm_contact' requires edit access to its master record (detail record has no master reference)","code":"PERMISSION_DENIED"}` + +`crm_contact.crm_account` is `Field.masterDetail('crm_account', { required: true, … })`, so the record is +genuinely invalid — but the response presents a **validation** problem as a **security** problem. +Every other missing-required-field case in this run returned a clean +`400 VALIDATION_FAILED` with a `fields[]` array (e.g. win_reason in C1, `name` on forecast). +A UI or integration mapping 403 → "you lack permission" will mislead the user. +**Attribution: PLATFORM (master-detail insert guard runs before required-field validation).** + +### NEW-5 — lookup fields re-render as "Select..." after a failed form submit although the value is retained — **LOW/MEDIUM (cosmetic but alarming)** + +**Reproduction** +1. Console → Contracts → **New**. +2. On **Parties**, pick Account and Primary Contact via the lookup pickers. +3. Fill something on another tab; submit with the **Terms** tab required fields empty → validation fails. +4. Return to **Parties**. + +**Observed** — both lookup buttons display `"Select..."` as if cleared +(`shots/uif2-contract-survival.png`), while plain text fields on other tabs keep their values. + +**The data is NOT lost** — the very next submit reported only `Status is required` and did **not** +re-report `Account`/`Primary Contact`, proving the form state still held both references. So this is a +**display regression in the lookup control after re-mount**, not data loss. It nonetheless reads as data +loss to a user, who will re-pick the values. +**Attribution: PLATFORM (console lookup widget loses its display label on re-mount; value survives).** + +### NEW-6 — screen-flow submit is a silent no-op when a conditionally-revealed required field is empty — **MEDIUM** + +**Reproduction** +1. Open any qualified lead → **Convert Lead** → **Continue**. +2. On the `Conversion Details` screen, tick **Create Opportunity?**. This reveals two new fields: + **Opportunity Name \*** (required) and **Opportunity Amount**. +3. Leave **Opportunity Name** empty and click **Submit**. + +**Observed** — nothing happens. Measured over 15 s: **0 `resume` requests**, **0 visible error messages** +(`c3f-03-silent-noop.png`), dialog stays open, no field highlight, no toast. The user has no indication +why Submit does nothing. Filling Opportunity Name and resubmitting works immediately +(`POST .../runs//resume` **200**, `acted:6`). + +This cost real diagnosis time — my first two conversion attempts appeared to "hang" and the lead stayed +unconverted with no error anywhere, including the server log. +**Attribution: PLATFORM (screen-flow renderer suppresses required-field validation feedback).** +Distinct from NEW-1: here the conditional reveal works correctly, but the validation message is missing. + +--- + +## 4. Known open issues — behaviour observed + +| Issue | Match? | Note | +|---|---|---| +| **#620** `owner` `cel os.user.id` default fails to evaluate | matches | Warnings present throughout the run; `owner` still resolved to the acting user on every object I created, so no functional impact observed in this phase | +| **#617** hooks run over seed rows | matches | `case_escalation` `runAs:'user'` REFUSED errors are in the log from boot, unrelated to my writes | +| **#650** decision-node singular condition inert | not exercised | The lead_conversion flow's decision path behaved correctly for both branches (opportunity created only when requested) | +| **#664** address fields render as raw JSON | matches | `billing_address` returns as a JSON blob; contract form exposes flattened Street/City/State/ZIP/Country alongside a "Billing Address" field | +| **#671** crm_event has no seeds | **confirmed** | `crm_event` and `crm_event_attendee` both had 0 rows at start. C6 created rows, proved the pages work, then removed them — **so the condition is back**. Recommend seeding both objects | + +Not re-filed, per instructions. + +## 5. Notes for other executors + +- Account **name uniqueness IS enforced** (`409 UNIQUE_VIOLATION` on a duplicate name), despite the + comment in `account.object.ts` saying `account_name_unique` was removed in 7.6 — a normalized-name + unique index (`name_normalized`) appears to still be live. Behaviour is correct; only the comment is stale. +- Both line-item rollups are **async** — do not assert parent totals in the same tick as the child write. +- The console SPA needs ~8–10 s on first navigation; shorter waits produce a false + "no New button on the list page" reading. I hit this and it was a test artifact, not a defect. +- The console form dialog is itself `[role="dialog"]`; lookup pickers open as a **second** dialog. + Target `.last()`. diff --git a/docs/acceptance-17/evidence/w1-ledger.md b/docs/acceptance-17/evidence/w1-ledger.md new file mode 100644 index 00000000..7ba90753 --- /dev/null +++ b/docs/acceptance-17/evidence/w1-ledger.md @@ -0,0 +1,56 @@ +# RC1ACC-W1 Data Ledger + +All records created by W1 during the 17.0.0-rc.2 acceptance run. **All deleted; 0 leftovers verified.** +Final verification scan: `LEFTOVER RC1ACC-W1 RECORDS: 0` across all 17 objects. + +| # | Object | ID | Name / label | DELETE | +|---|--------|----|--------------|--------| +| 1 | crm_account | C0nPYZmo0VY1XZ1q | RC1ACC-W1-Acme Industrial | 200 | +| 2 | crm_account | xr_uuhWHBy20Yl-A | RC1ACC-W1-UI Account (UI form) | 200 | +| 3 | crm_account | XnswVU4p3NRlVnMg | RC1ACC-W1-Navy Systems Corp (C3 convert) | 200 | +| 4 | crm_account | T0zMC76xUMvcD--T | RC1ACC-W1-Bletchley Analytics (C3 convert) | 400 → **200 on retry** | +| 5 | crm_contact | QpQV0XJFcR1WkU7A | RC1ACC-W1-Ada Lovelace | 200 | +| 6 | crm_contact | wFyQmEQI49uFyp9v | RC1ACC-W1-Grace Hopper (C3 convert) | 200 | +| 7 | crm_contact | DLYEv33yitW1UPf0 | RC1ACC-W1-Alan Turing (C3 convert) | 400 → **200 on retry** | +| 8 | crm_lead | DAZbCqzw-wxQwV2T | RC1ACC-W1-Grace Hopper | 200 | +| 9 | crm_lead | Vdevm-f8EtXaVTff | RC1ACC-W1-Alan Turing | 200 | +| 10 | crm_opportunity | x-OZsTliHqIUeSjh | RC1ACC-W1-Platform Deal (C1) | 200 | +| 11 | crm_opportunity | f1RWTRo2X-T6SF8O | RC1ACC-W1-Open Rollup Deal (C5) | 200 | +| 12 | crm_opportunity | M5zWCn0cfE7HgseX | RC1ACC-W1-UI Opp (UI form + C4 drag) | 200 | +| 13 | crm_opportunity | OYGW35Bu4tPbjLaX | RC1ACC-W1-UI Opp (dup from timed-out run) | 200 | +| 14 | crm_opportunity | 8uv9Mk4OrC4n_cmN | RC1ACC-W1-Converted Opp (C3) | 400 → **200 on retry** | +| 15 | crm_product | FC-tjJLj_3wvPLId | RC1ACC-W1-Enterprise Seat | 200 | +| 16 | crm_quote | 61XYdhuseVfTikfL | RC1ACC-W1-Q1 Quote | 200 | +| 17 | crm_contract | Jf7e0OOFRfm-g2gg | CTR-0001 (RC1ACC-W1-Master Subscription) | 200 | +| 18 | crm_case | XDtvf66Sf3ge5zZb | RC1ACC-W1-Login failure | 200 | +| 19 | crm_case | J2RCLvqD1bFUHv5w | RC1ACC-W1-UI Case (UI form) | 200 | +| 20 | crm_campaign | KUTaiMnn-oYO9rcO | RC1ACC-W1-Summer Webinar | 200 | +| 21 | crm_forecast | HPj0OVb3sckgicFh | RC1ACC-W1-Q3 2026 | 200 | +| 22 | crm_knowledge_article | eCH1gIWCmOpDDZZK | RC1ACC-W1-How to reset SSO | 200 | +| 23 | crm_task | MI_idX5qwudf7xT2 | RC1ACC-W1-Follow up call | 200 | +| 24 | crm_event | 5iaCvHN1DccFu0bG | RC1ACC-W1-Discovery Call (C6) | 200 | +| 25 | crm_event | jLfNMx5361zHbj_1 | RC1ACC-W1-UI Event (UI form) | 200 | +| 26 | crm_event_attendee | oitrSpODtdT2xeLq | EA-00001 (C6) | 200 | +| 27 | crm_opportunity_line_item | AI0J0WMC_mc9h_5L | RC1ACC-W1-OLI | 200 | +| 28 | crm_opportunity_line_item | XJ1GzZGM-83omZBV | RC1ACC-W1-OLI2 (C5) | 200 | +| 29 | crm_quote_line_item | N5F0MwLxnPObA1qC | RC1ACC-W1-QLI | 200 | +| 30 | crm_campaign_member | _cDCxbMUxtUk716- | CM-00001 | 200 | + +## Hook/flow side-effect records (created BY my test data, also removed) + +| Object | ID | Subject | DELETE | +|--------|----|---------|--------| +| crm_task | eLuXDag-mtdP2ZSp | Follow up with qualified lead (DAZbCqzw-wxQwV2T) | 200 | +| crm_task | EqLaX3OvzpmCpuH3 | Escalated case XDtvf66Sf3ge5zZb needs attention | 200 | +| crm_task | zZtM2WoDw0O1RpGA | Activate new customer for opportunity x-OZsTliHqIUeSjh | 200 | + +Also deleted: one throwaway probe account `7KfnnI5MSmeMOU2X` (RC1ACC-W1-Probe), DELETE 200. + +**No seeded record was modified or deleted at any point.** + +## Delete-ordering note + +Three deletes returned 400 on the first pass purely because of ordering — the converted-lead chain +(lead → account/contact/opportunity) must be torn down opportunity-last-but-lead-first. All three +succeeded on retry once the converted lead row was gone. See NEW-3 in RESULTS.md for the misleading +error text this surfaces. diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index bba5201e..844fb883 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -7,7 +7,7 @@ - **被测代码**:`main` @ 5a78f88(`feat(activity): first-class events…` #670) - **环境**:远程沙箱,`objectstack dev -p 4001`,SQLite `.objectstack/data`(全新种子); 登录 `admin@objectos.ai`,鉴权走 `POST /api/v1/auth/sign-in/email` → Bearer token -- **测试计划**:[test-plan.md](./test-plan.md) +- **测试计划**:[test-plan.md](./test-plan.md);**接手请先读 [HANDOFF.md](./HANDOFF.md)**;原始记录见 [evidence/](./evidence/) - **纪律**:只测不修 —— 未改动任何 `src/` 代码;新发现逐条立 issue;测试数据统一 `RC1ACC-` 前缀 ## 结论(TL;DR) @@ -56,13 +56,13 @@ validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风 | 1. 14 对象列表 + 详情 | ✅ 12 通过 / 2 阻塞 | 见 §3 | | 1. 3 子对象(经父相关列表) | ✅ 通过 | 见 §3 | | 2. 17 对象 CRUD | ✅ REST 17/17 通过 / ❌ UI 1 项失败 | 见 §6;`crm_lead` UI 新建完全不可用(#688) | -| 3. 26 actions | *(填充中)* | | +| 3. 26 actions | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P2 包 | | 4. 5 仪表盘 | ✅ 通过 | 56 widget 全渲染出数,见 §4 | | 4. 10 报表 | ✅ 通过 | 见 §4 | -| 5. 24 flows | *(填充中)* | | -| 6. Profile 权限矩阵 | *(填充中)* | | +| 5. 24 flows | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P3 包(含 #684 关键待验点) | +| 6. Profile 权限矩阵 | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P4 包 | | 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | -| 8. 阻塞 issue 复测 | *(填充中)* | 已确认解除:#520、#523,见 §7 | +| 8. 阻塞 issue 复测 | ⚠ **部分完成** | 已解除:#520、#523(见 §7);#521 #522 #509 #508 #524 #510 #526 #528 **未复测** | ## 3. 对象:列表与详情(执行者 R1) @@ -174,7 +174,8 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 ## 7. 平台阻塞 issue 复测 -> 本节在复测阶段完成后定稿。已取得的确认: +> ⚠ 本节 10 条中**仅完成 2 条**,其余 8 条(#521 #522 #509 #508 #524 #510 #526 #528) +> **未复测**,见 §10。已取得的确认: - **#520(datetime 时间窗过滤返回空集)—— 已解除。** rc.0 下客服仪表盘全空;rc.2 下 38 条种子工单全部出数:未结 30 / 紧急 7 / 平均解决 45.0h / SLA 违约 3,30 天日粒度 From 23b8c7cd9b5d02c4d8894e2a08747b5a305737f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 07:25:11 +0000 Subject: [PATCH 07/11] =?UTF-8?q?docs(acceptance):=20record=20the=20#526?= =?UTF-8?q?=20and=20#528=20retest=20verdicts=20=E2=80=94=20both=20lifted?= =?UTF-8?q?=20on=20rc.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Isolated-environment retest (own port, own DB file, nothing shared at risk): migrate now detects an occupied database at plan time, refuses apply before any DDL with a --force escape hatch, and demands --yes even when free — the rc.0 confirmation-less DDL behaviour is gone. On a fresh rc.2 database all five __search columns are live and plan reports in-sync, with a planted junk column proving the orphan detector still fires; the orphan condition this issue tracked only exists on rc.0-era databases. One residual recorded on the issue rather than filed: the occupied-DB refusal exits 0, which scripting would misread as success. Refs #526 #528 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index 844fb883..59e818d6 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -62,7 +62,7 @@ validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风 | 5. 24 flows | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P3 包(含 #684 关键待验点) | | 6. Profile 权限矩阵 | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P4 包 | | 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | -| 8. 阻塞 issue 复测 | ⚠ **部分完成** | 已解除:#520、#523(见 §7);#521 #522 #509 #508 #524 #510 #526 #528 **未复测** | +| 8. 阻塞 issue 复测 | ⚠ 进行中 4/10 | 已解除:#520 #523 #526 #528(见 §7);#521 #522 #509 #508 #524 #510 复测执行中 | ## 3. 对象:列表与详情(执行者 R1) @@ -174,8 +174,8 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 ## 7. 平台阻塞 issue 复测 -> ⚠ 本节 10 条中**仅完成 2 条**,其余 8 条(#521 #522 #509 #508 #524 #510 #526 #528) -> **未复测**,见 §10。已取得的确认: +> 进度:10 条中 **4 条已有判定**(#520 #523 #526 #528),其余 6 条 +> (#521 #522 #509 #508 #524 #510)复测执行中。已取得的确认: - **#520(datetime 时间窗过滤返回空集)—— 已解除。** rc.0 下客服仪表盘全空;rc.2 下 38 条种子工单全部出数:未结 30 / 紧急 7 / 平均解决 45.0h / SLA 违约 3,30 天日粒度 @@ -184,6 +184,15 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 (2026-02…2026-08),`pipeline_coverage_by_quarter` 按季(2026-Q3/Q4), `cases_opened_by_day_priority` 按日(2026-07-06…2026-08-04),均按时间正序且同期记录 聚合入同一列,rc.0 的"逐原始日期爆列"现象消失。 +- **#526(migrate 对占用中的库缺少保护)—— 已解除。** 隔离环境(4009 端口独立库)全链验证: + ① `plan` 对运行中的库**预警占用**(报出持有 pid 与 wal/shm 证据);② `apply --allow-destructive` + 在任何 DDL 前**拒绝执行**并提示 `--force` 逃生门,已植入的漂移列未被触碰;③ 即便服务已停, + apply 也要求显式 `--yes` 确认 —— rc.0 的"确认前即发 DDL"行为消失;④ 停服 + `--yes` 后正确 + 应用并删除漂移列。残留小缺口:占用拒绝的退出码为 0(脚本会误判成功),已在 issue 评论中记录。 +- **#528(孤儿 `__search` 列)—— 新库上条件已不存在。** 全新 rc.2 库 5 个 `__search` 列 + 全部活跃,`migrate plan` 对运行中的库报 in-sync、零孤儿(对照:故意植入的垃圾列**被**正确 + 标记为 destructive,证明检测器本身工作正常)。上游 #3955 修复确认;issue 里的"9 个孤儿" + 仅存在于 rc.0 时代的旧库,建议按"仅限遗留库"重新界定或关闭。 ## 8. CRM 侧遗留与新发现 From fbcda76632e6f079a233e9898fdbdd1c8153b67d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 08:04:22 +0000 Subject: [PATCH 08/11] docs(acceptance): record the full action sweep and all ten blocker verdicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The relaunched action executor covered all 26 actions: 25 pass with verified data artifacts, the one failure being mass_update_stage. Blocker retests are now 10/10 — nine lifted (including all three rc.0 upgrade blockers), #508 still reproducing on every invocation path though now honestly. All verdicts are commented on their issues with evidence. Also records the quantified #548 evidence (ctx.api inserts never stamp owner_id, making any record with a logged activity undeletable for everyone), files #698 (case autonumber lag + burn-on-failure), and retracts W1's silent-no-op finding after W2B could not reproduce it — the retraction and its reasoning live in the honest-record section. Refs #508 #509 #510 #520 #521 #522 #523 #524 #526 #528 #548 #673 #682 #684 #688 #696 #698 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 117 ++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 29 deletions(-) diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index 59e818d6..198fa080 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -12,12 +12,12 @@ ## 结论(TL;DR) -> ⚠ **本轮为部分完成**。计划 8 节中 5 节已执行完毕,3 节(actions 全量、flows 全量、 -> profile 权限矩阵)因会话额度耗尽**未执行**。已完成部分的结论如下,未完成部分见 §10。 +> 进度:计划 8 节中 **7 节已执行**(第 5 节 flows 与第 6 节权限矩阵执行中,见 §10)。 -- **平台侧:rc.0 的三个升级阻断项已解除两个,第三个未验。** #520(datetime 时间窗过滤空集) - 与 #523(矩阵报表日期粒度)在 rc.2 上均取得确凿解除证据;#521/#522/#508/#509/#510 - 的复测尚未执行。 +- **平台侧:阻塞 issue 复测 10/10 完成 —— 9 条解除、1 条仍复现。** rc.0 的三个升级阻断项 + (#520 datetime 过滤、#521 action 写库 FORBIDDEN、#522 全局 action 派发)**全部解除**; + 仍复现的是 #508(批量 action 无任何可用调用路径,平台选择传递 + CRM body 签名两半都缺, + 但 rc.0 的假成功已消失、所有失败可见)。 - **基线全绿**:`pnpm verify` 退出码 0(vitest 1280 通过)、Playwright e2e 16/16、 冷启动 0 ERROR。 - **数据层健康**:REST 层 **17/17 对象全量 CRUD 通过**,商机 hook 派生、行项目 rollup、 @@ -28,11 +28,15 @@ 为数据本身如此,而非查询缺陷。 - **i18n 分化明显**:zh-CN 完整;ja-JP / es-ES 各缺 355 条,导航与详情页分节标题仍是英文; 四语言的校验文案均已正确翻译。 -- **本轮共立 13 个新 issue**(#680–#684、#688–#693、#696),另给 5 个既有 issue - 回填 rc.2 实测数据(#494/#525/#645/#661 及 #684 关联的 #507)。 +- **26 个 action 25 通过 / 1 失败**(§6b):11 个业务 action 与 15 个活动类 action 均验证到 + 真实数据产物;唯一失败即 #508。rc.0 的"静默吞错"标志性缺陷已专项探测、未复现。 +- **本轮共立 14 个新 issue**(#680–#684、#688–#693、#696、#698),另给 11 个既有 issue + 回填 rc.2 实测判定(#494 #508 #509 #510 #521 #522 #524 #525 #526 #528 #645 #661 #548 #682)。 -**对 GA 的建议**:在 §11 的最小回归清单跑通、且 #688 修复前,不要认为 UI 层已通过验收。 -剩余三节的测试必须补齐后才能给出完整的 GA 放行判断。 +**对 GA 的建议**:平台阻断项已基本清空(唯 #508 待修,且它有单条记录的绕行路径)。 +CRM/console 侧的硬伤是 **#688**(线索 UI 无法创建,p0)与 **#548**(本轮取得定量证据: +被记录过活动的记录连 admin 都无法删除,见 §8)。在这两条修复、§11 清单跑通前, +不要认为 UI 层已通过验收。flows 与权限矩阵结论待 §10 两个执行中阶段补齐。 ## 1. 自动化基线 @@ -56,13 +60,13 @@ validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风 | 1. 14 对象列表 + 详情 | ✅ 12 通过 / 2 阻塞 | 见 §3 | | 1. 3 子对象(经父相关列表) | ✅ 通过 | 见 §3 | | 2. 17 对象 CRUD | ✅ REST 17/17 通过 / ❌ UI 1 项失败 | 见 §6;`crm_lead` UI 新建完全不可用(#688) | -| 3. 26 actions | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P2 包 | +| 3. 26 actions | ✅ 25 通过 / 1 失败 | 见 §6b;唯一失败是 `mass_update_stage`(#508 仍复现) | | 4. 5 仪表盘 | ✅ 通过 | 56 widget 全渲染出数,见 §4 | | 4. 10 报表 | ✅ 通过 | 见 §4 | | 5. 24 flows | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P3 包(含 #684 关键待验点) | | 6. Profile 权限矩阵 | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P4 包 | | 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | -| 8. 阻塞 issue 复测 | ⚠ 进行中 4/10 | 已解除:#520 #523 #526 #528(见 §7);#521 #522 #509 #508 #524 #510 复测执行中 | +| 8. 阻塞 issue 复测 | ✅ **10/10 完成** | **9 解除 / 1 仍复现**(#508);逐条判定见 §7,均已评论到 issue | ## 3. 对象:列表与详情(执行者 R1) @@ -172,10 +176,53 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 #690(`crm_forecast.display_title` 全行为 null)、#691(缺失 master-detail 父记录返回 403 而非 400)、 #692(lookup 显示回退)、#693(级联删除报错指向错误对象)。 +## 6b. Actions 全量(执行者 W2B) + +**26 个 action:25 通过 / 1 失败。** 测试数据 `RC1ACC-W2B-` 前缀(96 行台账,逐行记录最终去向)。 + +- **11 个业务 action 全部通过**,每个都验证到真实数据效果:工单升级/关闭(字段联动 + + 按钮可见性规则)、线索转化(客户+联系人+商机三件套)、跟进任务 + `next_followup_date` + 盖章、报价生成(QTE-0003 + 商机 stage→proposal)、克隆、`send_email`(sys_email 落库 + status=sent + 时间线 + 双向 recency 盖章)、`mark_primary`、市场活动报名(含去重:二次 + 执行 `count:0,skipped:1`)。 +- **15 个活动类 action(3 种 × 5 对象)全部通过**:每个组合验证 `crm_event` + + `sys_activity` + `crm_event_attendee` 三类产物;工单 `first_response_date` 首次通话 + 盖章、后续不重复盖章;`schedule_meeting` 的日期+时间对拼成 UTC 精确无误。 +- **唯一失败:`mass_update_stage`**(#508 仍复现,见 §7)。 +- **rc.0 的标志性缺陷(200 包裹内层失败 + 弹窗静默关闭)已专项探测,未复现**: + `/actions` 路径的失败现在返回 400/404 并显示红色错误 toast。 +- 已知项确认:#673(`sys_activity.actor_name` 存原始用户 id)行为未变,且 UI 时间线 + 直接显示该原始 id。 + +**W2B 的新发现**:#698(工单自动编号计数器落后于既有数据,REST 建单连续 409 且每次失败 +烧号,实测重试 25 次才成功;另发现种子行 `organization_id=NULL` 的口径不一致)、 +#682 追加(时间线钻取渲染原始 key `detail.viewSource`;Activity tab 计数只数审计行)、 +**#548 的定量证据**(见 §8)。 + +**流程性限制**:`sys_activity` / `sys_email` 按设计拒绝 REST DELETE(405),本轮产生的 +77 条时间线/邮件工件无法经 API 清理 —— 写阶段验收要想全清,需要管理员清除通道或测试租户隔离。 + ## 7. 平台阻塞 issue 复测 -> 进度:10 条中 **4 条已有判定**(#520 #523 #526 #528),其余 6 条 -> (#521 #522 #509 #508 #524 #510)复测执行中。已取得的确认: +**10/10 完成:9 条解除,1 条仍复现。** 每条判定已评论到对应 issue(含证据与建议)。 + +| issue | 判定 | 关键证据 | +|---|---|---| +| #520 | ✅ 解除 | 客服仪表盘 38 工单全部出数,时间窗序列正常 | +| #523 | ✅ 解除 | 矩阵报表按月/季/日正确分桶有序 | +| #521 | ✅ 解除 | 全程 **0 条 FORBIDDEN**;script body 对 crm_contact 与受共享治理的 crm_case 的 UPDATE 均持久化 | +| #522 | ✅ 解除 | 按对象派发全通;`/actions/global/...` 干净 404(ADR-0110 治理文案);失败非 200 + 红 toast | +| #509 | ✅ 解除 | 与 #522 同一证据基座;'global' vs '*' 幽灵消失 | +| #508 | ❌ **仍复现** | 多选零请求 + "select exactly one row";REST `selectedIds` 无法送达(顶层被忽略、params 被校验器拒);单选路径死于 CRM 侧 `update(id,{stage})` 签名 bug。**诚实性已修**(无假成功),功能仍缺 | +| #524 | ✅ 解除 | 菜单项活;工程化构造的服务端失败(amount=0)以红 toast 逐字回显,线索未变 | +| #510 | ✅ 解除 | `{current_user_id}` 解析为绑定参数(32 行 ≡ 字面 id ≡ 对照);未知 token 响亮报错并给建议。CRM 侧"My"widget 恢复仍是待办 | +| #526 | ✅ 解除 | plan 预警占用 + apply 拒绝 + `--yes` 确认门,全链验证 | +| #528 | ✅ 条件消失 | 新库 5 个 `__search` 列全活跃、plan in-sync;孤儿仅存于 rc.0 旧库 | + +残留的上游小尾巴(不阻塞,已在各 issue 记录):automation resume 路径仍 200 包裹内层失败 +(console 已会读内层信封,用户可见错误);migrate 占用拒绝退出码为 0。 + +> 已取得判定的细节: - **#520(datetime 时间窗过滤返回空集)—— 已解除。** rc.0 下客服仪表盘全空;rc.2 下 38 条种子工单全部出数:未结 30 / 紧急 7 / 平均解决 45.0h / SLA 违约 3,30 天日粒度 @@ -202,7 +249,14 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 加上该声明并附相同注释,但**记录变更流被系统写入触发时同样没有用户**这一情形被漏掉。 单次启动+种子+一轮 CRUD 写入即产生 **73 条 `[runAs]` 拒绝警告与 12 次运行失败** (`case_escalation` 38/3、`opportunity_approval` 23/9)。影响:种子加载、流触发流、 - 未来的集成/webhook 写入都会静默丢失这些自动化。 + 未来的集成/webhook 写入都会静默丢失这些自动化。用户会话路径的判定由 flows 阶段(W3)补齐。 +- **#548 在本轮取得定量证据**(已评论到 issue):action 沙箱 `ctx.api` 插入只盖 app 层 + `owner`,从不盖平台 `owner_id` —— 19 条 action 创建的 `crm_event` 全部 `owner_id=NULL`, + admin 对其 DELETE 与修复性 PATCH 均 403;级联进一步使 9 条父记录(2 商机/1 工单/2 联系人/ + 1 客户/2 线索)不可删除。**实际语义:任何被记录过通话/会议的记录,对所有人永久不可删除。** + 对照:REST 直建的 event 删除 200 —— 差别就在创建路径是否走平台 owner 盖章。 +- **#696**(本轮新立)报名过市场活动的线索/联系人无法删除(级联置空 campaign_member 引用 + 后违反其自身校验);#698(本轮新立)工单自动编号计数器落后 + 失败烧号。 ## 9. 诚实记录(测试过程说明) @@ -229,21 +283,26 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 (低优先观察项,未单独立案)。 - **rc.0 报告的两处记录经本轮推翻**,已在 §6 注明:合同状态看板在当前 main 并不存在; 看板拖拽**可以**自动化验证(rc.0 结论「不可自动化」应撤回)。 - -## 10. 未完成的阶段(会话额度耗尽,需另起会话补齐) - -本轮在 W2 阶段开始后不久因额度耗尽终止。以下三节**未执行**,不得视为通过: - -| 未执行项 | 计划位置 | 备注 | -|---|---|---| -| **26 个 actions 全量** | §3 | W2 已建 16 条记录后被终止,无有效结论 | -| **24 条 flows 全量** | §5 | 含 #684 的关键待验点:用户会话驱动的写入路径是否正常 | -| **5 profile 权限矩阵** | §6 | 权限期望矩阵已备妥(见并行任务包 P4) | -| **阻塞 issue 复测 6 条** | §8 | #521 #522 #509 #508 #524 #510 未复测 | -| **#526 / #528 CLI 复测** | §8 | 需独立端口+独立库隔离验证,刻意排在最后未及执行 | - -补齐方式见 [parallel-task-packages.md](./parallel-task-packages.md) 的 P2/P3/P4/P5 四个包, -每包自带记录前缀与隔离约定,可并行投放。 +- **W1 的一条发现被 W2B 推翻并撤回**:W1 曾报告"screen flow 在条件显隐的必填字段留空时 + 静默无操作"(0 请求、0 提示)。W2B 复跑同一场景:客户端以红 toast「Please fill: + Opportunity Name」拦截、弹窗保持打开 —— **不静默、不可复现**。已在 #524 评论中声明撤回, + 以 rc.2 行为为准。两次观察相隔数小时、同一构建,差异原因未明(W1 可能受当时表单状态干扰); + 按证据规则,不可复现的发现不予保留。 + +## 10. 阶段完成度(续跑更新) + +首轮在 W2 开始后因额度耗尽中断;额度恢复后续跑,当前状态: + +| 项 | 状态 | +|---|---| +| 26 个 actions 全量 | ✅ 已完成(W2B,§6b)—— 中断的 W2 无有效结论,已由 W2B 全量重做 | +| 阻塞 issue 复测 10 条 | ✅ 已完成(§7,9 解除 / 1 仍复现) | +| #526 / #528 CLI 复测 | ✅ 已完成(隔离端口+独立库,§7) | +| **24 条 flows 全量** | ⏳ **执行中**(W3)—— 含 #684 用户会话路径判定 | +| **5 profile 权限矩阵** | ⏳ **待执行**(W4,W3 之后串行) | + +若本会话再次中断,剩余工作按 [parallel-task-packages.md](./parallel-task-packages.md) +的 P3/P4 包投放即可。 ### 终止时的环境状态(诚实记录) From baeba5245b235c1367fef25dec8d71ba74179a65 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 08:28:40 +0000 Subject: [PATCH 09/11] =?UTF-8?q?docs(acceptance):=20record=20the=20full?= =?UTF-8?q?=20flow=20sweep=20=E2=80=94=2024/24=20pass,=20#684=20verdict,?= =?UTF-8?q?=20three=20new=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 24 flows verified with run-store evidence (trigger.userId per run): screen flows re-confirmed, all nine record-change flows healthy on the user-session path, both approval flows through approve and reject with lock enforcement, all seven scheduled sweeps exercised via manual trigger with idempotency checks, and demo_bootstrap green under the #663 shape. The #684 verdict is the headline: user path healthy, system path fully broken, demonstrated in-window as an approval bypass — a 50K flow-created deal shipped approval_status=not_required with no request. Files #700 (automation create_record inserts owner_id/organization_id NULL — sweep-created records born admin-untouchable on real installs), #701 (record-change flows re-enter on their own write; the engine loop-breaker, not the authored condition, is what stops the loop) and #702 (re-seed × snapshot phantom forecast row). Refs #507 #548 #617 #663 #671 #684 #690 #696 #698 #700 #701 #702 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 53 +++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index 198fa080..9427da3d 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -12,7 +12,7 @@ ## 结论(TL;DR) -> 进度:计划 8 节中 **7 节已执行**(第 5 节 flows 与第 6 节权限矩阵执行中,见 §10)。 +> 进度:计划 8 节中 **7 节已完成**,最后一节(profile 权限矩阵,W4)执行中。 - **平台侧:阻塞 issue 复测 10/10 完成 —— 9 条解除、1 条仍复现。** rc.0 的三个升级阻断项 (#520 datetime 过滤、#521 action 写库 FORBIDDEN、#522 全局 action 派发)**全部解除**; @@ -30,13 +30,15 @@ 四语言的校验文案均已正确翻译。 - **26 个 action 25 通过 / 1 失败**(§6b):11 个业务 action 与 15 个活动类 action 均验证到 真实数据产物;唯一失败即 #508。rc.0 的"静默吞错"标志性缺陷已专项探测、未复现。 -- **本轮共立 14 个新 issue**(#680–#684、#688–#693、#696、#698),另给 11 个既有 issue - 回填 rc.2 实测判定(#494 #508 #509 #510 #521 #522 #524 #525 #526 #528 #645 #661 #548 #682)。 +- **24/24 flows 通过**(§6c),含审批全链与 7 条定时扫;**#684 判定**:用户路径健康、 + 系统路径全断并实证**审批绕过**($150K 流建商机无审批直接放行)—— 建议升级该 issue 严重度。 +- **本轮共立 17 个新 issue**(#680–#684、#688–#693、#696、#698、#700–#702),另给 14 个 + 既有 issue 回填 rc.2 实测判定。 **对 GA 的建议**:平台阻断项已基本清空(唯 #508 待修,且它有单条记录的绕行路径)。 -CRM/console 侧的硬伤是 **#688**(线索 UI 无法创建,p0)与 **#548**(本轮取得定量证据: -被记录过活动的记录连 admin 都无法删除,见 §8)。在这两条修复、§11 清单跑通前, -不要认为 UI 层已通过验收。flows 与权限矩阵结论待 §10 两个执行中阶段补齐。 +GA 前必须解决的硬伤:**#688**(线索 UI 无法创建,p0)、**#548/#700**(两条不同写入路径 +都不盖 owner_id:被记录过活动的记录、及定时扫创建的记录,连 admin 都无法改删)、 +**#684**(系统驱动写入下自动化全断,含审批绕过)。权限矩阵结论待 W4 补齐。 ## 1. 自动化基线 @@ -63,8 +65,8 @@ validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风 | 3. 26 actions | ✅ 25 通过 / 1 失败 | 见 §6b;唯一失败是 `mass_update_stage`(#508 仍复现) | | 4. 5 仪表盘 | ✅ 通过 | 56 widget 全渲染出数,见 §4 | | 4. 10 报表 | ✅ 通过 | 见 §4 | -| 5. 24 flows | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P3 包(含 #684 关键待验点) | -| 6. Profile 权限矩阵 | ❌ **未执行** | 额度耗尽,见 §10;补齐用 P4 包 | +| 5. 24 flows | ✅ **24/24 通过** | 见 §6c;#684 判定:用户路径健康 / 系统路径全断且实证审批绕过 | +| 6. Profile 权限矩阵 | ⏳ 执行中 | W4,最后一个阶段 | | 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | | 8. 阻塞 issue 复测 | ✅ **10/10 完成** | **9 解除 / 1 仍复现**(#508);逐条判定见 §7,均已评论到 issue | @@ -202,6 +204,37 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 **流程性限制**:`sys_activity` / `sys_email` 按设计拒绝 REST DELETE(405),本轮产生的 77 条时间线/邮件工件无法经 API 清理 —— 写阶段验收要想全清,需要管理员清除通道或测试租户隔离。 +## 6c. Flows 全量(执行者 W3) + +**24/24 通过。** 测试数据 `RC1ACC-W3-` 前缀;判定基座是 `GET /api/v1/automation//runs` +返回的逐节点运行记录(含 `trigger.userId`)—— 每条"触发/拒绝"结论都有 run 行或日志行背书。 + +| 组 | 结果 | +|---|---| +| 6 条 screen flow(W2B 已深测,轻确认) | ✅ 全过,含报价折扣计算(200000×0.9)与转化联动 | +| 9 条 record-change(用户会话驱动) | ✅ 全过:trigger.userId=admin,数据操作全部执行,通知实际送达(sys_inbox_message + sys_email sent) | +| 2 条审批流 | ✅ 全过:批准/驳回双路径、`sys_approval_request` 创建、`RECORD_LOCKED` 锁定强制、批后解锁、≤$500K 跳过 director 签核的分层路由、阈值下不触发的对照 | +| 7 条 scheduled | ✅ 全过:cron 注册有据(当天 07:30/08:00 实际执行记录)+ 人工触发扫全部验证到效果(SLA 违约升级、到期任务提醒+去重、合同续约三件套+幂等、报价/合同过期、活动完结、预测快照);两条 00:00/01:00 槽位因服务器当时未运行仅注册可证 | +| demo_bootstrap | ✅ #663 修复确认:list+assignment 形态每 10 分钟全绿,无 findOne 报错 | + +**审批流的环境保留**:本库 `sys_user_position` 为空(未跑 demo:staff),审批按声明的 +`onEmptyApprovers: 'admin_rescue'` 由 admin 裁决 —— 岗位实际路由在本安装不可验,已按声明行为通过。 + +**#684 判定(已评论至 issue,建议升级严重度)**:用户会话路径**健康**(全部 record-change 流 +trigger.userId 就位、零 REFUSED,且用户身份穿越多跳链与挂起恢复);系统路径**全断**,并在窗口内 +实证**审批绕过**:$150K 的流建商机以 `approval_status='not_required'`、不锁定、无审批请求直接放行 +—— 恰是 on-create 审批流存在的目的场景。潜在延伸:经 Close Case 按钮关闭的工单,其 CSAT 流以 +无用户状态挂在 P1D 定时器上,到期通知可能被拒(#684 评论 N4)。 + +**W3 的新发现(已立案)**:**#700**(p1,自动化引擎系统扫插入的行 owner_id/organization_id/ +created_by 全 NULL,出生即 admin 不可改删;demo_bootstrap 十分钟自愈是演示拐杖,真实安装没有)、 +#701(记录变更流重入自身,拦住死循环的是引擎 loop-breaker 而非作者写的 start condition)、 +#702(启动重播种 × 快照流互动产生无 owner 幻影预测行)。#698 的 409 风暴当日未再现 +(计数器已被烧穿越过种子区,机制未修,新库仍会复现 —— 已评论)。 + +**清理**:36 条业务记录全部删除(200);按设计不可删的 sys_* 通知/审批工件与 2 条无用户的 +CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 + ## 7. 平台阻塞 issue 复测 **10/10 完成:9 条解除,1 条仍复现。** 每条判定已评论到对应 issue(含证据与建议)。 @@ -298,8 +331,8 @@ rc.0 的观察项 O1「合成事件不触发 dnd」成立,但由此得出的「 | 26 个 actions 全量 | ✅ 已完成(W2B,§6b)—— 中断的 W2 无有效结论,已由 W2B 全量重做 | | 阻塞 issue 复测 10 条 | ✅ 已完成(§7,9 解除 / 1 仍复现) | | #526 / #528 CLI 复测 | ✅ 已完成(隔离端口+独立库,§7) | -| **24 条 flows 全量** | ⏳ **执行中**(W3)—— 含 #684 用户会话路径判定 | -| **5 profile 权限矩阵** | ⏳ **待执行**(W4,W3 之后串行) | +| 24 条 flows 全量 | ✅ 已完成(W3,§6c)—— 24/24 通过,#684 判定已评论 | +| **5 profile 权限矩阵** | ⏳ **执行中**(W4) | 若本会话再次中断,剩余工作按 [parallel-task-packages.md](./parallel-task-packages.md) 的 P3/P4 包投放即可。 From 0327045165a8ec1db9695ff869f41ad050c7372f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 09:03:23 +0000 Subject: [PATCH 10/11] =?UTF-8?q?docs(acceptance):=20finalize=20=E2=80=94?= =?UTF-8?q?=20permission=20matrix=20does=20not=20hold;=20all=208=20section?= =?UTF-8?q?s=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 5-profile matrix (188 probes) is the run's heaviest result: the app's declared authorization posture is largely fiction at runtime. Five platform root causes — a member_default wildcard grant that voids the object gate on create/read/edit (#703), controlled_by_parent reads going org-wide and leaking line-item pricing (#704), modifyAllRecords and edit-shares granting no write (#705), a regressed campaign RLS widener (#706), and admin bypassing the export hard-gate (#707) — plus demo_bootstrap confiscating user records (#708) and matrix docs promising unfulfillable writes (#709). FLS, private-OWD read sets, is_private RLS, share reads and the four personas' export bits are the layers that match exactly. #548 is confirmed systematically: ownership splits across three columns and a complete hand-off is impossible. Finalizes the TL;DR (GA not recommended until the permission model, #688, the ownership/write family and #684 are fixed), adds §6d, the C2 permission gate to the GA checklist, and squares §8/§10 with what the later phases proved. All eight planned sections are now executed. Refs #547 #548 #620 #684 #688 #700 #703 #704 #705 #706 #707 #708 #709 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/test-report.md | 130 +++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 29 deletions(-) diff --git a/docs/acceptance-17/test-report.md b/docs/acceptance-17/test-report.md index 9427da3d..9afaa7f0 100644 --- a/docs/acceptance-17/test-report.md +++ b/docs/acceptance-17/test-report.md @@ -12,7 +12,7 @@ ## 结论(TL;DR) -> 进度:计划 8 节中 **7 节已完成**,最后一节(profile 权限矩阵,W4)执行中。 +> **全部 8 节执行完毕。** - **平台侧:阻塞 issue 复测 10/10 完成 —— 9 条解除、1 条仍复现。** rc.0 的三个升级阻断项 (#520 datetime 过滤、#521 action 写库 FORBIDDEN、#522 全局 action 派发)**全部解除**; @@ -32,13 +32,25 @@ 真实数据产物;唯一失败即 #508。rc.0 的"静默吞错"标志性缺陷已专项探测、未复现。 - **24/24 flows 通过**(§6c),含审批全链与 7 条定时扫;**#684 判定**:用户路径健康、 系统路径全断并实证**审批绕过**($150K 流建商机无审批直接放行)—— 建议升级该 issue 严重度。 -- **本轮共立 17 个新 issue**(#680–#684、#688–#693、#696、#698、#700–#702),另给 14 个 +- **权限矩阵大面积不成立(§6d)** —— 这是全场最重的结论:平台 `member_default` 通配授权把 + 应用的对象门在建/读/改三轴上整体架空(任何 profile 都能在任何对象建记录,#703); + `controlled_by_parent` 读取全组织开放,service_agent 在 0 个可见商机下读全部 74 条行项目 + 价格(#704);`modifyAllRecords` 与 edit 级共享均不给写(#705);#547 验证过的 campaign + 写扩权回归失效(#706)。**完好的层**:FLS 100% 匹配、四个真实角色的导出位精确、 + private OWD 的读行集与 is_private RLS 精确。#548 系统化坐实:所有权分裂在三列上, + 完整移交不可能(已评论)。 +- **本轮共立 23 个新 issue**(#680–#684、#688–#693、#696、#698、#700–#709),另给 15 个 既有 issue 回填 rc.2 实测判定。 -**对 GA 的建议**:平台阻断项已基本清空(唯 #508 待修,且它有单条记录的绕行路径)。 -GA 前必须解决的硬伤:**#688**(线索 UI 无法创建,p0)、**#548/#700**(两条不同写入路径 -都不盖 owner_id:被记录过活动的记录、及定时扫创建的记录,连 admin 都无法改删)、 -**#684**(系统驱动写入下自动化全断,含审批绕过)。权限矩阵结论待 W4 补齐。 +**对 GA 的建议:当前状态不建议放行。** rc.0 时代的平台阻断项虽已基本清空(唯 #508 待修), +但本轮新确立的阻断级问题更重: +1. **#703/#704(p0,平台)** 权限模型默认开放 —— 任何成员在任何对象上可建可读(含行项目价格 + 泄漏),应用声明的安全姿态是纸面文章; +2. **#688(p0,console)** 线索 UI 完全无法创建; +3. **#548/#700/#705 一族** 所有权与写门破碎:owner 移交不可能、定时扫产物出生即不可改删、 + 经理改不了下属记录; +4. **#684** 系统驱动写入下自动化全断,含审批绕过实证。 +这四组修复并按 §11 清单复归前,17.0 GA 不应发布带权限声明的 HotCRM。 ## 1. 自动化基线 @@ -66,7 +78,7 @@ validate 的 5 条警告均为既有已知项(2 个审批流的岗位空槽风 | 4. 5 仪表盘 | ✅ 通过 | 56 widget 全渲染出数,见 §4 | | 4. 10 报表 | ✅ 通过 | 见 §4 | | 5. 24 flows | ✅ **24/24 通过** | 见 §6c;#684 判定:用户路径健康 / 系统路径全断且实证审批绕过 | -| 6. Profile 权限矩阵 | ⏳ 执行中 | W4,最后一个阶段 | +| 6. Profile 权限矩阵 | ❌ **矩阵不成立** | 188 探针:134 匹配 / **38 真实不匹配**,5 个平台根因,见 §6d;FLS 与导出位是唯二完好层 | | 7. i18n 四语言 | ⚠ 部分通过 | 见 §5,zh-CN 完整,ja/es 有实质缺口 | | 8. 阻塞 issue 复测 | ✅ **10/10 完成** | **9 解除 / 1 仍复现**(#508);逐条判定见 §7,均已评论到 issue | @@ -232,8 +244,48 @@ created_by 全 NULL,出生即 admin 不可改删;demo_bootstrap 十分钟自愈 #702(启动重播种 × 快照流互动产生无 owner 幻影预测行)。#698 的 409 风暴当日未再现 (计数器已被烧穿越过种子区,机制未修,新库仍会复现 —— 已评论)。 -**清理**:36 条业务记录全部删除(200);按设计不可删的 sys_* 通知/审批工件与 2 条无用户的 -CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 +**清理**:36 条业务记录删除(200);**1 条定时扫创建的 crm_forecast 行被 #700 阻塞留存** +(demo_bootstrap 不认领 crm_forecast,故永不自愈 —— #700 最纯净的活体证据);按设计不可删的 +sys_* 通知/审批工件与 2 条无用户的 CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 + +## 6d. Profile 权限矩阵(执行者 W4)—— 矩阵不成立 + +**188 个探针(每用户各自 token):134 匹配 / 38 真实不匹配 / 13 信息项。** +测试用户 4 个(`rc1acc-w4-{rep,mgr,agent,mkt}@test.local`,留在库中未删);system_admin +断言以 dev-admin(org owner)执行 —— 纯 CRM system_admin 集用户无法经 create-user 构造, +该保留已记录。 + +**5 个根因解释全部 38 处不匹配(逐条已立案)**: + +| # | 根因 | 后果 | issue | +|---|---|---|---| +| 1 | 平台 `member_default` 集带 `*` 通配 allowCreate/Read/Edit,并集合并进每个成员 | 对象门在建/读/改三轴失效:**21 个建拒探针全部 201**(含 service_agent 在显式全否商机上建单);public_read 对象对无授权者返回**全部行** | **#703(p0)** | +| 2 | `controlled_by_parent` 读取全组织开放(插入侧却正确按父门控) | service_agent 0 可见商机 → 读全部 74 行项目价格;rep 1 可见客户 → 读全部 14 联系人 | **#704(p0)** | +| 3 | `modifyAllRecords` 对行级写门无效;edit 级 `sys_record_share` 实际只给读 | sales_manager 改不了任何非本人创建的记录;团队/领地/升级三条 edit 共享全部只读 | **#705(p1)** | +| 4 | #547 验证过的 `marketing_campaign_updates` RLS 写扩权回归失效(member 扩权仍好) | marketing 改不了非本人建的 campaign;错误形状显示 sharing 中间件在 RLS 之前抢答 | **#706(p1)** | +| 5 | org-admin 路径绕过 17.0 allowExport 硬门 | admin 导出 quote/campaign/task 均 200(四个真实角色的导出位全部精确) | **#707(p2)** | + +**完好的层(与声明 100% 一致)**:字段级安全(internal_notes 掩码、只读字段、过滤谓词拒绝 +全部精确)、四个真实角色的导出位、private OWD 的读行集(own 与 viewAll 均为精确行集)、 +is_private RLS(五角色列表+单记录 GET 双验)、共享规则的**读**扩权与撤销(含钩子即时命中 +新建记录)、删除轴、审批 slate 解析。 + +**#548 系统化判定(已评论至 issue)**:所有权分裂在**三列** —— `owner`(仅 UI 与 +is_private 谓词)、`owner_id`(可见性)、`created_by`(经 member_default 通配 RLS 共同门 +控写)。改 `owner` 不移动任何访问;直接改 `owner_id` 只移动读;**完整所有权移交在任何 API +面上不可能**。NULL `owner_id` 行对 admin 的改删双 403 复现;demo_bootstrap 对 crm_task +数分钟自愈、对 crm_event 永不(留有活体标本 `m2amex4D0LZYYr16`)。 + +**连带发现**:**#708(p1)** demo_bootstrap 认领扫把 owner 未显式设置的用户新建记录改判给 +第一个用户(rep 的报价 10 分钟内被静默没收 —— #620 的 owner 默认失效使用户记录与无主种子 +在 `owner` 列上不可区分);**#709(p2)** 矩阵文档承诺了平台无法兑现的写权(marketing 的 +contact 建被 master-detail 门拒;agent/mkt 的"可编辑"期望在 owner_id+created_by 写门下 +不可实现)。#549 现状确认(领地共享客户的子相关列表为空 —— 但注意对 cbp 子对象该问题 +方向相反:所有人读所有行,见根因 2)。 + +**环境注记**:后台演示流使种子计数在会话中漂移(无 W4 写入时 accounts 13→12 等), +故所有行集断言按记录 ID 隶属验证而非总数;≥100k 商机创建即自动送审并 RECORD_LOCKED +(admin 亦不可 PATCH,删除不受锁限);可能残留 1 条指向已删 fixture 的 sys_record_share。 ## 7. 平台阻塞 issue 复测 @@ -290,6 +342,13 @@ CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 对照:REST 直建的 event 删除 200 —— 差别就在创建路径是否走平台 owner 盖章。 - **#696**(本轮新立)报名过市场活动的线索/联系人无法删除(级联置空 campaign_member 引用 后违反其自身校验);#698(本轮新立)工单自动编号计数器落后 + 失败烧号。 +- **权限/所有权模型一族(本轮最重,§6d 详述)**:#703(通配授权架空对象门,p0)、 + #704(cbp 读全组织开放,行项目价格泄漏,p0)、#705(modifyAllRecords 与 edit 共享不给写)、 + #706(#547 的 campaign 写扩权回归)、#707(admin 绕过导出硬门)、#708(demo_bootstrap + 没收用户记录)、#709(矩阵文档承诺平台无法兑现的写权)。#700(定时扫产物 owner_id=NULL, + 与 #548 同根不同写入方)。 +- **rc.0 遗留观察项本轮状态**:O3(时间线显示原始用户 id)= 既有 #673,确认未变且 UI 可见; + O4/O5(浮点/Markdown 格式)未专门复验;L1–L4 未在本轮范围。 ## 9. 诚实记录(测试过程说明) @@ -332,7 +391,7 @@ CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 | 阻塞 issue 复测 10 条 | ✅ 已完成(§7,9 解除 / 1 仍复现) | | #526 / #528 CLI 复测 | ✅ 已完成(隔离端口+独立库,§7) | | 24 条 flows 全量 | ✅ 已完成(W3,§6c)—— 24/24 通过,#684 判定已评论 | -| **5 profile 权限矩阵** | ⏳ **执行中**(W4) | +| 5 profile 权限矩阵 | ✅ 已完成(W4,§6d)—— 矩阵不成立,5 根因 7 新 issue | 若本会话再次中断,剩余工作按 [parallel-task-packages.md](./parallel-task-packages.md) 的 P3/P4 包投放即可。 @@ -348,12 +407,11 @@ CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 - **#696**(已立案):报名过市场活动的线索/联系人无法删除 —— 级联把 campaign_member 的引用置空后违反其自身校验规则。删掉 campaign member 后线索即可删除(400 → 200), 机制已证实。 - - **未立案的观察**:admin 对部分记录删除返回 **403 FORBIDDEN - "insufficient privileges to delete"**,尤其是活动类 action 创建的 `crm_event` - (W1 用 REST 自建的 event 删除是 200,差别在创建路径)。 - **未立案的原因**:这很可能是既有 #548(app `owner` lookup 与平台 `owner_id` 不一致) - 的症状,在未做隔离验证前立新案有重复风险。补齐 W2/W4 时应优先验证这一条, - 并把结论回填到 #548 或据此另立。 + - **当时未立案的观察,后经 W2B/W4 坐实**:admin 对活动类 action 创建的 `crm_event` 删除 + 返回 **403 FORBIDDEN**。W2B 与 W4 隔离验证确认这是 **#548 的机制**(action 沙箱与 + 自动化引擎的系统上下文插入都不盖 `owner_id`,而访问键控 `owner_id`),证据已系统化 + 评论到 #548,并把"不同写入方"的自动化引擎路径单列为 #700。当初"先不立案、避免重复" + 的判断被后续证据证明是对的 —— 它确实是 #548 而非新缺陷。 ## 11. 附录 A:GA 发布时需重跑的最小回归清单 @@ -370,9 +428,11 @@ CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 3. ★ **#520** 客服仪表盘出数:未结工单、紧急数、SLA 违约数非零,30 天时间序列有数 —— datetime 时间窗过滤的判定现场 4. ★ **#523** 三个矩阵报表的日期列按月/季/日正确分桶且有序,无"逐原始日期爆列" -5. ⚠ **#521** 带写库 body 的 script action(如 `close_case`)执行后**写入确实落库** -6. ⚠ **#522 / #509** 全局(无 objectName)action 可派发,且失败时返回非 200、UI 有可见报错 -7. ⚠ **#508** `mass_update_stage` 三条调用路径(REST `selectedIds` / 选择栏 / 工具栏) +5. ★ **#521** 带写库 body 的 script action(如 `close_case`)执行后**写入确实落库** + (本轮:全程 0 FORBIDDEN,已解除) +6. ★ **#522 / #509** 全局 action 派发 + 失败非 200 + UI 可见报错(本轮已解除) +7. ⚠ **#508** `mass_update_stage` 三条调用路径 —— **唯一仍复现的阻塞项**,GA 前须修 + (平台不传 `selectedIds` + CRM `update(id,{stage})` 签名 bug 两半都缺) ### C. 核心业务链路(每条都有真实数据效果判据) @@ -382,26 +442,38 @@ CSAT 挂起 run 留存并记录;未触碰 W2/W2B 残留与种子数据。 **industry 枚举跨对象映射不报错**(rc.0 的失败点) 10. ★ 行项目 rollup:报价/商机行项目增改后父记录金额重算(注意 rollup 是 **async**, 勿在同一 tick 断言) -11. ⚠ 24 条 flows 全量触发 —— 尤其 **#684**:确认记录变更流在**用户会话驱动**的写入下 - 正常执行(系统驱动下已确认被拒) -12. ⚠ 5 个 profile 的对象级 CRUD、readScope、sharing 与字段级权限与声明一致 +11. ★ 24 条 flows 用户会话驱动路径(本轮 24/24 通过);⚠ **#684** 系统驱动路径仍全断, + 含审批绕过 —— 修复后须复验 `runAs:'system'` 缺失的 7 条记录变更流 + +### C2. 权限与所有权模型(本轮最重的失败区,GA 前必须逐条复归) + +12. ⚠ **#703(p0)** 任何 profile 不能在其被拒对象上建记录(当前 21/21 建拒探针都 201) +13. ⚠ **#704(p0)** `controlled_by_parent` 读取须随父可见性,不得全组织开放 + (当前 service_agent 在 0 可见商机下读全部 74 行项目价格) +14. ⚠ **#705** sales_manager 的 `modifyAllRecords` 与 edit 级共享须真正给写(当前全 403) +15. ⚠ **#706** marketing 的 campaign 写扩权(#547 曾验证通过,rc.2 回归) +16. ⚠ **#548 / #700 / #708** 所有权:owner 重指派应移动访问;定时扫/action 创建的记录 + 不得 owner_id=NULL 而对 admin 不可改删;用户记录不被 demo_bootstrap 没收 +17. ★ FLS(掩码/只读/过滤谓词拒绝)、private OWD 读行集、is_private RLS、四角色导出位、 + 共享**读**扩权与撤销 —— 本轮 100% 匹配,GA 时确认未回归 ### D. UI 可用性(本轮发现的重灾区) -13. ⚠ **#688(p0)** 从界面新建线索必须能成功 —— 当前完全不可用,是本清单最高优先级 -14. ★ 多 tab 表单校验失败后,其它 tab 已填值不丢(在 `crm_contract` 上测, +18. ⚠ **#688(p0)** 从界面新建线索必须能成功 —— 当前完全不可用 +19. ★ 多 tab 表单校验失败后,其它 tab 已填值不丢(在 `crm_contract` 上测, `crm_case` 已无跨 tab 必填布局) -15. ★ 看板拖拽触发真实 PATCH 且 hook 重算(用真实鼠标 API,合成事件无效) -16. ⚠ 删除链路:报名过市场活动的线索可删除(#696);admin 能删除 action 创建的记录 +20. ★ 看板拖拽触发真实 PATCH 且 hook 重算(用真实鼠标 API,合成事件无效) +21. ⚠ 删除链路:报名过市场活动的线索可删除(#696);action 创建的记录 admin 能删除 + (当前被 #548/#700 阻塞) ### E. i18n(抽查即可) -17. ⚠ 四语言的 **action 标签与校验文案**(本轮:标签与文案均正确, +22. ⚠ 四语言的 **action 标签与校验文案**(本轮:标签与文案均正确, 但参数弹窗标题仍是英文 #680) -18. ⚠ `objectstack lint` 不带 `--skip-i18n` 的警告数**不高于 710**(本轮基线), +23. ⚠ `objectstack lint` 不带 `--skip-i18n` 的警告数**不高于 710**(本轮基线), 确认债务未继续增长 ### 执行顺序建议 -A → B → C → D → E。A 不绿则其余不必开始;B 中任一条失败即为**升级阻断**, +A → B → C → C2 → D → E。A 不绿则其余不必开始;B 或 C2 中任一 p0 失败即为**升级阻断**, 应在 GA 前解决或明确接受。 From 3593b13c6c47859b40f56a37abbf69f44cf08b78 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 09:04:31 +0000 Subject: [PATCH 11/11] =?UTF-8?q?docs(acceptance):=20mark=20the=20handoff?= =?UTF-8?q?=20page=20complete=20=E2=80=94=20all=208=20sections=20executed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframes HANDOFF.md from a mid-run baton-pass to a re-run/repair guide now that every section is done, and puts the one-line GA verdict up top: not recommended to ship until the permission model (#703/#704), lead-create UI (#688), the ownership/write family (#548/#700/#705) and #684 are fixed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01URfByxcWd39Vv8u5z4F11g --- docs/acceptance-17/HANDOFF.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/acceptance-17/HANDOFF.md b/docs/acceptance-17/HANDOFF.md index f0931ef9..ba9d4c3c 100644 --- a/docs/acceptance-17/HANDOFF.md +++ b/docs/acceptance-17/HANDOFF.md @@ -1,6 +1,11 @@ # rc.2 验收 —— 交接说明 -给接手这轮验收的下一位(或下一个会话)。**先读这一页,再读别的。** +> ✅ **本轮验收已全部完成(8/8 节)。** 本页最初为中途交接而写,现保留作为"如何重跑/续验" +> 的操作指南与踩坑清单。结论看 [test-report.md](./test-report.md);GA 复归清单看其 §11。 +> **一句话结论:当前状态不建议 GA 放行** —— 权限模型默认开放(#703/#704 p0)、线索 UI 无法 +> 创建(#688 p0)、所有权/写模型破碎(#548/#700/#705)、系统驱动自动化全断含审批绕过(#684)。 + +给接手复验或修复的下一位(或下一个会话)。**先读这一页,再读别的。** - **PR**:[#676](https://github.com/objectstack-ai/hotcrm/pull/676)(draft),分支 `claude/hotcrm-17-rc1-acceptance-hj869o`,基于 `main` @ 5a78f88 @@ -16,19 +21,22 @@ | [parallel-task-packages.md](./parallel-task-packages.md) | 剩余工作的 P1–P5 任务包,**直接照着投** | | [evidence/](./evidence/) | 上一轮的执行记录与准备资产(见下) | -## 二、进度:8 节完成 5 节 +## 二、进度:8/8 节全部完成 | 计划节 | 状态 | |---|---| -| 0 自动化基线 | ✅ 完成(verify 1280 例、e2e 16/16、冷启动 0 ERROR) | -| 1 对象列表/详情 | ✅ 完成(15/17 通过,2 项因无种子数据阻塞) | -| 2 对象 CRUD | ✅ 完成(REST 17/17 全量通过) | -| 4 仪表盘/报表 | ✅ 完成(5 仪表盘 56 widget、10 报表全部出数) | -| 7 i18n 四语言 | ✅ 完成 | -| **3 actions 全量(26 个)** | ❌ **未执行** → 用 **P2** 包 | -| **5 flows 全量(24 条)** | ❌ **未执行** → 用 **P3** 包 | -| **6 profile 权限矩阵** | ❌ **未执行** → 用 **P4** 包 | -| **8 阻塞 issue 复测** | ⚠ **部分**:#520/#523 已确证解除;#521 #522 #509 #508 #524 #510 未测 → **P2**;#526 #528 未测 → **P5** | +| 0 自动化基线 | ✅ verify 1280 例、e2e 16/16、冷启动 0 ERROR | +| 1 对象列表/详情 | ✅ 15/17 通过(2 项曾因无种子阻塞,W1 自建记录后补测通过) | +| 2 对象 CRUD | ✅ REST 17/17 全量通过 | +| 3 actions 全量(26 个) | ✅ 25 通过 / 1 失败(#508) | +| 4 仪表盘/报表 | ✅ 5 仪表盘 56 widget、10 报表全部出数 | +| 5 flows 全量(24 条) | ✅ 24/24 通过;#684 判定:用户路径健康/系统路径全断 | +| 6 profile 权限矩阵 | ✅ 188 探针,**矩阵不成立**,5 平台根因(#703-707) | +| 7 i18n 四语言 | ✅ zh-CN 完整,ja/es 有实质缺口 | +| 8 阻塞 issue 复测 | ✅ 10/10:9 解除 / 1 仍复现(#508) | + +**若要复验或修复后回归**,按 [parallel-task-packages.md](./parallel-task-packages.md) 的 +P1–P5 包重跑对应部分即可;每包自带记录前缀与隔离约定。 ## 三、上手三步