Fix styled switch-back repaint

This commit is contained in:
2026-05-14 17:20:23 +01:00
parent d5ee50fa65
commit 36e738b5c6
12 changed files with 423 additions and 62 deletions

View File

@@ -3,6 +3,7 @@ package harness
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
@@ -74,6 +75,43 @@ func runStep(s *Session, step Step, results map[string]json.RawMessage) error {
return fmt.Errorf("screen does not contain %q:\n%s", step.Contains, screen)
}
return nil
case "assert_not_contains":
screen, err := s.Screen()
if err != nil {
return err
}
if strings.Contains(screen, step.Contains) {
return fmt.Errorf("screen contains %q:\n%s", step.Contains, screen)
}
return nil
case "mark_raw":
if step.SaveAs == "" {
return fmt.Errorf("mark_raw requires save_as")
}
raw, err := json.Marshal(s.RawOffset())
if err != nil {
return err
}
results[step.SaveAs] = raw
return nil
case "assert_raw_since_regex":
raw, ok := results[step.From]
if !ok {
return fmt.Errorf("no saved result %q", step.From)
}
var offset int
if err := json.Unmarshal(raw, &offset); err != nil {
return fmt.Errorf("saved result %q is not a raw offset: %w", step.From, err)
}
re, err := regexp.Compile(step.Regex)
if err != nil {
return err
}
b := s.RawSince(offset)
if !re.Match(b) {
return fmt.Errorf("raw output since %q does not match %q:\n%s", step.From, step.Regex, string(b))
}
return nil
case "assert_regex":
return s.WaitForRegex(step.Regex, timeoutMS(step.TimeoutMS))
case "wait_text":

View File

@@ -0,0 +1,35 @@
{
"name": "switch_replay_clears_viewport",
"cols": 80,
"rows": 24,
"scripts": [
{
"name": "blanktop",
"body": "#!/bin/sh\nprintf '\\033[2;1HFIRST-ROW-TWO\\n'\nsleep 5\n"
}
],
"steps": [
{
"type": "mcp_call",
"method": "spawn_process",
"params": { "kind": "command", "argv": ["blanktop"], "name": "first" },
"save_as": "first"
},
{ "type": "wait_text", "contains": "FIRST-ROW-TWO", "timeout_ms": 5000 },
{
"type": "mcp_call",
"method": "spawn_process",
"params": { "kind": "command", "argv": ["sh", "-lc", "echo SECOND READY; sleep 5"], "name": "second" },
"save_as": "second"
},
{ "type": "wait_text", "contains": "SECOND READY", "timeout_ms": 5000 },
{
"type": "mcp_call",
"method": "select_process",
"params": { "process_id": "{{first.process_id}}" }
},
{ "type": "wait_stable", "timeout_ms": 5000 },
{ "type": "assert_contains", "contains": "FIRST-ROW-TWO" },
{ "type": "assert_not_contains", "contains": "SECOND READY" }
]
}

View File

@@ -0,0 +1,36 @@
{
"name": "switch_replay_preserves_color",
"cols": 80,
"rows": 24,
"scripts": [
{
"name": "color-frame",
"body": "#!/bin/sh\nprintf '\\033[31mREDMARK\\033[0m\\n'\nsleep 5\n"
}
],
"steps": [
{
"type": "mcp_call",
"method": "spawn_process",
"params": { "kind": "command", "argv": ["color-frame"], "name": "color" },
"save_as": "color"
},
{ "type": "wait_text", "contains": "REDMARK", "timeout_ms": 5000 },
{
"type": "mcp_call",
"method": "spawn_process",
"params": { "kind": "command", "argv": ["sh", "-lc", "echo SECOND READY; sleep 5"], "name": "second" },
"save_as": "second"
},
{ "type": "wait_text", "contains": "SECOND READY", "timeout_ms": 5000 },
{ "type": "mark_raw", "save_as": "before_switch_back" },
{
"type": "mcp_call",
"method": "select_process",
"params": { "process_id": "{{color.process_id}}" }
},
{ "type": "wait_stable", "timeout_ms": 5000 },
{ "type": "assert_contains", "contains": "REDMARK" },
{ "type": "assert_raw_since_regex", "from": "before_switch_back", "regex": "\u001b\\[[0-9;]*38;2;[^m]*mREDMARK" }
]
}

View File

@@ -262,3 +262,23 @@ func (s *Session) rawBytes() []byte {
copy(out, s.bytes)
return out
}
func (s *Session) RawOffset() int {
s.bytesMu.Lock()
defer s.bytesMu.Unlock()
return len(s.bytes)
}
func (s *Session) RawSince(offset int) []byte {
s.bytesMu.Lock()
defer s.bytesMu.Unlock()
if offset < 0 {
offset = 0
}
if offset > len(s.bytes) {
offset = len(s.bytes)
}
out := make([]byte, len(s.bytes)-offset)
copy(out, s.bytes[offset:])
return out
}