tui: fix nested source abandoning the calling script

Tui::Source kept the script position in single member fields
(loading_lines/idx/...), so a sourced line that was itself `source inner`
overwrote them: when the inner file finished, the outer script's remaining
lines were silently dropped.

The state is now a stack of SourceFrames — the stack is the call chain. A
nested source just pushes a frame (the running driver, ticker thread or
headless drain, picks it up next line) and the caller's frame resumes when
it pops. Each frame still prints its own "source: <file> (N line(s))"
summary; an interactive-line abort clears the whole chain; depth capped at
32 like the core script engine. The Computing modal shows the top frame.

Regression-tested headless via BootDispatch (tests/tui/test_source.cpp):
nested-then-continue, and self-recursion hitting the depth guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 20:32:36 +02:00
parent 0b10e1c1b7
commit dceb61237d
4 changed files with 123 additions and 27 deletions

71
tests/tui/test_source.cpp Normal file
View File

@@ -0,0 +1,71 @@
#include <doctest/doctest.h>
#include "frontends/tui/tui.hpp"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
// Tui::Source nesting — regression for the bug where a nested `source`
// overwrote the single loading state, so the CALLING script's remaining
// lines never ran. Headless path (no screen): BootDispatch drains
// synchronously, exactly like `essim --batch --source`.
namespace {
std::string run_boot(const std::string &cmd) {
Tui t;
t.BootDispatch(cmd);
std::ostringstream oss;
t.DumpOutput(oss);
return oss.str();
}
} // namespace
TEST_CASE("source: lines after a nested source still run") {
const char *inner = "test_src_inner.essim";
const char *outer = "test_src_outer.essim";
{
std::ofstream f(inner);
f << "new\n";
}
{
std::ofstream f(outer);
f << "source " << inner << "\n"
"verify\n";
}
std::string out = run_boot(std::string("source ") + outer);
// The inner script ran and was summarised…
CHECK(out.find("system created.") != std::string::npos);
CHECK(out.find(std::string("source: ") + inner) != std::string::npos);
// …and the OUTER script kept going after it: verify executed…
CHECK(out.find("verify: 0 local mismatch(es)") != std::string::npos);
// …and the outer summary counts its 2 effective lines.
CHECK(out.find(std::string("source: ") + outer + " (2 line(s))")
!= std::string::npos);
std::remove(inner);
std::remove(outer);
}
TEST_CASE("source: self-recursion stops at the depth guard") {
const char *loop = "test_src_loop.essim";
{
std::ofstream f(loop);
f << "source " << loop << "\n";
}
std::string out = run_boot(std::string("source ") + loop);
CHECK(out.find("source: nesting too deep, skipping")
!= std::string::npos);
// Every frame still closes with its own summary.
CHECK(out.find(std::string("source: ") + loop + " (1 line(s))")
!= std::string::npos);
std::remove(loop);
}