diff --git a/marketplace/builder.go b/marketplace/builder.go index 448e9a8..6ff7fc6 100644 --- a/marketplace/builder.go +++ b/marketplace/builder.go @@ -22,6 +22,10 @@ const IndexVersion = 1 // Builder constructs a marketplace Index by crawling directories for // core.json (compiled manifests) or .core/manifest.yaml files. type Builder struct { + // Medium is the filesystem abstraction used for manifest reads. + // If nil, io.Local is used. + Medium coreio.Medium + // BaseURL is the prefix for constructing repository URLs, e.g. // "https://forge.lthn.ai". When set, module Repo is derived as // BaseURL + "/" + org + "/" + code + ".git". @@ -147,9 +151,14 @@ func WriteIndex(m coreio.Medium, path string, idx *Index) error { // loadFromDir tries core.json first, then falls back to .core/manifest.yaml. func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) { + medium := b.Medium + if medium == nil { + medium = coreio.Local + } + // Prefer compiled manifest (core.json). coreJSON := filepath.Join(dir, "core.json") - if raw, err := coreio.Local.Read(coreJSON); err == nil { + if raw, err := medium.Read(coreJSON); err == nil { cm, err := manifest.ParseCompiled([]byte(raw)) if err != nil { return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse core.json", err) @@ -159,7 +168,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) { // Fall back to source manifest. manifestYAML := filepath.Join(dir, ".core", "manifest.yaml") - raw, err := coreio.Local.Read(manifestYAML) + raw, err := medium.Read(manifestYAML) if err != nil { return nil, nil // No manifest — skip silently. } diff --git a/marketplace/builder_test.go b/marketplace/builder_test.go index 60dd2e5..abcc040 100644 --- a/marketplace/builder_test.go +++ b/marketplace/builder_test.go @@ -108,6 +108,34 @@ func TestBuildFromDirs_Good_CoreJSON_Good(t *testing.T) { assert.Equal(t, "Compiled Module", idx.Modules[0].Name) } +func TestBuildFromDirs_Good_UsesInjectedMedium_Good(t *testing.T) { + root := t.TempDir() + modDir := filepath.Join(root, "virtual-mod") + require.NoError(t, os.MkdirAll(modDir, 0755)) + + medium := io.NewMockMedium() + cm := manifest.CompiledManifest{ + Manifest: manifest.Manifest{ + Code: "virtual-mod", + Name: "Virtual Module", + Version: "9.9.9", + Sign: "sig-virtual", + }, + Commit: "commit-virtual", + } + data, err := json.Marshal(cm) + require.NoError(t, err) + require.NoError(t, medium.Write(filepath.Join(modDir, "core.json"), string(data))) + + b := &Builder{Medium: medium} + idx, err := b.BuildFromDirs(root) + require.NoError(t, err) + + require.Len(t, idx.Modules, 1) + assert.Equal(t, "virtual-mod", idx.Modules[0].Code) + assert.Equal(t, "sig-virtual", idx.Modules[0].SignKey) +} + func TestBuildFromDirs_Good_PrefersCompiledOverSource_Good(t *testing.T) { root := t.TempDir() modDir := filepath.Join(root, "dual-mod") diff --git a/marketplace/discovery.go b/marketplace/discovery.go index a2b3d84..0bb98aa 100644 --- a/marketplace/discovery.go +++ b/marketplace/discovery.go @@ -29,9 +29,16 @@ type DiscoveredProvider struct { // Only manifests with provider fields (namespace + binary) are returned. // Usage: DiscoverProviders(...) func DiscoverProviders(dir string) ([]DiscoveredProvider, error) { - entries, err := os.ReadDir(dir) + return DiscoverProvidersWithMedium(coreio.Local, dir) +} + +// DiscoverProvidersWithMedium scans the given directory for runtime provider +// manifests using the supplied filesystem medium. +// Usage: DiscoverProvidersWithMedium(...) +func DiscoverProvidersWithMedium(medium coreio.Medium, dir string) ([]DiscoveredProvider, error) { + entries, err := medium.List(dir) if err != nil { - if os.IsNotExist(err) { + if !medium.Exists(dir) { return nil, nil // No providers directory — not an error. } return nil, coreerr.E("marketplace.DiscoverProviders", "read directory", err) @@ -46,7 +53,7 @@ func DiscoverProviders(dir string) ([]DiscoveredProvider, error) { providerDir := filepath.Join(dir, e.Name()) manifestPath := filepath.Join(providerDir, ".core", "manifest.yaml") - raw, err := coreio.Local.Read(manifestPath) + raw, err := medium.Read(manifestPath) if err != nil { core.Warn(core.Sprintf("marketplace: skipping %s: %v", e.Name(), err)) continue @@ -90,7 +97,14 @@ type ProviderRegistryFile struct { // Returns an empty registry if the file does not exist. // Usage: LoadProviderRegistry(...) func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) { - raw, err := coreio.Local.Read(path) + return LoadProviderRegistryWithMedium(coreio.Local, path) +} + +// LoadProviderRegistryWithMedium reads a registry.yaml file using the supplied +// filesystem medium. +// Usage: LoadProviderRegistryWithMedium(...) +func LoadProviderRegistryWithMedium(medium coreio.Medium, path string) (*ProviderRegistryFile, error) { + raw, err := medium.Read(path) if err != nil { if os.IsNotExist(err) { return &ProviderRegistryFile{ @@ -116,7 +130,14 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) { // SaveProviderRegistry writes the registry to the given path. // Usage: SaveProviderRegistry(...) func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error { - if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil { + return SaveProviderRegistryWithMedium(coreio.Local, path, reg) +} + +// SaveProviderRegistryWithMedium writes the registry using the supplied +// filesystem medium. +// Usage: SaveProviderRegistryWithMedium(...) +func SaveProviderRegistryWithMedium(medium coreio.Medium, path string, reg *ProviderRegistryFile) error { + if err := medium.EnsureDir(filepath.Dir(path)); err != nil { return coreerr.E("marketplace.SaveProviderRegistry", "ensure directory", err) } @@ -125,7 +146,7 @@ func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error { return coreerr.E("marketplace.SaveProviderRegistry", "marshal failed", err) } - return coreio.Local.Write(path, string(data)) + return medium.Write(path, string(data)) } // Add adds or updates a provider entry in the registry. diff --git a/marketplace/discovery_test.go b/marketplace/discovery_test.go index a08dfa8..42e8a2e 100644 --- a/marketplace/discovery_test.go +++ b/marketplace/discovery_test.go @@ -7,6 +7,7 @@ import ( os "dappco.re/go/core/scm/internal/ax/osx" "testing" + "dappco.re/go/core/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -163,6 +164,31 @@ binary: ./test-prov assert.Equal(t, filepath.Join(dir, "test-prov"), providers[0].Dir) } +func TestDiscoverProvidersWithMedium_Good(t *testing.T) { + medium := io.NewMockMedium() + medium.Dirs["/providers"] = true + medium.Dirs["/providers/cool-widget"] = true + medium.Dirs["/providers/data-viz"] = true + medium.Files["/providers/cool-widget/.core/manifest.yaml"] = ` +code: cool-widget +name: Cool Widget +version: 1.0.0 +namespace: /api/v1/cool-widget +binary: ./cool-widget +` + medium.Files["/providers/data-viz/.core/manifest.yaml"] = ` +code: data-viz +name: Data Visualiser +version: 0.2.0 +namespace: /api/v1/data-viz +binary: ./data-viz +` + + providers, err := DiscoverProvidersWithMedium(medium, "/providers") + require.NoError(t, err) + assert.Len(t, providers, 2) +} + // -- ProviderRegistryFile tests ----------------------------------------------- func TestProviderRegistry_LoadSave_Good(t *testing.T) { @@ -201,6 +227,30 @@ func TestProviderRegistry_Load_Good_NonexistentFile_Good(t *testing.T) { assert.Empty(t, reg.Providers) } +func TestProviderRegistry_LoadSave_WithMedium_Good(t *testing.T) { + medium := io.NewMockMedium() + path := "/registry/registry.yaml" + + reg := &ProviderRegistryFile{ + Version: 1, + Providers: map[string]ProviderRegistryEntry{}, + } + reg.Add("cool-widget", ProviderRegistryEntry{ + Installed: "2026-03-14T12:00:00Z", + Version: "1.0.0", + Source: "forge.lthn.ai/someone/cool-widget", + AutoStart: true, + }) + + err := SaveProviderRegistryWithMedium(medium, path, reg) + require.NoError(t, err) + + loaded, err := LoadProviderRegistryWithMedium(medium, path) + require.NoError(t, err) + assert.Equal(t, 1, loaded.Version) + assert.Len(t, loaded.Providers, 1) +} + func TestProviderRegistry_Add_Good(t *testing.T) { reg := &ProviderRegistryFile{ Version: 1,