-
Notifications
You must be signed in to change notification settings - Fork 307
Expand file tree
/
Copy pathagent_apm_test.go
More file actions
2583 lines (2166 loc) · 108 KB
/
Copy pathagent_apm_test.go
File metadata and controls
2583 lines (2166 loc) · 108 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package main
import (
"archive/zip"
"crypto/md5" // #nosec G501 -- checksum verification against Artifactory's own reported MD5, not security-sensitive
"crypto/sha1" // #nosec G505 -- checksum verification against Artifactory's own reported SHA1, not security-sensitive
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
buildinfo "github.com/jfrog/build-info-go/entities"
artUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build"
"github.com/jfrog/jfrog-cli-core/v2/common/spec"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"github.com/jfrog/jfrog-cli/inttestutils"
"github.com/jfrog/jfrog-cli/utils/tests"
accessServices "github.com/jfrog/jfrog-client-go/access/services"
clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
apmBuildName = "apm-test-build"
dirPerms = 0755
filePerms = 0644
)
// captureStdout runs fn with os.Stdout redirected to a pipe and returns everything written to
// it. apm's own diagnostics (e.g. "HTTP 404 ...") are printed straight to os.Stdout by the
// underlying apm subprocess and never appear in the Go error returned by CLI commands, so
// assertions on that text must inspect captured stdout instead of err.Error().
func captureStdout(t *testing.T, fn func() error) (string, error) {
t.Helper()
origStdout := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
fnErr := fn()
require.NoError(t, w.Close())
os.Stdout = origStdout
out, readErr := io.ReadAll(r)
require.NoError(t, readErr)
return string(out), fnErr
}
// computeFileSHA1 and computeFileMD5 mirror apk_test.go's computeFileSHA256 (same package) for
// the other two checksums build-info round-trip tests need to independently verify.
func computeFileSHA1(t *testing.T, path string) string {
t.Helper()
f, err := os.Open(path) // #nosec G304 -- path is always a test-controlled temp download destination
require.NoError(t, err, "open file for SHA1: %s", path)
defer func() { require.NoError(t, f.Close()) }()
h := sha1.New() // #nosec G401 -- checksum verification, not a security-relevant crypto use
_, err = io.Copy(h, f)
require.NoError(t, err, "compute SHA1 for: %s", path)
return fmt.Sprintf("%x", h.Sum(nil))
}
func computeFileMD5(t *testing.T, path string) string {
t.Helper()
f, err := os.Open(path) // #nosec G304 -- path is always a test-controlled temp download destination
require.NoError(t, err, "open file for MD5: %s", path)
defer func() { require.NoError(t, f.Close()) }()
h := md5.New() // #nosec G401 -- checksum verification, not a security-relevant crypto use
_, err = io.Copy(h, f)
require.NoError(t, err, "compute MD5 for: %s", path)
return fmt.Sprintf("%x", h.Sum(nil))
}
// initApmTest initializes the APM test environment.
func initApmTest(t *testing.T) {
if !*tests.TestApm {
t.Skip("Skipping APM tests. To run APM test add the '-test.apm=true' option.")
}
// Ensure APM is installed
_, err := exec.LookPath("apm")
require.NoError(t, err, "APM must be installed to run APM tests. Install from: https://github.com/microsoft/apm/releases")
// Ensure JFROG_RUN_NATIVE is not set (clean state for non-native tests)
_ = os.Unsetenv("JFROG_RUN_NATIVE")
createJfrogHomeConfig(t, true)
createApmRepository(t)
initApmConfig(t)
}
// getApmCli returns a CLI configured for APM commands (without "rt" prefix).
// APM commands are: jfrog agent apm ..., not jfrog rt agent apm ...
func getApmCli() *coreTests.JfrogCli {
return coreTests.NewJfrogCli(execMain, "jfrog", "")
}
// publishApmDependencyPackage publishes a minimal, real APM package, always at version 1.0.0, to
// the default registry (tests.AgentPackagesLocalRepo) so other tests can declare it as a
// resolvable dependency (via the "owner/name#1.0.0" shorthand) and exercise real
// install/build-info collection. packageSpec is "owner/name".
func publishApmDependencyPackage(t *testing.T, packageSpec string) {
t.Helper()
publishApmDependencyPackageToRegistry(t, packageSpec, tests.AgentPackagesLocalRepo)
}
// publishApmDependencyPackageToRegistry is publishApmDependencyPackage targeting a specific,
// already-configured registry name (e.g. one of several distinct repos set up via
// "jf setup apm --repo <name>"), instead of always the default tests.AgentPackagesLocalRepo.
// Always publishes at version 1.0.0 - no caller has ever needed a different one.
func publishApmDependencyPackageToRegistry(t *testing.T, packageSpec, registryName string) {
t.Helper()
pubDir, err := os.MkdirTemp("", "apm-dep-publish-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(pubDir)
}()
require.NoError(t, os.MkdirAll(filepath.Join(pubDir, ".apm", "primitives"), dirPerms))
_, pkgName, ok := strings.Cut(packageSpec, "/")
require.True(t, ok, "packageSpec must be in owner/name form, got %q", packageSpec)
apmYaml := fmt.Sprintf(`name: %s
version: 1.0.0
license: UNLICENSED
targets:
- claude
primitives:
agents: []
`, pkgName)
require.NoError(t, os.WriteFile(filepath.Join(pubDir, "apm.yml"), []byte(apmYaml), filePerms))
require.NoError(t, os.WriteFile(filepath.Join(pubDir, ".apm", "primitives", "placeholder.txt"), []byte("placeholder content"), filePerms))
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, pubDir)
require.NoError(t, getApmCli().Exec("agent", "apm", "publish", "--package", packageSpec, "--registry", registryName),
"publishing dependency package %s to registry %s should succeed", packageSpec, registryName)
}
// createApmRepository creates a local APM repository for testing.
func createApmRepository(t *testing.T) {
if !isRepoExist(tests.AgentPackagesLocalRepo) {
repoConfig := tests.GetTestResourcesPath() + tests.AgentPackagesLocalRepositoryConfig
repoConfig, err := tests.ReplaceTemplateVariables(repoConfig, "")
require.NoError(t, err)
execCreateRepoRest(repoConfig, tests.AgentPackagesLocalRepo)
}
}
// createAgentPackagesRepoWithKey creates an agent-packages local repository whose "key"
// field matches repoName. ReplaceTemplateVariables always substitutes the ${AGENT_PACKAGES_LOCAL_REPO}
// placeholder with the tests.AgentPackagesLocalRepo constant, so for repos with a different name
// we patch the "key" field ourselves after substitution to avoid an Artifactory key/path conflict.
func createAgentPackagesRepoWithKey(t *testing.T, repoName string) {
repoConfig := tests.GetTestResourcesPath() + tests.AgentPackagesLocalRepositoryConfig
repoConfig, err := tests.ReplaceTemplateVariables(repoConfig, "")
require.NoError(t, err)
content, err := os.ReadFile(repoConfig)
require.NoError(t, err)
patched := strings.Replace(string(content), `"key": "`+tests.AgentPackagesLocalRepo+`"`, `"key": "`+repoName+`"`, 1)
patchedPath := filepath.Join(filepath.Dir(repoConfig), repoName+"_repository_config.json")
require.NoError(t, os.WriteFile(patchedPath, []byte(patched), filePerms)) // #nosec G703 -- repoName is always one of this test's own hardcoded literals, not external input
execCreateRepoRest(patchedPath, repoName)
}
// ensureApmTestProjectExists creates the shared tests.ProjectKey Artifactory project and assigns
// tests.AgentPackagesLocalRepo to it. "--project" scoping on jf commands (e.g. jf rt bp
// --project=X) requires a real Project entity server-side - it's not just a local metadata tag -
// so tests exercising project scoping must provision one first.
//
// Skips (not fails) the calling test when Projects/Access isn't available in the current
// environment: local Artifactory instances used in some CI/test setups don't have it licensed
// or enabled, which is an environment limitation, not a defect in the apm code under test. Same
// graceful-skip pattern as TestApkAdd_ProjectBuildInfoCollection.
func ensureApmTestProjectExists(t *testing.T) {
t.Helper()
accessManager, err := artUtils.CreateAccessServiceManager(serverDetails, false)
if err != nil {
t.Skipf("Skipping project-scoped test - cannot create access manager: %v", err)
}
// Best-effort: ignore "doesn't exist yet" and any other delete failure alike, since the
// only thing that matters is a clean CreateProject call next.
_ = accessManager.DeleteProject(tests.ProjectKey)
if err := accessManager.CreateProject(accessServices.ProjectParams{
ProjectDetails: accessServices.Project{
DisplayName: "apm test project " + tests.ProjectKey,
ProjectKey: tests.ProjectKey,
},
}); err != nil {
t.Skipf("Skipping project-scoped test - cannot create project: %v", err)
}
if err := accessManager.AssignRepoToProject(tests.AgentPackagesLocalRepo, tests.ProjectKey, true); err != nil {
t.Skipf("Skipping project-scoped test - cannot assign repo to project: %v", err)
}
}
// initApmConfig sets up the APM configuration in ~/.apm/config.json via jf setup.
func initApmConfig(t *testing.T) {
// Use jf setup to configure APM (not jf rt setup)
setupCli := coreTests.NewJfrogCli(execMain, "jfrog", "")
err := setupCli.Exec("setup", "apm", "--repo", tests.AgentPackagesLocalRepo)
require.NoError(t, err, "jf setup apm should succeed")
}
// cleanApmTest cleans up resources after APM tests.
func cleanApmTest(t *testing.T) {
clientTestUtils.UnSetEnvAndAssert(t, coreutils.HomeDir)
deleteSpec := spec.NewBuilder().Pattern(tests.AgentPackagesLocalRepo).BuildSpec()
_, _, err := tests.DeleteFiles(deleteSpec, serverDetails)
require.NoError(t, err, "cleanup should remove test artifacts")
tests.CleanFileSystem()
}
// createApmTestProject creates a minimal APM project structure with apm.yml.
func createApmTestProject(t *testing.T, projectDir string) {
err := os.MkdirAll(projectDir, dirPerms)
require.NoError(t, err)
// Create minimal .apm directory
apmDir := filepath.Join(projectDir, ".apm")
err = os.MkdirAll(apmDir, dirPerms)
require.NoError(t, err)
// Create basic primitives directory
primitivesDir := filepath.Join(apmDir, "primitives")
err = os.MkdirAll(primitivesDir, dirPerms)
require.NoError(t, err)
// Create apm.yml
apmYamlContent := `version: "1.0.0"
name: test-apm-package
description: Test APM package for e2e testing
license: UNLICENSED
targets:
- claude
primitives:
agents: []
skills: []
models: []
tools: []
dependencies:
apm: []
mcp: []
`
apmYamlPath := filepath.Join(projectDir, "apm.yml")
err = os.WriteFile(apmYamlPath, []byte(apmYamlContent), filePerms)
require.NoError(t, err)
// Create a dummy file for packaging
dummyFile := filepath.Join(primitivesDir, "placeholder.txt")
err = os.WriteFile(dummyFile, []byte("placeholder content"), filePerms)
require.NoError(t, err)
}
// createApmTestProjectWithDependency creates the same minimal project as createApmTestProject,
// but declares depSpec (e.g. "test/dep-pkg#1.0.0") as a real APM dependency. The caller is
// responsible for having already published depSpec's package (see publishApmDependencyPackage)
// so install actually resolves it and produces apm.lock.yaml / build info.
func createApmTestProjectWithDependency(t *testing.T, projectDir, depSpec string) {
createApmTestProject(t, projectDir)
apmYamlContent := `version: "1.0.0"
name: test-apm-package
description: Test APM package for e2e testing
license: UNLICENSED
targets:
- claude
primitives:
agents: []
skills: []
models: []
tools: []
dependencies:
apm:
- ` + depSpec + `
`
require.NoError(t, os.WriteFile(filepath.Join(projectDir, "apm.yml"), []byte(apmYamlContent), filePerms))
}
// fetchPublishedApmBuildInfo publishes the locally-collected build info to Artifactory
// (jf rt bp) and reads it back from the server.
//
// apm's install/publish commands only ever call Build.AddArtifacts /
// Build.SavePartialBuildInfo, which write *partial* build-info files under
// <buildDir>/partials/ - they never call Build.SaveBuildInfo to materialize a combined,
// "generated" build info directly under <buildDir> (the file build.GetGeneratedBuildsInfo
// reads). That's consistent with the rest of jfrog-cli's build-info design: jf rt bp
// itself calls Build.ToBuildInfo(), which reads the same partials and assembles the
// final build info at publish time - GetGeneratedBuildsInfo is for package managers whose
// commands call Build.SaveBuildInfo directly (npm, docker, conan, etc.), not for reading
// pre-publish partials. So build.GetGeneratedBuildsInfo(name, number, "") is always
// guaranteed to return zero results for apm and cannot be used to validate its build info
// pre-publish; publish-then-verify-on-server is required.
func fetchPublishedApmBuildInfo(t *testing.T, buildName, buildNumber string) *buildinfo.BuildInfo {
t.Helper()
return fetchPublishedApmBuildInfoInProject(t, buildName, buildNumber, "")
}
// fetchPublishedApmBuildInfoInProject is fetchPublishedApmBuildInfo scoped to an Artifactory project key.
func fetchPublishedApmBuildInfoInProject(t *testing.T, buildName, buildNumber, projectKey string) *buildinfo.BuildInfo {
t.Helper()
bpArgs := []string{"bp", buildName, buildNumber}
if projectKey != "" {
bpArgs = append(bpArgs, "--project", projectKey)
}
require.NoError(t, artifactoryCli.Exec(bpArgs...), "jf rt bp should succeed")
published, found, err := tests.GetBuildInfoInProject(serverDetails, buildName, buildNumber, projectKey)
require.NoError(t, err)
require.True(t, found, "published build info should be found on the server")
return &published.BuildInfo
}
// readLocalApmPartialBuildInfo reads locally-collected build info directly via
// Build.ToBuildInfo(), which assembles it from partial files without touching the server or
// clearing anything - the same mechanism pnpm_test.go/npm_test.go use to validate build info
// collection. Unlike fetchPublishedApmBuildInfo, this must be used for INTERMEDIATE checks
// within a multi-step test: "jf rt bp" calls Build.Clean() after a successful publish, wiping
// local partials for that exact build name/number. Calling fetchPublishedApmBuildInfo (or any
// validate* built on it) more than once for the same build/number silently loses whatever an
// earlier step wrote - confirmed live: a dependency captured after install disappeared from a
// later "has both artifacts and dependencies" check, once an intervening bp call for that same
// build/number had already run and cleared it. Reserve the server round-trip for a single,
// final check per build/number.
func readLocalApmPartialBuildInfo(t *testing.T, buildName, buildNumber string) *buildinfo.BuildInfo {
t.Helper()
buildInfoService := buildUtils.CreateBuildInfoService()
apmBuild, err := buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, "")
require.NoError(t, err)
bi, err := apmBuild.ToBuildInfo()
require.NoError(t, err)
return bi
}
// apmRegistryURL builds the real registry URL for repoName, matching exactly what
// AgentPackagesBaseURL in jfrog-cli-artifactory constructs from serverDetails
// (<ArtifactoryUrl>/api/agentpackages/<repo>/). A registry declared in apm.yml's own
// registries: block is used by apm as its literal API base URL for that registry - not merely
// matched by host for credential discovery - so it must be this exact form, not just any URL on
// the right host, or apm's own HTTP requests 404/403 against the wrong path.
func apmRegistryURL(repoName string) string {
return strings.TrimSuffix(*tests.JfrogUrl, "/") + "/artifactory/api/agentpackages/" + repoName + "/"
}
// validateApmBuildInfo publishes and validates the build info collected by an APM command.
func validateApmBuildInfo(t *testing.T, buildName, buildNumber string, expectedArtifacts int) {
buildResult := fetchPublishedApmBuildInfo(t, buildName, buildNumber)
// Verify build properties
assert.Equal(t, buildName, buildResult.Name)
assert.Equal(t, buildNumber, buildResult.Number)
// Verify modules exist if artifacts expected
if expectedArtifacts > 0 && len(buildResult.Modules) > 0 {
module := buildResult.Modules[0]
// Verify all artifacts have checksums
for _, artifact := range module.Artifacts {
assert.NotEmpty(t, artifact.Sha256, "Artifact should have SHA256 checksum")
assert.NotEmpty(t, artifact.Path, "Artifact should have path")
}
// Verify dependencies if present
for _, dep := range module.Dependencies {
assert.NotEmpty(t, dep.Sha256, "Dependency should have SHA256 checksum")
assert.NotEmpty(t, dep.Id, "Dependency should have ID")
}
}
}
// validateBuildInfoDependencies validates dependencies exist in the published build info
func validateBuildInfoDependencies(t *testing.T, buildName, buildNumber string) {
buildResult := fetchPublishedApmBuildInfo(t, buildName, buildNumber)
require.Len(t, buildResult.Modules, 1, "Build should have at least one module")
module := buildResult.Modules[0]
require.NotEmpty(t, module.Dependencies, "Dependencies should be present in build info")
// Dependency checksums come from the same HEAD-based resolution as artifact checksums (see
// resolveChecksumsByHead in jfrog-cli-artifactory), so all three are required here too, not
// merely the ID.
for _, dep := range module.Dependencies {
assert.NotEmpty(t, dep.Id, "Dependency should have ID")
assert.NotEmpty(t, dep.Sha256, "Dependency should have SHA256")
assert.Len(t, dep.Sha1, 40, "Dependency should have a 40 hex-character SHA1")
assert.Len(t, dep.Md5, 32, "Dependency should have a 32 hex-character MD5")
}
}
// validateBuildInfoArtifacts validates artifacts in the published build info
func validateBuildInfoArtifacts(t *testing.T, buildName, buildNumber string, expectedCount int) {
buildResult := fetchPublishedApmBuildInfo(t, buildName, buildNumber)
require.Len(t, buildResult.Modules, 1, "Build should have at least one module")
module := buildResult.Modules[0]
require.Len(t, module.Artifacts, expectedCount, "Artifacts count should match expected")
for _, artifact := range module.Artifacts {
assert.NotEmpty(t, artifact.Path, "Artifact should have path")
assert.NotEmpty(t, artifact.Sha256, "Artifact should have SHA256")
assert.Len(t, artifact.Sha1, 40, "Artifact should have a 40 hex-character SHA1")
assert.Len(t, artifact.Md5, 32, "Artifact should have a 32 hex-character MD5")
}
}
// validateBuildInfoHasBothArtifactsAndDependencies validates both exist in the published build
// info, with checksums on each - not just presence of the two lists.
func validateBuildInfoHasBothArtifactsAndDependencies(t *testing.T, buildName, buildNumber string) {
buildResult := fetchPublishedApmBuildInfo(t, buildName, buildNumber)
require.Len(t, buildResult.Modules, 1, "Build should have at least one module")
module := buildResult.Modules[0]
require.NotEmpty(t, module.Dependencies, "Build info should have dependencies")
require.NotEmpty(t, module.Artifacts, "Build info should have artifacts")
for _, dep := range module.Dependencies {
assert.NotEmpty(t, dep.Sha256, "Dependency should have SHA256")
}
for _, artifact := range module.Artifacts {
assert.NotEmpty(t, artifact.Sha256, "Artifact should have SHA256")
}
}
// TestApmSetupAndConfig validates APM setup with apm config file persistence (P0: Scenario #1).
// apmRegistryEntry mirrors one entry under ~/.apm/config.json's "registries" map. Default is
// only ever present (and true) on whichever registry "jf setup apm" most recently
// configured - apm's own config command clears it from any previously-default entry, so at most
// one registry has Default == true at a time.
type apmRegistryEntry struct {
URL string `json:"url"`
Token string `json:"token"`
Default bool `json:"default"`
}
// readApmRegistries parses ~/.apm/config.json's registries map.
func readApmRegistries(t *testing.T) map[string]apmRegistryEntry {
t.Helper()
homeDir, err := os.UserHomeDir()
require.NoError(t, err)
configData, err := os.ReadFile(filepath.Join(homeDir, ".apm", "config.json"))
require.NoError(t, err)
var config struct {
Registries map[string]apmRegistryEntry `json:"registries"`
}
require.NoError(t, json.Unmarshal(configData, &config))
return config.Registries
}
func TestApmSetupAndConfig(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
homeDir, err := os.UserHomeDir()
require.NoError(t, err)
apmConfigPath := filepath.Join(homeDir, ".apm", "config.json")
// First setup call (use correct CLI prefix: jfrog, not jfrog rt)
setupCli := coreTests.NewJfrogCli(execMain, "jfrog", "")
err = setupCli.Exec("setup", "apm", "--repo", tests.AgentPackagesLocalRepo)
require.NoError(t, err, "jf setup apm should succeed")
assert.FileExists(t, apmConfigPath, "APM config file should be created")
// Verify --repo maps to an actual registry entry (not just "some registries exist"), with a
// URL that references the repo and a token, and that it's the default registry.
registries := readApmRegistries(t)
primary, ok := registries[tests.AgentPackagesLocalRepo]
require.True(t, ok, "registries should contain an entry named after --repo (%s)", tests.AgentPackagesLocalRepo)
assert.Contains(t, primary.URL, tests.AgentPackagesLocalRepo, "registry URL should reference the configured repo")
assert.NotEmpty(t, primary.Token, "registry entry should have a token")
assert.True(t, primary.Default, "the just-configured repo should be the default registry")
// Second setup call against a DIFFERENT repo should flip the default to it, and clear
// Default from the previously-default entry - proving "default" tracks the most recently
// configured repo, not just whichever was configured first.
//
// ~/.apm/config.json is a real user-global file, not scoped per test, and several other
// tests in this file install without an explicit --registry (relying on default
// resolution) - so restore tests.AgentPackagesLocalRepo as the default before returning,
// regardless of how this test's own assertions turn out.
secondRepo := "apm-setup-config-test-repo"
if !isRepoExist(secondRepo) {
createAgentPackagesRepoWithKey(t, secondRepo)
}
defer deleteRepo(secondRepo)
defer func() {
_ = setupCli.Exec("setup", "apm", "--repo", tests.AgentPackagesLocalRepo)
}()
err = setupCli.Exec("setup", "apm", "--repo", secondRepo)
require.NoError(t, err, "jf setup apm should succeed against a second, different repo")
registries = readApmRegistries(t)
second, ok := registries[secondRepo]
require.True(t, ok, "registries should now contain an entry named after the second --repo (%s)", secondRepo)
assert.Contains(t, second.URL, secondRepo, "second registry URL should reference the second repo")
assert.True(t, second.Default, "the most recently configured repo should be the default registry")
if first, ok := registries[tests.AgentPackagesLocalRepo]; ok {
assert.False(t, first.Default, "the previously-default registry should no longer be marked default")
}
// Verify idempotency - re-running setup for the same (now non-default) repo should still
// succeed and flip default back to it.
err = setupCli.Exec("setup", "apm", "--repo", tests.AgentPackagesLocalRepo)
require.NoError(t, err, "jf setup apm should be idempotent")
registries = readApmRegistries(t)
assert.True(t, registries[tests.AgentPackagesLocalRepo].Default, "re-running setup for the primary repo should make it the default again")
}
// TestApmInstallWithBuildInfo validates `jf agent apm install` with build-info capture (P0: Scenario #13).
func TestApmInstallWithBuildInfo(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
publishApmDependencyPackage(t, "test/install-bi-dep")
projectDir, err := os.MkdirTemp("", "apm-install-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
// A real, resolvable dependency is required: apm only writes apm.lock.yaml (and thus only
// jfrog-cli only collects build-info) when the project has at least one dependency.
createApmTestProjectWithDependency(t, projectDir, "test/install-bi-dep#1.0.0")
buildNumber := "101"
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, projectDir)
// Run apm install with build-info capture
err = getApmCli().Exec("agent", "apm", "install", "--build-name", apmBuildName, "--build-number", buildNumber)
require.NoError(t, err, "jf agent apm install should succeed with build-info")
// Validate build info was created
validateApmBuildInfo(t, apmBuildName, buildNumber, 0)
// Publish the build info
err = artifactoryCli.Exec("bp", apmBuildName, buildNumber)
require.NoError(t, err, "jf rt bp should succeed")
// Clean up build info
inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, apmBuildName, artHttpDetails)
}
// TestApmPublishWithBuildInfo validates `jf agent apm publish` with build-info capture (P0: Scenario #3).
func TestApmPublishWithBuildInfo(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
projectDir, err := os.MkdirTemp("", "apm-publish-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
createApmTestProject(t, projectDir)
buildNumber := "102"
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, projectDir)
// Run apm publish with build-info capture
err = getApmCli().Exec("agent", "apm", "publish", "--package", "jfrog/test-apm-pkg", "--registry", tests.AgentPackagesLocalRepo, "--build-name", apmBuildName, "--build-number", buildNumber)
require.NoError(t, err, "jf agent apm publish should succeed with build-info")
// Validate build info was created with artifact
validateApmBuildInfo(t, apmBuildName, buildNumber, 1)
// Publish the build info
err = artifactoryCli.Exec("bp", apmBuildName, buildNumber)
require.NoError(t, err, "jf rt bp should succeed")
// Verify artifact was uploaded to Artifactory
deleteSpec := spec.NewBuilder().
Pattern(tests.AgentPackagesLocalRepo + "/jfrog/test-apm-pkg/*.zip").
BuildSpec()
artifacts, _, err := tests.SearchFiles(deleteSpec, serverDetails)
require.NoError(t, err)
assert.NotEmpty(t, artifacts, "Published APM package should be found in repository")
// Clean up
_, _, _ = tests.DeleteFiles(deleteSpec, serverDetails)
inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, apmBuildName, artHttpDetails)
}
// TestApmPublishArtifactPath validates artifact upload to correct path (P0: Scenario #4).
func TestApmPublishArtifactPath(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
projectDir, err := os.MkdirTemp("", "apm-publish-path-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
createApmTestProject(t, projectDir)
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, projectDir)
owner := "acme"
packageName := "my-agent-skill"
err = getApmCli().Exec("agent", "apm", "publish", "--package", fmt.Sprintf("%s/%s", owner, packageName), "--registry", tests.AgentPackagesLocalRepo)
require.NoError(t, err, "jf agent apm publish should succeed")
// Verify artifact path: <owner>/<name>/<name>-<version>.zip
searchSpec := spec.NewBuilder().
Pattern(fmt.Sprintf("%s/%s/%s/*.zip", tests.AgentPackagesLocalRepo, owner, packageName)).
BuildSpec()
artifacts, _, err := tests.SearchFiles(searchSpec, serverDetails)
require.NoError(t, err)
assert.NotEmpty(t, artifacts, "Artifact should be found at expected path: <owner>/<name>/<name>-<version>.zip")
// Verify artifact name format. Note: ResultItem.Path is the artifact's *directory* (e.g.
// "acme/my-agent-skill"); the filename itself is a separate field, Name.
if len(artifacts) > 0 {
assert.True(t,
strings.HasPrefix(artifacts[0].Name, packageName+"-") && strings.HasSuffix(artifacts[0].Name, ".zip"),
"Artifact name should follow pattern: <name>-<version>.zip, got %q", artifacts[0].Name)
}
// Clean up
_, _, _ = tests.DeleteFiles(searchSpec, serverDetails)
}
// TestApmPublishRequiresPackageFlag validates that --package flag is required (P0: Scenario #23).
func TestApmPublishRequiresPackageFlag(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
projectDir, err := os.MkdirTemp("", "apm-publish-no-pkg-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
createApmTestProject(t, projectDir)
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, projectDir)
// Attempt publish without --package flag
err = getApmCli().Exec("agent", "apm", "publish")
assert.Error(t, err, "jf agent apm publish without --package should fail")
assert.Contains(t, err.Error(), "package", "Error message should mention --package flag")
}
// TestApmInstallInvalidPackage validates handling of missing/invalid package references (P0: Scenario #15).
func TestApmInstallInvalidPackage(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
projectDir, err := os.MkdirTemp("", "apm-invalid-pkg-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
// Create project with invalid dependency
err = os.MkdirAll(filepath.Join(projectDir, ".apm"), 0755)
require.NoError(t, err)
// APM dependency shorthand is "owner/name#version" (a plain string), resolved against the
// default registry. A nonexistent package fails at resolve time with a 404-style error.
apmYamlContent := `version: "1.0.0"
name: test-with-missing-dep
license: UNLICENSED
targets:
- claude
dependencies:
apm:
- nonexistent/package#1.0.0
`
apmYamlPath := filepath.Join(projectDir, "apm.yml")
err = os.WriteFile(apmYamlPath, []byte(apmYamlContent), 0644)
require.NoError(t, err)
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, projectDir)
// Attempt install with invalid package. apm's own diagnostics (including the "HTTP 404"
// detail) are printed to stdout by the apm subprocess, not embedded in the Go error, so we
// must capture stdout to assert on them.
output, cmdErr := captureStdout(t, func() error {
return getApmCli().Exec("agent", "apm", "install")
})
assert.Error(t, cmdErr, "install of a nonexistent package should fail")
assert.True(t,
strings.Contains(output, "404") || strings.Contains(output, "no package"),
"Output should indicate package not found, got: %s", output)
}
// TestApmAuthEnvVarBehavior validates two distinct env-var-auth scenarios (both via
// APM_REGISTRY_TOKEN_<REGISTRY>, agent/apm/common/apmenv.go) as subtests sharing one
// initApmTest/cleanApmTest cycle instead of two separate test functions.
func TestApmAuthEnvVarBehavior(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
// The registry name apm actually knows about is the repo key itself ("cli-agent-packages-local"),
// not a literal name "default" - jf setup apm calls ConfigureApmRegistryPersistent(repoName),
// which writes registry.<repoName>.{url,token,default} into ~/.apm/config.json using repoName
// verbatim. apm sanitizes that name into its env var form the same way jf does
// (sanitizeApmEnvName in apmenv.go: uppercase, "-"/"." -> "_"). Using any other name here (e.g.
// the earlier "default") produces an env var apm never looks at for this registry, so a "wrong
// token" set under that name is silently never consulted - confirmed live, this is exactly why
// the wrong-token subtest below kept passing for the wrong reason before this fix.
registryName := tests.AgentPackagesLocalRepo
tokenEnvVar := fmt.Sprintf("APM_REGISTRY_TOKEN_%s", strings.ToUpper(strings.ReplaceAll(registryName, "-", "_")))
t.Run("wrong token is honored instead of silently overridden", func(t *testing.T) {
// jf's own BuildApmEnv (agent/apm/common/apmenv.go) always auto-injects
// APM_REGISTRY_TOKEN_<NAME> from the configured server before running apm - a plain
// install with the CORRECT token set ourselves would succeed identically whether or not
// jf actually reads our value or silently substitutes its own, so that alone wouldn't
// prove anything. Setting an intentionally WRONG token instead only fails if jf genuinely
// leaves our value alone (injectRegistryCredentialEnv's "respecting existing value" branch)
// instead of overriding it with the correct one - which is exactly what this proves.
//
// (A debug-log assertion on "credential env var already set" was tried here first, but
// log.SetDefaultLogger() - which reads JFROG_CLI_LOG_LEVEL - is only called from
// main(), not execMain(); this test harness invokes execMain() directly in-process, so
// the log level set via os.Setenv here is never actually picked up. Confirmed live: the
// log line never appeared no matter what level was set.)
publishApmDependencyPackage(t, "test/auth-env-wrong-token-dep")
// Belt and braces: apm's own docs say an env var token outranks ~/.apm/config.json's
// stored one, but remove the stored token for this registry anyway so there is no valid
// fallback credential at all - the only credential apm can possibly use is the wrong one
// set below. Restored afterward by re-running jf setup apm (initApmConfig), which
// every other test in this file also depends on having a correctly configured registry.
require.NoError(t, exec.Command("apm", "config", "unset", fmt.Sprintf("registry.%s.token", registryName)).Run(), // #nosec G204 -- fixed argv, no user input
"removing the stored registry token should succeed")
defer initApmConfig(t)
projectDir, err := os.MkdirTemp("", "apm-auth-env-wrong-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
createApmTestProjectWithDependency(t, projectDir, "test/auth-env-wrong-token-dep#1.0.0")
defer setupTestWorkingDirectory(t, projectDir)()
require.NoError(t, os.Setenv(tokenEnvVar, "definitely-not-a-real-token"))
defer func() {
_ = os.Unsetenv(tokenEnvVar)
}()
err = getApmCli().Exec("agent", "apm", "install", "--build-name", apmBuildName, "--build-number", "103")
assert.Error(t, err, "install should fail when the pre-set (invalid) token env var is honored instead of silently overridden")
})
t.Run("correct token is not exposed in output", func(t *testing.T) {
projectDir := createApmProjectWithYaml(t, getBasicApmYaml())
defer func() {
_ = os.RemoveAll(projectDir)
}()
defer setupTestWorkingDirectory(t, projectDir)()
require.NoError(t, os.Setenv(tokenEnvVar, *tests.JfrogAccessToken))
defer func() {
_ = os.Unsetenv(tokenEnvVar)
}()
// The token must be usable for auth but never echoed back in apm's own stdout/log output.
output, err := captureStdout(t, func() error {
return getApmCli().Exec("agent", "apm", "install")
})
require.NoError(t, err, "install should work with env var auth")
assert.NotContains(t, output, *tests.JfrogAccessToken, "access token should not be exposed in command output")
})
}
// TestApmMissingCredentials validates that install fails when there is no registry to discover
// at all - not, as the name might suggest, because credentials are generically "missing". apm
// always gets its actual token from jf's own configured server (BuildApmEnv in
// jfrog-cli-artifactory), regardless of ~/.apm/config.json; that file (and apm.yml's own
// registries: block) only supply the registry NAME+URL to route that token through. With
// neither source present, BuildApmEnv fails before credentials are ever considered - confirmed
// here by asserting on its exact error text ("no APM registry found"), not just a non-nil error,
// so this test can't silently start passing for an unrelated reason.
// See TestApmInstallSucceedsWithRegistryDeclaredInApmYml for the complementary case: apm.yml's
// own registries: block is sufficient on its own, even with ~/.apm/config.json absent.
func TestApmMissingCredentials(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
projectDir, err := os.MkdirTemp("", "apm-no-creds-test-*")
require.NoError(t, err)
defer func() {
_ = os.RemoveAll(projectDir)
}()
createApmTestProject(t, projectDir) // apm.yml here declares no registries: block
wd, err := os.Getwd()
require.NoError(t, err)
defer clientTestUtils.ChangeDirAndAssert(t, wd)
clientTestUtils.ChangeDirAndAssert(t, projectDir)
// Remove ~/.apm/config.json - with apm.yml declaring no registries: block either, this
// leaves BuildApmEnv nothing to discover a registry from.
homeDir, err := os.UserHomeDir()
require.NoError(t, err)
apmConfigPath := filepath.Join(homeDir, ".apm", "config.json")
err = os.Remove(apmConfigPath)
if err != nil && !os.IsNotExist(err) {
require.NoError(t, err)
}
defer initApmConfig(t) // restore ~/.apm/config.json for later tests regardless of outcome
// Unset any auth env vars
for _, envVar := range os.Environ() {
if strings.Contains(envVar, "APM_REGISTRY") {
key := strings.Split(envVar, "=")[0]
_ = os.Unsetenv(key)
}
}
// Attempt install with no registry source available.
err = getApmCli().Exec("agent", "apm", "install")
require.Error(t, err, "jf agent apm install without a discoverable registry should fail")
assert.Contains(t, err.Error(), "no APM registry found",
"the failure should specifically be 'no registry found', not some unrelated error")
}
// apmRegistryToken reads registry.<repoName>.token out of ~/.apm/config.json, the same file
// ConfigureApmRegistryPersistent writes to via `apm config set`. Returns "" if the file or the
// registry entry doesn't exist.
func apmRegistryToken(t *testing.T, repoName string) string {
t.Helper()
homeDir, err := os.UserHomeDir()
require.NoError(t, err)
data, err := os.ReadFile(filepath.Join(homeDir, ".apm", "config.json")) // #nosec G304 -- fixed, test-controlled path
if err != nil {
if os.IsNotExist(err) {
return ""
}
require.NoError(t, err)
}
var cfg struct {
Registries map[string]struct {
Token string `json:"token"`
} `json:"registries"`
}
require.NoError(t, json.Unmarshal(data, &cfg))
return cfg.Registries[repoName].Token
}
// TestApmAuthWithUsernamePassword validates BuildRegistryEntry's Priority-2 path
// (agent/apm/common/apmenv.go): when the configured jf server has no AccessToken - only
// User+Password - jf must mint a brand-new Artifactory access token itself and write THAT into
// ~/.apm/config.json, rather than ever embedding the raw password. This is the one auth path in
// the whole APM registry flow with no other e2e coverage: every other test in this file
// configures the "default" server via --access-token and so only ever exercises Priority-1
// (use the existing token as-is).
//
// To force Priority-2 for real (not just in a mocked unit test) without needing every
// environment this suite runs in to hand out a plaintext platform password, this reconfigures
// "default" with --user/--password derived from the current access token itself (Artifactory
// accepts a token as a Basic Auth password) via tests.SetBasicAuthFromAccessToken. `jf setup`
// always calls config.GetSpecificConfig with excludeRefreshableTokens=true (buildtools/cli.go),
// which - per excludeRefreshableTokensFromDetails - strips the AccessToken jf's own config layer
// auto-mints alongside User+Password back out again before BuildRegistryEntry ever sees
// serverDetails. So this hits the real Priority-2 branch, not a contrived edge case.
func TestApmAuthWithUsernamePassword(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
repoName := tests.AgentPackagesLocalRepo
tokenBeforeSwitch := apmRegistryToken(t, repoName)
require.NotEmpty(t, tokenBeforeSwitch, "initApmTest/initApmConfig should have already written a token for %s", repoName)
// Switch "default" to User+Password only, remembering the original mode to restore it.
origAccessToken := *tests.JfrogAccessToken
origUser, origPassword := tests.SetBasicAuthFromAccessToken()
defer func() {
*tests.JfrogUser, *tests.JfrogPassword, *tests.JfrogAccessToken = origUser, origPassword, origAccessToken
createJfrogHomeConfig(t, true) // restore "default" to its original access-token mode
initApmConfig(t) // re-run `jf setup apm` so later tests get a valid token again
}()
// Recreate "default" via the same add-with-explicit-URL helper used everywhere else in this
// file, rather than `config edit` without --url: edit's URL-preservation behavior isn't
// guaranteed, and re-adding keeps this test's setup identical to every other server-config
// switch in this file.
*tests.JfrogAccessToken = ""
createJfrogHomeConfig(t, true)
// Force a fresh mint: without this, BuildRegistryEntry's own Priority-1 check
// (serverDetails.AccessToken != "") would never even run, since the OLD token is still
// sitting in ~/.apm/config.json from initApmConfig - but that's a stale value ConfigureApmRegistryPersistent
// is about to overwrite anyway, not something BuildRegistryEntry reads back to decide its
// own priority. Removing it first just makes the "did a fresh token actually get minted"
// assertion below unambiguous.
require.NoError(t, exec.Command("apm", "config", "unset", fmt.Sprintf("registry.%s.token", repoName)).Run()) // #nosec G204 -- fixed argv
require.NoError(t,
coreTests.NewJfrogCli(execMain, "jfrog setup", "").Exec("apm", "--repo", repoName),
"jf setup apm should succeed when the server only has User+Password configured")
mintedToken := apmRegistryToken(t, repoName)
require.NotEmpty(t, mintedToken, "jf setup apm should have written a freshly minted token")
assert.NotEqual(t, tokenBeforeSwitch, mintedToken, "the token should be freshly minted, not the stale one left over from access-token mode")
assert.Equal(t, 2, strings.Count(mintedToken, "."), "a real Artifactory access token is a JWT (header.payload.signature); a base64(user:pass) blob would not have this shape")
assert.NotEqual(t, basicAuthBase64(*tests.JfrogUser, *tests.JfrogPassword), mintedToken, "the minted token must not just be the raw credentials re-encoded")
// Prove the minted token isn't just well-formed but actually authenticates: publish and
// install a real package through it, end to end.
publishApmDependencyPackage(t, "test/auth-username-password-dep")
projectDir, err := os.MkdirTemp("", "apm-auth-userpass-*")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(projectDir) }()
createApmTestProjectWithDependency(t, projectDir, "test/auth-username-password-dep#1.0.0")
defer setupTestWorkingDirectory(t, projectDir)()
require.NoError(t, getApmCli().Exec("agent", "apm", "install", "--build-name", apmBuildName, "--build-number", "104"),
"install should succeed authenticating with the freshly minted access token")
inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, apmBuildName, artHttpDetails)
}
func basicAuthBase64(user, password string) string {
return base64.StdEncoding.EncodeToString([]byte(user + ":" + password))
}
// TestApmRegistriesDeclaredInApmYml validates that apm.yml's own registries: block (a url: only -
// see manifest.go's ManifestRegistry - matched to jf's configured server by host, via
// discoverMatchingRegistries) is sufficient on its own for registry discovery: with a single
// entry and ~/.apm/config.json entirely absent, and with multiple entries declared at once
// alongside a present config.json. jf still injects the actual token from its own configured
// server (serverDetails) in both cases; apm.yml never carries a token itself, only the name->URL
// mapping that tells jf which registry name to inject that token under. Both cases share one
// initApmTest/cleanApmTest cycle as subtests rather than two separate test functions.
func TestApmRegistriesDeclaredInApmYml(t *testing.T) {
initApmTest(t)
defer cleanApmTest(t)
cases := []struct {
name string
registryNames []string
removeApmConfig bool
}{
{
name: "single registry, config.json absent",
registryNames: []string{tests.AgentPackagesLocalRepo},
removeApmConfig: true,
},
{
name: "multiple registries, config.json present",
registryNames: []string{"registry-one", "registry-two"},
},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var registriesYaml strings.Builder
for _, name := range tc.registryNames {
_, _ = fmt.Fprintf(®istriesYaml, " %s:\n url: \"%s\"\n", name, apmRegistryURL(tests.AgentPackagesLocalRepo))
}
apmYaml := fmt.Sprintf(`name: registry-in-yaml-project
version: 1.0.0
license: UNLICENSED
targets:
- claude
registries:
%sdependencies: