ac0ff8e2af
ci / build-test (push) Has been cancelled
- untrack CLAUDE.md (tailnet leak) + stale HANDOFF.md; keep local via .gitignore - add source-available LICENSE (attribution on fork, royalty on commercial use) - add lab/ reproduction kit (fake-cred LIVE-VM/mock rotation POCs) + lab/README - rewrite README to current status (22 drivers, 8 LIVE-VM/14 MOCK-ONLY/0 LIVE-REAL, 247 tests) and carry the credential-handling safety rules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
4.8 KiB
Python
134 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
# Drive the incredigo `guide` Bubble Tea TUI under a REAL pty so it renders genuine
|
|
# frames, send a scripted key sequence, and assert on what it draws. FAKE creds only.
|
|
# Run unbuffered (python3 -u). A SIGALRM watchdog guarantees it can never hang.
|
|
#
|
|
# argv: <guide-args...>
|
|
import os, pty, re, select, sys, time, fcntl, termios, struct, signal
|
|
|
|
ANSI = re.compile(rb'\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[=>]')
|
|
def clean(b): return ANSI.sub(b'', b).decode('utf-8', 'replace')
|
|
|
|
GUIDE_ARGS = sys.argv[1:]
|
|
ENV = dict(os.environ,
|
|
GOPASS_HOMEDIR=os.path.expanduser('~/.lab-gopass'),
|
|
GNUPGHOME=os.path.expanduser('~/.lab-gnupg'),
|
|
INCREDIGO_PASSPHRASE='lab-seal-passphrase-001',
|
|
PATH='/usr/local/bin:' + os.environ.get('PATH', ''),
|
|
TERM='xterm-256color')
|
|
|
|
pid, fd = pty.fork()
|
|
if pid == 0:
|
|
os.execvpe('incredigo', ['incredigo', 'guide', *GUIDE_ARGS], ENV)
|
|
os._exit(127)
|
|
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', 40, 120, 0, 0))
|
|
|
|
# watchdog: kill everything after 25s no matter what, then report
|
|
def boom(*_):
|
|
print(" WATCHDOG: timed out — killing child", flush=True)
|
|
try: os.kill(pid, signal.SIGKILL)
|
|
except OSError: pass
|
|
print("\nTUI-PROBE FAILS: ['watchdog timeout']", flush=True)
|
|
os._exit(2)
|
|
signal.signal(signal.SIGALRM, boom); signal.alarm(25)
|
|
|
|
buf = bytearray()
|
|
def respond(data):
|
|
# Act like a real terminal: answer the queries termenv/bubbletea send before
|
|
# it will render, otherwise it blocks forever waiting for the reply.
|
|
if b'\x1b[6n' in data: # DSR cursor-position request
|
|
os.write(fd, b'\x1b[24;80R')
|
|
if b'\x1b]11;?' in data: # OSC 11 background-color query
|
|
os.write(fd, b'\x1b]11;rgb:1c1c/1c1c/1c1c\x1b\\')
|
|
if b'\x1b]10;?' in data: # OSC 10 foreground-color query
|
|
os.write(fd, b'\x1b]10;rgb:c0c0/c0c0/c0c0\x1b\\')
|
|
if b'\x1b[c' in data or b'\x1b[0c' in data: # Primary Device Attributes
|
|
os.write(fd, b'\x1b[?1;2c')
|
|
def drain(t=0.6):
|
|
end = time.time() + t
|
|
while time.time() < end:
|
|
r, _, _ = select.select([fd], [], [], 0.15)
|
|
if r:
|
|
try: data = os.read(fd, 65536)
|
|
except OSError: break
|
|
if not data: break
|
|
respond(data); buf.extend(data); end = time.time() + t
|
|
return clean(bytes(buf))
|
|
def send(keys):
|
|
os.write(fd, keys); time.sleep(0.25)
|
|
|
|
fails = []
|
|
def check(cond, label):
|
|
print((' OK: ' if cond else ' FAIL: ') + label, flush=True)
|
|
if not cond: fails.append(label)
|
|
def cur(scr):
|
|
# the LAST 'X of N' in the accumulated buffer is the currently-visible frame
|
|
ms = re.findall(r'(\d+) of (\d+)', scr)
|
|
return ms[-1] if ms else None
|
|
|
|
print(">> initial frame", flush=True)
|
|
scr = drain(1.5)
|
|
print(" (captured %d chars)" % len(scr), flush=True)
|
|
check('guided rotation' in scr, "header 'guided rotation' rendered")
|
|
m = re.search(r'(\d+) of (\d+)', scr)
|
|
total = int(m.group(2)) if m else 0
|
|
print(" parsed total =", total, flush=True)
|
|
check(m is not None and m.group(1) == '1', "starts at item 1")
|
|
check('0 marked done' in scr, "starts with 0 marked done")
|
|
check('Read-only' in scr, "read-only notice shown")
|
|
check('manual' in scr or 'auto' in scr, "rotation label (auto/manual) rendered")
|
|
buf.clear()
|
|
|
|
print(">> n,n -> item 3", flush=True)
|
|
send(b'n'); send(b'n'); scr = drain(0.5)
|
|
c = cur(scr)
|
|
check(c is not None and c[0] == '3', "after n,n -> item 3")
|
|
buf.clear()
|
|
|
|
print(">> prev clamp at top", flush=True)
|
|
for _ in range(5): send(b'p')
|
|
scr = drain(0.5)
|
|
c = cur(scr)
|
|
check(c is not None and c[0] == '1', "prev past top clamps at item 1")
|
|
buf.clear()
|
|
|
|
print(">> enter marks done", flush=True)
|
|
send(b'\r'); scr = drain(0.5)
|
|
check('done' in scr and '1 marked done' in scr, "enter -> ✓ done, count 1, advances")
|
|
buf.clear()
|
|
|
|
print(">> s skips", flush=True)
|
|
send(b's'); scr = drain(0.5)
|
|
check('1 marked done' in scr, "s skips, count still 1")
|
|
buf.clear()
|
|
|
|
print(">> next clamp at end", flush=True)
|
|
for _ in range(total + 2): send(b'n')
|
|
scr = drain(0.5)
|
|
c = cur(scr)
|
|
check(c is not None and c[0] == str(total), "next past end clamps at item %d" % total)
|
|
buf.clear()
|
|
|
|
print(">> 'o' open-link no crash", flush=True)
|
|
send(b'o'); scr = drain(0.6)
|
|
check('guided rotation' in scr or scr.strip() == '', "'o' did not crash TUI")
|
|
buf.clear()
|
|
|
|
print(">> quit with q", flush=True)
|
|
send(b'q'); drain(0.6)
|
|
# reap with bounded wait; escalate if needed
|
|
ok = False
|
|
for _ in range(20):
|
|
wpid, status = os.waitpid(pid, os.WNOHANG)
|
|
if wpid == pid:
|
|
ok = os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
|
|
break
|
|
time.sleep(0.1)
|
|
else:
|
|
os.kill(pid, signal.SIGKILL); os.waitpid(pid, 0)
|
|
check(ok, "q quits cleanly (exit 0)")
|
|
|
|
signal.alarm(0)
|
|
print("\nTUI-PROBE FAILS:", fails if fails else "none", flush=True)
|
|
sys.exit(1 if fails else 0)
|