mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
380 lines
14 KiB
Go
380 lines
14 KiB
Go
package e2e
|
|
|
|
import (
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("TUI musn't know about teams", func(t *testing.T) {
|
|
imports := listImports(t, "../pkg/tui")
|
|
|
|
assert.True(t, imports["github.com/docker/docker-agent/pkg/runtime"])
|
|
assert.False(t, imports["github.com/docker/docker-agent/pkg/team"])
|
|
})
|
|
|
|
// Embedders that build teams in code (agent.New + team.New + the runtime,
|
|
// see docs/guides/go-sdk) must not link docker-agent's heavy optional dependencies:
|
|
// the goja JS engine, the OpenAI SDK, the SQLite driver, full go-git or
|
|
// expr. Each entry below was cut deliberately; if this test fails you
|
|
// probably added an import that drags one of them back in — keep it
|
|
// behind a leaf package or a registration hook instead.
|
|
t.Run("code-built team surface stays lean", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
deps := listTransitiveDeps(t,
|
|
"./pkg/runtime",
|
|
"./pkg/agent",
|
|
"./pkg/team",
|
|
"./pkg/session",
|
|
"./pkg/tools",
|
|
"./pkg/model/provider/anthropic",
|
|
)
|
|
|
|
// Prefixes, so subpackage-only leaks (e.g. openai-go/v3/option) are
|
|
// caught too.
|
|
forbiddenPrefixes := []string{
|
|
"github.com/dop251/goja",
|
|
"github.com/expr-lang/expr",
|
|
"github.com/openai/openai-go",
|
|
"github.com/go-git/go-git",
|
|
"github.com/rumpl/harness",
|
|
"modernc.org/sqlite",
|
|
"github.com/docker/docker-agent/pkg/js",
|
|
"github.com/docker/docker-agent/pkg/codingharness",
|
|
"github.com/docker/docker-agent/pkg/tools/mcp",
|
|
"github.com/docker/docker-agent/pkg/toolinstall",
|
|
}
|
|
// Deliberate exceptions: fsx's gitignore matching uses go-git's
|
|
// lightweight format package (and the helpers it drags in), not the
|
|
// full object/transport stack; the runtime's remote OAuth login uses
|
|
// the MCP-toolset-free oauthflow leaf.
|
|
allowed := map[string]bool{
|
|
"github.com/go-git/go-git/v5/plumbing/format/gitignore": true,
|
|
"github.com/go-git/go-git/v5/plumbing/format/config": true,
|
|
"github.com/go-git/go-git/v5/internal/path_util": true,
|
|
"github.com/go-git/go-git/v5/utils/ioutil": true,
|
|
"github.com/docker/docker-agent/pkg/tools/mcp/oauthflow": true,
|
|
}
|
|
|
|
for dep := range deps {
|
|
if allowed[dep] {
|
|
continue
|
|
}
|
|
for _, prefix := range forbiddenPrefixes {
|
|
if dep == prefix || strings.HasPrefix(dep, prefix+"/") {
|
|
assert.Fail(t, "heavy dependency leaked back into the embedder surface", "%s (matched %s)", dep, prefix)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Embedders that load YAML through teamloader with hand-picked registries
|
|
// (teamloader.WithToolsetRegistry / WithProviderRegistry / WithStrict)
|
|
// must not link every toolset, provider and agent source. The default
|
|
// registries live in pkg/teamloader/defaults, pkg/teamloader/toolsets and
|
|
// pkg/model/provider/providers; the OCI/HCL source stacks live in
|
|
// pkg/config/ocisource, pkg/config/hcl and pkg/config/sources; optional
|
|
// loader features (JS expansion, code mode, TOON, deferred tools, LSP
|
|
// merging, DMR probing) are opt-in options. None of them may be reachable
|
|
// from the loader itself: pkg/teamloader must add no module over
|
|
// pkg/runtime.
|
|
t.Run("YAML-loading surface with custom registries stays lean", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
deps := listTransitiveDeps(t,
|
|
"./pkg/config",
|
|
"./pkg/teamloader",
|
|
"./pkg/bootstrap",
|
|
"./pkg/embeddedchat",
|
|
"./pkg/tools/builtin/api/client",
|
|
"./examples/golibrary/leanapi",
|
|
"./pkg/tools/builtin/think",
|
|
"./pkg/model/provider/anthropic",
|
|
)
|
|
|
|
forbiddenPrefixes := []string{
|
|
// Opt-in loader features.
|
|
"github.com/dop251/goja",
|
|
"github.com/docker/docker-agent/pkg/js",
|
|
"github.com/docker/docker-agent/pkg/tools/codemode",
|
|
"github.com/docker/docker-agent/pkg/tools/toon",
|
|
"github.com/alpkeskin/gotoon",
|
|
"github.com/docker/docker-agent/pkg/tools/builtin/deferred",
|
|
"github.com/junegunn/fzf",
|
|
"github.com/docker/docker-agent/pkg/tools/builtin/lsp",
|
|
"github.com/docker/docker-agent/pkg/toolinstall",
|
|
"github.com/expr-lang/expr",
|
|
"github.com/docker/docker-agent/pkg/model/provider/dmr",
|
|
"github.com/openai/openai-go",
|
|
"github.com/docker/docker-agent/pkg/codingharness",
|
|
"github.com/rumpl/harness",
|
|
// Other providers and their SDKs.
|
|
"github.com/docker/docker-agent/pkg/model/provider/providers",
|
|
"github.com/docker/docker-agent/pkg/model/provider/bedrock",
|
|
"github.com/docker/docker-agent/pkg/model/provider/gemini",
|
|
"github.com/docker/docker-agent/pkg/model/provider/vertexai",
|
|
"github.com/aws/aws-sdk-go-v2",
|
|
"google.golang.org/genai",
|
|
// The full toolset registry and the heavier toolsets.
|
|
"github.com/docker/docker-agent/pkg/teamloader/defaults",
|
|
"github.com/docker/docker-agent/pkg/teamloader/toolsets",
|
|
"github.com/docker/docker-agent/pkg/tools/builtin/rag",
|
|
"github.com/docker/docker-agent/pkg/tools/builtin/memory",
|
|
"github.com/docker/docker-agent/pkg/tools/builtin/shell",
|
|
"github.com/docker/docker-agent/pkg/tools/builtin/filesystem",
|
|
"modernc.org/sqlite",
|
|
// Agent sources the embedder did not ask for.
|
|
"github.com/docker/docker-agent/pkg/config/sources",
|
|
"github.com/docker/docker-agent/pkg/config/ocisource",
|
|
"github.com/docker/docker-agent/pkg/config/hcl",
|
|
"github.com/docker/docker-agent/pkg/remote",
|
|
"github.com/docker/docker-agent/pkg/content",
|
|
"github.com/hashicorp/hcl",
|
|
"github.com/zclconf/go-cty",
|
|
"github.com/google/go-containerregistry",
|
|
}
|
|
// Reference classification (config.IsOCIReference) only needs the
|
|
// registry-free name parser; DMR model discovery is a stdlib-only leaf.
|
|
allowed := map[string]bool{
|
|
"github.com/google/go-containerregistry/pkg/name": true,
|
|
"github.com/docker/docker-agent/pkg/model/provider/dmr/dmrmodels": true,
|
|
}
|
|
|
|
for dep := range deps {
|
|
if allowed[dep] {
|
|
continue
|
|
}
|
|
for _, prefix := range forbiddenPrefixes {
|
|
if dep == prefix || strings.HasPrefix(dep, prefix+"/") {
|
|
assert.Fail(t, "dependency leaked into the YAML-loading embedder surface", "%s (matched %s)", dep, prefix)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Check the target-specific closure without cross-compiling the native test
|
|
// dependencies, which intentionally exercise providers unavailable in WASM.
|
|
func TestWasmProviderDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
cmd := exec.CommandContext(t.Context(), "go", "list", "-deps",
|
|
"./pkg/model/provider", "./pkg/teamloader", "./pkg/runtime",
|
|
"./pkg/model/provider/anthropic")
|
|
cmd.Dir = ".."
|
|
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "%s", out)
|
|
forbidden := []string{
|
|
"github.com/docker/docker-agent/pkg/model/provider/openai",
|
|
"github.com/docker/docker-agent/pkg/model/provider/gemini",
|
|
"github.com/docker/docker-agent/pkg/model/provider/providers",
|
|
"github.com/openai/openai-go",
|
|
"google.golang.org/genai",
|
|
"google.golang.org/grpc",
|
|
"cloud.google.com/go",
|
|
}
|
|
for dep := range strings.FieldsSeq(string(out)) {
|
|
for _, prefix := range forbidden {
|
|
assert.False(t, dep == prefix || strings.HasPrefix(dep, prefix+"/"), "unrequested provider dependency: %s", dep)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The default browser binary links only the Gemini API, OpenAI and Anthropic
|
|
// clients; the cloud SDKs stay behind -tags docker_agent_wasm_cloud, and the
|
|
// portable tool and hook builds keep sqlite and the fzf fork out.
|
|
func TestWasmDefaultBinaryDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
cmd := exec.CommandContext(t.Context(), "go", "list", "-deps", "./cmd/wasm")
|
|
cmd.Dir = ".."
|
|
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "%s", out)
|
|
forbidden := []string{
|
|
"github.com/docker/docker-agent/pkg/model/provider/bedrock",
|
|
"github.com/docker/docker-agent/pkg/model/provider/vertexai",
|
|
"github.com/docker/docker-agent/pkg/model/provider/anthropic/vertex",
|
|
"github.com/aws/aws-sdk-go-v2",
|
|
"golang.org/x/oauth2/google",
|
|
"modernc.org/sqlite",
|
|
"github.com/junegunn/fzf",
|
|
}
|
|
for dep := range strings.FieldsSeq(string(out)) {
|
|
for _, prefix := range forbidden {
|
|
assert.False(t, dep == prefix || strings.HasPrefix(dep, prefix+"/"), "opt-in dependency in the default WASM binary: %s", dep)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWasmRAGDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
for _, tc := range []struct {
|
|
name string
|
|
tags string
|
|
rag bool
|
|
}{
|
|
{name: "default"},
|
|
{name: "with RAG", tags: "docker_agent_wasm_rag", rag: true},
|
|
{name: "cloud", tags: "docker_agent_wasm_cloud"},
|
|
{name: "cloud with RAG", tags: "docker_agent_wasm_cloud,docker_agent_wasm_rag", rag: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
cmd := exec.CommandContext(t.Context(), "go", "list", "-deps", "-tags", tc.tags, "./cmd/wasm")
|
|
cmd.Dir = ".."
|
|
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "%s", out)
|
|
|
|
const rag = "github.com/docker/docker-agent/pkg/rag"
|
|
const builtinRAG = "github.com/docker/docker-agent/pkg/tools/builtin/rag"
|
|
deps := strings.Fields(string(out))
|
|
if tc.rag {
|
|
assert.Contains(t, deps, rag)
|
|
assert.Contains(t, deps, builtinRAG)
|
|
return
|
|
}
|
|
for _, dep := range deps {
|
|
if dep == rag+"/types" || dep == rag+"/prompts" {
|
|
continue
|
|
}
|
|
for _, prefix := range []string{rag, builtinRAG, "github.com/fsnotify/fsnotify"} {
|
|
assert.False(t, dep == prefix || strings.HasPrefix(dep, prefix+"/"), "RAG dependency in the no-RAG WASM binary: %s", dep)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// listTransitiveDeps returns the full (non-test) dependency closure of the
|
|
// given packages, as reported by `go list -deps`.
|
|
func listTransitiveDeps(t *testing.T, pkgs ...string) map[string]bool {
|
|
t.Helper()
|
|
|
|
cmd := exec.CommandContext(t.Context(), "go", append([]string{"list", "-deps"}, pkgs...)...)
|
|
cmd.Dir = ".."
|
|
out, err := cmd.Output()
|
|
require.NoError(t, err)
|
|
|
|
deps := map[string]bool{}
|
|
for dep := range strings.FieldsSeq(string(out)) {
|
|
deps[dep] = true
|
|
}
|
|
return deps
|
|
}
|
|
|
|
func listImports(t *testing.T, pkg string) map[string]bool {
|
|
t.Helper()
|
|
|
|
imports := map[string]bool{}
|
|
|
|
fileSet := token.NewFileSet()
|
|
err := filepath.WalkDir(pkg, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil || !strings.HasSuffix(path, ".go") || d.IsDir() {
|
|
return err
|
|
}
|
|
|
|
ast, err := parser.ParseFile(fileSet, path, nil, parser.ImportsOnly)
|
|
require.NoError(t, err)
|
|
|
|
for _, i := range ast.Imports {
|
|
imports[strings.Trim(i.Path.Value, `"`)] = true
|
|
}
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return imports
|
|
}
|
|
|
|
func TestLocalRuntimeDoesNotImportRemoteTransport(t *testing.T) {
|
|
t.Parallel()
|
|
const prefix = "github.com/docker/docker-agent/pkg/runtime"
|
|
local := listTransitiveDeps(t, "./pkg/runtime")
|
|
assert.NotContains(t, local, prefix+"/remote", "local execution must not link remote transport")
|
|
remote := listTransitiveDeps(t, "./pkg/runtime/remote")
|
|
assert.Contains(t, remote, prefix, "remote execution uses the existing runtime interface and events")
|
|
for _, deps := range []map[string]bool{local, remote} {
|
|
assert.NotContains(t, deps, prefix+"/contract")
|
|
assert.NotContains(t, deps, prefix+"/events")
|
|
}
|
|
}
|
|
|
|
func TestReusableTUIDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const prefix = "github.com/docker/docker-agent/"
|
|
// Runtime event/state contracts retain the runtime's own tools and MCP protocol.
|
|
// Presentation may not add an executable builtin merely for a name or wire type.
|
|
runtimeDeps := listTransitiveDeps(t, "./pkg/runtime")
|
|
for _, entry := range []string{
|
|
"pkg/tui/commands",
|
|
"pkg/tui/components/editor/completions",
|
|
"pkg/tui/components/editor",
|
|
"pkg/tui/dialog/common",
|
|
"pkg/tui/dialog/toolconfirmation",
|
|
"pkg/tui/components/messages",
|
|
"pkg/tui/components/transcript",
|
|
"pkg/tui/components/reasoningblock",
|
|
"pkg/tui/components/tool",
|
|
"pkg/tui/components/tool/api",
|
|
"pkg/tui/components/tool/defaulttool",
|
|
} {
|
|
t.Run(entry, func(t *testing.T) {
|
|
t.Parallel()
|
|
deps := listTransitiveDeps(t, "./"+entry)
|
|
for dep := range deps {
|
|
for _, forbidden := range []string{
|
|
prefix + "pkg/app", prefix + "pkg/cli", prefix + "pkg/js",
|
|
prefix + "pkg/tui/dialog", prefix + "pkg/tui/commands/defaults",
|
|
"github.com/dop251/goja",
|
|
} {
|
|
if forbidden == prefix+"pkg/tui/dialog" {
|
|
assert.NotEqual(t, forbidden, dep, "application dialogs leaked into %s", entry)
|
|
continue
|
|
}
|
|
assert.False(t, dep == forbidden || strings.HasPrefix(dep, forbidden+"/"), "%s imports %s", entry, dep)
|
|
}
|
|
if strings.HasPrefix(dep, prefix+"pkg/tools/mcp") {
|
|
assert.Equal(t, prefix+"pkg/tools/mcp/oauthflow", dep, "MCP implementation leaked into %s", entry)
|
|
}
|
|
if strings.HasPrefix(dep, prefix+"pkg/tools/builtin/") && !strings.HasSuffix(dep, "/types") {
|
|
assert.True(t, runtimeDeps[dep], "%s added executable tool dependency %s", entry, dep)
|
|
}
|
|
if strings.HasPrefix(dep, prefix+"pkg/tui/components/tool/") {
|
|
assert.Contains(t, []string{prefix + "pkg/tui/components/tool/api", prefix + "pkg/tui/components/tool/defaulttool"}, dep,
|
|
"optional builtin renderer leaked into %s", entry)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToolContractDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const prefix = "github.com/docker/docker-agent/"
|
|
deps := listTransitiveDeps(t,
|
|
"./pkg/tools/builtin/fetch/types", "./pkg/tools/builtin/filesystem/types",
|
|
"./pkg/tools/builtin/handoff/types", "./pkg/tools/builtin/plan/types",
|
|
"./pkg/tools/builtin/shell/types", "./pkg/tools/builtin/todo/types",
|
|
"./pkg/tools/builtin/transfertask/types", "./pkg/tools/builtin/userprompt/types",
|
|
)
|
|
for dep := range deps {
|
|
if strings.HasPrefix(dep, prefix) {
|
|
assert.True(t, dep == prefix+"pkg/safety" || strings.HasSuffix(dep, "/types"), "execution dependency in tool contracts: %s", dep)
|
|
}
|
|
}
|
|
}
|