summaryrefslogtreecommitdiff
path: root/spec/test_models.lua
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-16 20:42:43 -0600
committert <t@tjp.lol>2026-08-17 20:31:29 -0600
commit4f0a91ef55fe96835172bdad34feec1e2a0a0977 (patch)
treeed4f3e86575aa6243043bc22f8037be153a609c6 /spec/test_models.lua
parentc1ab34754d3f3695fafd344fe1a181ecf0740761 (diff)
subagents extension on the generic host surfaces
The rock now owns all subagent policy on top of libpanto-lua's generic APIs: children are ordinary panto.agent instances over rock-constructed stores, started with agent:run_async and awaited by arming uv.new_poll on each job's wake_fd from the tool handler's coroutine. subagents/jobs.lua carries the session policy the host used to own: the concurrency gate (4 running, FIFO queue, cancel-while-queued never starts), the await contract (results in input order; "first" returns settled plus remaining by identity), and settle-time shaping. subagents/spawn.lua seeds new children (primary system context, child role, profile body with manifest metadata), resolves model/reasoning through panto.ext.resolve_model, filters subagents.* out of the inherited tool set via agent:set_tools, and reads resume defaults back from stored message metadata. One-shot structured workers are a null_store agent with a declaration-only output tool, tool_choice forced, dispatch_tools=false. subagents/progress.lua renders per-tool-entry cards through the component handle's invalidate seam; turn_interrupt cancels live children, turn_end closes them. Spec suite rewritten against fakes of the new surfaces (98 cases), including gate/queue/cancel bounds, resume-default extraction, one-shot capture via unresolved tool calls, tool filtering, and manifest seeding.
Diffstat (limited to 'spec/test_models.lua')
-rw-r--r--spec/test_models.lua158
1 files changed, 158 insertions, 0 deletions
diff --git a/spec/test_models.lua b/spec/test_models.lua
new file mode 100644
index 0000000..ae01396
--- /dev/null
+++ b/spec/test_models.lua
@@ -0,0 +1,158 @@
+-- subagents/models.lua: the four catalog query forms and the profile join.
+--
+-- The fake host answers by query shape, so each case checks both what the tool
+-- asked the host for (`handle.models_queries`) and how it rendered the answer.
+
+local fake = require("spec.fake_ext")
+local models = require("subagents.models")
+
+local function has(text, needle)
+ assert(type(text) == "string", "expected a string result, got " .. type(text))
+ assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text))
+end
+
+local function catalog(query)
+ if query.model then
+ if query.model == "anthropic:sonnet" then
+ return {
+ found = true,
+ ref = "anthropic:sonnet",
+ wire_model = "claude-sonnet-4-6",
+ reasoning_default = "medium",
+ reasoning_levels = { "low", "medium", "high" },
+ context_window = 200000,
+ max_tokens = 64000,
+ }
+ end
+ return { found = false }
+ end
+ if query.provider or query.query then
+ return {
+ matches = {
+ { ref = "anthropic:sonnet", wire_model = "claude-sonnet-4-6", reasoning_default = "medium" },
+ { ref = "anthropic:opus", wire_model = "claude-opus-4-6" },
+ },
+ truncated = true,
+ }
+ end
+ return {
+ model = "anthropic:sonnet",
+ reasoning = "medium",
+ providers = { { name = "anthropic", style = "messages", models = 7 } },
+ }
+end
+
+local function profile_set()
+ local reviewer = {
+ name = "reviewer",
+ description = "Reviews changes",
+ model = "anthropic:sonnet",
+ reasoning = "high",
+ body = "b",
+ }
+ local scout = { name = "scout", description = "", body = "b" }
+ return {
+ list = { reviewer, scout },
+ by_name = { reviewer = reviewer, scout = scout },
+ warnings = {},
+ }
+end
+
+local function with_host(fn)
+ local handle = fake.install({ models_response = catalog })
+ local ok, err = pcall(fn, handle, profile_set())
+ handle.restore()
+ if not ok then
+ error(err, 0)
+ end
+end
+
+return {
+ { "no arguments reports the inherited model and provider counts", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({}, profiles)
+ has(text, "inherited model: anthropic:sonnet")
+ has(text, "inherited reasoning: medium")
+ has(text, "anthropic (messages, 7 models)")
+ assert(next(handle.models_queries[1]) == nil, "the overview query carries no fields")
+ end)
+ end },
+
+ { "an exact model lookup reports its reasoning levels", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({ model = "anthropic:sonnet" }, profiles)
+ has(text, "model: anthropic:sonnet")
+ has(text, "wire model: claude-sonnet-4-6")
+ has(text, "default reasoning: medium")
+ has(text, "reasoning levels: low, medium, high")
+ has(text, "context window: 200000")
+ assert(handle.models_queries[1].model == "anthropic:sonnet")
+ end)
+ end },
+
+ { "an unknown model says so and suggests a search", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({ model = "acme:turbo" }, profiles)
+ has(text, "No configured model matches 'acme:turbo'")
+ has(text, "subagents.models")
+ end)
+ end },
+
+ { "a search reports matches and says when more exist", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({ query = "son" }, profiles)
+ has(text, "2 match(es):")
+ has(text, "anthropic:sonnet — wire claude-sonnet-4-6, default reasoning medium")
+ has(text, "more exist — refine the query")
+ assert(handle.models_queries[1].limit == 10, "the default limit is 10")
+ end)
+ end },
+
+ { "limit is clamped to 1..50", function()
+ with_host(function(handle, profiles)
+ models.handle({ query = "son", limit = 500 }, profiles)
+ assert(handle.models_queries[1].limit == 50, tostring(handle.models_queries[1].limit))
+ models.handle({ provider = "anthropic", limit = 0 }, profiles)
+ assert(handle.models_queries[2].limit == 1, tostring(handle.models_queries[2].limit))
+ models.handle({ query = "son", limit = 7.6 }, profiles)
+ assert(handle.models_queries[3].limit == 7, "a fractional limit is floored")
+ end)
+ end },
+
+ { "an agent with a model reports that model", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({ agent = "reviewer" }, profiles)
+ has(text, "agent: reviewer")
+ has(text, "description: Reviews changes")
+ has(text, "wire model: claude-sonnet-4-6")
+ has(text, "profile reasoning: high")
+ assert(handle.models_queries[1].model == "anthropic:sonnet", "the profile model is looked up")
+ end)
+ end },
+
+ { "an agent without a model says it inherits", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({ agent = "scout" }, profiles)
+ has(text, "agent: scout")
+ has(text, "model: inherits the primary model")
+ has(text, "inherited model: anthropic:sonnet")
+ assert(next(handle.models_queries[1]) == nil, "the inherited case asks for the overview")
+ end)
+ end },
+
+ { "an unknown agent names the known profiles", function()
+ with_host(function(handle, profiles)
+ local text = models.handle({ agent = "ghost" }, profiles)
+ has(text, "Error: unknown agent 'ghost'")
+ has(text, "reviewer, scout")
+ end)
+ end },
+
+ { "a non-string field is refused", function()
+ with_host(function(handle, profiles)
+ has(models.handle({ query = 12 }, profiles), "Error: `query` must be a non-empty string")
+ has(models.handle({ model = "" }, profiles), "Error: `model` must be a non-empty string")
+ assert(#handle.models_queries == 0, "a refused call never reaches the host")
+ end)
+ end },
+}