Skip to content
354 changes: 346 additions & 8 deletions cmd/ateapi/internal/store/ateredis/ateredis.go

Large diffs are not rendered by default.

797 changes: 797 additions & 0 deletions cmd/ateapi/internal/store/ateredis/ateredis_test.go

Large diffs are not rendered by default.

57 changes: 56 additions & 1 deletion cmd/ateapi/internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,51 @@ type Interface interface {
// (e.g. there are actors in it).
DeleteAtespace(ctx context.Context, name string) (*ateapipb.Atespace, error)

// Stores a new ActorTemplate and returns the stored resource with
// server-assigned metadata (uid, version, timestamps). The input is not
// mutated. Returns ErrAlreadyExists if the (atespace, name) is taken.
CreateActorTemplate(ctx context.Context, template *ateapipb.ActorTemplate) (*ateapipb.ActorTemplate, error)

// Fetches an ActorTemplate by reference. Returns ErrNotFound if missing.
GetActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef) (*ateapipb.ActorTemplate, error)

// ActorTemplateExists reports whether the ActorTemplate exists.
ActorTemplateExists(ctx context.Context, templateRef resources.ActorTemplateRef) (bool, error)

// UpdateActorTemplate performs a transactional read-modify-write and returns
// the updated template with advanced metadata (version, update_time).
UpdateActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef, mutate func(dbTemplate *ateapipb.ActorTemplate) error) (*ateapipb.ActorTemplate, error)

// Lists ActorTemplates in an atespace, or across all atespaces when
// atespace is empty. Returns a page of templates and a next page token.
ListActorTemplates(ctx context.Context, atespace string, pageSize int32, pageToken string) ([]*ateapipb.ActorTemplate, string, error)

// Removes an ActorTemplate and returns the deleted resource. Returns
// ErrNotFound if missing, or ErrFailedPrecondition while any
// ActorTemplateVersion still names it as parent.
DeleteActorTemplate(ctx context.Context, templateRef resources.ActorTemplateRef) (*ateapipb.ActorTemplate, error)

// Stores a new ActorTemplateVersion and returns the stored resource with
// server-assigned metadata. The caller is responsible for the
// parent-exists check and for initializing the status fields. The input is not
// mutated. Returns ErrAlreadyExists if the (atespace, name) is taken.
CreateActorTemplateVersion(ctx context.Context, version *ateapipb.ActorTemplateVersion) (*ateapipb.ActorTemplateVersion, error)

// Fetches an ActorTemplateVersion by reference. Returns ErrNotFound if
// missing.
GetActorTemplateVersion(ctx context.Context, versionRef resources.ActorTemplateVersionRef) (*ateapipb.ActorTemplateVersion, error)

// Lists ActorTemplateVersions in an atespace (all atespaces when atespace
// is empty), filtered to one parent template when actorTemplateRef is
// non-zero. The parent lives in the same atespace as its versions.
ListActorTemplateVersions(ctx context.Context, atespace string, actorTemplateRef resources.ActorTemplateRef, pageSize int32, pageToken string) ([]*ateapipb.ActorTemplateVersion, string, error)

// Removes an ActorTemplateVersion and returns the deleted resource, also
// deleting the golden snapshot recorded in golden_snapshot, if any.
// Returns ErrNotFound if missing, or ErrFailedPrecondition while the
// version is its parent's default_version_on_create.
DeleteActorTemplateVersion(ctx context.Context, versionRef resources.ActorTemplateVersionRef) (*ateapipb.ActorTemplateVersion, error)

// Fetches worker state by namespace, pool, and pod name. Returns ErrNotFound if missing.
GetWorker(ctx context.Context, namespace, pool, pod string) (*ateapipb.Worker, error)

Expand Down Expand Up @@ -169,7 +214,17 @@ const (
// outside of it. Returns ErrUIDConflict or ErrVersionConflict, which UpdateActor
// surfaces verbatim.
func CheckActorPrecondition(dbActor *ateapipb.Actor, uid string, version int64) error {
md := dbActor.GetMetadata()
return checkPrecondition(dbActor.GetMetadata(), uid, version)
}

// CheckActorTemplatePrecondition is CheckActorPrecondition for ActorTemplates:
// call it at the top of an UpdateActorTemplate mutation to pin the uid and
// version the caller observed.
func CheckActorTemplatePrecondition(dbTemplate *ateapipb.ActorTemplate, uid string, version int64) error {
return checkPrecondition(dbTemplate.GetMetadata(), uid, version)
}

func checkPrecondition(md *ateapipb.ResourceMetadata, uid string, version int64) error {
if uid != AnyUID && uid != md.GetUid() {
return ErrUIDConflict
}
Expand Down
111 changes: 111 additions & 0 deletions internal/resources/actortemplateref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package resources

import (
"log/slog"

"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
)

// ActorTemplateRef identifies an ActorTemplate by the (atespace, name).
//
// ActorTemplateRef is the in-process form of the identity that
// ateapipb.ObjectRef carries on the wire.
type ActorTemplateRef struct {
// Atespace is the isolation boundary the template was created into. Required.
Atespace string
// Name is the template's name, unique within Atespace. Required.
Name string
}

func (r ActorTemplateRef) String() string {
return r.Atespace + "/" + r.Name
}

// LogValue implements slog.LogValuer so that slog.Any("template", ref) records
// the two components as a group ("template.atespace", "template.name") rather
// than flattening them into one opaque string.
func (r ActorTemplateRef) LogValue() slog.Value {
return slog.GroupValue(
slog.String("atespace", r.Atespace),
slog.String("name", r.Name),
)
}

// ToObjectRef converts the reference to its wire form.
func (r ActorTemplateRef) ToObjectRef() *ateapipb.ObjectRef {
return &ateapipb.ObjectRef{Atespace: r.Atespace, Name: r.Name}
}

// ActorTemplateRefFromObjectRef converts a wire reference to an ActorTemplateRef.
func ActorTemplateRefFromObjectRef(ref *ateapipb.ObjectRef) ActorTemplateRef {
return ActorTemplateRef{Atespace: ref.GetAtespace(), Name: ref.GetName()}
}

// ActorTemplateRefFromActorTemplate returns the reference addressing the given
// template.
func ActorTemplateRefFromActorTemplate(t *ateapipb.ActorTemplate) ActorTemplateRef {
return ActorTemplateRef{
Atespace: t.GetMetadata().GetAtespace(),
Name: t.GetMetadata().GetName(),
}
}

// ActorTemplateVersionRef identifies an ActorTemplateVersion by the
// (atespace, name).
//
// ActorTemplateVersionRef is the in-process form of the identity that
// ateapipb.ObjectRef carries on the wire.
type ActorTemplateVersionRef struct {
// Atespace is the isolation boundary the version was created into. Required.
Atespace string
// Name is the version's name, unique within Atespace. Required.
Name string
}

func (r ActorTemplateVersionRef) String() string {
return r.Atespace + "/" + r.Name
}

// LogValue implements slog.LogValuer so that slog.Any("version", ref) records
// the two components as a group ("version.atespace", "version.name") rather
// than flattening them into one opaque string.
func (r ActorTemplateVersionRef) LogValue() slog.Value {
return slog.GroupValue(
slog.String("atespace", r.Atespace),
slog.String("name", r.Name),
)
}

// ToObjectRef converts the reference to its wire form.
func (r ActorTemplateVersionRef) ToObjectRef() *ateapipb.ObjectRef {
return &ateapipb.ObjectRef{Atespace: r.Atespace, Name: r.Name}
}

// ActorTemplateVersionRefFromObjectRef converts a wire reference to an
// ActorTemplateVersionRef.
func ActorTemplateVersionRefFromObjectRef(ref *ateapipb.ObjectRef) ActorTemplateVersionRef {
return ActorTemplateVersionRef{Atespace: ref.GetAtespace(), Name: ref.GetName()}
}

// ActorTemplateVersionRefFromActorTemplateVersion returns the reference
// addressing the given version.
func ActorTemplateVersionRefFromActorTemplateVersion(v *ateapipb.ActorTemplateVersion) ActorTemplateVersionRef {
return ActorTemplateVersionRef{
Atespace: v.GetMetadata().GetAtespace(),
Name: v.GetMetadata().GetName(),
}
}
111 changes: 111 additions & 0 deletions internal/resources/actortemplateref_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package resources

import (
"testing"

"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
)

func TestActorTemplateRefString(t *testing.T) {
got := ActorTemplateRef{Atespace: "team-a", Name: "tmpl-1"}.String()
if want := "team-a/tmpl-1"; got != want {
t.Errorf("String() = %q, want %q", got, want)
}
}

func TestActorTemplateRefObjectRefRoundTrip(t *testing.T) {
templateRef := ActorTemplateRef{Atespace: "team-a", Name: "tmpl-1"}

obj := templateRef.ToObjectRef()
if obj.GetAtespace() != "team-a" || obj.GetName() != "tmpl-1" {
t.Errorf("ToObjectRef() = (%q, %q), want (team-a, tmpl-1)", obj.GetAtespace(), obj.GetName())
}
if got := ActorTemplateRefFromObjectRef(obj); got != templateRef {
t.Errorf("round-trip = %+v, want %+v", got, templateRef)
}
}

func TestActorTemplateRefFromActorTemplate(t *testing.T) {
tests := []struct {
name string
template *ateapipb.ActorTemplate
want ActorTemplateRef
}{
{
name: "populated",
template: &ateapipb.ActorTemplate{Metadata: &ateapipb.ResourceMetadata{
Atespace: "team-a",
Name: "tmpl-1",
}},
want: ActorTemplateRef{Atespace: "team-a", Name: "tmpl-1"},
},
{"nil template", nil, ActorTemplateRef{}},
{"nil metadata", &ateapipb.ActorTemplate{}, ActorTemplateRef{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ActorTemplateRefFromActorTemplate(tt.template); got != tt.want {
t.Errorf("ActorTemplateRefFromActorTemplate() = %+v, want %+v", got, tt.want)
}
})
}
}

func TestActorTemplateVersionRefString(t *testing.T) {
got := ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-1-v1"}.String()
if want := "team-a/tmpl-1-v1"; got != want {
t.Errorf("String() = %q, want %q", got, want)
}
}

func TestActorTemplateVersionRefObjectRefRoundTrip(t *testing.T) {
versionRef := ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-1-v1"}

obj := versionRef.ToObjectRef()
if obj.GetAtespace() != "team-a" || obj.GetName() != "tmpl-1-v1" {
t.Errorf("ToObjectRef() = (%q, %q), want (team-a, tmpl-1-v1)", obj.GetAtespace(), obj.GetName())
}
if got := ActorTemplateVersionRefFromObjectRef(obj); got != versionRef {
t.Errorf("round-trip = %+v, want %+v", got, versionRef)
}
}

func TestActorTemplateVersionRefFromActorTemplateVersion(t *testing.T) {
tests := []struct {
name string
version *ateapipb.ActorTemplateVersion
want ActorTemplateVersionRef
}{
{
name: "populated",
version: &ateapipb.ActorTemplateVersion{Metadata: &ateapipb.ResourceMetadata{
Atespace: "team-a",
Name: "tmpl-1-v1",
}},
want: ActorTemplateVersionRef{Atespace: "team-a", Name: "tmpl-1-v1"},
},
{"nil version", nil, ActorTemplateVersionRef{}},
{"nil metadata", &ateapipb.ActorTemplateVersion{}, ActorTemplateVersionRef{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ActorTemplateVersionRefFromActorTemplateVersion(tt.version); got != tt.want {
t.Errorf("ActorTemplateVersionRefFromActorTemplateVersion() = %+v, want %+v", got, tt.want)
}
})
}
}
Loading
Loading