Skip to content

Commit 0843b41

Browse files
committed
test: add integration tests for CNCF format packaging and diff ID extraction
1 parent d1e04b1 commit 0843b41

File tree

4 files changed

+233
-1
lines changed

4 files changed

+233
-1
lines changed

cmd/cli/commands/integration_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,34 @@ func TestIntegration_PackageModel(t *testing.T) {
11461146
require.NoError(t, err, "Failed to remove model")
11471147
})
11481148

1149+
// Test case 4: Package with CNCF format
1150+
t.Run("package GGUF with CNCF format", func(t *testing.T) {
1151+
targetTag := "ai/packaged-cncf:latest"
1152+
1153+
// Create package options with CNCF format
1154+
opts := packageOptions{
1155+
ggufPath: absPath,
1156+
tag: targetTag,
1157+
format: "cncf",
1158+
}
1159+
1160+
// Execute the package command using the helper function with test client
1161+
t.Logf("Packaging GGUF file as CNCF format %s", targetTag)
1162+
err := packageModel(env.ctx, newPackagedCmd(), env.client, opts)
1163+
require.NoError(t, err, "Failed to package GGUF model with CNCF format")
1164+
1165+
// Verify the model was loaded and tagged
1166+
model, err := env.client.Inspect(targetTag, false)
1167+
require.NoError(t, err, "Failed to inspect CNCF packaged model")
1168+
require.Contains(t, model.Tags, normalizeRef(t, targetTag), "Model should have the expected tag")
1169+
1170+
t.Logf("✓ Successfully packaged model with CNCF format: %s (ID: %s)", targetTag, model.ID[7:19])
1171+
1172+
// Cleanup
1173+
err = removeModel(env.client, model.ID, true)
1174+
require.NoError(t, err, "Failed to remove model")
1175+
})
1176+
11491177
// Verify all models are cleaned up
11501178
models, err = listModels(false, env.client, true, false, "")
11511179
require.NoError(t, err)

pkg/distribution/builder/from_directory.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ func WithCreatedTime(t time.Time) DirectoryOption {
6868
}
6969

7070
// WithOutputFormat sets the output artifact format for the directory builder.
71+
// Defaults to BuildFormatDocker if not specified.
72+
// This is the DirectoryOption equivalent of WithFormat (BuildOption).
7173
func WithOutputFormat(f BuildFormat) DirectoryOption {
7274
return func(opts *DirectoryOptions) {
7375
opts.Format = f

pkg/distribution/modelpack/convert.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ func ClassifyLayer(dockerMT oci.MediaType, path string) LayerKind {
5454
return classifyByPath(path)
5555
}
5656

57-
// Safe default: treat as weight config.
57+
// Default: treat unknown media types (without filepath hints) as weight
58+
// config. This is intentional for the directory-based packaging flow
59+
// where ambiguous files (tokenizer.json, config.json, etc.) are common
60+
// and typically carry configuration rather than model weights. All known
61+
// weight media types — both Docker (MediaTypeGGUF, MediaTypeSafetensors,
62+
// etc.) and CNCF (MediaTypeWeightRaw, etc.) — are handled explicitly in
63+
// the switch above, so this fallback only triggers for truly unrecognized
64+
// media types.
5865
return KindWeightConfig
5966
}
6067

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package remote
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/docker/model-runner/pkg/distribution/oci"
8+
)
9+
10+
// Valid 64-char hex strings for SHA256 test hashes.
11+
const (
12+
hexA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
13+
hexB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
14+
hexC = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
15+
hex1 = "1111111111111111111111111111111111111111111111111111111111111111"
16+
hex2 = "2222222222222222222222222222222222222222222222222222222222222222"
17+
)
18+
19+
func TestExtractDiffIDs_DockerFormat(t *testing.T) {
20+
config := map[string]interface{}{
21+
"rootfs": map[string]interface{}{
22+
"type": "rootfs",
23+
"diff_ids": []string{"sha256:" + hexA, "sha256:" + hexB, "sha256:" + hexC},
24+
},
25+
}
26+
raw, err := json.Marshal(config)
27+
if err != nil {
28+
t.Fatalf("marshal config: %v", err)
29+
}
30+
31+
tests := []struct {
32+
name string
33+
index int
34+
wantHex string
35+
wantOk bool
36+
}{
37+
{"first layer", 0, hexA, true},
38+
{"second layer", 1, hexB, true},
39+
{"last layer", 2, hexC, true},
40+
{"index out of bounds", 3, "", false},
41+
{"negative index", -1, "", false},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
h, err := extractDiffIDs(raw, tt.index)
47+
if err != nil {
48+
t.Fatalf("unexpected error: %v", err)
49+
}
50+
if tt.wantOk {
51+
if h == (oci.Hash{}) {
52+
t.Fatal("expected non-zero hash, got zero")
53+
}
54+
if h.Hex != tt.wantHex {
55+
t.Errorf("expected hex %q, got %q", tt.wantHex, h.Hex)
56+
}
57+
} else {
58+
if h != (oci.Hash{}) {
59+
t.Errorf("expected zero hash, got %v", h)
60+
}
61+
}
62+
})
63+
}
64+
}
65+
66+
func TestExtractDiffIDs_CNCFModelPackFormat(t *testing.T) {
67+
config := map[string]interface{}{
68+
"modelfs": map[string]interface{}{
69+
"type": "layers",
70+
"diffIds": []string{"sha256:" + hex1, "sha256:" + hex2},
71+
},
72+
}
73+
raw, err := json.Marshal(config)
74+
if err != nil {
75+
t.Fatalf("marshal config: %v", err)
76+
}
77+
78+
tests := []struct {
79+
name string
80+
index int
81+
wantHex string
82+
wantOk bool
83+
}{
84+
{"first layer", 0, hex1, true},
85+
{"second layer", 1, hex2, true},
86+
{"index out of bounds", 2, "", false},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
h, err := extractDiffIDs(raw, tt.index)
92+
if err != nil {
93+
t.Fatalf("unexpected error: %v", err)
94+
}
95+
if tt.wantOk {
96+
if h == (oci.Hash{}) {
97+
t.Fatal("expected non-zero hash, got zero")
98+
}
99+
if h.Hex != tt.wantHex {
100+
t.Errorf("expected hex %q, got %q", tt.wantHex, h.Hex)
101+
}
102+
} else {
103+
if h != (oci.Hash{}) {
104+
t.Errorf("expected zero hash, got %v", h)
105+
}
106+
}
107+
})
108+
}
109+
}
110+
111+
func TestExtractDiffIDs_DockerTakesPrecedence(t *testing.T) {
112+
// When both rootfs and modelfs are present, Docker format should win.
113+
config := map[string]interface{}{
114+
"rootfs": map[string]interface{}{
115+
"type": "rootfs",
116+
"diff_ids": []string{"sha256:" + hexA},
117+
},
118+
"modelfs": map[string]interface{}{
119+
"type": "layers",
120+
"diffIds": []string{"sha256:" + hex1},
121+
},
122+
}
123+
raw, err := json.Marshal(config)
124+
if err != nil {
125+
t.Fatalf("marshal config: %v", err)
126+
}
127+
128+
h, err := extractDiffIDs(raw, 0)
129+
if err != nil {
130+
t.Fatalf("unexpected error: %v", err)
131+
}
132+
if h.Hex != hexA {
133+
t.Errorf("expected Docker format to take precedence (hex %q), got %q", hexA, h.Hex)
134+
}
135+
}
136+
137+
func TestExtractDiffIDs_EmptyConfig(t *testing.T) {
138+
raw := []byte(`{}`)
139+
h, err := extractDiffIDs(raw, 0)
140+
if err != nil {
141+
t.Fatalf("unexpected error: %v", err)
142+
}
143+
if h != (oci.Hash{}) {
144+
t.Errorf("expected zero hash for empty config, got %v", h)
145+
}
146+
}
147+
148+
func TestExtractDiffIDs_InvalidJSON(t *testing.T) {
149+
raw := []byte(`not valid json`)
150+
_, err := extractDiffIDs(raw, 0)
151+
if err == nil {
152+
t.Fatal("expected error for invalid JSON, got nil")
153+
}
154+
}
155+
156+
func TestExtractDiffIDs_MalformedRootFS(t *testing.T) {
157+
// rootfs exists but is not an object — should fall through gracefully.
158+
config := map[string]interface{}{
159+
"rootfs": "not an object",
160+
}
161+
raw, err := json.Marshal(config)
162+
if err != nil {
163+
t.Fatalf("marshal config: %v", err)
164+
}
165+
166+
h, err := extractDiffIDs(raw, 0)
167+
if err != nil {
168+
t.Fatalf("unexpected error: %v", err)
169+
}
170+
if h != (oci.Hash{}) {
171+
t.Errorf("expected zero hash for malformed rootfs, got %v", h)
172+
}
173+
}
174+
175+
func TestExtractDiffIDs_MalformedModelFS(t *testing.T) {
176+
// modelfs exists but diffIds contains invalid hashes (not valid SHA256).
177+
config := map[string]interface{}{
178+
"modelfs": map[string]interface{}{
179+
"type": "layers",
180+
"diffIds": []string{"not-a-valid-hash"},
181+
},
182+
}
183+
raw, err := json.Marshal(config)
184+
if err != nil {
185+
t.Fatalf("marshal config: %v", err)
186+
}
187+
188+
h, err := extractDiffIDs(raw, 0)
189+
if err != nil {
190+
t.Fatalf("unexpected error: %v", err)
191+
}
192+
if h != (oci.Hash{}) {
193+
t.Errorf("expected zero hash for malformed modelfs hash, got %v", h)
194+
}
195+
}

0 commit comments

Comments
 (0)