Sync MCP surface to SPEC §7 process model
Rename list_children/read_output/kill/send_message_to to their SPEC §7 process_id-shaped names; drop report_to_parent (direction inferred by send_message) and policy_check (replaced by per-project trust gating). Add the SPEC's missing tools: start_process, restart_process, close_process, rename_process, select_process, get_process_status, get_project_status, get_process_raw_output, search_output, get_process_ports, whoami, help. Process model now distinguishes agent/terminal/command kinds with opaque p_<6hex> IDs. Command entries are session-persistent so they survive PTY exit and can be Restart'd. Status enum gains starting and stopped. screen_version, port detection, and bracketed-paste send_input land alongside. Trust gating (internal/trust) replaces the regex policy: command-preset spawns return needs_trust on first use; the user confirms in a status-line modal and the grant persists to \$XDG_DATA_HOME/patterm/projects/<key>/trust.json. Tests cover send_message direction inference (parent↔child, sibling rejection, nil caller paths) and trust grant persistence across reopen.
This commit is contained in:
99
internal/app/host_test.go
Normal file
99
internal/app/host_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mkChild builds a Child without starting a PTY. Use sparingly — the
|
||||
// resulting child has no emulator / ring buffer / status pointer set
|
||||
// the way newChild would.
|
||||
func mkChild(id, name, parent string) *Child {
|
||||
return &Child{ID: id, Name: name, ParentID: parent}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageOrchestratorToChild(t *testing.T) {
|
||||
parent := mkChild("p_aaa", "claude-1", "")
|
||||
child := mkChild("p_bbb", "codex-1", "p_aaa")
|
||||
|
||||
line, err := classifySendMessage(parent, child, parent.ID, "hi child")
|
||||
if err != nil {
|
||||
t.Fatalf("expected success, got %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(line, "[orchestrator] hi child") {
|
||||
t.Fatalf("parent→child should tag [orchestrator], got %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageChildToParent(t *testing.T) {
|
||||
parent := mkChild("p_aaa", "claude-1", "")
|
||||
child := mkChild("p_bbb", "codex-1", "p_aaa")
|
||||
|
||||
line, err := classifySendMessage(child, parent, child.ID, "status update")
|
||||
if err != nil {
|
||||
t.Fatalf("expected success, got %v", err)
|
||||
}
|
||||
if !strings.Contains(line, "[sub-agent:codex-1]") {
|
||||
t.Fatalf("child→parent should tag [sub-agent:<name>], got %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageSiblingRejected(t *testing.T) {
|
||||
parent := mkChild("p_aaa", "claude-1", "")
|
||||
sibA := mkChild("p_bbb", "codex-1", "p_aaa")
|
||||
sibB := mkChild("p_ccc", "codex-2", "p_aaa")
|
||||
_ = parent
|
||||
|
||||
_, err := classifySendMessage(sibA, sibB, sibA.ID, "hey")
|
||||
if err == nil {
|
||||
t.Fatalf("sibling send_message should fail with not_related")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "neither parent nor child") {
|
||||
t.Fatalf("error should mention sibling routing rule, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageUnrelatedRejected(t *testing.T) {
|
||||
// Two unrelated top-level processes — they have no shared lineage.
|
||||
a := mkChild("p_aaa", "claude-1", "")
|
||||
b := mkChild("p_bbb", "codex-1", "")
|
||||
|
||||
_, err := classifySendMessage(a, b, a.ID, "hi")
|
||||
if err == nil {
|
||||
t.Fatalf("unrelated top-level send_message should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageCannotSendToSelf(t *testing.T) {
|
||||
c := mkChild("p_aaa", "claude-1", "")
|
||||
_, err := classifySendMessage(c, c, c.ID, "talking to myself")
|
||||
if err == nil {
|
||||
t.Fatalf("self-send should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageNilCallerAcceptsTopLevel(t *testing.T) {
|
||||
// Caller arrived over an MCP connection without a resolved patterm
|
||||
// identity (top-level tool client). Target is top-level; should
|
||||
// land as [orchestrator].
|
||||
target := mkChild("p_aaa", "claude-1", "")
|
||||
line, err := classifySendMessage(nil, target, "" /* unknown caller id */, "go")
|
||||
if err != nil {
|
||||
t.Fatalf("expected success for nil-caller → top-level target, got %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(line, "[orchestrator] go") {
|
||||
t.Fatalf("expected [orchestrator] tag, got %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySendMessageNilCallerRejectsNonTopLevelTarget(t *testing.T) {
|
||||
parent := mkChild("p_aaa", "claude-1", "")
|
||||
child := mkChild("p_bbb", "codex-1", "p_aaa")
|
||||
_ = parent
|
||||
|
||||
_, err := classifySendMessage(nil, child, "", "hi")
|
||||
if err == nil {
|
||||
t.Fatalf("nil caller → non-top-level should fail")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user