Ports the foundation of Fabric.OpenclawPlugin to a native Plexum channel plugin (Go). F-2+ phases (socket.io inbound, wakeup gate, tools, presence, etc.) follow. Layout: internal/identity/ — fabric-identity.json registry (agent → API key) internal/fabric/ — REST client (Center auth + Guild messaging) internal/config/ — channels/<name>.json fabric extension parser cmd/plexum-fabric-register/ — agent registration CLI cmd/plexum-fabric-channel-plugin/— Plexum SDK plugin entry scripts/install.sh — build + install + manifest generator Plugin behavior (F-1): - Reads <profile>/channels/*.json, filters plugin=plexum-fabric-channel, builds (plexum-channel-name → fabric channel-id) index - Validates each bound agent's API key against Center at init (warmSessions); logs warning but doesn't refuse init on bad keys - `send` MCP tool: POST plain text to the bound Fabric channel as the agent user; selects guild endpoint+token from cached session - Manifest channels[] is generated by install.sh from current channels/*.json — re-run with --reset-manifest after adding bindings - Plugin-private config at <profile>/plugins/plexum-fabric-channel/config.json (center_api_base, default http://localhost:7001/api) Live smoke verified: - plexum-fabric-register against running Fabric Center (port 7001): validated fak_..., wrote identity file with user_id + email captured Tests: identity (5) + config (6) = 11 unit tests. F-2 will hook socket.io for inbound + wakeup gating + token refresh. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
89 lines
2.1 KiB
Go
89 lines
2.1 KiB
Go
package identity
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpenMissingFileEmpty(t *testing.T) {
|
|
r, err := Open(filepath.Join(t.TempDir(), "nope.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(r.AgentIDs()) != 0 {
|
|
t.Errorf("expected empty registry")
|
|
}
|
|
}
|
|
|
|
func TestSetSaveReload(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "id.json")
|
|
r, _ := Open(path)
|
|
r.Set("alice", &Entry{FabricAPIKey: "fak_alice", FabricEmail: "a@x", Enabled: true})
|
|
r.Set("bob", &Entry{FabricAPIKey: "fak_bob", Enabled: false})
|
|
if err := r.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r2, err := Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := r2.Lookup("alice")
|
|
if a == nil || a.FabricAPIKey != "fak_alice" || !a.Enabled {
|
|
t.Errorf("alice = %+v", a)
|
|
}
|
|
b := r2.Lookup("bob")
|
|
if b == nil || b.Enabled {
|
|
t.Errorf("bob = %+v", b)
|
|
}
|
|
|
|
st, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.Mode().Perm() != 0o600 {
|
|
t.Errorf("perms = %o, want 0600", st.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestEnabledEntriesFiltersDisabled(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "id.json")
|
|
r, _ := Open(path)
|
|
r.Set("a", &Entry{FabricAPIKey: "x", Enabled: true})
|
|
r.Set("b", &Entry{FabricAPIKey: "y", Enabled: false})
|
|
r.Set("c", &Entry{FabricAPIKey: "z", Enabled: true})
|
|
out := r.EnabledEntries()
|
|
if len(out) != 2 || out["a"] == nil || out["c"] == nil {
|
|
t.Errorf("EnabledEntries = %+v", out)
|
|
}
|
|
if out["b"] != nil {
|
|
t.Errorf("disabled should be filtered")
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
r, _ := Open(filepath.Join(t.TempDir(), "id.json"))
|
|
r.Set("a", &Entry{FabricAPIKey: "x", Enabled: true})
|
|
if !r.Delete("a") {
|
|
t.Errorf("delete present should return true")
|
|
}
|
|
if r.Delete("a") {
|
|
t.Errorf("delete missing should return false")
|
|
}
|
|
}
|
|
|
|
func TestAgentIDsSorted(t *testing.T) {
|
|
r, _ := Open(filepath.Join(t.TempDir(), "id.json"))
|
|
for _, k := range []string{"z", "a", "m"} {
|
|
r.Set(k, &Entry{FabricAPIKey: "x", Enabled: true})
|
|
}
|
|
ids := r.AgentIDs()
|
|
want := []string{"a", "m", "z"}
|
|
for i := range want {
|
|
if ids[i] != want[i] {
|
|
t.Errorf("ids[%d] = %q, want %q", i, ids[i], want[i])
|
|
}
|
|
}
|
|
}
|