From 65b0d8a4f295fe6a826a2de5b716e24d07fa8119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Thu, 30 Oct 2025 21:04:32 +0100 Subject: [PATCH 01/19] feat(tests): add integration tests using Testcontainers --- .github/workflows/ci.yml | 3 + Makefile | 8 +- cmd/cli/commands/integration_test.go | 180 +++++++++++++++++++++++++++ cmd/cli/desktop/context.go | 20 +++ cmd/cli/go.mod | 20 ++- cmd/cli/go.sum | 38 ++++++ go.work.sum | 21 +++- 7 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 cmd/cli/commands/integration_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00024d033..2db544a7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: - name: Run tests with race detection run: go test -race ./... + - name: Run integration tests with race detection + run: go test -v -race -tags=integration -timeout=5m ./... + - name: validate run: make validate diff --git a/Makefile b/Makefile index 66c5e162f..fee13e930 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ TAG ?= LICENSE ?= # Main targets -.PHONY: build run clean test docker-build docker-build-multiplatform docker-run docker-build-vllm docker-run-vllm docker-run-impl help validate model-distribution-tool +.PHONY: build run clean test integration-tests docker-build docker-build-multiplatform docker-run docker-build-vllm docker-run-vllm docker-run-impl help validate model-distribution-tool # Default target .DEFAULT_GOAL := build @@ -56,6 +56,12 @@ clean: test: go test -v ./... +integration-tests: + @echo "Running integration tests..." + @echo "Note: This requires Docker to be running" + @go test -v -race -tags=integration -timeout=5m . + @echo "Integration tests completed!" + validate: find . -type f -name "*.sh" | xargs shellcheck diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go new file mode 100644 index 000000000..41d5ef7c9 --- /dev/null +++ b/cmd/cli/commands/integration_test.go @@ -0,0 +1,180 @@ +//go:build integration + +package commands + +import ( + "context" + "fmt" + "io" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/docker/model-runner/cmd/cli/desktop" + "github.com/docker/model-runner/pkg/distribution/builder" + "github.com/docker/model-runner/pkg/distribution/registry" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" + "github.com/testcontainers/testcontainers-go/wait" +) + +// testEnv holds the test environment components +type testEnv struct { + ctx context.Context + registryURL string + client *desktop.Client +} + +// setupTestEnv creates the complete test environment with registry and DMR +func setupTestEnv(t *testing.T) *testEnv { + ctx := context.Background() + + // Create a custom network for container communication + net, err := network.New(ctx) + require.NoError(t, err) + testcontainers.CleanupNetwork(t, net) + + registryURL := ociRegistry(t, ctx, net) + dmrURL := dockerModelRunner(t, ctx, net) + + modelRunnerCtx, err := desktop.NewContextForTest(dmrURL, nil) + require.NoError(t, err, "Failed to create model runner context") + + client := desktop.New(modelRunnerCtx) + if !client.Status().Running { + t.Fatal("DMR is not running") + } + + return &testEnv{ + ctx: ctx, + registryURL: registryURL, + client: client, + } +} + +func ociRegistry(t *testing.T, ctx context.Context, net *testcontainers.DockerNetwork) string { + t.Log("Starting OCI registry container...") + ctr, err := testcontainers.Run( + ctx, "registry:2", + testcontainers.WithExposedPorts("5000/tcp"), + testcontainers.WithWaitStrategy(wait.ForHTTP("/v2/").WithPort("5000/tcp").WithStartupTimeout(30*time.Second)), + network.WithNetwork([]string{"registry.local"}, net), + ) + require.NoError(t, err) + testcontainers.CleanupContainer(t, ctr) + + registryEndpoint, err := ctr.Endpoint(ctx, "") + require.NoError(t, err) + registryURL := fmt.Sprintf("http://%s", registryEndpoint) + t.Logf("Registry available at: %s", registryURL) + return registryURL +} + +func dockerModelRunner(t *testing.T, ctx context.Context, net *testcontainers.DockerNetwork) string { + t.Log("Starting DMR container...") + ctr, err := testcontainers.Run( + ctx, "docker/model-runner:latest", + testcontainers.WithExposedPorts("12434/tcp"), + testcontainers.WithWaitStrategy(wait.ForHTTP("/engines/status").WithPort("12434/tcp").WithStartupTimeout(10*time.Second)), + network.WithNetwork([]string{"dmr"}, net), + ) + require.NoError(t, err) + testcontainers.CleanupContainer(t, ctr) + + dmrEndpoint, err := ctr.Endpoint(ctx, "") + require.NoError(t, err) + + dmrURL := fmt.Sprintf("http://%s", dmrEndpoint) + t.Logf("DMR available at: %s", dmrURL) + return dmrURL +} + +// createAndPushTestModel creates a minimal test model and pushes it to the local registry. +// Returns the model ID and FQDNs for host and network access. +func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextSize uint64) (modelID, hostFQDN, networkFQDN string) { + ctx := context.Background() + + // Use the dummy GGUF file from assets + dummyGGUFPath := filepath.Join("../../../assets/dummy.gguf") + absPath, err := filepath.Abs(dummyGGUFPath) + require.NoError(t, err) + + // Check if the file exists + _, err = os.Stat(absPath) + require.NoError(t, err, "dummy.gguf not found at %s", absPath) + + // Create a builder from the GGUF file + t.Logf("Creating test model %s from %s", modelRef, absPath) + pkg, err := builder.FromGGUF(absPath) + require.NoError(t, err) + + // Set context size if specified + if contextSize > 0 { + pkg = pkg.WithContextSize(contextSize) + } + + // Construct the full reference with the local registry host for pushing from test host + uri, err := url.Parse(registryURL) + require.NoError(t, err) + + hostFQDN = fmt.Sprintf("%s/%s", uri.Host, modelRef) + t.Logf("Pushing to local registry: %s", hostFQDN) + + // Create registry client + client := registry.NewClient(registry.WithUserAgent("integration-test/1.0")) + target, err := client.NewTarget(hostFQDN) + require.NoError(t, err) + + // Push the model + err = pkg.Build(ctx, target, io.Discard) + require.NoError(t, err) + t.Logf("Successfully pushed test model: %s", hostFQDN) + + // For pulling from DMR, use the network alias "registry.local:5000" + // go-containerregistry will automatically use HTTP for .local hostnames + networkFQDN = fmt.Sprintf("registry.local:5000/%s", modelRef) + + id, err := pkg.Model().ID() + require.NoError(t, err) + t.Logf("Model ID: %s", id) + + return id, hostFQDN, networkFQDN +} + +// TestIntegration_PullModel tests pulling a model from the local OCI registry via DMR +func TestIntegration_PullModel(t *testing.T) { + env := setupTestEnv(t) + + models, err := listModels(false, env.client, true, false, "") + require.NoError(t, err) + + if len(models) != 0 { + t.Fatal("No models found after pull") + } + + // Create and push a test model + modelRef := "test/test-model:latest" + modelID, hostFQDN, networkFQDN := createAndPushTestModel(t, env.registryURL, modelRef, 2048) + t.Logf("Test model pushed: %s", hostFQDN) + + // Pull the model using the network alias (for inter-container communication) + // go-containerregistry automatically uses HTTP for single-word hostnames without dots + t.Logf("Pulling model %s", networkFQDN) + err = pullModel(newPullCmd(), env.client, networkFQDN, false) + require.NoError(t, err) + + models, err = listModels(false, env.client, true, false, "") + require.NoError(t, err) + + if len(models) == 0 { + t.Fatal("No models found after pull") + } + + if strings.Contains(models, modelID) == false { + t.Fatalf("Pulled model ID %s not found in model list", modelID) + } +} diff --git a/cmd/cli/desktop/context.go b/cmd/cli/desktop/context.go index d3ff0c5b0..e0caf1478 100644 --- a/cmd/cli/desktop/context.go +++ b/cmd/cli/desktop/context.go @@ -105,6 +105,26 @@ func NewContextForMock(client DockerHttpClient) *ModelRunnerContext { } } +// NewContextForTest creates a ModelRunnerContext for integration testing +// with a custom URL endpoint. This is intended for use in integration tests +// where the Model Runner endpoint is dynamically created (e.g., testcontainers). +func NewContextForTest(endpoint string, client DockerHttpClient) (*ModelRunnerContext, error) { + urlPrefix, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("invalid endpoint URL: %w", err) + } + + if client == nil { + client = http.DefaultClient + } + + return &ModelRunnerContext{ + kind: types.ModelRunnerEngineKindMoby, + urlPrefix: urlPrefix, + client: client, + }, nil +} + // DetectContext determines the current Docker Model Runner context. func DetectContext(ctx context.Context, cli *command.DockerCli) (*ModelRunnerContext, error) { // Check for an explicit endpoint setting. diff --git a/cmd/cli/go.mod b/cmd/cli/go.mod index 3f2eefc75..5712ca8da 100644 --- a/cmd/cli/go.mod +++ b/cmd/cli/go.mod @@ -7,8 +7,8 @@ require ( github.com/containerd/errdefs v1.0.0 github.com/docker/cli v28.3.0+incompatible github.com/docker/cli-docs-tool v0.10.0 - github.com/docker/docker v28.2.2+incompatible - github.com/docker/go-connections v0.5.0 + github.com/docker/docker v28.3.3+incompatible + github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/docker/model-runner v0.0.0 github.com/emirpasic/gods/v2 v2.0.0-alpha @@ -30,6 +30,7 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect @@ -48,6 +49,7 @@ require ( github.com/containerd/platforms v1.0.0-rc.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/creack/pty v1.1.24 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -55,6 +57,7 @@ require ( github.com/dlclark/regexp2 v1.11.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/ebitengine/purego v0.8.4 // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -77,14 +80,20 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/kolesnikovae/go-winjob v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect github.com/moby/locker v1.0.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -95,14 +104,21 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect + github.com/testcontainers/testcontainers-go v0.39.0 // indirect + github.com/testcontainers/testcontainers-go/modules/dockermodelrunner v0.39.0 // indirect + github.com/testcontainers/testcontainers-go/modules/socat v0.39.0 // indirect github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.8 // indirect diff --git a/cmd/cli/go.sum b/cmd/cli/go.sum index 9bb5d886c..37f58915d 100644 --- a/cmd/cli/go.sum +++ b/cmd/cli/go.sum @@ -1,3 +1,5 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -69,6 +71,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRcc github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -93,6 +97,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= @@ -100,6 +106,8 @@ github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= @@ -109,6 +117,8 @@ github.com/docker/go-winjob v0.0.0-20250829235554-57b487ebcbc5 h1:dxSFEb0EEmvceI github.com/docker/go-winjob v0.0.0-20250829235554-57b487ebcbc5/go.mod h1:ICOGmIXdwhfid7rQP+tLvDJqVg0lHdEk3pI5nsapTtg= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q= github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= @@ -146,6 +156,7 @@ github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= @@ -196,7 +207,11 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -220,14 +235,20 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -265,6 +286,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= @@ -285,6 +308,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -311,8 +336,18 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= +github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= +github.com/testcontainers/testcontainers-go/modules/dockermodelrunner v0.39.0 h1:Qov/F4NIEgDlja5ktZV5GMoKatF9n63eVRUkxSoRWiQ= +github.com/testcontainers/testcontainers-go/modules/dockermodelrunner v0.39.0/go.mod h1:xkhfM/NCXHUm4y742j1h9yzs15Ls+k9wbRQ0FXOo6Ko= +github.com/testcontainers/testcontainers-go/modules/socat v0.39.0 h1:X4d9ouGP0w2cYCUc0u9X75++bNKkoTa6kB78PGRsPgQ= +github.com/testcontainers/testcontainers-go/modules/socat v0.39.0/go.mod h1:PWR8vCQ+7tBdoskNbM0shmMNlZEnex4xI31t9KVzCiU= github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0= github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -393,11 +428,14 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= diff --git a/go.work.sum b/go.work.sum index 08b981d87..9ca7063c5 100644 --- a/go.work.sum +++ b/go.work.sum @@ -348,12 +348,14 @@ github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b h1:Ga1nclDSe8gOw37MV github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= @@ -374,6 +376,8 @@ github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U= github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI= +github.com/openai/openai-go v0.1.0-beta.9 h1:ABpubc5yU/3ejee2GgRrbFta81SG/d7bQbB8mIdP0Xo= +github.com/openai/openai-go v0.1.0-beta.9/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -398,6 +402,8 @@ github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= +github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= @@ -460,8 +466,18 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtse github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA= +github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= @@ -612,7 +628,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From 189b64a39bf0361747d46edffd5268174a0fb02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Fri, 31 Oct 2025 17:14:30 +0100 Subject: [PATCH 02/19] feat(tests): enhance integration tests for model pulling with various reference formats --- cmd/cli/commands/integration_test.go | 116 ++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 21 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 65634530d..8077a2de7 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -93,6 +93,12 @@ func dockerModelRunner(t *testing.T, ctx context.Context, net *testcontainers.Do return dmrURL } +// removeModel removes a model from the local store +func removeModel(client *desktop.Client, modelID string) error { + _, err := client.Remove([]string{modelID}, true) + return err +} + // createAndPushTestModel creates a minimal test model and pushes it to the local registry. // Returns the model ID and FQDNs for host and network access. func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextSize uint64) (modelID, hostFQDN, networkFQDN string) { @@ -146,6 +152,7 @@ func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextS } // TestIntegration_PullModel tests pulling a model from the local OCI registry via DMR +// with various model reference formats to ensure proper normalization. func TestIntegration_PullModel(t *testing.T) { env := setupTestEnv(t) @@ -156,28 +163,95 @@ func TestIntegration_PullModel(t *testing.T) { t.Fatal("Expected no initial models, but found some") } - // Create and push a test model - modelRef := "test/test-model:latest" - modelID, hostFQDN, networkFQDN := createAndPushTestModel(t, env.registryURL, modelRef, 2048) - t.Logf("Test model pushed: %s", hostFQDN) - - // Pull the model using the network alias (for inter-container communication) - // go-containerregistry automatically uses HTTP for single-word hostnames without dots - t.Logf("Pulling model %s", networkFQDN) - err = pullModel(newPullCmd(), env.client, networkFQDN, false) - require.NoError(t, err) - - models, err = listModels(false, env.client, true, false, "") - require.NoError(t, err) - - if len(models) == 0 { - t.Fatal("No models found after pull") + // Create and push two test models with different organizations + // Model 1: custom org (test/test-model:latest) + modelRef1 := "test/test-model:latest" + modelID1, hostFQDN1, networkFQDN1 := createAndPushTestModel(t, env.registryURL, modelRef1, 2048) + t.Logf("Test model 1 pushed: %s (ID: %s)", hostFQDN1, modelID1) + + // Model 2: default org (ai/test-model:latest) + modelRef2 := "ai/test-model:latest" + modelID2, hostFQDN2, networkFQDN2 := createAndPushTestModel(t, env.registryURL, modelRef2, 2048) + t.Logf("Test model 2 pushed: %s (ID: %s)", hostFQDN2, modelID2) + + // Test cases for different model reference formats + testCases := []struct { + name string + pullRef string // Reference to use when pulling + expectedModelID string // Expected model ID after pull + expectedModelName string // Expected model name for logging + }{ + { + name: "explicit custom org and tag", + pullRef: networkFQDN1, // registry.local:5000/test/test-model:latest + expectedModelID: modelID1, + expectedModelName: "test/test-model:latest", + }, + { + name: "custom org without tag (should default to :latest)", + pullRef: "registry.local:5000/test/test-model", + expectedModelID: modelID1, + expectedModelName: "test/test-model:latest", + }, + { + name: "explicit default org and tag", + pullRef: networkFQDN2, // registry.local:5000/ai/test-model:latest + expectedModelID: modelID2, + expectedModelName: "ai/test-model:latest", + }, + { + name: "default org without tag (should default to :latest)", + pullRef: "registry.local:5000/ai/test-model", + expectedModelID: modelID2, + expectedModelName: "ai/test-model:latest", + }, + { + name: "no org with tag (should default to ai/)", + pullRef: "registry.local:5000/test-model:latest", + expectedModelID: modelID2, + expectedModelName: "ai/test-model:latest", + }, + { + name: "no org and no tag (should default to ai/:latest)", + pullRef: "registry.local:5000/test-model", + expectedModelID: modelID2, + expectedModelName: "ai/test-model:latest", + }, } - // Extract truncated ID format (sha256:xxx... -> xxx where xxx is 12 chars) - // listModels with quiet=true returns modelID[7:19] - truncatedID := modelID[7:19] - if strings.Contains(models, truncatedID) == false { - t.Fatalf("Pulled model ID %s (truncated: %s) not found in model list:\n%s", modelID, truncatedID, models) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Pull the model using the test case reference + t.Logf("Pulling model with reference: %s", tc.pullRef) + err := pullModel(newPullCmd(), env.client, tc.pullRef, false) + require.NoError(t, err, "Failed to pull model with reference: %s", tc.pullRef) + + // List models and verify the expected model is present + models, err := listModels(false, env.client, true, false, "") + require.NoError(t, err) + + if len(models) == 0 { + t.Fatalf("No models found after pulling %s", tc.pullRef) + } + + // Extract truncated ID format (sha256:xxx... -> xxx where xxx is 12 chars) + // listModels with quiet=true returns modelID[7:19] + truncatedID := tc.expectedModelID[7:19] + if !strings.Contains(models, truncatedID) { + t.Errorf("Expected model ID %s (truncated: %s) not found in model list after pulling %s.\nExpected model: %s\nModel list:\n%s", + tc.expectedModelID, truncatedID, tc.pullRef, tc.expectedModelName, models) + } else { + t.Logf("✓ Successfully verified model %s (ID: %s) after pulling with reference: %s", + tc.expectedModelName, truncatedID, tc.pullRef) + } + + // Clean up: remove the model for the next test iteration + // Note: We use the full model ID for removal to ensure we remove the correct model + t.Logf("Removing model %s", truncatedID) + err = removeModel(env.client, tc.expectedModelID) + if err != nil { + t.Logf("Warning: Failed to remove model %s: %v (continuing anyway)", truncatedID, err) + } + }) } } From 1922d93eab9968fe9f6f2b9a0ca7c07cf6a8aed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Fri, 31 Oct 2025 21:12:07 +0100 Subject: [PATCH 03/19] refactor(tests): comment out tests requiring default OCI registry override --- cmd/cli/commands/integration_test.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 8077a2de7..feb31a284 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -205,18 +205,19 @@ func TestIntegration_PullModel(t *testing.T) { expectedModelID: modelID2, expectedModelName: "ai/test-model:latest", }, - { - name: "no org with tag (should default to ai/)", - pullRef: "registry.local:5000/test-model:latest", - expectedModelID: modelID2, - expectedModelName: "ai/test-model:latest", - }, - { - name: "no org and no tag (should default to ai/:latest)", - pullRef: "registry.local:5000/test-model", - expectedModelID: modelID2, - expectedModelName: "ai/test-model:latest", - }, + // TODO: these tests require to override the default OCI registry, + //{ + // name: "no org with tag (should default to ai/)", + // pullRef: "test-model:latest", + // expectedModelID: modelID2, + // expectedModelName: "ai/test-model:latest", + //}, + //{ + // name: "no org and no tag (should default to ai/:latest)", + // pullRef: "test-model", + // expectedModelID: modelID2, + // expectedModelName: "ai/test-model:latest", + //}, } for _, tc := range testCases { From cda1cfbee1812e676f4fd10908c5049b62780b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 15:04:42 +0100 Subject: [PATCH 04/19] fix(tests): update OCI registry version and add environment variables for insecure access --- cmd/cli/commands/integration_test.go | 45 +++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index feb31a284..83d71474e 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -59,7 +59,7 @@ func setupTestEnv(t *testing.T) *testEnv { func ociRegistry(t *testing.T, ctx context.Context, net *testcontainers.DockerNetwork) string { t.Log("Starting OCI registry container...") ctr, err := testcontainers.Run( - ctx, "registry:2", + ctx, "registry:3", testcontainers.WithExposedPorts("5000/tcp"), testcontainers.WithWaitStrategy(wait.ForHTTP("/v2/").WithPort("5000/tcp").WithStartupTimeout(30*time.Second)), network.WithNetwork([]string{"registry.local"}, net), @@ -80,6 +80,10 @@ func dockerModelRunner(t *testing.T, ctx context.Context, net *testcontainers.Do ctx, "docker/model-runner:latest", testcontainers.WithExposedPorts("12434/tcp"), testcontainers.WithWaitStrategy(wait.ForHTTP("/engines/status").WithPort("12434/tcp").WithStartupTimeout(10*time.Second)), + testcontainers.WithEnv(map[string]string{ + "DEFAULT_REGISTRY": "registry.local:5000", + "INSECURE_REGISTRY": "true", + }), network.WithNetwork([]string{"dmr"}, net), ) require.NoError(t, err) @@ -167,12 +171,12 @@ func TestIntegration_PullModel(t *testing.T) { // Model 1: custom org (test/test-model:latest) modelRef1 := "test/test-model:latest" modelID1, hostFQDN1, networkFQDN1 := createAndPushTestModel(t, env.registryURL, modelRef1, 2048) - t.Logf("Test model 1 pushed: %s (ID: %s)", hostFQDN1, modelID1) + t.Logf("Test model 1 pushed: %s (ID: %s) FQDN: %s", hostFQDN1, modelID1, networkFQDN1) // Model 2: default org (ai/test-model:latest) modelRef2 := "ai/test-model:latest" modelID2, hostFQDN2, networkFQDN2 := createAndPushTestModel(t, env.registryURL, modelRef2, 2048) - t.Logf("Test model 2 pushed: %s (ID: %s)", hostFQDN2, modelID2) + t.Logf("Test model 2 pushed: %s (ID: %s) FQDN: %s", hostFQDN2, modelID2, networkFQDN2) // Test cases for different model reference formats testCases := []struct { @@ -183,7 +187,7 @@ func TestIntegration_PullModel(t *testing.T) { }{ { name: "explicit custom org and tag", - pullRef: networkFQDN1, // registry.local:5000/test/test-model:latest + pullRef: "registry.local:5000/test/test-model:latest", expectedModelID: modelID1, expectedModelName: "test/test-model:latest", }, @@ -195,7 +199,7 @@ func TestIntegration_PullModel(t *testing.T) { }, { name: "explicit default org and tag", - pullRef: networkFQDN2, // registry.local:5000/ai/test-model:latest + pullRef: "registry.local:5000/ai/test-model:latest", expectedModelID: modelID2, expectedModelName: "ai/test-model:latest", }, @@ -205,26 +209,25 @@ func TestIntegration_PullModel(t *testing.T) { expectedModelID: modelID2, expectedModelName: "ai/test-model:latest", }, - // TODO: these tests require to override the default OCI registry, - //{ - // name: "no org with tag (should default to ai/)", - // pullRef: "test-model:latest", - // expectedModelID: modelID2, - // expectedModelName: "ai/test-model:latest", - //}, - //{ - // name: "no org and no tag (should default to ai/:latest)", - // pullRef: "test-model", - // expectedModelID: modelID2, - // expectedModelName: "ai/test-model:latest", - //}, + { + name: "no org with tag (should default to ai/)", + pullRef: "test-model:latest", + expectedModelID: modelID2, + expectedModelName: "ai/test-model:latest", + }, + { + name: "no org and no tag (should default to ai/:latest)", + pullRef: "test-model", + expectedModelID: modelID2, + expectedModelName: "ai/test-model:latest", + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Pull the model using the test case reference t.Logf("Pulling model with reference: %s", tc.pullRef) - err := pullModel(newPullCmd(), env.client, tc.pullRef, false) + err := pullModel(newPullCmd(), env.client, tc.pullRef, true) require.NoError(t, err, "Failed to pull model with reference: %s", tc.pullRef) // List models and verify the expected model is present @@ -235,10 +238,12 @@ func TestIntegration_PullModel(t *testing.T) { t.Fatalf("No models found after pulling %s", tc.pullRef) } + models = strings.TrimSpace(models) + // Extract truncated ID format (sha256:xxx... -> xxx where xxx is 12 chars) // listModels with quiet=true returns modelID[7:19] truncatedID := tc.expectedModelID[7:19] - if !strings.Contains(models, truncatedID) { + if models != truncatedID { t.Errorf("Expected model ID %s (truncated: %s) not found in model list after pulling %s.\nExpected model: %s\nModel list:\n%s", tc.expectedModelID, truncatedID, tc.pullRef, tc.expectedModelName, models) } else { From 090ba167cf63435d0bcba7186058685377a25bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 15:32:57 +0100 Subject: [PATCH 05/19] Adds test case of pulling models with digest --- cmd/cli/commands/integration_test.go | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 83d71474e..5fa51ecee 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -104,8 +104,8 @@ func removeModel(client *desktop.Client, modelID string) error { } // createAndPushTestModel creates a minimal test model and pushes it to the local registry. -// Returns the model ID and FQDNs for host and network access. -func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextSize uint64) (modelID, hostFQDN, networkFQDN string) { +// Returns the model ID, FQDNs for host and network access, and the manifest digest. +func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextSize uint64) (modelID, hostFQDN, networkFQDN, digest string) { ctx := context.Background() // Use the dummy GGUF file from assets @@ -152,7 +152,13 @@ func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextS require.NoError(t, err) t.Logf("Model ID: %s", id) - return id, hostFQDN, networkFQDN + // Get the manifest digest + manifestDigest, err := pkg.Model().Digest() + require.NoError(t, err) + digest = manifestDigest.String() + t.Logf("Model digest: %s", digest) + + return id, hostFQDN, networkFQDN, digest } // TestIntegration_PullModel tests pulling a model from the local OCI registry via DMR @@ -170,13 +176,13 @@ func TestIntegration_PullModel(t *testing.T) { // Create and push two test models with different organizations // Model 1: custom org (test/test-model:latest) modelRef1 := "test/test-model:latest" - modelID1, hostFQDN1, networkFQDN1 := createAndPushTestModel(t, env.registryURL, modelRef1, 2048) - t.Logf("Test model 1 pushed: %s (ID: %s) FQDN: %s", hostFQDN1, modelID1, networkFQDN1) + modelID1, hostFQDN1, networkFQDN1, digest1 := createAndPushTestModel(t, env.registryURL, modelRef1, 2048) + t.Logf("Test model 1 pushed: %s (ID: %s) FQDN: %s Digest: %s", hostFQDN1, modelID1, networkFQDN1, digest1) // Model 2: default org (ai/test-model:latest) modelRef2 := "ai/test-model:latest" - modelID2, hostFQDN2, networkFQDN2 := createAndPushTestModel(t, env.registryURL, modelRef2, 2048) - t.Logf("Test model 2 pushed: %s (ID: %s) FQDN: %s", hostFQDN2, modelID2, networkFQDN2) + modelID2, hostFQDN2, networkFQDN2, digest2 := createAndPushTestModel(t, env.registryURL, modelRef2, 2048) + t.Logf("Test model 2 pushed: %s (ID: %s) FQDN: %s Digest: %s", hostFQDN2, modelID2, networkFQDN2, digest2) // Test cases for different model reference formats testCases := []struct { @@ -221,6 +227,18 @@ func TestIntegration_PullModel(t *testing.T) { expectedModelID: modelID2, expectedModelName: "ai/test-model:latest", }, + { + name: "pull by digest with full registry path", + pullRef: fmt.Sprintf("registry.local:5000/test/test-model@%s", digest1), + expectedModelID: modelID1, + expectedModelName: "test/test-model (by digest)", + }, + { + name: "pull by digest with default registry (using default org)", + pullRef: fmt.Sprintf("test-model@%s", digest2), + expectedModelID: modelID2, + expectedModelName: "ai/test-model (by digest)", + }, } for _, tc := range testCases { From e85c72db948d2d4494826c40265731a462661d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 15:52:32 +0100 Subject: [PATCH 06/19] Adds test case of pulling models with digest --- cmd/cli/commands/integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 5fa51ecee..5a1181de5 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -231,13 +231,13 @@ func TestIntegration_PullModel(t *testing.T) { name: "pull by digest with full registry path", pullRef: fmt.Sprintf("registry.local:5000/test/test-model@%s", digest1), expectedModelID: modelID1, - expectedModelName: "test/test-model (by digest)", + expectedModelName: "test/test-model", }, { name: "pull by digest with default registry (using default org)", pullRef: fmt.Sprintf("test-model@%s", digest2), expectedModelID: modelID2, - expectedModelName: "ai/test-model (by digest)", + expectedModelName: "ai/test-model", }, } From 3221a8bc988c1a451ecd9e4f825051951bafb22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 15:58:36 +0100 Subject: [PATCH 07/19] Use a separate workflow for now --- .github/workflows/ci.yml | 3 --- .github/workflows/integration-test.yml | 36 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/integration-test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2db544a7b..00024d033 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,6 @@ jobs: - name: Run tests with race detection run: go test -race ./... - - name: Run integration tests with race detection - run: go test -v -race -tags=integration -timeout=5m ./... - - name: validate run: make validate diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 000000000..235cc6b35 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,36 @@ +name: Integration Tests + +on: + workflow_dispatch: # Manual trigger only + +jobs: + integration-test: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + + - name: Set up Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 + with: + go-version: 1.24.2 + cache: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 + + - name: Build Docker image + run: make docker-build + env: + DOCKER_BUILDKIT: 1 + + - name: Verify Docker image + run: docker images | grep docker/model-runner + + - name: Run integration tests + working-directory: cmd/cli/commands + run: go test -v -tags=integration -timeout=5m -run TestIntegration_PullModel + env: + CGO_ENABLED: 1 From 594dc6872c0d0ad06863ec95e95e3a333fe51d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 16:12:06 +0100 Subject: [PATCH 08/19] fix(index): handle digest references in tag function --- pkg/distribution/internal/store/index.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/distribution/internal/store/index.go b/pkg/distribution/internal/store/index.go index 692fd442c..7f4a9357e 100644 --- a/pkg/distribution/internal/store/index.go +++ b/pkg/distribution/internal/store/index.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/google/go-containerregistry/pkg/name" @@ -18,6 +19,12 @@ type Index struct { } func (i Index) Tag(reference string, tag string) (Index, error) { + // Remove @sha256 in case the reference is a digest + tag = strings.TrimSpace(tag) + if idx := strings.Index(tag, "@sha256"); idx != -1 { + tag = tag[:idx] + } + tag = strings.TrimPrefix(tag, reference) tagRef, err := name.NewTag(tag, registry.GetDefaultRegistryOptions()...) if err != nil { return Index{}, fmt.Errorf("invalid tag: %w", err) From 5ce4125ce22af9a541e4f1e898bffe9efb30bf01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 16:27:00 +0100 Subject: [PATCH 09/19] add tests for inspect command --- cmd/cli/commands/integration_test.go | 139 ++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 4 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 5a1181de5..9fecc7cd0 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -103,6 +103,32 @@ func removeModel(client *desktop.Client, modelID string) error { return err } +// verifyModelInspect inspects a model using the given reference and verifies it matches expectations +func verifyModelInspect(t *testing.T, client *desktop.Client, ref, expectedID, expectedDigest string) { + t.Helper() + + model, err := client.Inspect(ref, false) + require.NoError(t, err, "Failed to inspect model with reference: %s", ref) + + // Verify model ID matches + require.Equal(t, expectedID, model.ID, + "Model ID mismatch when inspecting with reference: %s. Expected: %s, Got: %s", + ref, expectedID, model.ID) + + // Verify digest matches if provided + if expectedDigest != "" { + require.Equal(t, expectedDigest, model.ID, + "Model digest mismatch when inspecting with reference: %s. Expected: %s, Got: %s", + ref, expectedDigest, model.ID) + } + + // Verify model has tags + require.NotEmpty(t, model.Tags, "Model should have at least one tag") + + t.Logf("✓ Successfully inspected model with reference: %s (ID: %s, Tags: %v)", + ref, model.ID[7:19], model.Tags) +} + // createAndPushTestModel creates a minimal test model and pushes it to the local registry. // Returns the model ID, FQDNs for host and network access, and the manifest digest. func createAndPushTestModel(t *testing.T, registryURL, modelRef string, contextSize uint64) (modelID, hostFQDN, networkFQDN, digest string) { @@ -234,7 +260,14 @@ func TestIntegration_PullModel(t *testing.T) { expectedModelName: "test/test-model", }, { - name: "pull by digest with default registry (using default org)", + name: "pull by digest with default registry", + pullRef: fmt.Sprintf("ai/test-model@%s", digest2), + expectedModelID: modelID2, + expectedModelName: "ai/test-model", + }, + + { + name: "pull by digest with default registry and default org", pullRef: fmt.Sprintf("test-model@%s", digest2), expectedModelID: modelID2, expectedModelName: "ai/test-model", @@ -269,13 +302,111 @@ func TestIntegration_PullModel(t *testing.T) { tc.expectedModelName, truncatedID, tc.pullRef) } + // Verify inspect works with the same reference used for pulling + // Determine expected digest based on which model was pulled + expectedDigest := "" + if tc.expectedModelID == modelID1 { + expectedDigest = digest1 + } else if tc.expectedModelID == modelID2 { + expectedDigest = digest2 + } + verifyModelInspect(t, env.client, tc.pullRef, tc.expectedModelID, expectedDigest) + // Clean up: remove the model for the next test iteration // Note: We use the full model ID for removal to ensure we remove the correct model t.Logf("Removing model %s", truncatedID) err = removeModel(env.client, tc.expectedModelID) - if err != nil { - t.Logf("Warning: Failed to remove model %s: %v (continuing anyway)", truncatedID, err) - } + require.NoError(t, err, "Failed to remove model") + }) + } +} + +// TestIntegration_InspectModel tests inspecting a model with various reference formats +// to ensure proper reference normalization and consistent output. +func TestIntegration_InspectModel(t *testing.T) { + env := setupTestEnv(t) + + // Ensure no models exist initially + models, err := listModels(false, env.client, true, false, "") + require.NoError(t, err) + if len(models) != 0 { + t.Fatal("Expected no initial models, but found some") + } + + // Create and push a test model with default org (ai/inspect-test:latest) + modelRef := "ai/inspect-test:latest" + modelID, hostFQDN, networkFQDN, digest := createAndPushTestModel(t, env.registryURL, modelRef, 2048) + t.Logf("Test model pushed: %s (ID: %s) FQDN: %s Digest: %s", hostFQDN, modelID, networkFQDN, digest) + + // Pull the model using a short reference + pullRef := "inspect-test" + t.Logf("Pulling model with reference: %s", pullRef) + err = pullModel(newPullCmd(), env.client, pullRef, true) + require.NoError(t, err, "Failed to pull model") + + // Verify the model was pulled + models, err = listModels(false, env.client, true, false, "") + require.NoError(t, err) + truncatedID := modelID[7:19] + require.Equal(t, truncatedID, strings.TrimSpace(models), "Model not found after pull") + + // Test cases: different ways to reference the same model + testCases := []struct { + name string + ref string + }{ + { + name: "short form (no org, no tag)", + ref: "inspect-test", + }, + { + name: "with tag (no org)", + ref: "inspect-test:latest", + }, + { + name: "with org (no tag)", + ref: "ai/inspect-test", + }, + { + name: "fully qualified (org and tag)", + ref: "ai/inspect-test:latest", + }, + { + name: "with registry (fully qualified)", + ref: "registry.local:5000/ai/inspect-test:latest", + }, + { + name: "with registry (no tag)", + ref: "registry.local:5000/ai/inspect-test", + }, + { + name: "full model ID", + ref: modelID, + }, + { + name: "truncated model ID (12 chars)", + ref: truncatedID, + }, + { + name: "model ID without sha256 prefix", + ref: strings.TrimPrefix(modelID, "sha256:"), + }, + } + + // Verify inspect works with all reference formats + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + verifyModelInspect(t, env.client, tc.ref, modelID, digest) }) } + + // Cleanup: remove the model + t.Logf("Removing model %s", truncatedID) + err = removeModel(env.client, modelID) + require.NoError(t, err, "Failed to remove model") + + // Verify model was removed + models, err = listModels(false, env.client, true, false, "") + require.NoError(t, err) + require.Empty(t, strings.TrimSpace(models), "Model should be removed") } From fc35bcc318fde3fdb6139a8fb61eaa20b8adc4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 17:46:46 +0100 Subject: [PATCH 10/19] add integration tests for model reference formats --- cmd/cli/commands/integration_test.go | 280 +++++++++++++++------------ 1 file changed, 160 insertions(+), 120 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 9fecc7cd0..99f9d9301 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -29,6 +29,79 @@ type testEnv struct { client *desktop.Client } +// modelInfo contains all the information needed to generate model references +type modelInfo struct { + name string // e.g., "test-model" + org string // e.g., "ai" + tag string // e.g., "latest" + registry string // e.g., "registry.local:5000" + modelID string // Full ID: sha256:... + digest string // sha256:... + expectedName string // What we expect to see: "ai/test-model:latest" +} + +// referenceTestCase represents a test case for a specific reference format +type referenceTestCase struct { + name string + ref string +} + +// generateReferenceTestCases generates a comprehensive list of reference formats for testing +func generateReferenceTestCases(info modelInfo) []referenceTestCase { + cases := []referenceTestCase{ + { + name: "short form (no registry, no org, no tag)", + ref: info.name, + }, + { + name: "with tag", + ref: fmt.Sprintf("%s:%s", info.name, info.tag), + }, + { + name: "with org", + ref: fmt.Sprintf("%s/%s", info.org, info.name), + }, + { + name: "with org and tag", + ref: fmt.Sprintf("%s/%s:%s", info.org, info.name, info.tag), + }, + { + name: "fqdn", + ref: fmt.Sprintf("%s/%s/%s:%s", info.registry, info.org, info.name, info.tag), + }, + { + name: "with registry and org", + ref: fmt.Sprintf("%s/%s/%s", info.registry, info.org, info.name), + }, + { + name: "by digest", + ref: fmt.Sprintf("%s@%s", info.name, info.digest), + }, + { + name: "by digest with org", + ref: fmt.Sprintf("%s/%s@%s", info.org, info.name, info.digest), + }, + { + name: "by digest with registry and org", + ref: fmt.Sprintf("%s/%s/%s@%s", info.registry, info.org, info.name, info.digest), + }, + { + name: "full model ID", + ref: info.modelID, + }, + { + name: "truncated model ID (12 chars)", + ref: info.modelID[7:19], + }, + { + name: "model ID without sha256 prefix", + ref: strings.TrimPrefix(info.modelID, "sha256:"), + }, + } + + return cases +} + // setupTestEnv creates the complete test environment with registry and DMR func setupTestEnv(t *testing.T) *testEnv { ctx := context.Background() @@ -115,12 +188,10 @@ func verifyModelInspect(t *testing.T, client *desktop.Client, ref, expectedID, e "Model ID mismatch when inspecting with reference: %s. Expected: %s, Got: %s", ref, expectedID, model.ID) - // Verify digest matches if provided - if expectedDigest != "" { - require.Equal(t, expectedDigest, model.ID, - "Model digest mismatch when inspecting with reference: %s. Expected: %s, Got: %s", - ref, expectedDigest, model.ID) - } + // Verify digest matches + require.Equal(t, expectedDigest, model.ID, + "Model digest mismatch when inspecting with reference: %s. Expected: %s, Got: %s", + ref, expectedDigest, model.ID) // Verify model has tags require.NotEmpty(t, model.Tags, "Model should have at least one tag") @@ -205,88 +276,96 @@ func TestIntegration_PullModel(t *testing.T) { modelID1, hostFQDN1, networkFQDN1, digest1 := createAndPushTestModel(t, env.registryURL, modelRef1, 2048) t.Logf("Test model 1 pushed: %s (ID: %s) FQDN: %s Digest: %s", hostFQDN1, modelID1, networkFQDN1, digest1) + // Generate test cases for custom org model (test/test-model) + customOrgInfo := modelInfo{ + name: "test-model", + org: "test", + tag: "latest", + registry: "registry.local:5000", + modelID: modelID1, + digest: digest1, + expectedName: "test/test-model:latest", + } + customOrgCases := generateReferenceTestCases(customOrgInfo) + // Model 2: default org (ai/test-model:latest) modelRef2 := "ai/test-model:latest" modelID2, hostFQDN2, networkFQDN2, digest2 := createAndPushTestModel(t, env.registryURL, modelRef2, 2048) t.Logf("Test model 2 pushed: %s (ID: %s) FQDN: %s Digest: %s", hostFQDN2, modelID2, networkFQDN2, digest2) - // Test cases for different model reference formats - testCases := []struct { - name string - pullRef string // Reference to use when pulling - expectedModelID string // Expected model ID after pull - expectedModelName string // Expected model name for logging - }{ - { - name: "explicit custom org and tag", - pullRef: "registry.local:5000/test/test-model:latest", - expectedModelID: modelID1, - expectedModelName: "test/test-model:latest", - }, - { - name: "custom org without tag (should default to :latest)", - pullRef: "registry.local:5000/test/test-model", + // Generate test cases for default org model (ai/test-model) + defaultOrgInfo := modelInfo{ + name: "test-model", + org: "ai", + tag: "latest", + registry: "registry.local:5000", + modelID: modelID2, + digest: digest2, + expectedName: "ai/test-model:latest", + } + defaultOrgCases := generateReferenceTestCases(defaultOrgInfo) + + // Combine test cases with expected model IDs + // References without explicit org should resolve to ai/ (default org) + // References with explicit "test" org should resolve to test/test-model + type pullTestCase struct { + referenceTestCase + expectedModelID string + expectedModelName string + expectedDigest string + } + + var testCases []pullTestCase + + // Add custom org cases (with explicit "test" org in reference) + // Only include cases where the reference explicitly contains the "test" org + // Cases without explicit org will normalize to "ai/" (default org) + for _, tc := range customOrgCases { + // Skip ID-based references for pull tests (can't pull by ID) + if strings.Contains(tc.name, "model ID") { + continue + } + // Skip cases that don't have explicit org in the reference + // These will normalize to the default org (ai/) and should only be in defaultOrgCases + if !strings.Contains(tc.ref, "test/") && !strings.Contains(tc.ref, "registry.local:5000/test/") { + continue + } + testCases = append(testCases, pullTestCase{ + referenceTestCase: tc, expectedModelID: modelID1, expectedModelName: "test/test-model:latest", - }, - { - name: "explicit default org and tag", - pullRef: "registry.local:5000/ai/test-model:latest", - expectedModelID: modelID2, - expectedModelName: "ai/test-model:latest", - }, - { - name: "default org without tag (should default to :latest)", - pullRef: "registry.local:5000/ai/test-model", - expectedModelID: modelID2, - expectedModelName: "ai/test-model:latest", - }, - { - name: "no org with tag (should default to ai/)", - pullRef: "test-model:latest", - expectedModelID: modelID2, - expectedModelName: "ai/test-model:latest", - }, - { - name: "no org and no tag (should default to ai/:latest)", - pullRef: "test-model", - expectedModelID: modelID2, - expectedModelName: "ai/test-model:latest", - }, - { - name: "pull by digest with full registry path", - pullRef: fmt.Sprintf("registry.local:5000/test/test-model@%s", digest1), - expectedModelID: modelID1, - expectedModelName: "test/test-model", - }, - { - name: "pull by digest with default registry", - pullRef: fmt.Sprintf("ai/test-model@%s", digest2), - expectedModelID: modelID2, - expectedModelName: "ai/test-model", - }, + expectedDigest: digest1, + }) + } - { - name: "pull by digest with default registry and default org", - pullRef: fmt.Sprintf("test-model@%s", digest2), + // Add default org cases (references that should default to ai/) + for _, tc := range defaultOrgCases { + // Skip ID-based references for pull tests (can't pull by ID) + if strings.Contains(tc.name, "model ID") { + continue + } + testCases = append(testCases, pullTestCase{ + referenceTestCase: tc, expectedModelID: modelID2, - expectedModelName: "ai/test-model", - }, + expectedModelName: "ai/test-model:latest", + expectedDigest: digest2, + }) } + // Run all test cases for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Pull the model using the test case reference - t.Logf("Pulling model with reference: %s", tc.pullRef) - err := pullModel(newPullCmd(), env.client, tc.pullRef, true) - require.NoError(t, err, "Failed to pull model with reference: %s", tc.pullRef) + t.Logf("Pulling model with reference: %s", tc.ref) + err := pullModel(newPullCmd(), env.client, tc.ref, true) + require.NoError(t, err, "Failed to pull model with reference: %s", tc.ref) // List models and verify the expected model is present models, err := listModels(false, env.client, true, false, "") require.NoError(t, err) if len(models) == 0 { - t.Fatalf("No models found after pulling %s", tc.pullRef) + t.Fatalf("No models found after pulling %s", tc.ref) } models = strings.TrimSpace(models) @@ -296,24 +375,16 @@ func TestIntegration_PullModel(t *testing.T) { truncatedID := tc.expectedModelID[7:19] if models != truncatedID { t.Errorf("Expected model ID %s (truncated: %s) not found in model list after pulling %s.\nExpected model: %s\nModel list:\n%s", - tc.expectedModelID, truncatedID, tc.pullRef, tc.expectedModelName, models) + tc.expectedModelID, truncatedID, tc.ref, tc.expectedModelName, models) } else { t.Logf("✓ Successfully verified model %s (ID: %s) after pulling with reference: %s", - tc.expectedModelName, truncatedID, tc.pullRef) + tc.expectedModelName, truncatedID, tc.ref) } // Verify inspect works with the same reference used for pulling - // Determine expected digest based on which model was pulled - expectedDigest := "" - if tc.expectedModelID == modelID1 { - expectedDigest = digest1 - } else if tc.expectedModelID == modelID2 { - expectedDigest = digest2 - } - verifyModelInspect(t, env.client, tc.pullRef, tc.expectedModelID, expectedDigest) + verifyModelInspect(t, env.client, tc.ref, tc.expectedModelID, tc.expectedDigest) // Clean up: remove the model for the next test iteration - // Note: We use the full model ID for removal to ensure we remove the correct model t.Logf("Removing model %s", truncatedID) err = removeModel(env.client, tc.expectedModelID) require.NoError(t, err, "Failed to remove model") @@ -350,48 +421,17 @@ func TestIntegration_InspectModel(t *testing.T) { truncatedID := modelID[7:19] require.Equal(t, truncatedID, strings.TrimSpace(models), "Model not found after pull") - // Test cases: different ways to reference the same model - testCases := []struct { - name string - ref string - }{ - { - name: "short form (no org, no tag)", - ref: "inspect-test", - }, - { - name: "with tag (no org)", - ref: "inspect-test:latest", - }, - { - name: "with org (no tag)", - ref: "ai/inspect-test", - }, - { - name: "fully qualified (org and tag)", - ref: "ai/inspect-test:latest", - }, - { - name: "with registry (fully qualified)", - ref: "registry.local:5000/ai/inspect-test:latest", - }, - { - name: "with registry (no tag)", - ref: "registry.local:5000/ai/inspect-test", - }, - { - name: "full model ID", - ref: modelID, - }, - { - name: "truncated model ID (12 chars)", - ref: truncatedID, - }, - { - name: "model ID without sha256 prefix", - ref: strings.TrimPrefix(modelID, "sha256:"), - }, + // Generate all reference test cases using the unified system + info := modelInfo{ + name: "inspect-test", + org: "ai", + tag: "latest", + registry: "registry.local:5000", + modelID: modelID, + digest: digest, + expectedName: "ai/inspect-test:latest", } + testCases := generateReferenceTestCases(info) // Verify inspect works with all reference formats for _, tc := range testCases { From 3dc6dc098f6ac4abe3c934c4f1dca84d8d9d221c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 20:08:05 +0100 Subject: [PATCH 11/19] fix(inspect): enhance model ID expansion logic to exclude tagged and digest references --- cmd/cli/desktop/desktop.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/cli/desktop/desktop.go b/cmd/cli/desktop/desktop.go index 0c2e08d9f..fc8ac8fa3 100644 --- a/cmd/cli/desktop/desktop.go +++ b/cmd/cli/desktop/desktop.go @@ -257,7 +257,13 @@ func (c *Client) ListOpenAI() (dmrm.OpenAIModelList, error) { func (c *Client) Inspect(model string, remote bool) (dmrm.Model, error) { model = normalizeHuggingFaceModelName(model) if model != "" { - if !strings.Contains(strings.Trim(model, "/"), "/") { + // Only try to expand to model ID if the reference doesn't contain: + // - A slash (org/name format) + // - A colon (tagged reference like name:tag) + // - An @ symbol (digest reference like name@sha256:...) + if !strings.Contains(strings.Trim(model, "/"), "/") && + !strings.Contains(model, ":") && + !strings.Contains(model, "@") { // Do an extra API call to check if the model parameter isn't a model ID. modelId, err := c.fullModelID(model) if err != nil { From dc3c86c5aaf70c5fb28a254bc0889a61ba4495a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Mon, 3 Nov 2025 20:18:22 +0100 Subject: [PATCH 12/19] Temporary add trigger on push to let Github know about this workflow --- .github/workflows/integration-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 235cc6b35..5373b5402 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -2,6 +2,7 @@ name: Integration Tests on: workflow_dispatch: # Manual trigger only + push: jobs: integration-test: From 8ef78a3952d6354ea6f38baa01ec09cd52bebc0c Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Tue, 4 Nov 2025 12:21:48 +0200 Subject: [PATCH 13/19] tests: build docker/model-runner locally Signed-off-by: Dorin Geman --- Makefile | 5 ++++- cmd/cli/commands/integration_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d34b771af..6c7d7bc7a 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,9 @@ SOURCE ?= TAG ?= LICENSE ?= +# Test configuration +BUILD_DMR ?= 1 + # Main targets .PHONY: build run clean test integration-tests docker-build docker-build-multiplatform docker-run docker-build-vllm docker-run-vllm docker-run-impl help validate model-distribution-tool # Default target @@ -63,7 +66,7 @@ test: integration-tests: @echo "Running integration tests..." @echo "Note: This requires Docker to be running" - @go test -v -race -tags=integration -timeout=5m . + @BUILD_DMR=$(BUILD_DMR) go test -v -race -count=1 -tags=integration -timeout=5m ./cmd/cli/commands/ @echo "Integration tests completed!" validate: diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 99f9d9301..a5d32f496 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -8,6 +8,7 @@ import ( "io" "net/url" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -148,6 +149,13 @@ func ociRegistry(t *testing.T, ctx context.Context, net *testcontainers.DockerNe } func dockerModelRunner(t *testing.T, ctx context.Context, net *testcontainers.DockerNetwork) string { + if os.Getenv("BUILD_DMR") == "1" { + t.Log("Building DMR container...") + out, err := exec.CommandContext(ctx, "make", "-C", "../../..", "docker-build").CombinedOutput() + if err != nil { + t.Fatalf("Failed to build DMR container: %v\n%s", err, out) + } + } t.Log("Starting DMR container...") ctr, err := testcontainers.Run( ctx, "docker/model-runner:latest", From 5f42d7094bb9601e839e40958af1ea9ccc510568 Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Tue, 4 Nov 2025 12:54:58 +0200 Subject: [PATCH 14/19] tests: only run "^TestIntegration" Signed-off-by: Dorin Geman --- Makefile | 2 +- cmd/cli/commands/integration_test.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6c7d7bc7a..02cfb5a44 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ test: integration-tests: @echo "Running integration tests..." @echo "Note: This requires Docker to be running" - @BUILD_DMR=$(BUILD_DMR) go test -v -race -count=1 -tags=integration -timeout=5m ./cmd/cli/commands/ + @BUILD_DMR=$(BUILD_DMR) go test -v -race -count=1 -run "^TestIntegration" -timeout=5m ./cmd/cli/commands @echo "Integration tests completed!" validate: diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index a5d32f496..d0dc8ed83 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -1,5 +1,3 @@ -//go:build integration - package commands import ( From 080bb686fc6fe2e2ccf8698a92fc9340bfa172d8 Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Tue, 4 Nov 2025 13:07:32 +0200 Subject: [PATCH 15/19] tests: add check for invalid tests Signed-off-by: Dorin Geman --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makefile b/Makefile index 02cfb5a44..998680dcb 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,13 @@ test: integration-tests: @echo "Running integration tests..." @echo "Note: This requires Docker to be running" + @echo "Checking test naming conventions..." + @INVALID_TESTS=$$(grep "^func Test" cmd/cli/commands/integration_test.go | grep -v "^func TestIntegration"); \ + if [ -n "$$INVALID_TESTS" ]; then \ + echo "Error: Found test functions that don't start with 'TestIntegration':"; \ + echo "$$INVALID_TESTS" | sed 's/func \([^(]*\).*/\1/'; \ + exit 1; \ + fi @BUILD_DMR=$(BUILD_DMR) go test -v -race -count=1 -run "^TestIntegration" -timeout=5m ./cmd/cli/commands @echo "Integration tests completed!" From 61324ba979d64f5c903ceb14a3c0144be735bde5 Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Tue, 4 Nov 2025 13:14:31 +0200 Subject: [PATCH 16/19] tests: always pull the image if it's not build locally Signed-off-by: Dorin Geman --- cmd/cli/commands/integration_test.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index d0dc8ed83..9071e6a61 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -147,23 +147,29 @@ func ociRegistry(t *testing.T, ctx context.Context, net *testcontainers.DockerNe } func dockerModelRunner(t *testing.T, ctx context.Context, net *testcontainers.DockerNetwork) string { + containerCustomizerOpts := []testcontainers.ContainerCustomizer{ + testcontainers.WithExposedPorts("12434/tcp"), + testcontainers.WithWaitStrategy(wait.ForHTTP("/engines/status").WithPort("12434/tcp").WithStartupTimeout(10 * time.Second)), + testcontainers.WithEnv(map[string]string{ + "DEFAULT_REGISTRY": "registry.local:5000", + "INSECURE_REGISTRY": "true", + }), + network.WithNetwork([]string{"dmr"}, net), + } if os.Getenv("BUILD_DMR") == "1" { t.Log("Building DMR container...") out, err := exec.CommandContext(ctx, "make", "-C", "../../..", "docker-build").CombinedOutput() if err != nil { t.Fatalf("Failed to build DMR container: %v\n%s", err, out) } + } else { + // Always pull the image if it's not build locally. + containerCustomizerOpts = append(containerCustomizerOpts, testcontainers.WithAlwaysPull()) } t.Log("Starting DMR container...") ctr, err := testcontainers.Run( ctx, "docker/model-runner:latest", - testcontainers.WithExposedPorts("12434/tcp"), - testcontainers.WithWaitStrategy(wait.ForHTTP("/engines/status").WithPort("12434/tcp").WithStartupTimeout(10*time.Second)), - testcontainers.WithEnv(map[string]string{ - "DEFAULT_REGISTRY": "registry.local:5000", - "INSECURE_REGISTRY": "true", - }), - network.WithNetwork([]string{"dmr"}, net), + containerCustomizerOpts..., ) require.NoError(t, err) testcontainers.CleanupContainer(t, ctr) From 2b73996c86c52be2c65332b366b510ec7780a4eb Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Tue, 4 Nov 2025 13:18:57 +0200 Subject: [PATCH 17/19] ci: simplify integration test workflow to use Makefile Signed-off-by: Dorin Geman --- .github/workflows/integration-test.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 5373b5402..b0ab2f6a8 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -7,8 +7,8 @@ on: jobs: integration-test: runs-on: ubuntu-latest - timeout-minutes: 5 - + timeout-minutes: 10 + steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 @@ -18,20 +18,9 @@ jobs: with: go-version: 1.24.2 cache: true - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 - - - name: Build Docker image - run: make docker-build - env: - DOCKER_BUILDKIT: 1 - - - name: Verify Docker image - run: docker images | grep docker/model-runner - + - name: Run integration tests - working-directory: cmd/cli/commands - run: go test -v -tags=integration -timeout=5m -run TestIntegration_PullModel - env: - CGO_ENABLED: 1 + run: make integration-tests From 46d041c0ba680a5b7da96b9c2e42f2b8739e6391 Mon Sep 17 00:00:00 2001 From: Dorin Geman Date: Tue, 4 Nov 2025 13:25:11 +0200 Subject: [PATCH 18/19] fix(test): bring back build tag to exclude integration tests from unit test runs Signed-off-by: Dorin Geman --- Makefile | 2 +- cmd/cli/commands/integration_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 998680dcb..1204ccbff 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ integration-tests: echo "$$INVALID_TESTS" | sed 's/func \([^(]*\).*/\1/'; \ exit 1; \ fi - @BUILD_DMR=$(BUILD_DMR) go test -v -race -count=1 -run "^TestIntegration" -timeout=5m ./cmd/cli/commands + @BUILD_DMR=$(BUILD_DMR) go test -v -race -count=1 -tags=integration -run "^TestIntegration" -timeout=5m ./cmd/cli/commands @echo "Integration tests completed!" validate: diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 9071e6a61..3a4b42ecc 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -1,3 +1,5 @@ +//go:build integration + package commands import ( From 70ff06bf67cd9c788988e787491c31f90fccefb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Tue, 4 Nov 2025 13:58:27 +0100 Subject: [PATCH 19/19] chore: use tc module for the registry --- cmd/cli/commands/integration_test.go | 6 ++---- cmd/cli/go.mod | 1 + cmd/cli/go.sum | 2 ++ go.work.sum | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/cli/commands/integration_test.go b/cmd/cli/commands/integration_test.go index 3a4b42ecc..8b7e8c874 100644 --- a/cmd/cli/commands/integration_test.go +++ b/cmd/cli/commands/integration_test.go @@ -19,6 +19,7 @@ import ( "github.com/docker/model-runner/pkg/distribution/registry" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" + tc "github.com/testcontainers/testcontainers-go/modules/registry" "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" ) @@ -132,10 +133,7 @@ func setupTestEnv(t *testing.T) *testEnv { func ociRegistry(t *testing.T, ctx context.Context, net *testcontainers.DockerNetwork) string { t.Log("Starting OCI registry container...") - ctr, err := testcontainers.Run( - ctx, "registry:3", - testcontainers.WithExposedPorts("5000/tcp"), - testcontainers.WithWaitStrategy(wait.ForHTTP("/v2/").WithPort("5000/tcp").WithStartupTimeout(30*time.Second)), + ctr, err := tc.Run(context.Background(), "registry:3", network.WithNetwork([]string{"registry.local"}, net), ) require.NoError(t, err) diff --git a/cmd/cli/go.mod b/cmd/cli/go.mod index a57fdd9b4..0f06bc227 100644 --- a/cmd/cli/go.mod +++ b/cmd/cli/go.mod @@ -24,6 +24,7 @@ require ( github.com/spf13/pflag v1.0.9 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.39.0 + github.com/testcontainers/testcontainers-go/modules/registry v0.39.0 go.opentelemetry.io/otel v1.37.0 go.uber.org/mock v0.5.0 golang.org/x/sync v0.17.0 diff --git a/cmd/cli/go.sum b/cmd/cli/go.sum index a78ecf4df..e64bd9d5f 100644 --- a/cmd/cli/go.sum +++ b/cmd/cli/go.sum @@ -334,6 +334,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= +github.com/testcontainers/testcontainers-go/modules/registry v0.39.0 h1:Wq08A4G5o/OYb68xWVzVWSHrpckpYab4+5u+8T5UaYQ= +github.com/testcontainers/testcontainers-go/modules/registry v0.39.0/go.mod h1:RIRXImSUJ5MYAiM8Hl39JdMD6pHsRsgfhgR+L22dhMk= github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0= github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= diff --git a/go.work.sum b/go.work.sum index 9ca7063c5..497ba02fe 100644 --- a/go.work.sum +++ b/go.work.sum @@ -133,6 +133,7 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=