From e7f0227bcb48fc43bbb8141c148cd4b99348fd62 Mon Sep 17 00:00:00 2001 From: retoor Date: Sun, 25 Oct 2026 07:12:42 +0100 Subject: [PATCH] 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 --- taicpp/CHANGELOG.md | 9 + taicpp/Makefile | 138 ++++++++++-- taicpp/src/agent/agent.mk | 1 - taicpp/src/cap/cap.mk | 1 - taicpp/src/comms/comms.mk | 1 - taicpp/src/llm/llm.mk | 1 - taicpp/src/mcp/mcp.mk | 1 - taicpp/src/mplex/mplex.mk | 25 --- taicpp/src/plexer/plexer.mk | 1 - taicpp/src/shell/shell.mk | 1 - taicpp/src/tui/tui.mk | 2 - taicpp/src/util/util.mk | 2 - taicpp/tests/foundation.mk | 36 --- taicpp/tests/misc.mk | 34 --- taicpp/tests/run_all.py | 209 ++++++++++++++++++ .../{test_main.cpp => util_config_test.cpp} | 63 +----- 16 files changed, 346 insertions(+), 179 deletions(-) create mode 100644 taicpp/CHANGELOG.md delete mode 100644 taicpp/src/agent/agent.mk delete mode 100644 taicpp/src/cap/cap.mk delete mode 100644 taicpp/src/comms/comms.mk delete mode 100644 taicpp/src/llm/llm.mk delete mode 100644 taicpp/src/mcp/mcp.mk delete mode 100644 taicpp/src/mplex/mplex.mk delete mode 100644 taicpp/src/plexer/plexer.mk delete mode 100644 taicpp/src/shell/shell.mk delete mode 100644 taicpp/src/tui/tui.mk delete mode 100644 taicpp/src/util/util.mk delete mode 100644 taicpp/tests/foundation.mk delete mode 100644 taicpp/tests/misc.mk create mode 100644 taicpp/tests/run_all.py rename taicpp/tests/{test_main.cpp => util_config_test.cpp} (50%) diff --git a/taicpp/CHANGELOG.md b/taicpp/CHANGELOG.md new file mode 100644 index 0000000..f9b6b8d --- /dev/null +++ b/taicpp/CHANGELOG.md @@ -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. diff --git a/taicpp/Makefile b/taicpp/Makefile index 00e3009..109d591 100644 --- a/taicpp/Makefile +++ b/taicpp/Makefile @@ -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//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/.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) diff --git a/taicpp/src/agent/agent.mk b/taicpp/src/agent/agent.mk deleted file mode 100644 index 0fa9b2a..0000000 --- a/taicpp/src/agent/agent.mk +++ /dev/null @@ -1 +0,0 @@ -SRCS += src/agent/agent.cpp diff --git a/taicpp/src/cap/cap.mk b/taicpp/src/cap/cap.mk deleted file mode 100644 index be07c86..0000000 --- a/taicpp/src/cap/cap.mk +++ /dev/null @@ -1 +0,0 @@ -SRCS += src/cap/cap.cpp diff --git a/taicpp/src/comms/comms.mk b/taicpp/src/comms/comms.mk deleted file mode 100644 index 0e82ebe..0000000 --- a/taicpp/src/comms/comms.mk +++ /dev/null @@ -1 +0,0 @@ -SRCS += src/comms/comms.cpp diff --git a/taicpp/src/llm/llm.mk b/taicpp/src/llm/llm.mk deleted file mode 100644 index 8c2c98c..0000000 --- a/taicpp/src/llm/llm.mk +++ /dev/null @@ -1 +0,0 @@ -LLM_SRCS = src/llm/llm.cpp diff --git a/taicpp/src/mcp/mcp.mk b/taicpp/src/mcp/mcp.mk deleted file mode 100644 index 59fa9be..0000000 --- a/taicpp/src/mcp/mcp.mk +++ /dev/null @@ -1 +0,0 @@ -SRCS += src/mcp/mcp.cpp diff --git a/taicpp/src/mplex/mplex.mk b/taicpp/src/mplex/mplex.mk deleted file mode 100644 index 0114b95..0000000 --- a/taicpp/src/mplex/mplex.mk +++ /dev/null @@ -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 diff --git a/taicpp/src/plexer/plexer.mk b/taicpp/src/plexer/plexer.mk deleted file mode 100644 index 343515d..0000000 --- a/taicpp/src/plexer/plexer.mk +++ /dev/null @@ -1 +0,0 @@ -SRCS += src/plexer/plexer.cpp diff --git a/taicpp/src/shell/shell.mk b/taicpp/src/shell/shell.mk deleted file mode 100644 index d1a908e..0000000 --- a/taicpp/src/shell/shell.mk +++ /dev/null @@ -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 diff --git a/taicpp/src/tui/tui.mk b/taicpp/src/tui/tui.mk deleted file mode 100644 index bd4aab5..0000000 --- a/taicpp/src/tui/tui.mk +++ /dev/null @@ -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). diff --git a/taicpp/src/util/util.mk b/taicpp/src/util/util.mk deleted file mode 100644 index 91d7c13..0000000 --- a/taicpp/src/util/util.mk +++ /dev/null @@ -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 diff --git a/taicpp/tests/foundation.mk b/taicpp/tests/foundation.mk deleted file mode 100644 index e1f6b40..0000000 --- a/taicpp/tests/foundation.mk +++ /dev/null @@ -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 diff --git a/taicpp/tests/misc.mk b/taicpp/tests/misc.mk deleted file mode 100644 index e761ef3..0000000 --- a/taicpp/tests/misc.mk +++ /dev/null @@ -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) diff --git a/taicpp/tests/run_all.py b/taicpp/tests/run_all.py new file mode 100644 index 0000000..3aa3a63 --- /dev/null +++ b/taicpp/tests/run_all.py @@ -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/.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()) diff --git a/taicpp/tests/test_main.cpp b/taicpp/tests/util_config_test.cpp similarity index 50% rename from taicpp/tests/test_main.cpp rename to taicpp/tests/util_config_test.cpp index 19288a2..b3e2635 100644 --- a/taicpp/tests/test_main.cpp +++ b/taicpp/tests/util_config_test.cpp @@ -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 -#include #include -using detz::llm::ChatClient; -using detz::llm::ChatRequest; +#include +#include + 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&) { - 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&) { - return std::string("data: {\"content\":\"a\"}\n\ndata: [DONE]\n\n"); - }; - ChatClient cs("anthropic", "m", smock); - std::vector 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 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; }