## Purpose

定义 TCP checker 的配置格式、连接执行、banner 读取、expect 校验、失败结构和状态摘要。

## Requirements

### Requirement: tcp target 配置
系统 SHALL 支持 `type: tcp` 的 target 配置，通过 `tcp.host` 和 `tcp.port` 描述目标 TCP 地址，并通过可选字段控制 banner 读取行为。

#### Scenario: 解析最简 tcp target
- **WHEN** YAML 中 target 配置 `type: tcp`、`tcp.host: "127.0.0.1"` 和 `tcp.port: 6379`
- **THEN** 系统 SHALL 将其解析为 tcp checker，并填充 `readBanner=false`、`bannerReadTimeout=2000`、`maxBannerBytes=4096`、interval、timeout、group 和 expect 配置

#### Scenario: tcp target 缺少 host
- **WHEN** YAML 中 target 配置 `type: tcp` 但缺少 `tcp.host`
- **THEN** 系统 SHALL 以配置错误退出，并提示该 target 缺少 tcp.host 字段

#### Scenario: tcp target 缺少 port
- **WHEN** YAML 中 target 配置 `type: tcp` 但缺少 `tcp.port`
- **THEN** 系统 SHALL 以配置错误退出，并提示该 target 缺少 tcp.port 字段

#### Scenario: tcp port 范围非法
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是 1 到 65535 之间的整数
- **THEN** 系统 SHALL 以配置错误退出，并提示 tcp.port 必须为合法 TCP 端口

#### Scenario: tcp defaults 覆盖 banner 参数
- **WHEN** YAML 中配置 `defaults.tcp.bannerReadTimeout: 1000` 和 `defaults.tcp.maxBannerBytes: "8KB"`
- **THEN** 未显式配置对应字段的 tcp target SHALL 使用 defaults.tcp 中的值

#### Scenario: per-target banner 参数覆盖 defaults
- **WHEN** defaults.tcp 配置了 banner 参数，且某个 tcp target 显式配置 `tcp.bannerReadTimeout` 或 `tcp.maxBannerBytes`
- **THEN** 该 target SHALL 使用自身 tcp 分组中的值

#### Scenario: tcp 序列化展示摘要
- **WHEN** 系统同步 tcp target 到 targets 表
- **THEN** `target` 展示摘要 SHALL 为 `<host>:<port>`，`config` JSON SHALL 包含 resolved 后的 host、port、readBanner、bannerReadTimeout 和 maxBannerBytes

### Requirement: tcp checker 执行
系统 SHALL 按 tcp target 配置建立 TCP 连接，记录完整执行耗时和 TCP observation，并在连接失败、超时或资源超限时产生结构化失败信息。

#### Scenario: TCP 连接成功
- **WHEN** tcp target 指向可连接的 TCP 服务，且未配置 expect 或 `expect.connected` 为 `true`
- **THEN** 系统 SHALL 记录 `matched=true`、`durationMs` 和包含 connected、connectTimeMs、banner 的 observation，并关闭 socket

#### Scenario: TCP 连接失败
- **WHEN** tcp target 指向不可连接的 host/port，且未配置 expect 或 `expect.connected` 为 `true`
- **THEN** 系统 SHALL 记录 `matched=false`，observation SHALL 包含 connected=false 和错误信息，failure 的 kind 为 `error`，phase 为 `connect`，message 包含可读连接失败原因

#### Scenario: 期望端口不可达且连接失败
- **WHEN** tcp target 配置 `expect.connected: false`，且 TCP 连接失败
- **THEN** 系统 SHALL 记录 `matched=true`，observation SHALL 包含 connected=false 和实际连接失败原因，API detail SHALL 展示实际连接失败原因摘要

#### Scenario: 期望端口不可达但连接成功
- **WHEN** tcp target 配置 `expect.connected: false`，但 TCP 连接成功
- **THEN** 系统 SHALL 记录 `matched=false`，observation SHALL 包含 connected=true，failure 的 kind 为 `mismatch`，phase 为 `connected`

#### Scenario: TCP 执行超时
- **WHEN** 引擎注入的 `ctx.signal` 在 TCP 连接或 banner 读取过程中 abort
- **THEN** 系统 SHALL best-effort 关闭 socket，记录 `matched=false`，failure 的 kind 为 `error`，phase 为 `connect` 或 `banner`，message 包含超时信息，并在可收集时记录 observation

#### Scenario: duration 包含 banner 读取
- **WHEN** tcp target 开启 `readBanner` 且服务端延迟发送 banner
- **THEN** 结果中的 `durationMs` SHALL 覆盖连接建立、banner 等待、banner 读取和 expect 校验的完整耗时

### Requirement: tcp banner 读取
系统 SHALL 仅在 `tcp.readBanner: true` 时读取服务端主动发送的 banner 数据，并同时受 `bannerReadTimeout` 和 `maxBannerBytes` 限制。

#### Scenario: 默认不读取 banner
- **WHEN** tcp target 未配置 `readBanner` 或配置为 `false`
- **THEN** 系统 SHALL 在连接建立后立即进入 connected 和 duration 校验，不等待服务端数据

#### Scenario: 读取服务端 banner
- **WHEN** tcp target 配置 `readBanner: true`，且服务端连接后发送 `220 smtp.example.com ESMTP`
- **THEN** 系统 SHALL 收集 banner 文本，并允许后续 `expect.banner` 对该文本执行 operator 断言

#### Scenario: banner 等待超时无数据
- **WHEN** tcp target 配置 `readBanner: true`，但服务端在 `bannerReadTimeout` 内未发送任何数据
- **THEN** 系统 SHALL 将 banner 视为空字符串并继续执行 expect 校验，不将无 banner 本身作为连接错误

#### Scenario: banner 读取超过最大字节数
- **WHEN** 服务端发送的 banner 数据超过 `maxBannerBytes`
- **THEN** 系统 SHALL 停止读取并记录 `matched=false`、failure.kind=`error`、failure.phase=`banner` 的结构化错误

#### Scenario: banner detail 截断展示
- **WHEN** tcp target 成功读取到较长 banner
- **THEN** observation.banner SHALL 保存截断后的 banner 摘要，API detail SHALL 展示截断后的 banner 摘要，避免 UI 展示过长文本

### Requirement: tcp expect 校验
系统 SHALL 支持 tcp 专属 expect，包括 `connected`、`banner` 和 `durationMs`，并按 connected、banner、durationMs 的阶段顺序快速失败。`connected` SHALL 保持布尔状态语义，未配置时在 Resolved expect 中默认 `true`。`banner` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`，且仅在 `tcp.readBanner: true` 时允许配置。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验包含连接和 banner 读取在内的完整执行耗时。

#### Scenario: 默认 connected 成功语义
- **WHEN** tcp target 未显式配置 `expect.connected`
- **THEN** 系统 SHALL 在 Resolved tcp expect 中使用默认 `connected: true` 进行校验

#### Scenario: durationMs 校验
- **WHEN** tcp target 配置 `expect.durationMs: {lte: 100}`，且完整执行耗时超过 100ms
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 phase 为 `duration`

#### Scenario: banner ContentExpectations 校验通过
- **WHEN** tcp target 配置 `readBanner: true`、`expect.banner: [{contains: "ESMTP"}]`，且实际 banner 包含 `ESMTP`
- **THEN** 系统 SHALL 判定 banner 阶段通过

#### Scenario: banner regex 校验失败
- **WHEN** tcp target 配置 `readBanner: true`、`expect.banner: [{regex: "^SSH-2\\.0"}]`，且实际 banner 不匹配该正则
- **THEN** 系统 SHALL 返回 `matched=false`，failure 的 kind 为 `mismatch`，phase 为 `banner`，path 指向失败的 banner expectation

#### Scenario: banner 多规则快速失败
- **WHEN** tcp target 配置两条 banner expectation 且第一条失败
- **THEN** 系统 SHALL 返回第一条失败 expectation 的 failure，并 MUST NOT 执行第二条 expectation

#### Scenario: expect.banner 未开启 readBanner
- **WHEN** tcp target 配置 `expect.banner`，但 `tcp.readBanner` 未配置为 `true`
- **THEN** 系统 SHALL 在启动期配置校验失败，提示 banner 断言需要启用 tcp.readBanner

#### Scenario: tcp expect 未知字段失败
- **WHEN** YAML 中 tcp target 的 expect 包含 `status: [200]`、`maxDurationMs: 1000` 或其他非 tcp expect 字段
- **THEN** 系统 SHALL 以配置错误退出，提示 expect 包含未知字段
