-- Self-check for panto-warming. Run with: -- -- panto lua _selfcheck.lua -- -- Every module under test is pure Lua (no session, no panto module needed), -- so plain lua5.4 runs it too. The fidelity probe at the bottom uses the -- host's real JSON codec when the interpreter provides one. local here = (arg and arg[0] or "_selfcheck.lua"):match("^(.*)/") or "." package.path = here .. "/?.lua;" .. here .. "/?/init.lua;" .. package.path local config = require("warming.config") local policy = require("warming.policy") local SEC, MIN, HOUR = 1000, 60000, 3600000 -- --------------------------------------------------------------------------- -- Duration strings -- --------------------------------------------------------------------------- assert(config.parse_duration("4 minutes") == 4 * MIN, "4 minutes") assert(config.parse_duration("30 seconds") == 30 * SEC, "30 seconds") assert(config.parse_duration("1 hour") == HOUR, "1 hour") assert(config.parse_duration("2h") == 2 * HOUR, "2h") assert(config.parse_duration(" 10 mins ") == 10 * MIN, "whitespace and synonyms") local function duration_fails(text, why) local ms, err = config.parse_duration(text) assert(ms == nil, "parse_duration accepted " .. why) assert(type(err) == "string", "parse_duration explains " .. why) end duration_fails("soon", "a non-duration") duration_fails("4", "a bare number") duration_fails("0 minutes", "a zero duration") duration_fails("-1 minutes", "a negative duration") duration_fails("1 fortnight", "an unknown unit") duration_fails(4, "a non-string") -- --------------------------------------------------------------------------- -- Layer merging -- --------------------------------------------------------------------------- local s = config.defaults() assert(s.enabled == false, "warming is off unless configured") assert(s.interval_ms == 4 * MIN and s.duration_ms == 30 * MIN, "opencode defaults") assert(s.prompt:find("OK"), "default prompt asks for OK") assert(config.apply_layer(s, true, "x") == true, "scalar true applies") assert(s.enabled == true, "scalar true enables") assert(config.apply_layer(s, { interval = "2 minutes" }, "x") == true, "table applies") assert(s.interval_ms == 2 * MIN, "interval overridden") assert(s.duration_ms == 30 * MIN, "omitted keys keep earlier values") assert(config.apply_layer(s, false, "x") == true, "scalar false applies") assert(s.enabled == false, "scalar false disables") assert(config.apply_layer(s, { enabled = false, duration = "10 minutes" }, "x") == true, "enabled key applies") assert(s.enabled == false, "warming.enabled = false disables") assert(s.duration_ms == 10 * MIN, "duration still merged while disabled") assert(config.apply_layer(s, { prompt = "stay put" }, "x") == true, "prompt applies") assert(s.prompt == "stay put" and s.enabled == true, "table presence enables") local function layer_fails(value, why) local ok, err = config.apply_layer(config.defaults(), value, "/x/config.toml") assert(ok == nil, "apply_layer accepted " .. why) assert(type(err) == "string" and err:find("/x/config.toml"), "error names the file for " .. why) end layer_fails("always", "a string scalar") layer_fails({ bogus = 1 }, "an unknown key") layer_fails({ prompt = "" }, "an empty prompt") layer_fails({ interval = "1 fortnight" }, "a bad duration") layer_fails({ enabled = "yes" }, "a non-boolean enabled") -- Layer walk over fake config layers (base < user < project < local). local docs = { ["/base/config.toml"] = { warming = true }, ["/user/config.toml"] = { warming = { interval = "2 minutes" } }, ["/project/config.toml"] = { warming = { duration = "10 minutes" } }, ["/local/config.toml"] = { warming = false }, } local fake_io = { read_file = function(path) return docs[path] and path or nil -- the "file contents" are its path end, parse = function(text) return docs[text] end, } local fake_ext = { dirs = { layers = { { name = "base", dir = "/base" }, { name = "user", dir = "/user" }, { name = "project", dir = "/project" }, { name = "local", dir = "/local" }, { name = "missing", dir = "/nowhere" }, } }, } local merged = assert(config.read(fake_ext, fake_io)) assert(merged.interval_ms == 2 * MIN and merged.duration_ms == 10 * MIN, "layers merge per key") assert(merged.enabled == false, "the highest layer's scalar wins") docs["/local/config.toml"] = { warming = { interval = "90 seconds" } } merged = assert(config.read(fake_ext, fake_io)) assert(merged.enabled == true and merged.interval_ms == 90 * SEC, "later layers enable and override") docs["/local/config.toml"] = { warming = { typo = true } } local bad, err = config.read(fake_ext, fake_io) assert(bad == nil and err:find("/local/config.toml"), "a bad layer fails with its path") docs["/local/config.toml"] = { warming = true } fake_io.parse = function() error("boom") end bad, err = config.read(fake_ext, fake_io) assert(bad == nil and err:find("boom"), "a parse error surfaces its reason") -- --------------------------------------------------------------------------- -- Timing policy: the opencode timelines -- --------------------------------------------------------------------------- local function fire_times(p, from, to, step) local fired = {} local t = from while t <= to do if p:due(t) then fired[#fired + 1] = t / MIN p:attempted(t) end t = t + step end return fired end -- 00:00 real request; warm every 4 minutes; stop at 30 minutes. do local p = policy.new(4 * MIN, 30 * MIN) assert(p:expired(0), "no window before any activity") p:activity(0) assert(not p:due(0), "no warm request at the moment of activity") local fired = fire_times(p, 15 * SEC, 40 * MIN, 15 * SEC) assert(#fired == 7, "seven warm attempts in the default window, got " .. #fired) for i, at in ipairs(fired) do assert(at == i * 4, "warm attempt " .. i .. " at " .. at .. "m, want " .. (i * 4) .. "m") end assert(p:expired(30 * MIN), "window closes at 30m") assert(not p:expired(29 * MIN), "window still open at 29m") end -- 00:00 real request; 00:20 real request restarts both timers; warms at -- 00:24; stops at 00:50. do local p = policy.new(4 * MIN, 30 * MIN) p:turn_started(0) assert(not p:due(4 * MIN), "a turn in flight blocks warming") p:turn_ended(20 * MIN) assert(not p:due(20 * MIN), "interval restarts after the turn") assert(not p:due(23 * MIN), "not due at 23m") assert(p:due(24 * MIN), "due at 24m") p:attempted(24 * MIN) assert(not p:due(27 * MIN + 59 * SEC), "next attempt is a full interval later") assert(p:due(28 * MIN), "due at 28m") assert(not p:expired(49 * MIN), "window runs to 50m") assert(p:expired(50 * MIN), "window closes at 50m") end -- An interval as long as the window: the window always expires first. do local p = policy.new(30 * MIN, 30 * MIN) p:activity(0) local fired = fire_times(p, 0, 60 * MIN, 15 * SEC) assert(#fired == 0, "interval >= duration means no warm request ever") end -- Entry eval contract: loading the extension chunk is side-effect-free and -- returns the one activation entry. local entry = assert(loadfile(here .. "/init.lua"))() assert(type(entry) == "table" and entry.name == "warming" and type(entry.activate) == "function", "init.lua returns the warming entry") -- Activation binds lifecycle timing to session_ready and the banner to the -- render boundary; neither handler is the old session_start event. do local handlers = {} local previous_panto = package.loaded.panto local previous_luv = package.loaded.luv local previous_config = package.loaded["warming.config"] local previous_policy = package.loaded["warming.policy"] local previous_warm = package.loaded["warming.warm"] package.loaded["warming.config"] = { default_io = function() return {} end, read = function() return { enabled = true, interval_ms = MIN, duration_ms = MIN, prompt = "OK" } end, } package.loaded["warming.policy"] = { new = function() return { activity = function(self, now) self.activity_at = now end, turn_started = function() end, turn_ended = function() end, expired = function() return false end, due = function() return false end, attempted = function() end, } end, } package.loaded["warming.warm"] = { start = function() end, poll = function() end, abandon = function() end } package.loaded.panto = { agent = function() end, ext = { on = function(name, handler) handlers[name] = handler end, resolve_model = function() end, style = function() return { dim = "", reset = "" } end, } } package.loaded.luv = { now = function() return 123 end, new_timer = function() return { start = function(self) self.active = true end, is_active = function(self) return self.active end, is_closing = function() return false end, stop = function(self) self.active = false end, close = function() end } end, } local active_entry = assert(loadfile(here .. "/init.lua"))() active_entry.activate() assert(handlers.render_header and handlers.session_ready, "new event subscriptions are installed") assert(handlers.session_start == nil, "old session_start subscription is absent") handlers.session_ready() local inner = { render = function() return { "header" } end } local event = { get_component = function() return inner end } event.set_component = function(_, component) event.component = component end handlers.render_header(event) assert(event.component and event.component:render(80)[1] == "header", "render_header wraps the banner") package.loaded.panto = previous_panto package.loaded.luv = previous_luv package.loaded["warming.config"] = previous_config package.loaded["warming.policy"] = previous_policy package.loaded["warming.warm"] = previous_warm end -- --------------------------------------------------------------------------- -- Tool-schema fidelity probe (report-only) -- -- Warming copies the session's tool declarations to the warm request -- through the platform JSON codec: `agent:tools()` decodes the session's -- schema bytes and `set_tools` re-encodes them. If encode(decode(x)) is not -- byte-identical to x, the warm request's tool prefix diverges from the -- session's own requests and cannot write a cache entry they will hit (see -- the README). This probe measures how often that drift happens with the -- codec in this interpreter. It reports, but does not fail: the drift is a -- property of the codec, not of this rock. -- --------------------------------------------------------------------------- local ok_panto, panto = pcall(require, "panto") local json = ok_panto and type(panto) == "table" and panto.ext and panto.ext.json if json and type(json.encode) == "function" then local schemas = { { type = "object", properties = {}, additionalProperties = false }, { type = "object", properties = { query = { type = "string" } }, required = { "query" } }, { type = "object", title = "run", properties = { command = { type = "string" }, cwd = { type = "string" }, timeout = { type = "integer", minimum = 1 }, env = { type = "array", items = { type = "string" } }, }, required = { "command" }, additionalProperties = false }, { type = "object", properties = { name = { type = "string" }, description = { type = "string" }, schema = { type = "object", additionalProperties = true }, labels = { type = "array", items = { type = "string" } }, nested = { type = "object", properties = { a = { type = "number" }, b = { type = "boolean" }, c = { type = "null" }, } }, } }, } local drift = 0 for _, schema in ipairs(schemas) do local encoded = json.encode(schema) if json.encode(json.decode(encoded)) ~= encoded then drift = drift + 1 end end print(string.format( "probe: schema re-encode drift %d/%d (%s)", drift, #schemas, drift == 0 and "byte-stable" or "tool prefix fidelity degraded; see README")) else print("probe: skipped (no panto.ext.json in this interpreter)") end print("panto-warming selfcheck: OK")