Merge remote-tracking branch 'origin/main' into codex/todo-skill-optimization-run1

# Conflicts:
#	internal/shortcut/todo/lifecycle.go
This commit is contained in:
瑞达
2026-08-21 14:51:29 +08:00
350 changed files with 50130 additions and 6669 deletions
+279
View File
@@ -0,0 +1,279 @@
package scripts_test
import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"testing"
)
type packagedAgent struct {
universal bool
path string
}
var upstreamAgentIDs = strings.Fields(`
aider-desk amp antigravity antigravity-cli astrbot autohand-code augment bob
claude-code openclaw cline codearts-agent codebuddy codemaker codestudio codex
command-code continue cortex crush cursor deepagents devin dexto droid eve
firebender forgecode gemini-cli github-copilot goose grok hermes-agent inference-sh
jazz junie iflow-cli kilo kimchi kimi-code-cli kiro-cli kode lingma loaf mcpjam
minimax-code mistral-vibe moxby mux opencode openhands ona pi qoder qoder-cn
qwen-code replit reasonix rovodev roo tabnine-cli terramind tinycloud trae trae-cn
warp windsurf zed zcode zencoder zenflow neovate pochi promptscript adal universal
`)
func TestAgentRegistriesMatchUpstream76(t *testing.T) {
t.Parallel()
root := filepath.Join("..", "..")
installPS := readTestFile(t, filepath.Join(root, "scripts", "install.ps1"))
devappPS := readTestFile(t, filepath.Join(root, "scripts", "install-devapp.ps1"))
verifier := readTestFile(t, filepath.Join(root, "scripts", "release", "verify-package-managers.sh"))
wantIDs := append([]string(nil), upstreamAgentIDs...)
sort.Strings(wantIDs)
registries := map[string]map[string]packagedAgent{
"install.ps1": parseInstallPowerShellRegistry(t, installPS),
"install-devapp.ps1": parsePipeRegistry(t, between(t, devappPS, "$AgentRegistryRows = @(", ")\n$LegacyAgentCleanupRows")),
"verifier": parsePipeRegistry(t, between(t, verifier, "UPSTREAM_AGENT_REGISTRY='", "'\n\n# DWS-only")),
}
for name, registry := range registries {
gotIDs := make([]string, 0, len(registry))
universal := 0
for id, agent := range registry {
gotIDs = append(gotIDs, id)
if agent.universal {
universal++
}
}
sort.Strings(gotIDs)
if strings.Join(gotIDs, "\n") != strings.Join(wantIDs, "\n") {
t.Fatalf("%s agent IDs differ from upstream 76\ngot: %v\nwant: %v", name, gotIDs, wantIDs)
}
if universal != 19 || len(registry)-universal != 57 {
t.Fatalf("%s classification = %d universal/%d non-universal, want 19/57", name, universal, len(registry)-universal)
}
}
canonical := registries["install.ps1"]
for name, got := range registries {
for id, want := range canonical {
if got[id] != want {
t.Errorf("%s[%s] = %+v, want %+v", name, id, got[id], want)
}
}
}
for id, wantPath := range map[string]string{
"amp": ".config/agents/skills",
"github-copilot": ".copilot/skills",
"opencode": ".config/opencode/skills",
"windsurf": ".codeium/windsurf/skills",
"cline": ".agents/skills",
"eve": "-",
"promptscript": "-",
} {
if got := canonical[id].path; got != wantPath {
t.Errorf("upstream path for %s = %q, want %q", id, got, wantPath)
}
}
}
func TestDWSCompatibilityAgentTargets(t *testing.T) {
t.Parallel()
root := filepath.Join("..", "..")
installPS := readTestFile(t, filepath.Join(root, "scripts", "install.ps1"))
legacyBlock := between(t, installPS, "$LegacyAgentCleanupTargets = @(", ")\n\n# Kept as")
if !strings.Contains(legacyBlock, `Id = "dws-qoderwork"; Universal = $false`) {
t.Error("DWS Qoderwork compatibility target must remain non-universal")
}
for _, id := range []string{
"dws-legacy-github", "dws-legacy-amp", "dws-legacy-cline", "dws-legacy-windsurf",
} {
if !strings.Contains(legacyBlock, `Id = "`+id+`"; Universal = $true`) {
t.Errorf("legacy cleanup registry missing cleanup-only target %s", id)
}
}
for _, id := range upstreamAgentIDs {
if strings.HasPrefix(id, "dws-") {
t.Fatalf("DWS compatibility target %s leaked into upstream registry", id)
}
}
}
func TestInstallPowerShellUsesAbsoluteJunctionTargetsAndCopyFallback(t *testing.T) {
t.Parallel()
installPS := readTestFile(t, filepath.Join("..", "..", "scripts", "install.ps1"))
for _, want := range []string{
`$absoluteTarget = [System.IO.Path]::GetFullPath((Join-Path $canonical $name))`,
`New-Item -ItemType Junction -Path (Join-Path $stageRoot $name) -Target $absoluteTarget`,
`Install-MultiToBase -MultiSrc $MultiSrc -BaseDir $baseDir`,
`Install-MonoToBase -SkillSrc $SkillSrc -BaseDir $baseDir`,
} {
if !strings.Contains(installPS, want) {
t.Errorf("install.ps1 missing Windows junction/fallback contract %q", want)
}
}
}
func TestAgentInstallersCarryUpstreamShallowAndApplicationDetection(t *testing.T) {
t.Parallel()
checks := map[string][]string{
"build/npm/install.js": {"agentTargetDetected", `case "kimchi"`, `case "tabnine-cli"`, "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
"scripts/install.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
"scripts/install-skills.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
"scripts/install-event.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
"scripts/install-devapp.sh": {"agent_skill_base_detected", ".config/kimchi/harness/skills", ".tabnine/agent/skills", "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
"scripts/install.ps1": {"Test-AgentSkillBaseDetected", `"kimchi"`, `"tabnine-cli"`, "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
"scripts/install-devapp.ps1": {"Test-AgentBaseDetected", `"kimchi"`, `"tabnine-cli"`, "/Applications/ZCode.app", "/Applications/MiniMax Code.app"},
}
for rel, wants := range checks {
body := readTestFile(t, filepath.Join("..", "..", filepath.FromSlash(rel)))
for _, want := range wants {
if !strings.Contains(body, want) {
t.Errorf("%s missing detection contract %q", rel, want)
}
}
}
}
func TestInstallPowerShellResolvesCustomAndLegacyAgentHomes(t *testing.T) {
pwsh, err := exec.LookPath("pwsh")
if err != nil {
if runtime.GOOS == "windows" {
pwsh, err = exec.LookPath("powershell")
}
if err != nil {
t.Skip("PowerShell is not available")
}
}
root := filepath.Join("..", "..")
installPS := readTestFile(t, filepath.Join(root, "scripts", "install.ps1"))
cut := strings.LastIndex(installPS, "# ── Main")
if cut < 0 {
t.Fatal("install.ps1 main section not found")
}
home := t.TempDir()
custom := filepath.Join(t.TempDir(), "custom roots")
xdg := filepath.Join(custom, "xdg")
clawdbot := filepath.Join(home, ".clawdbot")
if err := os.MkdirAll(clawdbot, 0o755); err != nil {
t.Fatal(err)
}
prefix := strings.ReplaceAll(installPS[:cut], "$HOME", "$env:DWS_TEST_HOME")
prefix += `
$ids = @("amp", "autohand-code", "claude-code", "codex", "grok", "hermes-agent", "mistral-vibe", "openclaw", "goose", "kimchi", "opencode")
foreach ($id in $ids) {
$agent = $AgentRegistry | Where-Object { $_.Id -eq $id } | Select-Object -First 1
Write-Output ($id + "|" + (Resolve-AgentSkillBase -Root $env:DWS_TEST_HOME -Agent $agent))
}
exit 0
`
harness := filepath.Join(t.TempDir(), "resolve-agent-roots.ps1")
mustWriteFile(t, harness, []byte(prefix), 0o644)
envRoots := map[string]string{
"AUTOHAND_HOME": filepath.Join(custom, "autohand"),
"CLAUDE_CONFIG_DIR": filepath.Join(custom, "claude"),
"CODEX_HOME": filepath.Join(custom, "codex"),
"GROK_HOME": filepath.Join(custom, "grok"),
"HERMES_HOME": filepath.Join(custom, "hermes"),
"VIBE_HOME": filepath.Join(custom, "vibe"),
"XDG_CONFIG_HOME": xdg,
}
cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harness)
cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home)
for key, value := range envRoots {
cmd.Env = append(cmd.Env, key+"="+value)
}
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("PowerShell root resolver failed: %v\n%s", err, output)
}
want := map[string]string{
"amp": filepath.Join(xdg, "agents", "skills"),
"autohand-code": filepath.Join(envRoots["AUTOHAND_HOME"], "skills"),
"claude-code": filepath.Join(envRoots["CLAUDE_CONFIG_DIR"], "skills"),
"codex": filepath.Join(envRoots["CODEX_HOME"], "skills"),
"grok": filepath.Join(envRoots["GROK_HOME"], "skills"),
"hermes-agent": filepath.Join(envRoots["HERMES_HOME"], "skills"),
"mistral-vibe": filepath.Join(envRoots["VIBE_HOME"], "skills"),
"openclaw": filepath.Join(clawdbot, "skills"),
"goose": filepath.Join(xdg, "goose", "skills"),
"kimchi": filepath.Join(xdg, "kimchi", "harness", "skills"),
"opencode": filepath.Join(xdg, "opencode", "skills"),
}
got := make(map[string]string)
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
parts := strings.SplitN(strings.TrimSpace(line), "|", 2)
if len(parts) == 2 {
got[parts[0]] = filepath.Clean(parts[1])
}
}
for id, path := range want {
if got[id] != filepath.Clean(path) {
t.Errorf("resolved %s = %q, want %q\noutput:\n%s", id, got[id], path, output)
}
}
}
func readTestFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile(%s): %v", path, err)
}
return string(b)
}
func between(t *testing.T, text, start, end string) string {
t.Helper()
i := strings.Index(text, start)
if i < 0 {
t.Fatalf("start marker %q missing", start)
}
i += len(start)
j := strings.Index(text[i:], end)
if j < 0 {
t.Fatalf("end marker %q missing", end)
}
return text[i : i+j]
}
func parseInstallPowerShellRegistry(t *testing.T, text string) map[string]packagedAgent {
t.Helper()
block := between(t, text, "$AgentRegistry = @(", ")\n\n# DWS compatibility targets")
re := regexp.MustCompile(`Id = "([^"]+)"; Universal = \$(true|false); Dir = (?:"([^"]+)"|\$null)`)
result := make(map[string]packagedAgent)
for _, match := range re.FindAllStringSubmatch(block, -1) {
path := strings.ReplaceAll(match[3], `\`, "/")
if path == "" {
path = "-"
}
result[match[1]] = packagedAgent{universal: match[2] == "true", path: path}
}
return result
}
func parsePipeRegistry(t *testing.T, block string) map[string]packagedAgent {
t.Helper()
re := regexp.MustCompile(`([a-z0-9-]+)\|([01])\|([.a-z0-9_\\/-]+)`)
result := make(map[string]packagedAgent)
for _, match := range re.FindAllStringSubmatch(block, -1) {
path := strings.ReplaceAll(match[3], `\`, "/")
if _, exists := result[match[1]]; exists {
t.Fatalf("duplicate agent %q in pipe registry", match[1])
}
result[match[1]] = packagedAgent{universal: match[2] == "1", path: path}
}
return result
}
+46
View File
@@ -305,6 +305,52 @@ func TestChangelogPRContentOnlyAcceptsReleaseFragmentArchival(t *testing.T) {
}
}
func TestReleaseFragmentPolicyAcceptsStablePostBetaArchival(t *testing.T) {
repo := newChangelogGateRepo(t)
changelogGateWrite(t, repo.root, ".changes/1236-stable-followup.md", "---\ncategory: Fixed\n---\n\n- Stable follow-up.\n", 0o644)
repo.commit(t, "add post-beta release fragment")
sealBase := strings.TrimSpace(changelogGateGit(t, repo.root, "rev-parse", "HEAD"))
changelogGateWrite(t, repo.root, "CHANGELOG.md", `# Changelog
## [Unreleased]
## [1.0.1] - 2026-07-17
This release promotes the sealed `+"`v1.0.1-beta.1`"+` contents to stable.
### Changed
- Promote the complete beta release to stable.
### Changes since `+"`v1.0.1-beta.1`"+`
### Fixed
- Stable follow-up.
## [1.0.0] - 2026-07-01
### Added
- Initial release.
`, 0o644)
archiveDir := filepath.Join(repo.root, ".changes", "released", "1.0.1")
if err := os.MkdirAll(archiveDir, 0o755); err != nil {
t.Fatalf("MkdirAll stable archive: %v", err)
}
if err := os.Rename(
filepath.Join(repo.root, ".changes", "1236-stable-followup.md"),
filepath.Join(archiveDir, "1236-stable-followup.md"),
); err != nil {
t.Fatalf("Rename stable release fragment: %v", err)
}
repo.commit(t, "seal stable release notes")
if output, err := repo.runFragmentPolicy(t, sealBase, "HEAD"); err != nil {
t.Fatalf("release fragment policy rejected stable seal: %v\noutput:\n%s", err, output)
}
}
func TestReleaseFragmentPolicyRejectsInvalidActiveFragmentAndWrongArchiveVersion(t *testing.T) {
t.Run("invalid active fragment", func(t *testing.T) {
repo := newChangelogGateRepo(t)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+128 -6
View File
@@ -30,6 +30,10 @@ var expectedPackagedSkillTargets = []string{
".augment/skills/dingtalk-shared",
".cline/skills/dingtalk-shared",
".amp/skills/dingtalk-shared",
".copilot/skills/dingtalk-shared",
".config/opencode/skills/dingtalk-shared",
".config/agents/skills/dingtalk-shared",
".codeium/windsurf/skills/dingtalk-shared",
".kiro/skills/dingtalk-shared",
".trae/skills/dingtalk-shared",
".openclaw/skills/dingtalk-shared",
@@ -70,9 +74,12 @@ func TestPackageManagerVersionVerificationReadsRawBinary(t *testing.T) {
t.Fatal("package-manager verifier still requires the version marker to occupy a strings(1) line")
}
for _, want := range []string{
"HOME_SPECIFIC_SKILL_BASES=",
`$base/dingtalk-shared/SKILL.md`,
`$base/dingtalk-misc/SKILL.md`,
"UPSTREAM_AGENT_REGISTRY=",
"verify_compatible_skill_base",
`diff -qr "$canonical/$name" "$target"`,
"broken canonical Skill link",
"Skill link does not resolve to canonical source",
"Skill copy fallback differs from canonical source",
"unexpected mono Skill layout",
`verify_npm_install "$tarball_path" "specific-agent-roots"`,
`verify_npm_install "$tarball_path" "generic-fallback"`,
@@ -128,11 +135,18 @@ func TestPackageManagerVerifierCoversSpecificAndFallbackSkillRoots(t *testing.T)
}
verifyCmd := exec.Command("sh", verifierPath, "--npm-only")
verifyCmd.Env = append(os.Environ(), "DWS_PACKAGE_DIST_DIR="+distDir)
hostXDG := filepath.Join(t.TempDir(), "host-xdg")
verifyCmd.Env = replaceTestEnv(os.Environ(),
"DWS_PACKAGE_DIST_DIR", distDir,
"XDG_CONFIG_HOME", hostXDG,
)
output, err := verifyCmd.CombinedOutput()
if err != nil {
t.Fatalf("verify-package-managers.sh error = %v\noutput:\n%s", err, output)
}
if _, err := os.Stat(hostXDG); !os.IsNotExist(err) {
t.Fatalf("package verifier touched inherited XDG_CONFIG_HOME %s: %v", hostXDG, err)
}
for _, scenario := range []string{"specific-agent-roots", "generic-fallback"} {
if !strings.Contains(string(output), "verifying npm package install ("+scenario+")") {
t.Errorf("verifier output is missing %s scenario:\n%s", scenario, output)
@@ -140,6 +154,102 @@ func TestPackageManagerVerifierCoversSpecificAndFallbackSkillRoots(t *testing.T)
}
}
func TestPackageManagerVerifierAcceptsOnlyCorrectLinksOrCopies(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "release", "verify-package-managers.sh"))
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatal(err)
}
text := string(data)
cut := strings.Index(text, "\nverify_npm_install()")
if cut < 0 {
t.Fatal("verify-package-managers.sh helper boundary missing")
}
library := filepath.Join(t.TempDir(), "verifier-lib.sh")
mustWriteFile(t, library, data[:cut], 0o755)
for _, tc := range []struct {
name string
prepare func(t *testing.T, home, base string)
wantOK bool
}{
{
name: "correct-link",
prepare: func(t *testing.T, _, base string) {
t.Helper()
// The canonical layout publishes RELATIVE links; the verifier now
// rejects absolute targets, so the fixture must match production.
if err := os.Symlink(filepath.Join("..", "..", ".agents", "skills", "dingtalk-shared"), filepath.Join(base, "dingtalk-shared")); err != nil {
t.Fatal(err)
}
},
wantOK: true,
},
{
name: "complete-copy",
prepare: func(t *testing.T, _, base string) {
t.Helper()
mustWriteFile(t, filepath.Join(base, "dingtalk-shared", "SKILL.md"), []byte("canonical\n"), 0o644)
},
wantOK: true,
},
{
name: "wrong-link",
prepare: func(t *testing.T, home, base string) {
t.Helper()
wrong := filepath.Join(home, "wrong", "dingtalk-shared")
mustWriteFile(t, filepath.Join(wrong, "SKILL.md"), []byte("canonical\n"), 0o644)
if err := os.Symlink(wrong, filepath.Join(base, "dingtalk-shared")); err != nil {
t.Fatal(err)
}
},
wantOK: false,
},
{
name: "broken-link",
prepare: func(t *testing.T, home, base string) {
t.Helper()
if err := os.Symlink(filepath.Join(home, "missing"), filepath.Join(base, "dingtalk-shared")); err != nil {
t.Fatal(err)
}
},
wantOK: false,
},
{
name: "different-copy",
prepare: func(t *testing.T, _, base string) {
t.Helper()
mustWriteFile(t, filepath.Join(base, "dingtalk-shared", "SKILL.md"), []byte("different\n"), 0o644)
},
wantOK: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
home := t.TempDir()
base := filepath.Join(home, ".claude", "skills")
mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dingtalk-shared", "SKILL.md"), []byte("canonical\n"), 0o644)
if err := os.MkdirAll(base, 0o755); err != nil {
t.Fatal(err)
}
tc.prepare(t, home, base)
cmd := exec.Command("sh", "-c", `. "$DWS_TEST_LIBRARY"; verify_compatible_skill_base "$DWS_TEST_HOME" "$DWS_TEST_BASE"`)
cmd.Env = append(os.Environ(), "DWS_TEST_LIBRARY="+library, "DWS_TEST_HOME="+home, "DWS_TEST_BASE="+base)
output, err := cmd.CombinedOutput()
if tc.wantOK && err != nil {
t.Fatalf("verifier rejected valid target: %v\n%s", err, output)
}
if !tc.wantOK && err == nil {
t.Fatalf("verifier accepted invalid target\n%s", output)
}
})
}
}
func seedDistArchive(t *testing.T, path string) {
t.Helper()
file, err := os.Create(path)
@@ -1193,13 +1303,25 @@ func TestReleaseWorkflowParallelizesSealedValidationWithoutWeakeningPublication(
"- e2e",
"verify-github-tag-authority.sh",
"go test -v -count=1 -timeout=5m ./test/scripts/... -run '^TestRelease'",
"check-command-compatibility.sh",
`tmp/trusted-release-tooling/scripts/release/check-release-compatibility.sh`,
`--repo-root "$GITHUB_WORKSPACE"`,
`--base-ref HEAD`,
`--stable-ref "$PREVIOUS_STABLE"`,
`--candidate-ref HEAD`,
"test-multi-profile-e2e.sh",
} {
if !strings.Contains(validation, required) {
t.Errorf("parallel release validation is missing %q", required)
}
}
for _, forbidden := range []string{
"./scripts/policy/check-command-compatibility.sh",
"./scripts/policy/check-authoritative-schema-compatibility.sh",
} {
if strings.Contains(validation, forbidden) {
t.Errorf("parallel release validation bypasses the shared compatibility runner with %q", forbidden)
}
}
if strings.Contains(build, "test-multi-profile-e2e.sh") {
t.Error("multi-profile E2E must not serialize GoReleaser")
@@ -1269,7 +1391,7 @@ func TestReleaseWorkflowHidesOnlyVerifiedSealedTagFromCompatibilityBaseline(t *t
verifiedTag := strings.Index(validation, "verify-github-tag-authority.sh")
deleteLocalTag := strings.Index(validation, "git update-ref -d")
compatibility := strings.Index(validation, "check-command-compatibility.sh")
compatibility := strings.Index(validation, "tmp/trusted-release-tooling/scripts/release/check-release-compatibility.sh")
if verifiedTag == -1 || deleteLocalTag == -1 || compatibility == -1 ||
verifiedTag > deleteLocalTag || deleteLocalTag > compatibility {
t.Fatal("release tag authority verification, local candidate removal, and compatibility checking must stay ordered")
+312 -6
View File
@@ -75,6 +75,7 @@ type releaseTestRepo struct {
prepare string
render string
releaseCmd string
compat string
lib string
verify string
}
@@ -110,11 +111,206 @@ func newReleaseTestRepo(t *testing.T) *releaseTestRepo {
prepare: filepath.Join(sourceRoot, "scripts", "release", "prepare-changelog.sh"),
render: filepath.Join(sourceRoot, "scripts", "release", "render-release-fragments.sh"),
releaseCmd: filepath.Join(sourceRoot, "scripts", "release", "release.sh"),
compat: filepath.Join(sourceRoot, "scripts", "release", "check-release-compatibility.sh"),
lib: filepath.Join(sourceRoot, "scripts", "release", "release-lib.sh"),
verify: filepath.Join(sourceRoot, "scripts", "release", "verify-release-artifacts.sh"),
}
}
func seedReleaseCompatibilityRefs(t *testing.T, repo string) (stable, base, candidate string) {
t.Helper()
mustRun(t, repo, "git", "config", "user.name", "Release Compatibility Test")
mustRun(t, repo, "git", "config", "user.email", "release-compatibility-test@example.com")
mustWriteFile(t, filepath.Join(repo, "seed.txt"), []byte("stable\n"), 0o644)
mustRun(t, repo, "git", "add", "seed.txt")
mustRun(t, repo, "git", "commit", "-m", "stable")
mustRun(t, repo, "git", "tag", "stable-ref")
stable = strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD^{commit}"))
mustWriteFile(t, filepath.Join(repo, "seed.txt"), []byte("base\n"), 0o644)
mustRun(t, repo, "git", "add", "seed.txt")
mustRun(t, repo, "git", "commit", "-m", "base")
mustRun(t, repo, "git", "branch", "base-ref")
base = strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD^{commit}"))
mustWriteFile(t, filepath.Join(repo, "seed.txt"), []byte("candidate\n"), 0o644)
mustRun(t, repo, "git", "add", "seed.txt")
mustRun(t, repo, "git", "commit", "-m", "candidate")
mustRun(t, repo, "git", "branch", "candidate-ref")
candidate = strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD^{commit}"))
return stable, base, candidate
}
func TestReleaseCompatibilityCheckRunsCLIAndSchemaWithExactRefs(t *testing.T) {
t.Parallel()
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
runner := filepath.Join(sourceRoot, "scripts", "release", "check-release-compatibility.sh")
repo := t.TempDir()
mustRun(t, repo, "git", "init", "-b", "main")
stable, base, candidate := seedReleaseCompatibilityRefs(t, repo)
trace := filepath.Join(t.TempDir(), "compatibility.log")
checker := func(name string) []byte {
return []byte("#!/bin/sh\nset -eu\nprintf '" + name + ":%s\\n' \"$*\" >> \"$TRACE\"\n")
}
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-interface-baselines.sh"), checker("cli"), 0o755)
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-schema-compatibility.sh"), checker("schema"), 0o755)
cmd := exec.Command(
runner,
"--repo-root", repo,
"--base-ref", "base-ref",
"--stable-ref", "stable-ref",
"--candidate-ref", "candidate-ref",
)
cmd.Env = append(os.Environ(), "TRACE="+trace)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release compatibility check error = %v\noutput:\n%s", err, output)
}
got, err := os.ReadFile(trace)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", trace, err)
}
want := fmt.Sprintf("cli:--base-ref %s --stable-ref %s --candidate-ref %s\n", base, stable, candidate) +
fmt.Sprintf("schema:--base-ref %s --stable-ref %s --candidate-ref %s\n", base, stable, candidate)
if string(got) != want {
t.Fatalf("release compatibility calls = %q, want %q", got, want)
}
}
func TestReleaseCompatibilityCheckFreezesRefsBeforeRunningEitherChecker(t *testing.T) {
t.Parallel()
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
runner := filepath.Join(sourceRoot, "scripts", "release", "check-release-compatibility.sh")
repo := t.TempDir()
mustRun(t, repo, "git", "init", "-b", "main")
stable, base, candidate := seedReleaseCompatibilityRefs(t, repo)
trace := filepath.Join(t.TempDir(), "compatibility.log")
resolveAndLog := `
base="$2"
stable="$4"
candidate="$6"
printf '%s:%s:%s:%s\n' "$CHECK_NAME" \
"$(git -C "$REPO_ROOT_FOR_TEST" rev-parse --verify "${base}^{commit}")" \
"$(git -C "$REPO_ROOT_FOR_TEST" rev-parse --verify "${stable}^{commit}")" \
"$(git -C "$REPO_ROOT_FOR_TEST" rev-parse --verify "${candidate}^{commit}")" >> "$TRACE"
`
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-interface-baselines.sh"), []byte("#!/bin/sh\nset -eu\nCHECK_NAME=cli\n"+resolveAndLog+"git -C \"$REPO_ROOT_FOR_TEST\" update-ref refs/heads/base-ref \"$MUTATE_TO\"\n"), 0o755)
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-schema-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nCHECK_NAME=schema\n"+resolveAndLog), 0o755)
cmd := exec.Command(
runner,
"--repo-root", repo,
"--base-ref", "base-ref",
"--stable-ref", "stable-ref",
"--candidate-ref", "candidate-ref",
)
cmd.Env = append(os.Environ(), "TRACE="+trace, "REPO_ROOT_FOR_TEST="+repo, "MUTATE_TO="+candidate)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("release compatibility check error = %v\noutput:\n%s", err, output)
}
got, err := os.ReadFile(trace)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", trace, err)
}
want := fmt.Sprintf("cli:%s:%s:%s\nschema:%s:%s:%s\n", base, stable, candidate, base, stable, candidate)
if string(got) != want {
t.Fatalf("release compatibility resolved commits = %q, want %q", got, want)
}
if moved := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "base-ref^{commit}")); moved != candidate {
t.Fatalf("mutating checker did not move base-ref: got %s, want %s", moved, candidate)
}
}
func TestReleaseCompatibilityCheckStopsBeforeSchemaWhenCLIFails(t *testing.T) {
t.Parallel()
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
runner := filepath.Join(sourceRoot, "scripts", "release", "check-release-compatibility.sh")
repo := t.TempDir()
mustRun(t, repo, "git", "init", "-b", "main")
seedReleaseCompatibilityRefs(t, repo)
trace := filepath.Join(t.TempDir(), "compatibility.log")
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-interface-baselines.sh"), []byte("#!/bin/sh\nprintf 'cli\\n' >> \"$TRACE\"\nexit 17\n"), 0o755)
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-schema-compatibility.sh"), []byte("#!/bin/sh\nprintf 'schema\\n' >> \"$TRACE\"\n"), 0o755)
cmd := exec.Command(
runner,
"--repo-root", repo,
"--base-ref", "base-ref",
"--stable-ref", "stable-ref",
"--candidate-ref", "candidate-ref",
)
cmd.Env = append(os.Environ(), "TRACE="+trace)
output, err := cmd.CombinedOutput()
exitErr, ok := err.(*exec.ExitError)
if !ok || exitErr.ExitCode() != 17 {
t.Fatalf("release compatibility CLI failure = %v, want exit 17\noutput:\n%s", err, output)
}
if !strings.Contains(string(output), "error: authoritative CLI compatibility failed") {
t.Fatalf("release compatibility CLI failure has no boundary message:\n%s", output)
}
got, err := os.ReadFile(trace)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", trace, err)
}
if string(got) != "cli\n" {
t.Fatalf("checks after CLI failure = %q, want only CLI", got)
}
}
func TestReleaseCompatibilityCheckFailsWhenSchemaFails(t *testing.T) {
t.Parallel()
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
runner := filepath.Join(sourceRoot, "scripts", "release", "check-release-compatibility.sh")
repo := t.TempDir()
mustRun(t, repo, "git", "init", "-b", "main")
seedReleaseCompatibilityRefs(t, repo)
trace := filepath.Join(t.TempDir(), "compatibility.log")
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-interface-baselines.sh"), []byte("#!/bin/sh\nprintf 'cli\\n' >> \"$TRACE\"\n"), 0o755)
mustWriteFile(t, filepath.Join(repo, "scripts", "policy", "check-authoritative-schema-compatibility.sh"), []byte("#!/bin/sh\nprintf 'schema\\n' >> \"$TRACE\"\nexit 19\n"), 0o755)
cmd := exec.Command(
runner,
"--repo-root", repo,
"--base-ref", "base-ref",
"--stable-ref", "stable-ref",
"--candidate-ref", "candidate-ref",
)
cmd.Env = append(os.Environ(), "TRACE="+trace)
output, err := cmd.CombinedOutput()
exitErr, ok := err.(*exec.ExitError)
if !ok || exitErr.ExitCode() != 19 {
t.Fatalf("release compatibility Schema failure = %v, want exit 19\noutput:\n%s", err, output)
}
if !strings.Contains(string(output), "error: authoritative Schema compatibility failed") {
t.Fatalf("release compatibility Schema failure has no boundary message:\n%s", output)
}
got, err := os.ReadFile(trace)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", trace, err)
}
if string(got) != "cli\nschema\n" {
t.Fatalf("checks before Schema failure = %q, want CLI then Schema", got)
}
}
func TestReleaseVersionOrdering(t *testing.T) {
sourceRoot, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
@@ -1699,6 +1895,85 @@ func TestReleasePrepareChangelogRendersAndArchivesPrereleaseFragments(t *testing
}
}
func TestReleasePrepareChangelogRendersAndArchivesStableFragments(t *testing.T) {
r := newReleaseTestRepo(t)
releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644)
releaseCopyFile(t, r.prepare, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), 0o755)
releaseCopyFile(t, r.render, filepath.Join(r.root, "scripts", "release", "render-release-fragments.sh"), 0o755)
mustWriteFile(t, filepath.Join(r.root, ".changes", "1236-stable-followup.md"), []byte("---\ncategory: Fixed\n---\n\n- **Stable follow-up** (#1236) — includes a change merged after the beta.\n"), 0o644)
r.commitAndPush(t, "install stable changelog preparation")
cmd := exec.Command(
"sh",
filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"),
"stable",
"v1.0.1",
"--from-beta",
"v1.0.1-beta.1",
)
cmd.Dir = r.root
cmd.Env = append(os.Environ(), "DWS_RELEASE_DATE=2026-07-11")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("prepare stable changelog error = %v\noutput:\n%s", err, output)
}
changelog, err := os.ReadFile(filepath.Join(r.root, "CHANGELOG.md"))
if err != nil {
t.Fatalf("ReadFile(CHANGELOG.md) error = %v", err)
}
content := string(changelog)
if !strings.Contains(content, "## [1.0.1] - 2026-07-11") ||
!strings.Contains(content, "promotes the sealed `v1.0.1-beta.1` contents to stable") ||
!strings.Contains(content, "### Changes since `v1.0.1-beta.1`") ||
!strings.Contains(content, "### Fixed") ||
!strings.Contains(content, "Stable follow-up") {
t.Fatalf("prepared stable changelog did not render post-beta fragments:\n%s", changelog)
}
name := "1236-stable-followup.md"
if _, err := os.Stat(filepath.Join(r.root, ".changes", name)); !os.IsNotExist(err) {
t.Fatalf("unconsumed stable release fragment %s still present: %v", name, err)
}
if _, err := os.Stat(filepath.Join(r.root, ".changes", "released", "1.0.1", name)); err != nil {
t.Fatalf("archived stable release fragment %s missing: %v", name, err)
}
}
func TestReleasePrepareChangelogStableWithoutFragmentsKeepsTemplate(t *testing.T) {
r := newReleaseTestRepo(t)
releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644)
releaseCopyFile(t, r.prepare, filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"), 0o755)
releaseCopyFile(t, r.render, filepath.Join(r.root, "scripts", "release", "render-release-fragments.sh"), 0o755)
r.commitAndPush(t, "install stable changelog preparation")
cmd := exec.Command(
"sh",
filepath.Join(r.root, "scripts", "release", "prepare-changelog.sh"),
"stable",
"v1.0.1",
"--from-beta",
"v1.0.1-beta.1",
)
cmd.Dir = r.root
cmd.Env = append(os.Environ(), "DWS_RELEASE_DATE=2026-07-11")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("prepare stable changelog without fragments error = %v\noutput:\n%s", err, output)
}
changelog, err := os.ReadFile(filepath.Join(r.root, "CHANGELOG.md"))
if err != nil {
t.Fatalf("ReadFile(CHANGELOG.md) error = %v", err)
}
content := string(changelog)
if !strings.Contains(content, "## [1.0.1] - 2026-07-11") ||
!strings.Contains(content, "TODO: summarize the complete user-visible release") ||
strings.Contains(content, "### Changes since") {
t.Fatalf("stable changelog without fragments changed template behavior:\n%s", changelog)
}
if _, err := os.Stat(filepath.Join(r.root, ".changes", "released", "1.0.1")); !os.IsNotExist(err) {
t.Fatalf("stable preparation without fragments created an archive: %v", err)
}
}
func TestReleasePrepareChangelogRejectsInvalidReleaseFragment(t *testing.T) {
r := newReleaseTestRepo(t)
releaseCopyFile(t, r.lib, filepath.Join(r.root, "scripts", "release", "release-lib.sh"), 0o644)
@@ -2631,12 +2906,35 @@ func TestReleaseCommandRejectsDifferentFetchAndPushRepositories(t *testing.T) {
}
}
func TestReleaseCommandRunsCLIAndSchemaCompatibilityInInitialPreflight(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(betaSection())), 0o644)
r.commitAndPush(t, "install release compatibility fixture")
output, err := runReleaseScript(t, r.root, filepath.Join(r.root, "scripts", "release", "release.sh"),
"prerelease", "v1.0.1-beta.1", "--remote", "origin",
)
if err != nil {
t.Fatalf("release validation error = %v\noutput:\n%s", err, output)
}
head := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "HEAD^{commit}"))
stable := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "v1.0.0^{commit}"))
for _, want := range []string{
fmt.Sprintf("cli-compatibility --base-ref %s --stable-ref %s --candidate-ref %s", head, stable, head),
fmt.Sprintf("schema-compatibility --base-ref %s --stable-ref %s --candidate-ref %s", head, stable, head),
} {
if !strings.Contains(output, want) {
t.Errorf("initial release preflight is missing %q\noutput:\n%s", want, output)
}
}
}
func TestReleaseCommandRechecksAdvancedStableAuthority(t *testing.T) {
r := newReleaseTestRepo(t)
installReleaseCommandFixture(t, r)
section := "## [1.0.2-beta.1] - 2026-07-11\n\n### Changed\n\n- Validate a candidate after stable authority advances.\n\n"
mustWriteFile(t, filepath.Join(r.root, "CHANGELOG.md"), []byte(releaseChangelog(section)), 0o644)
mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-command-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nprintf 'compatibility %s\\n' \"$*\"\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte("test:\n\t@:\nbuild:\n\t@:\npolicy:\n\t@:\npackage:\n\t@git tag -a v1.0.1 -m 'Release v1.0.1'\n\t@git push origin refs/tags/v1.0.1\n"), 0o644)
r.commitAndPush(t, "install advancing release fixture")
@@ -2646,20 +2944,28 @@ func TestReleaseCommandRechecksAdvancedStableAuthority(t *testing.T) {
if err != nil {
t.Fatalf("release validation error = %v\noutput:\n%s", err, output)
}
if !strings.Contains(output, "Stable authority advanced from v1.0.0 to v1.0.1") ||
!strings.Contains(output, "--stable-ref v1.0.1") {
t.Fatalf("advanced stable command tree was not rechecked:\n%s", output)
head := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "HEAD^{commit}"))
stable := strings.TrimSpace(mustOutput(t, r.root, "git", "rev-parse", "v1.0.1^{commit}"))
for _, want := range []string{
"Stable authority advanced from v1.0.0 to v1.0.1",
fmt.Sprintf("cli-compatibility --base-ref %s --stable-ref %s --candidate-ref %s", head, stable, head),
fmt.Sprintf("schema-compatibility --base-ref %s --stable-ref %s --candidate-ref %s", head, stable, head),
} {
if !strings.Contains(output, want) {
t.Errorf("advanced stable authority recheck is missing %q\noutput:\n%s", want, output)
}
}
}
func installReleaseCommandFixture(t *testing.T, r *releaseTestRepo) {
t.Helper()
for _, source := range []string{r.lib, r.contract, r.releaseCmd} {
for _, source := range []string{r.lib, r.contract, r.releaseCmd, r.compat} {
releaseCopyFile(t, source, filepath.Join(r.root, "scripts", "release", filepath.Base(source)), 0o755)
}
mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "verify-package-managers.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "release", "verify-release-artifacts.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-command-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nexit 0\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-authoritative-interface-baselines.sh"), []byte("#!/bin/sh\nset -eu\nprintf 'cli-compatibility %s\\n' \"$*\"\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "scripts", "policy", "check-authoritative-schema-compatibility.sh"), []byte("#!/bin/sh\nset -eu\nprintf 'schema-compatibility %s\\n' \"$*\"\n"), 0o755)
mustWriteFile(t, filepath.Join(r.root, "Makefile"), []byte("test:\n\t@:\nbuild:\n\t@:\npolicy:\n\t@:\npackage:\n\t@:\n"), 0o644)
}
+320
View File
@@ -0,0 +1,320 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scripts_test
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// shellPruneScriptTargets are the standalone installers that embed their own
// copy of prune_skill_backups. Each must keep every backup stamp the running
// process created, mirroring Go's run-root registry and install.js's
// currentRunBackupRoots, so a migration retiring more than the keep limit
// stays reversible.
var shellPruneScriptTargets = []string{
"install.sh",
"install-skills.sh",
"install-event.sh",
"install-devapp.sh",
}
// The ownership-marker contract shared with the Go core
// (internal/upgrade/paths.go): every stamp root DWS creates carries the
// marker, and prune only counts or removes roots with these exact bytes.
const (
skillBackupMarkerFile = ".dws-skill-backup"
skillBackupMarkerBody = "dws skill backup v1\n"
)
// extractShellFunction pulls a top-level `name() { ... }` definition out of a
// script so the shipped logic — not a copy — is what the test exercises.
func extractShellFunction(t *testing.T, scriptPath, name string) string {
t.Helper()
data, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatalf("read %s: %v", scriptPath, err)
}
text := string(data)
start := strings.Index(text, "\n"+name+"() {")
if start < 0 {
t.Fatalf("function %s not found in %s", name, scriptPath)
}
start++
end := strings.Index(text[start:], "\n}\n")
if end < 0 {
t.Fatalf("function %s has no top-level closing brace in %s", name, scriptPath)
}
return text[start : start+end+3]
}
// mkdirStampWithMarker creates a DWS-owned stamp fixture: the stamp directory
// plus the exact ownership marker bytes prune verifies. Without the marker a
// stamp-shaped directory is foreign data that prune must neither count nor
// remove.
func mkdirStampWithMarker(t *testing.T, backupRoot, stamp string) {
t.Helper()
dir := filepath.Join(backupRoot, stamp)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
mustWriteFile(t, filepath.Join(dir, skillBackupMarkerFile), []byte(skillBackupMarkerBody), 0o644)
}
// runShellPruneScenario builds a sandbox HOME with the given pre-existing
// (earlier-run) stamps, registers the given stamps as created by the current
// run, optionally also plants stamp-shaped directories without the ownership
// marker, runs the script's own prune_skill_backups, and returns the
// surviving stamp directories.
func runShellPruneScenario(t *testing.T, scriptPath string, foreignStamps, runStamps []string, unmarkedStamps ...string) []string {
t.Helper()
home := t.TempDir()
backupRoot := filepath.Join(home, ".dws", "skill-backups")
for _, stamp := range foreignStamps {
mkdirStampWithMarker(t, backupRoot, stamp)
}
for _, stamp := range unmarkedStamps {
if err := os.MkdirAll(filepath.Join(backupRoot, stamp, "payload"), 0o755); err != nil {
t.Fatal(err)
}
}
var driver strings.Builder
driver.WriteString("#!/bin/sh\n")
driver.WriteString("say() { :; }\n")
// The keep limit is a script-level variable outside the extracted
// functions; set both spellings the four installers use.
driver.WriteString("DWS_SKILL_BACKUP_KEEP=5\n")
driver.WriteString("SKILL_BACKUP_KEEP=5\n")
driver.WriteString(extractShellFunction(t, scriptPath, "is_skill_backup_stamp"))
driver.WriteString("\n")
driver.WriteString(extractShellFunction(t, scriptPath, "record_current_run_backup_stamp"))
driver.WriteString("\n")
driver.WriteString(extractShellFunction(t, scriptPath, "prune_skill_backups"))
driver.WriteString("\n")
for _, stamp := range runStamps {
mkdirStampWithMarker(t, backupRoot, stamp)
driver.WriteString("record_current_run_backup_stamp \"$HOME/.dws/skill-backups/" + stamp + "\"\n")
}
driver.WriteString("prune_skill_backups\n")
driverPath := filepath.Join(t.TempDir(), "prune-driver.sh")
if err := os.WriteFile(driverPath, []byte(driver.String()), 0o755); err != nil {
t.Fatal(err)
}
cmd := exec.Command("sh", driverPath)
cmd.Env = append(os.Environ(), "HOME="+home)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("prune driver failed: %v\n%s", err, out)
}
entries, err := os.ReadDir(backupRoot)
if err != nil {
t.Fatal(err)
}
var surviving []string
for _, entry := range entries {
if entry.IsDir() {
surviving = append(surviving, entry.Name())
}
}
return surviving
}
func assertStampsSurvive(t *testing.T, surviving []string, want ...string) {
t.Helper()
left := make(map[string]bool)
for _, stamp := range surviving {
left[stamp] = true
}
for _, stamp := range want {
if !left[stamp] {
t.Fatalf("stamp %s must survive pruning, surviving=%v", stamp, surviving)
}
delete(left, stamp)
}
for stamp := range left {
t.Fatalf("stamp %s must have been pruned, surviving=%v", stamp, surviving)
}
}
// TestInstallShellPruneKeepsCurrentRunBackups pins the reversibility contract
// the changelog states for every install surface: pruning may only remove
// batches from earlier runs. The old shell logic deleted the oldest excess
// stamps regardless of origin, so a single migration retiring more than five
// batches silently destroyed its own rollback material.
func TestInstallShellPruneKeepsCurrentRunBackups(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skipf("shell installer tests do not run on %s", runtime.GOOS)
}
for _, name := range shellPruneScriptTargets {
t.Run(name, func(t *testing.T) {
scriptPath := filepath.Join("..", "..", "scripts", name)
if _, err := os.Stat(scriptPath); err != nil {
t.Fatalf("stat %s: %v", scriptPath, err)
}
t.Run("run that exceeds the keep limit loses nothing", func(t *testing.T) {
var runStamps []string
for i := 1; i <= 8; i++ {
runStamps = append(runStamps, stampName(i))
}
surviving := runShellPruneScenario(t, scriptPath, nil, runStamps)
assertStampsSurvive(t, surviving, runStamps...)
})
t.Run("only earlier-run batches are pruned", func(t *testing.T) {
var foreignStamps []string
for i := 1; i <= 6; i++ {
foreignStamps = append(foreignStamps, oldStampName(i))
}
runStamps := []string{stampName(1), stampName(2)}
surviving := runShellPruneScenario(t, scriptPath, foreignStamps, runStamps)
// Total 8, keep 5: three oldest foreign batches go, the
// current run keeps both of its own.
assertStampsSurvive(t, surviving,
oldStampName(4), oldStampName(5), oldStampName(6),
stampName(1), stampName(2))
})
t.Run("foreign-only history still prunes to the keep limit", func(t *testing.T) {
var foreignStamps []string
for i := 1; i <= 7; i++ {
foreignStamps = append(foreignStamps, oldStampName(i))
}
surviving := runShellPruneScenario(t, scriptPath, foreignStamps, nil)
assertStampsSurvive(t, surviving,
oldStampName(3), oldStampName(4), oldStampName(5),
oldStampName(6), oldStampName(7))
})
// A stamp-shaped directory without the ownership marker is foreign
// data: it must be preserved untouched, never counted toward the
// keep limit, and never treated as prunable excess. Oldest on
// purpose — if the marker were not required, this directory would
// be the first pruning candidate.
t.Run("unmarked stamp-shaped directory is preserved and uncounted", func(t *testing.T) {
var foreignStamps []string
for i := 1; i <= 7; i++ {
foreignStamps = append(foreignStamps, oldStampName(i))
}
unmarked := "20200101-000000"
surviving := runShellPruneScenario(t, scriptPath, foreignStamps, nil, unmarked)
assertStampsSurvive(t, surviving,
append([]string{unmarked},
oldStampName(3), oldStampName(4), oldStampName(5),
oldStampName(6), oldStampName(7))...)
})
})
}
}
func stampName(i int) string {
return "20260817-0000" + twoDigits(i)
}
func oldStampName(i int) string {
return "20200101-0000" + twoDigits(i)
}
func twoDigits(i int) string {
if i < 10 {
return "0" + string(rune('0'+i))
}
return string(rune('0'+i/10)) + string(rune('0'+i%10))
}
// TestInstallShellBackupNeverAdoptsUnprovenStampRoots pins the ownership
// contract at creation time: a stamp-shaped directory whose marker does not
// verify must never receive the DWS marker or a payload — the backup moves
// to a fresh suffixed root instead, and the foreign data survives untouched.
func TestInstallShellBackupNeverAdoptsUnprovenStampRoots(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX shell semantics are unavailable")
}
for _, scriptName := range []string{"install.sh", "install-skills.sh", "install-event.sh", "install-devapp.sh"} {
scriptName := scriptName
t.Run(scriptName, func(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName))
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatal(err)
}
cut := strings.LastIndex(string(data), "\nmain\n")
if cut < 0 {
t.Fatalf("%s final main invocation not found", scriptName)
}
library := filepath.Join(t.TempDir(), scriptName+"-lib.sh")
mustWriteFile(t, library, data[:cut], 0o755)
home := t.TempDir()
// install.sh and install-skills.sh name the payload by basename;
// the standalone event/devapp publishers mangle the full path.
payloadName := "victim"
if scriptName == "install-event.sh" || scriptName == "install-devapp.sh" {
// Mirror the scripts' own mangling: full path, slashes and
// backslashes to dashes, leading dash dropped.
payloadName = strings.NewReplacer("/", "-", "\\", "-").Replace(filepath.Join(home, "victim"))
payloadName = strings.TrimPrefix(payloadName, "-")
}
harness := `. "$DWS_TEST_LIBRARY"
date() { printf '%s\n' 20260101-120000; }
ROOT="$HOME/.dws/skill-backups"
mkdir -p "$ROOT/20260101-120000" || exit 9
printf 'must-survive\n' > "$ROOT/20260101-120000/user-data.txt" || exit 9
mkdir -p "$HOME/victim" || exit 9
printf 'payload\n' > "$HOME/victim/SKILL.md" || exit 9
if ! backup_fn "$HOME/victim" >/dev/null 2>&1; then
if [ -e "$HOME/victim/SKILL.md" ]; then
exit 0 # a refused backup that keeps the source is contract-compliant
fi
exit 9
fi
[ -f "$ROOT/20260101-120000-1/` + "`printf '%s' \"" + payloadName + "\"`" + `/SKILL.md" ] || exit 3
[ ! -e "$HOME/victim" ] || exit 4
[ -f "$ROOT/20260101-120000/user-data.txt" ] || exit 5
[ "$(cat "$ROOT/20260101-120000/user-data.txt")" = "must-survive" ] || exit 6
[ ! -e "$ROOT/20260101-120000/.dws-skill-backup" ] || exit 7
[ "$(cat "$ROOT/20260101-120000-1/.dws-skill-backup" 2>/dev/null)" = "dws skill backup v1" ] || exit 8
exit 0
`
harnessPath := filepath.Join(t.TempDir(), "backup-adoption.sh")
mustWriteFile(t, harnessPath, []byte(harness), 0o755)
cmd := exec.Command("sh", "-c", "backup_fn() { "+backupFnName(scriptName)+" \"$1\"; }\n. \"$0\"", harnessPath)
cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%s must not adopt an unproven stamp root: %v\n%s", scriptName, err, string(output))
}
if backup := findSkillBackupContent(home, "payload\n"); !strings.Contains(backup, "20260101-120000-1") {
t.Fatalf("%s payload must land in the suffixed root, got %s", scriptName, backup)
}
})
}
}
func backupFnName(scriptName string) string {
if scriptName == "install-event.sh" || scriptName == "install-devapp.sh" {
return "backup_skill_dir"
}
return "backup_and_remove_skill_dir"
}