Files
syft/syft/pkg/cataloger/ai/processor_test.go
T
Alex Goodman a4d3aedf4e fix(ai): GGUF layer handling and safetensors parameter counts (#5343)
* fix(ai): keep merging GGUF layers when one fails to parse

a single unparseable layer in an OCI GGUF artifact skipped the merge, so the
named model lost its parts and nameless layers were dropped. The error is
still returned.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(ai): only send GGUF layers to the GGUF cataloger

the media type glob `application/vnd.docker.ai*` also matched config,
model.file, license and safetensors layers, so every safetensors OCI model
reported spurious "invalid GGUF magic number" unknowns. Match
`application/vnd.docker.ai.gguf.v3` exactly.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(ai): don't wrap safetensors parameter counts

- tensors whose shape product overflows uint64 are skipped instead of wrapping
- per-file and per-shard parameter totals saturate instead of wrapping
- parameter count and dominant dtype are computed in one pass
- GGUF licenses are created with the scan context, and drop no-op unknown checks

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* refactor(ai): derive safetensors model info from the header in one place

the file and OCI layer parsers built identical metadata from a parsed header.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(ai): keep the GGUF media type with the GGUF cataloger

also corrects stale comments in the GGUF merge processor.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-09-29 09:40:17 -04:00

82 lines
2.3 KiB
Go

package ai
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
)
func Test_ggufMergeProcessor(t *testing.T) {
tests := []struct {
name string
pkgs []pkg.Package
wantPkgCount int
wantFilePartCount int
}{
{
name: "single named package merges nameless headers",
pkgs: []pkg.Package{
{Name: "model", Metadata: pkg.GGUFFileHeader{MetadataKeyValuesHash: "abc"}},
{Name: "", Metadata: pkg.GGUFFileHeader{MetadataKeyValuesHash: "part1"}},
{Name: "", Metadata: pkg.GGUFFileHeader{MetadataKeyValuesHash: "part2"}},
},
wantPkgCount: 1,
wantFilePartCount: 2,
},
{
name: "multiple named packages returns all without merging",
pkgs: []pkg.Package{
{Name: "model1", Metadata: pkg.GGUFFileHeader{}},
{Name: "model2", Metadata: pkg.GGUFFileHeader{}},
{Name: "", Metadata: pkg.GGUFFileHeader{}},
},
wantPkgCount: 2,
wantFilePartCount: 0,
},
{
name: "no named packages returns empty result",
pkgs: []pkg.Package{
{Name: "", Metadata: pkg.GGUFFileHeader{}},
{Name: "", Metadata: pkg.GGUFFileHeader{}},
},
wantPkgCount: 0,
wantFilePartCount: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, _, err := ggufMergeProcessor(test.pkgs, nil, nil)
require.NoError(t, err)
assert.Len(t, got, test.wantPkgCount)
if test.wantPkgCount == 1 && test.wantFilePartCount > 0 {
header, ok := got[0].Metadata.(pkg.GGUFFileHeader)
require.True(t, ok)
assert.Len(t, header.Parts, test.wantFilePartCount)
}
})
}
}
func Test_ggufMergeProcessor_mergesDespiteError(t *testing.T) {
// one layer failing to parse must not drop the rest of the model
parseErr := errors.New("one layer failed to parse")
pkgs := []pkg.Package{
{Name: "model", Metadata: pkg.GGUFFileHeader{MetadataKeyValuesHash: "abc"}},
{Name: "", Metadata: pkg.GGUFFileHeader{MetadataKeyValuesHash: "part1"}},
}
got, _, err := ggufMergeProcessor(pkgs, nil, parseErr)
require.ErrorIs(t, err, parseErr)
require.Len(t, got, 1)
assert.Equal(t, "model", got[0].Name)
header, ok := got[0].Metadata.(pkg.GGUFFileHeader)
require.True(t, ok)
assert.Len(t, header.Parts, 1)
}