P1-a sortTime 泄露进输出契约 —— 采集端无条件写内部排序字段 sortTime,而 emit 仅在单层(reqDepth==1)经 stripDriveDepthDecorations 整体剥离。depth>1 的所有路径都把 sortTime 漏进 stdout;#971 引入的 --type/时间区间过滤同样读该字段,泄露面随之扩大。修法:在 emitDriveDepthResult 尾部无条件 delete,一处覆盖正常 emit / SIGINT 取消 / unrecoverable partial 三条路径。采集端保持不动(内部字段,排序与筛选时才读)。 P1-b 不完整扫描仍以退出码 0 产出「Top-N」 —— 尾部拒绝 guard 只拦全局截断,不拦递归途中目录读取失败;后者把可恢复失败记进 errs[] 后照常 emit,Top-N 落在漏扫子树的不完整集合上却冒充全局最新。修法:guard 扩为 latest>0 && (truncated || len(errs)>0),走新增 driveLatestIncompleteError(LATEST_SCAN_TRUNCATED / LATEST_SCAN_INCOMPLETE 双 token,二者同真时都带,目录失败详情排在截断之前);unrecoverable 分支在 latest>0 时不吐 partial,直接回根因错误。 恢复命令必须能原样复现原候选集:driveLatestScope 快照查询域(--workspace / --space-id)、扫描根(--folder)与全部过滤条件(--pattern / --type / --start / --end),缺任一项,用户照抄后就在另一个集合上取 Top-N,看起来成功却答非所问。扫描根取 runDriveListDepth 实际使用的 rootFolderID 而非重读 flag:用户可能传 URL,解析后的 ID 才是真正被扫的目标。「按原范围重跑」原样带回原 --folder,原调用在空间根时不带。 拒绝产出后 errors[] 不再进 stdout,目录名与服务端错误文本从 JSON(编码会转义)挪进纯文本 stderr —— 原样透传会让 ANSI/OSC 序列被终端执行,可清屏、伪造彩色成功、隐藏后续输出、改窗口标题,Agent 场景还会污染上下文。改为复用仓库既有的 output.SanitizeForTerminal(canonical 实现在 pkg/validate),再把它按设计保留的换行与制表符折成空格。Reason 无需处理:它是 classifyDriveDepthReason 的固定三值映射。latest=0 的既有路径仍把原值放进 errors[] JSON,不受影响。 Windows 下恢复命令的注入面:POSIX 单引号在 cmd.exe 里不是引用,--space-id 传入 sp-7 加 & 加 whoami 时,单引号包裹后的片段粘贴进 cmd 仍会执行 whoami;而唯一做真 shell 往返验证的测试被 build tag 排除在 Windows 之外。不采用「按目标 shell 生成引用」的路线:cmd.exe 的双引号挡不住 %VAR% 展开,PowerShell 的内嵌单引号写法又与 POSIX 不同,且生成命令时无法知道用户会粘贴进哪个 shell。改为平台分流 —— POSIX 构建继续单引号内联;Windows 构建只内联全部由白名单字符组成的值,含元字符的值不进命令,降级为占位符加 strconv.Quote 展示行并标注非可执行(与 internal/auth 展示 profile 标识的既有做法同一思路)。安全性由此不再依赖引用是否正确,而依赖「不受信任的值不进入可执行命令」这个更强的不变量。 顺带修掉白名单里的一个漏洞:% 原本免引用(当初为 URL 的 %20),但 cmd.exe 会无条件展开 %VAR%,于是 %PATH% 这类值会被判为安全并原样内联。% 已移除,POSIX 侧只是多一对无害引号;并新增逐字符断言,锁定白名单不含 POSIX sh / PowerShell / cmd.exe 三套元字符,同时作为该缺陷的回归锁。 两条平台策略写成与构建平台无关的纯函数,平台文件只做一行编译期绑定,因此 Windows 形态能在 POSIX 机器上端到端验证 —— 否则该分支在 POSIX 上永不可达,平台覆盖率门禁会直接报未覆盖(第一版实测 97.3451%)。另做了一次本地全量模拟:临时把 POSIX 绑定切到 Windows 策略后跑全部测试,唯一失败的是专门断言绑定的那条,据此确认没有断言会在 Windows runner 误报,并借此修掉两条原本只在 POSIX 下成立的断言。 SIGINT 取消路径刻意不套用该防线:取消由用户主动发起、退出码 130 已明确告知结果不完整,partial 是用户的预期产物。已加注释说明并补测试锁定该契约。 skill 文档(mono/multi 两份 drive.md)原在过滤章节声明「触顶截断 truncated=true、退出码 0」,同章节又说明可与 --latest 组合 —— 组合后该描述不再成立,故补一条拒绝产出的说明,并注明 Windows 下的占位符形态,避免 agent 按旧契约预期退出码或误解析。 测试命名统一 TestCrossPlatformCoverage 前缀:平台覆盖率门禁 run-platform-coverage-gate.sh 只跑匹配 ^(TestAllShortcuts|TestCrossPlatformCoverage) 的测试。本 PR 因新增带平台名的 go:build 文件被判定 platform_sensitive,Coverage (macOS) / (Windows) 由 SKIPPED 转为实跑;不带该前缀时新增语句在平台 profile 里是零覆盖,实测 69.0476%,改名后 100.0000%(当前 114 条语句仍为 100%)。已在测试文件头写明该前缀是门禁约定而非命名风格。 发布说明按 .changes fragment 机制落在 .changes/899-drive-latest-incomplete-scan.md,不改 CHANGELOG.md。
732 lines
35 KiB
Go
732 lines
35 KiB
Go
package helpers
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
// 本文件锁定 drive list --latest 的两个 P1 行为,独立于 pr868_*_test.go / drive_depth_test.go:
|
||
//
|
||
// P1-a:sortTime 是内部排序字段,任何输出路径都不得泄露进契约;
|
||
// P1-b:递归途中目录读取失败时,Top-N 建立在不完整集合上,必须拒绝产出而非吐 partial。
|
||
//
|
||
// 改代码前这些断言对 origin/main 应为红:main 采集端无条件写 sortTime、emit 仅在单层
|
||
// latest/filter 才剥;main 尾部拒绝 guard 只拦截断、不拦目录失败。
|
||
//
|
||
// 另锁定评审反馈的三个边界:
|
||
//
|
||
// 截断与目录失败同真时,失败详情(permission_denied 等)不得被截断提示吞掉;
|
||
// 拒绝产出时给的恢复命令必须保留原查询域(--workspace / --space-id),照抄不会切换查询域;
|
||
// 恢复命令里的用户值必须过 argv 引用,URL 查询串与 shell 元字符都不能改变命令解析。
|
||
//
|
||
// 关于 TestCrossPlatformCoverage 前缀:这是门禁约定,不是命名风格。平台覆盖率门禁
|
||
// scripts/policy/run-platform-coverage-gate.sh 只跑
|
||
// -run '^(TestAllShortcuts|TestCrossPlatformCoverage)',本包新增的生产代码若没有带该前缀的
|
||
// 测试覆盖,Coverage (macOS) / (Windows) 会以「changed code coverage 低于 100%」失败
|
||
// (本 PR 改名前实测 69.0476%,84 条可执行语句)。改名务必保留前缀。
|
||
|
||
// assertNoSortTime 断言 stdout 每个 item 都不含内部排序字段 sortTime。
|
||
func assertNoSortTime(t *testing.T, result map[string]any) {
|
||
t.Helper()
|
||
items, ok := result["items"].([]any)
|
||
if !ok {
|
||
t.Fatalf("items missing or wrong type: %#v", result["items"])
|
||
}
|
||
for i, raw := range items {
|
||
item, ok := raw.(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("item[%d] not an object: %#v", i, raw)
|
||
}
|
||
if _, leaked := item["sortTime"]; leaked {
|
||
t.Fatalf("item[%d] leaked internal sortTime into output contract: %#v", i, item)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestNoSortTimeLeak 覆盖 main 的覆盖漏洞:main 的
|
||
// TestCrossPlatformCoverageDriveDepthLatestTruncatedAndSortTime 名字带 SortTime,
|
||
// 却只断言 TRUNCATED、从不检查输出无 sortTime。这里把三条泄露路径都钉住。
|
||
func TestCrossPlatformCoverageDriveLatestNoSortTimeLeak(t *testing.T) {
|
||
twoFiles := `{"items":[{"fileId":"f1","name":"a.txt","type":"FILE","modifiedTime":1000},{"fileId":"f2","name":"b.txt","type":"FILE","modifiedTime":2000}]}`
|
||
|
||
// 场景 A:depth>1 --latest —— 走 applyDriveListLatest(只读 sortTime、不剥),reqDepth>1 不触发 strip。
|
||
t.Run("depth_latest", func(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: twoFiles}}}
|
||
out := installDepthCaller(t, caller)
|
||
cmd := &cobra.Command{Use: "list"}
|
||
if err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{}, "", 3, "", true, 5, driveListFilter{}); err != nil {
|
||
t.Fatalf("runDriveListDepth: %v", err)
|
||
}
|
||
assertNoSortTime(t, decodeDepthResult(t, out))
|
||
})
|
||
|
||
// 场景 B:--depth 2 无 latest 无 filter —— 走 else 分支树序排序,同样不触发 strip。
|
||
t.Run("depth_no_latest", func(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: twoFiles}}}
|
||
out := installDepthCaller(t, caller)
|
||
cmd := &cobra.Command{Use: "list"}
|
||
if err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{}, "", 2, "", true, 0, driveListFilter{}); err != nil {
|
||
t.Fatalf("runDriveListDepth: %v", err)
|
||
}
|
||
assertNoSortTime(t, decodeDepthResult(t, out))
|
||
})
|
||
|
||
// 场景 C:--depth 2 --type file —— #971 引入的 filter 也读 sortTime(applyDriveListFilter),
|
||
// strip 条件 (latest>0||filter.active()) && reqDepth==1 在多层下依旧不成立,泄露面因此变大。
|
||
t.Run("depth_filter_no_latest", func(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: twoFiles}}}
|
||
out := installDepthCaller(t, caller)
|
||
cmd := &cobra.Command{Use: "list"}
|
||
if err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{}, "", 2, "", true, 0, driveListFilter{nodeType: "file"}); err != nil {
|
||
t.Fatalf("runDriveListDepth: %v", err)
|
||
}
|
||
result := decodeDepthResult(t, out)
|
||
if len(result["items"].([]any)) != 2 {
|
||
t.Fatalf("filter 应保留两个 FILE: %#v", result["items"])
|
||
}
|
||
assertNoSortTime(t, result)
|
||
})
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestSigintStillEmitsPartialWithoutSortTime 同时承担两件事:
|
||
// 1. P1-a 的第四条路径 —— SIGINT 取消也走 emitDriveDepthResult,同样不得泄露 sortTime;
|
||
// 2. SIGINT 契约锁 —— 本次 P1-b 只在 BFS 尾部 guard 与 unrecoverable 分支生效,**不改**
|
||
// 取消路径:SIGINT 是用户主动中断、退出码 130 已明确告知不完整,partial 是明确预期。
|
||
// 这条测试防止后续误把 fail-closed 扩到取消路径,也为外部评测用例
|
||
// test_sigint_exits_130_with_partial 提供本地对照。
|
||
func TestCrossPlatformCoverageDriveLatestSigintStillEmitsPartialWithoutSortTime(t *testing.T) {
|
||
caller := &scriptedToolCaller{}
|
||
out := installDepthCaller(t, caller)
|
||
items := []map[string]any{
|
||
{"fileId": "f1", "name": "a.txt", "type": "FILE", "sortTime": int64(2000), "rel_path": "a.txt", "depth": 2},
|
||
}
|
||
errs := []driveDepthError{
|
||
{Depth: 1, FolderID: "fid-1", FolderName: "半路目录", Reason: "permission_denied", Message: "denied"},
|
||
}
|
||
// latest>0 且 reqDepth>1:main 在此不 strip,sortTime 泄露。
|
||
err := emitDriveDepthCancelled(items, errs, "", 5, 3, newDrivePanDepthRoute(), driveListFilter{})
|
||
var cancelErr *driveDepthCancelledError
|
||
if !errors.As(err, &cancelErr) {
|
||
t.Fatalf("err = %T %v, want driveDepthCancelledError", err, err)
|
||
}
|
||
if cancelErr.ExitCode() != 130 {
|
||
t.Fatalf("exit code = %d, want 130", cancelErr.ExitCode())
|
||
}
|
||
result := decodeDepthResult(t, out)
|
||
// partial 契约不变:items 与 errors 都照吐,truncated 标记为真。
|
||
if len(result["items"].([]any)) != 1 {
|
||
t.Fatalf("取消路径应保留 partial items: %#v", result["items"])
|
||
}
|
||
if len(result["errors"].([]any)) != 1 {
|
||
t.Fatalf("取消路径应保留 errors[]: %#v", result["errors"])
|
||
}
|
||
if result["truncated"] != true {
|
||
t.Fatalf("取消结果 truncated = %#v, want true", result["truncated"])
|
||
}
|
||
assertNoSortTime(t, result)
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestRefusesOnFolderFailure 覆盖 P1-b:递归途中一个可恢复目录失败(403/business,
|
||
// 非 auth 非限流 → 记 errs[] 跳过),Top-N 落在不完整集合上,必须拒绝产出。
|
||
// 构造:根目录成功产出 FOLDER+FILE(collected>0 且 dirA 入队),子目录返回 forbidden.* →
|
||
// recoverable → errs=[1]。旧代码尾部 `if truncated && latest>0` 不触发 → emit 吐 partial(err=nil);
|
||
// 新代码 `len(errs)>0` → LATEST_SCAN_INCOMPLETE 且 stdout 无 items。
|
||
func TestCrossPlatformCoverageDriveLatestRefusesOnFolderFailure(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: `{"items":[{"fileId":"dirA","name":"dirA","type":"FOLDER"},{"fileId":"fX","name":"x.txt","type":"FILE","modifiedTime":1000}]}`},
|
||
{text: `{"errorCode":"forbidden.noPermission","errorMsg":"denied"}`},
|
||
}}
|
||
out := installDepthCaller(t, caller)
|
||
cmd := &cobra.Command{Use: "list"}
|
||
err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{}, "", 3, "", true, 5, driveListFilter{})
|
||
if err == nil || !strings.Contains(err.Error(), "LATEST_SCAN_INCOMPLETE") {
|
||
t.Fatalf("err = %v, want LATEST_SCAN_INCOMPLETE", err)
|
||
}
|
||
// 拒绝产出:stdout 必须没有 items(不是 partial)。旧代码此处会吐 partial,断言随之失败。
|
||
if out.Len() != 0 {
|
||
t.Fatalf("expected no stdout on refusal, got: %s", out.String())
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestRefusesOnTruncationWithFolderFailure 端到端证明「截断 + 目录失败」组合确实可达:
|
||
// 根目录出 dirA/dirB 两个子目录 → dirA 权限失败记 errs[] → dirB 返回 2000 条触发全局截断,
|
||
// 尾部 guard 拿到 truncated=true 且 len(errs)=1。旧实现在此让 truncated 短路,permission_denied
|
||
// 详情整块丢失(评审反馈的阻塞点);现在两个 token 与失败详情都必须在。
|
||
func TestCrossPlatformCoverageDriveLatestRefusesOnTruncationWithFolderFailure(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
var bulk strings.Builder
|
||
bulk.WriteString(`{"items":[`)
|
||
for i := 0; i < driveDepthMaxItems; i++ {
|
||
if i > 0 {
|
||
bulk.WriteString(",")
|
||
}
|
||
fmt.Fprintf(&bulk, `{"fileId":"f%d","name":"file-%d.txt","type":"FILE","modifiedTime":%d}`, i, i, 1000+i)
|
||
}
|
||
bulk.WriteString(`]}`)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: `{"items":[{"fileId":"dirA","name":"报表","type":"FOLDER"},{"fileId":"dirB","name":"dirB","type":"FOLDER"}]}`},
|
||
{text: `{"errorCode":"forbidden.noPermission","errorMsg":"denied"}`}, // dirA:可恢复 → 记 errs[]
|
||
{text: bulk.String()}, // dirB:撞 2000 上限 → truncated
|
||
}}
|
||
out := installDepthCaller(t, caller)
|
||
cmd := &cobra.Command{Use: "list"}
|
||
err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{}, "", 3, "", true, 5, driveListFilter{})
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) || cliErr.Code != CodeContentTruncated {
|
||
t.Fatalf("err = %T %v, want CodeContentTruncated", err, err)
|
||
}
|
||
msg := cliErr.Message
|
||
if !strings.Contains(msg, "LATEST_SCAN_TRUNCATED") {
|
||
t.Fatalf("组合场景缺 TRUNCATED token: %q", msg)
|
||
}
|
||
// 旧实现在这里丢掉整段目录失败详情。
|
||
if !strings.Contains(msg, "LATEST_SCAN_INCOMPLETE") ||
|
||
!strings.Contains(msg, "folder=报表") ||
|
||
!strings.Contains(msg, "permission_denied") {
|
||
t.Fatalf("组合场景丢失目录失败详情: %q", msg)
|
||
}
|
||
if out.Len() != 0 {
|
||
t.Fatalf("expected no stdout on refusal, got: %s", out.String())
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestScopeWiredFromCommand 端到端验证查询域从原命令一路带到恢复命令:单测构造器
|
||
// 拿不到的是 runDriveListDepth 里的接线(driveLatestScopeFromCmd(cmd, maxDepth, rootFolderID)),这里补上。
|
||
func TestCrossPlatformCoverageDriveLatestScopeWiredFromCommand(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: `{"items":[{"fileId":"dirA","name":"dirA","type":"FOLDER"},{"fileId":"fX","name":"x.txt","type":"FILE","modifiedTime":1000}]}`},
|
||
{text: `{"errorCode":"forbidden.noPermission","errorMsg":"denied"}`},
|
||
}}
|
||
installDepthCaller(t, caller)
|
||
cmd := newDriveListScopeCmd(t, map[string]string{"space-id": "sp-7"})
|
||
err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{"spaceId": "sp-7"}, "", 4, "", true, 5, driveListFilter{})
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) {
|
||
t.Fatalf("err = %T %v", err, err)
|
||
}
|
||
// 恢复命令必须带原 --space-id,且给出原 --depth 层数。
|
||
assertDriveLatestSuggestion(t, cliErr.Suggestion, "--space-id sp-7")
|
||
if !strings.Contains(cliErr.Suggestion, "--depth 4") {
|
||
t.Fatalf("恢复命令应保留原层数: %q", cliErr.Suggestion)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestRefusesOnUnrecoverableFailure 覆盖 P1-b 的另一半:递归途中遇不可恢复错误
|
||
// (auth 过期 / 网络不可达)且 latest>0 时,不吐 partial,直接回根因错误。
|
||
// 与 latest=0 的既有行为(TestCrossPlatformCoverageRunDriveListDepthUnrecoverable:partial
|
||
// + errors[] 进 stdout 后非零退出)对照——latest 下 partial 的 Top-N 会被误读为全局最新,
|
||
// 故必须拒绝产出;回根因错误而非 INCOMPLETE token,因为 auth/网络比通用截断提示更可操作。
|
||
func TestCrossPlatformCoverageDriveLatestRefusesOnUnrecoverableFailure(t *testing.T) {
|
||
useDriveDepthArgs(t)
|
||
caller := &scriptedToolCaller{steps: []scriptedToolStep{
|
||
{text: `{"items":[{"fileId":"dirA","name":"dirA","type":"FOLDER"},{"fileId":"fX","name":"x.txt","type":"FILE","modifiedTime":1000}]}`},
|
||
{text: `{"errorCode":"DWS_SERVICE_UNAUTHORIZED"}`},
|
||
}}
|
||
out := installDepthCaller(t, caller)
|
||
cmd := &cobra.Command{Use: "list"}
|
||
err := runDriveListDepth(cmd, newDrivePanDepthRoute(), map[string]any{}, "", 3, "", true, 5, driveListFilter{})
|
||
// 回根因错误:Code 仍是 auth 过期,不被包装成 LATEST_SCAN_INCOMPLETE。
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) || cliErr.Code != CodeAuthTokenExpired {
|
||
t.Fatalf("err = %T %v, want CodeAuthTokenExpired", err, err)
|
||
}
|
||
if strings.Contains(cliErr.Message, "LATEST_SCAN_INCOMPLETE") {
|
||
t.Fatalf("unrecoverable 应回根因错误而非 INCOMPLETE 包装: %q", cliErr.Message)
|
||
}
|
||
// 拒绝产出:不吐 partial(对照 latest=0 时会输出 2 条 items + 1 条 errors)。
|
||
if out.Len() != 0 {
|
||
t.Fatalf("expected no stdout on refusal, got: %s", out.String())
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestIncompleteErrorBranches 直接单测构造器的各条分支。
|
||
func TestCrossPlatformCoverageDriveLatestIncompleteErrorBranches(t *testing.T) {
|
||
// 纯截断分支:errs 为空 → 只有 TRUNCATED。
|
||
t.Run("truncated_only", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(5, true, nil, driveLatestScope{depth: 3})
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) || cliErr.Code != CodeContentTruncated {
|
||
t.Fatalf("err = %T %v, want CodeContentTruncated", err, err)
|
||
}
|
||
if !strings.Contains(cliErr.Message, "LATEST_SCAN_TRUNCATED") {
|
||
t.Fatalf("message = %q", cliErr.Message)
|
||
}
|
||
if strings.Contains(cliErr.Message, "LATEST_SCAN_INCOMPLETE") {
|
||
t.Fatalf("无目录失败时不应出现 INCOMPLETE: %q", cliErr.Message)
|
||
}
|
||
assertDriveLatestSuggestion(t, cliErr.Suggestion, "")
|
||
})
|
||
|
||
// 纯目录失败分支:未截断 → 只有 INCOMPLETE,Message 含首个失败的 folder/depth/reason。
|
||
t.Run("folder_failure_only", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(3, false, twoDriveDepthErrors(), driveLatestScope{depth: 3})
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) || cliErr.Code != CodeContentTruncated {
|
||
t.Fatalf("err = %T %v, want CodeContentTruncated", err, err)
|
||
}
|
||
msg := cliErr.Message
|
||
if !strings.Contains(msg, "LATEST_SCAN_INCOMPLETE") ||
|
||
!strings.Contains(msg, "报表") ||
|
||
!strings.Contains(msg, "depth=2") ||
|
||
!strings.Contains(msg, "permission_denied") ||
|
||
!strings.Contains(msg, "2 个目录未读全") {
|
||
t.Fatalf("message = %q", msg)
|
||
}
|
||
if strings.Contains(msg, "LATEST_SCAN_TRUNCATED") {
|
||
t.Fatalf("未截断时不应出现 TRUNCATED: %q", msg)
|
||
}
|
||
assertDriveLatestSuggestion(t, cliErr.Suggestion, "")
|
||
})
|
||
|
||
// 组合分支(评审反馈的阻塞点):截断与目录失败同真时,旧实现让 truncated 短路、
|
||
// permission_denied 这类目录失败详情整块丢失。现在两个 token 都必须在,且失败详情不得被吞。
|
||
t.Run("truncated_with_folder_failures", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(4, true, twoDriveDepthErrors(), driveLatestScope{depth: 3})
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) || cliErr.Code != CodeContentTruncated {
|
||
t.Fatalf("err = %T %v, want CodeContentTruncated", err, err)
|
||
}
|
||
msg := cliErr.Message
|
||
// 两个成因都要可被消费方 token 匹配到。
|
||
if !strings.Contains(msg, "LATEST_SCAN_INCOMPLETE") || !strings.Contains(msg, "LATEST_SCAN_TRUNCATED") {
|
||
t.Fatalf("组合场景需同时带两个 token: %q", msg)
|
||
}
|
||
// 目录失败详情必须完整保留——这是用户唯一能动手修的线索。
|
||
if !strings.Contains(msg, "报表") ||
|
||
!strings.Contains(msg, "depth=2") ||
|
||
!strings.Contains(msg, "permission_denied") ||
|
||
!strings.Contains(msg, "2 个目录未读全") {
|
||
t.Fatalf("组合场景丢失目录失败详情: %q", msg)
|
||
}
|
||
if !strings.Contains(msg, "拒绝输出不完整的 Top-4") {
|
||
t.Fatalf("message 缺少拒绝产出结论: %q", msg)
|
||
}
|
||
assertDriveLatestSuggestion(t, cliErr.Suggestion, "")
|
||
// 组合场景的指引要同时覆盖两条补救:换可读目录 + 降层数。
|
||
if !strings.Contains(cliErr.Suggestion, "确认目录权限") || !strings.Contains(cliErr.Suggestion, "--depth") {
|
||
t.Fatalf("组合场景 suggestion 需同时给出权限与降层数补救: %q", cliErr.Suggestion)
|
||
}
|
||
})
|
||
|
||
// folderName 为空回落 folderID;folderID 也空回落 <root>。
|
||
t.Run("folder_fallback", func(t *testing.T) {
|
||
byID := driveLatestIncompleteError(1, false, []driveDepthError{{FolderID: "fid-x"}}, driveLatestScope{depth: 2})
|
||
if !strings.Contains(byID.Error(), "folder=fid-x") {
|
||
t.Fatalf("fallback to folderID: %v", byID)
|
||
}
|
||
byRoot := driveLatestIncompleteError(1, false, []driveDepthError{{}}, driveLatestScope{depth: 2})
|
||
if !strings.Contains(byRoot.Error(), "folder=<root>") {
|
||
t.Fatalf("fallback to <root>: %v", byRoot)
|
||
}
|
||
})
|
||
}
|
||
|
||
// twoDriveDepthErrors 是两条目录失败样本,首条用于断言「首个失败」详情。
|
||
func twoDriveDepthErrors() []driveDepthError {
|
||
return []driveDepthError{
|
||
{Depth: 2, FolderID: "fid-9", FolderName: "报表", Reason: "permission_denied", Message: "denied"},
|
||
{Depth: 1, FolderID: "fid-3", FolderName: "归档", Reason: "api_error", Message: "boom"},
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestScopePreservedInSuggestion 覆盖评审反馈的第二个阻塞点:恢复命令此前固定
|
||
// 生成 `dws drive list --folder ...`,把原调用的 --workspace / --space-id 丢掉。用户照抄后
|
||
// 会从知识库切到普通钉盘(或从指定钉盘空间切到「我的文件」),在另一个查询域里拿到一份
|
||
// 「看起来对」的 Top-N —— 比直接报错更难发现。
|
||
func TestCrossPlatformCoverageDriveLatestScopePreservedInSuggestion(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
flags map[string]string
|
||
want string
|
||
}{
|
||
// 知识库路由:--workspace 决定路由,丢了就切到普通钉盘。
|
||
{name: "workspace", flags: map[string]string{"workspace": "ws-1"}, want: "--workspace ws-1"},
|
||
// 别名路径:--workspace-id 与 --workspace 同源(flagOrFallback)。
|
||
{name: "workspace_id_alias", flags: map[string]string{"workspace-id": "ws-alias"}, want: "--workspace ws-alias"},
|
||
// 钉盘路由:--space-id 丢了就退回「我的文件」。
|
||
{name: "space_id", flags: map[string]string{"space-id": "sp-7"}, want: "--space-id sp-7"},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
scope := driveLatestScopeFromCmd(newDriveListScopeCmd(t, tc.flags), 3, "")
|
||
// 三条成因组合下的恢复命令都必须带原查询域。
|
||
for _, variant := range []struct {
|
||
label string
|
||
truncated bool
|
||
errs []driveDepthError
|
||
}{
|
||
{"truncated_only", true, nil},
|
||
{"folder_failure_only", false, twoDriveDepthErrors()},
|
||
{"both", true, twoDriveDepthErrors()},
|
||
} {
|
||
err := driveLatestIncompleteError(5, variant.truncated, variant.errs, scope)
|
||
var cliErr *CLIError
|
||
if !errors.As(err, &cliErr) {
|
||
t.Fatalf("%s: err = %T %v", variant.label, err, err)
|
||
}
|
||
assertDriveLatestSuggestion(t, cliErr.Suggestion, tc.want)
|
||
}
|
||
})
|
||
}
|
||
|
||
// --workspace 优先于 --space-id:与 drive list 的路由判定同序(先看 workspace 再看 space-id),
|
||
// 否则恢复命令会把知识库查询写成钉盘查询。
|
||
t.Run("workspace_wins_over_space_id", func(t *testing.T) {
|
||
scope := driveLatestScopeFromCmd(newDriveListScopeCmd(t, map[string]string{
|
||
"workspace": "ws-1", "space-id": "sp-7",
|
||
}), 3, "")
|
||
err := driveLatestIncompleteError(5, false, twoDriveDepthErrors(), scope)
|
||
suggestion := err.(*CLIError).Suggestion
|
||
if !strings.Contains(suggestion, "--workspace ws-1") || strings.Contains(suggestion, "--space-id") {
|
||
t.Fatalf("workspace 应优先且不混入 space-id: %q", suggestion)
|
||
}
|
||
})
|
||
|
||
// 无查询域时不得凭空造 flag(原调用就是「我的文件」根,硬塞 scope 同样是改查询域)。
|
||
t.Run("no_scope_adds_nothing", func(t *testing.T) {
|
||
scope := driveLatestScopeFromCmd(newDriveListScopeCmd(t, nil), 3, "")
|
||
err := driveLatestIncompleteError(5, false, twoDriveDepthErrors(), scope)
|
||
suggestion := err.(*CLIError).Suggestion
|
||
if strings.Contains(suggestion, "--workspace") || strings.Contains(suggestion, "--space-id") {
|
||
t.Fatalf("无 scope 时不应凭空造查询域 flag: %q", suggestion)
|
||
}
|
||
assertDriveLatestSuggestion(t, suggestion, "")
|
||
})
|
||
|
||
// depth==1(知识库 --latest 单层)时不给 --depth 1:partial+errors[] 契约只在多层成立,
|
||
// 硬塞 --depth 1 会让「去掉 --latest 看明细」的子句自相矛盾。
|
||
t.Run("single_depth_omits_depth_flag", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(5, false, twoDriveDepthErrors(), driveLatestScope{domain: "--workspace ws-1", depth: 1})
|
||
suggestion := err.(*CLIError).Suggestion
|
||
if strings.Contains(suggestion, "--depth") {
|
||
t.Fatalf("单层不应出现 --depth: %q", suggestion)
|
||
}
|
||
})
|
||
|
||
// 多层时给出确切层数,用户无需把 <原层数> 换成数字。
|
||
t.Run("multi_depth_emits_actual_depth", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(5, false, twoDriveDepthErrors(), driveLatestScope{domain: "--space-id sp-7", depth: 4})
|
||
suggestion := err.(*CLIError).Suggestion
|
||
if !strings.Contains(suggestion, "--depth 4") {
|
||
t.Fatalf("应给出原层数 --depth 4: %q", suggestion)
|
||
}
|
||
if strings.Contains(suggestion, "<原层数>") {
|
||
t.Fatalf("不应残留占位符: %q", suggestion)
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestScopeQuotesHostileValues 覆盖评审的 P1 阻断项:恢复命令是给用户直接复制到
|
||
// shell 里执行的,查询域的值来自用户输入(workspace 常见形态就是带查询串的 URL)。未引用时
|
||
// 一个 `&` 就把命令拆成后台任务,`;` / `$()` 更能执行额外内容。
|
||
// 断言落在「值被完整包在单引号里」而不只是「出现过」——后者对裸拼接也成立,抓不住缺陷。
|
||
func TestCrossPlatformCoverageDriveLatestScopeQuotesHostileValues(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
flag string
|
||
value string
|
||
want string
|
||
}{
|
||
// 合法 workspace URL:& 在裸拼接下直接改变 shell 解析(前半段被丢进后台)。
|
||
{name: "workspace_url", flag: "workspace", value: "https://alidocs.dingtalk.com/i/nodes/abc?spaceId=1&type=doc",
|
||
want: `--workspace 'https://alidocs.dingtalk.com/i/nodes/abc?spaceId=1&type=doc'`},
|
||
// 命令替换:裸拼接会在用户复制执行时真的跑起来。
|
||
{name: "command_substitution", flag: "workspace", value: "$(id)", want: `--workspace '$(id)'`},
|
||
// 分号拆语句。
|
||
{name: "semicolon", flag: "space-id", value: "sp-7;id", want: `--space-id 'sp-7;id'`},
|
||
// 空格拆参:裸拼接会让 --folder 收到错误的值。
|
||
{name: "space", flag: "space-id", value: "sp 7", want: `--space-id 'sp 7'`},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
// 显式注入 POSIX 策略:本测试断言的是「危险值被单引号正确包住」这一 POSIX 形态。
|
||
// Windows 策略下这些值根本不内联(见 ...SuggestionNeverInlinesHostileValue),
|
||
// 若走平台绑定,这里在 Windows runner 上会因形态不同而误报。
|
||
scope := driveLatestScopeFrom(newDriveListScopeCmd(t, map[string]string{tc.flag: tc.value}), 3, "", driveLatestPosixScopeValue)
|
||
err := driveLatestIncompleteError(5, true, twoDriveDepthErrors(), scope)
|
||
suggestion := err.(*CLIError).Suggestion
|
||
if !strings.Contains(suggestion, tc.want) {
|
||
t.Fatalf("查询域未安全引用\n want substring: %s\n got suggestion: %s", tc.want, suggestion)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestScopePreservesFiltersAndFolder 覆盖自动 CR 的 P2:
|
||
// --pattern/--type/--start/--end 与 --folder 同样决定 --latest 的候选集合。恢复命令丢掉任一项,
|
||
// 用户照抄后就是在**另一个集合**上取 Top-N —— 原调用带 --pattern 时,缺了它的重试命令会对全部
|
||
// 条目排序,结果「看起来成功」却答非所问,比直接报错更难发现。
|
||
func TestCrossPlatformCoverageDriveLatestScopePreservesFiltersAndFolder(t *testing.T) {
|
||
cmd := newDriveListScopeCmd(t, map[string]string{
|
||
"workspace": "ws-1",
|
||
"pattern": "*日报*",
|
||
"type": "file",
|
||
"start": "7d",
|
||
"end": "2026-08-01",
|
||
})
|
||
// 注入 POSIX 策略:`--pattern '*日报*'` 这种引用形态是 POSIX 专属,Windows 下该值会降级为
|
||
// 占位符 + 展示行(见 ...SuggestionNeverInlinesHostileValue)。走平台绑定会在 Windows 误报。
|
||
scope := driveLatestScopeFrom(cmd, 4, "folder-root", driveLatestPosixScopeValue)
|
||
err := driveLatestIncompleteError(5, false, twoDriveDepthErrors(), scope)
|
||
suggestion := err.(*CLIError).Suggestion
|
||
|
||
// 每条示例命令都要带齐查询域与全部过滤条件;pattern 含 * 与中文,必须是引用后的形态。
|
||
for _, want := range []string{
|
||
"--workspace ws-1",
|
||
"--pattern '*日报*'",
|
||
"--type file",
|
||
"--start 7d",
|
||
"--end 2026-08-01",
|
||
} {
|
||
for _, clause := range strings.Split(suggestion, ";") {
|
||
cmdText := extractTrailingDwsCommand(clause)
|
||
if cmdText == "" {
|
||
continue
|
||
}
|
||
if !strings.Contains(cmdText, want) {
|
||
t.Fatalf("恢复命令丢失候选集条件 %q:\n clause: %s", want, cmdText)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 「去掉 --latest 按原范围重跑」是唯一的原范围命令,必须原样带回原 --folder 而非占位符,
|
||
// 否则它就不是「原范围」。
|
||
var origin string
|
||
for _, clause := range strings.Split(suggestion, ";") {
|
||
if strings.Contains(clause, "去掉 --latest") {
|
||
origin = extractTrailingDwsCommand(clause)
|
||
}
|
||
}
|
||
if origin == "" {
|
||
t.Fatalf("多层场景应给出「去掉 --latest 按原范围重跑」子句: %q", suggestion)
|
||
}
|
||
if !strings.Contains(origin, "--folder folder-root") {
|
||
t.Fatalf("原范围命令应保留原 --folder: %q", origin)
|
||
}
|
||
if strings.Contains(origin, "<可读子目录ID>") {
|
||
t.Fatalf("原范围命令不应把原 --folder 换成占位符: %q", origin)
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestScopeOmitsFolderAtSpaceRoot 原调用就在空间根时不得凭空
|
||
// 造 --folder:硬塞一个目录同样是改候选集。
|
||
func TestCrossPlatformCoverageDriveLatestScopeOmitsFolderAtSpaceRoot(t *testing.T) {
|
||
scope := driveLatestScopeFromCmd(newDriveListScopeCmd(t, map[string]string{"space-id": "sp-7"}), 3, "")
|
||
err := driveLatestIncompleteError(2, false, twoDriveDepthErrors(), scope)
|
||
suggestion := err.(*CLIError).Suggestion
|
||
for _, clause := range strings.Split(suggestion, ";") {
|
||
if !strings.Contains(clause, "去掉 --latest") {
|
||
continue
|
||
}
|
||
if strings.Contains(extractTrailingDwsCommand(clause), "--folder") {
|
||
t.Fatalf("空间根扫描的原范围命令不应带 --folder: %q", clause)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestErrorStripsRemoteControlChars 覆盖自动 CR 的 P2 安全项:
|
||
// 拒绝产出后,目录名与服务端错误文本从 JSON(编码时会被转义)挪进了纯文本 stderr。若原样透传,
|
||
// 其中的 ANSI/OSC 序列会被终端直接执行 —— 可清屏、伪造彩色「成功」、隐藏后续输出、改窗口标题,
|
||
// 在 AI Agent 场景还会污染上下文窗口。目录名对共享目录而言是他人可控输入。
|
||
func TestCrossPlatformCoverageDriveLatestErrorStripsRemoteControlChars(t *testing.T) {
|
||
assertNoControlChars := func(t *testing.T, msg string) {
|
||
t.Helper()
|
||
for name, r := range map[string]rune{"ESC": 0x1b, "BEL": 0x07, "CR": '\r', "LF": '\n', "TAB": '\t'} {
|
||
if strings.ContainsRune(msg, r) {
|
||
t.Fatalf("错误消息残留 %s 控制字符: %q", name, msg)
|
||
}
|
||
}
|
||
}
|
||
|
||
// folderName 路径:CSI 清屏 + 变色,外加 OSC 改标题。
|
||
t.Run("folder_name", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(3, false, []driveDepthError{{
|
||
Depth: 2,
|
||
FolderName: "报表\x1b[2J\x1b[31m看起来成功\x1b[0m",
|
||
Reason: "permission_denied",
|
||
Message: "denied\r\n\x1b]0;pwned\x07次行",
|
||
}}, driveLatestScope{depth: 2})
|
||
msg := err.(*CLIError).Message
|
||
assertNoControlChars(t, msg)
|
||
// 清理不能把诊断信息一起抹掉:可读部分必须留下。
|
||
if !strings.Contains(msg, "报表") || !strings.Contains(msg, "denied") || !strings.Contains(msg, "次行") {
|
||
t.Fatalf("清理后应保留可读文本: %q", msg)
|
||
}
|
||
})
|
||
|
||
// folderName 为空时回落到 folderID,该字段同样来自服务端。
|
||
t.Run("folder_id_fallback", func(t *testing.T) {
|
||
err := driveLatestIncompleteError(1, true, []driveDepthError{{
|
||
FolderID: "fid\x1b[1m-x",
|
||
Reason: "api_error",
|
||
Message: "boom",
|
||
}}, driveLatestScope{depth: 1})
|
||
msg := err.(*CLIError).Message
|
||
assertNoControlChars(t, msg)
|
||
if !strings.Contains(msg, "fid-x") {
|
||
t.Fatalf("剥离控制序列后 folderID 应连成 fid-x: %q", msg)
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestSafeRemoteText 直接钉住清理函数的各条分支:换行/制表符
|
||
// 折成空格(SanitizeForTerminal 按设计保留这两者,但单行错误消息里它们会拆出伪造行),
|
||
// 首尾空白收掉,可打印内容与中文原样保留。
|
||
func TestCrossPlatformCoverageDriveLatestSafeRemoteText(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
in string
|
||
want string
|
||
}{
|
||
{name: "plain", in: "denied", want: "denied"},
|
||
{name: "cjk", in: "报表目录", want: "报表目录"},
|
||
{name: "csi", in: "a\x1b[31mb", want: "ab"},
|
||
{name: "osc", in: "a\x1b]0;t\x07b", want: "ab"},
|
||
{name: "newline_to_space", in: "a\nb", want: "a b"},
|
||
{name: "tab_to_space", in: "a\tb", want: "a b"},
|
||
{name: "carriage_return_dropped", in: "a\rb", want: "ab"},
|
||
{name: "trim_outer", in: "\n denied \t", want: "denied"},
|
||
{name: "empty", in: "", want: ""},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if got := driveLatestSafeRemoteText(tc.in); got != tc.want {
|
||
t.Fatalf("driveLatestSafeRemoteText(%q) = %q, want %q", tc.in, got, tc.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageDriveLatestSuggestionNeverInlinesHostileValue 端到端锁定评审提出的
|
||
// Windows 注入阻断项:`--space-id sp-7&whoami` 这种值,在 POSIX 下靠单引号关停,但 cmd.exe
|
||
// 不把单引号当引号,粘贴后 `&whoami` 仍会执行。因此不变量必须是「恢复命令里不存在能被目标
|
||
// shell 解释的裸元字符」,两个平台分别用各自的手段满足:POSIX 引用内联,Windows 不内联。
|
||
//
|
||
// 两种形态都通过注入渲染策略在本机验证,不依赖当前 GOOS —— 否则「Windows 上降级为占位符」
|
||
// 这条路在 POSIX 机器上永不可达,就成了只有 Windows runner 才跑到的盲区(平台覆盖率门禁也会
|
||
// 因此报未覆盖)。平台绑定本身由 TestCrossPlatformCoverageDriveLatestScopeValueBinding 覆盖。
|
||
func TestCrossPlatformCoverageDriveLatestSuggestionNeverInlinesHostileValue(t *testing.T) {
|
||
const hostile = "sp-7&whoami"
|
||
newSuggestion := func(t *testing.T, render driveLatestValueRenderer) string {
|
||
t.Helper()
|
||
scope := driveLatestScopeFrom(newDriveListScopeCmd(t, map[string]string{"space-id": hostile}), 3, "", render)
|
||
return driveLatestIncompleteError(5, true, twoDriveDepthErrors(), scope).(*CLIError).Suggestion
|
||
}
|
||
|
||
t.Run("windows_never_inlines", func(t *testing.T) {
|
||
suggestion := newSuggestion(t, driveLatestWindowsScopeValue)
|
||
for _, clause := range strings.Split(suggestion, ";") {
|
||
cmdText := extractTrailingDwsCommand(clause)
|
||
if cmdText == "" {
|
||
continue
|
||
}
|
||
// cmd.exe 里 & 分隔命令,且单引号不是引号,故它压根不能进命令。
|
||
if strings.Contains(cmdText, "&") {
|
||
t.Fatalf("windows 形态的命令不得含 &: %s", cmdText)
|
||
}
|
||
if strings.Contains(cmdText, hostile) {
|
||
t.Fatalf("windows 形态的命令不得内联原值: %s", cmdText)
|
||
}
|
||
}
|
||
if !strings.Contains(suggestion, driveLatestUnsafeValuePlaceholder) {
|
||
t.Fatalf("应降级为占位符: %s", suggestion)
|
||
}
|
||
if !strings.Contains(suggestion, strconv.Quote(hostile)) {
|
||
t.Fatalf("应在展示行给出原值: %s", suggestion)
|
||
}
|
||
if !strings.Contains(suggestion, "不是可执行命令") {
|
||
t.Fatalf("展示行须显式声明非可执行: %s", suggestion)
|
||
}
|
||
})
|
||
|
||
t.Run("posix_quotes_inline", func(t *testing.T) {
|
||
suggestion := newSuggestion(t, driveLatestPosixScopeValue)
|
||
if !strings.Contains(suggestion, "'"+hostile+"'") {
|
||
t.Fatalf("posix 形态应单引号内联原值: %s", suggestion)
|
||
}
|
||
// POSIX 下不该无谓降级 —— 那会白白损失可复制体验。
|
||
if strings.Contains(suggestion, driveLatestUnsafeValuePlaceholder) {
|
||
t.Fatalf("posix 形态不应降级为占位符: %s", suggestion)
|
||
}
|
||
for _, clause := range strings.Split(suggestion, ";") {
|
||
cmdText := extractTrailingDwsCommand(clause)
|
||
if cmdText == "" {
|
||
continue
|
||
}
|
||
if strings.Contains(cmdText, "&") && !strings.Contains(cmdText, "'"+hostile+"'") {
|
||
t.Fatalf("posix 形态出现未引用的元字符: %s", cmdText)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// newDriveListScopeCmd 造一个带 drive list 查询域与过滤 flag 的命令。flag 名与 newDriveCommand
|
||
// 里 driveListCmd 的注册保持一致;workspace-id 是 cross-product 别名,此处显式注册以覆盖别名路径。
|
||
func newDriveListScopeCmd(t *testing.T, flags map[string]string) *cobra.Command {
|
||
t.Helper()
|
||
cmd := &cobra.Command{Use: "list"}
|
||
for _, name := range []string{
|
||
"workspace", "workspace-id", "space-id", // 查询域
|
||
"folder", // 扫描根(scope 从 rootFolder 参数取,注册仅为对齐真实命令)
|
||
"pattern", "type", "start", "end", // 决定候选集的过滤条件
|
||
} {
|
||
cmd.Flags().String(name, "", "")
|
||
}
|
||
for name, value := range flags {
|
||
if err := cmd.Flags().Set(name, value); err != nil {
|
||
t.Fatalf("set --%s=%s: %v", name, value, err)
|
||
}
|
||
}
|
||
return cmd
|
||
}
|
||
|
||
// assertDriveLatestSuggestion 钉住 Suggestion 的三条约束:
|
||
// 1. 每条示例命令都带原查询域 wantScope(空串表示原调用无查询域,此时只跳过该项检查);
|
||
// 2. 含 --latest 的引导子句存在;
|
||
// 3. 「去掉 --latest」子句给出的示例命令本身不带 --latest(否则照抄复现同一错误)。
|
||
func assertDriveLatestSuggestion(t *testing.T, suggestion, wantScope string) {
|
||
t.Helper()
|
||
clauses := strings.Split(suggestion, ";")
|
||
sawLatestGuide := false
|
||
for _, clause := range clauses {
|
||
cmd := extractTrailingDwsCommand(clause)
|
||
if cmd == "" {
|
||
continue
|
||
}
|
||
if wantScope != "" && !strings.Contains(cmd, wantScope) {
|
||
t.Fatalf("示例命令丢失原查询域 %q(照抄会切换查询域): %q", wantScope, cmd)
|
||
}
|
||
if strings.Contains(clause, "去掉 --latest") {
|
||
if strings.Contains(cmd, "--latest") {
|
||
t.Fatalf("「去掉 --latest」子句的示例命令仍含 --latest: %q", cmd)
|
||
}
|
||
continue
|
||
}
|
||
if strings.Contains(cmd, "--latest") {
|
||
sawLatestGuide = true
|
||
}
|
||
}
|
||
if !sawLatestGuide {
|
||
t.Fatalf("no --latest-bearing guidance clause in suggestion: %q", suggestion)
|
||
}
|
||
}
|
||
|
||
// extractTrailingDwsCommand 抽子句里以 "dws " 开头的尾部命令片段(到子句末),无则空串。
|
||
func extractTrailingDwsCommand(clause string) string {
|
||
idx := strings.LastIndex(clause, "dws ")
|
||
if idx < 0 {
|
||
return ""
|
||
}
|
||
return clause[idx:]
|
||
}
|