Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 28 additions & 17 deletions cmd/release-readiness/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func main() {
flag.StringVar(&opts.webuiBuildResult, "webui-build-result", "unknown", "WebUI build gate result: pass, fail, unknown")
flag.StringVar(&opts.liveResult, "live-result", "skip", "live gate result: pass, fail, skip, unknown")
flag.StringVar(&opts.liveSkipReason, "live-skip-reason", "not required unless high-risk live path changes", "Reason when live gate is skipped")
flag.StringVar(&opts.offlineCurrentInputSmokeResult, "offline-current-input-smoke-result", "skip", "offline current-input smoke result: pass, fail, skip, unknown")
flag.StringVar(&opts.historyAnalyzerJSON, "history-analyzer-json", "", "Future input: History Analyzer JSON report")
flag.StringVar(&opts.parserShadowJSON, "parser-shadow-json", "", "Future input: parser shadow JSON report")
flag.StringVar(&opts.contextShadowJSON, "context-shadow-json", "", "Future input: context shadow JSON report")
Expand All @@ -42,23 +43,24 @@ func main() {
}

type cliOptions struct {
version string
branch string
scope string
owner string
outMarkdown string
outJSON string
lintResult string
refactorResult string
unitResult string
webuiBuildResult string
liveResult string
liveSkipReason string
historyAnalyzerJSON string
parserShadowJSON string
contextShadowJSON string
autoContinueJSON string
capabilityRouterJSON string
version string
branch string
scope string
owner string
outMarkdown string
outJSON string
lintResult string
refactorResult string
unitResult string
webuiBuildResult string
liveResult string
liveSkipReason string
offlineCurrentInputSmokeResult string
historyAnalyzerJSON string
parserShadowJSON string
contextShadowJSON string
autoContinueJSON string
capabilityRouterJSON string
}

func run(opts cliOptions) error {
Expand Down Expand Up @@ -118,13 +120,18 @@ func buildGates(opts cliOptions) ([]readiness.GateResult, error) {
if err != nil {
return nil, fmt.Errorf("live-result: %w", err)
}
offlineCurrentInputSmoke, err := parseOptionalGateResult(opts.offlineCurrentInputSmokeResult)
if err != nil {
return nil, fmt.Errorf("offline-current-input-smoke-result: %w", err)
}

return []readiness.GateResult{
{Name: "lint", Result: lint, Evidence: "./scripts/lint.sh"},
{Name: "refactor line gate", Result: refactor, Evidence: "./tests/scripts/check-refactor-line-gate.sh"},
{Name: "unit all", Result: unit, Evidence: "./tests/scripts/run-unit-all.sh"},
{Name: "webui build", Result: webuiBuild, Evidence: "npm run build --prefix webui"},
{Name: "live", Result: live, Evidence: liveEvidence(live, opts.liveSkipReason)},
{Name: "offline current-input smoke", Result: offlineCurrentInputSmoke, Evidence: "./tests/scripts/run-offline-current-input-smoke.sh"},
}, nil
}

Expand Down Expand Up @@ -164,6 +171,10 @@ func parseRequiredGateResult(value string) (readiness.GateResultValue, error) {
}

func parseLiveGateResult(value string) (readiness.GateResultValue, error) {
return parseOptionalGateResult(value)
}

func parseOptionalGateResult(value string) (readiness.GateResultValue, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "skip":
return readiness.GateSkip, nil
Expand Down
27 changes: 27 additions & 0 deletions cmd/release-readiness/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,33 @@ func TestParseGateResultAllowsLiveSkip(t *testing.T) {
}
}

func TestBuildGatesIncludesOfflineCurrentInputSmoke(t *testing.T) {
gates, err := buildGates(cliOptions{
lintResult: "pass",
refactorResult: "pass",
unitResult: "pass",
webuiBuildResult: "pass",
liveResult: "skip",
liveSkipReason: "no credentials",
offlineCurrentInputSmokeResult: "pass",
})
if err != nil {
t.Fatal(err)
}
for _, gate := range gates {
if gate.Name == "offline current-input smoke" {
if gate.Result != readiness.GatePass {
t.Fatalf("offline current-input smoke result = %q, want %q", gate.Result, readiness.GatePass)
}
if gate.Evidence != "./tests/scripts/run-offline-current-input-smoke.sh" {
t.Fatalf("offline current-input smoke evidence = %q", gate.Evidence)
}
return
}
}
t.Fatalf("missing offline current-input smoke gate: %#v", gates)
}

func TestResolveBranchUsesExplicitValue(t *testing.T) {
got, err := resolveBranch("release/test")
if err != nil {
Expand Down
31 changes: 31 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ DS2API 提供两个层级的测试:
| 单元测试(Go) | `./tests/scripts/run-unit-go.sh` | 不需要真实账号 |
| 单元测试(Node) | `./tests/scripts/run-unit-node.sh` | 不需要真实账号 |
| 单元测试(全部) | `./tests/scripts/run-unit-all.sh` | 不需要真实账号 |
| 离线 current-input smoke | `./tests/scripts/run-offline-current-input-smoke.sh` | 不需要真实账号;覆盖协议入口、上下文文件化、工具引用和 completion runtime 的关键离线路径 |
| Release 目标交叉编译 | `./tests/scripts/check-cross-build.sh` | 覆盖发布包支持的 GOOS/GOARCH |
| 端到端测试 | `./tests/scripts/run-live.sh` | 使用真实账号执行全链路测试 |

Expand All @@ -37,6 +38,7 @@ npm run build --prefix webui
- `./scripts/lint.sh` 会运行 Go 格式化检查和 `golangci-lint`;修改 Go 文件后仍建议先执行 `gofmt -w <files>`。
- `run-unit-all.sh` 串行调用 Go 与 Node 单元测试入口。
- CI 还会额外在 macOS/Windows 跑 Go 单测,并执行 release 目标交叉编译检查。
- `run-offline-current-input-smoke.sh` 是无账号替代验证层,只验证本地协议归一、context rendering、filename policy、tool reference 和 runtime 装配,不证明 DeepSeek 账号、PoW、真实上游 completion 或网络路径可用。
- `run-live.sh` 是真实账号端到端测试,适合作为发布或高风险改动后的补充验证,不属于每次 PR 的固定本地门禁。

---
Expand All @@ -62,6 +64,35 @@ npm run build --prefix webui
./tests/scripts/check-cross-build.sh
```

### 无账号替代验证 | Offline Stand-in Smoke

当没有可用 DeepSeek 账号或真实上游环境时,不能把 live gate 记为通过。此时建议先执行:

```bash
./tests/scripts/run-offline-current-input-smoke.sh
```

该脚本用于快速覆盖最近高风险的纯本地路径:

- OpenAI Chat / Responses 的 current input file 应用。
- Claude / Gemini 入口归一化后的 current input file 应用。
- `hybrid_recent`、`neutral_random`、tool reference 分离和 prompt-visible 术语约束。
- completion runtime 在非流式和流式重试路径中的 current input 文件重传。

它不能覆盖:

- 真实 DeepSeek 账号登录、token 刷新和封禁状态。
- 真实 PoW 请求和上游 completion 语义。
- 真实网络超时、429、账号切换和文件上传服务状态。

Release Readiness 中应把 live gate 标记为 `SKIP`,并写明原因,例如:

```bash
go run ./cmd/release-readiness \
--live-result skip \
--live-skip-reason "no credentials; offline current-input smoke passed"
```
Comment on lines +91 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add offline smoke result flag in readiness example

This command example states that offline current-input smoke has passed, but it never sets --offline-current-input-smoke-result pass, so cmd/release-readiness keeps that gate at its default skip. Anyone copying this snippet will generate a report that omits the very evidence this section is trying to record, which undermines release-readiness traceability for no-credential runs.

Useful? React with 👍 / 👎.


说明:`plans/stage6-manual-smoke.md` 是阶段 6 手工烟测记录;`./tests/scripts/check-stage6-manual-smoke.sh` 只应在完成 live smoke 后通过,当前不属于常规 CI 单元门禁。

### 端到端测试 | End-to-End Tests
Expand Down
24 changes: 23 additions & 1 deletion docs/release-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Release Readiness 不是替代 PR Gate,而是回答:
|---|---|
| PR Gate | `lint.sh`、`check-refactor-line-gate.sh`、`run-unit-all.sh`、WebUI build |
| Live Gate | 高风险改动运行 `run-live.sh`,产物脱敏 |
| Offline Stand-in Smoke | 无真实账号时运行 `run-offline-current-input-smoke.sh`,作为本地路径补充证据 |
| History Analyzer | 输出异常统计和高风险样本 |
| Parser Shadow Report | diff 率、confidence 分布、marker leak |
| Context Shadow Report | warnings、trimmed、tool pair、budget |
Expand All @@ -45,6 +46,7 @@ M4.0 只建立 release readiness baseline,不改变任何主请求链路行为
- 不把 parser、context、auto continue 或 capability router 推到 `enforce`。
- 不读取或输出未脱敏 prompt、token、账号凭证和完整真实请求。
- 不把 live test 失败或缺凭证隐藏成通过结果。
- 无账号时可以记录 offline stand-in smoke 结果,但不能把它等同于 live gate 通过。

## 4. 报告模板

Expand All @@ -65,6 +67,7 @@ Decision owner:
| unit all | PASS/FAIL/UNKNOWN | link or local log | |
| webui build | PASS/FAIL/UNKNOWN | link or local log | |
| live | PASS/FAIL/SKIP/UNKNOWN | link or reason | |
| offline current-input smoke | PASS/FAIL/SKIP/UNKNOWN | link or local log | |

## Feature Flag Readiness

Expand Down Expand Up @@ -120,6 +123,7 @@ Required follow-ups:
- 有 critical analyzer finding,不允许 release,除非明确不影响本次变更范围。
- Auto Continue 没有 live smoke,不允许 stream enforce。
- Parser / Context diff 无人工审阅,不允许默认开启。
- live gate 因无账号 `SKIP` 时,必须写明是否执行了 offline stand-in smoke;该证据只能说明本地协议和上下文路径未回归。

### 6.1 Feature Flag 晋级矩阵

Expand All @@ -140,6 +144,23 @@ Required follow-ups:

`PENDING` 和 `UNKNOWN` 不能支持 feature flag 晋级到 `enforce`。

### 6.3 无账号验证口径

当没有真实账号或上游环境时:

- `live` gate 使用 `SKIP`,evidence 写明 `no credentials`,不要写成 `PASS`。
- 运行 `./tests/scripts/run-offline-current-input-smoke.sh`,把结果作为 `offline current-input smoke` 证据。
- 报告结论优先使用 `GO-WITH-FLAGS-OFF`,除非本次变更完全不影响请求链路。
- 后续拿到账号后补跑 `./tests/scripts/run-live.sh`,并更新 release readiness 或追加 live smoke 记录。

无账号替代验证重点覆盖:

- current input inline-first 和上传触发边界。
- `hybrid_recent` 渲染、`neutral_random` 文件名策略和 prompt-visible 术语约束。
- tool reference 与 conversation context 分离。
- OpenAI / Claude / Gemini 协议入口归一后的一致性。
- completion runtime 的账号切换重传和 token accounting 本地逻辑。

## 7. Phase Closure Review

每个 M4.0 Phase 完成后,必须对照以下问题做偏差检查:
Expand Down Expand Up @@ -176,7 +197,8 @@ go run ./cmd/release-readiness \
--unit-result pass \
--webui-build-result pass \
--live-result skip \
--live-skip-reason "no credentials"
--live-skip-reason "no credentials" \
--offline-current-input-smoke-result pass
```

也可以使用脚本封装:
Expand Down
1 change: 1 addition & 0 deletions internal/readiness/baseline.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func DefaultGateResults() []GateResult {
{Name: "unit all", Result: GateUnknown, Evidence: "./tests/scripts/run-unit-all.sh"},
{Name: "webui build", Result: GateUnknown, Evidence: "npm run build --prefix webui"},
{Name: "live", Result: GateSkip, Evidence: "not required unless high-risk live path changes"},
{Name: "offline current-input smoke", Result: GateSkip, Evidence: "./tests/scripts/run-offline-current-input-smoke.sh"},
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/readiness/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func TestRenderMarkdown(t *testing.T) {
Gates: []GateResult{
{Name: "lint", Result: GatePass, Evidence: "./scripts/lint.sh"},
{Name: "live", Result: GateSkip, Evidence: "no credentials"},
{Name: "offline current-input smoke", Result: GatePass, Evidence: "./tests/scripts/run-offline-current-input-smoke.sh"},
},
Features: []FeatureReadiness{
{
Expand Down Expand Up @@ -76,6 +77,7 @@ func TestRenderMarkdown(t *testing.T) {
"# Release Readiness Report",
"Generated at: 2026-05-12T00:00:00Z",
"| lint | pass | ./scripts/lint.sh |",
"| offline current-input smoke | pass | ./tests/scripts/run-offline-current-input-smoke.sh |",
"| parser_v2 | off | shadow | hold | waiting for shadow data | parser shadow report; manual diff review |",
"| tool | 0 | 1 | 2 | HA_TOOL_MARKER_LEAK |",
"| history analyzer | pending | 0 | M4.1 pending |",
Expand Down
15 changes: 15 additions & 0 deletions tests/scripts/run-offline-current-input-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT_DIR"

go test \
./internal/promptcompat \
./internal/httpapi/openai \
./internal/httpapi/openai/chat \
./internal/httpapi/openai/history \
./internal/httpapi/openai/responses \
./internal/httpapi/claude \
./internal/httpapi/gemini \
./internal/completionruntime
Loading