From 76b47006fe726bea34cb641311567dac1e1ac5d6 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 13 May 2026 18:40:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=A4=E4=B8=AA=20?= =?UTF-8?q?OpenSpec=20=E5=8F=98=E6=9B=B4=E6=8F=90=E6=A1=88=20=E2=80=94=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=9E=B6=E6=9E=84=E9=87=8D=E6=9E=84=E4=B8=8E?= =?UTF-8?q?=20HTTP=20Checker=20=E8=B4=A8=E9=87=8F=E5=8A=A0=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - frontend-architecture-refactor: 拆分 hooks/组件、类型筛选器动态化 - http-checker-quality-hardening: ReDoS 防护、failure 格式修正、测试补全 --- .../.openspec.yaml | 2 + .../frontend-architecture-refactor/design.md | 145 ++++++++++++++++++ .../proposal.md | 34 ++++ .../specs/meta-api/spec.md | 27 ++++ .../specs/tanstack-query-data-layer/spec.md | 109 +++++++++++++ .../specs/target-detail-drawer/spec.md | 91 +++++++++++ .../specs/target-table/spec.md | 69 +++++++++ .../specs/target-type-display/spec.md | 13 ++ .../frontend-architecture-refactor/tasks.md | 48 ++++++ .../.openspec.yaml | 2 + .../http-checker-quality-hardening/design.md | 93 +++++++++++ .../proposal.md | 28 ++++ .../specs/expect-body-checkers/spec.md | 101 ++++++++++++ .../http-checker-quality-hardening/tasks.md | 38 +++++ 14 files changed, 800 insertions(+) create mode 100644 openspec/changes/frontend-architecture-refactor/.openspec.yaml create mode 100644 openspec/changes/frontend-architecture-refactor/design.md create mode 100644 openspec/changes/frontend-architecture-refactor/proposal.md create mode 100644 openspec/changes/frontend-architecture-refactor/specs/meta-api/spec.md create mode 100644 openspec/changes/frontend-architecture-refactor/specs/tanstack-query-data-layer/spec.md create mode 100644 openspec/changes/frontend-architecture-refactor/specs/target-detail-drawer/spec.md create mode 100644 openspec/changes/frontend-architecture-refactor/specs/target-table/spec.md create mode 100644 openspec/changes/frontend-architecture-refactor/specs/target-type-display/spec.md create mode 100644 openspec/changes/frontend-architecture-refactor/tasks.md create mode 100644 openspec/changes/http-checker-quality-hardening/.openspec.yaml create mode 100644 openspec/changes/http-checker-quality-hardening/design.md create mode 100644 openspec/changes/http-checker-quality-hardening/proposal.md create mode 100644 openspec/changes/http-checker-quality-hardening/specs/expect-body-checkers/spec.md create mode 100644 openspec/changes/http-checker-quality-hardening/tasks.md diff --git a/openspec/changes/frontend-architecture-refactor/.openspec.yaml b/openspec/changes/frontend-architecture-refactor/.openspec.yaml new file mode 100644 index 0000000..93831bd --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-13 diff --git a/openspec/changes/frontend-architecture-refactor/design.md b/openspec/changes/frontend-architecture-refactor/design.md new file mode 100644 index 0000000..df71b33 --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/design.md @@ -0,0 +1,145 @@ +## Context + +当前前端代码约 970 行,功能完整但存在以下架构问题: + +1. **hook 职责过重**:`hooks/useTargetDetail.ts`(113 行)同时承载全局查询(`useSummary`、`useTargets`)、Drawer 状态管理、条件查询和通用 `fetchJson` 封装,文件名与实际职责不匹配 +2. **组件体积膨胀**:`TargetDetailDrawer.tsx`(228 行)混合了时间选择逻辑、两个 Tab 的完整渲染、列定义和统计计算 +3. **类型维护重复**:`target-type-display.ts` 和 `target-table-filters.ts` 各自硬编码 checker 类型列表,新增 checker 需改两处前端文件 +4. **测试覆盖不足**:仅 `constants/` 下 4 个纯函数有测试,`utils/time.ts` 和组件内统计逻辑未覆盖 +5. **小问题**:`StatusDonut` 用数组索引做 key、`StatusBar` 硬编码 30 格、`TrendChart` 冗余 loading prop + +后端 `CheckerRegistry` 已有 `supportedTypes` 属性,可直接暴露给前端。项目未上线,不需要向前兼容。 + +## Goals / Non-Goals + +**Goals:** + +- hook 按职责拆分,文件名匹配实际内容 +- `TargetDetailDrawer` 拆分为 3 个组件,每个 < 100 行 +- 类型筛选器由后端 meta API 驱动,新增 checker 前端零改动 +- 删除 type label 转换层,直接使用 type 原始文本 +- 补齐前端纯函数测试 +- 修复已知小问题 + +**Non-Goals:** + +- 不引入路由库或状态管理库 +- 不重构后端 API 结构 +- 不改变现有轮询策略和 QueryClient 配置 +- 不新增 CSS 文件(继续使用单一 `styles.css`) + +## Decisions + +### Decision 1: hook 拆分策略 + +**选择**:按查询层级拆分为 `use-queries.ts` 和 `use-target-detail.ts` + +- `use-queries.ts`:`queryKeys`、`fetchJson`(不导出)、`useSummary`、`useTargets`、`useMeta` +- `use-target-detail.ts`:仅保留 Drawer 状态管理(`selectedTargetId`、时间范围、分页、`openDrawer`/`closeDrawer`)和条件查询(trend/history) + +**理由**:全局查询(summary/targets/meta)是面板级别的,与 Drawer 详情无关。拆分后各文件职责单一,命名自解释。`fetchJson` 仅被 query 层使用,留在 `use-queries.ts` 内部不导出。 + +**备选方案**: +- 仅重命名为 `useQueries.ts` — 解决命名问题但不解决职责混合 +- 每个 query 一个文件 — 过度拆分,增加文件数量但无实质收益 + +### Decision 2: TargetDetailDrawer 拆分方式 + +**选择**:拆为 3 个组件 + 1 个常量文件 + +``` +TargetDetailDrawer.tsx ← Drawer 壳 + 时间选择 + Tab 切换 +OverviewTab.tsx ← 统计 + TrendChart + StatusDonut + Descriptions +HistoryTab.tsx ← PrimaryTable + 分页 +constants/history-table-columns.tsx ← HISTORY_COLUMNS +``` + +**理由**:两个 Tab 的内容完全独立,拆分后各组件 < 100 行。`HISTORY_COLUMNS` 与 `TARGET_TABLE_COLUMNS` 性质相同,应放在 `constants/` 下保持一致。 + +统计计算逻辑(`totalChecks`/`upChecks`/`downChecks`)提取为 `utils/stats.ts` 纯函数,便于测试和 `useMemo`。 + +### Decision 3: Meta API 设计 + +**选择**:`GET /api/meta` 返回 `{ checkerTypes: string[] }` + +```typescript +// src/shared/api.ts +export interface MetaResponse { + checkerTypes: string[]; +} +``` + +**理由**: +- 从 `checkerRegistry.supportedTypes` 直接获取,无需额外维护 +- 返回 `string[]` 而非 `{ key, label }[]`,因为决策是不做 label 转换 +- 端点命名为 `/api/meta` 而非 `/api/types`,为未来扩展预留空间(如版本号、功能开关等) +- `staleTime: Infinity`,应用生命周期内只请求一次 + +**备选方案**: +- 从 targets 响应中动态提取 — 只能获取"当前有数据的类型",不能获取"系统支持的全部类型" +- 在 health 端点中附带 — 语义不匹配,health 应保持最小化 + +### Decision 4: 类型展示策略 + +**选择**:删除 `target-type-display.ts`,所有展示位置直接使用 `target.type` 原始文本 + +**影响位置**: +- `target-table-columns.tsx` 类型列 cell:`row.type` 直接渲染 +- `TargetDetailDrawer.tsx` 标题栏 Tag:`target.type` 直接渲染 +- `typeFilter` 列表:从 meta API 获取,label 和 value 均为原始 type 文本 + +**理由**:type 文本本身已足够清晰(`http`、`command`、`tcp`),无需额外映射层。消除了前后端重复维护的问题。 + +### Decision 5: 列定义动态化 + +**选择**:`TARGET_TABLE_COLUMNS` 从静态常量改为工厂函数 + +```typescript +export function createTargetTableColumns(checkerTypes: string[]): PrimaryTableCol[] { + const typeFilter = { + list: [ + { label: "全部", value: "" }, + ...checkerTypes.map(t => ({ label: t, value: t })), + ], + type: "single" as const, + }; + // ... 返回列定义数组 +} +``` + +**数据流**:`useMeta()` → `checkerTypes` → `createTargetTableColumns(checkerTypes)` → `TargetGroup` columns prop + +**理由**:`typeFilter` 是唯一需要动态数据的部分,通过工厂函数注入参数,保持列定义的纯函数特性。`statusFilter` 保持静态(UP/DOWN 是固定的)。 + +`TargetGroup` 新增 `columns` prop 接收动态列定义,`TargetBoard` 负责调用工厂函数并传递。 + +### Decision 6: StatusBar maxSlots 参数化 + +**选择**:新增 `maxSlots` prop(默认 30),组件根据 prop 渲染格数 + +```typescript +interface StatusBarProps { + samples: Array<{ up: boolean }>; + maxSlots?: number; +} +``` + +**理由**:消除硬编码魔数,使组件可复用。默认值 30 保持向后兼容。 + +### Decision 7: TrendChart 移除 loading prop + +**选择**:移除 `loading` prop,组件只接收 `data: TrendPoint[]` + +**理由**:调用方(`OverviewTab`)已用 `Skeleton` 处理 loading 状态,`TrendChart` 只在有数据时渲染。组件内部的 `if (loading)` 分支和 `loading={false}` 传参都是死代码。 + +## Risks / Trade-offs + +| Risk | Mitigation | +|------|-----------| +| 列定义改为函数后,每次 `checkerTypes` 变化会重新创建列数组 | `useMemo` 包裹 `createTargetTableColumns` 调用;meta 数据 `staleTime: Infinity` 确保不会频繁变化 | +| meta API 在 targets 之前未返回时,筛选器暂时为空 | meta 请求极轻量(无 DB 查询),通常先于 targets 返回;即使晚到,筛选器会在数据到达后自动出现 | +| 拆分后组件间 props 传递增多 | 层级仅增加一层(Drawer → Tab),props 类型明确,不会造成 prop drilling 问题 | + +## Open Questions + +无。方案已在 explore 阶段与用户确认。 diff --git a/openspec/changes/frontend-architecture-refactor/proposal.md b/openspec/changes/frontend-architecture-refactor/proposal.md new file mode 100644 index 0000000..134e0de --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/proposal.md @@ -0,0 +1,34 @@ +## Why + +前端代码经过多轮功能迭代后,出现了 hook 职责过重(`useTargetDetail.ts` 承载全部数据层)、组件体积膨胀(`TargetDetailDrawer` 228 行混合多种逻辑)、类型筛选器与后端硬编码重复维护等架构问题。需要通过拆分、动态化和测试补齐来提升可维护性,使新增 checker 类型时前端零改动。 + +## What Changes + +- 拆分 `hooks/useTargetDetail.ts` 为 `use-queries.ts`(全局查询)和 `use-target-detail.ts`(Drawer 状态管理) +- 拆分 `TargetDetailDrawer.tsx` 为 Drawer 壳、`OverviewTab.tsx`、`HistoryTab.tsx` 三个组件 +- 将 `HISTORY_COLUMNS` 移至 `constants/history-table-columns.tsx` +- 提取统计计算逻辑为 `utils/stats.ts` 纯函数 +- 后端新增 `GET /api/meta` 端点,返回 `checkerTypes` 列表 +- 前端新增 `useMeta()` hook 消费 meta API,动态生成类型筛选器 +- **BREAKING** 删除 `constants/target-type-display.ts`,前端直接使用 type 原始文本,不再做 label 转换 +- 列定义从静态常量改为工厂函数 `createTargetTableColumns(checkerTypes)` +- 修复小问题:`StatusDonut` key、`StatusBar` 硬编码、`TrendChart` 冗余 prop、统计计算 useMemo +- 补充前端测试:`utils/time.ts`、`utils/stats.ts`、动态列生成 + +## Capabilities + +### New Capabilities +- `meta-api`: 后端 meta 信息 API,提供 checker 类型列表等运行时元数据 + +### Modified Capabilities +- `target-type-display`: 移除前端静态映射,改为直接使用后端返回的 type 原始文本,筛选器列表由 meta API 动态驱动 +- `tanstack-query-data-layer`: hook 文件拆分为 `use-queries.ts` 和 `use-target-detail.ts`,新增 `useMeta()` 查询 +- `target-detail-drawer`: 组件拆分为 Drawer 壳 + OverviewTab + HistoryTab,统计计算提取为纯函数 +- `target-table`: 列定义从静态常量改为工厂函数,接收动态 checkerTypes 参数;类型列直接显示 type 原始文本 + +## Impact + +- 后端:新增 `src/server/routes/meta.ts`、`src/shared/api.ts` 增加 `MetaResponse` 类型、`app.ts` 注册路由 +- 前端:hooks/components/constants 目录结构调整,删除 `target-type-display.ts` +- 测试:删除 `target-type-display.test.ts`,新增 `time.test.ts`、`stats.test.ts`,更新 `target-table-filters.test.ts` +- 文档:更新 DEVELOPMENT.md 中前端目录结构和组件清单 diff --git a/openspec/changes/frontend-architecture-refactor/specs/meta-api/spec.md b/openspec/changes/frontend-architecture-refactor/specs/meta-api/spec.md new file mode 100644 index 0000000..344ade2 --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/specs/meta-api/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Meta 信息 API +系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据。 + +#### Scenario: 获取 checker 类型列表 +- **WHEN** 客户端请求 `GET /api/meta` +- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符(如 `["http", "command"]`) + +#### Scenario: 类型列表来源 +- **WHEN** 系统启动并注册了 checker +- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致 + +#### Scenario: 仅允许 GET/HEAD 方法 +- **WHEN** 客户端使用 POST/PUT/DELETE 等方法请求 `/api/meta` +- **THEN** 系统 SHALL 返回 405 状态码 + +#### Scenario: HEAD 请求返回空体 +- **WHEN** 客户端使用 HEAD 方法请求 `/api/meta` +- **THEN** 系统 SHALL 返回 200 状态码和正确的 Content-Type header,body 为空 + +### Requirement: MetaResponse 共享类型 +系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。 + +#### Scenario: MetaResponse 类型定义 +- **WHEN** 前后端引用 `MetaResponse` 类型 +- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段 diff --git a/openspec/changes/frontend-architecture-refactor/specs/tanstack-query-data-layer/spec.md b/openspec/changes/frontend-architecture-refactor/specs/tanstack-query-data-layer/spec.md new file mode 100644 index 0000000..84756a3 --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/specs/tanstack-query-data-layer/spec.md @@ -0,0 +1,109 @@ +## MODIFIED Requirements + +### Requirement: TanStack Query 数据层 +前端 SHALL 使用 TanStack Query(@tanstack/react-query)管理所有 API 请求,数据层代码 SHALL 按职责拆分为独立 hook 文件。 + +#### Scenario: QueryClient 配置 +- **WHEN** 应用启动 +- **THEN** 系统 SHALL 创建 QueryClient,默认配置 retry=1、refetchOnWindowFocus=true、staleTime=5000 + +#### Scenario: QueryClientProvider 挂载 +- **WHEN** 应用渲染 +- **THEN** 根组件 SHALL 包裹在 QueryClientProvider 中,提供 QueryClient 实例 + +### Requirement: queryKey 工厂 +系统 SHALL 提供统一的 queryKey 工厂函数,确保 queryKey 的唯一性和一致性。 + +#### Scenario: summary queryKey +- **WHEN** 查询 summary 数据 +- **THEN** queryKey SHALL 为 ["summary"] + +#### Scenario: targets queryKey +- **WHEN** 查询 targets 数据 +- **THEN** queryKey SHALL 为 ["targets"] + +#### Scenario: meta queryKey +- **WHEN** 查询 meta 数据 +- **THEN** queryKey SHALL 为 ["meta"] + +#### Scenario: trend queryKey +- **WHEN** 查询某目标的趋势数据 +- **THEN** queryKey SHALL 为 ["trend", targetId, from, to] + +#### Scenario: history queryKey +- **WHEN** 查询某目标的历史记录 +- **THEN** queryKey SHALL 为 ["history", targetId, from, to, page] + +### Requirement: Hook 文件拆分 +数据层 hook SHALL 按职责拆分为独立文件。 + +#### Scenario: 全局查询 hook 文件 +- **WHEN** 开发者需要使用全局面板级查询 +- **THEN** `useSummary`、`useTargets`、`useMeta` SHALL 从 `hooks/use-queries.ts` 导出 + +#### Scenario: Drawer 状态 hook 文件 +- **WHEN** 开发者需要使用 Drawer 状态管理 +- **THEN** `useTargetDetail` SHALL 从 `hooks/use-target-detail.ts` 导出 + +#### Scenario: fetchJson 不导出 +- **WHEN** 数据层内部需要 fetch 封装 +- **THEN** `fetchJson` SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出 + +#### Scenario: queryKeys 不导出 +- **WHEN** 数据层内部需要 query key +- **THEN** `queryKeys` 对象 SHALL 定义在 `use-queries.ts` 内部,不作为公共 API 导出 + +### Requirement: Meta 查询 +系统 SHALL 提供 `useMeta` hook 查询系统元数据。 + +#### Scenario: meta 查询配置 +- **WHEN** 应用启动 +- **THEN** `useMeta` SHALL 请求 `/api/meta`,配置 `staleTime: Infinity`(应用生命周期内只请求一次) + +#### Scenario: meta 数据返回 +- **WHEN** meta 查询成功 +- **THEN** hook SHALL 返回 `MetaResponse` 类型数据,包含 `checkerTypes` 字段 + +### Requirement: Summary 轮询查询 +系统 SHALL 使用 useQuery 实现总览统计的自动轮询。 + +#### Scenario: summary 自动轮询 +- **WHEN** Dashboard 页面处于打开状态 +- **THEN** 系统 SHALL 每 8 秒自动请求 /api/summary,使用 refetchInterval=8000 + +#### Scenario: summary 后台刷新 +- **WHEN** 页面处于后台标签页 +- **THEN** 系统 SHALL 暂停轮询(refetchIntervalInBackground=false) + +### Requirement: Targets 轮询查询 +系统 SHALL 使用 useQuery 实现目标列表的自动轮询。 + +#### Scenario: targets 自动轮询 +- **WHEN** Dashboard 页面处于打开状态 +- **THEN** 系统 SHALL 每 8 秒自动请求 /api/targets,使用 refetchInterval=8000 + +### Requirement: 条件查询 +趋势和历史记录查询 SHALL 使用 enabled 条件控制,仅在目标被选中时触发。 + +#### Scenario: 未选中目标时不请求 +- **WHEN** 用户未点击任何目标表格行 +- **THEN** trend 和 history 的 useQuery SHALL enabled=false,不发起请求 + +#### Scenario: 选中目标时自动请求 +- **WHEN** 用户点击目标表格行 +- **THEN** trend 和 history 的 useQuery SHALL enabled=true,自动发起请求 + +#### Scenario: 时间范围变化时重新请求 +- **WHEN** 用户更改时间范围 +- **THEN** trend 和 history 的 useQuery SHALL 因 queryKey 变化自动重新请求 + +### Requirement: 开发调试面板 +开发环境下 SHALL 挂载 TanStack Query Devtools。 + +#### Scenario: 开发环境显示 Devtools +- **WHEN** 应用在开发模式下运行 +- **THEN** 页面 SHALL 显示 ReactQueryDevtools 浮动面板 + +#### Scenario: 生产环境排除 Devtools +- **WHEN** 应用在生产模式下构建 +- **THEN** ReactQueryDevtools SHALL 不被包含在产物中 diff --git a/openspec/changes/frontend-architecture-refactor/specs/target-detail-drawer/spec.md b/openspec/changes/frontend-architecture-refactor/specs/target-detail-drawer/spec.md new file mode 100644 index 0000000..0c21787 --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/specs/target-detail-drawer/spec.md @@ -0,0 +1,91 @@ +## MODIFIED Requirements + +### Requirement: 目标详情 Drawer +Dashboard SHALL 在用户点击目标表格行后从右侧滑出 Drawer,展示该目标的详细统计信息和检查记录。Drawer 标题栏和内容不使用内联 style。Drawer 内容 SHALL 拆分为独立的 Tab 组件。 + +#### Scenario: 打开 Drawer +- **WHEN** 用户点击某个目标表格行 +- **THEN** 系统 SHALL 从右侧滑出 Drawer(placement="right"),宽度为视口 60% + +#### Scenario: Drawer 标题栏 +- **WHEN** Drawer 渲染 +- **THEN** 标题栏 SHALL 使用 TDesign Space 组件(align="center")布局,包含 StatusDot、目标名称(TDesign Typography.Text strong)和类型标签(TDesign Tag,直接显示 target.type 原始文本),以及内建关闭按钮。不使用内联 style 的 flex 布局 + +#### Scenario: 关闭 Drawer +- **WHEN** 用户点击关闭按钮、ESC 键或遮罩层 +- **THEN** Drawer SHALL 关闭 + +#### Scenario: Drawer 无底部按钮 +- **WHEN** Drawer 渲染 +- **THEN** Drawer SHALL 不显示底部操作栏(footer={false}) + +#### Scenario: Drawer 数据同步 +- **WHEN** Drawer 打开期间后台轮询刷新了 targets 数据 +- **THEN** Drawer 中 selectedTarget 的状态 SHALL 随之同步更新 + +#### Scenario: 切换目标重置 Tab +- **WHEN** 用户从目标 A 切换到目标 B(点击不同的表格行) +- **THEN** Drawer SHALL 重置为概览 Tab,使用 key={target.id} 确保组件状态不残留 + +#### Scenario: Drawer 内容区间距 +- **WHEN** Drawer 内容渲染 +- **THEN** 时间选择器、Tabs 等区块之间的间距 SHALL 通过 TDesign Space 组件(direction="vertical", size={16})统一管理,不使用内联 style 的 marginBottom + +### Requirement: 概览面板组件化 +概览 Tab SHALL 作为独立组件 `OverviewTab` 实现,接收数据 props 进行渲染。 + +#### Scenario: OverviewTab 组件职责 +- **WHEN** 概览 Tab 渲染 +- **THEN** `OverviewTab` 组件 SHALL 负责统计卡片、趋势图、状态分布环形图和基本信息的渲染 + +#### Scenario: 统计计算使用纯函数 +- **WHEN** OverviewTab 需要计算 totalChecks、upChecks、downChecks +- **THEN** 计算逻辑 SHALL 通过 `utils/stats.ts` 中的纯函数实现,并使用 `useMemo` 缓存结果 + +#### Scenario: OverviewTab props +- **WHEN** OverviewTab 渲染 +- **THEN** 组件 SHALL 接收 `target: TargetStatus`、`trendData: TrendPoint[]`、`trendLoading: boolean` 作为 props + +### Requirement: 记录面板组件化 +记录 Tab SHALL 作为独立组件 `HistoryTab` 实现。 + +#### Scenario: HistoryTab 组件职责 +- **WHEN** 记录 Tab 渲染 +- **THEN** `HistoryTab` 组件 SHALL 负责检查结果表格和分页的渲染 + +#### Scenario: HistoryTab props +- **WHEN** HistoryTab 渲染 +- **THEN** 组件 SHALL 接收 `historyData: HistoryResponse`、`historyLoading: boolean`、`onPageChange: (page: number) => void` 作为 props + +#### Scenario: 历史记录列定义外置 +- **WHEN** HistoryTab 渲染表格 +- **THEN** 列定义 SHALL 从 `constants/history-table-columns.tsx` 导入,不在组件内部定义 + +### Requirement: TrendChart 简化 +TrendChart 组件 SHALL 仅接收数据 props,不处理 loading 状态。 + +#### Scenario: TrendChart 无 loading prop +- **WHEN** TrendChart 渲染 +- **THEN** 组件 SHALL 仅接收 `data: TrendPoint[]` prop,不接收 `loading` prop + +#### Scenario: TrendChart 空数据 +- **WHEN** TrendChart 接收空数组 +- **THEN** 组件 SHALL 显示"暂无趋势数据"占位文本 + +### Requirement: StatusDonut key 修复 +StatusDonut 组件 SHALL 使用语义化的 key。 + +#### Scenario: Pie Cell key +- **WHEN** StatusDonut 渲染 Pie Cell 列表 +- **THEN** 每个 Cell 的 key SHALL 使用 data item 的 `name` 字段,不使用数组索引 + +### Requirement: StatusBar 参数化 +StatusBar 组件 SHALL 支持可配置的格数。 + +#### Scenario: maxSlots prop +- **WHEN** StatusBar 渲染 +- **THEN** 组件 SHALL 接收可选的 `maxSlots` prop(默认 30),根据该值渲染对应数量的格子 + +#### Scenario: 格子渲染逻辑 +- **WHEN** StatusBar 渲染且 samples 数量少于 maxSlots +- **THEN** 多余的格子 SHALL 显示为 empty 状态 diff --git a/openspec/changes/frontend-architecture-refactor/specs/target-table/spec.md b/openspec/changes/frontend-architecture-refactor/specs/target-table/spec.md new file mode 100644 index 0000000..32cdc22 --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/specs/target-table/spec.md @@ -0,0 +1,69 @@ +## MODIFIED Requirements + +### Requirement: 表格列定义 +每个分组的 PrimaryTable SHALL 包含状态、名称、类型、可用率、最近状态条、延迟、间隔 7 列,不含分组列(同组内冗余)。列渲染不使用内联 style。列定义 SHALL 通过工厂函数动态生成。 + +#### Scenario: 状态列 +- **WHEN** 表格渲染 +- **THEN** 状态列 SHALL 使用 StatusDot 组件渲染,标题显示"#",宽度 60px,fixed="left",居中对齐,支持筛选(UP/DOWN/全部)。StatusDot SHALL 通过 CSS 类(`.status-dot--up` / `.status-dot--down`)控制颜色,不使用内联 style + +#### Scenario: 名称列 +- **WHEN** 表格渲染 +- **THEN** 名称列 SHALL 显示目标名称,支持字母排序(zh-CN),ellipsis 超长名称自动省略并 Tooltip 显示全名 + +#### Scenario: 类型列 +- **WHEN** 表格渲染 +- **THEN** 类型列 SHALL 使用 TDesign Tag 组件(size=small, theme=primary, variant=light-outline)直接显示 target.type 原始文本,支持单选筛选 + +#### Scenario: 类型筛选器动态生成 +- **WHEN** 表格渲染 +- **THEN** 类型列的筛选器列表 SHALL 从 meta API 返回的 `checkerTypes` 动态生成,包含"全部"选项和每个 checker 类型选项(label 和 value 均为 type 原始文本) + +#### Scenario: 可用率列 +- **WHEN** 表格渲染 +- **THEN** 可用率列 SHALL 使用 TDesign Progress 组件(theme=line, size=small)渲染,颜色通过 CSS 自定义属性 `--avail-N`(基于项目自定义色值)控制,每 10% 一档,label 显示百分比数值,支持排序(升序优先,最差排最前)。color-threshold 函数 SHALL 返回 CSS 自定义属性引用而非硬编码色值 + +#### Scenario: 最近状态列 +- **WHEN** 表格渲染 +- **THEN** 最近状态列 SHALL 使用 StatusBar 组件渲染采样色块,宽度 220px。StatusBar SHALL 通过 CSS 类(`.status-bar-block--up` / `.status-bar-block--down` / `.status-bar-block--empty`)控制色块颜色,不使用内联 style + +#### Scenario: 延迟列 +- **WHEN** 表格渲染 +- **THEN** 延迟列 SHALL 显示最近一次检查的延迟毫秒数,右对齐。颜色 SHALL 通过 CSS 类实现:≤100ms 使用 `.latency-ok`、100-500ms 使用 `.latency-warn`、>500ms 使用 `.latency-error`。无数据 SHALL 使用 `.text-disabled` 类显示 "-",数值 SHALL 使用 `.tabular-nums` 类等宽显示。不使用内联 style + +#### Scenario: 间隔列 +- **WHEN** 表格渲染 +- **THEN** 间隔列 SHALL 显示检查间隔(如 "5s"、"30s"),居中对齐,宽度 72px + +### Requirement: 列定义工厂函数 +列定义 SHALL 通过工厂函数生成,接收动态参数。 + +#### Scenario: createTargetTableColumns 函数 +- **WHEN** 需要生成表格列定义 +- **THEN** 系统 SHALL 调用 `createTargetTableColumns(checkerTypes: string[])` 函数,返回 `PrimaryTableCol[]` + +#### Scenario: checkerTypes 为空数组 +- **WHEN** meta API 尚未返回或返回空数组 +- **THEN** 类型列的筛选器 SHALL 仅包含"全部"选项 + +#### Scenario: 列定义缓存 +- **WHEN** TargetBoard 组件渲染 +- **THEN** 列定义 SHALL 通过 `useMemo` 缓存,仅在 `checkerTypes` 变化时重新生成 + +### Requirement: TargetGroup 接收 columns prop +TargetGroup 组件 SHALL 通过 prop 接收列定义,不再直接导入静态常量。 + +#### Scenario: columns prop +- **WHEN** TargetGroup 渲染 +- **THEN** 组件 SHALL 接收 `columns: PrimaryTableCol[]` prop 并传递给 PrimaryTable + +#### Scenario: TargetBoard 传递 columns +- **WHEN** TargetBoard 渲染子组件 +- **THEN** TargetBoard SHALL 调用 `createTargetTableColumns` 生成列定义并传递给每个 TargetGroup + +### Requirement: 列定义复用 +所有分组的表格 SHALL 共享同一套列定义常量。 + +#### Scenario: 列定义提取为常量 +- **WHEN** 多个分组表格渲染 +- **THEN** 列定义 SHALL 从独立的 constants/target-table-columns.tsx 导入,不在组件中重复定义 diff --git a/openspec/changes/frontend-architecture-refactor/specs/target-type-display/spec.md b/openspec/changes/frontend-architecture-refactor/specs/target-type-display/spec.md new file mode 100644 index 0000000..f31e5ec --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/specs/target-type-display/spec.md @@ -0,0 +1,13 @@ +## REMOVED Requirements + +### Requirement: 类型显示名称映射 +**Reason**: 前端不再维护 type → label 的静态映射,直接使用后端返回的 type 原始文本展示。类型筛选器列表改由 meta API 动态驱动。 +**Migration**: 所有使用 `getTargetTypeDisplay(type)` 的位置改为直接使用 `type` 字符串。`TARGET_TYPE_DISPLAY` 常量和 `target-type-display.ts` 文件删除。 + +### Requirement: 映射可扩展性 +**Reason**: 不再需要前端映射扩展机制,新增 checker 类型时后端注册即自动通过 meta API 暴露给前端。 +**Migration**: 无需迁移,删除即可。 + +### Requirement: 类型安全 +**Reason**: 不再有映射常量,无需 TypeScript 类型推导和 fallback 逻辑。 +**Migration**: 无需迁移,删除即可。 diff --git a/openspec/changes/frontend-architecture-refactor/tasks.md b/openspec/changes/frontend-architecture-refactor/tasks.md new file mode 100644 index 0000000..ed61c46 --- /dev/null +++ b/openspec/changes/frontend-architecture-refactor/tasks.md @@ -0,0 +1,48 @@ +## 1. 后端 Meta API + +- [ ] 1.1 在 `src/shared/api.ts` 中新增 `MetaResponse` 类型定义 +- [ ] 1.2 创建 `src/server/routes/meta.ts`,实现 `handleMeta` 从 `checkerRegistry.supportedTypes` 返回数据 +- [ ] 1.3 在 `src/server/app.ts` 中注册 `/api/meta` 路由 +- [ ] 1.4 在 `tests/server/app.test.ts` 中添加 `/api/meta` 端点测试(GET/HEAD/405) + +## 2. 前端 Hook 拆分 + +- [ ] 2.1 创建 `src/web/hooks/use-queries.ts`,迁入 `queryKeys`、`fetchJson`、`useSummary`、`useTargets`,新增 `useMeta` +- [ ] 2.2 重写 `src/web/hooks/use-target-detail.ts`,仅保留 Drawer 状态管理和条件查询(trend/history) +- [ ] 2.3 更新 `src/web/app.tsx` 的 import 路径适配新 hook 文件 + +## 3. 前端组件拆分 + +- [ ] 3.1 创建 `src/web/utils/stats.ts`,提取统计计算纯函数(`computeTrendStats`) +- [ ] 3.2 创建 `src/web/constants/history-table-columns.tsx`,将 `HISTORY_COLUMNS` 从 Drawer 中移出 +- [ ] 3.3 创建 `src/web/components/OverviewTab.tsx`,从 TargetDetailDrawer 中提取概览面板逻辑 +- [ ] 3.4 创建 `src/web/components/HistoryTab.tsx`,从 TargetDetailDrawer 中提取记录面板逻辑 +- [ ] 3.5 精简 `src/web/components/TargetDetailDrawer.tsx`,仅保留 Drawer 壳 + 时间选择 + Tab 切换 + +## 4. 类型筛选器动态化 + +- [ ] 4.1 将 `src/web/constants/target-table-columns.tsx` 中的 `TARGET_TABLE_COLUMNS` 改为工厂函数 `createTargetTableColumns(checkerTypes)` +- [ ] 4.2 从 `src/web/constants/target-table-filters.ts` 中移除 `typeFilter`(`statusFilter` 保留) +- [ ] 4.3 更新 `src/web/components/TargetBoard.tsx`,调用 `useMeta` + `useMemo` 生成列定义并传递给 TargetGroup +- [ ] 4.4 更新 `src/web/components/TargetGroup.tsx`,新增 `columns` prop 替代静态导入 +- [ ] 4.5 删除 `src/web/constants/target-type-display.ts` +- [ ] 4.6 更新 `src/web/components/TargetDetailDrawer.tsx` 标题栏,直接使用 `target.type` 替代 `getTargetTypeDisplay` + +## 5. 小问题修复 + +- [ ] 5.1 修复 `StatusDonut.tsx`:Cell key 从 `index` 改为 `data[index].name` +- [ ] 5.2 修复 `StatusBar.tsx`:新增 `maxSlots` prop(默认 30),用 prop 驱动格数渲染 +- [ ] 5.3 修复 `TrendChart.tsx`:移除 `loading` prop,仅保留 `data` prop + +## 6. 测试补充与更新 + +- [ ] 6.1 创建 `tests/web/utils/time.test.ts`,测试 `subtractHours`(正常、跨天、跨月、0 小时) +- [ ] 6.2 创建 `tests/web/utils/stats.test.ts`,测试 `computeTrendStats` 纯函数 +- [ ] 6.3 更新 `tests/web/constants/target-table-filters.test.ts`,移除 `typeFilter` 相关测试 +- [ ] 6.4 删除 `tests/web/constants/target-type-display.test.ts` +- [ ] 6.5 创建 `tests/web/constants/target-table-columns.test.ts`,测试 `createTargetTableColumns` 工厂函数 + +## 7. 质量保障与文档 + +- [ ] 7.1 执行 `bun run check` 确保类型检查、lint、测试全部通过 +- [ ] 7.2 更新 DEVELOPMENT.md 中前端目录结构、组件清单和 hook 说明 diff --git a/openspec/changes/http-checker-quality-hardening/.openspec.yaml b/openspec/changes/http-checker-quality-hardening/.openspec.yaml new file mode 100644 index 0000000..93831bd --- /dev/null +++ b/openspec/changes/http-checker-quality-hardening/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-13 diff --git a/openspec/changes/http-checker-quality-hardening/design.md b/openspec/changes/http-checker-quality-hardening/design.md new file mode 100644 index 0000000..ecafdc5 --- /dev/null +++ b/openspec/changes/http-checker-quality-hardening/design.md @@ -0,0 +1,93 @@ +## Context + +HTTP checker 是 DiAL 拨测系统的核心 runner 之一,负责对 HTTP 目标执行请求并校验响应。经审查发现以下质量问题: + +1. **actual 值截断格式不符合 spec**:spec 要求 failure 中的 actual 摘要需截断并附带字符计数,但当前 `truncateActual` 函数只加省略号无计数,导致用户无法判断原始响应体规模。 +2. **ReDoS 风险**:用户配置的 regex body 规则和 match operator 直接对大响应体执行 `new RegExp().test()`,恶意或不当正则可能导致 CPU 阻塞。 +3. **JSON 重复解析**:多条 json body 规则各自独立调用 `JSON.parse(body)`,对大 JSON 响应体造成不必要的重复开销。 +4. **CSS 规则分支冗余**:`checkCssRule` 中"无 operator 时检查元素存在"和"exists: true"是重复逻辑。 +5. **重定向测试不足**:303、307/308、相对路径 Location 等分支缺少测试覆盖。 + +当前代码结构: +- `src/server/checker/expect/failure.ts` — failure 构造函数 +- `src/server/checker/runner/http/body.ts` — body 规则检查 +- `src/server/checker/runner/http/execute.ts` — HTTP 执行主流程 +- `src/server/checker/expect/operator.ts` — operator 匹配逻辑 + +## Goals / Non-Goals + +**Goals:** +- 实现 failure actual 值截断,满足 spec 要求 +- 消除 regex 相关的 ReDoS 风险 +- 优化多条 JSON 规则的解析性能 +- 精简冗余代码分支 +- 补全重定向和集成测试覆盖 + +**Non-Goals:** +- 不改变 CheckResult / CheckFailure 的类型结构(截断在构造时完成,对外接口不变) +- 不引入新依赖 +- 不改变 HTTP checker 的功能行为(纯内部质量改进) +- 不添加 response timing 分段记录(暂缓) +- 不添加重试机制(拨测场景下重试会掩盖网络问题信号) + +## Decisions + +### Decision 1: actual 截断在 mismatchFailure 构造点统一实施 + +**选择**:在 `expect/failure.ts` 的 `mismatchFailure` 函数内部对 actual 参数截断,阈值 200 字符。 + +**替代方案**: +- 在存储层(store.ts insertCheckResult)截断 — 但这样 API 实时返回的 failure 仍然很大 +- 在每个调用点手动截断 — 分散且容易遗漏 + +**理由**:构造点截断是最集中的拦截位置,所有 mismatch failure 都经过此函数,一处修改全局生效。expected 值不截断(来自用户配置,通常很短)。 + +**截断格式**:`<前 200 字符>…(共 N 字符)` — 保留前缀便于诊断,附带总长度便于判断规模(省略号为单字符 U+2026)。 + +### Decision 2: ReDoS 防护使用正则复杂度静态检测 + +**选择**:在启动期 validate 阶段对 regex body 规则和 match operator 进行静态复杂度检测,拒绝含有嵌套量词等危险模式的正则。运行期不做额外防护。 + +**替代方案**: +- 运行期用 AbortSignal + setTimeout 强制中断 — Bun 的 RegExp 执行不可中断,无法实现 +- 使用 safe-regex 库 — 引入新依赖,违反项目规范 +- 限制正则执行的输入长度 — 会影响正常大响应体的匹配 + +**理由**:自行实现轻量级检测函数,检查常见 ReDoS 模式(嵌套量词 `(a+)+`、重叠交替 `(a|a)*`)。在 validate 阶段拒绝危险正则,比运行期防护更可靠——配置错误应该在启动时暴露。 + +**检测规则**: +- 嵌套量词:量词内包含量词(如 `(a+)+`、`(a*)*`、`(a+)*`) +- 重叠字符类交替后跟量词:`(x|x)+` 模式 + +### Decision 3: JSON parse 结果缓存在 checkBodyExpect 层 + +**选择**:在 `checkBodyExpect` 函数中,首次遇到 json 规则时执行 `JSON.parse`,将结果缓存并传递给后续 json 规则复用。 + +**实现方式**:修改 `checkSingleBodyRule` 签名,接受可选的 `parsedJson` 参数;在 `checkBodyExpect` 循环中维护一个 `let parsedJson: { ok: boolean; value?: unknown; error?: string }` 状态。 + +**理由**:改动最小,不改变外部接口,只在内部传递缓存。对于非 json 规则(contains、regex、css、xpath)无影响。 + +### Decision 4: CSS 规则分支合并策略 + +**选择**:将 `checkCssRule` 重构为线性流程: +1. 解析 HTML +2. 处理 `exists: false`(元素不存在即通过) +3. 查找元素(不存在则失败) +4. 处理 `exists: true`(到这里已确认存在,直接通过) +5. 提取值(attr 或 text) +6. 无 operator 时检查值非 undefined 即通过 +7. 有 operator 时执行匹配 + +**理由**:消除当前三层嵌套判断中的重复逻辑,使控制流线性化,更易理解和维护。 + +### Decision 5: execute.ts 提前 duration 检查保留但加注释 + +**选择**:保留第 56-74 行的提前 duration 检查逻辑(它是有效的性能优化——避免读取注定超时的 body),但重构为独立的 helper 函数使意图更明确。 + +**理由**:删除它会导致超时场景下仍然读取完整 body 后才报错,浪费网络带宽和时间。提取为 `checkEarlyTimeout` 函数名即可自解释。 + +## Risks / Trade-offs + +- **ReDoS 静态检测的误报**:过于严格的检测可能拒绝合法但看起来复杂的正则。→ 缓解:只检测最常见的嵌套量词模式,不做过度分析;提供清晰的错误信息指导用户修改。 +- **actual 截断丢失诊断信息**:截断后用户无法看到完整 actual 值。→ 缓解:200 字符的前缀通常足够定位问题;如需完整响应体,用户应直接请求目标 URL 查看。 +- **JSON parse 缓存的内存占用**:对于大 JSON 响应体,缓存的 parsed 对象会在整个 body rules 检查期间驻留内存。→ 缓解:这是短暂的(单次检查周期内),且原本每条规则都会各自 parse 一份,缓存反而减少了峰值内存。 diff --git a/openspec/changes/http-checker-quality-hardening/proposal.md b/openspec/changes/http-checker-quality-hardening/proposal.md new file mode 100644 index 0000000..fb66e9a --- /dev/null +++ b/openspec/changes/http-checker-quality-hardening/proposal.md @@ -0,0 +1,28 @@ +## Why + +HTTP checker 经过审查发现若干质量问题:failure 中 actual 值截断格式不符合 spec 要求(缺少字符计数)导致诊断信息不完整、regex 规则缺少 ReDoS 防护存在 CPU 阻塞风险、多条 JSON body 规则重复 parse 造成不必要开销、CSS 规则分支冗余、重定向测试覆盖不足。需要统一修复以提升健壮性和代码质量。 + +## What Changes + +- 修正 `mismatchFailure` 中 actual 值截断格式,添加字符计数信息,格式为 `前 N 字符…(共 M 字符)` +- 为 regex body 规则和 match operator 添加 ReDoS 防护(执行超时或正则复杂度检测) +- 优化多条 JSON body 规则共享同一次 `JSON.parse` 结果,避免重复解析 +- 精简 `body.ts` 中 `checkCssRule` 的冗余分支逻辑 +- 精简 `execute.ts` 中提前 duration 检查的代码结构 +- 补充重定向相关测试:303 method 转换、307/308 保持 method、相对路径 Location、混合 body rules 集成测试 + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `expect-body-checkers`: 新增 actual 值截断的具体实现要求(spec 已声明但未细化截断阈值和格式) + +## Impact + +- `src/server/checker/expect/failure.ts` — 新增截断逻辑 +- `src/server/checker/runner/http/body.ts` — JSON parse 优化、CSS 分支精简 +- `src/server/checker/runner/http/execute.ts` — duration 检查精简 +- `src/server/checker/expect/operator.ts` — match operator ReDoS 防护 +- `tests/server/checker/runner/http/runner.test.ts` — 补充重定向和集成测试 +- `tests/server/checker/runner/shared/body.test.ts` — 补充截断相关测试 diff --git a/openspec/changes/http-checker-quality-hardening/specs/expect-body-checkers/spec.md b/openspec/changes/http-checker-quality-hardening/specs/expect-body-checkers/spec.md new file mode 100644 index 0000000..f4530b7 --- /dev/null +++ b/openspec/changes/http-checker-quality-hardening/specs/expect-body-checkers/spec.md @@ -0,0 +1,101 @@ +## MODIFIED Requirements + +> 注:仅展示变更的 scenarios,其余 scenarios 保持不变 + +### Requirement: 结构化 expect 失败信息 +系统 SHALL 在任一 expect 规则失败时生成结构化 failure,用于标识失败阶段、规则路径、期望值、实际值和可读错误信息。actual 值 SHALL 在构造时截断至不超过 200 字符,超出部分以省略标记和总字符数替代。expected 值不截断。 + +#### Scenario: body 规则失败信息 +- **WHEN** HTTP target 的 `expect.body[1].json` 规则失败 +- **THEN** failure SHALL 包含 kind=`mismatch`、phase=`body`、path 指向 `expect.body[1]`,并包含 message + +#### Scenario: actual 值截断 +- **WHEN** 失败规则的实际值为字符串且长度超过 200 字符 +- **THEN** failure.actual SHALL 为前 200 字符加 `…(共 N 字符)` 后缀,其中 N 为原始总字符数 + +#### Scenario: actual 值未超限 +- **WHEN** 失败规则的实际值为字符串且长度不超过 200 字符 +- **THEN** failure.actual SHALL 保留完整原始值,不做截断 + +#### Scenario: actual 值为对象或数组 +- **WHEN** 失败规则的实际值为对象或数组,且 JSON 序列化后长度超过 200 字符 +- **THEN** failure.actual SHALL 为序列化后前 200 字符加 `…(共 N 字符)` 后缀 + +#### Scenario: actual 值为标量 +- **WHEN** 失败规则的实际值为 number、boolean、null 或 undefined +- **THEN** failure.actual SHALL 保留原始值,不做截断 + +### Requirement: HTTP expect 规则启动期校验 +系统 SHALL 在启动期校验 HTTP expect 中已支持字段的类型、格式、未知字段和可编译表达式。HTTP expect、body rule、json/css/xpath rule 和 operator 对象中的未知字段 SHALL 导致启动期配置失败。每个 body rule 对象 MUST 恰好包含 contains、regex、json、css、xpath 中的一种规则类型。纯 operator 对象 MUST 至少包含一个已知 operator;body 提取规则可以不配置 operator,并以路径、元素或节点存在作为通过语义。`equals` operator SHALL 支持任意 JSON value,包括数组和对象。系统 SHALL 在启动期对 regex body 规则和 match operator 的正则表达式进行 ReDoS 安全检测,含有嵌套量词等危险模式的正则 SHALL 导致启动期配置失败。 + +#### Scenario: body rule 使用 regex 字段 +- **WHEN** HTTP target 配置 `expect.body: [{regex: "ok|healthy"}]` 且 regex 可编译且无 ReDoS 风险 +- **THEN** 系统 SHALL 接受该配置,并在运行期按 regex body 规则匹配响应体 + +#### Scenario: body rule 不支持 match 字段 +- **WHEN** HTTP target 配置 `expect.body: [{match: "ok"}]` 且该规则没有 contains、regex、json、css、xpath 任一支持字段 +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: body rule 未知字段启动失败 +- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok", note: "ignored"}]` +- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `note` 是未知字段 + +#### Scenario: body rule 多支持字段非法 +- **WHEN** HTTP target 的同一条 body rule 同时配置 contains 和 regex +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: operator match 正则非法 +- **WHEN** HTTP target 的 expect.headers、json、css 或 xpath operator 配置了不可编译的 match 正则 +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: operator 数值比较类型非法 +- **WHEN** HTTP target 的 expect operator 配置 gt、gte、lt 或 lte,且对应值不是有限数字 +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: operator 布尔类型非法 +- **WHEN** HTTP target 的 expect operator 配置 empty 或 exists,且对应值不是布尔值 +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: JSONPath 子集非法 +- **WHEN** HTTP target 的 json body rule path 不符合系统支持的 JSONPath 子集 +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: operator 未知字段非法 +- **WHEN** HTTP target 的 expect operator 配置了 `foo: "bar"` 等未知 operator 字段 +- **THEN** 系统 SHALL 在启动期配置校验失败 + +#### Scenario: equals 支持对象 +- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.payload", equals: {status: "ok"}}}]` +- **THEN** 系统 SHALL 接受该配置,并在运行期使用深度相等比较提取值和对象期望 + +#### Scenario: equals 支持数组 +- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.items", equals: ["a", "b"]}}]` +- **THEN** 系统 SHALL 接受该配置,并在运行期使用深度相等比较提取值和数组期望 + +#### Scenario: 纯 operator 对象不能为空 +- **WHEN** HTTP target 的 `expect.headers` 中某个 header 期望配置为空对象 `{}` +- **THEN** 系统 SHALL 在启动期配置校验失败,要求显式配置至少一个 operator + +#### Scenario: json rule 允许存在性语义 +- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status"}}]` +- **THEN** 系统 SHALL 接受该配置,并在运行期以 JSONPath 值存在作为通过语义 + +#### Scenario: css rule 未知字段非法 +- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "h1", unknown: true}}]` +- **THEN** 系统 SHALL 在启动期配置校验失败,提示未知字段 + +#### Scenario: xpath rule 未知字段非法 +- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/html/body", unknown: true}}]` +- **THEN** 系统 SHALL 在启动期配置校验失败,提示未知字段 + +#### Scenario: regex body 规则含嵌套量词启动失败 +- **WHEN** HTTP target 配置 `expect.body: [{regex: "(a+)+$"}]` +- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险 + +#### Scenario: match operator 含嵌套量词启动失败 +- **WHEN** HTTP target 的 expect operator 配置 `{match: "(\\d+)*x"}` +- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险 + +#### Scenario: 安全正则通过校验 +- **WHEN** HTTP target 配置 `expect.body: [{regex: "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"}]` +- **THEN** 系统 SHALL 接受该配置(无嵌套量词,无 ReDoS 风险) diff --git a/openspec/changes/http-checker-quality-hardening/tasks.md b/openspec/changes/http-checker-quality-hardening/tasks.md new file mode 100644 index 0000000..9ae0125 --- /dev/null +++ b/openspec/changes/http-checker-quality-hardening/tasks.md @@ -0,0 +1,38 @@ +## 1. failure actual 截断 + +- [ ] 1.1 修改 `src/server/checker/expect/failure.ts` 中 `truncateActual` 函数,截断后缀从 `...` 改为 `…(共 N 字符)`,其中省略号为单字符 U+2026 +- [ ] 1.2 更新 `tests/server/checker/runner/shared/failure.test.ts` 中截断相关测试断言,匹配新格式(检查省略号为单字符且带字符计数) + +## 2. ReDoS 防护 + +- [ ] 2.1 在 `src/server/checker/expect/` 下新增 `redos.ts`,实现 `isUnsafeRegex(pattern: string): boolean` 函数,检测嵌套量词模式 +- [ ] 2.2 在 `src/server/checker/runner/http/validate.ts` 的 `validateRegexRule` 和 `src/server/checker/expect/validate-operator.ts` 的 match 校验中调用 `isUnsafeRegex`,不安全时返回 issue +- [ ] 2.3 在 `tests/server/checker/runner/http/runner.test.ts` 中补充 ReDoS 正则启动校验失败的测试用例 +- [ ] 2.4 在 `tests/server/checker/runner/shared/` 下新增 `redos.test.ts`,覆盖常见 ReDoS 模式和安全正则的判定 + +## 3. JSON parse 优化 + +- [ ] 3.1 修改 `src/server/checker/runner/http/body.ts` 中 `checkBodyExpect` 函数,维护 parsedJson 缓存状态,首次 json 规则 parse 后复用结果 +- [ ] 3.2 修改 `checkJsonRule` 签名接受可选的预解析 JSON 对象,避免重复 `JSON.parse` +- [ ] 3.3 在 `tests/server/checker/runner/shared/body.test.ts` 中补充多条 json 规则共享 parse 结果的测试(验证行为正确性) + +## 4. CSS 规则精简 + +- [ ] 4.1 重构 `src/server/checker/runner/http/body.ts` 中 `checkCssRule` 为线性流程:解析 HTML → exists:false 短路 → 查找元素 → exists:true 短路 → 提取值 → operator 匹配 +- [ ] 4.2 确认 `tests/server/checker/runner/shared/body.test.ts` 中现有 CSS 测试全部通过 + +## 5. execute.ts 精简 + +- [ ] 5.1 将 `src/server/checker/runner/http/execute.ts` 第 56-74 行的提前 duration 检查提取为 `checkEarlyTimeout` 辅助函数,明确意图 + +## 6. 补充测试 + +- [ ] 6.1 在 `tests/server/checker/runner/http/runner.test.ts` 中补充 303 重定向 method 转 GET 的测试 +- [ ] 6.2 在 `tests/server/checker/runner/http/runner.test.ts` 中补充 307/308 保持原始 method 和 body 的测试 +- [ ] 6.3 在 `tests/server/checker/runner/http/runner.test.ts` 中补充相对路径 Location header 重定向的测试 +- [ ] 6.4 在 `tests/server/checker/runner/http/runner.test.ts` 中补充混合 body rules(contains + json + css)集成测试 + +## 7. 质量保障 + +- [ ] 7.1 执行完整测试套件 `bun test`、代码检查 `bun run lint`、格式检查 `bun run format:check` 确保无回归 +- [ ] 7.2 更新 DEVELOPMENT.md 中 ReDoS 校验相关说明(如有必要)