Why: Shared project reads previously doubled as write permission, project mutations bypassed the domain lifecycle, and coarse retrieval shared private resources. Evidence: SELF red-before-implementation logs cover projection, lifecycle, private work, resource filtering, canonical retrieval scope, publication source and namespace. SELF go test ./..., isolated PostgreSQL go test ./internal/store -count=1, and CGO_ENABLED=0 go build ./cmd/agent-system passed; task owner owns replay/CI. Impact: Add optional actor-scoped projection/lifecycle capabilities; registered providers control public lifecycle and live work. Preserve owned conversation history and ordinary unregistered behavior. Authorize knowledge by stored ID/source, map current host scopes through trusted service metadata, and filter artifact lists. Refs: Dev project-member-access implementation plan r1 Task 1; approved design r0.1; 2026-10-13 trusted publication Source hardening ruling
84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestProjectProjectionIdentityAndOwnership(t *testing.T) {
|
|
st := openTestStore(t)
|
|
p, err := st.EnsureProject("alice", "host-stable", " Global ")
|
|
if err != nil || p.ID != "host-stable" || p.Name != "Global" || p.Owner != "alice" {
|
|
t.Fatalf("ensure: %+v %v", p, err)
|
|
}
|
|
retry, err := st.EnsureProject("alice", p.ID, "Global")
|
|
if err != nil || retry.ID != p.ID {
|
|
t.Fatalf("retry: %+v %v", retry, err)
|
|
}
|
|
if _, err = st.EnsureProject("bob", p.ID, "Global"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("other owner: %v", err)
|
|
}
|
|
if _, err = st.EnsureProject("alice", p.ID, "Different"); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("different retry name: %v", err)
|
|
}
|
|
if _, err = st.RenameOwnedProject("bob", p.ID, "Other"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("rename owner: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProjectProjectionGlobalNameAdmission(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if _, err := st.CreateProject("legacy", "Taken"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.EnsureProject("alice", "a", " Taken "); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("legacy collision: %v", err)
|
|
}
|
|
var wg sync.WaitGroup
|
|
results := make(chan error, 2)
|
|
for _, owner := range []string{"alice", "bob"} {
|
|
wg.Add(1)
|
|
go func(owner string) {
|
|
defer wg.Done()
|
|
_, err := st.EnsureProject(owner, "p-"+owner, "Race")
|
|
results <- err
|
|
}(owner)
|
|
}
|
|
wg.Wait()
|
|
close(results)
|
|
successes, conflicts := 0, 0
|
|
for err := range results {
|
|
if err == nil {
|
|
successes++
|
|
} else if errors.Is(err, ErrConflict) {
|
|
conflicts++
|
|
} else {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if successes != 1 || conflicts != 1 {
|
|
t.Fatalf("successes=%d conflicts=%d", successes, conflicts)
|
|
}
|
|
if _, err := st.EnsureProject("alice", "rename-a", "Rename source"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.RenameOwnedProject("alice", "rename-a", "Taken"); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("rename collision: %v", err)
|
|
}
|
|
available, err := st.ProjectNameAvailable("Taken", "")
|
|
if err != nil || available {
|
|
t.Fatalf("availability=%v %v", available, err)
|
|
}
|
|
}
|
|
|
|
func TestProjectProjectionAdmissionTrimsLegacyWhitespace(t *testing.T) {
|
|
st := openTestStore(t)
|
|
if _, err := st.CreateProject("legacy", "\tReserved\u00a0"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.EnsureProject("other", "new-id", "Reserved"); !errors.Is(err, ErrConflict) {
|
|
t.Fatalf("trimmed legacy admission: %v", err)
|
|
}
|
|
}
|