Files
Agent-System/internal/agent/pi_runner_image_test.go
T
weichenandCursor 369fe7bf51 fix(pi-agent): copy settle-policy into the runner image
runner.mjs imports ./settle-policy.mjs but the Dockerfile only copied
runner.mjs, which crashed shuimu-pi-agent-dev on import. COPY the
runtime module and assert every relative import is in the image set.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 19:29:34 +00:00

196 lines
5.4 KiB
Go

package agent
import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"
)
func piAgentRepoRoot(t *testing.T) string {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("runtime.Caller")
}
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
}
func TestPiAgentDockerfileCopiesRunnerRelativeImports(t *testing.T) {
root := piAgentRepoRoot(t)
runnerRel := filepath.ToSlash("deploy/pi-agent/runner.mjs")
df, err := os.ReadFile(filepath.Join(root, "deploy", "pi-agent", "Dockerfile"))
if err != nil {
t.Fatal(err)
}
copied := dockerCopyDestinations(string(df))
if len(copied) == 0 {
t.Fatal("deploy/pi-agent/Dockerfile has no COPY lines")
}
seen := map[string]bool{}
var missing []string
var walk func(repoRel string)
walk = func(repoRel string) {
repoRel = filepath.ToSlash(repoRel)
if seen[repoRel] {
return
}
seen[repoRel] = true
abs := filepath.Join(root, filepath.FromSlash(repoRel))
body, err := os.ReadFile(abs)
if err != nil {
t.Fatalf("read %s: %v", repoRel, err)
}
dir := filepath.ToSlash(filepath.Dir(repoRel))
for _, spec := range relativeModuleSpecs(string(body)) {
child := filepath.ToSlash(filepath.Clean(dir + "/" + strings.TrimPrefix(spec, "./")))
imageDest := runnerImportImagePath(child)
if !dockerCopyCovers(copied, imageDest) {
missing = append(missing, spec+" -> "+imageDest+" (from "+repoRel+")")
}
walk(child)
}
}
walk(runnerRel)
if len(missing) > 0 {
t.Fatalf("pi-agent image COPY set is missing runner runtime modules:\n %s", strings.Join(missing, "\n "))
}
if !dockerCopyCovers(copied, "/app/settle-policy.mjs") {
t.Fatal("deploy/pi-agent/Dockerfile must COPY settle-policy.mjs to /app/settle-policy.mjs")
}
}
func TestPiAgentDockerBuildContext_RunnerModulesPresent(t *testing.T) {
if _, err := exec.LookPath("docker"); err != nil {
t.Skip("docker CLI not available")
}
root := piAgentRepoRoot(t)
dfBody, err := os.ReadFile(filepath.Join(root, "deploy", "pi-agent", "Dockerfile"))
if err != nil {
t.Fatal(err)
}
var copies []string
for _, raw := range strings.Split(string(dfBody), "\n") {
line := strings.TrimSpace(raw)
if strings.HasPrefix(line, "COPY deploy/pi-agent/") && !strings.Contains(line, "--from=") {
copies = append(copies, line)
}
}
if len(copies) == 0 {
t.Fatal("no deploy/pi-agent COPY lines to replay")
}
slim := "FROM debian:bookworm-slim\nWORKDIR /app\n" + strings.Join(copies, "\n") + "\n"
df := filepath.Join(t.TempDir(), "Dockerfile")
if err := os.WriteFile(df, []byte(slim), 0o644); err != nil {
t.Fatal(err)
}
tag := "agent-system-pi-runner-modules-smoke:local"
build := exec.Command("docker", "build", "-f", df, "-t", tag, ".")
build.Dir = root
out, err := runCmdTimed(build, 3*time.Minute)
if err != nil {
t.Fatalf("docker build context failed: %v\n%s", err, out)
}
t.Cleanup(func() {
_ = exec.Command("docker", "rmi", "-f", tag).Run()
})
for _, path := range []string{"/app/runner.mjs", "/app/settle-policy.mjs", "/app/extensions/tavily-search.mjs"} {
cat := exec.Command("docker", "run", "--rm", "--entrypoint", "test", tag, "-f", path)
if body, err := runCmdTimed(cat, 30*time.Second); err != nil {
t.Fatalf("image missing %s: %v\n%s", path, err, body)
}
}
}
func runCmdTimed(cmd *exec.Cmd, d time.Duration) ([]byte, error) {
var buf strings.Builder
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Start(); err != nil {
return []byte(buf.String()), err
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
return []byte(buf.String()), err
case <-time.After(d):
_ = cmd.Process.Kill()
return []byte(buf.String()), os.ErrDeadlineExceeded
}
}
var relativeFromRe = regexp.MustCompile(`(?m)from\s+["'](\./[^"']+)["']`)
func relativeModuleSpecs(src string) []string {
var out []string
for _, m := range relativeFromRe.FindAllStringSubmatch(src, -1) {
if len(m) > 1 {
out = append(out, m[1])
}
}
return out
}
func runnerImportImagePath(repoRel string) string {
const prefix = "deploy/pi-agent/"
rel := filepath.ToSlash(repoRel)
if strings.HasPrefix(rel, prefix) {
return "/app/" + strings.TrimPrefix(rel, prefix)
}
return "/app/" + filepath.Base(rel)
}
type dockerCopy struct {
src string
dest string
}
func dockerCopyDestinations(df string) []dockerCopy {
var out []dockerCopy
for _, raw := range strings.Split(df, "\n") {
line := strings.TrimSpace(raw)
if !strings.HasPrefix(line, "COPY ") || strings.HasPrefix(line, "COPY --from=") {
continue
}
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
src := filepath.ToSlash(fields[1])
dest := filepath.ToSlash(fields[len(fields)-1])
out = append(out, dockerCopy{src: src, dest: dest})
}
return out
}
func dockerCopyCovers(copies []dockerCopy, imagePath string) bool {
imagePath = filepath.ToSlash(imagePath)
for _, c := range copies {
dest := c.dest
src := c.src
if dest == imagePath {
return true
}
if strings.HasSuffix(dest, "/") && strings.HasPrefix(imagePath, dest) {
return true
}
if strings.HasSuffix(src, "/") && strings.HasSuffix(dest, "/") {
relSrc := strings.TrimPrefix(src, "deploy/pi-agent/")
if strings.HasPrefix(imagePath, dest+relSrc) || strings.HasPrefix(imagePath, dest) {
// /app/extensions/ + tavily-search.mjs
base := strings.TrimPrefix(imagePath, dest)
if base != imagePath && !strings.Contains(base, "..") {
return true
}
}
}
}
return false
}