Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

374 lines
15 KiB
Go

package ssh
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/parser"
)
func TestBuildResolveScript(t *testing.T) {
script := buildResolveScript()
// Claude has CLAUDE_PROJECTS_DIR env var — must be referenced.
assert.Contains(t, script, "CLAUDE_PROJECTS_DIR")
assert.Contains(t, script, "CLAUDE_CONFIG_DIR")
// Only file-backed provider-authoritative agents belong in the resolver.
for _, def := range parser.Registry {
want := def.FileBased &&
parser.ProviderMigrationModes()[def.Type] ==
parser.ProviderMigrationProviderAuthoritative
if want {
assert.True(t, resolveScriptMentionsAgent(script, def.Type),
"file-backed provider-authoritative agent %s missing from script", def.Type)
continue
}
assert.False(t, resolveScriptMentionsAgent(script, def.Type),
"unsupported agent %s must stay out of the SSH resolver", def.Type)
}
}
func resolveScriptMentionsAgent(script string, agent parser.AgentType) bool {
name := string(agent)
return strings.Contains(script, "\""+name+":") ||
strings.Contains(script, " "+name+"\n")
}
func TestResolveScriptExcludesDevinProviderRoot(t *testing.T) {
home := t.TempDir()
devinRoot := filepath.Join(home, ".local", "share", "devin")
require.NoError(t, os.MkdirAll(devinRoot, 0o755))
out := runResolveScriptForTest(t, "HOME="+home, "DEVIN_DIR="+devinRoot)
dirs, _ := parseResolvedDirs(string(out))
assert.NotContains(t, dirs, parser.AgentDevin)
}
func TestResolveScriptHonorsClaudeConfigDirRoot(t *testing.T) {
home := t.TempDir()
root := filepath.Join(home, "claude personal")
projectsDir := filepath.Join(root, "projects")
require.NoError(t, os.MkdirAll(projectsDir, 0o755), "mkdir projects")
out := runResolveScriptForTest(t,
"HOME="+home,
"CLAUDE_CONFIG_DIR="+root,
)
dirs, _ := parseResolvedDirs(string(out))
assert.Contains(t, dirs[parser.AgentClaude], root+"/projects")
assert.NotContains(t, dirs[parser.AgentClaude], home+"/.claude/projects")
}
func TestResolveScriptTreatsEnvValuesAsData(t *testing.T) {
home := t.TempDir()
projectsDir := filepath.Join(home, "config root", "projects")
require.NoError(t, os.MkdirAll(projectsDir, 0o755), "mkdir projects")
script := buildResolveScript()
require.NotContains(t, script, "eval")
cmd := exec.Command("sh", "-c", script)
cmd.Env = []string{
"HOME=" + home,
"CLAUDE_PROJECTS_DIR=" + projectsDir,
}
out, err := cmd.CombinedOutput()
require.NoError(t, err, "resolve script failed: output: %s", out)
dirs, _ := parseResolvedDirs(string(out))
assert.Contains(t, dirs[parser.AgentClaude], projectsDir)
}
func TestResolveScriptExitsZero(t *testing.T) {
// The resolve script must exit 0 even when no agent
// dirs exist. Verify by running it against an empty
// HOME so no default dirs are found.
out := runResolveScriptForTest(t, "HOME=/nonexistent")
// No dirs should be found.
assert.Empty(t, strings.TrimSpace(string(out)))
}
// TestResolveScriptIncludesCodexIndex verifies the resolve script emits the
// Codex session_index.jsonl as an extra file when it exists, so renamed
// titles get transferred and imported during remote SSH sync. Runs the real
// script through sh against a temp HOME rather than mocking it.
func TestResolveScriptIncludesCodexIndex(t *testing.T) {
home := t.TempDir()
sessionsDir := filepath.Join(home, ".codex", "sessions")
require.NoError(t, os.MkdirAll(sessionsDir, 0o755), "mkdir sessions")
indexPath := filepath.Join(home, ".codex", "session_index.jsonl")
require.NoError(t, os.WriteFile(indexPath, []byte("{}\n"), 0o644), "write index")
out := runResolveScriptForTest(t, "HOME="+home)
// The script runs in a POSIX shell (MSYS on Windows), so it emits
// forward-slash paths that differ from native filepath.Join output.
// Match by POSIX suffix, which also guards against the parent
// expansion collapsing the index path to /session_index.jsonl.
dirs, extraFiles := parseResolvedDirs(string(out))
assert.Truef(t, hasSuffix(dirs[parser.AgentCodex], ".codex/sessions"),
"codex sessions dir should be resolved, got %v", dirs[parser.AgentCodex])
assert.Truef(t, hasSuffix(extraFiles, ".codex/session_index.jsonl"),
"codex session_index.jsonl should be an extra file, got %v", extraFiles)
}
// hasSuffix reports whether any element of paths ends with suffix.
func hasSuffix(paths []string, suffix string) bool {
for _, p := range paths {
if strings.HasSuffix(p, suffix) {
return true
}
}
return false
}
// TestResolveScriptSkipsMissingCodexIndex verifies that a missing index
// produces no extra-file entry, so the transfer's tar command never names a
// nonexistent path (which would be a fatal, non-benign error).
func TestResolveScriptSkipsMissingCodexIndex(t *testing.T) {
home := t.TempDir()
require.NoError(t,
os.MkdirAll(filepath.Join(home, ".codex", "sessions"), 0o755),
"mkdir sessions")
out := runResolveScriptForTest(t, "HOME="+home)
_, extraFiles := parseResolvedDirs(string(out))
assert.Empty(t, extraFiles,
"no extra files when session_index.jsonl is absent")
}
// TestResolveScriptSkipsAiderHomeDefault verifies the resolve script does
// NOT infer a bare-$HOME Aider root. The remote resolver tars every emitted
// target, so Aider must stay opt-in even when a history file exists at home
// root. Runs the real script through sh against a temp HOME.
func TestResolveScriptSkipsAiderHomeDefault(t *testing.T) {
home := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(home, ".aider.chat.history.md"),
[]byte("# aider chat started at 2024-01-01 00:00:00\n"),
0o644,
), "write history")
out := runResolveScriptForTest(t, "HOME="+home)
dirs, _ := parseResolvedDirs(string(out))
assert.Empty(t, dirs[parser.AgentAider],
"aider bare-$HOME default must not be resolved for remote tar, got %v",
dirs[parser.AgentAider])
// Guard against $HOME ever appearing as a tar target via aider.
assert.NotContains(t, string(out), "aider:"+home,
"aider must not resolve to the whole home dir")
}
// TestResolveScriptAiderScopedByEnvFindsHistoryFiles verifies that an explicit
// AIDER_DIR discovers only aider history files for transfer. The remote sync
// treats resolved entries as tar targets, so emitting the code root would
// archive the entire repository instead of just .aider.chat.history.md files.
func TestResolveScriptAiderScopedByEnvFindsHistoryFiles(t *testing.T) {
home := t.TempDir()
codeRoot := filepath.Join(home, "code")
repoA := filepath.Join(codeRoot, "repo-a")
repoB := filepath.Join(codeRoot, "nested", "repo-b")
require.NoError(t, os.MkdirAll(repoA, 0o755), "mkdir repo A")
require.NoError(t, os.MkdirAll(repoB, 0o755), "mkdir repo B")
historyA := filepath.Join(repoA, parser.AiderHistoryFileName())
historyB := filepath.Join(repoB, parser.AiderHistoryFileName())
require.NoError(t, os.WriteFile(historyA, []byte("# aider\n"), 0o644))
require.NoError(t, os.WriteFile(historyB, []byte("# aider\n"), 0o644))
require.NoError(t, os.WriteFile(
filepath.Join(repoA, "source.go"), []byte("package main\n"), 0o644,
))
skippedDir := filepath.Join(codeRoot, "node_modules", "dep")
require.NoError(t, os.MkdirAll(skippedDir, 0o755), "mkdir skipped dir")
skippedHistory := filepath.Join(skippedDir, parser.AiderHistoryFileName())
require.NoError(t, os.WriteFile(skippedHistory, []byte("# aider\n"), 0o644))
deepDir := filepath.Join(codeRoot, "a", "b", "c", "d", "e")
require.NoError(t, os.MkdirAll(deepDir, 0o755), "mkdir deep dir")
deepHistory := filepath.Join(deepDir, parser.AiderHistoryFileName())
require.NoError(t, os.WriteFile(deepHistory, []byte("# aider\n"), 0o644))
out := runResolveScriptForTest(t, "HOME="+home, "AIDER_DIR="+codeRoot)
dirs, _ := parseResolvedDirs(string(out))
aiderTargets := slashPaths(dirs[parser.AgentAider])
assert.ElementsMatch(t, []string{filepath.ToSlash(historyA), filepath.ToSlash(historyB)}, aiderTargets,
"explicit AIDER_DIR must resolve only aider history files")
assert.NotContains(t, aiderTargets, filepath.ToSlash(codeRoot),
"AIDER_DIR itself must not become a tar target")
assert.NotContains(t, aiderTargets, filepath.ToSlash(skippedHistory),
"remote aider discovery must prune local-discovery skip dirs")
assert.NotContains(t, aiderTargets, filepath.ToSlash(deepHistory),
"remote aider discovery must enforce the local depth cap")
}
func TestResolveScriptAiderNewlinePathCannotInjectTarget(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows path APIs reject embedded newlines; this regression covers POSIX remote shell output")
}
home := t.TempDir()
codeRoot := filepath.Join(home, "code")
injected := "/home/victim/" + parser.AiderHistoryFileName()
maliciousDir := filepath.Join(codeRoot, "repo\naider:", "home", "victim")
require.NoError(t, os.MkdirAll(maliciousDir, 0o755), "mkdir malicious dir")
maliciousHistory := filepath.Join(maliciousDir, parser.AiderHistoryFileName())
require.NoError(t, os.WriteFile(maliciousHistory, []byte("# aider\n"), 0o644))
out := runResolveScriptForTest(t, "HOME="+home, "AIDER_DIR="+codeRoot)
dirs, _ := parseResolvedDirs(string(out))
assert.NotContains(t, dirs[parser.AgentAider], injected,
"newline-bearing repository paths must not inject a second transfer target")
for _, target := range dirs[parser.AgentAider] {
assert.NotContains(t, target, "\n",
"aider transfer target must not contain record separators")
}
}
func slashPaths(paths []string) []string {
out := make([]string, len(paths))
for i, p := range paths {
out[i] = filepath.ToSlash(p)
}
return out
}
// TestResolveScriptAiderRejectsHomeOverride verifies that setting AIDER_DIR
// to literal $HOME (the very thing the home-default skip prevents) is also
// dropped, so an unscoped override cannot reintroduce a whole-home tar.
func TestResolveScriptAiderRejectsHomeOverride(t *testing.T) {
home := t.TempDir()
for _, override := range []string{home, home + "/"} {
out := runResolveScriptForTest(t, "HOME="+home, "AIDER_DIR="+override)
dirs, _ := parseResolvedDirs(string(out))
assert.Empty(t, dirs[parser.AgentAider],
"AIDER_DIR=%q (== $HOME) must not resolve to a whole-home tar, got %v",
override, dirs[parser.AgentAider])
}
}
func TestResolveScriptWindsurfTargetsOnlySessionFiles(t *testing.T) {
home := t.TempDir()
userRoot := filepath.Join(home, "AppData", "Roaming", "Windsurf", "User")
workspaceRoot := filepath.Join(userRoot, "workspaceStorage")
workspaceDir := filepath.Join(workspaceRoot, "workspace-a")
stateDB := filepath.Join(workspaceDir, parser.WindsurfStateDBName)
stateWAL := stateDB + "-wal"
stateSHM := stateDB + "-shm"
workspaceJSON := filepath.Join(workspaceDir, "workspace.json")
secretPath := filepath.Join(workspaceDir, "extension-secret.json")
require.NoError(t, os.MkdirAll(workspaceDir, 0o755))
require.NoError(t, os.WriteFile(stateDB, []byte("state"), 0o644))
require.NoError(t, os.WriteFile(stateWAL, []byte("wal"), 0o644))
require.NoError(t, os.WriteFile(stateSHM, []byte("shm"), 0o644))
require.NoError(t, os.WriteFile(workspaceJSON, []byte("{}\n"), 0o644))
require.NoError(t, os.WriteFile(secretPath, []byte("secret"), 0o644))
out := runResolveScriptForTest(t, "HOME="+home)
records := resolveOutputRecords(string(out))
userRootSuffix := filepath.ToSlash(filepath.Join("AppData", "Roaming", "Windsurf", "User"))
workspaceRootSuffix := filepath.ToSlash(filepath.Join(userRootSuffix, "workspaceStorage"))
workspaceSuffix := filepath.ToSlash(filepath.Join(workspaceRootSuffix, "workspace-a"))
agentFilePrefix := resolveAgentFilePrefix + ":" + string(parser.AgentWindsurf)
assert.True(t, hasRecordWithPathSuffix(records, string(parser.AgentWindsurf), userRootSuffix))
assert.True(t, hasRecordWithPathSuffix(records, agentFilePrefix,
filepath.ToSlash(filepath.Join(workspaceSuffix, parser.WindsurfStateDBName))))
assert.True(t, hasRecordWithPathSuffix(records, agentFilePrefix,
filepath.ToSlash(filepath.Join(workspaceSuffix, parser.WindsurfStateDBName+"-wal"))))
assert.False(t, hasRecordWithPathSuffix(records, agentFilePrefix,
filepath.ToSlash(filepath.Join(workspaceSuffix, parser.WindsurfStateDBName+"-shm"))))
assert.True(t, hasRecordWithPathSuffix(records, agentFilePrefix,
filepath.ToSlash(filepath.Join(workspaceSuffix, "workspace.json"))))
assert.False(t, hasRecordWithPathSuffix(records, string(parser.AgentWindsurf), workspaceRootSuffix))
assert.False(t, hasRecordWithPathSuffix(records, agentFilePrefix,
filepath.ToSlash(filepath.Join(workspaceSuffix, filepath.Base(secretPath)))))
}
func hasRecordWithPathSuffix(records []string, prefix, suffix string) bool {
for _, record := range records {
if strings.HasPrefix(record, prefix+":") && strings.HasSuffix(record, suffix) {
return true
}
}
return false
}
func runResolveScriptForTest(t *testing.T, env ...string) []byte {
t.Helper()
cmd := exec.Command("sh")
cmd.Stdin = strings.NewReader(buildResolveScript())
cmd.Env = env
out, err := cmd.CombinedOutput()
require.NoError(t, err, "resolve script failed: output: %s", out)
return out
}
func TestParseResolvedDirs(t *testing.T) {
input := "claude:/home/wes/.claude/projects\n" +
"codex:/home/wes/.codex/sessions\n" +
"codex:\n" +
"copilot:/home/wes/.copilot\n" +
"@file:/home/wes/.codex/session_index.jsonl\n" +
"@file:/home/wes/.codex/session_index.jsonl\n" +
"\n"
dirs, extraFiles := parseResolvedDirs(input)
// codex has one valid dir and one empty (excluded) entry.
assert.Equal(t, []string{"/home/wes/.codex/sessions"}, dirs[parser.AgentCodex])
// claude and copilot present.
assert.Equal(t, []string{"/home/wes/.claude/projects"}, dirs[parser.AgentClaude])
assert.Equal(t, []string{"/home/wes/.copilot"}, dirs[parser.AgentCopilot])
assert.Len(t, dirs, 3)
// The duplicate index file line is deduplicated.
assert.Equal(t,
[]string{"/home/wes/.codex/session_index.jsonl"}, extraFiles)
}
func TestParseResolvedDirsNULRecords(t *testing.T) {
input := "claude:/home/wes/.claude/projects\x00" +
"aider:/home/wes/code/repo/.aider.chat.history.md\x00" +
"@file:/home/wes/.codex/session_index.jsonl\x00"
dirs, extraFiles := parseResolvedDirs(input)
assert.Equal(t, []string{"/home/wes/.claude/projects"}, dirs[parser.AgentClaude])
assert.Equal(t,
[]string{"/home/wes/code/repo/.aider.chat.history.md"},
dirs[parser.AgentAider])
assert.Equal(t,
[]string{"/home/wes/.codex/session_index.jsonl"}, extraFiles)
}
func TestParseResolvedTargetsIncludesAgentFiles(t *testing.T) {
input := "windsurf:/home/wes/Windsurf/User\x00" +
"@agentfile:windsurf:/home/wes/Windsurf/User/workspaceStorage/a/state.vscdb\x00" +
"@agentfile:windsurf:/home/wes/Windsurf/User/workspaceStorage/a/state.vscdb\x00" +
"@agentfile:windsurf:/home/wes/Windsurf/User/workspaceStorage/a/workspace.json\x00" +
"@file:/home/wes/.codex/session_index.jsonl\x00"
dirs, files, extraFiles := parseResolvedTargets(input)
assert.Equal(t, []string{"/home/wes/Windsurf/User"}, dirs[parser.AgentWindsurf])
assert.Equal(t, []string{
"/home/wes/Windsurf/User/workspaceStorage/a/state.vscdb",
"/home/wes/Windsurf/User/workspaceStorage/a/workspace.json",
}, files[parser.AgentWindsurf])
assert.Equal(t,
[]string{"/home/wes/.codex/session_index.jsonl"}, extraFiles)
}