taicpp: integrate build and tests (libdetz.a, per-module test binaries, tests/run_all.py, real lint, parity targets)

make test builds and runs 32 steps; not green: misc.diff and mplex.e2e fail on real module bugs.
test_main.cpp replaced by util_config_test.cpp; stale *.mk and store_test rule removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 07:12:45 +01:00
co-authored by Claude Sonnet 5.5
parent f44e505b0f
commit e7f0227bcb
16 changed files with 346 additions and 179 deletions
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## [Unreleased] - 2026-10-25
- Build and test integration after the parallel module ports. Not released: `make test` is not green (see below), so VERSION stays 1.44.2 and no tag was made.
- Makefile rewritten: objects in `build/obj`, archive `build/libdetz.a` (no main.cpp), one test binary per delivered test source (foundation, store, cli, util/config, agent diff/e2e/sched, shell, cap/mcp unit/probe/cli, misc, tui spec/shot/script/rows/chat/grr, mplex x11), `make test`, `make lint` (real `-Werror -fsyntax-only` over src, main.cpp and tests, no `|| true`), `make parity`, `make parity-check` (docs/wiring/verify.md).
- `tests/run_all.py`: runs all 32 steps (C++ unit binaries plus the python differential drivers), logs to `build/test-logs/`, prints a summary table, exits 1 on any failure.
- `-I` for moloapp now searched in `../../moloapp/src`, the worktree-relative path and `$HOME/projects/moloapp/src` (`MOLOAPP_SRC=` overrides); src/cli needs `tenancy/strategy.h` from it.
- Removed the obsolete `store_test` rule, the unused per-module `*.mk` files and `tests/foundation.mk`, `tests/misc.mk`.
- `tests/test_main.cpp` replaced by `tests/util_config_test.cpp`: its shell section is covered by `shell_test.cpp`, its cli section by `cli_test.cpp`, its llm section tested the removed placeholder llm API (ChatClient/ModelHealth/Opencode); the util and config checks and the `--help` run are kept.
+116 -22
View File
@@ -1,46 +1,140 @@
CXX ?= g++
CXXSTD = -std=c++23
WARN = -Wall -Wextra
CXXFLAGS ?= $(CXXSTD) $(WARN) -O2 -I../../moloapp/src
OPT ?= -O1
BUILD = build
VERSION = $(shell cat VERSION)
BIN = $(BUILD)/detz
GEN_INC = $(BUILD)/gen
VERSION_HPP = $(GEN_INC)/version.hpp
# moloapp (src/cli includes tenancy/strategy.h). ../../moloapp/src resolves only from the main
# checkout (taicpp is /home/retoor/projects/tai/taicpp); from a worktree under
# .claude/worktrees/<id>/taicpp it is five levels up. Override with MOLOAPP_SRC=...
MOLOAPP_SRC ?= $(firstword $(wildcard ../../moloapp/src ../../../../../moloapp/src $(HOME)/projects/moloapp/src))
ifeq ($(MOLOAPP_SRC),)
$(error moloapp/src not found: set MOLOAPP_SRC=/path/to/moloapp/src)
endif
# Include path: every src directory (headers include their own directory first, so same-named
# headers in two modules do not collide).
SRC_DIRS = $(shell find src -type d)
INC = -I$(GEN_INC) $(addprefix -I,$(SRC_DIRS)) -I$(MOLOAPP_SRC)
CXXFLAGS ?= $(CXXSTD) $(WARN) $(OPT)
LDLIBS = -lsqlite3 -lcurl -lz -pthread -lutil
ALL_SRCS = $(shell find src -name '*.cpp')
ALL_OBJS = $(patsubst %.cpp,$(BUILD)/%.o,$(ALL_SRCS))
MAIN_SRC = main.cpp
MAIN_OBJ = $(BUILD)/main.o
ALL_OBJS = $(patsubst %.cpp,$(BUILD)/obj/%.o,$(ALL_SRCS))
MAIN_OBJ = $(BUILD)/obj/main.o
LIB = $(BUILD)/libdetz.a
.PHONY: all static lint test test-build test-run parity parity-check version-bump changelog tag release release-dry docs clean
all: $(BIN)
$(VERSION_HPP): VERSION
$(VERSION_HPP): VERSION src/cli/version.hpp.in
@mkdir -p $(GEN_INC)
@sed 's/@VERSION@/$(VERSION)/' src/cli/version.hpp.in > $@
$(BUILD)/%.o: %.cpp $(VERSION_HPP)
$(BUILD)/obj/%.o: %.cpp $(VERSION_HPP)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) -c $< -o $@
$(CXX) $(CXXFLAGS) $(INC) -MMD -MP -c $< -o $@
$(BUILD)/main.o: main.cpp $(VERSION_HPP)
@mkdir -p $(BUILD)
$(CXX) $(CXXFLAGS) -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) -c main.cpp -o $@
$(LIB): $(ALL_OBJS)
@rm -f $@
ar rcs $@ $^
$(BIN): $(MAIN_OBJ) $(ALL_OBJS)
$(CXX) $(CXXFLAGS) $(MAIN_OBJ) $(ALL_OBJS) -lsqlite3 -lcurl -lz -pthread -lutil -o $@
$(CXX) $(CXXFLAGS) $(MAIN_OBJ) $(ALL_OBJS) $(LDLIBS) -o $@
static: $(MAIN_OBJ) $(ALL_OBJS)
$(CXX) $(CXXFLAGS) -pthread -static $(MAIN_OBJ) $(ALL_OBJS) third_party/curl/libcurl.a third_party/sqlite/libsqlite3.a -lssl -lcrypto third_party/jitterentropy/libjitterentropy.a -lzstd -lz -lnghttp2 -lpsl -lbrotlidec -lidn2 -lrtmp -o build/detz-static
$(BUILD)/store_test: tests/store_test.cpp src/store/store.cpp
@mkdir -p $(BUILD)
$(CXX) $(CXXFLAGS) -Isrc/store tests/store_test.cpp src/store/store.cpp -lsqlite3 -lcurl -lz -pthread -lutil -o $@
$(CXX) $(CXXFLAGS) -pthread -static $(MAIN_OBJ) $(ALL_OBJS) third_party/curl/libcurl.a third_party/sqlite/libsqlite3.a -lssl -lcrypto third_party/jitterentropy/libjitterentropy.a -lzstd -lz -lnghttp2 -lpsl -lbrotlidec -lidn2 -lrtmp -o $(BUILD)/detz-static
$(BUILD)/detz_test: tests/test_main.cpp $(ALL_SRCS) $(VERSION_HPP)
@mkdir -p $(BUILD)
$(CXX) $(CXXFLAGS) -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) tests/test_main.cpp $(ALL_SRCS) -lsqlite3 -lcurl -lz -pthread -lutil -o $@
# ---------------------------------------------------------------------------
# Test binaries. Every test links tests/<name>.cpp against build/libdetz.a (the archive pulls
# only the objects a test needs, main.cpp is never in it). No test links main.cpp.
# TESTBIN(output, source, optional include flags). Several modules ship same-named headers
# (text.h, scheduler.h, consts.h, ...), so a test whose flat includes mean one module gets only
# that module's -I dirs.
MISC_INC = -Isrc/skills -Isrc/sched -Isrc/telegram -Isrc/rag -Isrc/install
SHELL_INC = -Isrc/util -Isrc/shell
define TESTBIN
TEST_BINS += $(1)
LINT_TESTS += $(2)
LINT_INC_$(2) := $(if $(3),$(3),$$(INC))
$(1): $(2) $(LIB) $(VERSION_HPP)
@mkdir -p $$(dir $$@)
$$(CXX) $$(CXXFLAGS) $(if $(3),$(3),$$(INC)) $(2) $$(LIB) $$(LDLIBS) -o $$@
endef
lint:
$(CXX) $(CXXSTD) $(WARN) -Werror -fsyntax-only main.cpp -I$(GEN_INC) $(foreach d,$(shell find src -type d),-I$d) || true
TEST_BINS =
LINT_TESTS =
# foundation (store, config, session, plexer, repl)
$(eval $(call TESTBIN,$(BUILD)/foundation_test,tests/foundation_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/foundation_probe,tests/foundation_probe.cpp))
$(eval $(call TESTBIN,$(BUILD)/repl_probe,tests/repl_probe.cpp))
$(eval $(call TESTBIN,$(BUILD)/store_test,tests/store_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/cli_test,tests/cli_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/util_config_test,tests/util_config_test.cpp))
# agent core
$(eval $(call TESTBIN,$(BUILD)/agent_diff,tests/agent_diff.cpp))
$(eval $(call TESTBIN,$(BUILD)/agent_e2e,tests/agent_e2e.cpp))
$(eval $(call TESTBIN,$(BUILD)/agent_sched,tests/agent_sched.cpp))
# shell and util (shell_parity_driver is built by tests/shell_parity.py itself; lint only)
LINT_TESTS += tests/shell_parity_driver.cpp
LINT_INC_tests/shell_parity_driver.cpp := $(SHELL_INC)
$(eval $(call TESTBIN,$(BUILD)/shell_test,tests/shell_test.cpp,$(SHELL_INC)))
# cap and mcp
$(eval $(call TESTBIN,$(BUILD)/capmcp/capmcp_unit,tests/capmcp_unit.cpp))
$(eval $(call TESTBIN,$(BUILD)/capmcp/capmcp_probe,tests/capmcp_probe.cpp))
$(eval $(call TESTBIN,$(BUILD)/capmcp/capmcp_cli,tests/capmcp_cli.cpp))
# misc (skills, sched, telegram, rag, install)
$(eval $(call TESTBIN,$(BUILD)/misc/misc_test,tests/misc_test.cpp,$(MISC_INC)))
$(eval $(call TESTBIN,$(BUILD)/misc/misc_driver,tests/misc_driver.cpp,$(MISC_INC)))
# tui and grr
$(eval $(call TESTBIN,$(BUILD)/tui_spec,tests/tui_spec.cpp))
$(eval $(call TESTBIN,$(BUILD)/tui_shot,tests/tui_shot.cpp))
$(eval $(call TESTBIN,$(BUILD)/tui_script,tests/tui_script.cpp))
$(eval $(call TESTBIN,$(BUILD)/tui_rows,tests/tui_rows.cpp))
$(eval $(call TESTBIN,$(BUILD)/tui_chat,tests/tui_chat.cpp))
$(eval $(call TESTBIN,$(BUILD)/tui_grr,tests/tui_grr.cpp))
# mplex
$(eval $(call TESTBIN,$(BUILD)/mplex_cli,tests/mplex_cli_main.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_foundation_test,tests/mplex_foundation_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_core_test,tests/mplex_core_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_model_test,tests/mplex_model_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_copy_test,tests/mplex_copy_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_vt_test,tests/mplex_vt_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_flow_test,tests/mplex_flow_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_regress_test,tests/mplex_regress_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_spec_test,tests/mplex_spec_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_server_test,tests/mplex_server_test.cpp))
$(eval $(call TESTBIN,$(BUILD)/mplex_commands_test,tests/mplex_commands_test.cpp))
# test-build compiles every test binary; test-run executes them (tests/run_all.py) and prints the
# summary table; `make test` does both and fails if any step fails or any binary does not build.
test-build: $(TEST_BINS)
test-run:
python3 tests/run_all.py
test: test-build
python3 tests/run_all.py
# lint: real -Werror -fsyntax-only over every source, main.cpp and every test source, in parallel.
# Test sources use the same include set their binary is built with (see TESTBIN).
LINT_SRCS = $(ALL_SRCS) main.cpp
lint: $(VERSION_HPP)
@mkdir -p $(BUILD)
@: > $(BUILD)/lint.cmds
@$(foreach f,$(LINT_SRCS),echo '$(CXX) $(CXXSTD) $(WARN) -Werror -fsyntax-only $(INC) $(f) || { echo "LINT FAIL $(f)"; exit 1; }' >> $(BUILD)/lint.cmds;)
@$(foreach f,$(LINT_TESTS),echo '$(CXX) $(CXXSTD) $(WARN) -Werror -fsyntax-only $(LINT_INC_$(f)) $(f) || { echo "LINT FAIL $(f)"; exit 1; }' >> $(BUILD)/lint.cmds;)
@if xargs -d '\n' -P $$(nproc) -I{} sh -c {} < $(BUILD)/lint.cmds; then echo "lint: $$(wc -l < $(BUILD)/lint.cmds) files clean"; else echo "lint: FAILED"; exit 1; fi
# Parity verification (docs/wiring/verify.md). Output goes to build/parity/ and PARITY.md.
parity: $(BIN)
DETZ_BIN=$(abspath $(BIN)) tools/parity.sh run --detz $(abspath $(BIN))
parity-check: $(BIN)
DETZ_BIN=$(abspath $(BIN)) tools/parity.sh check --detz $(abspath $(BIN))
version-bump:
tools/version-bump.sh $(PART)
@@ -58,9 +152,9 @@ release-dry:
PART=$(PART) MSG="$(MSG)" sh -n tools/release.sh && echo "release dry-run OK"
docs:
@test -f README.md && test -f SPEC.md && test -f workflow.md && test -f versioning.md && test -f docs/agent.md && test -f docs/tui.md && test -f docs/mcp.md && test -f docs/cap.md && echo "docs OK"
@for f in agent cap mcp tui mplex shell store util llm misc foundation parity; do test -f docs/$$f.md || { echo "missing docs/$$f.md"; exit 1; }; done; echo "docs OK"
clean:
rm -rf $(BUILD)
-include src/*/*.mk
-include $(shell find $(BUILD)/obj -name '*.d' 2>/dev/null)
-1
View File
@@ -1 +0,0 @@
SRCS += src/agent/agent.cpp
-1
View File
@@ -1 +0,0 @@
SRCS += src/cap/cap.cpp
-1
View File
@@ -1 +0,0 @@
SRCS += src/comms/comms.cpp
-1
View File
@@ -1 +0,0 @@
LLM_SRCS = src/llm/llm.cpp
-1
View File
@@ -1 +0,0 @@
SRCS += src/mcp/mcp.cpp
-25
View File
@@ -1,25 +0,0 @@
SRCS += src/mplex/mplex_args.cpp src/mplex/mplex_canvas.cpp src/mplex/mplex_chooser.cpp src/mplex/mplex_client.cpp
SRCS += src/mplex/mplex_clientconn.cpp src/mplex/mplex_clockmode.cpp src/mplex/mplex_command_specs.cpp src/mplex/mplex_commands.cpp
SRCS += src/mplex/mplex_common.cpp src/mplex/mplex_config.cpp src/mplex/mplex_copymode.cpp src/mplex/mplex_ctx.cpp
SRCS += src/mplex/mplex_default_keys.cpp src/mplex/mplex_flowscreen.cpp src/mplex/mplex_format.cpp src/mplex/mplex_input.cpp
SRCS += src/mplex/mplex_json.cpp src/mplex/mplex_keys.cpp src/mplex/mplex_layout.cpp src/mplex/mplex_mode.cpp
SRCS += src/mplex/mplex_options.cpp src/mplex/mplex_pane.cpp src/mplex/mplex_panenumbers.cpp src/mplex/mplex_prompt.cpp
SRCS += src/mplex/mplex_py.cpp src/mplex/mplex_renderer.cpp src/mplex/mplex_server.cpp src/mplex/mplex_style.cpp
SRCS += src/mplex/mplex_toml.cpp src/mplex/mplex_vtscreen.cpp
SRCS += src/mplex/mplex_plexer_flow.cpp
# Standalone client/server binary and its tests; every output lands in $(BUILD).
MPLEX_LIB_SRCS = $(filter src/mplex/%.cpp src/mplex/mplex_plexer_flow.cpp,$(SRCS))
MPLEX_INC = -Isrc/mplex -Isrc/plexer
$(BUILD)/mplex_cli: tests/mplex_cli_main.cpp $(MPLEX_LIB_SRCS)
@mkdir -p $(BUILD)
$(CXX) $(CXXSTD) $(WARN) -O2 $(MPLEX_INC) tests/mplex_cli_main.cpp $(MPLEX_LIB_SRCS) -o $@
$(BUILD)/mplex_flow_test: tests/mplex_flow_test.cpp $(MPLEX_LIB_SRCS)
@mkdir -p $(BUILD)
$(CXX) $(CXXSTD) $(WARN) -O2 $(MPLEX_INC) tests/mplex_flow_test.cpp $(MPLEX_LIB_SRCS) -o $@
mplex-flow-test: $(BUILD)/mplex_flow_test
python3 -I tests/mplex_flow_gen.py .. $(BUILD)/flow_corpus.jsonl
$(BUILD)/mplex_flow_test $(BUILD)/flow_corpus.jsonl
mplex-e2e: $(BUILD)/mplex_cli
python3 -I tests/mplex_e2e.py ..
.PHONY: mplex-flow-test mplex-e2e
-1
View File
@@ -1 +0,0 @@
SRCS += src/plexer/plexer.cpp
-1
View File
@@ -1 +0,0 @@
SHELL_SRCS = src/shell/audio.cpp src/shell/boxdata.cpp src/shell/edge.cpp src/shell/glow.cpp src/shell/lanes.cpp src/shell/livestream.cpp src/shell/proc.cpp src/shell/ptyproc.cpp src/shell/runlive.cpp src/shell/sandbox.cpp src/shell/session.cpp src/shell/shellio.cpp src/shell/spinner.cpp src/shell/targets.cpp
-2
View File
@@ -1,2 +0,0 @@
# Sources of src/tui are found by the top Makefile (find src -name '*.cpp'); nothing to list here.
# The grr client needs libcurl (already in the top Makefile link line) and pthreads (libc).
-2
View File
@@ -1,2 +0,0 @@
UTIL_SRCS = src/util/ansi.cpp src/util/diffrender.cpp src/util/digest.cpp src/util/http.cpp src/util/jsonvalue.cpp src/util/markdown.cpp src/util/pyglob.cpp src/util/pyhighlight.cpp src/util/pypath.cpp src/util/sequencematcher.cpp src/util/shlex.cpp src/util/text.cpp src/util/toolargs.cpp src/util/utf8.cpp
CONFIG_SRCS = src/config/config.cpp
-36
View File
@@ -1,36 +0,0 @@
# Foundation tests (store, config, session, plexer, repl).
# Run from taicpp/: make -f tests/foundation.mk test
# Outputs stay under build/. Independent of the main Makefile on purpose: the
# foundation area builds and tests itself with only sqlite3 as a link dependency.
CXX ?= g++
CXXFLAGS ?= -std=c++23 -Wall -Wextra -O1
INC = -Isrc -Isrc/store -Isrc/session -Isrc/config -Isrc/plexer -Isrc/repl
FB = build/foundation
FSRCS = $(wildcard src/store/*.cpp) $(wildcard src/session/*.cpp) $(wildcard src/config/*.cpp) $(wildcard src/plexer/*.cpp) $(wildcard src/repl/*.cpp)
FOBJS = $(patsubst src/%.cpp,$(FB)/%.o,$(FSRCS))
LIBS = -lsqlite3 -pthread
.PHONY: all test clean-foundation
all: build/foundation_probe build/repl_probe build/foundation_test
$(FB)/%.o: src/%.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(INC) -c $< -o $@
build/foundation_probe: tests/foundation_probe.cpp $(FOBJS)
$(CXX) $(CXXFLAGS) $(INC) $< $(FOBJS) $(LIBS) -o $@
build/repl_probe: tests/repl_probe.cpp $(FOBJS)
$(CXX) $(CXXFLAGS) $(INC) $< $(FOBJS) $(LIBS) -o $@
build/foundation_test: tests/foundation_test.cpp $(FOBJS)
$(CXX) $(CXXFLAGS) $(INC) $< $(FOBJS) $(LIBS) -o $@
test: all
build/foundation_test
python3 tests/foundation_parity.py
python3 tests/repl_persist.py
python3 tests/tmux_ask.py
clean-foundation:
rm -rf $(FB) build/foundation_probe build/repl_probe build/foundation_test
-34
View File
@@ -1,34 +0,0 @@
# 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)
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""Runs every taicpp test (C++ unit binaries and python differential drivers) and prints one
summary table. Exit 1 if any step fails; nothing is skipped silently: a step whose prerequisite is
missing (binary not built, tmux absent) is reported as FAIL or SKIP with the reason.
usage (from taicpp/, after `make test-build`; `make test` does both):
python3 tests/run_all.py [--only SUBSTR ...] [--list] [--timeout SECONDS] [-v]
Output of every step goes to build/test-logs/<step>.log. Nothing is written outside build/.
A step is a list of commands that all must exit 0; ("diff", A, B) runs two commands and requires
identical stdout. Counts in the table are numbers the step itself printed (checks, cases,
scenarios, tests); "-" means the step prints no count.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE) # taicpp/
REPO = os.path.dirname(ROOT) # repository root (tai.py lives here)
BUILD = os.path.join(ROOT, "build")
LOGS = os.path.join(BUILD, "test-logs")
TAI = os.path.join(REPO, "tai.py")
PY = sys.executable
STEP_TIMEOUT = 1200 # hard cap per step, seconds
def b(name):
return os.path.join(BUILD, name)
def t(name):
return os.path.join(HERE, name)
# (name, module, [commands], env or None). A command is an argv list or ("diff", argv, argv).
STEPS = [
# foundation
("foundation.unit", "foundation", [[b("foundation_test")]], None),
("foundation.parity", "foundation", [[PY, t("foundation_parity.py")]], None),
("foundation.repl_persist", "foundation", [[PY, t("repl_persist.py")]], None),
("foundation.tmux_ask", "foundation", [[PY, t("tmux_ask.py")]], None),
("store.smoke", "foundation", [[b("store_test")]], None),
("cli.unit", "cli", [[b("cli_test")]], None),
("util_config.unit", "util/config", [[b("util_config_test")]], None),
# agent
("agent.diff", "agent", [[PY, t("agent_diff.py")]], None),
("agent.e2e", "agent", [[PY, t("agent_e2e.py")]], None),
("agent.sched", "agent", [[PY, t("agent_sched.py")]], None),
# shell and util
("shell.unit", "shell", [[b("shell_test")]], None),
("shell.parity", "shell", [[PY, t("shell_parity.py")]], None),
# cap and mcp
("capmcp.unit", "cap/mcp", [[b("capmcp/capmcp_unit")]], None),
("capmcp.wire_diff", "cap/mcp", [[PY, "-I", t("capmcp_diff.py"), b("capmcp/capmcp_probe")]], None),
("capmcp.cli_diff", "cap/mcp", [[PY, "-I", t("capmcp_cli_diff.py"), b("capmcp/capmcp_cli"), "--tai", TAI]], None),
# misc
("misc.unit", "misc", [[b("misc/misc_test")]], None),
("misc.diff", "misc", [[PY, t("misc_diff.py")]], None),
# tui and grr
("tui.spec", "tui", [[b("tui_spec")]], None),
("tui.demo_shot", "tui", [("diff", [PY, TAI, "--demo-shot"], [b("tui_shot")])], None),
# mplex
("mplex.foundation", "mplex", [[b("mplex_foundation_test")]], {"TZ": "UTC"}),
("mplex.core", "mplex", [[PY, "-I", t("mplex_core_gen.py")], [b("mplex_core_test")]], None),
("mplex.model", "mplex", [[PY, "-I", t("mplex_model_gen.py"), b("mplex_model_cases.jsonl"), b("mplex_model_scratch")],
[b("mplex_model_test"), b("mplex_model_cases.jsonl"), b("mplex_model_scratch")]], None),
("mplex.copy", "mplex", [[PY, "-I", t("mplex_copy_gen.py"), REPO, b("mplex_copy_scenarios.txt"), "3600", "20251025"],
[b("mplex_copy_test"), b("mplex_copy_scenarios.txt")]], None),
("mplex.vt", "mplex", [[PY, "-I", t("mplex_vt_oracle.py"), TAI, b("mplex_vt_cases.txt")],
[b("mplex_vt_test"), TAI, b("mplex_vt_cases.txt")]], None),
("mplex.flow", "mplex", [[PY, "-I", t("mplex_flow_gen.py"), REPO, b("mplex_flow_corpus.jsonl")],
[b("mplex_flow_test"), b("mplex_flow_corpus.jsonl")]], None),
("mplex.flow_tmux", "mplex", [("diff", [b("mplex_flow_test"), "--tmux"], [PY, "-I", t("mplex_tmux_oracle.py"), REPO])], None),
("mplex.regress", "mplex", [[b("mplex_regress_test")]], None),
("mplex.spec", "mplex", [[b("mplex_spec_test"), REPO]], None),
("mplex.server", "mplex", [[b("mplex_server_test"), "--tai", TAI]], None),
("mplex.commands", "mplex", [[b("mplex_commands_test"), b("mplex_cli"), REPO]], None),
("mplex.commands_pty", "mplex", [[PY, "-I", t("mplex_commands_pty.py"), b("mplex_cli"), REPO, b("mplex_commands_pty_work")]], None),
("mplex.e2e", "mplex", [[PY, "-I", t("mplex_e2e.py"), REPO, "--cpp", b("mplex_cli")]], None),
]
COUNT_RE = re.compile(r"(\d[\d,]*)\s+(checks?|cases?|tests?|scenarios?|assertions?|passed|ok|probes?|runs?)\b", re.I)
SLASH_RE = re.compile(r"\b(\d+)\s*/\s*(\d+)\b")
def count_of(text):
"""Best effort: the biggest number the step reported next to a unit word. None if none."""
best = None
for m in COUNT_RE.finditer(text):
n = int(m.group(1).replace(",", ""))
if best is None or n > best:
best = n
return best or None
def run_cmd(argv, env, log):
start = time.time()
try:
done = subprocess.run(argv, cwd=ROOT, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL, timeout=STEP_TIMEOUT)
out, code = done.stdout.decode("utf-8", "replace"), done.returncode
except subprocess.TimeoutExpired as exc:
out, code = (exc.stdout or b"").decode("utf-8", "replace") + "\nTIMEOUT after %ds\n" % STEP_TIMEOUT, 124
except OSError as exc:
out, code = "cannot start %s: %s\n" % (argv[0], exc), 127
log.write("$ %s (rc=%d, %.1fs)\n%s\n" % (" ".join(argv), code, time.time() - start, out))
return code, out
def run_step(step):
name, module, cmds, extra = step
env = dict(os.environ)
for key in [k for k in env if k.startswith("TAI_")]:
env.pop(key, None)
env.update(extra or {})
start = time.time()
status, note, text = "PASS", "", ""
with open(os.path.join(LOGS, name + ".log"), "w") as log:
for cmd in cmds:
if isinstance(cmd, tuple):
c1, o1 = run_cmd(cmd[1], env, log)
c2, o2 = run_cmd(cmd[2], env, log)
text += o1 + o2
if c1 or c2:
status, note = "FAIL", "rc %d/%d" % (c1, c2)
elif o1 != o2:
status, note = "FAIL", "outputs differ (%d vs %d bytes)" % (len(o1), len(o2))
log.write("DIFF: outputs differ\n")
else:
text += "%d bytes identical\n" % len(o1)
else:
code, out = run_cmd(cmd, env, log)
text += out
if code:
status, note = "FAIL", "rc %d" % code
if status == "FAIL":
break
if status == "PASS" and re.search(r"^SKIP", text, re.M):
status, note = "SKIP", "reported SKIP (see log)"
if status == "PASS" and re.search(r"\bFAIL\b|\b[1-9]\d* fail(ed|ures?)\b(?!=)", text):
status, note = "FAIL", "exit 0 but output reports failures"
return {"name": name, "module": module, "status": status, "note": note, "secs": time.time() - start,
"count": count_of(text), "tail": text.strip().splitlines()[-12:]}
def snapshot():
"""Files under taicpp/ outside build/ and third_party/, to flag tests that leak outputs."""
seen = set()
for dirpath, dirnames, filenames in os.walk(ROOT):
dirnames[:] = [d for d in dirnames if d not in ("build", "third_party", "__pycache__")]
for name in filenames:
seen.add(os.path.relpath(os.path.join(dirpath, name), ROOT))
return seen
def main():
global STEP_TIMEOUT
ap = argparse.ArgumentParser()
ap.add_argument("--only", action="append", default=[])
ap.add_argument("--list", action="store_true")
ap.add_argument("--timeout", type=int, default=STEP_TIMEOUT)
ap.add_argument("-v", action="store_true")
opts = ap.parse_args()
STEP_TIMEOUT = opts.timeout
steps = [s for s in STEPS if not opts.only or any(o in s[0] for o in opts.only)]
if opts.list:
for s in steps:
print(s[0])
return 0
os.makedirs(LOGS, exist_ok=True)
before = snapshot()
results = []
for s in steps:
print("running %-26s" % s[0], end=" ", flush=True)
r = run_step(s)
print("%s %.1fs" % (r["status"], r["secs"]), flush=True)
if r["status"] != "PASS":
print("\n".join(" | " + line for line in r["tail"]))
results.append(r)
print()
print("%-26s %-12s %-6s %8s %8s %s" % ("step", "module", "status", "count", "secs", "note"))
print("-" * 90)
for r in results:
print("%-26s %-12s %-6s %8s %8.1f %s" % (r["name"], r["module"], r["status"],
"-" if r["count"] is None else r["count"], r["secs"], r["note"]))
print("-" * 90)
npass = sum(r["status"] == "PASS" for r in results)
nfail = sum(r["status"] == "FAIL" for r in results)
nskip = sum(r["status"] == "SKIP" for r in results)
total = sum(r["count"] or 0 for r in results)
leaked = sorted(snapshot() - before)
if leaked:
print("WARNING: tests wrote outside build/: " + ", ".join(leaked[:10]))
print("steps: %d pass: %d fail: %d skip: %d reported counts summed: %d logs: %s" %
(len(results), npass, nfail, nskip, total, os.path.relpath(LOGS, ROOT)))
if nfail:
print("RESULT: FAILED: " + ", ".join(r["name"] for r in results if r["status"] == "FAIL"))
return 1
print("RESULT: ok" + (" (with skips)" if nskip else ""))
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1,65 +1,27 @@
// Unit checks for src/util (text helpers, ansi) and src/config (consts, Config, tag resolution),
// plus the CLI help smoke. This is the surviving part of the old tests/test_main.cpp: its shell
// section is covered by tests/shell_test.cpp, its cli section by tests/cli_test.cpp, and its llm
// section exercised the placeholder llm API that the agent port removed (ChatClient("openai", ...),
// ModelHealth, Opencode, provider_info); the agent tests (agent_diff, agent_e2e) replace it.
// Plain asserts, so NDEBUG must stay off.
#undef NDEBUG
#include "../src/cli/cli.h"
#include "../src/shell/sandbox.h"
#include "../src/shell/session.h"
#include "../src/shell/pty.h"
#include "../src/shell/spinner.h"
#include "../src/shell/livestream.h"
#include "../src/shell/lanes.h"
#include "../src/shell/runlive.h"
#include "../src/shell/targets.h"
#include "../src/shell/glow.h"
#include "../src/llm/llm.h"
#include "../src/util/ansi.h"
#include "../src/util/text.h"
#include "../src/config/config.h"
#include "../src/config/consts.h"
#include <cassert>
#include <filesystem>
#include <cstdio>
using detz::llm::ChatClient;
using detz::llm::ChatRequest;
#include <filesystem>
#include <map>
int main() {
detz::cli::Parser p;
const char* a0 = "detz";
auto c = p.parse(1, (char**)&a0);
assert(c.name == "--help");
assert(c.run() == 0);
// shell (full coverage: tests/shell_test.cpp, differential: tests/shell_parity.py)
assert(!detz::shell::CurlHint("curl http://x").empty());
assert(detz::shell::CurlHint("echo hi").empty());
assert(detz::shell::ShellTargets::Segment({"rm", "a"}).size() == 1);
{ detz::shell::RunLiveOptions o; auto r = detz::shell::RunLive::Run(detz::shell::Command::Shell("echo hi"), o);
assert(r.code == 0 && r.output == "hi\n" && !r.timedOut); }
{ detz::shell::LiveStream ls(4, 40, nullptr, false); ls.Feed("x"); assert(ls.shown() == 0); }
assert(detz::shell::Glow::Ellipsis("short", 60) == "short");
assert(detz::shell::Sandbox::kName == std::string("tai-box"));
// llm mocked transport
auto mock = [](const std::string&, const std::string&, const std::map<std::string,std::string>&) {
return std::string("{\"content\":\"hello\"}");
};
ChatClient cc("openai", "m", mock);
auto cr = cc.chat(ChatRequest{"m", {{"user", "hi"}}, false});
assert(cr.text == "hello");
auto smock = [](const std::string&, const std::string&, const std::map<std::string,std::string>&) {
return std::string("data: {\"content\":\"a\"}\n\ndata: [DONE]\n\n");
};
ChatClient cs("anthropic", "m", smock);
std::vector<std::string> got;
cs.chat_stream(ChatRequest{"m", {}, true}, [&](const std::string& d) { got.push_back(d); return true; });
assert(got.size() == 1);
bool threw = false;
try { ChatClient bad("openai", "m"); bad.chat(ChatRequest{}); } catch (const detz::llm::BackendError&) { threw = true; }
assert(threw);
detz::llm::ModelHealth h; h.record("a", false, 5); h.record("b", true, 9);
assert(h.size() == 2 && h.best()->model == "b");
assert(detz::llm::provider_names().find("openrouter") != std::string::npos);
assert(detz::llm::provider_info("xai").base_url.find("x.ai") != std::string::npos);
detz::llm::Opencode oc("http://localhost:9", mock);
assert(oc.complete("m", {}).text == "hello");
bool be = false;
try { detz::llm::Opencode no; no.complete("m", {}); } catch (const detz::llm::BackendError&) { be = true; }
assert(be);
// M2 util+config
// util + config
assert(detz::Consts::kTotalStepCap == 500 && detz::Consts::kAuditMaxChars == 20000);
assert(std::string(detz::kDefaultModel) == "muse");
assert(!detz::ZenFreeModels().empty() && !detz::PollinationsModels().empty());
@@ -90,7 +52,6 @@ int main() {
auto ru = detz::ResolveTag("zzz-unknown-xyz", {}, ids, "/tmp"); assert(ru.first == "unknown");
std::map<std::string,std::string> bots = {{"bob", "BotBob"}};
auto rb = detz::ResolveTag("bob", bots, ids, "/tmp"); assert(rb.first == "bot"); }
std::puts("M2 OK");
std::puts("M4+M5 OK");
std::puts("util+config OK");
return 0;
}