package identityruntime import ( "context" "errors" "testing" "github.com/easyai/easyai-ai-gateway/apps/api/internal/identity" ) type runtimeRepositoryFake struct { revisions map[string]identity.Revision active identity.Revision activateCalled bool } func (f *runtimeRepositoryFake) IdentityConfigurationRevision(_ context.Context, id string) (identity.Revision, error) { revision, ok := f.revisions[id] if !ok { return identity.Revision{}, identity.ErrRevisionNotFound } return revision, nil } func (f *runtimeRepositoryFake) ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error) { if f.active.ID == "" { return identity.Revision{}, identity.ErrRevisionNotFound } return f.active, nil } func (f *runtimeRepositoryFake) MarkIdentityRevisionValidated(_ context.Context, id string, expected int64, _, _ string) (identity.Revision, error) { revision := f.revisions[id] if revision.Version != expected { return identity.Revision{}, identity.ErrRevisionConflict } revision.State, revision.Version = identity.RevisionValidated, revision.Version+1 f.revisions[id] = revision return revision, nil } func (f *runtimeRepositoryFake) MarkIdentityRevisionFailed(_ context.Context, id string, expected int64, category, _, _ string) (identity.Revision, error) { revision := f.revisions[id] if revision.Version != expected { return identity.Revision{}, identity.ErrRevisionConflict } revision.State, revision.Version, revision.LastErrorCategory = identity.RevisionFailed, revision.Version+1, category f.revisions[id] = revision return revision, nil } func (f *runtimeRepositoryFake) ActivateIdentityRevision(_ context.Context, id string, expected int64, traceID, auditID string) (identity.Revision, bool, error) { f.activateCalled = true revision := f.revisions[id] if revision.Version != expected || revision.State != identity.RevisionValidated { return identity.Revision{}, false, identity.ErrRevisionConflict } if f.active.ID != "" { old := f.active old.State = identity.RevisionSuperseded f.revisions[old.ID] = old } revision.State, revision.Version, revision.LastTraceID, revision.LastAuditID = identity.RevisionActive, revision.Version+1, traceID, auditID f.active, f.revisions[id] = revision, revision return revision, true, nil } func (f *runtimeRepositoryFake) DisableActiveIdentityRevision(_ context.Context, expected int64, traceID, auditID string) (identity.Revision, error) { if f.active.Version != expected { return identity.Revision{}, identity.ErrRevisionConflict } disabled := f.active disabled.State, disabled.Version, disabled.LastTraceID, disabled.LastAuditID = identity.RevisionSuperseded, disabled.Version+1, traceID, auditID f.revisions[disabled.ID] = disabled f.active = identity.Revision{} return disabled, nil } type runtimeBuilderFake struct { err error builtIDs []string } func (f *runtimeBuilderFake) Build(_ context.Context, revision identity.Revision) (*Runtime, error) { f.builtIDs = append(f.builtIDs, revision.ID) if f.err != nil { return nil, f.err } return &Runtime{Revision: revision}, nil } func TestValidationFailureKeepsCurrentRuntimeAndDoesNotActivate(t *testing.T) { active := identity.Revision{ID: "active", State: identity.RevisionActive, Version: 4} draft := identity.Revision{ID: "draft", State: identity.RevisionDraft, Version: 1} repository := &runtimeRepositoryFake{revisions: map[string]identity.Revision{"active": active, "draft": draft}, active: active} builder := &runtimeBuilderFake{err: errors.New("discovery failed")} manager := NewManager(repository, builder) manager.current.Store(&Runtime{Revision: active}) if _, err := manager.Validate(context.Background(), draft.ID, draft.Version, "trace", "audit"); err == nil { t.Fatal("validation failure was ignored") } if manager.Current().Revision.ID != active.ID || repository.activateCalled { t.Fatal("validation failure changed current runtime or activated the draft") } if repository.revisions[draft.ID].State != identity.RevisionFailed { t.Fatal("failed draft was not marked failed") } } func TestActivationSwapsRuntimeOnlyAfterRepositoryActivation(t *testing.T) { active := identity.Revision{ID: "old", State: identity.RevisionActive, Version: 2} candidate := identity.Revision{ID: "new", State: identity.RevisionValidated, Version: 3} repository := &runtimeRepositoryFake{revisions: map[string]identity.Revision{"old": active, "new": candidate}, active: active} manager := NewManager(repository, &runtimeBuilderFake{}) manager.current.Store(&Runtime{Revision: active}) activated, err := manager.Activate(context.Background(), candidate.ID, candidate.Version, "trace", "audit") if err != nil { t.Fatal(err) } if !repository.activateCalled || activated.State != identity.RevisionActive || manager.Current().Revision.ID != candidate.ID { t.Fatalf("activation order failed: activated=%#v current=%#v", activated, manager.Current()) } } func TestRollbackValidatesAndAtomicallyReplacesActiveRuntime(t *testing.T) { active := identity.Revision{ID: "current", State: identity.RevisionActive, Version: 5} previous := identity.Revision{ID: "previous", State: identity.RevisionSuperseded, Version: 3} repository := &runtimeRepositoryFake{ revisions: map[string]identity.Revision{"current": active, "previous": previous}, active: active, } manager := NewManager(repository, &runtimeBuilderFake{}) manager.current.Store(&Runtime{Revision: active}) rolledBack, err := manager.Rollback(context.Background(), previous.ID, previous.Version, "trace", "audit") if err != nil { t.Fatal(err) } if rolledBack.State != identity.RevisionActive || manager.Current().Revision.ID != previous.ID || repository.active.ID != previous.ID { t.Fatalf("rollback did not replace active runtime: revision=%#v current=%#v", rolledBack, manager.Current()) } }