Repository: apigee/registry
Pull request #653 (2022-07-08 00:30:41)

Update timestamp condition to figure out resource regeneration
Fixes https://github.com/apigee/registry/issues/641

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/653?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#653](https://codecov.io/gh/apigee/registry/pull/653?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (7b29c96) into [main](https://codecov.io/gh/apigee/registry/commit/62bdd9fe270638674e22abd212a4966619b9edd3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (62bdd9f) will **increase** coverage by `0.05%`.
> The diff coverage is `61.95%`.

```diff
@@            Coverage Diff             @@
##             main     #653      +/-   ##
==========================================
+ Coverage   53.91%   53.96%   +0.05%     
==========================================
  Files          77       78       +1     
  Lines        7222     7306      +84     
==========================================
+ Hits         3894     3943      +49     
- Misses       2914     2940      +26     
- Partials      414      423       +9     
```


| [Impacted Files](https://codecov.io/gh/apigee/registry/pull/653?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) | Coverage Δ | |
|---|---|---|
| [cmd/registry/patterns/resources.go](https://codecov.io/gh/apigee/registry/pull/653/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L3BhdHRlcm5zL3Jlc291cmNlcy5nbw==) | `19.50% <ø> (ø)` | |
| [cmd/registry/controller/list.go](https://codecov.io/gh/apigee/registry/pull/653/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NvbnRyb2xsZXIvbGlzdC5nbw==) | `57.83% <57.83%> (ø)` | |
| [cmd/registry/cmd/resolve/resolve.go](https://codecov.io/gh/apigee/registry/pull/653/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9yZXNvbHZlL3Jlc29sdmUuZ28=) | `72.85% <100.00%> (+0.79%)` | :arrow_up: |
| [cmd/registry/controller/controller.go](https://codecov.io/gh/apigee/registry/pull/653/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NvbnRyb2xsZXIvY29udHJvbGxlci5nbw==) | `75.43% <100.00%> (-0.11%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/apigee/registry/pull/653?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/apigee/registry/pull/653?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee). Last update [62bdd9f...7b29c96](https://codecov.io/gh/apigee/registry/pull/653?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee).


Patch:
diff --git a/cmd/registry/cmd/resolve/resolve.go b/cmd/registry/cmd/resolve/resolve.go
index d97e6b0..a6fb9d1 100644
--- a/cmd/registry/cmd/resolve/resolve.go
+++ b/cmd/registry/cmd/resolve/resolve.go
@@ -66,16 +66,18 @@ func Command() *cobra.Command {
 				log.FromContext(ctx).WithError(err).Fatal("Invalid manifest resource name")
 			}
 
-			client, err := connection.NewRegistryClient(ctx)
+			registryClient, err := connection.NewRegistryClient(ctx)
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
 
-			manifest, err := fetchManifest(ctx, client, name.String())
+			manifest, err := fetchManifest(ctx, registryClient, name.String())
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to fetch manifest")
 			}
 
+			client := &controller.RegistryLister{RegistryClient: registryClient}
+
 			log.Debug(ctx, "Generating the list of actions...")
 			actions := controller.ProcessManifest(ctx, client, name.ProjectID(), manifest)
 
diff --git a/cmd/registry/controller/controller.go b/cmd/registry/controller/controller.go
index 785d059..95efc27 100644
--- a/cmd/registry/controller/controller.go
+++ b/cmd/registry/controller/controller.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/apigee/registry/cmd/registry/patterns"
 	"github.com/apigee/registry/log"
-	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/rpc"
 )
 
@@ -34,7 +33,7 @@ type Action struct {
 
 func ProcessManifest(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client listingClient,
 	projectID string,
 	manifest *rpc.Manifest) []*Action {
 	var actions []*Action
@@ -68,7 +67,7 @@ func ProcessManifest(
 
 func processManifestResource(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client listingClient,
 	projectID string,
 	generatedResource *rpc.GeneratedResource) ([]*Action, error) {
 	resourcePattern := fmt.Sprintf("projects/%s/locations/global/%s", projectID, generatedResource.Pattern)
@@ -91,7 +90,7 @@ func processManifestResource(
 
 func generateDependencyMap(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client listingClient,
 	resourcePattern string,
 	dependency *rpc.Dependency) (map[string]time.Time, error) {
 	// Creates a map of the resources to group them into corresponding buckets
@@ -118,7 +117,7 @@ func generateDependencyMap(
 	}
 
 	// Fetch resources using the extDependencyQuery
-	sourceList, err := patterns.ListResources(ctx, client, extDependencyName.String(), dependency.Filter)
+	sourceList, err := listResources(ctx, client, extDependencyName.String(), dependency.Filter)
 	if err != nil {
 		return nil, err
 	}
@@ -145,7 +144,7 @@ func generateDependencyMap(
 
 func generateActions(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client listingClient,
 	resourcePattern string,
 	filter string,
 	dependencyMaps []map[string]time.Time,
@@ -170,7 +169,7 @@ func generateActions(
 // Go over the list of existing target resources to figure out which ones need an update.
 func generateUpdateActions(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client listingClient,
 	resourcePattern string,
 	filter string,
 	dependencyMaps []map[string]time.Time,
@@ -180,7 +179,7 @@ func generateUpdateActions(
 	actions := make([]*Action, 0)
 
 	// Generate resource list
-	resourceList, err := patterns.ListResources(ctx, client, resourcePattern, filter)
+	resourceList, err := listResources(ctx, client, resourcePattern, filter)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -245,7 +244,7 @@ func excludeVisitedParents(v map[string]bool) string {
 // we will use the parent resources to derive which new target resources should be created.
 func generateCreateActions(
 	ctx context.Context,
-	client connection.RegistryClient,
+	client listingClient,
 	resourcePattern string,
 	dependencyMaps []map[string]time.Time,
 	generatedResource *rpc.GeneratedResource,
@@ -280,14 +279,13 @@ func generateCreateActions(
 
 	default:
 		filter := excludeVisitedParents(visited)
-		parentList, err = patterns.ListResources(ctx, client, parentName.String(), filter)
+		parentList, err = listResources(ctx, client, parentName.String(), filter)
 		if err != nil {
 			return nil, err
 		}
 	}
 
 	actions := make([]*Action, 0)
-
 	for _, parent := range parentList {
 		// Since the GeneratedResource is non-existent here,
 		// we will have to derive the exact name of the target resource
@@ -348,8 +346,10 @@ func needsUpdate(
 			return false, nil
 		}
 
-		if maxUpdateTime.After(targetResourceTime) {
-			return true, nil // Take action if atleast one dependency timestamp is later than resource timestamp
+		// Take action if the target resource is less than n seconds newer compared to the dependencies, where n=thresholdSeconds.
+		// https://github.com/apigee/registry/issues/641
+		if maxUpdateTime.Add(patterns.ResourceUpdateThresholdSeconds).After(targetResourceTime) {
+			return true, nil
 		}
 	}
 	return false, nil
diff --git a/cmd/registry/controller/list.go b/cmd/registry/controller/list.go
new file mode 100644
index 0000000..670266b
--- /dev/null
+++ b/cmd/registry/controller/list.go
@@ -0,0 +1,134 @@
+package controller
+
+import (
+	"context"
+
+	"github.com/apigee/registry/cmd/registry/core"
+	"github.com/apigee/registry/cmd/registry/patterns"
+	"github.com/apigee/registry/pkg/connection"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+)
+
+type listingClient interface {
+	ListAPIs(context.Context, names.Api, string, core.ApiHandler) error
+	ListVersions(context.Context, names.Version, string, core.VersionHandler) error
+	ListSpecs(context.Context, names.Spec, string, core.SpecHandler) error
+	ListArtifacts(context.Context, names.Artifact, string, bool, core.ArtifactHandler) error
+}
+
+type RegistryLister struct {
+	RegistryClient connection.RegistryClient
+}
+
+func (r *RegistryLister) ListAPIs(ctx context.Context, api names.Api, filter string, handler core.ApiHandler) error {
+	return core.ListAPIs(ctx, r.RegistryClient, api, filter, handler)
+}
+
+func (r *RegistryLister) ListVersions(ctx context.Context, version names.Version, filter string, handler core.VersionHandler) error {
+	return core.ListVersions(ctx, r.RegistryClient, version, filter, handler)
+}
+
+func (r *RegistryLister) ListSpecs(ctx context.Context, spec names.Spec, filter string, handler core.SpecHandler) error {
+	return core.ListSpecs(ctx, r.RegistryClient, spec, filter, handler)
+}
+
+func (r *RegistryLister) ListArtifacts(ctx context.Context, artifact names.Artifact, filter string, contents bool, handler core.ArtifactHandler) error {
+	return core.ListArtifacts(ctx, r.RegistryClient, artifact, filter, contents, handler)
+}
+
+func listResources(ctx context.Context, client listingClient, pattern, filter string) ([]patterns.ResourceInstance, error) {
+	var result []patterns.ResourceInstance
+	var err2 error
+
+	// First try to match collection names.
+	if api, err := names.ParseApiCollection(pattern); err == nil {
+		err2 = client.ListAPIs(ctx, api, filter, generateApiHandler(&result))
+	} else if version, err := names.ParseVersionCollection(pattern); err == nil {
+		err2 = client.ListVersions(ctx, version, filter, generateVersionHandler(&result))
+	} else if spec, err := names.ParseSpecCollection(pattern); err == nil {
+		err2 = client.ListSpecs(ctx, spec, filter, generateSpecHandler(&result))
+	} else if artifact, err := names.ParseArtifactCollection(pattern); err == nil {
+		err2 = client.ListArtifacts(ctx, artifact, filter, false, generateArtifactHandler(&result))
+	}
+
+	// Then try to match resource names.
+	if api, err := names.ParseApi(pattern); err == nil {
+		err2 = client.ListAPIs(ctx, api, filter, generateApiHandler(&result))
+	} else if version, err := names.ParseVersion(pattern); err == nil {
+		err2 = client.ListVersions(ctx, version, filter, generateVersionHandler(&result))
+	} else if spec, err := names.ParseSpec(pattern); err == nil {
+		err2 = client.ListSpecs(ctx, spec, filter, generateSpecHandler(&result))
+	} else if artifact, err := names.ParseArtifact(pattern); err == nil {
+		err2 = client.ListArtifacts(ctx, artifact, filter, false, generateArtifactHandler(&result))
+	}
+
+	if err2 != nil {
+		return nil, err2
+	}
+	return result, nil
+}
+
+func generateApiHandler(result *[]patterns.ResourceInstance) func(*rpc.Api) error {
+	return func(api *rpc.Api) error {
+		name, err := names.ParseApi(api.GetName())
+		if err != nil {
+			return err
+		}
+
+		(*result) = append((*result), patterns.ApiResource{
+			ApiName:   patterns.ApiName{Name: name},
+			Timestamp: api.UpdateTime.AsTime(),
+		})
+
+		return nil
+	}
+}
+
+func generateVersionHandler(result *[]patterns.ResourceInstance) func(*rpc.ApiVersion) error {
+	return func(version *rpc.ApiVersion) error {
+		name, err := names.ParseVersion(version.GetName())
+		if err != nil {
+			return err
+		}
+
+		(*result) = append((*result), patterns.VersionResource{
+			VersionName: patterns.VersionName{Name: name},
+			Timestamp:   version.UpdateTime.AsTime(),
+		})
+
+		return nil
+	}
+}
+
+func generateSpecHandler(result *[]patterns.ResourceInstance) func(*rpc.ApiSpec) error {
+	return func(spec *rpc.ApiSpec) error {
+		name, err := names.ParseSpec(spec.GetName())
+		if err != nil {
+			return err
+		}
+
+		(*result) = append((*result), patterns.SpecResource{
+			SpecName:  patterns.SpecName{Name: name},
+			Timestamp: spec.RevisionUpdateTime.AsTime(),
+		})
+
+		return nil
+	}
+}
+
+func generateArtifactHandler(result *[]patterns.ResourceInstance) func(*rpc.Artifact) error {
+	return func(artifact *rpc.Artifact) error {
+		name, err := names.ParseArtifact(artifact.GetName())
+		if err != nil {
+			return err
+		}
+
+		(*result) = append((*result), patterns.ArtifactResource{
+			ArtifactName: patterns.ArtifactName{Name: name},
+			Timestamp:    artifact.UpdateTime.AsTime(),
+		})
+
+		return nil
+	}
+}
diff --git a/cmd/registry/patterns/resources.go b/cmd/registry/patterns/resources.go
index 276350e..f0c01ed 100644
--- a/cmd/registry/patterns/resources.go
+++ b/cmd/registry/patterns/resources.go
@@ -24,6 +24,8 @@ import (
 	"github.com/apigee/registry/server/registry/names"
 )
 
+const ResourceUpdateThresholdSeconds = time.Second * 2
+
 // This interface is used to describe generic resource names
 // Example:
 // projects/demo/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml


Test patch:
diff --git a/cmd/registry/controller/controller-error-path_test.go b/cmd/registry/controller/controller-error-path_test.go
index ce814ef..af31e55 100644
--- a/cmd/registry/controller/controller-error-path_test.go
+++ b/cmd/registry/controller/controller-error-path_test.go
@@ -21,8 +21,26 @@ import (
 	"github.com/apigee/registry/pkg/connection"
 	"github.com/apigee/registry/rpc"
 	"github.com/apigee/registry/server/registry/test/seeder"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 )
 
+func deleteProject(
+	ctx context.Context,
+	client connection.AdminClient,
+	t *testing.T,
+	projectID string) {
+	t.Helper()
+	req := &rpc.DeleteProjectRequest{
+		Name:  "projects/" + projectID,
+		Force: true,
+	}
+	err := client.DeleteProject(ctx, req)
+	if err != nil && status.Code(err) != codes.NotFound {
+		t.Fatalf("Failed DeleteProject(%v): %s", req, err.Error())
+	}
+}
+
 // Tests for error paths in the controller
 
 func TestControllerErrors(t *testing.T) {
@@ -180,8 +198,10 @@ func TestControllerErrors(t *testing.T) {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
 			}
 
+			lister := &RegistryLister{RegistryClient: registryClient}
+
 			// Test GeneratedResource pattern
-			actions, err := processManifestResource(ctx, registryClient, projectID, test.generatedResource)
+			actions, err := processManifestResource(ctx, lister, projectID, test.generatedResource)
 			if err == nil {
 				t.Errorf("Expected processManifestResource() to return an error, got: %v", actions)
 			}
diff --git a/cmd/registry/controller/controller-timestamp_test.go b/cmd/registry/controller/controller-timestamp_test.go
new file mode 100644
index 0000000..89242a6
--- /dev/null
+++ b/cmd/registry/controller/controller-timestamp_test.go
@@ -0,0 +1,835 @@
+package controller
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/apigee/registry/cmd/registry/core"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+	"github.com/apigee/registry/server/registry/test/seeder"
+	"github.com/google/go-cmp/cmp"
+	"google.golang.org/protobuf/types/known/durationpb"
+	"google.golang.org/protobuf/types/known/timestamppb"
+)
+
+type fakeLister struct {
+	projects    []*rpc.Project
+	apis        []*rpc.Api
+	versions    []*rpc.ApiVersion
+	specs       []*rpc.ApiSpec
+	artifacts   []*rpc.Artifact
+	deployments []*rpc.ApiDeployment
+}
+
+// This implementation doesn't support filter functionality
+func (f *fakeLister) ListAPIs(ctx context.Context, api names.Api, filter string, handler core.ApiHandler) error {
+	for _, a := range f.apis {
+		name, _ := names.ParseApi(a.GetName())
+		if strings.Contains(filter, name.Parent()) || (api.ApiID != "-" && name.ApiID != api.ApiID) {
+			continue
+		}
+		if err := handler(a); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (f *fakeLister) ListVersions(ctx context.Context, version names.Version, filter string, handler core.VersionHandler) error {
+	for _, v := range f.versions {
+		name, _ := names.ParseVersion(v.GetName())
+		if strings.Contains(filter, name.Parent()) || (version.VersionID != "-" && name.VersionID != version.VersionID) {
+			continue
+		}
+		if err := handler(v); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (f *fakeLister) ListSpecs(ctx context.Context, spec names.Spec, filter string, handler core.SpecHandler) error {
+	for _, s := range f.specs {
+		name, _ := names.ParseSpec(s.GetName())
+		if strings.Contains(filter, name.Parent()) || (spec.SpecID != "-" && name.SpecID != spec.SpecID) {
+			continue
+		}
+
+		if err := handler(s); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (f *fakeLister) ListArtifacts(ctx context.Context, artifact names.Artifact, filter string, contents bool, handler core.ArtifactHandler) error {
+	for _, a := range f.artifacts {
+		name, _ := names.ParseArtifact(a.GetName())
+		if strings.Contains(filter, name.Parent()) || (artifact.ArtifactID() != "-" && name.ArtifactID() != artifact.ArtifactID()) {
+			continue
+		}
+
+		if err := handler(a); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// These functions are needed to use the fakeLister with the seeder package.
+func (f *fakeLister) CreateProject(ctx context.Context, req *rpc.CreateProjectRequest) (*rpc.Project, error) {
+	project := &rpc.Project{
+		Name: fmt.Sprintf("projects/%s", req.GetProjectId()),
+	}
+	f.projects = append(f.projects, project)
+	return project, nil
+}
+
+func (f *fakeLister) UpdateApi(ctx context.Context, req *rpc.UpdateApiRequest) (*rpc.Api, error) {
+	api := req.Api
+	if api.UpdateTime == nil {
+		api.UpdateTime = timestamppb.Now()
+	}
+	for i, a := range f.apis {
+		if a.GetName() == api.GetName() {
+			// remove the old copy
+			f.apis = append(f.apis[:i], f.apis[i+1:]...)
+			break
+		}
+	}
+	f.apis = append(f.apis, api)
+	return api, nil
+}
+
+func (f *fakeLister) UpdateApiVersion(ctx context.Context, req *rpc.UpdateApiVersionRequest) (*rpc.ApiVersion, error) {
+	version := req.ApiVersion
+	if version.UpdateTime == nil {
+		version.UpdateTime = timestamppb.Now()
+	}
+	for i, v := range f.versions {
+		if v.GetName() == version.GetName() {
+			// remove the old copy
+			f.versions = append(f.versions[:i], f.versions[i+1:]...)
+			break
+		}
+	}
+	f.versions = append(f.versions, version)
+	return version, nil
+}
+
+func (f *fakeLister) UpdateApiSpec(ctx context.Context, req *rpc.UpdateApiSpecRequest) (*rpc.ApiSpec, error) {
+	spec := req.ApiSpec
+	if spec.RevisionUpdateTime == nil {
+		spec.RevisionUpdateTime = timestamppb.Now()
+	}
+	for i, s := range f.specs {
+		if s.GetName() == spec.GetName() {
+			// remove the old copy
+			f.specs = append(f.specs[:i], f.specs[i+1:]...)
+			break
+		}
+	}
+	f.specs = append(f.specs, spec)
+	return spec, nil
+}
+
+func (f *fakeLister) UpdateApiDeployment(ctx context.Context, req *rpc.UpdateApiDeploymentRequest) (*rpc.ApiDeployment, error) {
+	deployment := req.ApiDeployment
+	if deployment.RevisionUpdateTime == nil {
+		deployment.RevisionUpdateTime = timestamppb.Now()
+	}
+	for i, d := range f.deployments {
+		if d.GetName() == deployment.GetName() {
+			// remove the old copy
+			f.deployments = append(f.deployments[:i], f.deployments[i+1:]...)
+			break
+		}
+	}
+	f.deployments = append(f.deployments, deployment)
+	return deployment, nil
+}
+
+func (f *fakeLister) CreateArtifact(ctx context.Context, req *rpc.CreateArtifactRequest) (*rpc.Artifact, error) {
+	artifact := req.Artifact
+	if artifact.UpdateTime == nil {
+		artifact.UpdateTime = timestamppb.Now()
+	}
+	f.artifacts = append(f.artifacts, artifact)
+	return artifact, nil
+}
+
+func (f *fakeLister) ReplaceArtifact(ctx context.Context, req *rpc.ReplaceArtifactRequest) (*rpc.Artifact, error) {
+	artifact := req.Artifact
+	if artifact.UpdateTime == nil {
+		artifact.UpdateTime = timestamppb.Now()
+	}
+	for i, a := range f.artifacts {
+		if a.GetName() == artifact.GetName() {
+			// remove the old copy
+			f.artifacts = append(f.artifacts[:i], f.artifacts[i+1:]...)
+			break
+		}
+	}
+	f.artifacts = append(f.artifacts, artifact)
+	return artifact, nil
+}
+
+// Tests for artifacts as resources and specs as dependencies
+func TestTimestampArtifacts(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []*Action
+	}{
+		{
+			desc: "partially existing artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					MimeType:           gzipOpenAPIv3,
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 2)),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+			},
+		},
+		{
+			desc: "outdated artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType:           gzipOpenAPIv3,
+					RevisionUpdateTime: timestamppb.New(time.Now()),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+				&rpc.ApiSpec{
+					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					MimeType: gzipOpenAPIv3,
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+			},
+		},
+		{
+			desc: "not recent enough artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					MimeType:           gzipOpenAPIv3,
+					RevisionUpdateTime: timestamppb.New(time.Now()),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					MimeType:           gzipOpenAPIv3,
+					RevisionUpdateTime: timestamppb.New(time.Now()),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					MimeType:           gzipOpenAPIv3,
+					RevisionUpdateTime: timestamppb.New(time.Now()),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+				{
+					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml --linter gnostic",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+				},
+			},
+		},
+	}
+
+	const projectID = "controller-test"
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+
+			client := new(fakeLister)
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			manifest := &rpc.Manifest{
+				Id: "controller-test",
+				GeneratedResources: []*rpc.GeneratedResource{
+					{
+						Pattern: "apis/-/versions/-/specs/-/artifacts/lint-gnostic",
+						Dependencies: []*rpc.Dependency{
+							{
+								Pattern: "$resource.spec",
+								Filter:  "mime_type.contains('openapi')",
+							},
+						},
+						Action: "registry compute lint $resource.spec --linter gnostic",
+					},
+				},
+			}
+			actions := ProcessManifest(ctx, client, projectID, manifest)
+
+			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
+				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
+			}
+		})
+	}
+}
+
+// Tests for aggregated artifacts at api level and specs as resources
+func TestTimestampAggregateArtifacts(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []*Action
+	}{
+		{
+			desc: "outdated artifacts",
+			seed: []seeder.RegistryResource{
+				// test api 1
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-1/versions/1.0.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-1/versions/1.1.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-1/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/test-api-1/artifacts/vocabulary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+				// test api 2
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.1.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/test-api-2/artifacts/vocabulary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 3)),
+				},
+				// Update underlying spec to make artifact outdated
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * 4)),
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute vocabulary projects/controller-test/locations/global/apis/test-api-2",
+					GeneratedResource: "projects/controller-test/locations/global/apis/test-api-2/artifacts/vocabulary",
+				},
+			},
+		},
+		{
+			desc: "not recent enough artifacts",
+			seed: []seeder.RegistryResource{
+				// test api 1
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-1/versions/1.0.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-1/versions/1.1.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-1/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/test-api-1/artifacts/vocabulary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)), // should be atleast 2 seconds newer than dependencies.
+				},
+				// test api 2
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.1.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/test-api-2/artifacts/vocabulary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)), // should be atleast 2 seconds newer than dependencies.
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute vocabulary projects/controller-test/locations/global/apis/test-api-1",
+					GeneratedResource: "projects/controller-test/locations/global/apis/test-api-1/artifacts/vocabulary",
+				},
+				{
+					Command:           "registry compute vocabulary projects/controller-test/locations/global/apis/test-api-2",
+					GeneratedResource: "projects/controller-test/locations/global/apis/test-api-2/artifacts/vocabulary",
+				},
+			},
+		},
+	}
+
+	const projectID = "controller-test"
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+
+			client := new(fakeLister)
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			manifest := &rpc.Manifest{
+
+				Id: "controller-test",
+				GeneratedResources: []*rpc.GeneratedResource{
+					{
+						Pattern: "apis/-/artifacts/vocabulary",
+						Dependencies: []*rpc.Dependency{
+							{
+								Pattern: "$resource.api/versions/-/specs/-",
+							},
+						},
+						Action: "registry compute vocabulary $resource.api",
+					},
+				},
+			}
+			actions := ProcessManifest(ctx, client, projectID, manifest)
+
+			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
+				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
+			}
+		})
+	}
+}
+
+// Tests for derived artifacts with artifacts as dependencies
+func TestTimestampDerivedArtifacts(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []*Action
+	}{
+		{
+			desc: "outdated artifacts",
+			seed: []seeder.RegistryResource{
+				// version 1.0.0
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/summary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 2)),
+				},
+				// version 1.0.1
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/summary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 2)),
+				},
+				// version 1.1.0
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/summary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 2)),
+				},
+				// Make some artifacts outdated from the above setup
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 4)),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 4)),
+				},
+			},
+			want: []*Action{
+				{
+					Command: fmt.Sprintf(
+						"registry compute summary %s %s",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity"),
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/summary",
+				},
+				{
+					Command: fmt.Sprintf(
+						"registry compute summary %s %s",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity"),
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/summary",
+				},
+			},
+		},
+		{
+			desc: "not recent enough artifacts",
+			seed: []seeder.RegistryResource{
+				// version 1.0.0
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/summary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+				// version 1.0.1
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/summary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+				// version 1.1.0
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity",
+					UpdateTime: timestamppb.Now(),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/summary",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * 1)),
+				},
+			},
+			want: []*Action{
+				{
+					Command: fmt.Sprintf(
+						"registry compute summary %s %s",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity"),
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/summary",
+				},
+				{
+					Command: fmt.Sprintf(
+						"registry compute summary %s %s",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/complexity"),
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/summary",
+				},
+				{
+					Command: fmt.Sprintf(
+						"registry compute summary %s %s",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
+						"projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity"),
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/summary",
+				},
+			},
+		},
+	}
+
+	const projectID = "controller-test"
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+
+			client := new(fakeLister)
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			manifest := &rpc.Manifest{
+				Id: "controller-test",
+				GeneratedResources: []*rpc.GeneratedResource{
+					{
+						Pattern: "apis/-/versions/-/specs/-/artifacts/summary",
+						Dependencies: []*rpc.Dependency{
+							{
+								Pattern: "$resource.spec/artifacts/lint-gnostic",
+							},
+							{
+								Pattern: "$resource.spec/artifacts/complexity",
+							},
+						},
+						Action: "registry compute summary $resource.spec/artifacts/lint-gnostic $resource.spec/artifacts/complexity",
+					},
+				},
+			}
+			actions := ProcessManifest(ctx, client, projectID, manifest)
+
+			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
+				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
+			}
+		})
+	}
+}
+
+func TestRefreshArtifacts(t *testing.T) {
+	tests := []struct {
+		desc string
+		seed []seeder.RegistryResource
+		want []*Action
+	}{
+		{
+			desc: "non-existing artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+				},
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+				},
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+			},
+		},
+		{
+			desc: "existing valid artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+				},
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+				},
+				&rpc.ApiSpec{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+			},
+			want: nil,
+		},
+		{
+			desc: "existing invalid artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * -5)),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * -5)),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * -5)),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * -3)),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * -3)),
+				},
+				&rpc.Artifact{
+					Name:       "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+					UpdateTime: timestamppb.New(time.Now().Add(time.Second * -3)),
+				},
+			},
+			want: []*Action{
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+				{
+					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+					RequiresReceipt:   true,
+				},
+			},
+		},
+		{
+			desc: "existing valid artifacts",
+			seed: []seeder.RegistryResource{
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * -2)),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * -2)),
+				},
+				&rpc.ApiSpec{
+					Name:               "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
+					RevisionUpdateTime: timestamppb.New(time.Now().Add(time.Second * -2)),
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
+				},
+				&rpc.Artifact{
+					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
+				},
+			},
+			want: nil,
+		},
+	}
+
+	const projectID = "controller-test"
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			client := new(fakeLister)
+
+			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
+				t.Fatalf("Setup: failed to seed registry: %s", err)
+			}
+
+			// time.Sleep(test.wait * time.Second)
+
+			manifest := &rpc.Manifest{
+				Id: "controller-test",
+				GeneratedResources: []*rpc.GeneratedResource{
+					{
+						Pattern: "apis/-/versions/-/specs/-/artifacts/score-receipt",
+						Receipt: true,
+						Refresh: &durationpb.Duration{
+							Seconds: 2,
+						},
+						Action: "registry compute score $resource.spec",
+					},
+				},
+			}
+			actions := ProcessManifest(ctx, client, projectID, manifest)
+
+			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
+				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
+			}
+		})
+	}
+}
diff --git a/cmd/registry/controller/controller_test.go b/cmd/registry/controller/controller_test.go
index f3fa9e6..ad28c68 100644
--- a/cmd/registry/controller/controller_test.go
+++ b/cmd/registry/controller/controller_test.go
@@ -18,7 +18,6 @@ import (
 	"context"
 	"fmt"
 	"testing"
-	"time"
 
 	"github.com/apigee/registry/cmd/registry/core"
 	"github.com/apigee/registry/pkg/connection"
@@ -28,10 +27,7 @@ import (
 	"github.com/apigee/registry/server/registry/test/seeder"
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp/cmpopts"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/types/known/durationpb"
 )
 
 // TestMain will set up a local RegistryServer and grpc.Server for all
@@ -68,22 +64,6 @@ func protoMarshal(m proto.Message) []byte {
 	return b
 }
 
-func deleteProject(
-	ctx context.Context,
-	client connection.AdminClient,
-	t *testing.T,
-	projectID string) {
-	t.Helper()
-	req := &rpc.DeleteProjectRequest{
-		Name:  "projects/" + projectID,
-		Force: true,
-	}
-	err := client.DeleteProject(ctx, req)
-	if err != nil && status.Code(err) != codes.NotFound {
-		t.Fatalf("Failed DeleteProject(%v): %s", req, err.Error())
-	}
-}
-
 // Tests for artifacts as resources and specs as dependencies
 func TestArtifacts(t *testing.T) {
 	tests := []struct {
@@ -137,73 +117,6 @@ func TestArtifacts(t *testing.T) {
 				},
 			},
 		},
-		{
-			desc: "partially existing artifacts",
-			seed: []seeder.RegistryResource{
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-			},
-			want: []*Action{
-				{
-					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml --linter gnostic",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				{
-					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml --linter gnostic",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-			},
-		},
-		{
-			desc: "outdated artifacts",
-			seed: []seeder.RegistryResource{
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-				&rpc.ApiSpec{
-					Name:     "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
-					MimeType: gzipOpenAPIv3,
-				},
-			},
-			want: []*Action{
-				{
-					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml --linter gnostic",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				{
-					Command:           "registry compute lint projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml --linter gnostic",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-			},
-		},
 	}
 
 	const projectID = "controller-test"
@@ -229,6 +142,7 @@ func TestArtifacts(t *testing.T) {
 				RegistryClient: registryClient,
 				AdminClient:    adminClient,
 			}
+			lister := &RegistryLister{RegistryClient: registryClient}
 
 			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
@@ -249,7 +163,7 @@ func TestArtifacts(t *testing.T) {
 					},
 				},
 			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+			actions := ProcessManifest(ctx, lister, projectID, manifest)
 
 			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
 				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
@@ -300,47 +214,6 @@ func TestAggregateArtifacts(t *testing.T) {
 				},
 			},
 		},
-		{
-			desc: "outdated arttifacts",
-			seed: []seeder.RegistryResource{
-				// test api 1
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-1/versions/1.0.0/specs/openapi.yaml",
-				},
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-1/versions/1.1.0/specs/openapi.yaml",
-				},
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-1/versions/1.0.1/specs/openapi.yaml",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/test-api-1/artifacts/vocabulary",
-				},
-				// test api 2
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.0/specs/openapi.yaml",
-				},
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-2/versions/1.1.0/specs/openapi.yaml",
-				},
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.1/specs/openapi.yaml",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/test-api-2/artifacts/vocabulary",
-				},
-				// Update underlying spec to make artifact outdated
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/test-api-2/versions/1.0.1/specs/openapi.yaml",
-				},
-			},
-			want: []*Action{
-				{
-					Command:           "registry compute vocabulary projects/controller-test/locations/global/apis/test-api-2",
-					GeneratedResource: "projects/controller-test/locations/global/apis/test-api-2/artifacts/vocabulary",
-				},
-			},
-		},
 	}
 
 	const projectID = "controller-test"
@@ -366,6 +239,7 @@ func TestAggregateArtifacts(t *testing.T) {
 				RegistryClient: registryClient,
 				AdminClient:    adminClient,
 			}
+			lister := &RegistryLister{RegistryClient: registryClient}
 
 			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
@@ -386,13 +260,11 @@ func TestAggregateArtifacts(t *testing.T) {
 					},
 				},
 			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+			actions := ProcessManifest(ctx, lister, projectID, manifest)
 
 			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
 				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
 			}
-
-			deleteProject(ctx, adminClient, t, "controller-test")
 		})
 	}
 }
@@ -482,64 +354,6 @@ func TestDerivedArtifacts(t *testing.T) {
 				},
 			},
 		},
-		{
-			desc: "outdated artifacts",
-			seed: []seeder.RegistryResource{
-				// version 1.0.0
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/summary",
-				},
-				// version 1.0.1
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/complexity",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/summary",
-				},
-				// version 1.1.0
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/summary",
-				},
-				// Make some artifacts outdated from the above setup
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity",
-				},
-			},
-			want: []*Action{
-				{
-					Command: fmt.Sprintf(
-						"registry compute summary %s %s",
-						"projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/lint-gnostic",
-						"projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/complexity"),
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/summary",
-				},
-				{
-					Command: fmt.Sprintf(
-						"registry compute summary %s %s",
-						"projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/lint-gnostic",
-						"projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/complexity"),
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/summary",
-				},
-			},
-		},
 	}
 
 	const projectID = "controller-test"
@@ -565,6 +379,7 @@ func TestDerivedArtifacts(t *testing.T) {
 				RegistryClient: registryClient,
 				AdminClient:    adminClient,
 			}
+			lister := &RegistryLister{RegistryClient: registryClient}
 
 			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
@@ -587,13 +402,11 @@ func TestDerivedArtifacts(t *testing.T) {
 					},
 				},
 			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+			actions := ProcessManifest(ctx, lister, projectID, manifest)
 
 			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
 				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
 			}
-
-			deleteProject(ctx, adminClient, t, "controller-test")
 		})
 	}
 }
@@ -661,6 +474,7 @@ func TestReceiptArtifacts(t *testing.T) {
 				RegistryClient: registryClient,
 				AdminClient:    adminClient,
 			}
+			lister := &RegistryLister{RegistryClient: registryClient}
 
 			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
@@ -681,13 +495,11 @@ func TestReceiptArtifacts(t *testing.T) {
 					},
 				},
 			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+			actions := ProcessManifest(ctx, lister, projectID, manifest)
 
 			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
 				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
 			}
-
-			deleteProject(ctx, adminClient, t, "controller-test")
 		})
 	}
 }
@@ -770,6 +582,7 @@ func TestReceiptAggArtifacts(t *testing.T) {
 				RegistryClient: registryClient,
 				AdminClient:    adminClient,
 			}
+			lister := &RegistryLister{RegistryClient: registryClient}
 
 			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
@@ -790,13 +603,11 @@ func TestReceiptAggArtifacts(t *testing.T) {
 					},
 				},
 			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+			actions := ProcessManifest(ctx, lister, projectID, manifest)
 
 			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
 				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
 			}
-
-			deleteProject(ctx, adminClient, t, "controller-test")
 		})
 	}
 }
@@ -937,6 +748,7 @@ func TestMultipleEntitiesArtifacts(t *testing.T) {
 				RegistryClient: registryClient,
 				AdminClient:    adminClient,
 			}
+			lister := &RegistryLister{RegistryClient: registryClient}
 
 			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
 				t.Fatalf("Setup: failed to seed registry: %s", err)
@@ -960,169 +772,11 @@ func TestMultipleEntitiesArtifacts(t *testing.T) {
 					},
 				},
 			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
+			actions := ProcessManifest(ctx, lister, projectID, manifest)
 
 			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
 				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
 			}
-
-			deleteProject(ctx, adminClient, t, "controller-test")
-		})
-	}
-}
-
-func TestRefreshArtifacts(t *testing.T) {
-	tests := []struct {
-		desc string
-		seed []seeder.RegistryResource
-		want []*Action
-		wait time.Duration
-	}{
-		{
-			desc: "non-existing artifacts",
-			seed: []seeder.RegistryResource{
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-				},
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
-				},
-				&rpc.ApiSpec{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
-				},
-			},
-			want: []*Action{
-				{
-					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
-					RequiresReceipt:   true,
-				},
-				{
-					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
-					RequiresReceipt:   true,
-				},
-				{
-					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
-					RequiresReceipt:   true,
-				},
-			},
-		},
-		{
-			desc: "existing valid artifacts",
-			seed: []seeder.RegistryResource{
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
-				},
-			},
-			want: nil,
-		},
-		{
-			desc: "existing invalid artifacts",
-			seed: []seeder.RegistryResource{
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
-				},
-			},
-			want: []*Action{
-				{
-					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
-					RequiresReceipt:   true,
-				},
-				{
-					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
-					RequiresReceipt:   true,
-				},
-				{
-					Command:           "registry compute score projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml",
-					GeneratedResource: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
-					RequiresReceipt:   true,
-				},
-			},
-			wait: 5,
-		},
-		{
-			desc: "existing valid artifacts",
-			seed: []seeder.RegistryResource{
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.0/specs/openapi.yaml/artifacts/score-receipt",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.0.1/specs/openapi.yaml/artifacts/score-receipt",
-				},
-				&rpc.Artifact{
-					Name: "projects/controller-test/locations/global/apis/petstore/versions/1.1.0/specs/openapi.yaml/artifacts/score-receipt",
-				},
-			},
-			want: nil,
-		},
-	}
-
-	const projectID = "controller-test"
-	for _, test := range tests {
-		t.Run(test.desc, func(t *testing.T) {
-			ctx := context.Background()
-			registryClient, err := connection.NewRegistryClient(ctx)
-			if err != nil {
-				t.Fatalf("Failed to create client: %+v", err)
-			}
-			t.Cleanup(func() { registryClient.Close() })
-
-			adminClient, err := connection.NewAdminClient(ctx)
-			if err != nil {
-				t.Fatalf("Failed to create client: %+v", err)
-			}
-			t.Cleanup(func() { adminClient.Close() })
-
-			deleteProject(ctx, adminClient, t, "controller-test")
-			t.Cleanup(func() { deleteProject(ctx, adminClient, t, "controller-test") })
-
-			client := seeder.Client{
-				RegistryClient: registryClient,
-				AdminClient:    adminClient,
-			}
-
-			if err := seeder.SeedRegistry(ctx, client, test.seed...); err != nil {
-				t.Fatalf("Setup: failed to seed registry: %s", err)
-			}
-
-			time.Sleep(test.wait * time.Second)
-
-			manifest := &rpc.Manifest{
-				Id: "controller-test",
-				GeneratedResources: []*rpc.GeneratedResource{
-					{
-						Pattern: "apis/-/versions/-/specs/-/artifacts/score-receipt",
-						Receipt: true,
-						Refresh: &durationpb.Duration{
-							Seconds: 2,
-						},
-						Action: "registry compute score $resource.spec",
-					},
-				},
-			}
-			actions := ProcessManifest(ctx, registryClient, projectID, manifest)
-
-			if diff := cmp.Diff(test.want, actions, sortActions); diff != "" {
-				t.Errorf("ProcessManifest(%+v) returned unexpected diff (-want +got):\n%s", manifest, diff)
-			}
-
-			deleteProject(ctx, adminClient, t, "controller-test")
 		})
 	}
 }