Skip to content

Commit b3f9860

Browse files
committed
chore(agents): fix version normalization for lockfiles
1 parent 05ff6d8 commit b3f9860

2 files changed

Lines changed: 109 additions & 40 deletions

File tree

pkg/agentfs/sdk_version_check.go

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ func isVersionSatisfied(version, minVersion string, sourceType SourceType) (bool
660660
case SourceTypeLock:
661661
// For lock files, we have the exact version that was installed
662662
// Check if this exact version is >= the minimum version
663-
normalizedVersion := normalizeVersion(version)
663+
normalizedVersion := normalizeVersion(version, sourceType)
664664
v, err := semver.NewVersion(normalizedVersion)
665665
if err != nil {
666666
return false, fmt.Errorf("invalid version format: %s", version)
@@ -687,37 +687,62 @@ func isVersionSatisfied(version, minVersion string, sourceType SourceType) (bool
687687
return false, fmt.Errorf("invalid minimum version format: %s", minVersion)
688688
}
689689

690-
// Check if the package constraint would allow installing a version >= minimum
691-
// We do this by checking if there exists a version >= minimum that satisfies the package constraint
690+
// The key insight: we need to check if the package constraint allows installing
691+
// ANY version that is >= the minimum version, not if the minimum version itself
692+
// satisfies the package constraint.
693+
694+
// First, check if the minimum version itself satisfies the package constraint
692695
if packageConstraint.Check(min) {
693696
// The minimum version satisfies the package constraint, so it would be installable
694697
return true, nil
695698
}
696699

700+
// If the minimum version doesn't satisfy the package constraint, we need to check
701+
// if the package constraint allows installing a version >= minimum.
702+
// This handles cases like "^0.7.9" vs minimum "0.0.7" where the constraint
703+
// requires >=0.7.9 but would still satisfy the minimum requirement of 0.0.7
704+
// because 0.7.9 > 0.0.7.
705+
697706
// Check if the package constraint allows any version >= minimum
698-
// This handles cases like ">=1.5.0" where 1.0.0 doesn't satisfy it, but it would install 1.5.0+ which > 1.0.0
699-
// We'll test a few strategic versions to see if any satisfy the package constraint and are >= minimum
707+
// We'll test strategic versions to see if any satisfy both conditions
700708
testVersions := []string{
701-
minVersion, // The minimum version itself
702-
fmt.Sprintf("%d.%d.%d", min.Major()+1, 0, 0),
703-
fmt.Sprintf("%d.%d.%d", min.Major(), min.Minor()+1, 0),
709+
// Test the minimum version itself (already checked above, but included for clarity)
710+
minVersion,
711+
// Test versions that are >= minimum and might satisfy the package constraint
704712
fmt.Sprintf("%d.%d.%d", min.Major(), min.Minor(), min.Patch()+1),
713+
fmt.Sprintf("%d.%d.%d", min.Major(), min.Minor()+1, 0),
714+
fmt.Sprintf("%d.%d.%d", min.Major(), min.Minor()+2, 0),
715+
fmt.Sprintf("%d.%d.%d", min.Major(), min.Minor()+5, 0),
716+
fmt.Sprintf("%d.%d.%d", min.Major(), min.Minor()+10, 0),
717+
fmt.Sprintf("%d.%d.%d", min.Major()+1, 0, 0),
718+
fmt.Sprintf("%d.%d.%d", min.Major()+2, 0, 0),
705719
}
706720

707-
// Add more versions to cover edge cases
721+
// Add more specific versions that might be common in constraints
708722
if min.Major() > 0 {
709723
testVersions = append(testVersions, fmt.Sprintf("%d.0.0", min.Major()))
710724
}
711725
if min.Minor() > 0 {
712726
testVersions = append(testVersions, fmt.Sprintf("%d.%d.0", min.Major(), min.Minor()))
713727
}
714728

715-
// Add some specific versions that might be common in constraints
716-
testVersions = append(testVersions,
717-
fmt.Sprintf("%d.%d.0", min.Major(), min.Minor()+2),
718-
fmt.Sprintf("%d.%d.0", min.Major(), min.Minor()+5),
719-
fmt.Sprintf("%d.%d.0", min.Major(), min.Minor()+10),
720-
)
729+
// Add the actual version from the constraint if it's >= minimum
730+
// This handles cases like "^0.7.9" where we need to test 0.7.9 specifically
731+
// Use regex to extract the version number from constraints like ^0.7.9, ~1.2.0, >=1.5.0, etc.
732+
versionRegex := regexp.MustCompile(`^[=~><!^]+\s*(\d+\.\d+\.\d+)`)
733+
if matches := versionRegex.FindStringSubmatch(version); matches != nil {
734+
constraintVersionStr := matches[1]
735+
if constraintVersion, err := semver.NewVersion(constraintVersionStr); err == nil {
736+
if !constraintVersion.LessThan(min) {
737+
testVersions = append(testVersions, constraintVersion.String())
738+
// Also add some versions around the constraint version
739+
testVersions = append(testVersions,
740+
fmt.Sprintf("%d.%d.%d", constraintVersion.Major(), constraintVersion.Minor(), constraintVersion.Patch()+1),
741+
fmt.Sprintf("%d.%d.%d", constraintVersion.Major(), constraintVersion.Minor()+1, 0),
742+
)
743+
}
744+
}
745+
}
721746

722747
for _, testVersion := range testVersions {
723748
if v, err := semver.NewVersion(testVersion); err == nil {
@@ -735,16 +760,22 @@ func isVersionSatisfied(version, minVersion string, sourceType SourceType) (bool
735760
}
736761
}
737762

738-
// normalizeVersion normalizes version strings for semver parsing
739-
func normalizeVersion(version string) string {
740-
// Remove common prefixes and suffixes
763+
// normalizeVersion cleans up version strings for parsing
764+
func normalizeVersion(version string, sourceType SourceType) string {
765+
// Remove whitespace and quotes
741766
version = strings.TrimSpace(version)
742-
version = strings.Trim(version, " \"'")
767+
version = strings.Trim(version, `"'`)
743768

744-
// Handle npm version ranges (^ and ~ are npm-specific, not semver constraints)
745-
if strings.HasPrefix(version, "^") || strings.HasPrefix(version, "~") {
746-
version = version[1:]
769+
// For lock files, we want the exact version, so remove npm-style prefixes
770+
// For package files, we want to preserve the constraint operators
771+
if sourceType == SourceTypeLock {
772+
// Handle npm version ranges (^ and ~ are npm-specific, not semver constraints)
773+
if strings.HasPrefix(version, "^") || strings.HasPrefix(version, "~") {
774+
version = version[1:]
775+
}
747776
}
777+
// For SourceTypePackage, we preserve all operators including ^ and ~
778+
// as they are valid semver constraints that the semver library can parse
748779

749780
return version
750781
}

pkg/agentfs/sdk_version_check_test.go

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ livekit-agents = ">=1.0.0"`,
111111
expectError: true,
112112
errorMsg: "too old",
113113
},
114+
{
115+
name: "Node package.json with good version",
116+
projectType: ProjectTypeNode,
117+
setupFiles: map[string]string{
118+
"package.json": `{
119+
"dependencies": {
120+
"@livekit/agents": "^1.1.1"
121+
}
122+
}`,
123+
},
124+
expectError: false,
125+
},
126+
114127
{
115128
name: "Node package-lock.json with valid version",
116129
projectType: ProjectTypeNode,
@@ -207,19 +220,24 @@ func TestIsVersionSatisfied(t *testing.T) {
207220
{"0.9.0", "1.0.0", SourceTypeLock, false, false},
208221
{"1.5.0", "2.0.0", SourceTypeLock, false, false},
209222
{"2.0.0", "2.0.0", SourceTypeLock, true, false},
210-
223+
211224
// Package file tests (constraint satisfaction)
212225
{">=1.5.0", "1.0.0", SourceTypePackage, true, false},
213226
{"<2.0.0", "1.0.0", SourceTypePackage, true, false},
214227
{">=2.0.0", "1.0.0", SourceTypePackage, true, false},
215228
{"~1.2.0", "1.0.0", SourceTypePackage, true, false},
216229
{"^1.0.0", "1.0.0", SourceTypePackage, true, false},
217-
230+
// Test the specific case that was failing: ^0.7.9 should satisfy minimum 0.0.7
231+
{"^0.7.9", "0.0.7", SourceTypePackage, true, false},
232+
// Test other caret scenarios
233+
{"^0.5.0", "0.0.7", SourceTypePackage, true, false}, // ^0.5.0 allows 0.5.0+ which >= 0.0.7
234+
{"^1.0.0", "0.0.7", SourceTypePackage, true, false}, // 1.0.0+ >= 0.0.7
235+
218236
// Special cases
219237
{"latest", "1.0.0", SourceTypeLock, true, false},
220238
{"*", "1.0.0", SourceTypeLock, true, false},
221239
{"", "1.0.0", SourceTypeLock, true, false},
222-
240+
223241
// Error cases
224242
{"invalid", "1.0.0", SourceTypeLock, false, true},
225243
{"1.5.0", "invalid", SourceTypeLock, false, true},
@@ -251,25 +269,45 @@ func TestIsVersionSatisfied(t *testing.T) {
251269

252270
func TestNormalizeVersion(t *testing.T) {
253271
tests := []struct {
254-
input string
255-
expected string
272+
input string
273+
sourceType SourceType
274+
expected string
275+
description string
256276
}{
257-
{"1.5.0", "1.5.0"},
258-
{"^1.5.0", "1.5.0"},
259-
{"~1.5.0", "1.5.0"},
260-
{">=1.5.0", ">=1.5.0"},
261-
{"<2.0.0", "<2.0.0"},
262-
{"==1.5.0", "==1.5.0"},
263-
{" 1.5.0 ", "1.5.0"},
264-
{`"1.5.0"`, "1.5.0"},
265-
{`'1.5.0'`, "1.5.0"},
266-
{"*", "*"},
267-
{"latest", "latest"},
277+
// Lock file tests (should remove ^ and ~)
278+
{"1.5.0", SourceTypeLock, "1.5.0", "exact version"},
279+
{"^1.5.0", SourceTypeLock, "1.5.0", "npm caret removed for lock file"},
280+
{"~1.5.0", SourceTypeLock, "1.5.0", "npm tilde removed for lock file"},
281+
{">=1.5.0", SourceTypeLock, ">=1.5.0", "semver operators preserved for lock file"},
282+
{"<2.0.0", SourceTypeLock, "<2.0.0", "semver operators preserved for lock file"},
283+
{"==1.5.0", SourceTypeLock, "==1.5.0", "semver operators preserved for lock file"},
284+
{" 1.5.0 ", SourceTypeLock, "1.5.0", "whitespace removed"},
285+
{`"1.5.0"`, SourceTypeLock, "1.5.0", "quotes removed"},
286+
{`'1.5.0'`, SourceTypeLock, "1.5.0", "quotes removed"},
287+
{"*", SourceTypeLock, "*", "wildcard preserved"},
288+
{"latest", SourceTypeLock, "latest", "latest preserved"},
289+
290+
// Package file tests (should preserve ^ and ~)
291+
{"1.5.0", SourceTypePackage, "1.5.0", "exact version"},
292+
{"^1.5.0", SourceTypePackage, "^1.5.0", "npm caret preserved for package file"},
293+
{"~1.5.0", SourceTypePackage, "~1.5.0", "npm tilde preserved for package file"},
294+
{">=1.5.0", SourceTypePackage, ">=1.5.0", "semver operators preserved for package file"},
295+
{"<2.0.0", SourceTypePackage, "<2.0.0", "semver operators preserved for package file"},
296+
{"==1.5.0", SourceTypePackage, "==1.5.0", "semver operators preserved for package file"},
297+
{" 1.5.0 ", SourceTypePackage, "1.5.0", "whitespace removed"},
298+
{`"1.5.0"`, SourceTypePackage, "1.5.0", "quotes removed"},
299+
{`'1.5.0'`, SourceTypePackage, "1.5.0", "quotes removed"},
300+
{"*", SourceTypePackage, "*", "wildcard preserved"},
301+
{"latest", SourceTypePackage, "latest", "latest preserved"},
268302
}
269303

270304
for _, tt := range tests {
271-
t.Run(tt.input, func(t *testing.T) {
272-
result := normalizeVersion(tt.input)
305+
sourceTypeStr := "Package"
306+
if tt.sourceType == SourceTypeLock {
307+
sourceTypeStr = "Lock"
308+
}
309+
t.Run(fmt.Sprintf("%s_%s_%s", tt.input, sourceTypeStr, tt.description), func(t *testing.T) {
310+
result := normalizeVersion(tt.input, tt.sourceType)
273311
if result != tt.expected {
274312
t.Errorf("Expected %s but got %s", tt.expected, result)
275313
}

0 commit comments

Comments
 (0)