* feat: add multi-workspace mode and base_dir config fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add WorkspaceBindingManager for channel-to-workspace persistence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add workspacePool for managing per-workspace agent state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add ChannelNameResolver interface and Slack implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add multi-workspace fields, SetMultiWorkspace, and idle reaper to Engine Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add resolveWorkspace method for channel-to-directory mapping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: wire multi-workspace mode setup in main.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add workspace init flow for cloning repos and binding channels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add /workspace commands (init, unbind, list, status) Add handleWorkspaceCommand with subcommands: - (no args): show current workspace binding for the channel - init <git-url>: clone repo and bind workspace to channel - unbind: remove workspace binding from channel - list: show all workspace bindings for the project Only available when multi-workspace mode is enabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: wire multi-workspace routing into handleMessage with per-workspace agents Add workspace resolution to handleMessage that resolves channels to workspace directories, creates per-workspace agents with correct work_dir, and routes messages through workspace-specific session managers. The normal (single-workspace) code path is unchanged — all new logic is gated behind e.multiWorkspace. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add integration tests for multi-workspace resolution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve multi-workspace bugs found during live testing Three bugs fixed: 1. App mention events not handled - Slack sends `app_mention` events (not `message.channels`) when the bot is @mentioned. Added AppMentionEvent handler in the Slack platform. 2. Bot mention prefix not stripped - app_mention text includes `<@UBOTID> ` prefix, breaking command parsing and workspace resolution. Now stripped before passing to engine. 3. Slash command syntax incompatible with Slack - `/workspace` commands were being intercepted by Slack as slash commands. Changed all user-facing references and prefix detection to use `workspace` without the leading slash. Also added debug logging to handleEvent for diagnosing event delivery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: instruct agent to use Slack mrkdwn formatting Add Slack formatting guidelines to the agent system prompt so responses use Slack's mrkdwn syntax instead of standard Markdown (e.g. *bold* instead of **bold**, no ## headings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
62 lines
1.7 KiB
Go
62 lines
1.7 KiB
Go
package core
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestWorkspaceBindingManager_SaveLoad(t *testing.T) {
|
|
dir := t.TempDir()
|
|
storePath := filepath.Join(dir, "bindings.json")
|
|
|
|
mgr := NewWorkspaceBindingManager(storePath)
|
|
mgr.Bind("project:claude", "C123", "my-channel", "/home/user/workspace/my-channel")
|
|
|
|
b := mgr.Lookup("project:claude", "C123")
|
|
if b == nil {
|
|
t.Fatal("expected binding, got nil")
|
|
}
|
|
if b.ChannelName != "my-channel" {
|
|
t.Errorf("expected channel name 'my-channel', got %q", b.ChannelName)
|
|
}
|
|
if b.Workspace != "/home/user/workspace/my-channel" {
|
|
t.Errorf("expected workspace path, got %q", b.Workspace)
|
|
}
|
|
|
|
// Reload from disk
|
|
mgr2 := NewWorkspaceBindingManager(storePath)
|
|
b2 := mgr2.Lookup("project:claude", "C123")
|
|
if b2 == nil {
|
|
t.Fatal("expected binding after reload, got nil")
|
|
}
|
|
if b2.Workspace != "/home/user/workspace/my-channel" {
|
|
t.Errorf("expected workspace path after reload, got %q", b2.Workspace)
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceBindingManager_Unbind(t *testing.T) {
|
|
dir := t.TempDir()
|
|
storePath := filepath.Join(dir, "bindings.json")
|
|
|
|
mgr := NewWorkspaceBindingManager(storePath)
|
|
mgr.Bind("project:claude", "C123", "chan", "/path")
|
|
mgr.Unbind("project:claude", "C123")
|
|
|
|
if b := mgr.Lookup("project:claude", "C123"); b != nil {
|
|
t.Error("expected nil after unbind")
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceBindingManager_ListByProject(t *testing.T) {
|
|
dir := t.TempDir()
|
|
mgr := NewWorkspaceBindingManager(filepath.Join(dir, "bindings.json"))
|
|
mgr.Bind("project:claude", "C1", "chan1", "/path1")
|
|
mgr.Bind("project:claude", "C2", "chan2", "/path2")
|
|
mgr.Bind("project:other", "C3", "chan3", "/path3")
|
|
|
|
list := mgr.ListByProject("project:claude")
|
|
if len(list) != 2 {
|
|
t.Errorf("expected 2 bindings, got %d", len(list))
|
|
}
|
|
}
|