T20-I2/I3/I5/I8: dev compose /api/v1/ready, main events networks match test/dev, Dockerfile golang:1.27.1-bookworm, and testing.Short skip for pi-agent docker build.
199 lines
5.5 KiB
Go
199 lines
5.5 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 testing.Short() {
|
|
t.Skip("docker build smoke skipped under -short")
|
|
}
|
|
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
|
|
}
|