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.
141 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|