mirror of
https://github.com/theniceboy/.config.git
synced 2026-07-16 22:01:21 +08:00
agent tracker update
This commit is contained in:
parent
816bf84824
commit
f667ba7225
39 changed files with 8408 additions and 104 deletions
20
agent-tracker/test/docker/Dockerfile
Normal file
20
agent-tracker/test/docker/Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
FROM golang:1.25-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tmux zsh jq python3 fzf git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd -m -s /bin/zsh testuser
|
||||
|
||||
WORKDIR /home/testuser/agent-tracker
|
||||
|
||||
# Source is bind-mounted at runtime; build happens in entrypoint
|
||||
COPY test/docker/entrypoint.sh /entrypoint.sh
|
||||
COPY test/docker/fake-agent.sh /home/testuser/fake-agent.sh
|
||||
COPY test/docker/run-tests.sh /run-tests.sh
|
||||
RUN chmod +x /entrypoint.sh /home/testuser/fake-agent.sh /run-tests.sh
|
||||
|
||||
ENV TMUX_BIN=/usr/bin/tmux
|
||||
ENV GOPROXY=off
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
184
agent-tracker/test/docker/capture_grid.py
Normal file
184
agent-tracker/test/docker/capture_grid.py
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Capture tmux pane as a grid of cells with full ANSI attribute info.
|
||||
|
||||
Handles 256-color (38;5;N / 48;5;N) and true-color (38;2;R;G;B) sequences.
|
||||
"""
|
||||
import sys, json, subprocess, re
|
||||
|
||||
def capture(target):
|
||||
result = subprocess.run(
|
||||
["tmux", "capture-pane", "-e", "-p", "-t", target],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
ANSI_RE = re.compile(r'\x1b\[([0-9;]*)m')
|
||||
|
||||
def empty_cell():
|
||||
return {"ch": " ", "fg": None, "bg": None, "bold": False, "dim": False,
|
||||
"underline": False, "reverse": False, "italic": False}
|
||||
|
||||
class AnsiState:
|
||||
def __init__(self):
|
||||
self.fg = None
|
||||
self.bg = None
|
||||
self.bold = False
|
||||
self.dim = False
|
||||
self.underline = False
|
||||
self.reverse = False
|
||||
self.italic = False
|
||||
|
||||
def reset(self):
|
||||
self.fg = None
|
||||
self.bg = None
|
||||
self.bold = False
|
||||
self.dim = False
|
||||
self.underline = False
|
||||
self.reverse = False
|
||||
self.italic = False
|
||||
|
||||
def cell(self, ch):
|
||||
return {
|
||||
"ch": ch,
|
||||
"fg": self.fg,
|
||||
"bg": self.bg,
|
||||
"bold": self.bold,
|
||||
"dim": self.dim,
|
||||
"underline": self.underline,
|
||||
"reverse": self.reverse,
|
||||
"italic": self.italic,
|
||||
}
|
||||
|
||||
def apply_sgr(params, st):
|
||||
"""Apply an SGR parameter list to the state."""
|
||||
if not params or params == [0]:
|
||||
st.reset()
|
||||
return
|
||||
|
||||
i = 0
|
||||
while i < len(params):
|
||||
p = params[i]
|
||||
if p == 0:
|
||||
st.reset()
|
||||
elif p == 1:
|
||||
st.bold = True
|
||||
elif p == 2:
|
||||
st.dim = True
|
||||
elif p == 3:
|
||||
st.italic = True
|
||||
elif p == 4:
|
||||
st.underline = True
|
||||
elif p == 7:
|
||||
st.reverse = True
|
||||
elif p == 22:
|
||||
st.bold = False
|
||||
st.dim = False
|
||||
elif p == 23:
|
||||
st.italic = False
|
||||
elif p == 24:
|
||||
st.underline = False
|
||||
elif p == 27:
|
||||
st.reverse = False
|
||||
elif p == 39:
|
||||
st.fg = None
|
||||
elif p == 49:
|
||||
st.bg = None
|
||||
elif 30 <= p <= 37:
|
||||
st.fg = str(p - 30)
|
||||
elif 40 <= p <= 47:
|
||||
st.bg = str(p - 40)
|
||||
elif 90 <= p <= 97:
|
||||
st.fg = str(p - 90 + 8)
|
||||
elif 100 <= p <= 107:
|
||||
st.bg = str(p - 100 + 8)
|
||||
elif p == 38 or p == 48:
|
||||
# Extended color: 38;5;N (256-color) or 38;2;R;G;B (true-color)
|
||||
if i + 1 < len(params):
|
||||
if params[i+1] == 5 and i + 2 < len(params):
|
||||
# 256-color
|
||||
color = str(params[i+2])
|
||||
if p == 38:
|
||||
st.fg = color
|
||||
else:
|
||||
st.bg = color
|
||||
i += 2
|
||||
elif params[i+1] == 2 and i + 4 < len(params):
|
||||
# True-color: approximate to 256-color palette
|
||||
r, g, b = params[i+2], params[i+3], params[i+4]
|
||||
color = rgb_to_256(r, g, b)
|
||||
if p == 38:
|
||||
st.fg = color
|
||||
else:
|
||||
st.bg = color
|
||||
i += 4
|
||||
i += 1
|
||||
|
||||
def rgb_to_256(r, g, b):
|
||||
"""Approximate RGB to nearest 256-color palette index."""
|
||||
# Check grayscale first (232-255)
|
||||
if r == g == b:
|
||||
if r < 8:
|
||||
return "16"
|
||||
if r > 248:
|
||||
return "231"
|
||||
gray = int(round((r - 8) / 247 * 24))
|
||||
return str(232 + gray)
|
||||
# 6x6x6 color cube (16-231)
|
||||
def to_cube(v):
|
||||
if v < 48:
|
||||
return 0
|
||||
if v < 115:
|
||||
return 1
|
||||
return min(5, int(round((v - 115) / 40)) + 2)
|
||||
return str(16 + 36 * to_cube(r) + 6 * to_cube(g) + to_cube(b))
|
||||
|
||||
def parse_to_grid(raw, rows, cols):
|
||||
lines = raw.split('\n')
|
||||
grid = []
|
||||
st = AnsiState()
|
||||
for ri in range(rows):
|
||||
if ri >= len(lines):
|
||||
grid.append([empty_cell() for _ in range(cols)])
|
||||
continue
|
||||
line = lines[ri]
|
||||
cells = []
|
||||
i = 0
|
||||
while i < len(line) and len(cells) < cols:
|
||||
ch = line[i]
|
||||
if ch == '\x1b':
|
||||
m = ANSI_RE.match(line, i)
|
||||
if m:
|
||||
param_str = m.group(1)
|
||||
if param_str:
|
||||
params = [int(x) for x in param_str.split(';') if x]
|
||||
else:
|
||||
params = [0]
|
||||
apply_sgr(params, st)
|
||||
i = m.end()
|
||||
continue
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if ch == '\r':
|
||||
i += 1
|
||||
continue
|
||||
|
||||
cells.append(st.cell(ch))
|
||||
i += 1
|
||||
|
||||
while len(cells) < cols:
|
||||
c = empty_cell()
|
||||
c["bg"] = st.bg
|
||||
cells.append(c)
|
||||
|
||||
grid.append(cells[:cols])
|
||||
return grid
|
||||
|
||||
if __name__ == '__main__':
|
||||
target = sys.argv[1]
|
||||
rows = int(sys.argv[2]) if len(sys.argv) > 2 else 50
|
||||
cols = int(sys.argv[3]) if len(sys.argv) > 3 else 100
|
||||
raw = capture(target)
|
||||
grid = parse_to_grid(raw, rows, cols)
|
||||
print(json.dumps(grid))
|
||||
40
agent-tracker/test/docker/debug_grid.py
Normal file
40
agent-tracker/test/docker/debug_grid.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug helper: dump grid cells with attributes for specific rows."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from capture_grid import parse_to_grid
|
||||
import subprocess, json
|
||||
|
||||
target = sys.argv[1]
|
||||
rows_to_show = [int(x) for x in sys.argv[2:]] if len(sys.argv) > 2 else [3, 4, 5]
|
||||
|
||||
raw = subprocess.run(["tmux", "capture-pane", "-e", "-p", "-t", target],
|
||||
capture_output=True, text=True).stdout
|
||||
grid = parse_to_grid(raw, 50, 100)
|
||||
|
||||
for r in rows_to_show:
|
||||
if r >= len(grid):
|
||||
continue
|
||||
row = grid[r]
|
||||
text = "".join(c["ch"] for c in row).rstrip()
|
||||
bg_count = sum(1 for c in row if str(c.get("bg")) not in ("None", "none") and c.get("bg") is not None)
|
||||
bg238 = sum(1 for c in row if str(c.get("bg")) == "238")
|
||||
print(f"\nRow {r}: |{text}|")
|
||||
print(f" cells with any bg: {bg_count}/100, with bg=238: {bg238}/100")
|
||||
# Show cells with attributes
|
||||
shown = 0
|
||||
for i, c in enumerate(row):
|
||||
bg = c.get("bg")
|
||||
fg = c.get("fg")
|
||||
bold = c.get("bold", False)
|
||||
if (bg and str(bg) not in ("None", "none")) or (fg and str(fg) not in ("None", "none") and c["ch"] != " "):
|
||||
ch = c["ch"]
|
||||
parts = []
|
||||
if bg: parts.append(f"bg={bg}")
|
||||
if fg: parts.append(f"fg={fg}")
|
||||
if bold: parts.append("bold")
|
||||
print(f" col {i}: {repr(ch)} [{', '.join(parts)}]")
|
||||
shown += 1
|
||||
if shown > 20:
|
||||
print(" ... (truncated)")
|
||||
break
|
||||
24
agent-tracker/test/docker/entrypoint.sh
Executable file
24
agent-tracker/test/docker/entrypoint.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export HOME=/home/testuser
|
||||
AGENT_SRC="/home/testuser/agent-tracker"
|
||||
AGENT_BIN="/home/testuser/bin"
|
||||
|
||||
mkdir -p "$AGENT_BIN"
|
||||
echo "=== Building agent-tracker ==="
|
||||
cd "$AGENT_SRC"
|
||||
go build -o "$AGENT_BIN/agent" ./cmd/agent 2>&1
|
||||
echo "Build complete."
|
||||
|
||||
echo "=== Running TUI tests ==="
|
||||
bash "$AGENT_SRC/test/docker/run-tests.sh"
|
||||
TUI_EXIT=$?
|
||||
|
||||
echo "=== Running visual tests ==="
|
||||
AGENT_BIN="/home/testuser/bin/agent" python3 "$AGENT_SRC/test/docker/visual_tests.py"
|
||||
VISUAL_EXIT=$?
|
||||
|
||||
echo ""
|
||||
echo "TUI tests: $TUI_EXIT Visual tests: $VISUAL_EXIT"
|
||||
exit $((TUI_EXIT + VISUAL_EXIT))
|
||||
37
agent-tracker/test/docker/fake-agent.sh
Executable file
37
agent-tracker/test/docker/fake-agent.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
# Fake agent: simulates an opencode turn by firing tracker hook events.
|
||||
# Usage: fake-agent.sh <window_id> <user_message>
|
||||
# This mimics what the real hook script does: start_task with the message as summary.
|
||||
set -euo pipefail
|
||||
|
||||
WINDOW_ID="${1:-}"
|
||||
MESSAGE="${2:-working...}"
|
||||
|
||||
AGENT_BIN="/home/testuser/bin/agent"
|
||||
TRACKER_BIN="/home/testuser/bin/tracker-client"
|
||||
|
||||
# Resolve tmux context for the window
|
||||
PANE=$(tmux list-panes -a -F '#{window_id}|#{pane_id}' 2>/dev/null | grep "^${WINDOW_ID}|" | head -1 | cut -d'|' -f2 || true)
|
||||
if [ -z "$PANE" ]; then
|
||||
echo "fake-agent: no pane for window $WINDOW_ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SESSION_ID=$(tmux display-message -p -t "$PANE" "#{session_id}" 2>/dev/null || echo "")
|
||||
WINDOW_NAME=$(tmux display-message -p -t "$PANE" "#{window_name}" 2>/dev/null || echo "")
|
||||
SESSION_NAME=$(tmux display-message -p -t "$PANE" "#{session_name}" 2>/dev/null || echo "")
|
||||
|
||||
echo "fake-agent: window=$WINDOW_ID pane=$PANE session=$SESSION_ID message=$MESSAGE"
|
||||
|
||||
# Start a tracker task (this creates the draft thread)
|
||||
$TRACKER_BIN command \
|
||||
-session-id "$SESSION_ID" \
|
||||
-window-id "$WINDOW_ID" \
|
||||
-pane "$PANE" \
|
||||
-summary "$MESSAGE" \
|
||||
start_task 2>/dev/null || true
|
||||
|
||||
# Revive any done thread for this window with the new name
|
||||
$AGENT_BIN goal revive --window "$WINDOW_ID" --name "$MESSAGE" 2>/dev/null || true
|
||||
|
||||
echo "fake-agent: turn started"
|
||||
43
agent-tracker/test/docker/interactive-entrypoint.sh
Executable file
43
agent-tracker/test/docker/interactive-entrypoint.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export HOME=/home/testuser
|
||||
AGENT_SRC="/home/testuser/agent-tracker"
|
||||
AGENT_BIN="/home/testuser/bin"
|
||||
|
||||
mkdir -p "$AGENT_BIN"
|
||||
echo "=== Building agent-tracker ==="
|
||||
cd "$AGENT_SRC"
|
||||
go build -o "$AGENT_BIN/agent" ./cmd/agent 2>&1
|
||||
echo "Build complete."
|
||||
|
||||
# Seed fake data and create tmux session with drafts
|
||||
bash "$AGENT_SRC/test/docker/seed-data.sh"
|
||||
|
||||
# Launch goals panel in the main window
|
||||
MAIN_WID=$(tmux display-message -p -t test:0 "#{window_id}")
|
||||
tmux send-keys -t test:0 "$AGENT_BIN/agent palette --mode goals --window=$MAIN_WID --path=/tmp --session-name=test --window-name=main" Enter
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Interactive test container ready"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "tmux session 'test' is running with:"
|
||||
echo " - 4 goals (Ship V2 > Board UX, API Refactor; Marketing Site)"
|
||||
echo " - 5 threads (3 assigned, 2 standalone, 1 done)"
|
||||
echo " - 5 drafts (unassigned agent windows)"
|
||||
echo ""
|
||||
echo "Goals panel is open in window 0."
|
||||
echo ""
|
||||
echo "To interact:"
|
||||
echo " docker exec -it <container> tmux attach -t test"
|
||||
echo ""
|
||||
echo "To send keys from outside:"
|
||||
echo " docker exec <container> tmux send-keys -t test:0 <key>"
|
||||
echo ""
|
||||
echo "To capture screen:"
|
||||
echo " docker exec <container> tmux capture-pane -p -t test:0"
|
||||
echo ""
|
||||
echo "Keeping container alive..."
|
||||
exec sleep infinity
|
||||
113
agent-tracker/test/docker/run-tests.sh
Executable file
113
agent-tracker/test/docker/run-tests.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env bash
|
||||
# TUI test: drives the goals panel via tmux send-keys + capture-pane.
|
||||
set -uo pipefail
|
||||
|
||||
AGENT="/home/testuser/bin/agent"
|
||||
PASS=0; FAIL=0
|
||||
|
||||
check() {
|
||||
if echo "$2" | grep -q -- "$3"; then
|
||||
echo " PASS: $1"; PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: $1 (expected '$3')"
|
||||
echo "$2" | head -24 | sed 's/^/ | /'
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
tk() { tmux send-keys -t test:0.0 "$@"; sleep 0.4; }
|
||||
cap() { tmux capture-pane -p -t test:0.0 2>/dev/null; }
|
||||
type_() { printf '%s' "$1" | while IFS= read -r -n1 ch; do tmux send-keys -t test:0.0 -l "$ch"; sleep 0.03; done; sleep 0.2; }
|
||||
|
||||
# ── Setup ───────────────────────────────────────────────────
|
||||
tmux kill-session -t test 2>/dev/null || true
|
||||
tmux new-session -d -s test -x 200 -y 60
|
||||
sleep 0.3
|
||||
rm -rf ~/.cache/agent; mkdir -p ~/.cache/agent
|
||||
|
||||
$AGENT goal add-goal --title "Ship V2" >/dev/null
|
||||
$AGENT goal add-goal --title "Board UX" --parent ship-v2 >/dev/null
|
||||
$AGENT goal add-thread --name "drag-drop" --goal board-ux >/dev/null
|
||||
$AGENT goal add-thread --name "notif" --goal board-ux >/dev/null
|
||||
|
||||
# ── 1. Palette + Goals view ─────────────────────────────────
|
||||
echo "=== 1. Palette + Goals view ==="
|
||||
tmux send-keys -t test:0.0 "$AGENT palette --window=@0 --path=/tmp --session-name=test --window-name=test0" Enter
|
||||
sleep 1.5
|
||||
OUT=$(cap); check "palette shows" "$OUT" "Command Palette"
|
||||
|
||||
tk M-r
|
||||
OUT=$(cap)
|
||||
check "goals header" "$OUT" "Goals"
|
||||
check "goal Ship V2" "$OUT" "Ship V2"
|
||||
check "sub-goal Board UX" "$OUT" "Board UX"
|
||||
check "thread drag-drop" "$OUT" "drag-drop"
|
||||
check "thread notif" "$OUT" "notif"
|
||||
|
||||
# ── 2. Navigation ───────────────────────────────────────────
|
||||
echo "=== 2. Navigation ==="
|
||||
tk e; OUT=$(cap); check "e moves down" "$OUT" "Goals"
|
||||
tk e; OUT=$(cap); check "e again" "$OUT" "Goals"
|
||||
tk e; OUT=$(cap); check "e to thread" "$OUT" "Goals"
|
||||
tk u; OUT=$(cap); check "u moves up" "$OUT" "Goals"
|
||||
tk n; OUT=$(cap); check "n parent jump" "$OUT" "Goals"
|
||||
|
||||
# ── 3. Expand thread ────────────────────────────────────────
|
||||
echo "=== 3. Expand thread ==="
|
||||
tk e; tk e # navigate to a thread
|
||||
tk Right
|
||||
OUT=$(cap); check "expand keeps goals" "$OUT" "Goals"
|
||||
tk Right # collapse
|
||||
OUT=$(cap); check "collapse keeps goals" "$OUT" "Goals"
|
||||
|
||||
# ── 4. More options (o) ─────────────────────────────────────
|
||||
echo "=== 4. More options ==="
|
||||
tk o
|
||||
OUT=$(cap)
|
||||
check "more-options opens" "$OUT" "[Aa]ction"
|
||||
# filter for "add goal"
|
||||
type_ "add goal"
|
||||
OUT=$(cap); check "filter works" "$OUT" "[Aa]dd [Gg]oal"
|
||||
tk Enter
|
||||
OUT=$(cap); check "add-goal input opens" "$OUT" "[Nn]ew"
|
||||
|
||||
# ── 5. Create goal via input ────────────────────────────────
|
||||
echo "=== 5. Create goal ==="
|
||||
type_ "NewGoal"
|
||||
tk Enter
|
||||
OUT=$(cap); check "new goal in view" "$OUT" "NewGoal"
|
||||
|
||||
# ── 6. Rename ───────────────────────────────────────────────
|
||||
echo "=== 6. Rename ==="
|
||||
tk u; tk u # move up to a goal
|
||||
tk r
|
||||
OUT=$(cap); check "rename input opens" "$OUT" "[Nn]ame\|[Tt]itle"
|
||||
tk C-u # clear existing text
|
||||
type_ "RenamedGoal"
|
||||
tk Enter
|
||||
OUT=$(cap); check "rename shows in view" "$OUT" "RenamedGoal"
|
||||
|
||||
# ── 7. Done toggle (D) ──────────────────────────────────────
|
||||
echo "=== 7. Done toggle ==="
|
||||
tk e; tk e # move to a thread
|
||||
tk S-D # shift-D
|
||||
OUT=$(cap); check "D confirm opens" "$OUT" "[Dd]elete\|[Dd]one\|[Tt]oggle"
|
||||
tk y # confirm
|
||||
OUT=$(cap); check "D action applied" "$OUT" "Goals"
|
||||
|
||||
# ── 8. Close + persist check ────────────────────────────────
|
||||
echo "=== 8. Persist ==="
|
||||
sleep 0.5
|
||||
tk M-s
|
||||
sleep 0.5
|
||||
LIST=$($AGENT goal list 2>/dev/null)
|
||||
check "new goal persisted" "$LIST" "NewGoal"
|
||||
check "rename persisted" "$LIST" "RenamedGoal"
|
||||
|
||||
# ── Summary ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "TUI Results: $PASS passed, $FAIL failed"
|
||||
echo "========================================"
|
||||
tmux kill-session -t test 2>/dev/null || true
|
||||
exit $FAIL
|
||||
66
agent-tracker/test/docker/seed-data.sh
Executable file
66
agent-tracker/test/docker/seed-data.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export HOME=/home/testuser
|
||||
AGENT=/home/testuser/bin/agent
|
||||
|
||||
echo "=== Seeding fake data ==="
|
||||
rm -rf ~/.cache/agent
|
||||
mkdir -p ~/.cache/agent
|
||||
|
||||
# Goals
|
||||
$AGENT goal add-goal --title "Ship V2"
|
||||
$AGENT goal add-goal --title "Board UX" --parent ship-v2
|
||||
$AGENT goal add-goal --title "API Refactor" --parent ship-v2
|
||||
$AGENT goal add-goal --title "Marketing Site"
|
||||
|
||||
# Threads (assigned to goals)
|
||||
$AGENT goal add-thread --name "drag-drop upload" --goal board-ux
|
||||
$AGENT goal add-thread --name "notification system" --goal board-ux
|
||||
$AGENT goal add-thread --name "auth middleware" --goal api-refactor
|
||||
|
||||
# Threads (unassigned, top-level)
|
||||
$AGENT goal add-thread --name "Standalone thread A"
|
||||
$AGENT goal add-thread --name "Standalone thread B"
|
||||
|
||||
# Mark one as done
|
||||
DONE_ID=$($AGENT goal list 2>/dev/null | grep 'drag-drop' | grep -o 'th_[a-f0-9]*' | head -1)
|
||||
if [ -n "$DONE_ID" ]; then
|
||||
$AGENT goal done --thread "$DONE_ID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "=== Creating fake drafts ==="
|
||||
# Kill any existing tmux session
|
||||
tmux kill-session -t test 2>/dev/null || true
|
||||
|
||||
# Create session
|
||||
tmux new-session -d -s test -x 120 -y 55 -n main
|
||||
sleep 0.3
|
||||
|
||||
# Create windows to simulate agent sessions (drafts)
|
||||
declare -A WINDOWS
|
||||
WINDOWS=(
|
||||
["fix-bug"]="Fix the login page redirect bug"
|
||||
["add-tests"]="Add unit tests for payment module"
|
||||
["refactor-nav"]="Refactor the navigation component"
|
||||
["docs-readme"]="Update README with new install steps"
|
||||
["perf-query"]="Optimize the slow database query"
|
||||
)
|
||||
|
||||
for wname in "${!WINDOWS[@]}"; do
|
||||
msg="${WINDOWS[$wname]}"
|
||||
tmux new-window -t test -n "$wname"
|
||||
sleep 0.1
|
||||
wid=$(tmux display-message -p -t test "=#{window_id}")
|
||||
/home/testuser/fake-agent.sh "$wid" "$msg" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Go back to first window
|
||||
tmux select-window -t test:0
|
||||
|
||||
echo ""
|
||||
echo "=== Seed data complete ==="
|
||||
$AGENT goal list 2>/dev/null
|
||||
echo ""
|
||||
echo "Drafts (unassigned windows):"
|
||||
echo " fix-bug, add-tests, refactor-nav, docs-readme, perf-query"
|
||||
153
agent-tracker/test/docker/test_goal_move_ue.py
Normal file
153
agent-tracker/test/docker/test_goal_move_ue.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Scenario: cursor on Goal B, press m, u, e.
|
||||
Setup:
|
||||
Goal A
|
||||
Goal B
|
||||
Thread C
|
||||
-----------------------
|
||||
Draft 1
|
||||
Draft 2
|
||||
Draft 3
|
||||
Prints the panel after each keypress so we can observe the ghost position.
|
||||
"""
|
||||
import sys, os, subprocess, time
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from visual_lib import *
|
||||
|
||||
TARGET = "test:0.0"
|
||||
AGENT = os.environ.get("AGENT_BIN", "/home/testuser/bin/agent")
|
||||
TRACKER_SERVER = "/home/testuser/bin/tracker-server"
|
||||
ROWS, COLS = 50, 100
|
||||
|
||||
|
||||
def dump(label):
|
||||
g = capture(TARGET, ROWS, COLS)
|
||||
print(f"\n===== {label} =====")
|
||||
for r in range(ROWS):
|
||||
rt = row_text(r)
|
||||
if rt.strip():
|
||||
print(f" {r:2d}: |{rt}|")
|
||||
# locate ghost
|
||||
for r in range(ROWS):
|
||||
if "(moving)" in row_text(r):
|
||||
print(f" >> ghost at row {r}: |{row_text(r)}|")
|
||||
break
|
||||
|
||||
|
||||
def navigate_to(text):
|
||||
"""Move cursor until the selected (bg 238) row contains text. Down then up."""
|
||||
for _ in range(60):
|
||||
capture(TARGET, ROWS, COLS)
|
||||
for r in range(ROWS):
|
||||
if row_has_bg(r, "238") > 5:
|
||||
if text in row_text(r):
|
||||
return True
|
||||
break
|
||||
send_keys(TARGET, "e", 0.08)
|
||||
for _ in range(60):
|
||||
capture(TARGET, ROWS, COLS)
|
||||
for r in range(ROWS):
|
||||
if row_has_bg(r, "238") > 5:
|
||||
if text in row_text(r):
|
||||
return True
|
||||
break
|
||||
send_keys(TARGET, "u", 0.08)
|
||||
return False
|
||||
|
||||
|
||||
def setup():
|
||||
subprocess.run(["tmux", "kill-session", "-t", "test"], capture_output=True)
|
||||
time.sleep(0.2)
|
||||
subprocess.run(["go", "build", "-o", TRACKER_SERVER, "./cmd/tracker-server"],
|
||||
cwd="/home/testuser/agent-tracker", capture_output=True)
|
||||
subprocess.run(["pkill", "-f", "tracker-server"], capture_output=True)
|
||||
time.sleep(0.2)
|
||||
subprocess.Popen([TRACKER_SERVER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(0.5)
|
||||
subprocess.run(["tmux", "new-session", "-d", "-s", "test",
|
||||
"-x", str(COLS), "-y", str(ROWS)], capture_output=True)
|
||||
time.sleep(0.3)
|
||||
cache = os.path.expanduser("~/.cache/agent")
|
||||
subprocess.run(["rm", "-rf", cache], capture_output=True)
|
||||
subprocess.run(["mkdir", "-p", cache], capture_output=True)
|
||||
|
||||
ga = subprocess.run([AGENT, "goal", "add-goal", "--title", "Goal A"],
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
gb = subprocess.run([AGENT, "goal", "add-goal", "--title", "Goal B"],
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
print(f"goal ids: A={ga} B={gb}")
|
||||
subprocess.run([AGENT, "goal", "add-thread", "--name", "Thread C", "--goal", gb],
|
||||
capture_output=True)
|
||||
|
||||
for name in ["Draft 1", "Draft 2", "Draft 3"]:
|
||||
result = subprocess.run(["tmux", "new-window", "-d", "-t", "test", "-n", name,
|
||||
"-P", "-F", "#{window_id}"], capture_output=True, text=True)
|
||||
wid = result.stdout.strip()
|
||||
if wid:
|
||||
subprocess.run([AGENT, "tracker", "command", "--window-id", wid,
|
||||
"--session", "test", "--window", name, "start_task", name],
|
||||
capture_output=True)
|
||||
time.sleep(0.3)
|
||||
subprocess.run(["tmux", "select-window", "-t", "test:0"], capture_output=True)
|
||||
|
||||
subprocess.run(
|
||||
["tmux", "send-keys", "-t", TARGET,
|
||||
f"{AGENT} palette --mode goals --window=@0 --path=/tmp --session-name=test --window-name=test0",
|
||||
"Enter"],
|
||||
capture_output=True)
|
||||
time.sleep(1.5)
|
||||
capture(TARGET, ROWS, COLS)
|
||||
|
||||
|
||||
def main():
|
||||
setup()
|
||||
dump("initial")
|
||||
|
||||
if not navigate_to("Goal B"):
|
||||
print("FAIL: could not navigate cursor to Goal B")
|
||||
return 1
|
||||
dump("cursor on Goal B")
|
||||
|
||||
send_keys(TARGET, "m", 0.5)
|
||||
dump("after m (enter move mode)")
|
||||
|
||||
send_keys(TARGET, "u", 0.4)
|
||||
dump("after u (expect: nest under Goal A)")
|
||||
|
||||
send_keys(TARGET, "e", 0.4)
|
||||
dump("after e (expect: pop back to root; NOT in draft area)")
|
||||
|
||||
# Assertions
|
||||
g = capture(TARGET, ROWS, COLS)
|
||||
ghost_row = -1
|
||||
first_draft_row = -1
|
||||
for r in range(ROWS):
|
||||
rt = row_text(r)
|
||||
if "(moving)" in rt and ghost_row < 0:
|
||||
ghost_row = r
|
||||
if "Draft" in rt and first_draft_row < 0:
|
||||
first_draft_row = r
|
||||
print(f"\nghost_row={ghost_row} first_draft_row={first_draft_row}")
|
||||
check("ghost still visible after e", ghost_row >= 0)
|
||||
if ghost_row >= 0 and first_draft_row >= 0:
|
||||
check("ghost above draft section (not in draft area)",
|
||||
ghost_row < first_draft_row,
|
||||
f"ghost row {ghost_row} >= first draft row {first_draft_row}")
|
||||
if ghost_row >= 0:
|
||||
gt = row_text(ghost_row)
|
||||
ghost_lead = len(gt) - len(gt.lstrip())
|
||||
# root rows carry 1 leading space (panel padding); find Goal A's indent as the root reference
|
||||
root_lead = 1
|
||||
for r in range(ROWS):
|
||||
if "Goal A" in row_text(r):
|
||||
root_lead = len(row_text(r)) - len(row_text(r).lstrip())
|
||||
break
|
||||
check("ghost at root depth (matches Goal A indent)", ghost_lead == root_lead,
|
||||
f"ghost leading={ghost_lead}, root leading={root_lead}: |{gt}|")
|
||||
|
||||
print(summary())
|
||||
return 0 if FAIL == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
170
agent-tracker/test/docker/test_thread_demote_cursor.py
Normal file
170
agent-tracker/test/docker/test_thread_demote_cursor.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Scenario: cursor on a thread, press m, move ghost into the draft section,
|
||||
press Enter to demote the thread to a draft. The cursor should land on the
|
||||
new draft after Enter.
|
||||
|
||||
Setup:
|
||||
Goal A
|
||||
Thread T (bound to window "AlphaWin")
|
||||
-----------------------
|
||||
Draft 1
|
||||
Draft 2
|
||||
"""
|
||||
import sys, os, subprocess, time
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from visual_lib import *
|
||||
|
||||
TARGET = "test:0.0"
|
||||
AGENT = os.environ.get("AGENT_BIN", "/home/testuser/bin/agent")
|
||||
TRACKER_SERVER = "/home/testuser/bin/tracker-server"
|
||||
ROWS, COLS = 50, 100
|
||||
DRAFT_WINDOW = "AlphaWin"
|
||||
|
||||
|
||||
def navigate_to(text):
|
||||
for _ in range(60):
|
||||
capture(TARGET, ROWS, COLS)
|
||||
for r in range(ROWS):
|
||||
if row_has_bg(r, "238") > 5:
|
||||
if text in row_text(r):
|
||||
return True
|
||||
break
|
||||
send_keys(TARGET, "e", 0.08)
|
||||
for _ in range(60):
|
||||
capture(TARGET, ROWS, COLS)
|
||||
for r in range(ROWS):
|
||||
if row_has_bg(r, "238") > 5:
|
||||
if text in row_text(r):
|
||||
return True
|
||||
break
|
||||
send_keys(TARGET, "u", 0.08)
|
||||
return False
|
||||
|
||||
|
||||
def find_row(substring):
|
||||
capture(TARGET, ROWS, COLS)
|
||||
for r in range(ROWS):
|
||||
if substring in row_text(r):
|
||||
return r
|
||||
return -1
|
||||
|
||||
|
||||
def ghost_below_separator():
|
||||
"""Return True if the (moving) ghost row is below the separator row."""
|
||||
capture(TARGET, ROWS, COLS)
|
||||
sep_row = -1
|
||||
ghost_row = -1
|
||||
for r in range(ROWS):
|
||||
rt = row_text(r)
|
||||
if sep_row < 0 and "───" in rt:
|
||||
sep_row = r
|
||||
if "(moving)" in rt:
|
||||
ghost_row = r
|
||||
return ghost_row > sep_row >= 0
|
||||
|
||||
|
||||
def setup():
|
||||
subprocess.run(["tmux", "kill-session", "-t", "test"], capture_output=True)
|
||||
time.sleep(0.2)
|
||||
subprocess.run(["go", "build", "-o", TRACKER_SERVER, "./cmd/tracker-server"],
|
||||
cwd="/home/testuser/agent-tracker", capture_output=True)
|
||||
subprocess.run(["pkill", "-f", "tracker-server"], capture_output=True)
|
||||
time.sleep(0.2)
|
||||
subprocess.Popen([TRACKER_SERVER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(0.5)
|
||||
subprocess.run(["tmux", "new-session", "-d", "-s", "test", "-x", str(COLS), "-y", str(ROWS)],
|
||||
capture_output=True)
|
||||
time.sleep(0.3)
|
||||
cache = os.path.expanduser("~/.cache/agent")
|
||||
subprocess.run(["rm", "-rf", cache], capture_output=True)
|
||||
subprocess.run(["mkdir", "-p", cache], capture_output=True)
|
||||
|
||||
ga = subprocess.run([AGENT, "goal", "add-goal", "--title", "Goal A"],
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
|
||||
# Create the thread's window + tracker task, then promote it to a thread under Goal A
|
||||
res = subprocess.run(["tmux", "new-window", "-d", "-t", "test", "-n", DRAFT_WINDOW,
|
||||
"-P", "-F", "#{window_id}"], capture_output=True, text=True)
|
||||
wid = res.stdout.strip()
|
||||
subprocess.run([AGENT, "tracker", "command", "--window-id", wid, "--session", "test",
|
||||
"--window", DRAFT_WINDOW, "start_task", DRAFT_WINDOW], capture_output=True)
|
||||
subprocess.run([AGENT, "goal", "promote", "--window", wid, "--name", "Thread T",
|
||||
"--goal", ga], capture_output=True)
|
||||
|
||||
# Two existing drafts (so there is a draft section to move into)
|
||||
for name in ["Draft 1", "Draft 2"]:
|
||||
r = subprocess.run(["tmux", "new-window", "-d", "-t", "test", "-n", name,
|
||||
"-P", "-F", "#{window_id}"], capture_output=True, text=True)
|
||||
w = r.stdout.strip()
|
||||
if w:
|
||||
subprocess.run([AGENT, "tracker", "command", "--window-id", w, "--session", "test",
|
||||
"--window", name, "start_task", name], capture_output=True)
|
||||
time.sleep(0.3)
|
||||
subprocess.run(["tmux", "select-window", "-t", "test:0"], capture_output=True)
|
||||
|
||||
subprocess.run(
|
||||
["tmux", "send-keys", "-t", TARGET,
|
||||
f"{AGENT} palette --mode goals --window=@0 --path=/tmp --session-name=test --window-name=test0",
|
||||
"Enter"],
|
||||
capture_output=True)
|
||||
time.sleep(1.5)
|
||||
capture(TARGET, ROWS, COLS)
|
||||
|
||||
|
||||
def dump(label):
|
||||
g = capture(TARGET, ROWS, COLS)
|
||||
print(f"\n--- {label} ---")
|
||||
for r in range(ROWS):
|
||||
rt = row_text(r)
|
||||
if rt.strip():
|
||||
print(f" {r:2d}: |{rt}|")
|
||||
|
||||
|
||||
def main():
|
||||
setup()
|
||||
dump("initial")
|
||||
|
||||
if not navigate_to("Thread T"):
|
||||
print("FAIL: could not navigate to Thread T")
|
||||
return 1
|
||||
dump("cursor on Thread T")
|
||||
|
||||
send_keys(TARGET, "m", 0.5)
|
||||
dump("after m")
|
||||
|
||||
# Move ghost down until it is in the draft section
|
||||
moved = False
|
||||
for _ in range(12):
|
||||
if ghost_below_separator():
|
||||
moved = True
|
||||
break
|
||||
send_keys(TARGET, "e", 0.3)
|
||||
dump("ghost in draft section" if moved else "ghost NEVER reached draft section")
|
||||
if not moved:
|
||||
print("FAIL: ghost did not reach the draft section")
|
||||
return 1
|
||||
|
||||
# Commit demotion
|
||||
send_keys(TARGET, "Enter", 0.6)
|
||||
dump("after Enter (demote)")
|
||||
|
||||
# Assertions: cursor (selected row) should be on the new draft (DRAFT_WINDOW)
|
||||
g = capture(TARGET, ROWS, COLS)
|
||||
sel_row = -1
|
||||
for r in range(ROWS):
|
||||
if row_has_bg(r, "238") > 5:
|
||||
sel_row = r
|
||||
break
|
||||
check("a row is selected after Enter", sel_row >= 0)
|
||||
if sel_row >= 0:
|
||||
sel_text = row_text(sel_row)
|
||||
check("cursor on the demoted draft", DRAFT_WINDOW in sel_text,
|
||||
f"selected row {sel_row}: |{sel_text}|")
|
||||
check("Thread T demoted (no longer a thread row)", find_row("Thread T") < 0)
|
||||
|
||||
print(summary())
|
||||
return 0 if FAIL == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
217
agent-tracker/test/docker/visual_lib.py
Normal file
217
agent-tracker/test/docker/visual_lib.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Visual test framework for agent-tracker TUI.
|
||||
|
||||
Captures tmux pane cells with full ANSI attribute info and runs assertions
|
||||
on background colors, foreground colors, bold, and spatial layout.
|
||||
"""
|
||||
import sys, json, subprocess, time, os
|
||||
|
||||
GRID = [] # type: list
|
||||
|
||||
def send_keys(target, keys, delay=0.3):
|
||||
subprocess.run(["tmux", "send-keys", "-t", target] + keys.split(),
|
||||
capture_output=True)
|
||||
time.sleep(delay)
|
||||
|
||||
def send_keys_literal(target, text, delay=0.2):
|
||||
"""Send text literally, char by char, preserving spaces."""
|
||||
for ch in text:
|
||||
subprocess.run(["tmux", "send-keys", "-t", target, "-l", ch],
|
||||
capture_output=True)
|
||||
time.sleep(0.03)
|
||||
time.sleep(delay)
|
||||
|
||||
def capture(target, rows=50, cols=100):
|
||||
result = subprocess.run(
|
||||
["python3", os.path.join(os.path.dirname(__file__), "capture_grid.py"),
|
||||
target, str(rows), str(cols)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
global GRID
|
||||
new_grid = json.loads(result.stdout)
|
||||
GRID.clear()
|
||||
GRID.extend(new_grid)
|
||||
return GRID
|
||||
|
||||
def cell(row, col):
|
||||
if row < 0 or row >= len(GRID):
|
||||
return {"ch": " ", "fg": None, "bg": None, "bold": False, "dim": False}
|
||||
r = GRID[row]
|
||||
if col < 0 or col >= len(r):
|
||||
return {"ch": " ", "fg": None, "bg": None, "bold": False, "dim": False}
|
||||
return r[col]
|
||||
|
||||
def row_text(row, cols=None):
|
||||
if row < 0 or row >= len(GRID):
|
||||
return ""
|
||||
r = GRID[row]
|
||||
if cols:
|
||||
r = r[:cols]
|
||||
return "".join(c["ch"] for c in r).rstrip()
|
||||
|
||||
def row_has_bg(row, bg_val, min_cols=1, max_col=None):
|
||||
"""Count cells in a row that have a specific background."""
|
||||
if row < 0 or row >= len(GRID):
|
||||
return 0
|
||||
r = GRID[row]
|
||||
if max_col is None:
|
||||
max_col = len(r)
|
||||
count = 0
|
||||
for c in r[:max_col]:
|
||||
if str(c.get("bg")) == str(bg_val):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def row_bg_extent(row):
|
||||
"""Return (start_col, end_col) of contiguous bg on a row, or (0,0) if none."""
|
||||
if row < 0 or row >= len(GRID):
|
||||
return (0, 0)
|
||||
r = GRID[row]
|
||||
bgs = set()
|
||||
for c in r:
|
||||
bg = c.get("bg")
|
||||
if bg is not None and str(bg) not in ("None", "none"):
|
||||
bgs.add(str(bg))
|
||||
if not bgs:
|
||||
return (0, 0)
|
||||
# find first and last cell with any bg
|
||||
first = None
|
||||
last = None
|
||||
for i, c in enumerate(r):
|
||||
bg = c.get("bg")
|
||||
if bg is not None and str(bg) not in ("None", "none"):
|
||||
if first is None:
|
||||
first = i
|
||||
last = i
|
||||
return (first if first else 0, last if last else 0)
|
||||
|
||||
def find_row_with_text(text, start=0, end=None):
|
||||
"""Find row index containing text."""
|
||||
if end is None:
|
||||
end = len(GRID)
|
||||
for r in range(start, end):
|
||||
if text in row_text(r):
|
||||
return r
|
||||
return -1
|
||||
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
FAILURES = []
|
||||
|
||||
def check(name, condition, detail=""):
|
||||
global PASS, FAIL
|
||||
if condition:
|
||||
PASS += 1
|
||||
print(f" PASS: {name}")
|
||||
else:
|
||||
FAIL += 1
|
||||
FAILURES.append(name)
|
||||
print(f" FAIL: {name}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
def check_cell_bg(name, row, col, expected_bg):
|
||||
c = cell(row, col)
|
||||
actual = c.get("bg")
|
||||
ok = str(actual) == str(expected_bg)
|
||||
check(name, ok,
|
||||
f"row {row} col {col}: expected bg={expected_bg}, got bg={actual} ch={repr(c['ch'])}")
|
||||
|
||||
def check_cell_fg(name, row, col, expected_fg):
|
||||
c = cell(row, col)
|
||||
actual = c.get("fg")
|
||||
ok = str(actual) == str(expected_fg)
|
||||
check(name, ok,
|
||||
f"row {row} col {col}: expected fg={expected_fg}, got fg={actual} ch={repr(c['ch'])}")
|
||||
|
||||
def check_cell_bold(name, row, col, expected=True):
|
||||
c = cell(row, col)
|
||||
actual = c.get("bold", False)
|
||||
ok = actual == expected
|
||||
check(name, ok,
|
||||
f"row {row} col {col}: expected bold={expected}, got bold={actual} ch={repr(c['ch'])}")
|
||||
|
||||
def check_row_bg_full(name, row, expected_bg, width, min_pct=0.9):
|
||||
"""Check that a row has bg color extending at least min_pct of width."""
|
||||
count = row_has_bg(row, expected_bg, max_col=width)
|
||||
pct = count / width if width > 0 else 0
|
||||
ok = pct >= min_pct
|
||||
check(name, ok,
|
||||
f"row {row}: expected bg={expected_bg} covering >= {min_pct*100:.0f}% of {width} cols, "
|
||||
f"got {count}/{width} ({pct*100:.0f}%)")
|
||||
|
||||
def check_row_bg_continuous(name, row, expected_bg, expected_width):
|
||||
"""Check that bg extends continuously from first cell to at least expected_width."""
|
||||
if row >= len(GRID):
|
||||
check(name, False, f"row {row} out of bounds")
|
||||
return
|
||||
r = GRID[row]
|
||||
first_bg = None
|
||||
for i, c in enumerate(r):
|
||||
if str(c.get("bg")) == str(expected_bg):
|
||||
first_bg = i
|
||||
break
|
||||
if first_bg is None:
|
||||
check(name, False, f"row {row}: no cell with bg={expected_bg} found")
|
||||
return
|
||||
# check continuity from first_bg
|
||||
continuous = 0
|
||||
for i in range(first_bg, len(r)):
|
||||
if str(r[i].get("bg")) == str(expected_bg):
|
||||
continuous += 1
|
||||
else:
|
||||
break
|
||||
ok = continuous >= expected_width
|
||||
check(name, ok,
|
||||
f"row {row}: expected continuous bg={expected_bg} for >= {expected_width} cols from col {first_bg}, "
|
||||
f"got {continuous}")
|
||||
|
||||
def dump_row(row, cols=None):
|
||||
"""Pretty-print a row with cell attributes."""
|
||||
if row < 0 or row >= len(GRID):
|
||||
print(f" [row {row}: out of bounds]")
|
||||
return
|
||||
r = GRID[row]
|
||||
if cols:
|
||||
r = r[:cols]
|
||||
text = "".join(c["ch"] for c in r)
|
||||
print(f" row {row}: |{text}|")
|
||||
for i, c in enumerate(r):
|
||||
attrs = []
|
||||
if c.get("bg") and str(c["bg"]) not in ("None", "none"):
|
||||
attrs.append(f"bg={c['bg']}")
|
||||
if c.get("fg") and str(c["fg"]) not in ("None", "none"):
|
||||
attrs.append(f"fg={c['fg']}")
|
||||
if c.get("bold"):
|
||||
attrs.append("bold")
|
||||
if attrs and c["ch"] != " ":
|
||||
print(f" col {i}: {repr(c['ch'])} [{', '.join(attrs)}]")
|
||||
elif attrs and c["ch"] == " ":
|
||||
# only print first few bg-only cells to avoid noise
|
||||
pass
|
||||
|
||||
def dump_row_bg(row, cols=None):
|
||||
"""Show only bg coverage for a row."""
|
||||
if row < 0 or row >= len(GRID):
|
||||
print(f" [row {row}: out of bounds]")
|
||||
return
|
||||
r = GRID[row]
|
||||
if cols:
|
||||
r = r[:cols]
|
||||
parts = []
|
||||
for i, c in enumerate(r):
|
||||
bg = c.get("bg")
|
||||
if bg and str(bg) not in ("None", "none"):
|
||||
parts.append(str(bg))
|
||||
else:
|
||||
parts.append("·")
|
||||
bg_str = " ".join(parts[:60])
|
||||
text = "".join(c["ch"] for c in r).rstrip()
|
||||
print(f" row {row} text: |{text}|")
|
||||
print(f" row {row} bg: {bg_str}")
|
||||
|
||||
def summary():
|
||||
print(f"\n{'='*40}")
|
||||
print(f"Visual Results: {PASS} passed, {FAIL} failed")
|
||||
print(f"{'='*40}")
|
||||
if FAILURES:
|
||||
print(f"Failed: {', '.join(FAILURES)}")
|
||||
return FAIL
|
||||
1805
agent-tracker/test/docker/visual_tests.py
Normal file
1805
agent-tracker/test/docker/visual_tests.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue