Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
164 lines
6.5 KiB
Go
164 lines
6.5 KiB
Go
package app
|
|
|
|
import (
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/server"
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/settings"
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store"
|
|
"git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/pkg/extension"
|
|
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func completeLifecycle() extension.ProjectLifecycle {
|
|
return extension.ProjectLifecycle{
|
|
List: func(context.Context, extension.Host) ([]core.Project, error) { return nil, nil },
|
|
Create: func(context.Context, extension.Host, string) (core.Project, error) { return core.Project{}, nil },
|
|
Rename: func(context.Context, extension.Host, string, string) (core.Project, error) {
|
|
return core.Project{}, nil
|
|
},
|
|
Archive: func(context.Context, extension.Host, string) error { return nil },
|
|
Work: func(context.Context, string, core.ID) (bool, error) { return true, nil },
|
|
}
|
|
}
|
|
func TestProjectLifecycleOptionalRegistration(t *testing.T) {
|
|
slot := &projectLifecycleSlot{}
|
|
reg := pluginRegistry{pluginID: "rdm", caps: hostcaps{projects: slot}}
|
|
if err := reg.RegisterProjectLifecycle(extension.ProjectLifecycle{}); err == nil {
|
|
t.Fatal("incomplete registration accepted")
|
|
}
|
|
if err := reg.RegisterProjectLifecycle(completeLifecycle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reg.RegisterProjectLifecycle(completeLifecycle()); err == nil {
|
|
t.Fatal("duplicate registration accepted")
|
|
}
|
|
if err := (compiledRegistry{}).RegisterProjectLifecycle(completeLifecycle()); err == nil {
|
|
t.Fatal("declarative callback accepted")
|
|
}
|
|
rr := recordRegistry{Registry: pluginRegistry{pluginID: "other", caps: hostcaps{projects: &projectLifecycleSlot{}}}}
|
|
optional, ok := any(rr).(extension.ProjectLifecycleRegistry)
|
|
if !ok {
|
|
t.Fatal("record registry hides optional registration")
|
|
}
|
|
if err := optional.RegisterProjectLifecycle(completeLifecycle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func TestProjectHostMetadataDoesNotGrantMutation(t *testing.T) {
|
|
st, err := store.OpenSQLite(t.TempDir() + "/t.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
alice := inprocHost{owner: "alice", caps: hostcaps{st: st}}
|
|
p, err := alice.EnsureProject("stable", "Shared")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
auth := &projectAuthz{}
|
|
auth.set("rdm", func(owner string, id core.ID) (bool, error) { return owner == "bob" && id == core.ID(p.ID), nil })
|
|
bob := inprocHost{owner: "bob", caps: hostcaps{st: st, authz: auth}}
|
|
if _, err := bob.ProjectMetadata(p.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := bob.OwnedProject(p.ID); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("owned lookup=%v", err)
|
|
}
|
|
if _, err := bob.RenameOwnedProject(p.ID, "Other"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("shared rename=%v", err)
|
|
}
|
|
if _, err := bob.EnsureProject(p.ID, p.Name); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("shared ensure=%v", err)
|
|
}
|
|
auth.setV2("rdm", func(q extension.ResourceAuthzQuery) (bool, error) { return false, nil })
|
|
if _, err := bob.ProjectMetadata(p.ID); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("V2 denial ignored=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestProjectHostMetadataMasksSharedSecretsWithoutMutation(t *testing.T) {
|
|
st, err := store.OpenSQLite(t.TempDir() + "/t.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
p, err := st.CreateProject("alice", "Shared")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw := map[string]any{"models": map[string]any{"chat": map[string]any{"apiKey": "fixture-nested-secret"}}, "models.embedding.apiKey": "fixture-flat-secret", "legacy.token": "fixture-legacy-secret", "plugins.private": map[string]any{"credential": "fixture-schema-secret"}, "display": map[string]any{"caption": "Shared configuration"}, "rdm.projectId": "canonical", "rdm.state": "ready"}
|
|
if err := st.UpdateProjectSettings("alice", p.ID, raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc := settings.NewService(st)
|
|
svc.SetExtraSchema([]core.SettingDesc{{Key: "plugins.private.credential", Secret: true}})
|
|
slot := &projectLifecycleSlot{}
|
|
auth := &projectAuthz{}
|
|
caps := hostcaps{st: st, set: svc, projects: slot, authz: auth}
|
|
provider := completeLifecycle()
|
|
provider.List = func(_ context.Context, h extension.Host) ([]core.Project, error) {
|
|
project, err := h.(extension.ProjectHost).ProjectMetadata(p.ID)
|
|
return []core.Project{project}, err
|
|
}
|
|
if err := (pluginRegistry{pluginID: "rdm", caps: caps}).RegisterProjectLifecycle(provider); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
auth.setV2("rdm", func(q extension.ResourceAuthzQuery) (bool, error) {
|
|
return q.Actor == "bob" && q.ResourceType == "project" && q.ResourceID == p.ID, nil
|
|
})
|
|
for _, actor := range []string{"bob", "alice"} {
|
|
h := inprocHost{owner: actor, caps: caps}
|
|
view, err := h.ProjectMetadata(p.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encoded, _ := json.Marshal(view.Settings)
|
|
for _, value := range []string{"fixture-nested-secret", "fixture-flat-secret", "fixture-legacy-secret", "fixture-schema-secret"} {
|
|
if bytes.Contains(encoded, []byte(value)) {
|
|
t.Fatalf("%s metadata exposed a fixture credential", actor)
|
|
}
|
|
}
|
|
if view.Settings["rdm.projectId"] != "canonical" || view.Settings["rdm.state"] != "ready" || view.Settings["display"].(map[string]any)["caption"] != "Shared configuration" {
|
|
t.Fatal("allowed service/configuration metadata changed")
|
|
}
|
|
}
|
|
// Exercise the real registry -> actor-scoped Host -> public HTTP list chain.
|
|
srv := server.New(st, nil, nil, nil, svc, nil)
|
|
srv.Auth = server.NewAuthenticator(st, true)
|
|
slot.wire(srv, caps)
|
|
key, err := st.CreateAPIKey("bob", "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest("GET", "/api/projects", nil)
|
|
request.Header.Set("Authorization", "Bearer "+key)
|
|
response := httptest.NewRecorder()
|
|
srv.Handler().ServeHTTP(response, request)
|
|
if response.Code != 200 {
|
|
t.Fatalf("member list=%d", response.Code)
|
|
}
|
|
for _, value := range []string{"fixture-nested-secret", "fixture-flat-secret", "fixture-legacy-secret", "fixture-schema-secret"} {
|
|
if bytes.Contains(response.Body.Bytes(), []byte(value)) {
|
|
t.Fatal("actor-scoped lifecycle list exposed a fixture credential")
|
|
}
|
|
}
|
|
if !bytes.Contains(response.Body.Bytes(), []byte(`"rdm.projectId":"canonical"`)) {
|
|
t.Fatal("actor-scoped list lost canonical metadata")
|
|
}
|
|
|
|
owned, err := (inprocHost{owner: "alice", caps: caps}).OwnedProject(p.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, _ := json.Marshal(raw)
|
|
after, _ := json.Marshal(owned.Settings)
|
|
if !bytes.Equal(before, after) {
|
|
t.Fatal("metadata read mutated persisted owner settings")
|
|
}
|
|
}
|