Files
patterm/internal/app/mcp_inject_test.go
Harry Bayliss 3622c41fd0 Land staged session/MCP/chrome work + sidebar clear-J fix
This batches the in-flight [Unreleased] block from CHANGELOG.md into a
single commit. Highlights:

- Real MCP protocol layer (initialize / tools/list / tools/call) so
  vendor MCP clients can complete the handshake against the per-PID
  socket. Legacy direct-dispatch preserved for the harness.
- New mcp_injection kinds — cli_override for codex, config_env for
  opencode — joining the existing env-var and config_file paths so
  patterm can slot into more agents without touching their real
  config or auth.
- Ctrl+A/D and Ctrl+W/S focus navigation across tabs and intra-tab
  process lists, recognised in legacy / kitty CSI u / xterm
  modifyOtherKeys encodings.
- Palette macros (sw / k / sp ) and reordering so open sessions
  surface above spawn-new entries.
- Two-row tab bar, sidebar/tabbar/status chrome cache, viewport-wipe
  on agent spawn, CR-terminated orchestrator injections, and split-
  Enter PTY writes so paste-detecting TUIs see Enter as a key event.

Also fixes the bug logged in TODO: claude's Ctrl+O tool-call expansion
emits CSI 0 J, which the viewport renderer was forwarding verbatim —
wiping the sidebar to the right of the cursor and leaving the chrome
cache convinced nothing had changed. CSI 0 J and CSI 1 J are now
translated into per-row ECH sequences clamped to the viewport, same
as CSI 2 J and CSI K already were.

Agent guides (CLAUDE.md / AGENTS.md) now spell out the
TODO->CHANGELOG workflow so completed items land in the changelog
rather than as ticked entries left behind in TODO.
2026-05-14 19:09:35 +01:00

141 lines
3.9 KiB
Go

package app
import (
"encoding/json"
"strings"
"testing"
)
func TestMergeTOMLMCPFreshFile(t *testing.T) {
out, err := mergeTOMLMCP(nil, "mcp_servers", "/usr/local/bin/patterm",
[]string{"mcp-stdio", "--socket", "/run/patterm/1.sock", "--identity", "abc123"})
if err != nil {
t.Fatal(err)
}
s := string(out)
if !strings.Contains(s, "[mcp_servers.patterm]") {
t.Fatalf("missing patterm table:\n%s", s)
}
if !strings.Contains(s, `command = "/usr/local/bin/patterm"`) {
t.Fatalf("missing command line:\n%s", s)
}
if !strings.Contains(s, `args = ["mcp-stdio", "--socket", "/run/patterm/1.sock", "--identity", "abc123"]`) {
t.Fatalf("missing args line:\n%s", s)
}
}
func TestMergeTOMLMCPPreservesOtherSections(t *testing.T) {
existing := `model = "gpt-5"
[mcp_servers.something_else]
command = "x"
args = ["y"]
`
out, err := mergeTOMLMCP([]byte(existing), "mcp_servers", "/bin/patterm",
[]string{"mcp-stdio", "--socket", "/s", "--identity", "id"})
if err != nil {
t.Fatal(err)
}
s := string(out)
if !strings.Contains(s, `model = "gpt-5"`) {
t.Fatalf("lost top-level model setting:\n%s", s)
}
if !strings.Contains(s, "[mcp_servers.something_else]") {
t.Fatalf("lost neighbouring mcp_servers entry:\n%s", s)
}
if !strings.Contains(s, "[mcp_servers.patterm]") {
t.Fatalf("missing patterm entry:\n%s", s)
}
}
func TestMergeTOMLMCPReplacesStalePatternEntry(t *testing.T) {
existing := `[mcp_servers.patterm]
command = "/old/path"
args = ["stale"]
[mcp_servers.keep]
command = "k"
`
out, err := mergeTOMLMCP([]byte(existing), "mcp_servers", "/new/bin",
[]string{"mcp-stdio", "--socket", "/s2", "--identity", "id2"})
if err != nil {
t.Fatal(err)
}
s := string(out)
if strings.Contains(s, "/old/path") {
t.Fatalf("stale command remained:\n%s", s)
}
if strings.Contains(s, "stale") {
t.Fatalf("stale args remained:\n%s", s)
}
if !strings.Contains(s, "[mcp_servers.keep]") {
t.Fatalf("dropped sibling section:\n%s", s)
}
// New patterm block appears exactly once.
if c := strings.Count(s, "[mcp_servers.patterm]"); c != 1 {
t.Fatalf("expected single patterm block, got %d:\n%s", c, s)
}
}
func TestMergeJSONMCPFreshFile(t *testing.T) {
out, err := mergeJSONMCP(nil, "mcp", "/bin/patterm",
[]string{"mcp-stdio", "--socket", "/s", "--identity", "id"})
if err != nil {
t.Fatal(err)
}
var root map[string]any
if err := json.Unmarshal(out, &root); err != nil {
t.Fatalf("output not valid json: %v\n%s", err, out)
}
mcp, ok := root["mcp"].(map[string]any)
if !ok {
t.Fatalf("mcp key missing or wrong type: %v", root)
}
entry, ok := mcp["patterm"].(map[string]any)
if !ok {
t.Fatalf("patterm entry missing: %v", mcp)
}
if entry["type"] != "local" {
t.Fatalf("expected type=local, got %v", entry["type"])
}
cmd, ok := entry["command"].([]any)
if !ok || len(cmd) != 6 || cmd[0] != "/bin/patterm" {
t.Fatalf("unexpected command: %#v", entry["command"])
}
}
func TestMergeJSONMCPPreservesExistingKeysAndReplacesPatterm(t *testing.T) {
existing := `{
"$schema": "https://opencode.ai/config.json",
"model": "claude-sonnet-4",
"mcp": {
"patterm": {"type": "local", "command": ["old"]},
"other": {"type": "local", "command": ["k"]}
}
}`
out, err := mergeJSONMCP([]byte(existing), "mcp", "/new/bin",
[]string{"mcp-stdio", "--socket", "/s", "--identity", "id"})
if err != nil {
t.Fatal(err)
}
var root map[string]any
if err := json.Unmarshal(out, &root); err != nil {
t.Fatalf("output not valid json: %v\n%s", err, out)
}
if root["$schema"] != "https://opencode.ai/config.json" {
t.Fatalf("lost $schema: %v", root["$schema"])
}
if root["model"] != "claude-sonnet-4" {
t.Fatalf("lost model: %v", root["model"])
}
mcp := root["mcp"].(map[string]any)
if _, ok := mcp["other"]; !ok {
t.Fatalf("dropped sibling mcp entry")
}
entry := mcp["patterm"].(map[string]any)
cmd := entry["command"].([]any)
if cmd[0] != "/new/bin" {
t.Fatalf("patterm entry not refreshed: %v", cmd)
}
}