Files
BakNRet/tests/BakNRet.Formats.Tests.ps1
T
Shuery 7173e8ae10 备份前空间预估;manifest 的 archive 字段只在文件真的存在时才写
需求
- 写满盘这件事只做一件事:**备份前预测本次所需大小,并提示用户存储够不够**,
  不再往"更复杂的占用控制"方向做。

实现
- Backup.ps1 新增只读的"备份前空间预估",在动手之前按清单顺序模拟一遍:
  * 逐个条目枚举源目录得到真实源大小/文件数,并读取现有归档大小;
  * 沿用主循环那套"源未更新就跳过"的判断,所以列出来的就是**本次真的会重打**的条目;
  * 估算模型:临时归档写完时旧归档还在,那一刻占用 = 当前累计净增量 + 本次预估;
    原子替换后净增量 = 预估 − 旧归档大小;峰值取整个过程的最大值;
  * 预估归档大小:有历史归档取 min(源大小, 旧归档 × 1.3),没有则按"不压缩"的悲观值;
  * 打印:可用空间、要重打的条目数(及跳过/缺源的数量)、逐条目明细(前 15 条)、
    预计峰值新增与净增量,最后给一句结论——"空间足够"或"空间可能不够!…差 Z GB"。
  * 不够时**只告警、不中断**:真正放不下的条目仍由逐条目守卫跳过。
- 新增 Sync-BaknretManifestArchive(Common.psm1,Backup/Restore 都在写 manifest 前调用):
  维持不变式"manifest 里写了 archive 的记录,磁盘上就一定有那个文件",
  把指向不存在归档的 archive 字段清空,但保留 source / action / 历史计数。
  这样"源不存在的条目"不会再让 Restore 反复报"manifest 记录的归档不存在",
  人工删掉归档(例如把它并进别的条目)之后记录也会自我纠正。

整理(本机备份集)
- 按用户确认,删除了已被 scoop.7z 覆盖的 scoop-config.7z 与 scoop-persist.7z
  (删除前先 7z t 复验 scoop.7z:233 MB / 29478 项 / 顶层 [persist, scoop]),
  并清掉 manifest 里这两条历史记录;24 条记录的 archive 现在全部存在于磁盘上。

测试
- Pester 88 项、零依赖单元 49 项、端到端 23 项,全部通过。
  新增覆盖:对象数组名录、两种写法 × :+/-、同名目录拒绝执行、行尾 # 说明、
  孤儿审计(含"manifest 里还有历史记录"的说明)、archive 字段一致性、空间预估输出。
2026-09-22 08:26:09 +08:00

468 lines
23 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
清单"两种写法 + 追加/排除"的 Pester 测试:软件名、手写路径,:+/:- 两者都要生效。
.DESCRIPTION
这里覆盖的是清单/名录的**输入格式**契约:
* 写法一:直接写 SoftwareCatalog.psd1 里的软件名;
* 写法二:用户手写目录(含 \ / 或 % 就按路径处理);
* 两种写法都要支持 `:+` 追加与 `:-` 排除;
* 名录里一个软件可以挂**对象数组**(每个目录带 Description),运行时会逐条介绍;
* 同一条目里出现两个同名目录时,必须在归档前就明确报错(Blocking),
而不是把两棵树悄悄混在一起。
跟 BakNRet.Tests.ps1 一样,脚本调用统一走**子进程**:Backup.ps1 / Restore.ps1 结尾会
`exit`,同进程 `&` 调用会把 Pester 宿主一起带走。
.EXAMPLE
pwsh -File .\tests\Run-Pester.ps1
#>
$script:HasSevenZip = [bool](Get-Command 7z -ErrorAction SilentlyContinue)
$script:HasPwsh = [bool](Get-Command pwsh -ErrorAction SilentlyContinue)
BeforeAll {
$script:ProjectRoot = Split-Path -Parent $PSScriptRoot
$script:BackupScript = Join-Path $script:ProjectRoot 'Backup.ps1'
$script:RestoreScript = Join-Path $script:ProjectRoot 'Restore.ps1'
$script:SevenZip = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
Import-Module (Join-Path $script:ProjectRoot 'Common.psm1') -Force
$script:Sandbox = Join-Path $env:TEMP ('baknret-formats-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $script:Sandbox -Force | Out-Null
function Invoke-BaknretScript {
param(
[Parameter(Mandatory = $true)][string]$Script,
[hashtable]$Parameters = @{}
)
$arguments = @('-NoProfile', '-NonInteractive', '-File', $Script)
foreach ($name in ($Parameters.Keys | Sort-Object)) {
$value = $Parameters[$name]
if ($value -is [bool]) {
if ($value) { $arguments += "-$name" }
continue
}
$arguments += "-$name"
if ($value -is [array]) { $arguments += $value } else { $arguments += [string]$value }
}
$lines = & pwsh @arguments 2>&1
return [pscustomobject]@{
ExitCode = $LASTEXITCODE
Lines = @($lines | ForEach-Object { [string]$_ })
Output = (($lines | Out-String))
}
}
function Write-ListFile {
param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Content)
[System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false))
return $Path
}
}
AfterAll {
if ($script:Sandbox -and (Test-Path -LiteralPath $script:Sandbox)) {
Remove-Item -LiteralPath $script:Sandbox -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ============================================================================
Describe '软件名录:对象数组写法' {
# ============================================================================
BeforeAll {
$script:FormatRoot = Join-Path $script:Sandbox 'format'
New-Item -ItemType Directory -Path $script:FormatRoot -Force | Out-Null
$script:DirA = Join-Path $script:FormatRoot 'dirA'
$script:DirB = Join-Path $script:FormatRoot 'dirB'
foreach ($directory in $script:DirA, $script:DirB) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
Set-Content -LiteralPath (Join-Path $directory 'keep.txt') "keep-$directory"
}
# 两个不同父目录下各有一个**同名**子目录 —— 用来看"归档内同名"有没有被拦住
$script:CollideRoot = Join-Path $script:FormatRoot 'collide'
foreach ($parent in 'p1', 'p2') {
New-Item -ItemType Directory -Path (Join-Path $script:CollideRoot "$parent\dupdir") -Force | Out-Null
Set-Content -LiteralPath (Join-Path $script:CollideRoot "$parent\dupdir\x.txt") $parent
}
$dirAPath = $script:DirA
$dirBPath = $script:DirB
$collideP1 = Join-Path $script:CollideRoot 'p1\dupdir'
$collideP2 = Join-Path $script:CollideRoot 'p2\dupdir'
$script:FormatCatalog = Write-ListFile -Path (Join-Path $script:FormatRoot 'cat.psd1') -Content @"
@{
'pair' = @(
@{ Path = '$dirAPath'; Description = '第一个目录' }
@{ Path = '$dirBPath'; Description = '第二个目录' }
)
'collide' = @(
@{ Path = '$collideP1'; Description = 'p1 里的' }
@{ Path = '$collideP2'; Description = 'p2 里的' }
)
}
"@
$script:MissingDir = Join-Path $script:FormatRoot 'not-here'
$missingPath = $script:MissingDir
$script:PartialCatalog = Write-ListFile -Path (Join-Path $script:FormatRoot 'cat-partial.psd1') -Content @"
@{
'pair' = @(
@{ Path = '$dirAPath'; Description = '存在' }
@{ Path = '$missingPath'; Description = '不存在' }
)
}
"@
}
It '一个软件多个目录:顺序与说明都被保留' {
$catalog = Get-SoftwareCatalog -Path $script:FormatCatalog -MaxDepth 3
$catalog['pair'].Kind | Should -Be 'Multi'
@($catalog['pair'].Items).Count | Should -Be 2
$catalog['pair'].Items[0].Resolved | Should -Be $script:DirA
$catalog['pair'].Items[0].Description | Should -Be '第一个目录'
$catalog['pair'].Items[1].Description | Should -Be '第二个目录'
}
It '解析成多个源,每个源带着自己的说明' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 2
$resolved.Sources[0].SourcePath | Should -Be $script:DirA
$resolved.Sources[0].Description | Should -Be '第一个目录'
$resolved.Sources[1].Description | Should -Be '第二个目录'
@($resolved.Sources | ForEach-Object { $_.Origin }) | Should -Be @('catalog', 'catalog')
}
It '数组里"当前不存在"的目录仍然产出源(恢复要靠它还原回原位)' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:PartialCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 2
@($resolved.Sources | ForEach-Object { $_.SourcePath }) | Should -Contain $script:MissingDir
$resolved.Error | Should -Not -BeNullOrEmpty # 有提示
$resolved.Blocking | Should -BeNullOrEmpty # 但不算致命
}
It '纯字符串数组写法继续可用' {
$plain = Write-ListFile -Path (Join-Path $script:FormatRoot 'cat-plain.psd1') -Content "@{ 'pair2' = @('$script:DirA', '$script:DirB') }"
$catalog = Get-SoftwareCatalog -Path $plain -MaxDepth 3
$catalog['pair2'].Kind | Should -Be 'Multi'
@($catalog['pair2'].Dirs).Count | Should -Be 2
}
It '旧的 @{ Dirs = @(...) } 写法继续可用' {
$legacy = Write-ListFile -Path (Join-Path $script:FormatRoot 'cat-legacy.psd1') -Content "@{ 'pair3' = @{ Dirs = @('$script:DirA', '$script:DirB') } }"
$catalog = Get-SoftwareCatalog -Path $legacy -MaxDepth 3
$catalog['pair3'].Kind | Should -Be 'Multi'
@($catalog['pair3'].Dirs).Count | Should -Be 2
}
It '数组形式的名录条目在清单里仍然按软件名命名归档' {
$entry = ConvertFrom-BackupListLine -Line 'pair'
(Get-ItemArchiveName -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3) | Should -Be 'pair'
}
}
# ============================================================================
Describe '两种写法都要支持 :+ 追加与 :- 排除' {
# ============================================================================
BeforeAll {
$script:FormatRoot = Join-Path $script:Sandbox 'format'
$script:DirA = Join-Path $script:FormatRoot 'dirA'
$script:DirB = Join-Path $script:FormatRoot 'dirB'
$script:FormatCatalog = Join-Path $script:FormatRoot 'cat.psd1'
$script:CollideRoot = Join-Path $script:FormatRoot 'collide'
}
It '软件名写法::+ 追加一个目录' {
$entry = ConvertFrom-BackupListLine -Line "pair :+ $script:CollideRoot"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 3
@($resolved.Sources | ForEach-Object { $_.Origin }) | Should -Contain 'append-path'
}
It '软件名写法::+ 追加"另一个软件名"会按名录展开成它的全部目录' {
$entry = ConvertFrom-BackupListLine -Line 'pair :+ pair'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 4
@($resolved.Sources | ForEach-Object { $_.Origin }) | Should -Contain 'append-catalog'
}
# ---- 回归:手写路径的 :+ 以前会被整段丢掉 ----
It '[回归] 手写路径写法::+ 追加一个目录' {
$entry = ConvertFrom-BackupListLine -Line "$script:DirA :+ $script:DirB"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 2
@($resolved.Sources | ForEach-Object { $_.SourcePath }) | Should -Contain $script:DirB
@($resolved.Sources | ForEach-Object { $_.Origin }) | Should -Contain 'append-path'
}
It '手写路径写法::- 排除与 :+ 追加并存' {
$entry = ConvertFrom-BackupListLine -Line "$script:DirA :+ $script:DirB :- skip.log,!*Cache"
$entry.ExcludePatterns.Count | Should -Be 2
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 2
$resolved.Blocking | Should -BeNullOrEmpty
}
It '软件名与手写路径混在一行也认得(主目录是软件名,追加是路径)' {
$entry = ConvertFrom-BackupListLine -Line "pair :+ $script:CollideRoot :- logs\"
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
$resolved.IsName | Should -BeTrue
@($resolved.Sources).Count | Should -Be 3
$entry.ExcludePatterns | Should -Be @('logs\')
}
It '同一条目里出现两个同名目录 -> Blocking(明确报错,不静默混成一棵树)' {
$entry = ConvertFrom-BackupListLine -Line 'collide'
$resolved = Resolve-BackupEntry -Entry $entry -CatalogPath $script:FormatCatalog -MaxDepth 3
@($resolved.Sources).Count | Should -Be 2
$resolved.Blocking | Should -Match '顶层同名'
}
}
# ============================================================================
Describe '清单行尾的 `# 说明`' {
# ============================================================================
It '会作为这条目的说明解析出来' {
$entry = ConvertFrom-BackupListLine -Line 'Edge :- !*Cache # 缓存可再生'
$entry.Path | Should -Be 'Edge'
$entry.ExcludePatterns | Should -Be @('!*Cache')
$entry.Comment | Should -Be '缓存可再生'
}
It '路径里紧贴的 # 不会被当成注释' {
$entry = ConvertFrom-BackupListLine -Line 'C:\a#b\c'
$entry.Path | Should -Be 'C:\a#b\c'
$entry.Comment | Should -BeNullOrEmpty
}
It '没有说明时 Comment 为空' {
ConvertFrom-BackupListLine -Line 'legendary' | Select-Object -ExpandProperty Comment | Should -BeNullOrEmpty
}
}
# ============================================================================
Describe '集成:手写路径 + :+ 追加 的打包与恢复' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
# ============================================================================
BeforeAll {
$script:AppendRoot = Join-Path $script:Sandbox 'append-e2e'
# 刻意放在**两个不同的父目录**下:只有这样才能验证
# "恢复时不会把兄弟目录也复制过去"
$script:AppendA = Join-Path $script:AppendRoot 'srcA\dirA'
$script:AppendB = Join-Path $script:AppendRoot 'srcB\dirB'
foreach ($directory in $script:AppendA, $script:AppendB) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
}
Set-Content -LiteralPath (Join-Path $script:AppendA 'a.txt') 'A'
Set-Content -LiteralPath (Join-Path $script:AppendB 'b.txt') 'B'
Set-Content -LiteralPath (Join-Path $script:AppendA 'skip.log') 'S'
$script:AppendList = Write-ListFile -Path (Join-Path $script:AppendRoot 'list.txt') `
-Content "$script:AppendA :+ $script:AppendB :- skip.log`n"
$script:AppendBackupDir = Join-Path $script:AppendRoot 'Backups'
$script:AppendBackupRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:AppendList
BackupDir = $script:AppendBackupDir
Force = $true
QuietTool = $true
}
}
It '备份前会打印空间预估与"够不够"的结论' {
$script:AppendBackupRun.Output | Should -Match '备份前空间预估'
$script:AppendBackupRun.Output | Should -Match '要重打'
$script:AppendBackupRun.Output | Should -Match '结论:'
}
It '备份成功,manifest.roots 记录两棵子树' {
$script:AppendBackupRun.ExitCode | Should -Be 0
$archive = @(Get-ChildItem -LiteralPath $script:AppendBackupDir -File -Filter *.7z)[0]
$record = (Read-BaknretManifest -Path (Join-Path $script:AppendBackupDir 'manifest.json')).items[$archive.BaseName]
$record.roots | Should -Contain 'dirA'
$record.roots | Should -Contain 'dirB'
}
It '归档里两棵树都在,且 :- 排除生效' {
$verify = Join-Path $script:AppendRoot 'verify'
New-Item -ItemType Directory -Path $verify -Force | Out-Null
$archive = @(Get-ChildItem -LiteralPath $script:AppendBackupDir -File -Filter *.7z)[0]
(Invoke-ExternalCommand -FilePath $script:SevenZip -ArgumentList @('x', '-bso0', '-bsp0', '-y', "-o$verify", $archive.FullName)) | Should -Be 0
Test-Path -LiteralPath (Join-Path $verify 'dirA\a.txt') | Should -BeTrue
Test-Path -LiteralPath (Join-Path $verify 'dirB\b.txt') | Should -BeTrue
Test-Path -LiteralPath (Join-Path $verify 'dirA\skip.log') | Should -BeFalse
}
It '删源后恢复:每个目录只落回自己的父目录,兄弟目录不会被复制过去' {
Remove-Item -LiteralPath $script:AppendA -Recurse -Force
Remove-Item -LiteralPath $script:AppendB -Recurse -Force
$run = Invoke-BaknretScript -Script $script:RestoreScript -Parameters @{
BackupListPath = $script:AppendList
BackupDir = $script:AppendBackupDir
Force = $true
}
$run.ExitCode | Should -Be 0
Test-Path -LiteralPath (Join-Path $script:AppendA 'a.txt') | Should -BeTrue
Test-Path -LiteralPath (Join-Path $script:AppendB 'b.txt') | Should -BeTrue
# 关键:srcA 下不该冒出 dirB,srcB 下也不该冒出 dirA
Test-Path -LiteralPath (Join-Path $script:AppendRoot 'srcA\dirB') | Should -BeFalse
Test-Path -LiteralPath (Join-Path $script:AppendRoot 'srcB\dirA') | Should -BeFalse
}
}
# ============================================================================
Describe '集成:同一条目里两个同名目录会被拒绝执行' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
# ============================================================================
BeforeAll {
$script:RejectRoot = Join-Path $script:Sandbox 'collide-e2e'
$script:RejectCatalog = Join-Path $script:RejectRoot 'cat.psd1'
$p1 = Join-Path $script:RejectRoot 'p1\dupdir'
$p2 = Join-Path $script:RejectRoot 'p2\dupdir'
foreach ($directory in $p1, $p2) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
Set-Content -LiteralPath (Join-Path $directory 'x.txt') 'x'
}
Write-ListFile -Path $script:RejectCatalog -Content "@{`n 'collide' = @('$p1', '$p2')`n}`n" | Out-Null
$script:RejectList = Write-ListFile -Path (Join-Path $script:RejectRoot 'list.txt') -Content "collide`n"
$script:RejectConfig = Write-ListFile -Path (Join-Path $script:RejectRoot 'config.psd1') -Content "@{ SoftwareCatalog = '$script:RejectCatalog' }`n"
$script:RejectBackupDir = Join-Path $script:RejectRoot 'Backups'
$script:RejectRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:RejectList
BackupDir = $script:RejectBackupDir
ConfigPath = $script:RejectConfig
Force = $true
QuietTool = $true
}
}
It '退出码 1,且给出"顶层同名"的原因,不生成归档' {
$script:RejectRun.ExitCode | Should -Be 1
$script:RejectRun.Output | Should -Match '顶层同名'
@(Get-ChildItem -LiteralPath $script:RejectBackupDir -File -Filter *.7z -ErrorAction SilentlyContinue).Count | Should -Be 0
}
It 'manifest 里记下这次是 failed,并带上原因' {
$manifest = Read-BaknretManifest -Path (Join-Path $script:RejectBackupDir 'manifest.json')
$record = $manifest.items['collide']
$record.action | Should -Be 'failed'
$record.reason | Should -Match '顶层同名'
}
}
# ============================================================================
Describe '条目从清单里消失后,旧归档必须被点名为孤儿' -Skip:(-not ($script:HasSevenZip -and $script:HasPwsh)) {
# ============================================================================
# 这是"合并/改名条目"的真实后果:归档还在,manifest 里也还留着历史记录,
# 但清单里已经没有任何条目指向它 —— Restore.ps1 按条目名找归档,所以它恢复不到。
# 审计如果拿 manifest 当"已知",这种归档会被历史记录永远遮住,正是最该报的情况。
BeforeAll {
$script:OrphanRoot = Join-Path $script:Sandbox 'orphan-e2e'
$script:OrphanSource = Join-Path $script:OrphanRoot 'src\My App'
New-Item -ItemType Directory -Path $script:OrphanSource -Force | Out-Null
Set-Content -LiteralPath (Join-Path $script:OrphanSource 'data.txt') 'hello'
$script:OrphanCatalog = Write-ListFile -Path (Join-Path $script:OrphanRoot 'cat.psd1') `
-Content "@{`n 'my-app' = '$script:OrphanSource'`n}`n"
$script:OrphanConfig = Write-ListFile -Path (Join-Path $script:OrphanRoot 'config.psd1') `
-Content "@{ SoftwareCatalog = '$script:OrphanCatalog' }`n"
$script:OrphanList = Join-Path $script:OrphanRoot 'list.txt'
$script:OrphanBackupDir = Join-Path $script:OrphanRoot 'Backups'
# 第一次:清单里有 my-app
Write-ListFile -Path $script:OrphanList -Content "my-app`n" | Out-Null
$null = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:OrphanList
BackupDir = $script:OrphanBackupDir
ConfigPath = $script:OrphanConfig
Force = $true
QuietTool = $true
}
# 第二次:把条目从清单里拿掉,归档留在磁盘上
Write-ListFile -Path $script:OrphanList -Content "# 清单里没有条目了`n" | Out-Null
$script:OrphanRun = Invoke-BaknretScript -Script $script:BackupScript -Parameters @{
BackupListPath = $script:OrphanList
BackupDir = $script:OrphanBackupDir
ConfigPath = $script:OrphanConfig
QuietTool = $true
}
}
It '归档确实还在磁盘上,manifest 里也还留着历史记录' {
Test-Path -LiteralPath (Join-Path $script:OrphanBackupDir 'my-app.7z') | Should -BeTrue
$manifest = Read-BaknretManifest -Path (Join-Path $script:OrphanBackupDir 'manifest.json')
$manifest.items.Contains('my-app') | Should -BeTrue
}
It '第二次运行把 my-app.7z 点名成孤儿,并说明 manifest 里还有历史记录' {
$script:OrphanRun.ExitCode | Should -Be 0
$script:OrphanRun.Output | Should -Match '孤儿归档'
$script:OrphanRun.Output | Should -Match 'my-app\.7z'
$script:OrphanRun.Output | Should -Match 'manifest 里还留着它的历史记录'
}
}
# ============================================================================
Describe 'manifest 一致性:archive 字段只在文件真的存在时才写' {
# ============================================================================
BeforeAll {
$script:SyncRoot = Join-Path $script:Sandbox 'sync'
New-Item -ItemType Directory -Path $script:SyncRoot -Force | Out-Null
Set-Content -LiteralPath (Join-Path $script:SyncRoot 'real.7z') 'x'
$script:SyncManifestPath = Join-Path $script:SyncRoot 'manifest.json'
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$manifest.items['real'] = [pscustomobject]@{ archive = 'real.7z'; action = 'backed-up'; source = 'real' }
$manifest.items['gone'] = [pscustomobject]@{ archive = 'gone.7z'; action = 'missing-source'; source = 'gone' }
$manifest.items['none'] = [pscustomobject]@{ archive = $null; action = 'missing-source'; source = 'none' }
Write-BaknretManifest -Path $script:SyncManifestPath -Manifest $manifest | Out-Null
}
It '存在的归档保留 archive,不存在的被清空' {
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$cleared = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$cleared.Count | Should -Be 1
$cleared[0] | Should -Be 'gone'
$manifest.items['real'].archive | Should -Be 'real.7z'
$manifest.items['gone'].archive | Should -BeNullOrEmpty
$manifest.items['none'].archive | Should -BeNullOrEmpty
}
It '清空 archive 时保留条目本身的历史(source / action 不动)' {
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$null = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$manifest.items['gone'].source | Should -Be 'gone'
$manifest.items['gone'].action | Should -Be 'missing-source'
}
It '人工删掉归档之后再同步一次,记录会被纠正过来' {
Remove-Item -LiteralPath (Join-Path $script:SyncRoot 'real.7z') -Force
$manifest = Read-BaknretManifest -Path $script:SyncManifestPath
$cleared = Sync-BaknretManifestArchive -Manifest $manifest -BackupDir $script:SyncRoot
$cleared | Should -Contain 'real'
$manifest.items['real'].archive | Should -BeNullOrEmpty
}
}