Files
Plexum-fabric-channel-plugin/internal/subdisc/store_test.go
hzhang ed3676ebc8 feat: Phase F-5+F-7 — command-sync + sub-discussion + channel-create tools
Bundles the remaining "agent does things in Fabric" surface, skipping
presence-sync (Plexum's state machine isn't 1:1 with HF status semantics)
and attachments (Plexum has no media pipeline yet) — both deferred.

internal/subdisc/ (~140 LOC + 5 tests): persistent KV at
<profile>/state/plugin-kv/fabric-sub-discussions.json
- Entry: SubChannelID, HostAgentID, HostUserID, GuestUserIDs,
  HostGuide, GuestGuide, CallbackGuildNodeID, CallbackChannelID, CreatedAt
- Open / Lookup / LookupForHost (host-enforcement) / Add / Remove / All
- atomic tmp+rename persistence; corrupt file = start empty

internal/tools/tools.go — 7 new tools:
- create-chat-channel       (xType=general)
- create-work-channel       (xType=work)
- create-report-channel     (xType=report)
- create-discussion-channel (xType=discuss)
- discussion-complete       (post summary + close channel)
- create-sub-discussion     (create + invite + KV store + greeting)
- close-sub-discussion      (host-only; posts callback to parent + closes)

internal/fabric/client.go: CreateChannel / CloseChannel /
JoinChannel / LeaveChannel / SetChannelPurpose / Canvas CRUD /
SyncCommands (added in earlier F-4 commit; reused here)

cmd/plexum-fabric-channel-plugin/main.go:
- subdisc.Store opened from DefaultPath at init; passed into tools.Deps
- HostConfig adds commands_sync_key + sync_commands (defaults
  ["new","stop"]) — F-5 command-sync to every (agent,guild) pair at
  init when key is set; silently skips when omitted
- syncCommandsToGuilds: best-effort PUT per guild, logs per-guild
  outcome

scripts/install.sh: manifest tools[] expanded to 16 entries
(9 from F-4 + 7 new). create-channel variants share a schema shape.

Live verified:
  $ plexum plugin-call create-chat-channel \
      '{"agent_id":"fabrictester","guild_node_id":"test-guild2",
        "name":"plexum-f7-smoke","is_public":true}'
    → "created general channel plexum-f7-smoke (id=6315e636-...)"
  $ plexum plugin-call fabric-channel-list ...
    → 3 channels listed including the new one

F-6 (attachments) + F-5b (presence-sync) + F-8 (coalesce) deliberately
deferred — see DEV-NOTES.

Tests: 5 new in internal/subdisc (27 total in this repo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 15:40:09 +01:00

75 lines
1.8 KiB
Go

package subdisc
import (
"os"
"path/filepath"
"testing"
)
func TestRoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "sub.json")
s, _ := Open(path)
err := s.Add(Entry{
SubChannelID: "ch1", HostAgentID: "alice", HostUserID: "uA",
GuestUserIDs: []string{"uB"}, CallbackGuildNodeID: "g1", CallbackChannelID: "pCh",
})
if err != nil {
t.Fatal(err)
}
s2, _ := Open(path)
e := s2.Lookup("ch1")
if e == nil || e.HostAgentID != "alice" || e.CallbackChannelID != "pCh" {
t.Errorf("entry = %+v", e)
}
if e.CreatedAt == "" {
t.Errorf("CreatedAt should be auto-set")
}
}
func TestLookupForHost(t *testing.T) {
s, _ := Open(filepath.Join(t.TempDir(), "x.json"))
s.Add(Entry{SubChannelID: "c", HostAgentID: "alice"})
if s.LookupForHost("c", "alice") == nil {
t.Errorf("alice should match")
}
if s.LookupForHost("c", "bob") != nil {
t.Errorf("bob should NOT match")
}
if s.LookupForHost("missing", "alice") != nil {
t.Errorf("missing channel should return nil")
}
}
func TestRemove(t *testing.T) {
s, _ := Open(filepath.Join(t.TempDir(), "x.json"))
s.Add(Entry{SubChannelID: "c"})
if !s.Remove("c") {
t.Errorf("Remove existing should return true")
}
if s.Remove("c") {
t.Errorf("Remove missing should return false")
}
}
func TestAllReturnsCopy(t *testing.T) {
s, _ := Open(filepath.Join(t.TempDir(), "x.json"))
s.Add(Entry{SubChannelID: "a", HostAgentID: "alice"})
s.Add(Entry{SubChannelID: "b", HostAgentID: "bob"})
all := s.All()
if len(all) != 2 {
t.Errorf("len = %d", len(all))
}
}
func TestOpenCorruptStartsEmpty(t *testing.T) {
path := filepath.Join(t.TempDir(), "corrupt.json")
os.WriteFile(path, []byte("not json"), 0o600)
s, err := Open(path)
if err != nil {
t.Errorf("Open corrupt should tolerate: %v", err)
}
if len(s.All()) != 0 {
t.Errorf("corrupt should start empty")
}
}