feat(importer): availability probe + projects-only run
Create service/importer.Manager with Status (physical availability check only, no DB heuristic) and Run (delegates to legacyimport.Run). The app-state.json marker governs whether to prompt; this service only answers whether legacy data is physically present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7295956520
commit
666d5ba028
|
|
@ -0,0 +1,67 @@
|
|||
// Package importer is the controller-facing service for the legacy-AO import.
|
||||
// It wraps the internal/legacyimport engine with a detection probe (is a legacy
|
||||
// install present?) and a trigger that runs the import through the live daemon's
|
||||
// store, so the daemon stays the sole writer. Whether to PROMPT for the import
|
||||
// is the desktop app's job (the app-state.json migration marker), so this probe
|
||||
// reports only physical availability, not "already imported".
|
||||
package importer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aoagents/agent-orchestrator/backend/internal/legacyimport"
|
||||
)
|
||||
|
||||
// Store is the storage slice the import runs through; *sqlite.Store satisfies it.
|
||||
type Store interface {
|
||||
legacyimport.Store
|
||||
}
|
||||
|
||||
// Status reports whether a legacy AO install is physically present to import.
|
||||
type Status struct {
|
||||
Available bool `json:"available"`
|
||||
LegacyRoot string `json:"legacyRoot"`
|
||||
}
|
||||
|
||||
// Service is the controller-facing import contract.
|
||||
type Service interface {
|
||||
Status(ctx context.Context) (Status, error)
|
||||
Run(ctx context.Context) (legacyimport.Report, error)
|
||||
}
|
||||
|
||||
// Deps bundles the import service's dependencies.
|
||||
type Deps struct {
|
||||
// Store is the rewrite's durable store (the daemon's shared *sqlite.Store).
|
||||
Store Store
|
||||
// Root overrides the legacy AO root to read. Empty -> the default.
|
||||
Root string
|
||||
}
|
||||
|
||||
// Manager implements Service over the daemon's store.
|
||||
type Manager struct {
|
||||
store Store
|
||||
root string
|
||||
}
|
||||
|
||||
var _ Service = (*Manager)(nil)
|
||||
|
||||
// New constructs the import service. An empty Root falls back to the default.
|
||||
func New(deps Deps) *Manager {
|
||||
root := deps.Root
|
||||
if root == "" {
|
||||
root = legacyimport.DefaultLegacyRootDir()
|
||||
}
|
||||
return &Manager{store: deps.Store, root: root}
|
||||
}
|
||||
|
||||
// Status reports availability only: legacy data present at the root. It never
|
||||
// errors on a missing legacy store; that is simply "not available".
|
||||
func (m *Manager) Status(_ context.Context) (Status, error) {
|
||||
return Status{Available: legacyimport.HasLegacyData(m.root), LegacyRoot: m.root}, nil
|
||||
}
|
||||
|
||||
// Run executes the import through the daemon's store. Idempotent: the engine
|
||||
// skips rows that already exist. Legacy files are never modified.
|
||||
func (m *Manager) Run(ctx context.Context) (legacyimport.Report, error) {
|
||||
return legacyimport.Run(ctx, m.store, legacyimport.Options{Root: m.root})
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package importer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
|
||||
)
|
||||
|
||||
type fakeStore struct{ projects map[string]domain.ProjectRecord }
|
||||
|
||||
func newFakeStore() *fakeStore { return &fakeStore{projects: map[string]domain.ProjectRecord{}} }
|
||||
func (f *fakeStore) GetProject(_ context.Context, id string) (domain.ProjectRecord, bool, error) {
|
||||
r, ok := f.projects[id]
|
||||
return r, ok, nil
|
||||
}
|
||||
func (f *fakeStore) UpsertProject(_ context.Context, r domain.ProjectRecord) error {
|
||||
f.projects[r.ID] = r
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeLegacyRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := filepath.Join(t.TempDir(), ".agent-orchestrator")
|
||||
if err := os.MkdirAll(filepath.Join(root, "projects"), 0o750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := "projects:\n alpha:\n path: /repos/alpha\n name: Alpha\n"
|
||||
if err := os.WriteFile(filepath.Join(root, "config.yaml"), []byte(cfg), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func TestStatus_NoLegacyData(t *testing.T) {
|
||||
svc := New(Deps{Store: newFakeStore(), Root: filepath.Join(t.TempDir(), "nope")})
|
||||
st, err := svc.Status(context.Background())
|
||||
if err != nil || st.Available {
|
||||
t.Fatalf("want unavailable; got %+v err=%v", st, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus_LegacyPresentStaysAvailableAfterImport(t *testing.T) {
|
||||
root := writeLegacyRoot(t)
|
||||
svc := New(Deps{Store: newFakeStore(), Root: root})
|
||||
st, err := svc.Status(context.Background())
|
||||
if err != nil || !st.Available || st.LegacyRoot != root {
|
||||
t.Fatalf("want available at %q; got %+v err=%v", root, st, err)
|
||||
}
|
||||
if _, err := svc.Run(context.Background()); err != nil {
|
||||
t.Fatalf("run: %v", err)
|
||||
}
|
||||
// Availability is physical (legacy data still on disk), so it stays true; the
|
||||
// app marker is what stops the prompt after a completed import.
|
||||
st, _ = svc.Status(context.Background())
|
||||
if !st.Available {
|
||||
t.Fatal("availability must remain true after import (marker governs prompting)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ImportsProjects(t *testing.T) {
|
||||
root := writeLegacyRoot(t)
|
||||
svc := New(Deps{Store: newFakeStore(), Root: root})
|
||||
rep, err := svc.Run(context.Background())
|
||||
if err != nil || rep.ProjectsImported != 1 {
|
||||
t.Fatalf("projectsImported=%d err=%v", rep.ProjectsImported, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_DefaultsRoot(t *testing.T) {
|
||||
if New(Deps{Store: newFakeStore()}).root == "" {
|
||||
t.Fatal("empty Root should fall back to the default legacy root")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue