Files
multica/server/internal/daemon/execenv/codex_user_skills_test.go
a1da8e536f MUL-6000 fix(daemon): link user Codex skills into codex-home instead of copying them (#6742)
* fix(daemon): link user Codex skills into codex-home instead of copying them

Every Codex task got a full recursive copy of ~/.codex/skills/ inside its
per-task CODEX_HOME. For a user with npm-backed skills that is ~100 MB per
task directory, re-copied on every task start — hydrateCodexSkills wipes and
re-seeds on both the Prepare and the Reuse path — and never reclaimed: the GC
artifact sweep only matches node_modules / .next / .turbo, and whole-directory
reclaim needs the issue to reach a terminal status first. Total cost scales
with the number of live task directories.

Link instead. The same per-task home already links its other two shared
resources, sessions and the plugin cache, and a skill directory is read-only
input to the CLI, so the write isolation a copy incidentally bought has no
requester. EvalSymlinks still runs first, so an installer-shipped symlink
(lark-cli into ~/.agents/skills/) resolves to the real directory rather than
producing a link to a link.

The three paths that could reach through the new link are unchanged in
behavior: os.RemoveAll deletes a link and not its target, writeSkillFiles
probes with os.Lstat so a workspace skill colliding with a linked user skill
lands in a `-multica` sibling, and the GC refuses to walk into links. Tests
pin all three, plus a windows-tagged test for the junction form of the link.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): stop GC walks from descending into Windows directory junctions

The GC's artifact sweep and its size accounting both refuse to follow links
out of a task directory, but they tested for os.ModeSymlink alone. That misses
the link shape Windows actually produces: createDirLink falls back to a
directory junction (mklink /J) whenever os.Symlink is denied, which is the
default without Developer Mode, and since Go 1.23 os.Lstat reports a junction
as ModeDir|ModeIrregular with no ModeSymlink bit while its DirEntry still
answers IsDir() == true. filepath.WalkDir therefore descends straight into the
link target, and the sweep would delete a node_modules / .next / .turbo
directory that lives inside the user's real home.

Match both mode bits in all three walks. The task directory links user content
in three places today — the Codex session store, the plugin cache, and now the
user's skills — so this is reachable on any Windows host running Codex tasks.

Windows-tagged tests create the junction with mklink /J directly, so the
junction shape is covered even on a runner where symlinks are permitted, and
they run in the existing windows-execenv CI job.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-11 13:51:50 +08:00

221 lines
8.0 KiB
Go

package execenv
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// User skills reach the per-task CODEX_HOME as links, not copies (MUL-6000).
// Copying charged every task directory the full skill tree — ~100 MB for a
// user with npm-backed skills — re-paid on every task start and never
// reclaimed while the issue stayed open.
func writeUserSkill(t *testing.T, dir, body string) {
t.Helper()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(body), 0o644); err != nil {
t.Fatalf("write SKILL.md in %s: %v", dir, err)
}
}
// assertIsLink fails when path is a real directory rather than a link to one.
// A POSIX symlink reports ModeSymlink; a Windows junction reports
// ModeDir|ModeIrregular (Go 1.23+), which is why the mask covers both.
func assertIsLink(t *testing.T, path string) {
t.Helper()
fi, err := os.Lstat(path)
if err != nil {
t.Fatalf("lstat %s: %v", path, err)
}
if fi.Mode()&(os.ModeSymlink|os.ModeIrregular) == 0 {
t.Fatalf("%s is a real directory (mode %v), want a link", path, fi.Mode())
}
}
func TestSeedUserCodexSkillsLinksInsteadOfCopying(t *testing.T) {
// Cannot use t.Parallel() with t.Setenv.
sharedHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedHome)
userSkill := filepath.Join(sharedHome, "skills", "alpha")
writeUserSkill(t, userSkill, "alpha v1")
codexHome := t.TempDir()
if err := seedUserCodexSkills(codexHome, nil, testLogger()); err != nil {
t.Fatalf("seedUserCodexSkills: %v", err)
}
dst := filepath.Join(codexHome, "skills", "alpha")
assertIsLink(t, dst)
body, err := os.ReadFile(filepath.Join(dst, "SKILL.md"))
if err != nil {
t.Fatalf("read seeded SKILL.md: %v", err)
}
if string(body) != "alpha v1" {
t.Errorf("seeded SKILL.md = %q, want %q", body, "alpha v1")
}
// The decisive proof it is not a copy: content added to the user's real
// skill directory after seeding is visible through the per-task path.
if err := os.WriteFile(filepath.Join(userSkill, "reference.md"), []byte("added later"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dst, "reference.md")); err != nil {
t.Errorf("per-task skill dir is a snapshot copy, not a link: %v", err)
}
}
func TestSeedUserCodexSkillsResolvesInstallerSymlink(t *testing.T) {
// Installers like lark-cli put a symlink in ~/.codex/skills/<name>
// pointing into a shared ~/.agents/skills/<name>/.
agentsRoot := t.TempDir()
realSkill := filepath.Join(agentsRoot, "lark-base")
writeUserSkill(t, realSkill, "lark-base v1")
sharedHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedHome)
if err := os.MkdirAll(filepath.Join(sharedHome, "skills"), 0o755); err != nil {
t.Fatal(err)
}
installerLink := filepath.Join(sharedHome, "skills", "lark-base")
if err := os.Symlink(realSkill, installerLink); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
codexHome := t.TempDir()
if err := seedUserCodexSkills(codexHome, nil, testLogger()); err != nil {
t.Fatalf("seedUserCodexSkills: %v", err)
}
dst := filepath.Join(codexHome, "skills", "lark-base")
assertIsLink(t, dst)
body, err := os.ReadFile(filepath.Join(dst, "SKILL.md"))
if err != nil {
t.Fatalf("symlink-installed skill unreadable through the per-task link: %v", err)
}
if string(body) != "lark-base v1" {
t.Errorf("SKILL.md = %q, want %q", body, "lark-base v1")
}
// The per-task link must point at the real directory, not at the
// installer's own link, so it stays valid if the installer re-points it.
if runtime.GOOS != "windows" {
target, err := os.Readlink(dst)
if err != nil {
t.Fatalf("readlink %s: %v", dst, err)
}
want, err := filepath.EvalSymlinks(realSkill)
if err != nil {
t.Fatal(err)
}
if target != want {
t.Errorf("link target = %q, want the resolved skill dir %q", target, want)
}
}
}
func TestSeedUserCodexSkillsYieldsToWorkspaceSkill(t *testing.T) {
sharedHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedHome)
writeUserSkill(t, filepath.Join(sharedHome, "skills", "issue-review"), "user version")
writeUserSkill(t, filepath.Join(sharedHome, "skills", "keeper"), "keeper version")
codexHome := t.TempDir()
skills := []SkillContextForEnv{{Name: "Issue Review", Content: "workspace version"}}
if err := seedUserCodexSkills(codexHome, skills, testLogger()); err != nil {
t.Fatalf("seedUserCodexSkills: %v", err)
}
if _, err := os.Lstat(filepath.Join(codexHome, "skills", "issue-review")); !os.IsNotExist(err) {
t.Errorf("user skill claimed a workspace skill's slot (err = %v)", err)
}
if _, err := os.Lstat(filepath.Join(codexHome, "skills", "keeper")); err != nil {
t.Errorf("unrelated user skill was dropped: %v", err)
}
}
func TestSeedUserCodexSkillsWithNoEligibleSkillsCreatesNoDir(t *testing.T) {
sharedHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedHome)
if err := os.MkdirAll(filepath.Join(sharedHome, "skills"), 0o755); err != nil {
t.Fatal(err)
}
codexHome := t.TempDir()
if err := seedUserCodexSkills(codexHome, nil, testLogger()); err != nil {
t.Fatalf("seedUserCodexSkills: %v", err)
}
if _, err := os.Lstat(filepath.Join(codexHome, "skills")); !os.IsNotExist(err) {
t.Errorf("skills dir created with nothing to seed (err = %v)", err)
}
}
// hydrateCodexSkills wipes the skills dir before every seed, on both the
// Prepare and the Reuse path. With links in place that RemoveAll must delete
// the link and never reach through it into the user's real skill directory.
func TestHydrateCodexSkillsWipeKeepsUserSkillContent(t *testing.T) {
sharedHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedHome)
userSkill := filepath.Join(sharedHome, "skills", "alpha")
writeUserSkill(t, userSkill, "alpha v1")
if err := os.WriteFile(filepath.Join(userSkill, "reference.md"), []byte("support"), 0o644); err != nil {
t.Fatal(err)
}
codexHome := t.TempDir()
workspaceSkills := []SkillContextForEnv{{Name: "Beta", Description: "workspace skill", Content: "beta body"}}
// Twice: Prepare hydrates, then a follow-up task on the same env re-hydrates.
for i := 0; i < 2; i++ {
if err := hydrateCodexSkills(codexHome, workspaceSkills, nil, testLogger()); err != nil {
t.Fatalf("hydrateCodexSkills run %d: %v", i+1, err)
}
}
for _, name := range []string{"SKILL.md", "reference.md"} {
if _, err := os.Stat(filepath.Join(userSkill, name)); err != nil {
t.Fatalf("hydrate deleted %s from the user's real skill dir: %v", name, err)
}
}
assertIsLink(t, filepath.Join(codexHome, "skills", "alpha"))
if _, err := os.Stat(filepath.Join(codexHome, "skills", "beta", "SKILL.md")); err != nil {
t.Errorf("workspace skill missing after re-hydration: %v", err)
}
}
// A workspace skill whose name only collides after directory naming — the
// sanitized-name pre-filter does not catch it — must land in a sibling
// directory instead of being written through the user's link.
func TestHydrateCodexSkillsNeverWritesThroughAUserSkillLink(t *testing.T) {
sharedHome := t.TempDir()
t.Setenv("CODEX_HOME", sharedHome)
userSkill := filepath.Join(sharedHome, "skills", "alpha")
writeUserSkill(t, userSkill, "user version")
codexHome := t.TempDir()
skillsDir := filepath.Join(codexHome, "skills")
if err := seedUserCodexSkills(codexHome, nil, testLogger()); err != nil {
t.Fatalf("seedUserCodexSkills: %v", err)
}
// writeSkillFiles runs without the pre-filter that hydrateCodexSkills
// applies, which is exactly the case allocateCollisionFreeSkillDir guards.
if err := writeSkillFiles(skillsDir, []SkillContextForEnv{{Name: "alpha", Content: "workspace version"}}, nil); err != nil {
t.Fatalf("writeSkillFiles: %v", err)
}
body, err := os.ReadFile(filepath.Join(userSkill, "SKILL.md"))
if err != nil {
t.Fatalf("read user skill: %v", err)
}
if string(body) != "user version" {
t.Fatalf("workspace skill was written through the link into the user's skill dir: %q", body)
}
if _, err := os.Stat(filepath.Join(skillsDir, "alpha-multica", "SKILL.md")); err != nil {
t.Errorf("colliding workspace skill did not land in a sibling dir: %v", err)
}
}