|
# StarAgent 一致性测试夹具(conformance)
本目录是**语言无关**的黑盒一致性测试:用 Go 实现一个假 StarServer(HTTP + WebSocket)和场景 runner,启动任意语言的 Agent 可执行文件,验证登录、心跳、命令、回复、重连、事件补报等协议行为。
协议唯一事实源见 [`../spec/protocol.md`](/NewLife/Stardust/Blob/master/StarAgent.Native/conformance/../spec/protocol.md),JSON 契约见 [`../spec/schemas/node-agent.schema.json`](/NewLife/Stardust/Blob/master/StarAgent.Native/conformance/../spec/schemas/node-agent.schema.json)。
## 目录结构
```
conformance/
├── cmd/conformance/ # CLI runner(含 mock-agent 子命令)
├── internal/fakeserver/ # 假 StarServer + /control/* 测试控制接口
├── internal/mockagent/ # 内置最小 mock Agent(仅用于夹具自测)
├── internal/protocol/ # 与 spec 对齐的 PascalCase JSON 模型
├── internal/scenario/ # 场景定义、断言、进程与配置注入
└── templates/agent.json # 配置模板示例
```
## 快速开始
```powershell
cd StarAgent.Native\conformance
# 1. 单元测试 + 用 mock Agent 跑全部场景
go test ./...
go run ./cmd/conformance --mock-agent --scenario all
# 2. 构建
go build -o conformance ./cmd/conformance
# 3. 测试真实 Agent(示例)
.\conformance.exe --agent C:\path\to\staragent.exe --config .\templates\agent.json --scenario all
```
Windows 下生成 `conformance.exe`;Linux/macOS 为 `conformance`。
## Agent 接入约定
### 启动方式
runner 默认执行:
```text
<agent> run --config <配置文件>
```
也可以用环境变量提供配置;示例:
```powershell
.\conformance.exe --agent .\staragent --agent-args "run --config {config}" --scenario basic
```
`--agent-args` 支持占位符:`{config}`、`{server}`、`{data}`、`{workdir}`。参数按空白拆分,不支持带空格的引号参数;路径含空格时建议直接用默认参数。
runner 会在 Agent 进程环境中继承当前环境,并额外设置:
| 变量 | 值 |
|------|----|
| `STARAGENT_SERVER` / `STARAGENT_SERVER_URL` | 假服务器地址,如 `http://127.0.0.1:54321` |
| `STARAGENT_DATA_DIR` / `STARAGENT_DATADIR` | 本次场景的数据目录 |
| `STARAGENT_CONFIG` / `STARAGENT_CONFIG_FILE` | 生成的配置文件路径 |
| `STARAGENT_PRODUCTCODE` / `STARAGENT_PRODUCT_CODE` | `StarAgent-Conformance` |
| `STARAGENT_WORKDIR` | 场景工作目录 |
用户自行设置的环境变量不会被清除。若 Agent 支持 `STARAGENT_*` 覆盖,可用 `STARAGENT_METRICS_INTERVAL` 等调整行为(以 Agent 自身的优先级为准)。
### 配置文件注入
- `--config <template>` 指定模板;不指定时 runner 生成默认 `staragent.json`。
- JSON 模板中可使用字符串占位符:`{{SERVER}}`、`{{SERVER_URL}}`、`{{WS_URL}}`、`{{DATA_DIR}}`、`{{CONFIG_DIR}}`、`{{WORK_DIR}}`。
- runner 会强制把 `Server` 改为假服务器地址、把 `DataDir` 改到场景目录;其他字段原样保留。
- 非 JSON 模板会做纯文本占位符替换;此时 `Server`/`DataDir` 必须由占位符或环境变量提供。
- **`MetricsInterval` 默认写入 5 秒**(Go Agent 的合法范围是 5~3600)。模板显式给出时以模板为准,也可用 `--metrics-interval` 调整默认值。
- 假服务器心跳响应固定 `Period=1`(秒),与 `MetricsInterval` 无关;Agent 应使用响应中的 `Period` 控制心跳周期。
- 模板中的 `Code`/`Secret` 必须为空。首次登录由假服务器动态下发,Agent 必须持久化到 `DataDir` 或配置文件;`basic` 场景会重启 Agent 验证二次登录。
### 协议行为要求
| 行为 | 场景 | 要求 |
|------|------|------|
| 首次登录 | basic | `Code`/`Secret` 为空;返回后持久化服务端下发的值 |
| 二次登录 | basic | 携带已持久化的 `Code`/`Secret`;登录成功 |
| 心跳 | basic / reconnect | `POST /node/ping` 携带 Bearer Token;`PingInfo.Time` 为 Unix 毫秒 |
| WebSocket | basic / dedup / expired / reconnect | `WS /node/notify` 携带 Bearer Token;收到文本 `Ping` 回 `Pong` |
| 命令执行 | basic | `node/synctime` 回复 `Status=2` 且 `Data` 非空 |
| 去重 | dedup | 相同 `Id` 只回复一次 |
| 过期 | expired | `Expire` 早于当前时间时回复 `Status=3` |
| 断线重连 | reconnect | 服务端强制断开后重新连接 WS 并恢复心跳/命令 |
| HTTP 降级 | http-fallback | WS 不可用时,执行心跳响应 `Commands` 中的命令 |
| 事件补报 | events | 上报失败后重试;**至少产生一个事件**(如 `Startup`) |
`events` 场景依赖 Agent 在启动后上报事件(mock Agent 上报 `Startup`)。若被测 Agent 没有启动事件,请先补充事件上报或为该场景提供可触发的事件。
## 场景
| 名称 | 验证内容 |
|------|----------|
| `basic` | 动态注册、Token、心跳、WS 连接、应用层 Ping/Pong、WS 命令与回复、崩溃重启后复用 `Code`/`Secret` |
| `http-fallback` | 禁用 WS 后,命令经 `PingResponse.Commands` 下发并回复 |
| `dedup` | 同一命令 `Id` 投递两次,只执行/回复一次(1.5 秒观察窗口) |
| `expired` | `Expire` 已过期的命令回复 `Status=3` |
| `reconnect` | `/control/kick` 断开 WS 后 Agent 自动重连,连接数与心跳恢复,命令仍可送达 |
| `events` | 前两次 `/node/postevents` 返回 500,Agent 退避重试直至成功 |
`--scenario all` 运行全部;也支持逗号分隔,如 `--scenario basic,events`。`--list` 可列出场景。
## CLI
```text
conformance [flags]
--agent <path> 被测 Agent 可执行文件
--agent-args <args> 自定义启动参数(默认 "run --config <file>")
--config <path> 配置模板(可选)
--mock-agent 使用内置 mock Agent 代替外部进程
--scenario <name> 场景名、逗号列表或 all(默认 all)
--timeout <duration> 总超时(默认 5m)
--scenario-timeout <d> 单场景超时(默认 90s)
--metrics-interval <n> 生成配置中的 MetricsInterval(默认 5,模板值优先)
--workdir <path> 产物根目录(默认系统临时目录)
--keep 成功后也保留场景目录
--verbose 实时输出 Agent 日志
--list 列出场景并退出
```
退出码:`0` 全部通过;`1` 有场景失败;`2` 参数/环境错误。
失败时会输出失败原因、服务器状态摘要、最近请求、Agent 日志尾部与产物目录。失败场景的目录(含 `agent.log`、`staragent.json`、`data/`)默认保留以便排查;成功场景默认清理。
## 控制接口(仅测试)
假服务器额外暴露 `/control/*`,供 runner 驱动场景,也可用于自定义调试:
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/control/state` | 完整状态快照(登录、心跳、回复、事件、WS、请求记录等) |
| POST | `/control/command?via=ws\|http\|auto` | 入队命令;`ws` 立即下发,`http` 放入下一次心跳响应 |
| POST | `/control/ping` | 设置 `Period`/`NewServer`/`SyncTime`/`RotateToken`,并可附加 HTTP 命令 |
| POST | `/control/kick` | 强制关闭所有 WS 连接 |
| POST | `/control/events-fail` | 让接下来 N 次事件上报失败:`{"Count":2,"Status":500}` |
| POST | `/control/ws` | 禁用/启用 WS:`{"Disabled":true}` |
| POST | `/control/upgrade` | 设置 `/node/upgrade` 返回值;`{"Clear":true}` 清空 |
## 构建与测试
```powershell
go test ./...
go build ./cmd/conformance
go vet ./...
```
- 仅依赖标准库与 `github.com/coder/websocket`(服务端 `Accept` 与客户端 `Dial`)。
- 若 `proxy.golang.org` 超时,可改用:
```powershell
$env:GOPROXY = 'https://goproxy.cn,direct'
go mod download
```
- 测试包含 `internal/scenario` 的全场景集成测试:启动假服务器 + 内置 mock Agent,覆盖全部 6 个场景。
## 接入新语言(最短路径)
1. 按 [`../spec/protocol.md`](/NewLife/Stardust/Blob/master/StarAgent.Native/conformance/../spec/protocol.md) 实现 Agent,入口约定为 `<agent> run --config <file>`;至少读取 `Server`/`DataDir`,并建议支持 `STARAGENT_*` 环境变量兜底。
2. 直接复用本目录模板:`--config templates/agent.json`。runner 会把 `Server`、`DataDir` 重写到本次场景,`Code`/`Secret` 保持为空,`MetricsInterval` 默认 5。
3. 先跑 `basic` 建立最小闭环,再跑 `all`:
```powershell
.\conformance.exe --agent .\staragent --config .\templates\agent.json --scenario basic
.\conformance.exe --agent .\staragent --config .\templates\agent.json --scenario all
```
4. 失败时按输出定位:`reason` → `state` → `requests` → `replies`/`event attempts` → `<workdir>\<scenario>\agent.log`。修完重跑,目标输出 `6 passed, 0 failed`。
5. 新语言不需要链接任何库,只要可执行文件能启动并遵守协议;CI 中可直接 `go test ./...` + `go run ./cmd/conformance --agent <binary> --scenario all`。
6. 需要新增断言时,在 `internal/scenario/scenarios.go` 添加一个 `Definition` 即可复用假服务器、配置注入和进程管理。
## 新增语言实现检查清单
1. 实现 `run --config <file>`(或等价的环境变量入口),读取 `Server`/`DataDir`。
2. 首次登录发送空 `Code`/`Secret`,持久化响应值;重启后携带。
3. 用登录返回的 `Token` 作为后续 HTTP/WS 的 Bearer Token。
4. 按心跳响应 `Period` 发送 `POST /node/ping`,并处理 `Commands` 降级命令。
5. 连接 `WS /node/notify`,回复应用层 `Ping`,执行 JSON 命令并通过 HTTP 回复。
6. 实现 `node/synctime`、命令去重、过期判断。
7. 启动后上报至少一个事件,并实现失败重试。
8. 运行 `conformance --agent <binary> --config templates/agent.json --scenario all`。
## 当前限制
- 假服务器只监听 `127.0.0.1` 明文 HTTP,不测试 TLS;生产 TLS 行为不在本夹具范围。
- 已提供 `/node/upgrade`、`/node/logout`、`Token` 轮换、`NewServer` 的控制能力,但默认场景未断言这些行为。
- 不使用真实 StarServer/数据库;鉴权、命令归属等仅做最小校验。
|