Files
hzhang f8d43ae70e feat: Phase F-1 — Plexum-fabric-channel-plugin foundation
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>
2026-05-31 15:13:34 +01:00

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])
}
}
}