fix scratchpad routing by caller project
This commit is contained in:
@@ -150,6 +150,12 @@ func (r *ProjectRegistry) Count() int {
|
||||
return len(r.projects)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) DefaultProject() *Project {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.projects[r.defaultProjectKey]
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) Shutdown() {
|
||||
r.mu.Lock()
|
||||
projects := make([]*Project, 0, len(r.projects))
|
||||
@@ -418,24 +424,24 @@ func (r *ProjectRegistry) TimerList(callerID string) ([]mcp.TimerInfo, error) {
|
||||
return r.hostForCaller(callerID).TimerList(callerID)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) ScratchpadList() ([]scratchpad.Entry, error) {
|
||||
return r.hostForCaller("").ScratchpadList()
|
||||
func (r *ProjectRegistry) ScratchpadList(callerID string) ([]scratchpad.Entry, error) {
|
||||
return r.hostForCaller(callerID).ScratchpadList(callerID)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) ScratchpadRead(name string) (string, string, error) {
|
||||
return r.hostForCaller("").ScratchpadRead(name)
|
||||
func (r *ProjectRegistry) ScratchpadRead(callerID, name string) (string, string, error) {
|
||||
return r.hostForCaller(callerID).ScratchpadRead(callerID, name)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) ScratchpadWrite(name, content, expectedRevision string) (string, error) {
|
||||
return r.hostForCaller("").ScratchpadWrite(name, content, expectedRevision)
|
||||
func (r *ProjectRegistry) ScratchpadWrite(callerID, name, content, expectedRevision string) (string, error) {
|
||||
return r.hostForCaller(callerID).ScratchpadWrite(callerID, name, content, expectedRevision)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) ScratchpadAppend(name, content string) error {
|
||||
return r.hostForCaller("").ScratchpadAppend(name, content)
|
||||
func (r *ProjectRegistry) ScratchpadAppend(callerID, name, content string) error {
|
||||
return r.hostForCaller(callerID).ScratchpadAppend(callerID, name, content)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) ScratchpadDelete(name string) error {
|
||||
return r.hostForCaller("").ScratchpadDelete(name)
|
||||
func (r *ProjectRegistry) ScratchpadDelete(callerID, name string) error {
|
||||
return r.hostForCaller(callerID).ScratchpadDelete(callerID, name)
|
||||
}
|
||||
|
||||
func (r *ProjectRegistry) WhoAmI(callerID string) mcp.WhoAmI {
|
||||
|
||||
@@ -811,13 +811,13 @@ func (h *toolHost) TimerList(callerID string) ([]mcp.TimerInfo, error) {
|
||||
// Scratchpads / Meta
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *toolHost) ScratchpadList() ([]scratchpad.Entry, error) { return h.pads.List() }
|
||||
func (h *toolHost) ScratchpadList(string) ([]scratchpad.Entry, error) { return h.pads.List() }
|
||||
|
||||
func (h *toolHost) ScratchpadRead(name string) (string, string, error) {
|
||||
func (h *toolHost) ScratchpadRead(_ string, name string) (string, string, error) {
|
||||
return h.pads.Read(name)
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadWrite(name, content, expectedRevision string) (string, error) {
|
||||
func (h *toolHost) ScratchpadWrite(_, name, content, expectedRevision string) (string, error) {
|
||||
rev, err := h.pads.Write(name, content, expectedRevision)
|
||||
if err == nil && h.scratch != nil {
|
||||
h.scratch.scratchpadsChanged()
|
||||
@@ -825,7 +825,7 @@ func (h *toolHost) ScratchpadWrite(name, content, expectedRevision string) (stri
|
||||
return rev, err
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadAppend(name, content string) error {
|
||||
func (h *toolHost) ScratchpadAppend(_, name, content string) error {
|
||||
err := h.pads.Append(name, content)
|
||||
if err == nil && h.scratch != nil {
|
||||
h.scratch.scratchpadsChanged()
|
||||
@@ -833,7 +833,7 @@ func (h *toolHost) ScratchpadAppend(name, content string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadDelete(name string) error {
|
||||
func (h *toolHost) ScratchpadDelete(_, name string) error {
|
||||
err := h.pads.Delete(name)
|
||||
if err == nil && h.scratch != nil {
|
||||
h.scratch.scratchpadsChanged()
|
||||
|
||||
@@ -98,3 +98,65 @@ func TestSwitchProjectPreservesProjectProcessTrees(t *testing.T) {
|
||||
t.Fatalf("switching back should preserve both project process trees")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectRegistryScratchpadsRouteByCallerProject(t *testing.T) {
|
||||
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
reg := newProjectRegistry(preset.Set{}, defaultSettings(), nil, 80, 24)
|
||||
defer reg.Shutdown()
|
||||
|
||||
projectA, err := reg.Open(ctx, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("open project A: %v", err)
|
||||
}
|
||||
projectB, err := reg.Open(ctx, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("open project B: %v", err)
|
||||
}
|
||||
|
||||
a, err := projectA.Session.Spawn(SpawnSpec{
|
||||
Kind: KindCommand,
|
||||
Argv: []string{"sh", "-c", "trap 'exit 0' TERM; while :; do sleep 1; done"},
|
||||
Name: "a-caller",
|
||||
}, 80, 24)
|
||||
if err != nil {
|
||||
t.Fatalf("spawn project A caller: %v", err)
|
||||
}
|
||||
b, err := projectB.Session.Spawn(SpawnSpec{
|
||||
Kind: KindCommand,
|
||||
Argv: []string{"sh", "-c", "trap 'exit 0' TERM; while :; do sleep 1; done"},
|
||||
Name: "b-caller",
|
||||
}, 80, 24)
|
||||
if err != nil {
|
||||
t.Fatalf("spawn project B caller: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = projectA.Session.Kill(a.ID, syscall.SIGTERM)
|
||||
_ = projectB.Session.Kill(b.ID, syscall.SIGTERM)
|
||||
})
|
||||
waitUntilLive(t, a)
|
||||
waitUntilLive(t, b)
|
||||
|
||||
if _, err := reg.ScratchpadWrite(a.ID, "note.md", "project A", ""); err != nil {
|
||||
t.Fatalf("write project A scratchpad: %v", err)
|
||||
}
|
||||
if _, err := reg.ScratchpadWrite(b.ID, "note.md", "project B", ""); err != nil {
|
||||
t.Fatalf("write project B scratchpad: %v", err)
|
||||
}
|
||||
|
||||
gotA, _, err := reg.ScratchpadRead(a.ID, "note.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read project A scratchpad: %v", err)
|
||||
}
|
||||
gotB, _, err := reg.ScratchpadRead(b.ID, "note.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read project B scratchpad: %v", err)
|
||||
}
|
||||
if gotA != "project A" || gotB != "project B" {
|
||||
t.Fatalf("scratchpad routing leaked between projects: A=%q B=%q", gotA, gotB)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestToolHostScratchpadDeleteRemovesPadAndRefreshes(t *testing.T) {
|
||||
host := newToolHost(nil, pads, nil, preset.Set{}, nil, 120, 40)
|
||||
host.scratch = recorder
|
||||
|
||||
if err := host.ScratchpadDelete("doomed.md"); err != nil {
|
||||
if err := host.ScratchpadDelete("", "doomed.md"); err != nil {
|
||||
t.Fatalf("ScratchpadDelete: %v", err)
|
||||
}
|
||||
if recorder.count != 1 {
|
||||
@@ -128,7 +128,7 @@ func TestToolHostScratchpadDeleteRemovesPadAndRefreshes(t *testing.T) {
|
||||
if _, _, err := pads.Read("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("read deleted pad error = %v, want os.ErrNotExist", err)
|
||||
}
|
||||
if err := host.ScratchpadDelete("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||
if err := host.ScratchpadDelete("", "doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("delete missing error = %v, want os.ErrNotExist", err)
|
||||
}
|
||||
if recorder.count != 1 {
|
||||
|
||||
@@ -177,14 +177,14 @@ func (h *blockingToolHost) TimerResume(string, string) error { return nil }
|
||||
func (h *blockingToolHost) TimerList(string) ([]TimerInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (h *blockingToolHost) ScratchpadList() ([]scratchpad.Entry, error) { return nil, nil }
|
||||
func (h *blockingToolHost) ScratchpadRead(string) (string, string, error) {
|
||||
func (h *blockingToolHost) ScratchpadList(string) ([]scratchpad.Entry, error) { return nil, nil }
|
||||
func (h *blockingToolHost) ScratchpadRead(string, string) (string, string, error) {
|
||||
return "", "", nil
|
||||
}
|
||||
func (h *blockingToolHost) ScratchpadWrite(string, string, string) (string, error) {
|
||||
func (h *blockingToolHost) ScratchpadWrite(string, string, string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (h *blockingToolHost) ScratchpadAppend(string, string) error { return nil }
|
||||
func (h *blockingToolHost) ScratchpadDelete(string) error { return nil }
|
||||
func (h *blockingToolHost) ScratchpadAppend(string, string, string) error { return nil }
|
||||
func (h *blockingToolHost) ScratchpadDelete(string, string) error { return nil }
|
||||
func (h *blockingToolHost) WhoAmI(string) WhoAmI { return WhoAmI{} }
|
||||
func (h *blockingToolHost) Help(string, string) HelpResponse { return HelpResponse{} }
|
||||
|
||||
@@ -97,11 +97,11 @@ type ToolHost interface {
|
||||
TimerList(callerID string) ([]TimerInfo, error)
|
||||
|
||||
// Scratchpads.
|
||||
ScratchpadList() ([]scratchpad.Entry, error)
|
||||
ScratchpadRead(name string) (content string, revision string, err error)
|
||||
ScratchpadWrite(name, content, expectedRevision string) (revision string, err error)
|
||||
ScratchpadAppend(name, content string) error
|
||||
ScratchpadDelete(name string) error
|
||||
ScratchpadList(callerID string) ([]scratchpad.Entry, error)
|
||||
ScratchpadRead(callerID, name string) (content string, revision string, err error)
|
||||
ScratchpadWrite(callerID, name, content, expectedRevision string) (revision string, err error)
|
||||
ScratchpadAppend(callerID, name, content string) error
|
||||
ScratchpadDelete(callerID, name string) error
|
||||
|
||||
// Meta.
|
||||
WhoAmI(callerID string) WhoAmI
|
||||
@@ -724,7 +724,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
||||
return ts, 0, "", nil
|
||||
|
||||
case "scratchpad_list":
|
||||
entries, err := h.ScratchpadList()
|
||||
entries, err := h.ScratchpadList(callerID)
|
||||
if err != nil {
|
||||
return nil, codeInternal, err.Error(), nil
|
||||
}
|
||||
@@ -737,7 +737,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
||||
if err := unmarshalParams(params, &p); err != nil {
|
||||
return nil, codeInvalidParams, err.Error(), nil
|
||||
}
|
||||
content, rev, err := h.ScratchpadRead(p.Name)
|
||||
content, rev, err := h.ScratchpadRead(callerID, p.Name)
|
||||
if err != nil {
|
||||
return nil, codeInternal, err.Error(), nil
|
||||
}
|
||||
@@ -752,7 +752,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
||||
if err := unmarshalParams(params, &p); err != nil {
|
||||
return nil, codeInvalidParams, err.Error(), nil
|
||||
}
|
||||
rev, err := h.ScratchpadWrite(p.Name, p.Content, p.ExpectedRevision)
|
||||
rev, err := h.ScratchpadWrite(callerID, p.Name, p.Content, p.ExpectedRevision)
|
||||
if err != nil {
|
||||
// Optimistic-concurrency miss returns ok:false + current_revision
|
||||
// rather than a JSON-RPC error so callers can re-read + merge.
|
||||
@@ -772,7 +772,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
||||
if err := unmarshalParams(params, &p); err != nil {
|
||||
return nil, codeInvalidParams, err.Error(), nil
|
||||
}
|
||||
if err := h.ScratchpadAppend(p.Name, p.Content); err != nil {
|
||||
if err := h.ScratchpadAppend(callerID, p.Name, p.Content); err != nil {
|
||||
return nil, codeInternal, err.Error(), nil
|
||||
}
|
||||
return map[string]any{"ok": true}, 0, "", nil
|
||||
@@ -784,7 +784,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
||||
if err := unmarshalParams(params, &p); err != nil {
|
||||
return nil, codeInvalidParams, err.Error(), nil
|
||||
}
|
||||
if err := h.ScratchpadDelete(p.Name); err != nil {
|
||||
if err := h.ScratchpadDelete(callerID, p.Name); err != nil {
|
||||
return nil, codeInternal, err.Error(), nil
|
||||
}
|
||||
return map[string]any{"ok": true}, 0, "", nil
|
||||
|
||||
Reference in New Issue
Block a user