taicpp: port skills, scheduler, telegram, rag, install, release/audit backends with differential tests against tai.py
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generates the shared inputs of the differential tests (tests/misc_data/*.json) with a fixed seed.
|
||||
|
||||
Both the Python reference driver and the C++ driver read these files, so the two sides see identical
|
||||
inputs. Re-run: python3 -I tests/gen_misc_data.py (from taicpp/).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "misc_data")
|
||||
|
||||
|
||||
def write(name, data):
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
with open(os.path.join(OUT, name), "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, ensure_ascii=True, indent=0)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def json_cases():
|
||||
cases = [
|
||||
"{}", "[]", "null", "true", "false", "0", "-0", "1", "-1", "123456789012", "1.5", "-0.25", "1e3", "1E-7", "0.1", "1e22", "1e16", "123456789.123456789",
|
||||
"0.00001", "0.0001", "100.0", "1e-5", "5e-324", "1.7976931348623157e308", "NaN", "Infinity", "-Infinity",
|
||||
'"plain"', '"quote\\"inside"', '"back\\\\slash"', '"tab\\there"', '"nl\\nhere"', '"cr\\rhere"', '"\\b\\f"', '"\\u00e9"', '"\\u20ac"', '"\\ud83d\\ude00"',
|
||||
'"\\u007f"', '"\\u0000"', '"\\u001f"', '"/"', '"\\/"', '"café"', '"€100"', '"\U0001f600 smile"', '"\\ud83d"',
|
||||
'{"a": 1, "b": [1, 2, {"c": null}], "d": "e"}', '{"z": 1, "a": 2, "z": 3}', '[1, [2, [3, [4]]]]', ' { "x" : [ 1 , 2 ] } ',
|
||||
'{"source": "tai-bundled", "mode": "bundled", "scope": "home", "files": {"SKILL.md": "abc12345"}, "installed_at": "2026-10-25T10:11:12.123456+00:00"}',
|
||||
'[1,2', '{"a":}', '{"a" 1}', "{'a': 1}", '"unterminated', '[1,]', '{"a":1,}', "tru", "01", "1.", ".5", "+1", '"\\x41"', "", " ", "{} extra", '"a\nb"',
|
||||
'[true, false, null, 1, 2.0, "s"]', '{"k": "\\u00e9\\u20ac\\ud83d\\ude00"}', "12345678901234567890", "-9223372036854775808", "9223372036854775807",
|
||||
]
|
||||
return cases
|
||||
|
||||
|
||||
def datetime_cases():
|
||||
return [
|
||||
"2026-10-08T09:00", "2026-10-08 09:00", "2026-10-08", "2026-10-08T09:00:30", "2026-10-08T09:00:30.5", "2026-10-08T09:00:30.123456",
|
||||
"2026-10-08T09:00:30.1234567", "2026-10-08T09:00Z", "2026-10-08T09:00:00z", "2026-10-08T09:00+02:00", "2026-10-08T09:00-05:30", "2026-10-08T09:00:00+0200",
|
||||
"2026-10-08T09:00:00+02", "2026-10-08T09:00:00.25+02:00", "20261008T0900", "20261008", "20261008T090030", "2026-10-08T09", "2026-10-08T0900", "2026-10-08x09:00",
|
||||
"2026-13-01", "2026-02-30", "2024-02-29T12:00", "2023-02-29T12:00", "2026-10-08T25:00", "2026-10-08T09:61", "2026-10-08T09:00:61", "now", "", " ", "tomorrow",
|
||||
"2026-10-08T", "2026-10-08T09:", "2026-10-08T09:0", "2026-1-8", "26-10-08", "2026-10-08T09:00+25:00", "2026-10-08T09:00+02:00:30", " 2026-10-08T09:00 ",
|
||||
"2026-10-08T09:00:00.123+01:00", "0001-01-01T00:00", "9999-12-31T23:59:59.999999", "2026-10-08T09:00 +02:00", "2026-10-08T09:00:00Z\n", "2026-03-29T02:30",
|
||||
"2026-10-25T02:30", "2026-10-25T12:00:00+00:00", "2026-10-25T12:00:00.000001+00:00",
|
||||
"2026-10-08T09:00x+02:00", "2026-10-08T09:00\u00a0+02:00", "2026-10-08T09:00\n+02:00", "2026-10-08T09:00:00x-02:00", "2026-10-08T09:00:00xZ", "2026-10-08T09:00:00 Z",
|
||||
"2026-10-08T09 Z", "2026-10-08T0900 Z", "2026-10-08T090000 +01", "2026-10-08T09:00:00.123456789 +01", "2026-10-08T09:00:00,5 +01", "2026-10-08T09:00:00.5x+01",
|
||||
"2026-10-08T09:00:00+01 ", "2026-10-08T09:00:00+0100", "2026-10-08T09:00:00+01:0", "2026-10-08T09:00:00+1", "2026-10-08T09:00:00+01:00:", "2026-10-08T09:00:00++01",
|
||||
"2026-10-08T09:00:00+01Z", "2026-10-08T09:00:00 +", "2026-10-08T09:00:00-", "2026-10-08 T09:00", "2026-10-08TT09:00", "2026-10-08\u00e9", "2026-10-08\u00e909:00",
|
||||
"2026-10-08\u20ac09:00", "2026-10-08T09:00:00.000001+01:00:00.5", "1999-12-31T24:00:00", "2026-10-08T24:00", "2026-10-08T24:00:00.0", "9999-12-31T24:00",
|
||||
"2026-10-08T09:00:00+0100:00", "2026-10-08T09:00:00+01:00:0000", "2026-10-08T09:00:00-00:00", "2026-W01-1", "2026-W53-7", "2026-W00-1", "2026-W41-8", "2026W414",
|
||||
"2026W41", "2026-W41-4T10:00", "2026-W41T10:00", "2026-10-08t09:00", "2026-10-08T09:+02:00", "2026-10-08T09:00xx+02:00", "2026-10-08T09:00:00.12345x+02:00",
|
||||
"2026-10-08T09:00:00.1234567x+02:00", "2026-10-08T09:00:00.123456xy+02:00", "2026-10-08T09:0+02:00", "2026-10-08T09:00:00:00", "2026-10-08T09:00:00.5.5", "2026-10-08T0900:00",
|
||||
"2026-10-08T09:0000", "2026-10-08T09:00:00+01:00:00.1234567", "2026-10-08T09:00:00+01:00:00.", "2026-W53-1", "2020-W53-5", "2015-W53-4", "2026-W41-4 10:00", "2026-W41-4x",
|
||||
"2026-W41-4-10", "2026-W41-410", "2026W4141", "2026W41410", "2026-10-08T09:00-23:59", "2026-10-08T09:00+23:59:59", "2026-10-08T09:00+24:00", "2026-10-08T09:00-24:00",
|
||||
"0001-01-01T00:00+00:00", "0001-01-01T00:00+01:00", "0001-01-01T00:00-01:00", "0001-01-01T10:00:00+00:00", "9999-12-31T23:59:59.999999+00:00", "9999-12-31T23:59:59+00:00",
|
||||
"9999-12-31T23:59:59-01:00", "9999-12-31T00:00:00+01:00", "0001-01-01T00:00:00.000001+00:00", "1970-01-01T00:00:00", "1900-01-01T00:00:00", "1800-06-15T12:00:00",
|
||||
"2026-03-29T01:59:59", "2026-03-29T02:00:00", "2026-03-29T03:00:00", "2026-10-25T01:59:59", "2026-10-25T02:00:00", "2026-10-25T03:00:00", "2026-10-25T02:59:59.999999",
|
||||
"2038-01-19T03:14:08", "2100-12-31T23:59:59", "5000-01-01T00:00:00", "2026-10-08T09:00:00\u00e9", "\u2003 2026-10-08T09:00 \u2003", "2026-10-08T09:00:00,123",
|
||||
"2026-02-29", "2028-02-29", "2100-02-29", "2000-02-29", "20260229", "2026-10-08T09:00:00.5+02:00", "2026-10-08T09:00:00,5+02:00", "2026-10-08T09:00:00.+02:00",
|
||||
]
|
||||
|
||||
|
||||
def duration_cases():
|
||||
return ["30", "30s", "5m", "2h", "1d", "1w", "10 m", " 7D ", "0", "0s", "5x", "", "m", "-5", "5.5", "1e3", "00012h", "9999999999d", "5 M", "5mm", "5٠"]
|
||||
|
||||
|
||||
def delay_cases():
|
||||
return [-5.5, -1, -0.5, 0, 0.9, 1, 59, 59.99, 60, 61, 3599, 3600, 3601, 86399, 86400, 86401, 90061, 1000000, 1e9]
|
||||
|
||||
|
||||
def chunk_cases():
|
||||
rng = random.Random(20261025)
|
||||
words = ["alpha", "beta", "gamma", "delta", "epsilon", "café", "naïve", "€", "\U0001f600", "x", "long" * 20, "", " ", "\t"]
|
||||
seps = ["\n", "\r\n", "\r", "\x0b", "\x0c", "\x1c", "\x1d", "\x1e", "\x85", "
", "
"]
|
||||
cases = []
|
||||
fixed = [
|
||||
"", "one line", "a\nb\nc", "a\n\nb\n\n\nc", "\n\n\n", " \n ", "trail\n", "\nlead", "a\r\nb\rc\x0bd\x0ce\x1cf\x1dg\x1eh\x85i
j
k",
|
||||
"x" * 5000, "\n".join("line %d" % i for i in range(200)), "\n".join("w" * 50 for _ in range(100)), "é" * 4100 + "\nnext", "\n".join(["same"] * 12),
|
||||
"a\nb\nb\nb\nc\nc\nd", "p\np\np\nq\nq\nq\nq\nr", "same\nsame\nsame", "\n".join(["same"] * 2 + ["other"] + ["same"] * 3),
|
||||
]
|
||||
for text in fixed:
|
||||
cases.append({"text": text, "max_lines": 60, "overlap": 3, "max_chars": 4000})
|
||||
for text in fixed[:12]:
|
||||
cases.append({"text": text, "max_lines": 5, "overlap": 2, "max_chars": 40})
|
||||
cases.append({"text": text, "max_lines": 3, "overlap": 0, "max_chars": 10})
|
||||
cases.append({"text": text, "max_lines": 4, "overlap": 9, "max_chars": 100})
|
||||
for index in range(80):
|
||||
count = rng.randint(0, 150)
|
||||
parts = []
|
||||
for _ in range(count):
|
||||
parts.append(" ".join(rng.choice(words) for _ in range(rng.randint(0, 12))))
|
||||
parts.append(rng.choice(seps) if rng.random() < 0.3 else "\n")
|
||||
text = "".join(parts)
|
||||
cases.append({"text": text, "max_lines": rng.choice([60, 7, 13, 1, 2]), "overlap": rng.choice([3, 0, 1, 5]), "max_chars": rng.choice([4000, 120, 300, 17])})
|
||||
return cases
|
||||
|
||||
|
||||
def fts_cases():
|
||||
return ["", "hello world", "Hello, World!", "café naïve", "Kelvin", "İstanbul", "a-b_c d.e", "UPPER lower 123", " ", "!!!", "x", "\"quoted\" 'single'", "tab\tsep\nnl", "éè ü", "snake_case_word", "alpha1beta2"]
|
||||
|
||||
|
||||
def main():
|
||||
write("json_cases.json", json_cases())
|
||||
write("datetime_cases.json", datetime_cases())
|
||||
write("duration_cases.json", duration_cases())
|
||||
write("delay_cases.json", delay_cases())
|
||||
write("chunk_cases.json", chunk_cases())
|
||||
write("fts_cases.json", fts_cases())
|
||||
print("wrote %s" % OUT)
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
# Standalone build of the misc modules (skills, sched, telegram, rag, install) and their tests.
|
||||
# Usage (from taicpp/): make -f tests/misc.mk test
|
||||
# The integrator can fold these into the main Makefile; see docs/wiring/misc.md.
|
||||
CXX ?= g++
|
||||
CXXFLAGS ?= -std=c++23 -Wall -Wextra -O1
|
||||
B = build/misc
|
||||
DIRS = src/skills src/sched src/telegram src/rag src/install
|
||||
INC = $(foreach d,$(DIRS),-I$d)
|
||||
SRCS = $(foreach d,$(DIRS),$(wildcard $(d)/*.cpp))
|
||||
OBJS = $(patsubst %.cpp,$(B)/%.o,$(SRCS))
|
||||
LIBS = -lsqlite3 -lcurl -lz -lpthread
|
||||
|
||||
all: $(B)/misc_test $(B)/misc_driver
|
||||
|
||||
$(B)/%.o: %.cpp
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) $(INC) -MMD -MP -c $< -o $@
|
||||
|
||||
$(B)/misc_test: tests/misc_test.cpp $(OBJS)
|
||||
@mkdir -p $(B)
|
||||
$(CXX) $(CXXFLAGS) $(INC) tests/misc_test.cpp $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(B)/misc_driver: tests/misc_driver.cpp $(OBJS)
|
||||
@mkdir -p $(B)
|
||||
$(CXX) $(CXXFLAGS) $(INC) tests/misc_driver.cpp $(OBJS) $(LIBS) -lz -o $@
|
||||
|
||||
test: $(B)/misc_test
|
||||
$(B)/misc_test
|
||||
|
||||
objs: $(OBJS)
|
||||
|
||||
.PHONY: all test objs
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Differential test of the misc modules: runs each scenario through the real tai.py (tests/misc_py.py)
|
||||
and through the C++ driver (build/misc/misc_driver) on identical state, then diffs the transcripts and
|
||||
the resulting file trees.
|
||||
|
||||
Usage (from taicpp/): python3 tests/misc_diff.py [scenario ...] (build first: make -f tests/misc.mk all)
|
||||
Exit code 0 only when every selected scenario matches byte for byte after path/timestamp normalising.
|
||||
"""
|
||||
import difflib
|
||||
import http.server
|
||||
import json
|
||||
import threading
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
TAICPP = os.path.dirname(HERE)
|
||||
DRIVER = os.path.join(TAICPP, "build", "misc", "misc_driver")
|
||||
PYDRV = os.path.join(HERE, "misc_py.py")
|
||||
DATA = os.path.join(HERE, "misc_data")
|
||||
|
||||
ORDER = ["json", "datetime", "chunk", "fts", "skills_data", "skills_files", "skills_ops", "install_hooks", "install_hooks_noshell", "install_ops",
|
||||
"install_noengine", "vault_line", "sched", "rag", "telegram", "telegram_install", "release", "audit"]
|
||||
EXTRA_BIN = {"install_hooks": ("zsh", "fish")} # scenario -> extra executables to create in the restricted PATH dir
|
||||
TZ = "Europe/Amsterdam"
|
||||
|
||||
TS_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[+-]\d{2}:\d{2})?")
|
||||
STAMP_RE = re.compile(r"\d{8}T\d{6}Z")
|
||||
PID_RE = re.compile(r"\.tmp-\d+")
|
||||
|
||||
|
||||
TG_UPDATES = [
|
||||
{"update_id": 10, "message": {"chat": {"id": 111}, "text": "/start"}},
|
||||
{"update_id": 11, "message": {"chat": {"id": 111}, "text": "hello there"}},
|
||||
{"update_id": 12, "message": {"chat": {"id": 222}, "voice": {"file_id": "f1"}}},
|
||||
{"update_id": 13, "message": {"chat": {"id": 111}, "text": "explode now"}},
|
||||
{"update_id": 14},
|
||||
{"update_id": 15, "message": {"chat": {"id": 333}, "text": "/new"}},
|
||||
{"update_id": 16, "message": {"chat": {"id": 111}, "text": "long"}},
|
||||
{"message": {"chat": {"id": 5}, "text": "no update id"}},
|
||||
]
|
||||
|
||||
|
||||
class TgServer:
|
||||
"""Scripted Bot API on localhost; records every request so both sides can be compared on the wire."""
|
||||
|
||||
def __init__(self):
|
||||
self.log = []
|
||||
self.polls = {}
|
||||
outer = self
|
||||
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def reply(self, code, body, reason=None):
|
||||
data = body if isinstance(body, bytes) else body.encode()
|
||||
self.send_response(code, reason)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def handle_any(self, method):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
body = self.rfile.read(n).decode() if n else ""
|
||||
outer.log.append("%s %s ct=%s body=%s" % (method, self.path, self.headers.get("Content-Type"), body))
|
||||
path = self.path
|
||||
if path.startswith("/file/bot"):
|
||||
if path.endswith("voice/f1.oga"):
|
||||
return self.reply(200, b"AUDIO")
|
||||
if path.endswith("voice/fmodel.oga"):
|
||||
return self.reply(200, b"OTHER")
|
||||
return self.reply(404, "{}", "Not Found")
|
||||
token, _, name = path[len("/bot"):].partition("/")
|
||||
if token == "bad":
|
||||
return self.reply(401, json.dumps({"ok": False, "description": "Unauthorized"}), "Unauthorized")
|
||||
if token == "nojson" and name == "getMe":
|
||||
return self.reply(200, "not json")
|
||||
if name == "getMe":
|
||||
return self.reply(200, json.dumps({"ok": True, "result": {} if token == "noname" else {"username": "tai_test_bot"}}))
|
||||
if name == "getUpdates":
|
||||
count = outer.polls[token] = outer.polls.get(token, 0) + 1
|
||||
if count == 1:
|
||||
return self.reply(200, json.dumps({"ok": True, "result": TG_UPDATES}))
|
||||
return self.reply(502, "{}", "Bad Gateway")
|
||||
if name == "getFile":
|
||||
fid = json.loads(body).get("file_id")
|
||||
if fid == "nofile":
|
||||
return self.reply(200, json.dumps({"ok": True, "result": {}}))
|
||||
if fid == "fbad":
|
||||
return self.reply(500, "{}", "Internal Server Error")
|
||||
if fid == "fmodel":
|
||||
return self.reply(200, json.dumps({"ok": True, "result": {"file_path": "voice/fmodel.oga"}}))
|
||||
if fid == "fmissingfile":
|
||||
return self.reply(200, json.dumps({"ok": True, "result": {"file_path": "voice/gone.oga"}}))
|
||||
return self.reply(200, json.dumps({"ok": True, "result": {"file_path": "voice/f1.oga"}}))
|
||||
return self.reply(200, json.dumps({"ok": True, "result": {}}))
|
||||
|
||||
def do_POST(self):
|
||||
self.handle_any("POST")
|
||||
|
||||
def do_GET(self):
|
||||
self.handle_any("GET")
|
||||
|
||||
self.httpd = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
|
||||
self.port = self.httpd.server_address[1]
|
||||
threading.Thread(target=self.httpd.serve_forever, daemon=True).start()
|
||||
|
||||
def stop(self):
|
||||
self.httpd.shutdown()
|
||||
self.httpd.server_close()
|
||||
|
||||
|
||||
SERVER_SCENARIOS = {"telegram", "telegram_install"}
|
||||
|
||||
|
||||
def normalize(text, root):
|
||||
text = text.replace(root, "<ROOT>")
|
||||
text = TS_RE.sub("<TS>", text)
|
||||
text = STAMP_RE.sub("<STAMP>", text)
|
||||
text = PID_RE.sub(".tmp-<PID>", text)
|
||||
return text
|
||||
|
||||
|
||||
def make_root(base, name, extra_bins):
|
||||
root = os.path.join(base, name)
|
||||
for sub in ("home/.tai", "proj", "bin"):
|
||||
os.makedirs(os.path.join(root, sub), exist_ok=True)
|
||||
os.symlink(sys.executable, os.path.join(root, "bin", "python3"))
|
||||
for tool in extra_bins:
|
||||
path = os.path.join(root, "bin", tool)
|
||||
with open(path, "w") as handle:
|
||||
handle.write("#!/bin/sh\nexit 0\n")
|
||||
os.chmod(path, 0o755)
|
||||
return root
|
||||
|
||||
|
||||
def env_for(root):
|
||||
return {
|
||||
"HOME": os.path.join(root, "home"),
|
||||
"TAI_HOME": os.path.join(root, "home", ".tai"),
|
||||
"PATH": os.path.join(root, "bin"),
|
||||
"TZ": TZ,
|
||||
"LANG": "C.UTF-8",
|
||||
"MISC_DATA_DIR": DATA,
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
"SHELL": "/bin/bash",
|
||||
"MISC_TAI_PY": os.path.join(os.path.dirname(TAICPP), "tai.py"),
|
||||
}
|
||||
|
||||
|
||||
def tree(root):
|
||||
found = {}
|
||||
for dirpath, dirs, files in os.walk(root):
|
||||
dirs.sort()
|
||||
rel_dir = os.path.relpath(dirpath, root)
|
||||
if rel_dir == "bin" or rel_dir.startswith("bin" + os.sep):
|
||||
continue
|
||||
if rel_dir.startswith(os.path.join("home", ".tai", "venv")):
|
||||
continue # venv contents embed the invoking interpreter path; the report lines are compared instead
|
||||
for d in dirs:
|
||||
full = os.path.join(dirpath, d)
|
||||
if os.path.islink(full):
|
||||
found[os.path.relpath(full, root)] = "symlink -> " + normalize(os.readlink(full), root)
|
||||
for f in files:
|
||||
full = os.path.join(dirpath, f)
|
||||
rel = os.path.relpath(full, root)
|
||||
if os.path.islink(full):
|
||||
found[rel] = "symlink -> " + normalize(os.readlink(full), root)
|
||||
continue
|
||||
with open(full, "rb") as handle:
|
||||
raw = handle.read()
|
||||
mode = oct(os.stat(full).st_mode & 0o777)
|
||||
try:
|
||||
body = normalize(raw.decode("utf-8"), root)
|
||||
digest = hashlib.sha256(body.encode("utf-8")).hexdigest()[:16]
|
||||
except UnicodeDecodeError:
|
||||
digest = "bin-" + hashlib.sha256(raw).hexdigest()[:16]
|
||||
if rel.endswith((".db", ".db-wal", ".db-shm")):
|
||||
digest = "db"
|
||||
found[normalize(rel, root)] = "%s %s" % (mode, digest)
|
||||
return found
|
||||
|
||||
|
||||
def run(cmd, root, extra_env=None):
|
||||
env = env_for(root)
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
done = subprocess.run(cmd, cwd=os.path.join(root, "proj"), env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
start_new_session=True, timeout=600)
|
||||
return done.returncode, done.stdout.decode("utf-8", "replace"), done.stderr.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def check(name, base, verbose):
|
||||
root_py = make_root(base, name + "-py", EXTRA_BIN.get(name, ()))
|
||||
code, _out, err = run([sys.executable, "-I", PYDRV, name, root_py, "prepare"], root_py)
|
||||
if code != 0:
|
||||
return False, "python prepare failed (%d): %s" % (code, err[-2000:])
|
||||
root_cpp = os.path.join(base, name + "-cpp")
|
||||
shutil.copytree(root_py, root_cpp, symlinks=True)
|
||||
# copytree keeps absolute symlink targets; point the bin symlink at the same interpreter (already absolute)
|
||||
def one(cmd, root):
|
||||
if name not in SERVER_SCENARIOS:
|
||||
return run(cmd, root)
|
||||
srv = TgServer()
|
||||
try:
|
||||
extra = {"MISC_TG_API": "http://127.0.0.1:%d/bot" % srv.port, "MISC_TG_FILE": "http://127.0.0.1:%d/file/bot" % srv.port}
|
||||
code, o, e = run(cmd, root, extra)
|
||||
finally:
|
||||
srv.stop()
|
||||
o = o + "---- wire log\n" + "\n".join(l.replace(str(srv.port), "PORT") for l in srv.log)
|
||||
if name == "telegram":
|
||||
o += "\n---- stderr\n" + e
|
||||
return code, o, ""
|
||||
|
||||
code_p, out_p, err_p = one([sys.executable, "-I", PYDRV, name, root_py], root_py)
|
||||
code_c, out_c, err_c = one([DRIVER, name, root_cpp], root_cpp)
|
||||
out_p = normalize(out_p, root_py)
|
||||
out_c = normalize(out_c, root_cpp)
|
||||
problems = []
|
||||
if code_p != 0:
|
||||
problems.append("python exit %d: %s" % (code_p, err_p[-2000:]))
|
||||
if code_c != 0:
|
||||
problems.append("cpp exit %d: %s" % (code_c, err_c[-2000:]))
|
||||
if out_p != out_c:
|
||||
diff = list(difflib.unified_diff(out_p.splitlines(), out_c.splitlines(), "python", "cpp", lineterm="", n=1))
|
||||
problems.append("transcript differs (%d diff lines):\n%s" % (len(diff), "\n".join(diff[:60])))
|
||||
tp, tc = tree(root_py), tree(root_cpp)
|
||||
if tp != tc:
|
||||
lines = []
|
||||
for key in sorted(set(tp) | set(tc)):
|
||||
if tp.get(key) != tc.get(key):
|
||||
lines.append(" %s: py=%s cpp=%s" % (key, tp.get(key), tc.get(key)))
|
||||
problems.append("file tree differs:\n%s" % "\n".join(lines[:60]))
|
||||
if verbose:
|
||||
sys.stdout.write("---- python transcript (%d lines)\n%s\n" % (len(out_p.splitlines()), out_p))
|
||||
return not problems, "\n".join(problems), len(out_p.splitlines()), len(tp)
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
||||
verbose = "-v" in sys.argv
|
||||
if not os.path.isdir(DATA):
|
||||
subprocess.run([sys.executable, "-I", os.path.join(HERE, "gen_misc_data.py")], check=True)
|
||||
names = args or ORDER
|
||||
base = tempfile.mkdtemp(prefix="misc-diff-")
|
||||
failed = 0
|
||||
try:
|
||||
for name in names:
|
||||
result = check(name, base, verbose)
|
||||
ok, why = result[0], result[1]
|
||||
if ok:
|
||||
print("PASS %-14s transcript=%d lines, tree=%d files" % (name, result[2], result[3]))
|
||||
else:
|
||||
failed += 1
|
||||
print("FAIL %s\n%s" % (name, why))
|
||||
finally:
|
||||
if "--keep" not in sys.argv:
|
||||
shutil.rmtree(base, ignore_errors=True)
|
||||
else:
|
||||
print("kept %s" % base)
|
||||
print("%d scenario(s), %d failed" % (len(names), failed))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
// Fast unit checks for the misc modules. The behavioral proof against tai.py is tests/misc_diff.py.
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include "bundled.h"
|
||||
#include "bundled_data.h"
|
||||
#include "chunker.h"
|
||||
#include "miscdatetime.h"
|
||||
#include "miscjson.h"
|
||||
#include "miscpy.h"
|
||||
#include "miscsha256.h"
|
||||
#include "schedtime.h"
|
||||
#include "skillfile.h"
|
||||
using namespace detz;
|
||||
|
||||
int main() {
|
||||
assert(misc::Sha256::hex("abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
assert(misc::Sha256::hex("") == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
assert(misc::Json::loads("{\"a\": [1, 2.5, \"\\u00e9\"]}").dumps() == "{\"a\": [1, 2.5, \"\\u00e9\"]}");
|
||||
assert(misc::Json::float_repr(1e16) == "1e+16" && misc::Json::float_repr(0.0001) == "0.0001" && misc::Json::float_repr(1e-5) == "1e-05");
|
||||
assert(misc::DateTime::fromisoformat("2026-10-08T09:00Z").isoformat() == "2026-10-08T09:00:00+00:00");
|
||||
assert(sched::ScheduleTime::format_delay(3600) == "1h" && sched::ScheduleTime::format_delay(-1) == "overdue");
|
||||
assert(*sched::ScheduleTime::parse_duration("2h") == 7200);
|
||||
assert(rag::Chunker::chunk_lines("a\nb\nc", 2, 0, 100).size() == 2);
|
||||
assert(skills::BundledData::files().size() == 6);
|
||||
assert(skills::SkillFiles::skill_name_valid("deep-research") && !skills::SkillFiles::skill_name_valid("Bad"));
|
||||
assert(skills::BundledSkills::names().size() == 6);
|
||||
std::puts("misc_test ok");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user