docs(report): 新增 PROJECT_REPORT.md 与 CI/CD 流水线记录

PROJECT_REPORT.md:论文式项目报告(摘要 / 目录 / 7 章 / 参考文献 / 致谢 / 附录 A),
4 张 Mermaid 图同样经真实浏览器渲染验证。所有数字都与证据文件逐个核对过。

.scratch/ci-cd/:本次流水线的全部证据与可重跑脚本
  * 阶段报告:依赖安全扫描 / 许可证合规 / 阶段 0 静态分析 / 阶段 3 测试与性能
  * 证据:分析器原始输出(修复前 84 条、修复后 80 条)、Pester 详细输出、
    性能基线 JSON、黑盒用例结果(阶段 1 的 11 例与阶段 2 回归的 12 例)
  * 可重跑:blackbox-tests.ps1 / blackbox-regression.ps1 / perf-baseline.ps1

两条边界必须写在明处,不能含糊:

  * **VM 内验证只取到修复前的快照**(Pester 183 项全绿)。随后会话审批策略改为 never,
    gsudo 提权被自动拒绝(退出码 999),而 Hyper-V 与 PowerShell Direct 都需要管理员,
    于是修复后的三套件在 VM 内**没有跑成**。报告里明确标注了范围,没有把"宿主跑绿了"
    说成"VM 也跑绿了"。
  * **性能基线是首次建立**,无历史可比,故无退化可判;指标只在同机同宿主下对比,
    跨机比数字没有意义。

另外记一笔:静态分析的口径是"仓库自己的门禁"。剩余 80 条全是风格类(0 Error),且逐条
有依据 —— 行长 160 是配置里写明的有意偏离,PSPlaceCloseBrace 等集中在测试夹具字符串内
(改了会改变断言语义)。严格模式的"零容忍"在这里与仓库自身约定相抵,选择尊重仓库约定
并在报告里登记,而不是制造一个横跨 12 个文件的纯排版大 diff。
This commit is contained in:
Shuery committed 2026-10-02 01:17:40 +08:00
1 parent f4729baca7
commit 45bbd66815
18 files changed
+2974

No files matched your search

@@ -0,0 +1,333 @@
Pester 5.9.1 (D:\Workspace\Temp\BakNRet\.tools\modules\Pester\5.9.1)
测试文件:D:\Workspace\Temp\BakNRet\tests
Pester v5.9.1
Starting discovery in 3 files.
Discovery found 185 tests in 226ms.
Running tests.
Running tests from 'D:\Workspace\Temp\BakNRet\tests\BakNRet.Formats.Tests.ps1'
Describing 软件名录:Slot 形状(新契约)
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 一个软件多个 Slot:Kind=Multi,Slot 按名排序且说明被保留 163ms (131ms|32ms)
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 每个 Slot 都是一个独立的归档项来源(Kind=slot / Origin=catalog) 82ms (81ms|1ms)
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] Slot 级排除写在 Slot 自己身上(相对本 Slot 的归档根) 32ms (30ms|1ms)
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 数组里"当前不存在"的 Slot 仍然产出归档项(恢复要靠它还原回原位) 64ms (64ms|1ms)
[+] 文件 Slot:归档项是文件项(归档里就是名为 Slot 的文件) 8ms (7ms|1ms)
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 旧的裸字符串 / 字符串数组写法被拒绝(ERROR + 跳过) 22ms (22ms|1ms)
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacystr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 'legacyarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:05] [ERROR] 名录条目 legacydirs 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 旧的 @{ Dirs = @(...) } 写法不再展开,留下 Invalid 与原因 24ms (23ms|1ms)
[+] 多 Slot 的名录条目在清单里仍然按软件名命名归档 3ms (3ms|0ms)
Describing 清单修饰符:新契约格式
[+] :: 覆盖 Path:单 Slot 条目直接生效 13ms (12ms|2ms)
[+] :: 覆盖遇到多 Slot 条目 -> Blocking(不知道给哪一个,绝不猜) 6ms (6ms|0ms)
[+] :- 排除与 :+ 包含并存,顺序任意 15ms (15ms|1ms)
[+] @ Exclude / @ Include / @ Path 与记号写法等价 7ms (6ms|0ms)
[+] :encrypt / :!encrypt 覆盖名录里的加密默认值 11ms (11ms|0ms)
[+] 遗留写法 @encrypt / @pathname / @root= 仍可解析 18ms (17ms|1ms)
[+] 同一行里重复写同类记号会累积(不静默丢掉前一条规则) 12ms (11ms|1ms)
[2026-09-28 12:42:05] [WARN] 清单行缺少目标,已忽略::- logs\
[+] 行尾说明与缺少目标的行 6ms (6ms|1ms)
Describing 集成:Slot 布局的打包与恢复
[+] 备份退出码 0,归档名就是软件名 9ms (7ms|2ms)
[+] 归档顶层就是各个 Slot 名(目录 Slot + 文件 Slot + Include 项) 42ms (41ms|1ms)
[+] manifest.layouts 记下每个归档项是目录还是文件 8ms (7ms|1ms)
[+] 归档内容:\ 布局,文件 Slot 是名为 Slot 的文件,Slot 排除生效 40ms (39ms|1ms)
[+] 真实恢复:目录 Slot、文件 Slot 与 Include 都落回各自的原位 2.37s (2.37s|1ms)
[+] 文件 Slot 在目标不存在时靠 manifest.layouts 恢复成文件(而不是目录) 3ms (2ms|0ms)
[+] 恢复之后 manifest 记下 lastRestoreAt 4ms (4ms|0ms)
Describing 集成:旧布局归档的回退恢复
[+] 归档确实是旧布局:顶层是源目录名而不是 Slot 名 30ms (29ms|1ms)
[+] 归档里缺 Slot 层(真实旧归档)时按旧布局回退,把内容还原回原位 1.75s (1.75s|0ms)
[+] 归档里既没有 Slot 层、也没有旧布局名字时明确失败(不再"成功地什么都没恢复") 1.72s (1.72s|0ms)
Describing 集成:归档内路径冲突会被拒绝执行
[+] 退出码 1,且给出"归档内路径冲突"的原因,不生成归档 4ms (3ms|1ms)
[+] manifest 里记下这次是 failed,并带上原因 3ms (3ms|0ms)
Describing 条目从清单里消失后,旧归档必须被点名为孤儿
[+] 归档确实还在磁盘上,manifest 里也还留着历史记录 5ms (4ms|1ms)
[+] 第二次运行把 my-app.7z 点名成孤儿,并说明 manifest 里还有历史记录 2ms (2ms|0ms)
Describing manifest 一致性:archive 字段只在文件真的存在时才写
[+] 存在的归档保留 archive,不存在的被清空 9ms (8ms|1ms)
[+] 清空 archive 时保留条目本身的历史(source / action 不动) 3ms (3ms|0ms)
[+] 人工删掉归档之后再同步一次,记录会被纠正过来 4ms (3ms|0ms)
Running tests from 'D:\Workspace\Temp\BakNRet\tests\BakNRet.Security.Tests.ps1'
Describing 排除判定与 7z 的 -x! / -xr! 语义对齐
[+] 锚定模式只命中它自己那棵子树 8ms (7ms|1ms)
[+] ! 通配按任意层级的组件名匹配(* 不是正则) 2ms (1ms|0ms)
[+] !re: 走正则,且组件名与整条相对路径都算命中 2ms (2ms|0ms)
[+] 没有模式时一律不排除 1ms (1ms|0ms)
[+] 模式里的空格按 7z 的规矩当 ? 处理 1ms (1ms|0ms)
Describing SID 映射(跨机恢复)
[+] 整 SID 精确替换 5ms (5ms|1ms)
[+] 不会误伤以它为前缀的更长的 SID 1ms (1ms|0ms)
[+] 空映射表时原样返回 1ms (1ms|0ms)
Describing 安全描述符采集
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] Full:每个对象一条记录,键是归档内相对路径 340ms (340ms|1ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 根记录的 SDDL 保留了 CREATOR OWNER、IO 标志、孤儿 SID 和 protected 位 12ms (12ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] Smart 比 Full 少,但根永远保留 24ms (23ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] Roots 只存归档项的根,不再往下走 4ms (4ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] sidecar 往返:条数与 SDDL 原样保留 25ms (24ms|0ms)
[+] 旁挂文件不存在时读出 $null(调用方据此打告警,而不是静默当没事) 1ms (1ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 排除模式在采集时同样生效(采集树 == 归档树) 9ms (9ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
Describing 安全描述符回放
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 回放后根对象的安全描述符与源逐字节一致(protected / CO / 孤儿 SID 全在) 40ms (39ms|1ms)
[+] 全部对象的安全指纹与源一致(属主/属组/ACE 集合) 5ms (5ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 目标不存在或不是普通对象时记 Skipped,不记 Failed 3ms (3ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 归档根名对不上时一条都不回放(不会把兄弟项的 ACL 倒过来) 1ms (1ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 属组写不进去时不会连累 DACL:回退到底也要把 ACL 落下去 7ms (7ms|0ms)
[2026-09-28 12:42:21] [WARN] 这些特权不在当前令牌里(需要管理员或 SYSTEM):SeRestorePrivilege、SeBackupPrivilege —— 属主将无法改成别的账户,只能恢复 DACL
[+] 对象的安全描述符读不到时带 e 记账,回放时跳过而不是写坏 2ms (2ms|0ms)
Describing 与 Backup.ps1 / Restore.ps1 的集成
[+] 备份会写出 .acl.json,并在 manifest 里记下它 2.34s (2.34s|1ms)
[+] 恢复会把安全描述符回放回去(删源之后仍然逐对象与备份前一致) 2.14s (2.14s|0ms)
[+] -SkipSecurity 时不回放(目标保持新建对象的默认 ACL) 1.77s (1.77s|0ms)
[+] 归档旁边没有 acl.json 时打告警、不算失败(旧归档照样恢复得出来) 1.76s (1.76s|0ms)
Running tests from 'D:\Workspace\Temp\BakNRet\tests\BakNRet.Tests.ps1'
Describing BackupList.txt 解析
[+] 注释行与空行返回 $null 2ms (2ms|1ms)
[+] 裸路径:Path 原样、方向 both、不是软件名、没有任何覆盖 6ms (6ms|0ms)
[+] 软件名:IsName 为真 2ms (1ms|0ms)
[+] IsName 判定:含分隔符或 % 就算字面路径 3ms (2ms|0ms)
[+] 标记必须是独立记号:C:\a:-b 仍然只是一个路径 2ms (2ms|0ms)
[+] 行首 + 仅备份、- 仅恢复,方向标记不进入目标 4ms (4ms|0ms)
[+] 只有方向标记、没有目标 -> 忽略该行 2ms (2ms|0ms)
[+] 行首标记贴在目标上也算(+Name / -Path),且标记不进入目标 4ms (4ms|0ms)
[+] 非行首的 + / - 不是方向标记 22ms (22ms|0ms)
[+] :: 现在表示"覆盖 Path",与 :- 彻底分开 8ms (5ms|3ms)
[+] :: 的值可以带空格(一直取到下一个标记之前) 2ms (2ms|0ms)
[+] :- 的逗号 / 分号列表拆成多个模式 7ms (6ms|0ms)
[+] 排除模式的引号只保护空白,不保护逗号 3ms (2ms|0ms)
[+] :+ 的包含项是 : 2ms (2ms|0ms)
[+] :encrypt 与 :!encrypt 控制该条目的加密开关 2ms (2ms|0ms)
[+] @ Key='Value' 覆盖 Path / Exclude / Include / Encrypt 4ms (4ms|0ms)
[+] @ 的键名大小写不敏感,且支持紧跟 @ 的写法 2ms (2ms|0ms)
[+] 历史写法 @encrypt / @!encrypt 仍然被识别成加密覆盖 2ms (2ms|0ms)
[+] 历史写法 @pathname / @root= 进入 Flags 3ms (2ms|0ms)
[2026-09-28 12:42:30] [WARN] 清单里的 @ 字段 'UnknownKey' 不是已知字段(Path / Exclude / Include / Encrypt),已忽略:Foo @ UnknownKey='v' :- logs\
[+] 未知的 @ 字段进入 UnknownKeys,且不影响其余字段解析 2ms (2ms|0ms)
[+] 行尾的 # 说明会成为 Comment 2ms (2ms|0ms)
[+] 路径里紧贴的 # 不会被当成注释 1ms (1ms|0ms)
[+] 没有说明时 Comment 为空 1ms (1ms|0ms)
[+] 目标可以带空格:第一个标记之前整段都是目标 3ms (2ms|0ms)
[2026-09-28 12:42:30] [WARN] 清单行缺少目标,已忽略::- logs
[2026-09-28 12:42:30] [WARN] 清单行缺少目标,已忽略:@ Exclude='x'
[+] 缺少目标(标记出现在第一个位置)会被忽略并告警 2ms (2ms|0ms)
[+] 记号切分:引号内的空白不切分,引号本身留在记号里 2ms (1ms|0ms)
[+] 记号识别只认完整记号 2ms (2ms|0ms)
[+] 去引号:成对才去,不成对原样返回 2ms (1ms|0ms)
Describing 归档命名
[+] 基础命名规则:末级名_from_上级路径用加号连接 2ms (1ms|1ms)
[+] / 与 \ 以及重复分隔符结果一致 2ms (1ms|0ms)
[+] 命名与路径往返 6ms (6ms|0ms)
[+] 归档名里不含非法文件名字符 4ms (4ms|0ms)
[+] %变量% 写法里的 % 会保留在归档名里 1ms (1ms|0ms)
[+] 软件名条目:默认用软件名做归档名 5ms (4ms|0ms)
[+] 字面路径条目:仍用路径命名算法(现有清单无需改写) 2ms (2ms|0ms)
[+] @pathname 用名录里的真实路径命名,而不是软件名 4ms (3ms|0ms)
[2026-09-28 12:42:30] [WARN] 名录里没有 'no-such-thing',按目录名处理
[2026-09-28 12:42:30] [WARN] 名录里没有 'no-such-thing',按目录名处理
[+] 名录里没有该名字:归档名退回可读目录名,并给出 Error 5ms (4ms|0ms)
Describing 7z 排除参数翻译
[+] 相对模式自动补上归档根目录名 5ms (5ms|1ms)
[+] 模式已带根名前缀时,Split-BakNRetPatternScope 先摘掉前缀,结果不重复 5ms (4ms|0ms)
[+] ! 前缀翻译成递归组件匹配(-xr!) 1ms (1ms|0ms)
[+] 模式里的空格转成 ?(归档项自己的名字按原样保留) 2ms (1ms|0ms)
[+] 生成的参数里绝不出现引号(旧实现 -x!"路径" 让排除全部失效) 1ms (1ms|0ms)
[+] 空模式被忽略 2ms (1ms|0ms)
[+] !re: 会把正则展开成精确的 -x! 参数 25ms (25ms|0ms)
[+] 非法正则给出 Error,而不是抛异常 6ms (6ms|0ms)
[+] 正则命中数超过上限时明确报错 3ms (3ms|0ms)
[+] 参数总长超过安全上限时明确报错 1ms (1ms|0ms)
[+] Split-BakNRetPatternScope:指名 Slot 的模式只分给那个 Slot 2ms (1ms|0ms)
[+] Split-BakNRetPatternScope:没点名任何 Slot 的模式广播给每一项 1ms (1ms|0ms)
[+] Split-BakNRetPatternScope:! 与 !re: 模式广播给每一项 1ms (1ms|0ms)
[+] Split-BakNRetPatternScope:每个归档项都有键(没有模式时是空数组) 2ms (2ms|0ms)
[+] Merge-BakNRetExcludeArgument 去重且保持顺序 3ms (2ms|0ms)
[+] 多 Slot 条目:逐项分配 + 翻译 + 去重合成一份参数 3ms (3ms|0ms)
Describing 归档项与暂存目录
[+] New-BakNRetArchiveItem 规范化归档内路径并算出 TopName 3ms (3ms|1ms)
[+] Get-BakNRetArchiveTopName 取归档内相对路径的第一段 1ms (1ms|0ms)
[+] 暂存半途失败会自己清干净(不留指向真实数据的 junction) 33ms (32ms|0ms)
[+] 目录项用 junction 挂进暂存目录,文件项用硬链接(名字就是归档内路径) 18ms (17ms|0ms)
[+] Remove-BakNRetArchiveStaging 只删连接点,不顺着走进真实目录 8ms (8ms|0ms)
Describing 命令行参数拼接
[+] 无空格参数原样输出 2ms (1ms|1ms)
[+] 含空格参数加引号 3ms (2ms|0ms)
[+] 引号内的结尾反斜杠翻倍(否则会被当成转义引号) 2ms (1ms|0ms)
[+] 内部引号被转义 1ms (1ms|0ms)
[+] 空参数输出一对空引号 2ms (1ms|1ms)
Describing manifest 与配置
[+] manifest 读写往返 10ms (9ms|1ms)
[+] manifest 原样保存 layouts(name/kind),全新恢复时靠它判断目录还是文件 7ms (7ms|0ms)
[2026-09-28 12:42:30] [WARN] manifest 解析失败(将重新建立):C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\broken.json —— Conversion from JSON failed with error: Invalid character after parsing property name. Expected ':' but got: i. Path '', line 1, position 7.
[+] manifest 损坏时不抛异常,而是重建空清单 9ms (8ms|0ms)
[+] 配置缺失时返回默认值 4ms (4ms|0ms)
[+] 配置嵌套段落合并且不丢默认键 6ms (6ms|0ms)
[+] 口令:环境变量可读取,取不到时返回 $null(绝不退化成明文) 3ms (3ms|0ms)
Describing 软件名录
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 名录解析:条目与 Slot 的字段集合就是新契约 54ms (53ms|1ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 一个软件多个 Slot:Kind=Multi,Slot 按名字排序 33ms (33ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] Kind:Single / Multi / Partial / Unresolved / Invalid 34ms (34ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 前缀补全:_ 与 - 都会被补全 28ms (27ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 不会把 Legendary 误配成 LegendarySomething 28ms (27ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 一个 Slot 命中多个候选目录:报 Error,绝不悄悄挑一棵树 31ms (31ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 文件 Slot:Path 指向文件时 IsFile 为真,Encrypt 也带上 25ms (25ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] %变量% 会在名录路径里展开 27ms (27ms|0ms)
[+] $( ... ) 子表达式会被求值(含嵌套) 6ms (6ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[2026-09-28 12:42:30] [ERROR] 名录条目 nopath 有问题:Slot S 缺少 Path
[2026-09-28 12:42:30] [ERROR] 名录条目 dupslot 有问题:Slot D 的 Path 匹配到 2 个目录:C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_1、C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\catalog\dup_2;一个 Slot 只能对应一个目录,请拆成多个 Slot
[2026-09-28 12:42:30] [ERROR] 名录条目 badslot 有问题:Slot S 的写法不对,应写成 @{ Path = '...' }
[+] 读取结果按"路径 + 时间戳 + 长度 + 内容 MD5"缓存 63ms (62ms|0ms)
[+] 名录文件内容变了以后缓存自动失效 17ms (16ms|0ms)
[+] Includes:分文件维护的名录会被合并 12ms (11ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 'arr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:30] [ERROR] 名录条目 'objarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:30] [ERROR] 名录条目 'bare' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:30] [ERROR] 名录条目 dirstyle 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 旧的裸字符串 / 字符串数组 / 对象数组写法都会报 ERROR 并被跳过 3ms (3ms|0ms)
[2026-09-28 12:42:30] [ERROR] 名录条目 'arr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:30] [ERROR] 名录条目 'objarr' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:30] [ERROR] 名录条目 'bare' 格式不对:应写成 @{ <Slot 名> = @{ Path = '...' } }
[2026-09-28 12:42:30] [ERROR] 名录条目 dirstyle 有问题:Slot Dirs 的写法不对,应写成 @{ Path = '...' }
[+] 旧的 @{ Dirs = @(...) } 写法不再展开:留下 Invalid 条目和原因 4ms (3ms|0ms)
[+] 软件名会被规范化成合法文件名 2ms (1ms|0ms)
Describing Resolve-BakNRetBackupEntry:条目解析
[+] resolve 与归档项的字段集合就是新契约(旧字段已删除) 4ms (4ms|1ms)
[+] 软件名条目:isName/CatalogEntry/BaseName/ArchiveFlavor/Source 7ms (6ms|0ms)
[+] 软件名条目:Items 来自 Slot(ArchivePath=Slot 名,Kind=slot,Origin=catalog) 6ms (6ms|0ms)
[+] 字面路径条目:一个 path 项(ArchivePath 是末级名) 3ms (3ms|0ms)
[+] 字面路径条目上的 @ Path= 覆盖目标路径,但归档名仍按原路径 2ms (2ms|0ms)
[+] 名录里的路径不存在时仍给出 Items(恢复要靠它还原回原位) 5ms (5ms|0ms)
[+] :: 覆盖 Path:单 Slot 条目直接生效 5ms (5ms|0ms)
[+] :: / @ Path= 覆盖遇到多 Slot 条目 -> Blocking(不猜是哪一个) 5ms (5ms|0ms)
[+] 条目级 :- 覆盖名录里的排除:HasExcludeOverride 为真 3ms (3ms|0ms)
[+] 名录 Slot 的 Include 会追加成 include 项 5ms (5ms|0ms)
[+] 条目级 :+ / @ Include= 覆盖名录里的 Include 11ms (11ms|0ms)
[2026-09-28 12:42:30] [WARN] encapp:名录里各 Slot 的 Encrypt 不一致,整个归档按加密处理
[+] 加密:名录里各 Slot 取或;条目级 :encrypt / :!encrypt 覆盖 9ms (9ms|0ms)
[+] 方向标记会传递到 Resolve-BakNRetBackupEntry.Direction 5ms (5ms|0ms)
[+] 归档内路径冲突(include 与 Slot 同名)-> Blocking 5ms (4ms|0ms)
[+] 归档内路径冲突(父子关系)-> Blocking 5ms (5ms|0ms)
[2026-09-28 12:42:30] [WARN] 名录里没有 'no-such-thing',按目录名处理
[+] 名录里没有该名字:Error 给出,Items 为空 3ms (2ms|0ms)
Describing 外部命令退出码(旧实现的核心缺陷)
[2026-09-28 12:42:30] [DEBUG] 执行: cmd.exe /c "exit 0" -p<口令已隐藏>
[+] 口令不会进日志:DEBUG 下打印的命令行要遮蔽 -p 参数 31ms (30ms|1ms)
[+] 运行锁:同一份备份目录同时只能有一个持有者 27ms (27ms|0ms)
[+] 运行锁:释放之后可以重新取得 8ms (7ms|0ms)
[+] 空目录的摘要给 0 而不是 $null(否则空间守卫会静默失效) 9ms (9ms|0ms)
[+] 原子替换:成功时新内容到位且不留 .tmp;失败时旧内容完好 14ms (14ms|0ms)
[+] Invoke-ExternalCommand 能拿到真实退出码 13ms (12ms|0ms)
[+] 成功时拿到 0 12ms (12ms|0ms)
Describing 集成:真实 7z 压缩与排除规则
[+] 排除规则与空格处理在真实归档上生效 55ms (54ms|1ms)
[+] 对照组:不加排除时被排除的文件确实在归档里(证明上一条不是空归档) 58ms (57ms|0ms)
ERROR: C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\integration\corrupt.7z
C:\Users\Shuery\AppData\Local\Temp\baknret-pester-ffa25c80\integration\corrupt.7z
Open ERROR: Cannot open the file as [7z] archive
ERRORS:
Headers Error
[+] 7z t 对完好归档返回 0,对损坏归档返回非 0(归档后校验的依据) 52ms (52ms|0ms)
Describing 集成:Backup.ps1 / Restore.ps1 端到端(字面路径条目)
[+] 备份退出码为 0(旧实现会把成功的压缩判成失败) 2ms (1ms|1ms)
[+] 归档已生成且 manifest 记录了条目、动作、校验结果与 layouts 5ms (5ms|0ms)
[+] 备份过程写了日志文件 3ms (2ms|0ms)
[+] 字面路径条目沿用 布局;归档里保留应保留内容、不含被排除项 32ms (32ms|0ms)
[+] [回归] manifest.roots 记录的是归档内真实的顶层条目名 32ms (31ms|0ms)
[+] Restore -DryRun 退出码 0,且一个字节都不写(manifest SHA256 不变) 1.57s (1.57s|0ms)
[+] Restore -WhatIf 同样不写盘 1.6s (1.6s|0ms)
[+] 真实恢复:退出码 0,文件逐字节一致,被排除项没有被恢复出来 2.12s (2.12s|0ms)
[+] 真实恢复之后 manifest 才被更新(lastRestoreAt) 2ms (2ms|0ms)
[+] 孤儿归档会被点名报告,但不影响退出码 2.37s (2.37s|0ms)
[+] 带 -Only 时不做孤儿审计(避免把未选中的归档误报成孤儿) 1.74s (1.74s|0ms)
[+] 源路径不存在时记为 missing-source,退出码仍为 0(跳过不算失败) 1.76s (1.76s|0ms)
[+] 归档名重复时直接报失败(退出码 1),不静默互相覆盖 2.37s (2.37s|0ms)
[+] [回归] 显式指定的清单不存在时报失败(退出码 1),且不在错误位置建模板 1.43s (1.43s|0ms)
[+] [回归] 未显式指定清单时的首次运行仍建模板并退出 0(引导不能被误伤) 1.44s (1.44s|0ms)
Describing 集成:方向标记与孤儿审计
[+] 行首 - 的条目:备份跳过它,但仍把它算作"有主"(不报成孤儿) 4ms (3ms|1ms)
[+] 行首 + 的条目:恢复跳过它、不写回目标,同时登记归档名 2ms (2ms|0ms)
Tests completed in 50.99s
Tests Passed: 185, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Pester 测试全部通过:185 项(跳过 0 项)