Files
skills-win/tools-sync/tests/test_sync_repos_registries.ps1
T
Myfro9andClaude Opus 5 8566e23f49 tools-sync: 修复会删掉宿主自装 skill 的 registry 对账缺陷
本次同步实跑触发了数据丢失:orca-cli 的物理副本在 ~\.agents\skills(Orca 就是
这样装的——真目录放 .agents,再把每个工具链过去,本机 8 个工具全指向它),
而主副本里的同名条目本身是指向它的 junction。对账循环从不检查"主副本条目是不是
链接",于是 fingerprint 比对经链接解析到同一份内容判为"identical",删掉 registry
副本并建 junction → 主副本与 registry 互指成环,唯一的真副本没了。
orchestration 走 INGEST 分支,则把 Orca 的布局反了过来。

两者均已用 `orca skills install --skill <name>` 恢复为 Orca 自己的布局。

修复三处:
- 对账循环:主副本条目本身是链接时,判定该 skill 归宿主所有,计入 foreign 跳过。
- INGEST:新增 Get-SkillDirLinkers,若 ~\.* 下其他工具的 skills 目录有链接指向
  这个目录,说明它是宿主自装 skill 的物理家,不搬进主副本。
- 镜像阶段:跳过本身是链接的主副本条目,否则 Codex 会把同一 skill 注册两遍
  (.agents 与 .codex 都在扫)。

回归测试新增 hostowned 夹具(真目录在 registry、主副本是链接):在修复前的脚本上
以"real dir was collapsed into a link"失败,修复后通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01438Zbcx49e4o46uSA71mFF
2026-09-07 16:29:20 +08:00

86 lines
5.8 KiB
PowerShell

# Covers Phase 2 of sync_repos.ps1 -- the registry reconciliation that decides whether a
# directory found on the Codex side gets ingested, collapsed, or reported. Runs against a
# throwaway primary + registries + bare origin under TEMP; the real machine is never touched.
$ErrorActionPreference = "Stop"
$scriptUnderTest = Join-Path (Split-Path $PSScriptRoot -Parent) "scripts\sync_repos.ps1"
$root = Join-Path ([IO.Path]::GetTempPath()) ("tools-sync-repos-test-" + [guid]::NewGuid().ToString("N"))
function New-Skill([string]$Path, [string]$Body) {
New-Item -ItemType Directory -Force -Path $Path | Out-Null
Set-Content -LiteralPath (Join-Path $Path "SKILL.md") -Value $Body -Encoding utf8
}
try {
$primary = Join-Path $root "primary"
$codex = Join-Path $root "codex"
$agents = Join-Path $root "agents"
New-Item -ItemType Directory -Force -Path $primary, $codex, $agents | Out-Null
# One skill of each shape the reconciler must tell apart.
New-Skill (Join-Path $primary "shared") "shared body"
New-Skill (Join-Path $primary "forked") "primary body"
New-Skill (Join-Path $agents "shared") "shared body" # identical duplicate -> collapse
New-Skill (Join-Path $agents "forked") "registry body" # divergent -> conflict
New-Skill (Join-Path $agents "codexborn") "born on the codex side" # registry-only -> ingest
New-Item -ItemType Directory -Force -Path (Join-Path $agents "notaskill") | Out-Null # no SKILL.md
# A link into the other registry rather than the primary: the drift case.
New-Item -ItemType Junction -Path (Join-Path $codex "shared") -Target (Join-Path $agents "shared") | Out-Null
# Host-installed shape: the real directory lives in a registry and the PRIMARY is the link
# (Orca installs this way). Collapsing the registry copy here would make the link point at
# itself and delete the only copy - the reconciler must recognise it and keep its hands off.
New-Skill (Join-Path $agents "hostowned") "installed by the host"
New-Item -ItemType Junction -Path (Join-Path $primary "hostowned") -Target (Join-Path $agents "hostowned") | Out-Null
# A local bare repo stands in for origin so Phase 1 completes and Phase 2 actually runs.
$origin = Join-Path $root "origin.git"
& git init --quiet --bare $origin
& git init --quiet --initial-branch=main $primary
& git -C $primary remote add origin $origin
& git -C $primary -c user.email=t@t -c user.name=t commit --quiet --allow-empty -m init
& git -C $primary push --quiet -u origin main
# Invoked via -Command, not -File: -File stringifies every argument, so a [string[]]
# parameter silently takes only its first element and the rest slide into later positions.
$ErrorActionPreference = "Continue"
$invocation = "& '$scriptUnderTest' -RepoUrl '$origin' -ClaudeSkills '$primary' " +
"-Registry @('$codex','$agents') -MirrorRegistry '$codex'"
$output = & powershell -NoProfile -ExecutionPolicy Bypass -Command $invocation 2>&1 | Out-String
$ErrorActionPreference = "Stop"
if ($output -notmatch 'INGEST registry-only skill:\s*codexborn') { throw "codex-side skill was not ingested`n$output" }
if (-not (Test-Path (Join-Path $primary "codexborn\SKILL.md"))) { throw "ingested skill is not in the primary`n$output" }
if ((Get-Item (Join-Path $agents "codexborn")).LinkType -ne "Junction") { throw "ingested skill was not linked back`n$output" }
if ($output -notmatch 'MISDIRECTED link:\s*shared') { throw "link into another registry not detected`n$output" }
$sharedTarget = (Get-Item (Join-Path $codex "shared")).Target | Select-Object -First 1
if ($sharedTarget -ne (Join-Path $primary "shared")) { throw "misdirected link not repointed at the primary, got $sharedTarget`n$output" }
if ((Get-Item (Join-Path $agents "shared")).LinkType -ne "Junction") { throw "identical duplicate did not collapse`n$output" }
if ($output -notmatch 'CONFLICT:\s*forked') { throw "divergent duplicate not reported`n$output" }
if ((Get-Item (Join-Path $agents "forked")).LinkType -eq "Junction") { throw "divergent duplicate must be left untouched`n$output" }
if ((Get-Content -Raw (Join-Path $primary "forked\SKILL.md")) -notmatch 'primary body') { throw "primary was overwritten by a conflict`n$output" }
if ($output -notmatch 'not a skill dir') { throw "directory without SKILL.md not reported`n$output" }
# The host-installed skill must survive intact, with the primary still a link at the registry.
$hostReal = Get-Item -Force (Join-Path $agents "hostowned")
if ($hostReal.LinkType -eq "Junction") { throw "host-installed skill's real dir was collapsed into a link`n$output" }
if ((Get-Content -Raw (Join-Path $agents "hostowned\SKILL.md")) -notmatch 'installed by the host') { throw "host-installed skill's content was lost`n$output" }
$hostPrimary = Get-Item -Force (Join-Path $primary "hostowned")
if (($hostPrimary.Target | Select-Object -First 1) -ne (Join-Path $agents "hostowned")) { throw "primary link to the host-installed skill was repointed`n$output" }
# Mirroring it into the registry Codex scans would register the same skill twice.
if (Test-Path (Join-Path $codex "hostowned")) { throw "host-installed skill was mirrored, double-registering it`n$output" }
# Mirror registry must expose every primary skill, including the freshly ingested one.
foreach ($name in @("shared", "forked", "codexborn")) {
if (-not (Test-Path (Join-Path $codex "$name\SKILL.md"))) { throw "mirror missing $name`n$output" }
}
# Phase 1's success line is the ls-remote gate's verdict (upstream wording).
if ($output -notmatch 'verified: origin/main =') { throw "Phase 1 did not complete`n$output" }
Write-Host "PASS: registry ingest / repair / conflict fixtures"
} finally {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}