2026-08-22 09:36:15 +03:00
/**
* Job infrastructure tests — deterministic, zero network:
*
* 1. job store CRUD (create → get → update → list → dedup by toolCallId)
2026-09-01 19:15:43 +03:00
* 2. steer helpers (pure, contracts.ts): buildSubagentSteer shape per kind,
* isSubagentFailure, steerDelivered
* 3. dispatch (async protocol §2): drive handleChatRequest with a scripted
* mock teacher that emits a research tool call → the tool part resolves
* IMMEDIATELY with the fire-and-forget ack (the teacher's turn
* CONTINUES — no turn break at the call), a job record exists, and the
* test runner (fake deliver) produces the steer
* 4. runner completion: a fake researcher model (web_search call → final
* text brief, globalThis.fetch stubbed) → the job stores the result,
* the steer carries the brief + sources, the fake deliver flips the
* job done
* 5. runner failure: a model that throws → the failure steer still fires
* (the teacher learns the outcome) and the job lands error
* 6. the wake pipeline (src/server/jobs.ts): server-side steer delivery —
* happy path (fires the injected turn runner, flips the job), pending-
* quiz deferral, 409 retry, idempotent redelivery
2026-08-22 09:36:15 +03:00
*
* Cleanup follows tests/chat-flow.test.ts: created chat/job files removed in
* finally blocks / the after() hook.
*/
import assert from "node:assert/strict" ;
import * as fs from "node:fs" ;
import * as path from "node:path" ;
import { after , describe , it } from "node:test" ;
2026-09-12 02:34:18 +03:00
import { createFauxCore , fauxAssistantMessage , fauxText , fauxToolCall , type AssistantMessage , type FauxResponseStep } from "@earendil-works/pi-ai" ;
2026-09-01 19:15:43 +03:00
import {
2026-09-11 03:25:09 +03:00
accumulateJobUsage ,
buildAbortSteer ,
2026-09-01 19:15:43 +03:00
buildSubagentSteer ,
isSubagentFailure ,
steerDelivered ,
type DemoResult ,
2026-09-11 03:25:09 +03:00
type JobUsage ,
2026-09-19 15:24:26 +03:00
type PdfResult ,
2026-09-01 19:15:43 +03:00
type ResearchResult ,
2026-09-19 15:24:26 +03:00
type SubagentArtifactData ,
2026-09-11 03:25:09 +03:00
type SteerMessage ,
2026-09-01 19:15:43 +03:00
type VisualResult ,
2026-09-11 06:44:20 +03:00
buildProbeHandoffSteer ,
2026-09-01 19:15:43 +03:00
} from "../src/core/contracts.ts" ;
2026-09-26 00:37:46 +03:00
import { composeInstructions , createTeacherAgent } from "../src/agent/teacher-agent.ts" ;
2026-09-01 19:15:43 +03:00
import { createTeacherTools } from "../src/agent/tools.ts" ;
2026-09-12 02:34:18 +03:00
import { webTools } from "../src/agent/tools/web.ts" ;
2026-08-22 09:36:15 +03:00
import { handleChatRequest } from "../src/server/chat-handler.ts" ;
2026-09-19 13:16:34 +03:00
import { abortJob , claimSteers , deliverJobResult , recoverStaleJobs , setJobRespawn , setTurnRunner } from "../src/server/jobs.ts" ;
import { registerJobAbort , unregisterJobAbort } from "../src/agent/subagents/aborts.ts" ;
2026-09-11 03:25:09 +03:00
import { beginTurn , endTurn } from "../src/server/turn-liveness.ts" ;
2026-09-26 00:37:46 +03:00
import { persistTurn } from "../src/server/turn-service.ts" ;
2026-08-31 20:00:12 +03:00
import { chatStore } from "../src/store/chats.ts" ;
import { jobStore } from "../src/store/jobs.ts" ;
2026-09-19 15:24:26 +03:00
import { createJobRunner , pdfModelOverride , researchModelOverride } from "../src/agent/subagents/runner.ts" ;
2026-09-24 22:51:12 +03:00
import { providerKeyMissing , type ModelSlotName } from "../src/core/settings.ts" ;
2026-09-12 02:34:18 +03:00
import type { PiEngine } from "../src/agent/models.ts" ;
2026-09-22 13:55:01 +03:00
import type { SearchProvider } from "../src/core/search.ts" ;
2026-08-22 09:36:15 +03:00
import { PATHS } from "../config.ts" ;
2026-09-26 00:37:46 +03:00
import { projectNative } from "../src/core/projection.ts" ;
import type { LhMessage } from "../src/core/native-messages.ts" ;
import type { SteerInput , TurnInput } from "../src/core/input-events.ts" ;
2026-08-22 09:36:15 +03:00
const JOBS_DIR = path . join ( PATHS . data , "jobs" );
const createdChats : string [] = [];
2026-09-24 22:51:12 +03:00
// The key-presence turn gate (provider-key interlude, 2026-09-25): scripted
// turns pin openrouter per chat — the gate reads env-first truth, so the
// key must be present in the test process like any real setup.
process . env . OPENROUTER_API_KEY ??= "test-key-presence-gate" ;
2026-08-22 09:36:15 +03:00
const createdJobs : string [] = [];
function trackChat ( id : string ) {
createdChats . push ( id );
}
2026-09-26 00:37:46 +03:00
/** Wave B native fixture: user text + an assistant tool call + its ack —
* the record is agent history; the runner's transcript scan (input
* resolution) reads it through the projection. */
function nativeToolMessages ( text : string , tool : string , toolCallId : string , input : unknown ) : LhMessage [] {
return [
{ id : "u1" , role : "user" , content : text , timestamp : 1 } as never ,
{
id : "a1" ,
role : "assistant" ,
content : [{ type : "toolCall" , id : toolCallId , name : tool , arguments : input as never }],
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "toolUse" ,
timestamp : 1 ,
} as never ,
{
id : "r1" ,
role : "toolResult" ,
toolCallId ,
toolName : tool ,
content : [{ type : "text" , text : "Dispatched." }],
details : "Dispatched." ,
isError : false ,
timestamp : 1 ,
} as never ,
];
}
2026-08-22 09:36:15 +03:00
function trackJob ( id : string ) {
createdJobs . push ( id );
}
after (() => {
for ( const id of createdChats ) {
try {
fs . unlinkSync ( path . join ( PATHS . chats , ` ${ id } .json` ));
} catch {
/* already gone */
}
}
for ( const id of createdJobs ) {
try {
fs . unlinkSync ( path . join ( JOBS_DIR , ` ${ id } .json` ));
} catch {
/* already gone */
}
}
2026-09-01 19:15:43 +03:00
researchModelOverride . current = undefined ;
2026-08-22 09:36:15 +03:00
});
2026-09-12 02:34:18 +03:00
/** A scripted pi engine (faux provider): the response queue replaces the
* old doStream/doGenerate turn counters. tokenSize is set so every test
* string streams as ONE delta — the SSE-body assertions ("Back with the
* brief.") read contiguous text. */
function fauxEngine ( steps : FauxResponseStep []) : PiEngine {
const faux = createFauxCore ({ tokenSize : { min : 64 , max : 64 } });
faux . setResponses ( steps );
return { model : faux.getModel (), streamFn : faux.streamSimple };
}
/** A scripted response factory whose model call never resolves until its
* abort signal fires (a cancelled provider request); an already-aborted
* signal resolves at once — the faux stream then ends the message aborted. */
function abortableFactory () : FauxResponseStep {
const factory = ( _context : unknown , options : { signal? : AbortSignal } | undefined ) =>
new Promise < AssistantMessage >(( resolve ) => {
const sig = options ? . signal ;
if ( sig ? . aborted ) {
resolve ( fauxAssistantMessage ( "late" ));
return ;
}
sig ? . addEventListener ( "abort" , () => resolve ( fauxAssistantMessage ( "late" )), { once : true });
});
return factory as unknown as FauxResponseStep ;
2026-08-22 09:36:15 +03:00
}
async function drain ( res : Response ) : Promise < string > {
if ( res . status !== 200 ) {
assert . fail ( `expected 200, got ${ res . status } : ${ await res . text () } ` );
}
return res . text ();
}
2026-09-26 00:37:46 +03:00
function post ( chatId : string , input : unknown ) : Request {
2026-08-22 09:36:15 +03:00
return new Request ( "http://test/api/chat" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
2026-09-26 00:37:46 +03:00
// Wave B: the input-events contract — one explicit event, never a
// client transcript.
body : JSON.stringify ({ chatId , input }),
2026-08-22 09:36:15 +03:00
});
}
2026-09-01 19:15:43 +03:00
function waitFor ( cond : () => boolean , timeoutMs = 8000 , what = "condition" ) : Promise < void > {
return new Promise (( resolve , reject ) => {
const started = Date . now ();
const tick = () => {
if ( cond ()) return resolve ();
if ( Date . now () - started > timeoutMs ) return reject ( new Error ( `timeout waiting for ${ what } ` ));
setTimeout ( tick , 25 );
};
tick ();
});
}
2026-08-22 09:36:15 +03:00
// ─── 1. job store CRUD ───────────────────────────────────────────────────────
describe ( "job store" , () => {
it ( "create → get → update → list round-trips" , () => {
2026-08-31 20:00:12 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : "chat_storetest" , toolCallId : "call-store" });
2026-08-22 09:36:15 +03:00
trackJob ( job . id );
assert . match ( job . id , /^job_/ );
assert . equal ( job . status , "running" );
assert . equal ( job . activity , "starting" );
2026-08-31 20:00:12 +03:00
const loaded = jobStore . getJob ( job . id ) ! ;
2026-08-22 09:36:15 +03:00
assert . equal ( loaded . toolCallId , "call-store" );
assert . equal ( loaded . kind , "research" );
2026-08-31 20:00:12 +03:00
const updated = jobStore . updateJob ( job . id , { activity : 'web_search: "test"' , status : "done" , result : { brief : "b" } }) ! ;
2026-08-22 09:36:15 +03:00
assert . equal ( updated . activity , 'web_search: "test"' );
assert . equal ( updated . status , "done" );
assert . ok ( updated . updatedAt >= job . updatedAt );
// immutable identity fields survive a patch
assert . equal ( updated . toolCallId , "call-store" );
2026-08-31 20:00:12 +03:00
assert . equal ( jobStore . getJob ( updated . id ) ! . toolCallId , "call-store" );
2026-08-22 09:36:15 +03:00
2026-08-31 20:00:12 +03:00
const listed = jobStore . listJobs ( "chat_storetest" );
2026-08-22 09:36:15 +03:00
assert . ok ( listed . some (( j ) => j . id === job . id ));
2026-08-31 20:00:12 +03:00
assert . equal ( jobStore . listJobs ( "chat_other" ). some (( j ) => j . id === job . id ), false , "jobs are scoped to their chat" );
2026-08-22 09:36:15 +03:00
2026-08-31 20:00:12 +03:00
const byTool = jobStore . findJobByToolCallId ( "chat_storetest" , "call-store" );
2026-08-22 09:36:15 +03:00
assert . equal ( byTool ? . id , job . id );
2026-08-31 20:00:12 +03:00
assert . equal ( jobStore . findJobByToolCallId ( "chat_storetest" , "call-missing" ), null );
2026-08-22 09:36:15 +03:00
});
});
2026-09-01 19:15:43 +03:00
// ─── 2. steer helpers (pure) ─────────────────────────────────────────────────
describe ( "steer helpers (contracts.ts)" , () => {
const research : ResearchResult = { brief : "BRIEF" , sources : [{ title : "s" , url : "https://x.test" }] };
const demo : DemoResult = { title : "T" , html : "<html></html>" , tested : true , notes : "ok" };
const visual : VisualResult = { svg : "<svg/>" , notes : "ok" };
2026-09-26 00:37:46 +03:00
it ( "buildSubagentSteer: a native marker — brief on the model-facing content, artifact payload in details" , () => {
2026-09-01 19:15:43 +03:00
const job = { id : "job_x" , kind : "research" as const , toolCallId : "call-x" };
const steer = buildSubagentSteer ( job , research );
assert . equal ( steer . id , "steer_job_x" , "deterministic id per job — idempotent delivery" );
2026-09-26 00:37:46 +03:00
// Wave B: steers are LhMarkerMessage customs — role "custom", the
// model reads `content`, the UI reads `details` (never model-visible).
assert . equal ( steer . role , "custom" );
assert . equal ( steer . customType , "subagent-steer" );
assert . equal ( steer . display , true );
assert . ok ( steer . content . includes ( "BRIEF" ), "the model-facing text carries the brief" );
const data = steer . details as { jobId : string ; kind : string ; toolCallId : string ; result : ResearchResult };
assert . equal ( data . jobId , "job_x" );
assert . equal ( data . kind , "research" );
assert . equal ( data . toolCallId , "call-x" );
assert . deepEqual ( data . result , research );
2026-09-01 19:15:43 +03:00
});
2026-09-11 03:25:09 +03:00
it ( "accumulateJobUsage: sums per model call; fields NO call reported stay undefined (absent is not zero)" , () => {
let total : JobUsage | undefined = undefined ;
total = accumulateJobUsage ( total , "z-ai/glm-5.3-flash" , {
inputTokens : 10 ,
outputTokens : 5 ,
inputTokenDetails : { cacheReadTokens : 4 },
});
assert . deepEqual ( total , {
model : "z-ai/glm-5.3-flash" ,
inputTokens : 10 ,
outputTokens : 5 ,
cacheReadTokens : 4 ,
cacheWriteTokens : undefined ,
reasoningTokens : undefined ,
steps : 1 ,
lastStep : {
inputTokens : 10 ,
outputTokens : 5 ,
cacheReadTokens : 4 ,
cacheWriteTokens : undefined ,
reasoningTokens : undefined ,
},
}, "lastStep is the most recent call's accounting, not cumulative" );
// a later call that reports nothing new keeps reported fields summing
// (unreported contributes 0) and bumps the billing step count.
total = accumulateJobUsage ( total , "z-ai/glm-5.3-flash" , { inputTokens : 20 , outputTokens : 0 });
assert . equal ( total . inputTokens , 30 );
assert . equal ( total . outputTokens , 5 );
assert . equal ( total . cacheReadTokens , 4 , "unreported after reported contributes 0" );
assert . equal ( total . cacheWriteTokens , undefined , "never reported stays absent — never zero" );
assert . equal ( total . reasoningTokens , undefined );
assert . equal ( total . steps , 2 , "one step per model call, reported or not" );
assert . deepEqual ( total . lastStep , {
inputTokens : 20 ,
outputTokens : 0 ,
cacheReadTokens : undefined ,
cacheWriteTokens : undefined ,
reasoningTokens : undefined ,
}, "lastStep always mirrors the LATEST call, never the sums" );
});
2026-09-26 00:37:46 +03:00
it ( "buildAbortSteer: a subagent-abort marker — teacher-facing text on content, the abort payload in details" , () => {
2026-09-11 03:25:09 +03:00
const steer = buildAbortSteer ({ id : "job_c" , kind : "demo" , toolCallId : "call-c" });
assert . equal ( steer . id , "steer_job_c" , "same id space — steerDelivered treats either as delivered" );
2026-09-26 00:37:46 +03:00
assert . equal ( steer . role , "custom" );
assert . equal ( steer . customType , "subagent-abort" );
assert . ok ( /aborted/ . test ( steer . content ), "the teacher-facing text names the abort" );
// The marker's customType is the message type: the UI renders the
// abort badge off the projection (demos.test.tsx pins the card).
assert . deepEqual ( steer . details , { jobId : "job_c" , kind : "demo" , toolCallId : "call-c" , aborted : true });
2026-09-11 03:25:09 +03:00
});
2026-09-01 19:15:43 +03:00
it ( "isSubagentFailure: failure shapes only (empty html/svg, failure brief)" , () => {
assert . equal ( isSubagentFailure ( "research" , research ), false );
assert . equal ( isSubagentFailure ( "research" , { brief : "Research failed: x" , sources : [] }), true );
// a brief with sources is a success even if the text mentions failure
assert . equal ( isSubagentFailure ( "research" , { brief : "Research failed: x" , sources : [{ title : "s" , url : "https://x" }] }), false );
assert . equal ( isSubagentFailure ( "demo" , demo ), false );
assert . equal ( isSubagentFailure ( "demo" , { ... demo , html : "" }), true );
assert . equal ( isSubagentFailure ( "visual" , visual ), false );
assert . equal ( isSubagentFailure ( "visual" , { ... visual , svg : "" }), true );
});
2026-09-11 06:44:20 +03:00
it ( "steerDelivered: matches on the deterministic steer id (any steer kind)" , () => {
2026-09-01 19:15:43 +03:00
const steer = buildSubagentSteer ({ id : "job_y" , kind : "demo" , toolCallId : "c" }, demo );
2026-09-11 06:44:20 +03:00
assert . equal ( steer . id , "steer_job_y" );
assert . equal ( steerDelivered ([], steer . id ), false );
assert . equal ( steerDelivered ([ steer ], steer . id ), true );
assert . equal ( steerDelivered ([ steer ], "steer_job_z" ), false );
// the probe handoff shares the id-space contract
const handoff = buildProbeHandoffSteer ( "chat_q" );
assert . equal ( handoff . id , "steer_probe_chat_q" );
assert . equal ( steerDelivered ([ handoff ], handoff . id ), true );
assert . equal ( steerDelivered ([ steer ], handoff . id ), false );
2026-09-01 19:15:43 +03:00
});
it ( "failure steers tell the teacher how to recover (research/demo/visual wording)" , () => {
const failResearch = buildSubagentSteer ({ id : "j1" , kind : "research" , toolCallId : "c" }, { brief : "Research failed: API down" , sources : [] });
const failDemo = buildSubagentSteer ({ id : "j2" , kind : "demo" , toolCallId : "c" }, { title : "T" , html : "" , tested : true , notes : "FAILED: boom" });
const failVisual = buildSubagentSteer ({ id : "j3" , kind : "visual" , toolCallId : "c" }, { svg : "" , notes : "FAILED: boom" });
for ( const steer of [ failResearch , failDemo , failVisual ]) {
2026-09-26 00:37:46 +03:00
assert . ok ( ! steer . content . includes ( "rendered in the lesson" ), "failure text never claims the artifact landed" );
2026-09-01 19:15:43 +03:00
}
2026-09-26 00:37:46 +03:00
assert . ok ( failResearch . content . includes ( "flag the uncertainty" ));
2026-09-01 19:15:43 +03:00
});
2026-09-19 15:24:26 +03:00
it ( "pdf steers (sources v0): brief + page citations on success, PDF failed: shape on failure, abort wording per kind" , () => {
2026-09-19 16:52:09 +03:00
const pdf : PdfResult = { path : "/tmp/doc.pdf" , question : "q?" , brief : "The formula is on p. 2." , pagesRead : [ 2 ], pagesConsulted : [ 1 , 2 ] };
2026-09-19 15:24:26 +03:00
const ok = buildSubagentSteer ({ id : "jp" , kind : "pdf" , toolCallId : "cp" }, pdf );
2026-09-26 00:37:46 +03:00
assert . ok ( ok . content . includes ( "page citations" ), "the teacher is told to cite pages" );
assert . ok ( ok . content . includes ( "The formula is on p. 2." ));
assert . equal (( ok . details as { kind : string }). kind , "pdf" );
assert . deepEqual (( ok . details as { result : unknown }). result , pdf );
2026-09-19 15:24:26 +03:00
// failure shape mirrors research
assert . equal ( isSubagentFailure ( "pdf" , pdf ), false );
assert . equal ( isSubagentFailure ( "pdf" , { ... pdf , brief : "PDF failed: unreadable" }), true );
const fail = buildSubagentSteer ({ id : "jf" , kind : "pdf" , toolCallId : "cf" }, { ... pdf , brief : "PDF failed: unreadable" });
2026-09-26 00:37:46 +03:00
assert . ok ( fail . content . includes ( "flag the gap" ));
2026-09-19 15:24:26 +03:00
// abort wording names the PDF job
const aborted = buildAbortSteer ({ id : "ja" , kind : "pdf" , toolCallId : "ca" });
2026-09-26 00:37:46 +03:00
assert . ok ( aborted . content . includes ( "PDF reading job" ));
2026-09-19 15:24:26 +03:00
});
2026-09-01 19:15:43 +03:00
});
// ─── fake deliver (the wake pipeline's stand-in for runner tests) ───────────
/**
* A fake deliver seam: capture the steer and mimic the pipeline's final
* status flip (done/error by isSubagentFailure) so the job record reads
* exactly as it will in production after the wake turn completes.
*/
function fakeDeliver() {
2026-09-26 00:37:46 +03:00
const steers : SteerMessage [] = [];
2026-09-01 19:15:43 +03:00
return {
steers ,
2026-09-26 00:37:46 +03:00
deliver : async ( steer : SteerMessage ) => {
2026-09-01 19:15:43 +03:00
steers . push ( steer );
2026-09-26 00:37:46 +03:00
const jobId = ( steer . details as { jobId : string }). jobId ;
2026-09-01 19:15:43 +03:00
const job = jobStore . getJob ( jobId );
if ( job && job . status === "running" ) {
const failed = job . result ? isSubagentFailure ( job . kind , job . result as never ) : true ;
jobStore . updateJob ( jobId , { status : failed ? "error" : "done" , activity : failed ? "failed" : "delivered" });
}
},
};
}
// ─── 3. dispatch: the tool call acks mid-turn and spawns ─────────────────────
2026-08-22 09:36:15 +03:00
const RESEARCH_INPUT = {
question : "What is the latest stable sigma.js version?" ,
context : "Choosing a graph rendering library for the lesson." ,
};
2026-09-01 19:15:43 +03:00
/** A teacher that calls research (text → call), then plain text thereafter. */
2026-09-12 02:34:18 +03:00
function researchCallingTeacher () : PiEngine {
return fauxEngine ([
fauxAssistantMessage (
[ fauxText ( "Let me check the current state of the library." ), fauxToolCall ( "research" , RESEARCH_INPUT , { id : "call-r1" })],
{ stopReason : "toolUse" },
),
fauxAssistantMessage ([ fauxText ( "Back with the brief." )]),
]);
2026-08-22 09:36:15 +03:00
}
2026-09-01 19:15:43 +03:00
describe ( "dispatch (async protocol §2)" , () => {
it ( "the research call resolves immediately with an ack; the turn continues and the runner steers" , async () => {
const chat = chatStore . createChat ( "Test: research dispatch" );
2026-08-22 09:36:15 +03:00
trackChat ( chat . id );
2026-09-22 16:58:25 +03:00
// BYOK turn gate (owner, 2026-09-22): the scripted chat needs a
// conversation model — the per-chat pin, exactly like the setup flow.
chatStore . saveSettings ( chat . id , { model : { provider : "openrouter" , id : "z-ai/glm-5.3-flash" } });
2026-08-22 09:36:15 +03:00
2026-09-01 19:15:43 +03:00
const delivery = fakeDeliver ();
2026-09-22 13:55:01 +03:00
const search = stubSearch ();
const testRunner = createJobRunner ({ chatStore , jobStore , deliver : delivery.deliver , searchProvider : () => search });
2026-09-12 02:34:18 +03:00
const engine = researchCallingTeacher ();
const tools = createTeacherTools ({
chatId : chat.id ,
chatStore ,
jobStore ,
spawn : ( job , input ) => void testRunner . runJob ( job . id , input ),
});
2026-09-26 00:37:46 +03:00
// The handler hands the factory the native build options — the
// record IS the agent state (Wave B: no adapter conversion).
const agent : Parameters < typeof handleChatRequest >[ 1 ] = ( opts ) =>
createTeacherAgent ( undefined , { engine , tools , messages : opts.messages });
2026-08-22 09:36:15 +03:00
2026-09-01 19:15:43 +03:00
researchModelOverride . current = researcherModel ();
2026-09-26 00:37:46 +03:00
const body = await drain (
await handleChatRequest ( post ( chat . id , { type : "text" , text : "teach me graph rendering" }), agent ),
);
2026-08-22 09:36:15 +03:00
2026-09-22 13:55:01 +03:00
// The teacher's SAME turn carried on past the tool call.
assert . ok ( body . includes ( "Back with the brief." ), "the turn continued after the dispatch" );
2026-08-22 09:36:15 +03:00
2026-09-26 00:37:46 +03:00
const saved = projectNative ( chatStore . getChat ( chat . id ) ! . messages );
const part = saved [ 1 ] ! . parts . find (( p ) => p . type === "tool-research" );
2026-09-22 13:55:01 +03:00
assert . ok ( part && part . type === "tool-research" , "research tool part persisted" );
assert . equal ( part . state , "output-available" , "the part resolved at dispatch time (not held pending)" );
assert . equal ( typeof ( part as { output? : unknown }). output , "string" , "the output is the ack, not the result" );
assert . match ( String (( part as { output? : unknown }). output ), /fire-and-forget/ );
2026-09-01 19:15:43 +03:00
2026-09-22 13:55:01 +03:00
// The job record exists and the background runner completed it.
const job = jobStore . findJobByToolCallId ( chat . id , "call-r1" );
assert . ok ( job , "dispatch created a job record" );
trackJob ( job ! . id );
await waitFor (() => jobStore . getJob ( job ! . id ) ! . status !== "running" , 8000 , "research job completion" );
2026-09-01 19:15:43 +03:00
2026-09-22 13:55:01 +03:00
const done = jobStore . getJob ( job ! . id ) ! ;
assert . equal ( done . status , "done" );
assert . equal ( done . activity , "delivered" );
2026-09-01 19:15:43 +03:00
2026-09-26 00:37:46 +03:00
// The steer carries the brief on its model-facing content; the
// artifact payload rides details for the (UI) renderer.
2026-09-22 13:55:01 +03:00
assert . equal ( delivery . steers . length , 1 , "exactly one steer — one job, one result" );
const steer = delivery . steers [ 0 ] ! ;
2026-09-26 00:37:46 +03:00
assert . equal (( steer . details as { jobId : string }). jobId , job ! . id );
assert . ok ( steer . content . includes ( "sigma.js v3 is current" ), "steer text carries the brief" );
2026-09-22 13:55:01 +03:00
// Search threading (2026-09-20): the researcher's web_search ran on
// the INJECTED port — the stub saw the scripted query, no network.
assert . deepEqual ( search . queries , [ "sigma.js latest version (5)" ]);
researchModelOverride . current = undefined ;
2026-08-22 09:36:15 +03:00
});
});
// ─── the researcher loop with a fake model + stubbed fetch ──────────────────
const BRIEF = `## Summary
sigma.js v3 is current.
## Findings
1. **v3 is the latest major.** [sigma.js docs](https://www.sigmajs.org/docs)
## Sources
- sigma.js docs (https://www.sigmajs.org/docs)` ;
2026-09-12 02:34:18 +03:00
/** The researcher loop's scripted engine. Turn 1: a web_search tool call;
* turn 2: the final text brief. */
function researcherModel () : PiEngine {
return fauxEngine ([
fauxAssistantMessage ([ fauxToolCall ( "web_search" , { query : "sigma.js latest version" }, { id : "ws1" })], {
stopReason : "toolUse" ,
}),
fauxAssistantMessage ([ fauxText ( BRIEF )]),
]);
2026-08-22 09:36:15 +03:00
}
2026-09-22 13:55:01 +03:00
/** A recording stub provider (search port wave, 2026-09-20): the runner's
* injected search seam — no network, queries captured for assertions. */
function stubSearch () : SearchProvider & { queries : string [] } {
const queries : string [] = [];
return {
id : "tavily" ,
async search ( query : string , count : number ) {
queries . push ( ` ${ query } ( ${ count } )` );
return [{ title : "sigma.js" , url : "https://www.sigmajs.org/" , snippet : "v3 released" }];
},
queries ,
2026-08-22 09:36:15 +03:00
};
}
describe ( "research runner (async protocol §3)" , () => {
2026-09-01 19:15:43 +03:00
it ( "runJob completes the job: result + sources on the job, brief on the steer, activity recorded" , async () => {
2026-09-22 13:55:01 +03:00
const chat = chatStore . createChat ( "Test: runner completion" );
trackChat ( chat . id );
// A dispatched research part in the stored chat (the input is also
// what the runner falls back to on a crash-recovery re-spawn).
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , nativeToolMessages ( "which sigma version?" , "research" , "call-run1" , RESEARCH_INPUT ));
2026-09-22 13:55:01 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-run1" });
trackJob ( job . id );
const delivery = fakeDeliver ();
const search = stubSearch ();
const runner = createJobRunner ({ chatStore , jobStore , deliver : delivery.deliver , searchProvider : () => search });
researchModelOverride . current = researcherModel ();
2026-08-22 09:36:15 +03:00
try {
2026-09-22 13:55:01 +03:00
await runner . runJob ( job . id );
2026-08-22 09:36:15 +03:00
} finally {
2026-09-22 13:55:01 +03:00
researchModelOverride . current = undefined ;
2026-08-22 09:36:15 +03:00
}
2026-09-22 13:55:01 +03:00
// The job is done, with the activity line from the tool call.
const done = jobStore . getJob ( job . id ) ! ;
assert . equal ( done . status , "done" );
assert . equal ( done . activity , "delivered" , "the fake deliver flips the status like the pipeline" );
const result = done . result as ResearchResult ;
assert . equal ( result . brief , BRIEF );
assert . ok ( result . sources . length > 0 , "sources parsed from the brief" );
assert . equal ( result . sources [ 0 ] ! . url , "https://www.sigmajs.org/docs" );
// The activity trail: the last recorded activity is the deliver
// flip; the search line fired before it. The search itself ran on
// the INJECTED port — the stub saw the query (no network):
assert . deepEqual ( search . queries , [ "sigma.js latest version (5)" ]);
2026-08-22 09:36:15 +03:00
});
2026-09-01 19:15:43 +03:00
it ( "a failing runner still steers (the teacher learns the outcome) and the job lands error" , async () => {
2026-08-31 20:00:12 +03:00
const chat = chatStore . createChat ( "Test: runner failure" );
2026-08-22 09:36:15 +03:00
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , nativeToolMessages ( "hi" , "research" , "call-err1" , RESEARCH_INPUT ));
2026-08-22 09:36:15 +03:00
2026-08-31 20:00:12 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-err1" });
2026-08-22 09:36:15 +03:00
trackJob ( job . id );
2026-09-01 19:15:43 +03:00
const delivery = fakeDeliver ();
const runner = createJobRunner ({ chatStore , jobStore , deliver : delivery.deliver });
2026-09-12 02:34:18 +03:00
researchModelOverride . current = fauxEngine ([
() => {
2026-09-01 19:15:43 +03:00
throw new Error ( "model exploded" );
},
2026-09-12 02:34:18 +03:00
]);
2026-09-01 19:15:43 +03:00
try {
await runner . runJob ( job . id );
} finally {
researchModelOverride . current = undefined ;
}
2026-08-22 09:36:15 +03:00
2026-08-31 20:00:12 +03:00
const errored = jobStore . getJob ( job . id ) ! ;
2026-08-22 09:36:15 +03:00
assert . equal ( errored . status , "error" );
assert . match ( errored . error ?? "" , /model exploded/ );
2026-09-01 19:15:43 +03:00
const steer = delivery . steers [ 0 ] ! ;
assert . ok ( steer , "failure still delivers a steer" );
2026-09-26 00:37:46 +03:00
assert . ok ( steer . content . includes ( "Research failed: model exploded" ), "failure brief on the steer text" );
assert . deepEqual ((( steer . details as { result : ResearchResult }). result ). sources , []);
2026-09-01 19:15:43 +03:00
});
it ( "runJob resolves the input from the transcript when dispatch-time input is absent (crash recovery)" , async () => {
2026-09-22 13:55:01 +03:00
const chat = chatStore . createChat ( "Test: transcript input fallback" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , nativeToolMessages ( "go" , "research" , "call-fb1" , RESEARCH_INPUT ));
2026-09-22 13:55:01 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-fb1" });
trackJob ( job . id );
const delivery = fakeDeliver ();
const runner = createJobRunner ({
chatStore ,
jobStore ,
deliver : delivery.deliver ,
searchProvider : () => stubSearch (),
});
researchModelOverride . current = researcherModel ();
2026-09-01 19:15:43 +03:00
try {
2026-09-22 13:55:01 +03:00
// no input passed — as a recovery re-spawn would call it
await runner . runJob ( job . id );
2026-09-01 19:15:43 +03:00
} finally {
2026-09-22 13:55:01 +03:00
researchModelOverride . current = undefined ;
2026-09-01 19:15:43 +03:00
}
2026-09-22 13:55:01 +03:00
const done = jobStore . getJob ( job . id ) ! ;
assert . equal ( done . status , "done" , "the researcher ran from the transcript-resolved input" );
assert . equal (( done . result as ResearchResult ). brief , BRIEF );
2026-08-22 09:36:15 +03:00
});
});
// ─── the researcher agent construction (model resolution seam) ──────────────
2026-09-19 15:24:26 +03:00
describe ( "pdf runner (sources v0)" , () => {
const PDF_FIXTURE = path . join ( import . meta . dirname , "fixtures" , "sample-3page.pdf" );
const PDF_INPUT = { path : PDF_FIXTURE , question : "What is the integral of x dx?" };
/** A pdf-reader that triages, extracts pages 1-2, then briefs. */
function pdfReaderModel () : PiEngine {
return fauxEngine ([
fauxAssistantMessage ([ fauxToolCall ( "pdf_info" , {}, { id : "i1" })], { stopReason : "toolUse" }),
fauxAssistantMessage ([ fauxToolCall ( "pdf_extract" , { pages : "1-2" }, { id : "e1" })], { stopReason : "toolUse" }),
fauxAssistantMessage ([ fauxText ( "The integral rule appears on page 1 (p. 1): x dx integrates to x^2/2." )]),
]);
}
it ( "runJob completes a pdf job: brief + pagesConsulted land on the record, citation steer delivered" , async () => {
const chat = chatStore . createChat ( "Test: pdf runner" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , nativeToolMessages ( "read this pdf" , "pdf" , "call-pdf1" , PDF_INPUT ));
2026-09-19 15:24:26 +03:00
const job = jobStore . createJob ({ kind : "pdf" , chatId : chat.id , toolCallId : "call-pdf1" });
trackJob ( job . id );
const delivery = fakeDeliver ();
const runner = createJobRunner ({ chatStore , jobStore , deliver : delivery.deliver });
pdfModelOverride . current = pdfReaderModel ();
try {
await runner . runJob ( job . id );
} finally {
pdfModelOverride . current = undefined ;
}
const done = jobStore . getJob ( job . id ) ! ;
assert . equal ( done . status , "done" );
assert . equal ( done . activity , "delivered" );
const result = done . result as PdfResult ;
assert . equal ( result . path , PDF_FIXTURE );
assert . match ( result . brief , /integral rule/ );
2026-09-19 16:52:09 +03:00
assert . deepEqual ( result . pagesRead , [ 1 , 2 ], "extracted pages are READ pages" );
2026-09-19 15:24:26 +03:00
assert . deepEqual ( result . pagesConsulted , [ 1 , 2 ]);
assert . equal ( delivery . steers . length , 1 , "one job, one steer" );
const steer = delivery . steers [ 0 ] ! ;
2026-09-26 00:37:46 +03:00
assert . equal (( steer . details as { kind : string }). kind , "pdf" );
assert . ok ( steer . content . includes ( "integral rule" ), "steer text carries the brief" );
2026-09-19 15:24:26 +03:00
});
it ( "an unreadable source: the job lands error with the PDF failed: brief and the steer flags the gap" , async () => {
const chat = chatStore . createChat ( "Test: pdf runner failure" );
trackChat ( chat . id );
const badPath = path . join ( import . meta . dirname , "fixtures" , "not-a-pdf.txt" );
const job = jobStore . createJob ({ kind : "pdf" , chatId : chat.id , toolCallId : "call-pdf2" });
trackJob ( job . id );
const delivery = fakeDeliver ();
const runner = createJobRunner ({ chatStore , jobStore , deliver : delivery.deliver });
pdfModelOverride . current = pdfReaderModel ();
try {
await runner . runJob ( job . id , { path : badPath , question : "anything" });
} finally {
pdfModelOverride . current = undefined ;
}
const done = jobStore . getJob ( job . id ) ! ;
assert . equal ( done . status , "error" , "the failure brief is a subagent failure" );
const result = done . result as PdfResult ;
assert . match ( result . brief , /^PDF failed:/ );
2026-09-19 16:52:09 +03:00
assert . deepEqual ( result . pagesRead , []);
2026-09-19 15:24:26 +03:00
assert . deepEqual ( result . pagesConsulted , []);
const steer = delivery . steers [ 0 ] ! ;
2026-09-26 00:37:46 +03:00
assert . ok ( steer . content . includes ( "flag the gap" ), "the teacher learns to flag the gap" );
2026-09-19 15:24:26 +03:00
});
});
2026-08-22 09:36:15 +03:00
describe ( "researcher agent" , () => {
2026-09-12 02:34:18 +03:00
it ( "runs with the web tool set (no network touched)" , () => {
// The researcher is a runSubagent call over the webTools registry —
// the tool set is what the loop sees.
assert . ok ( webTools . web_search , "web_search available" );
assert . ok ( webTools . web_fetch , "web_fetch available" );
assert . equal ( Object . keys ( webTools ). length , 2 , "ONLY the web tools — no question/plan tools" );
2026-08-22 09:36:15 +03:00
// reset any accidental global override
researchModelOverride . current = undefined ;
});
2026-09-19 15:24:26 +03:00
it ( "the pdf-reader declares exactly the four skill tools (sources v0)" , async () => {
const { createPdfTools } = await import ( "../src/agent/subagents/pdf-reader.ts" );
const tools = createPdfTools ( path . join ( import . meta . dirname , "fixtures" , "sample-3page.pdf" ));
assert . deepEqual ( Object . keys ( tools ). sort (), [ "pdf_extract" , "pdf_info" , "pdf_render" , "pdf_search" ]);
// reset any accidental global override
pdfModelOverride . current = undefined ;
});
2026-08-22 09:36:15 +03:00
});
2026-09-01 19:15:43 +03:00
// ─── 6. the wake pipeline (server-side steer delivery) ────────────────────────
/**
* Pipeline tests drive deliverJobResult with an injected fake turn runner
* (setTurnRunner) — the real wiring (chat-handler's handleChatRequest
* adapter) never fires here, so no teacher model is touched.
*
* Timing: the pump polls at 400ms with a 1.5s grace after the gates open —
* each happy-path test takes ~2s; waitFor tolerates it.
*/
describe ( "wake pipeline (src/server/jobs.ts)" , () => {
/** A chat with a finished job ready for delivery. */
2026-09-26 00:37:46 +03:00
function setup ( kind : "research" | "demo" = "research" ) : { chatId : string ; jobId : string ; steer : SteerMessage } {
2026-09-01 19:15:43 +03:00
const chat = chatStore . createChat ( "Test: wake pipeline" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , [{ id : "u1" , role : "user" , content : "hello" , timestamp : 1 } as never ]);
2026-09-01 19:15:43 +03:00
const job = jobStore . createJob ({ kind , chatId : chat.id , toolCallId : `call- ${ jobStore . listJobs ( chat . id ). length } ` });
trackJob ( job . id );
const result : ResearchResult = { brief : "Done: it works." , sources : [{ title : "s" , url : "https://x.test" }] };
jobStore . updateJob ( job . id , { result , activity : "delivering result" });
const steer = buildSubagentSteer ( job , result );
return { chatId : chat.id , jobId : job.id , steer };
}
2026-09-11 06:44:20 +03:00
it ( "delivers a PROBE HANDOFF steer the same way — the teacher's wake turn" , async () => {
const chat = chatStore . createChat ( "Test: probe handoff wake" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
// The concluded probe: a persisted probe_done call + result (the
// native seam the chat-handler's construction branch keys on) and no
// open question.
chatStore . saveMessages ( chat . id , nativeToolMessages ( "teach me calc" , "probe_done" , "call-pd" , { ok : true }));
2026-09-11 06:44:20 +03:00
const handoff = buildProbeHandoffSteer ( chat . id );
2026-09-26 00:37:46 +03:00
let seen : { chatId : string ; steer : SteerMessage } | undefined ;
setTurnRunner ( async ( cid , input ) => {
seen = { chatId : cid , steer : ( input as SteerInput ). steer as SteerMessage };
2026-09-11 06:44:20 +03:00
return 200 ;
});
await deliverJobResult ( handoff );
2026-09-26 00:37:46 +03:00
await waitFor (() => seen !== undefined , 8000 , "handoff wake fired" );
const firedHandoff = seen ! ;
assert . equal ( firedHandoff . chatId , chat . id );
assert . equal ( firedHandoff . steer . id , `steer_probe_ ${ chat . id } ` , "the handoff IS the wake turn's input" );
2026-09-11 06:44:20 +03:00
});
it ( "a redelivered handoff dedupes via the transcript (idempotent by steer id)" , async () => {
const chat = chatStore . createChat ( "Test: handoff idempotent" );
trackChat ( chat . id );
const handoff = buildProbeHandoffSteer ( chat . id );
// The transcript already carries the steer (a previous wake completed).
chatStore . saveMessages ( chat . id , [ handoff as never ]);
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
await deliverJobResult ( handoff );
assert . equal ( fired , 0 , "already delivered — the pump resolves without firing a wake" );
});
2026-09-01 19:15:43 +03:00
it ( "delivers: fires the turn runner with the record + steer, then flips the job done" , async () => {
const { chatId , jobId , steer } = setup ();
2026-09-26 00:37:46 +03:00
let seen : { chatId : string ; steer : SteerMessage } | undefined ;
setTurnRunner ( async ( cid , input ) => {
seen = { chatId : cid , steer : ( input as SteerInput ). steer as SteerMessage };
2026-09-01 19:15:43 +03:00
return 200 ;
});
await deliverJobResult ( steer );
await waitFor (() => jobStore . getJob ( jobId ) ! . status !== "running" , 8000 , "job flip after delivery" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" );
assert . equal ( jobStore . getJob ( jobId ) ! . activity , "delivered" );
assert . ok ( seen , "the turn runner fired" );
2026-09-26 00:37:46 +03:00
assert . equal ( seen . chatId , chatId );
assert . equal ( seen . steer . id , steer . id , "the steer IS the wake turn's input" );
2026-09-01 19:15:43 +03:00
});
it ( "waits while a question part is pending, and fires once it clears" , async () => {
const { chatId , jobId , steer } = setup ();
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
2026-09-26 00:37:46 +03:00
// A dangling question call blocks the wake (the model would reject an
// unanswered tool call — fork 1: the record ends at the call).
2026-09-01 19:15:43 +03:00
chatStore . saveMessages ( chatId , [
2026-09-26 00:37:46 +03:00
{ id : "u1" , role : "user" , content : "hello" , timestamp : 1 } as never ,
2026-09-01 19:15:43 +03:00
{
id : "a1" ,
role : "assistant" ,
2026-09-26 00:37:46 +03:00
content : [{ type : "toolCall" , id : "quiz-call" , name : "quiz" , arguments : { question : "q" , choices : [ "a" , "b" ] } as never }],
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "toolUse" ,
timestamp : 1 ,
} as never ,
]);
2026-09-01 19:15:43 +03:00
// The delivery promise floats while the quiz blocks — awaiting it now
// would deadlock the test (the pump defers until the gate opens).
const delivery = deliverJobResult ( steer );
await delay ( 1200 );
assert . equal ( fired , 0 , "no wake while the quiz is pending" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "running" , "job stays running while blocked" );
2026-09-26 00:37:46 +03:00
// The learner answers: the real tool result clears the dangling call.
2026-09-01 19:15:43 +03:00
const record = chatStore . getChat ( chatId ) ! ;
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chatId , [
... record . messages ,
{
id : "r1" ,
role : "toolResult" ,
toolCallId : "quiz-call" ,
toolName : "quiz" ,
content : [{ type : "text" , text : "answered" }],
details : { answer : "a" , correct : true , explanation : "" },
isError : false ,
timestamp : 1 ,
} as never ,
]);
2026-09-01 19:15:43 +03:00
await waitFor (() => fired > 0 , 8000 , "wake after the quiz answer" );
await delivery ; // now it resolves — the steer was delivered
await waitFor (() => jobStore . getJob ( jobId ) ! . status !== "running" , 8000 , "job flip after the deferred wake" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" );
});
it ( "retries with backoff on a 409 (a client turn raced in) and delivers on the retry" , async () => {
const { jobId , steer } = setup ();
let calls = 0 ;
setTurnRunner ( async () => {
calls ++ ;
return calls === 1 ? 409 : 200 ;
});
await deliverJobResult ( steer );
assert . ok ( calls >= 2 , "409 was retried" );
await waitFor (() => jobStore . getJob ( jobId ) ! . status !== "running" , 8000 , "job flip after the retry" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" );
});
it ( "is idempotent: a steer already in the transcript flips the job without firing a turn" , async () => {
const { chatId , jobId , steer } = setup ();
// Simulate the crash window: the wake completed but the flip didn't.
const record = chatStore . getChat ( chatId ) ! ;
chatStore . saveMessages ( chatId , [... record . messages , steer ] as never );
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
await deliverJobResult ( steer );
assert . equal ( fired , 0 , "no duplicate wake for an already-delivered steer" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" , "the job still flips — recovery completes" );
});
2026-09-09 03:04:21 +03:00
// ─── continuation coalescing (the delivery-starvation fix) ───────────────────
describe ( "claimSteers + pump duplicate-guard (continuation coalescing)" , () => {
it ( "claims queued undelivered steers: resolves deliver(), flips the job done, empties the queue" , async () => {
const { chatId , jobId , steer } = setup ();
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
const delivery = deliverJobResult ( steer );
await delay ( 300 ); // the pump is inside waitForIdle's grace (750ms)
const claimed = claimSteers ( chatId );
assert . equal ( claimed . length , 1 , "the queued steer is claimed" );
assert . equal ( claimed [ 0 ] ! . id , steer . id );
// The handler's write-ahead persist lands the claim immediately after
// claiming (the very next statement in handleChatRequest).
const record = chatStore . getChat ( chatId ) ! ;
chatStore . saveMessages ( chatId , [... record . messages , ... claimed ] as never );
await delivery ; // the runner's deliver() promise resolves on claim
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" , "job flips done at claim" );
assert . deepEqual ( claimSteers ( chatId ), [], "queue is empty after the claim" );
await delay ( 800 );
assert . equal ( fired , 0 , "the pump never fires a wake for a claimed steer" );
});
2026-09-26 00:37:46 +03:00
it ( "finalizes already-delivered steers; the handler's steerDelivered filter keeps one copy (a wake raced in)" , async () => {
2026-09-09 03:04:21 +03:00
const { chatId , jobId , steer } = setup ();
const record = chatStore . getChat ( chatId ) ! ;
chatStore . saveMessages ( chatId , [... record . messages , steer ] as never );
const delivery = deliverJobResult ( steer );
2026-09-26 00:37:46 +03:00
await delay ( 100 ); // let the pump's top-of-loop idempotent skip run
2026-09-09 03:04:21 +03:00
const claimed = claimSteers ( chatId );
2026-09-26 00:37:46 +03:00
// The pump itself finalizes already-delivered steers at the top of
// its loop — the queue is empty by the time a racing turn claims.
// (A steer that DID ride the queue gets claimed whole; the HANDLER's
// steerDelivered filter keeps exactly one copy at append.)
assert . equal ( claimed . length , 0 , "the pump finalized the delivered steer without firing a wake" );
assert . ok ( steerDelivered ( chatStore . getChat ( chatId ) ! . messages , steer . id ), "the record already holds it" );
2026-09-09 03:04:21 +03:00
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" );
await delivery ;
});
it ( "claimSteers on a chat with no queue returns [] and touches nothing" , () => {
assert . deepEqual ( claimSteers ( "chat_never_existed" ), []);
});
2026-09-26 00:37:46 +03:00
it ( "pump path: a steer coalesced into the record mid-flight is claimed, filtered at append, never double-fired (duplicate-key bug, 2026-09-18)" , async () => {
2026-09-18 09:13:36 +03:00
const { chatId , jobId , steer } = setup ();
// Own runner: this chat's gates are OPEN, so the pump would otherwise
// fire whatever runner the NEXT test registers (leaked counter).
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
2026-09-26 00:37:46 +03:00
// The racing turn's write-ahead ALREADY landed the steer in the record
// while the entry is still queued — the exact window the handler
// claims in (the pump dequeues only after the turn returns 200).
2026-09-18 09:13:36 +03:00
const delivery = deliverJobResult ( steer );
await delay ( 300 ); // the pump is inside waitForIdle's grace (750ms)
2026-09-26 00:37:46 +03:00
const incoming = [... chatStore . getChat ( chatId ) ! . messages , steer ];
chatStore . saveMessages ( chatId , incoming ); // the racing turn's write-ahead persist
const claimed = claimSteers ( chatId );
assert . equal ( claimed . length , 1 , "the queued steer is claimed — its delivery rides the turn" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" , "job still finalizes — the claim resolves the delivery" );
2026-09-18 09:13:36 +03:00
await delivery ;
assert . deepEqual ( claimSteers ( chatId ), [], "queue is drained" );
2026-09-26 00:37:46 +03:00
// The handler appends only steers NOT already in the record
// (steerDelivered) — the write-ahead keeps exactly ONE copy of the
// steer id, and the pump's post-idle re-check reads the RECORD and
// skips the delivered id.
const appends = claimed . filter (( c ) => ! steerDelivered ( chatStore . getChat ( chatId ) ! . messages , c . id ));
assert . equal ( appends . length , 0 , "the already-persisted steer never appends again" );
const outgoing = [... chatStore . getChat ( chatId ) ! . messages , ... appends ];
const ids = outgoing . map (( m ) => m . id );
2026-09-18 09:13:36 +03:00
assert . equal ( ids . filter (( id ) => id === steer . id ). length , 1 , "exactly one steer copy persists" );
2026-09-26 00:37:46 +03:00
await delay ( 800 ); // the pump's grace elapses — its re-check sees the delivered id
assert . equal ( fired , 0 , "the claimed-and-persisted steer never double-fires a wake" );
2026-09-18 09:13:36 +03:00
});
2026-09-09 03:04:21 +03:00
it ( "pump duplicate-guard: a steer coalesced into the record while the gates were closed is never double-fired" , async () => {
const { chatId , jobId , steer } = setup ();
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
2026-09-26 00:37:46 +03:00
// Gates closed: a dangling question call parks the pump in waitForIdle.
2026-09-09 03:04:21 +03:00
chatStore . saveMessages ( chatId , [
2026-09-26 00:37:46 +03:00
{ id : "u1" , role : "user" , content : "hello" , timestamp : 1 } as never ,
2026-09-09 03:04:21 +03:00
{
id : "a1" ,
role : "assistant" ,
2026-09-26 00:37:46 +03:00
content : [{ type : "toolCall" , id : "quiz-call" , name : "quiz" , arguments : { question : "q" , choices : [ "a" , "b" ] } as never }],
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "toolUse" ,
timestamp : 1 ,
} as never ,
]);
2026-09-09 03:04:21 +03:00
const delivery = deliverJobResult ( steer );
await delay ( 1200 );
assert . equal ( fired , 0 , "parked behind the pending quiz" );
// The client turn coalesces: it claims the steer and lands it in the record.
const claimed = claimSteers ( chatId );
assert . equal ( claimed . length , 1 , "coalescing claims the parked steer" );
const record = chatStore . getChat ( chatId ) ! ;
chatStore . saveMessages ( chatId , [... record . messages , steer ] as never ); // write-ahead persist
// The learner answers: the gates open — the parked pump must see the
// delivered steer via its post-idle re-check, NOT fire a second turn.
2026-09-26 00:37:46 +03:00
const answeredRecord = chatStore . getChat ( chatId ) ! ;
chatStore . saveMessages ( chatId , [
... answeredRecord . messages ,
{
id : "r1" ,
role : "toolResult" ,
toolCallId : "quiz-call" ,
toolName : "quiz" ,
content : [{ type : "text" , text : "answered" }],
details : { answer : "a" , correct : true , explanation : "" },
isError : false ,
timestamp : 1 ,
} as never ,
]);
2026-09-09 03:04:21 +03:00
await delivery ;
await delay ( 1500 );
assert . equal ( fired , 0 , "no duplicate wake after coalescing" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" );
});
});
2026-09-01 19:15:43 +03:00
it ( "fails the job (error status) when the wake turn errors unrecoverably" , async () => {
const { jobId , steer } = setup ();
setTurnRunner ( async () => 500 );
await deliverJobResult ( steer ). then (
() => assert . fail ( "expected delivery to reject" ),
() => undefined ,
);
const errored = jobStore . getJob ( jobId ) ! ;
assert . equal ( errored . status , "error" );
assert . match ( errored . error ?? "" , /status 500/ );
});
});
function delay ( ms : number ) : Promise < void > {
return new Promise (( resolve ) => setTimeout ( resolve , ms ));
}
2026-09-02 02:34:02 +03:00
// ─── persist id hygiene (the duplicate-empty-key bug) ────────────────────────
2026-09-26 00:37:46 +03:00
describe ( "persistTurn (native history, Wave B)" , () => {
it ( "stamps EMPTY ids (pi minted no id) at the canonical write and preserves existing ones" , () => {
const chat = chatStore . createChat ( "Test: persist stamps ids" );
2026-09-09 05:43:28 +03:00
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
const stamped = persistTurn ( chatStore , chat . id , 0 , [
{ id : "" , role : "user" , content : "wake turn" , timestamp : 1 } as never ,
{
id : "msg_kept" ,
role : "assistant" ,
content : [{ type : "text" , text : "hi" }],
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "stop" ,
timestamp : 1 ,
} as never ,
2026-09-09 05:43:28 +03:00
], false );
2026-09-26 00:37:46 +03:00
assert . notEqual ( stamped [ 0 ] ! . id , "" , "empty id replaced" );
assert . match ( stamped [ 0 ] ! . id , /^msg_/ );
assert . equal ( stamped [ 1 ] ! . id , "msg_kept" , "server-authored id untouched" );
2026-09-09 05:43:28 +03:00
const record = chatStore . getChat ( chat . id ) ! ;
2026-09-26 00:37:46 +03:00
assert . equal ( record . messages . length , 2 , "both messages persisted" );
assert . ok ( record . messages . every (( m ) => m . id && m . id . length > 0 ), "no empty ids survive persist" );
2026-09-09 05:43:28 +03:00
});
2026-09-26 00:37:46 +03:00
it ( "baseCount slices the turn's own messages (the agent's state is append-only across turns)" , () => {
const chat = chatStore . createChat ( "Test: persist slices the turn tail" );
2026-09-02 02:34:02 +03:00
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , [
{ id : "base_1" , role : "user" , content : "first" , timestamp : 1 } as never ,
]);
// The agent's state at onFinish: the base record + the turn's OWN new
// messages (Wave B: onFinish receives the full native history — the
// slice keeps persist append-only).
const state = [
{ id : "base_1" , role : "user" , content : "first" , timestamp : 1 } as never ,
{
id : "" ,
role : "assistant" ,
content : [{ type : "text" , text : "wake turn" }],
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "stop" ,
timestamp : 1 ,
} as never ,
];
const stamped = persistTurn ( chatStore , chat . id , 1 , state , false );
assert . equal ( stamped . length , 1 , "only the turn's new tail persists" );
const record = chatStore . getChat ( chat . id ) ! ;
assert . equal ( record . messages . length , 2 , "the base record is untouched — append-only" );
assert . match ( record . messages [ 1 ] ! . id , /^msg_/ );
});
it ( "drops clean question placeholders, keeps errored ones (fork 1: the record ends at the unanswered call)" , () => {
const chat = chatStore . createChat ( "Test: persist placeholder filtering" );
trackChat ( chat . id );
const stamped = persistTurn ( chatStore , chat . id , 0 , [
{
id : "" ,
role : "assistant" ,
content : [{ type : "toolCall" , id : "q1" , name : "quiz" , arguments : { question : "q?" } as never }],
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "toolUse" ,
timestamp : 1 ,
} as never ,
// The loop's synthetic "question posed" result — never a fact.
{
id : "" ,
role : "toolResult" ,
toolCallId : "q1" ,
toolName : "quiz" ,
content : [{ type : "text" , text : "question posed" }],
details : "question posed" ,
isError : false ,
timestamp : 1 ,
} as never ,
// An ERRORED authoring call DOES persist — the model's retry context.
{
id : "" ,
role : "toolResult" ,
toolCallId : "q1" ,
toolName : "quiz" ,
content : [{ type : "text" , text : "bad options" }],
details : "bad options" ,
isError : true ,
timestamp : 1 ,
} as never ,
2026-09-02 02:34:02 +03:00
], false );
2026-09-26 00:37:46 +03:00
assert . equal ( stamped . length , 2 , "the clean placeholder is filtered, the errored result stays" );
const record = chatStore . getChat ( chat . id ) ! ;
assert . equal ( record . messages . length , 2 );
assert . equal (( record . messages [ 1 ] as { isError : boolean }). isError , true );
2026-09-02 02:34:02 +03:00
});
});
2026-09-02 03:19:03 +03:00
// ─── prompt composition hygiene (the README leak) ─────────────────────────────
describe ( "prompt composition" , () => {
it ( "loads ONLY numeric-prefix fragments — README.md never enters the system prompt" , () => {
2026-09-11 06:44:20 +03:00
const instructions = composeInstructions ( "teacher" );
2026-09-02 03:19:03 +03:00
assert . ok ( ! instructions . includes ( "Prompt fragments" ), "README content must not leak" );
assert . ok ( ! instructions . includes ( "Rules for adding a fragment" ), "README content must not leak (rules)" );
// every numeric fragment DOES compose, verbatim, after the base prompt
const base = fs . readFileSync ( "src/agent/system-prompt.md" , "utf-8" ). trim ();
assert . ok ( instructions . startsWith ( base ), "base prompt first" );
for ( const f of fs . readdirSync ( "src/agent/prompts" ). filter (( f ) => /^\d+-[\w.-]+\.md$/ . test ( f )). sort ()) {
const body = fs . readFileSync ( `src/agent/prompts/ ${ f } ` , "utf-8" ). trim ();
2026-09-05 21:33:13 +03:00
assert . ok ( instructions . includes ( body ), `fragment ${ f } must compose verbatim` );
2026-09-02 03:19:03 +03:00
}
});
2026-09-05 21:33:13 +03:00
2026-09-19 14:06:07 +03:00
it ( "the question principles compose into the teacher prompt; the probe's quick-measurement revision landed" , () => {
const teacher = composeInstructions ( "teacher" );
// the T2a doctrine fragment (user-locked wording 2026-09-15): verbatim presence
for ( const pin of [
"Answerable by reasoning from the session" ,
"Never just reciting your words back" ,
"Prefer problems that apply the knowledge" ,
"Catch confusion early, and make it nameable" ,
]) {
assert . ok ( teacher . includes ( pin ), `teacher prompt lost the principle: ${ pin } ` );
}
// it loads in sorted fragment order: right after 20-teaching, before 30-jobs
assert . ok (
2026-09-24 18:40:15 +03:00
teacher . indexOf ( "Question principles" ) > teacher . indexOf ( "# Teach — one step at a time" ),
2026-09-19 14:06:07 +03:00
"principles sit after the teaching loop" ,
);
// the prober's doctrine: the "no rush" framing is gone, quick measurement in
const prober = composeInstructions ( "prober" );
assert . ok ( ! prober . includes ( "There is no rush" ), "the old no-rush framing is retired" );
assert . ok ( prober . includes ( "measurement should be efficient" ), "the quick-measurement directive is in" );
assert . ok ( ! prober . includes ( "Question principles" ), "the teach-phase fragment never composes into the prober" );
});
2026-09-19 15:24:26 +03:00
it ( "the sources-v0 prompt lines compose: read/pdf doctrine in the teacher, the read line in the prober" , () => {
const teacher = composeInstructions ( "teacher" );
assert . ok ( teacher . includes ( "Learner source files" ), "the teacher carries the source-files section" );
assert . ok ( teacher . includes ( "brief with page citations" ), "the pdf brief's citation duty is stated" );
const prober = composeInstructions ( "prober" );
assert . ok ( prober . includes ( "the `read` tool can inspect it directly" ), "the prober knows it can read learner files" );
});
2026-09-19 16:50:20 +03:00
it ( "the sources-v1 shared fragment composes into BOTH agents (find/ls/grep doctrine)" , () => {
for ( const kind of [ "teacher" , "prober" ] as const ) {
const instructions = composeInstructions ( kind );
assert . ok ( instructions . includes ( "## Learner files" ), ` ${ kind } carries the shared learner-files fragment` );
assert . ok ( instructions . includes ( "find it yourself with `find`" ), ` ${ kind } knows to locate files itself` );
assert . ok ( instructions . includes ( "home directory" ), ` ${ kind } knows the home-anchored path rule` );
}
// shared composes BEFORE the agent's own fragments (the seam's order rule)
const teacher = composeInstructions ( "teacher" );
assert . ok (
teacher . indexOf ( "## Learner files" ) < teacher . indexOf ( "## Learner source files" ),
"shared fragment precedes the teacher's own 30-jobs section" ,
);
});
2026-09-05 21:33:13 +03:00
it ( "guards prompt size — no fragment can silently balloon (the README-leak class)" , () => {
// The wave-1 leak: README.md (815 bytes / ~200 tokens) rode into every
// teacher turn until the numeric-prefix filter stopped it. This guard
// catches the sibling failure mode — an accidental dump pasted into a
// fragment. Thresholds: largest real fragment is 40-demos.md at ~3.2k
// chars; 8k is generous headroom. Composed total ≤ 60k chars.
for ( const f of fs . readdirSync ( "src/agent/prompts" ). filter (( f ) => /^\d+-[\w.-]+\.md$/ . test ( f ))) {
const body = fs . readFileSync ( `src/agent/prompts/ ${ f } ` , "utf-8" ). trim ();
assert . ok ( body . length <= 8 _000 , `fragment ${ f } is ${ body . length } chars — over the 8k guard` );
}
2026-09-11 06:44:20 +03:00
assert . ok ( composeInstructions ( "teacher" ). length <= 60 _000 , "composed system prompt over the 60k guard" );
2026-09-05 21:33:13 +03:00
});
2026-09-02 03:19:03 +03:00
});
2026-09-11 03:25:09 +03:00
// ─── abort (2026-09 semantics: real kill, cancelled status, teacher told) ───
describe ( "abort (src/server/jobs.ts abortJob)" , () => {
2026-09-12 02:34:18 +03:00
/** A hanging model whose call never resolves until its abort signal
* fires — the real-kill path: the in-flight call itself is killed. */
function hangingModel () : PiEngine {
return fauxEngine ([ abortableFactory ()]);
2026-09-11 03:25:09 +03:00
}
2026-09-12 02:34:18 +03:00
/** One step (billed) then a hang — proves usage-so-far survives the kill. */
function oneStepThenHangModel () : PiEngine {
return fauxEngine ([
fauxAssistantMessage ([ fauxToolCall ( "web_search" , { query : "anything" }, { id : "ws1" })], {
stopReason : "toolUse" ,
}),
abortableFactory (),
]);
2026-09-11 03:25:09 +03:00
}
function newChatWithResearchJob ( label : string , toolCallId : string ) : { chatId : string ; jobId : string } {
const chat = chatStore . createChat ( `Test: ${ label } ` );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
// Native fixture — the record is agent history (Wave B).
2026-09-11 03:25:09 +03:00
chatStore . saveMessages ( chat . id , [
2026-09-26 00:37:46 +03:00
{ id : "u1" , role : "user" , content : "hi" , timestamp : 1 } as never ,
2026-09-11 03:25:09 +03:00
{
id : "a1" ,
role : "assistant" ,
2026-09-26 00:37:46 +03:00
content : [
2026-09-11 03:25:09 +03:00
{ type : "text" , text : "Checking." },
2026-09-26 00:37:46 +03:00
{ type : "toolCall" , id : toolCallId , name : "research" , arguments : RESEARCH_INPUT as never },
2026-09-11 03:25:09 +03:00
],
2026-09-26 00:37:46 +03:00
api : "openai-completions" ,
provider : "openrouter" ,
model : "test" ,
usage : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 },
stopReason : "toolUse" ,
timestamp : 1 ,
} as never ,
{
id : "r1" ,
role : "toolResult" ,
toolCallId ,
toolName : "research" ,
content : [{ type : "text" , text : "Dispatched." }],
details : "Dispatched." ,
isError : false ,
timestamp : 1 ,
} as never ,
]);
2026-09-11 03:25:09 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId });
trackJob ( job . id );
return { chatId : chat.id , jobId : job.id };
}
it ( "kills the running loop: error \"Aborted by learner\" immediately, loop unwinds silently, teacher gets the abort steer" , async () => {
const { jobId } = newChatWithResearchJob ( "abort running job" , "call-abort1" );
const fired : SteerMessage [] = [];
2026-09-26 00:37:46 +03:00
setTurnRunner ( async ( _cid , input ) => {
fired . push (( input as SteerInput ). steer as SteerMessage );
2026-09-11 03:25:09 +03:00
return 200 ;
});
researchModelOverride . current = hangingModel ();
const runner = createJobRunner ({
chatStore ,
jobStore ,
deliver : async () => {
throw new Error ( "a cancelled job must never deliver a result steer" );
},
});
try {
const settled = runner . runJob ( jobId , RESEARCH_INPUT );
await waitFor (() => jobStore . getJob ( jobId ) ! . activity === "researching" , 8000 , "loop started" );
const outcome = abortJob ( jobId );
assert . ok ( outcome . ok );
const killed = jobStore . getJob ( jobId ) ! ;
assert . equal ( killed . status , "error" , "flipped synchronously — the poll sees it at once" );
assert . equal ( killed . error , "Aborted by learner" , "abort reads as an error with a reason, not a failure" );
assert . equal ( killed . activity , "aborted" );
await settled ; // unwinds — no hang
assert . equal ( jobStore . getJob ( jobId ) ! . status , "error" , "the runner never overwrites the flip" );
assert . equal ( jobStore . getJob ( jobId ) ! . error , "Aborted by learner" );
assert . equal ( jobStore . getJob ( jobId ) ! . result , undefined , "no result on an aborted job" );
await waitFor (() => fired . length > 0 , 8000 , "abort wake fired" );
const last = fired [ 0 ] ! ;
2026-09-26 00:37:46 +03:00
assert . equal (( last . details as { aborted : boolean }). aborted , true );
2026-09-11 03:25:09 +03:00
assert . equal ( last . id , `steer_ ${ jobId } ` );
2026-09-26 00:37:46 +03:00
assert . equal ( last . customType , "subagent-abort" , "the abort marker — no artifact" );
2026-09-11 03:25:09 +03:00
// aborted is terminal: the wake completing must not flip it
assert . equal ( jobStore . getJob ( jobId ) ! . status , "error" , "terminal after the wake" );
} finally {
researchModelOverride . current = undefined ;
}
});
it ( "the usage folded so far survives the kill (money channel outlives the abort)" , async () => {
2026-09-22 13:55:01 +03:00
const { jobId } = newChatWithResearchJob ( "abort keeps usage" , "call-abort2" );
setTurnRunner ( async () => 200 );
researchModelOverride . current = oneStepThenHangModel ();
const runner = createJobRunner ({ chatStore , jobStore , deliver : async () => {}, searchProvider : () => stubSearch () });
2026-09-11 03:25:09 +03:00
try {
2026-09-22 13:55:01 +03:00
const settled = runner . runJob ( jobId , RESEARCH_INPUT );
await waitFor (() => ( jobStore . getJob ( jobId ) ! . usage ? . steps ?? 0 ) === 1 , 8000 , "first step folded" );
abortJob ( jobId );
await settled ;
const job = jobStore . getJob ( jobId ) ! ;
assert . equal ( job . status , "error" );
assert . equal ( job . error , "Aborted by learner" );
assert . equal ( job . usage ! . steps , 1 , "usage-so-far persisted by the runner's finally" );
// model slots wave: the stamp is the ENGINE's actual model,
// provider-qualified — the scripted faux engine here.
assert . equal ( job . usage ! . model , "faux:faux-1" );
// faux estimates tokens from the prompt/response text — the
// pinned fact is that the ONE completed step's spend survived.
assert . ok ( job . usage ! . inputTokens ! > 0 , "input tokens billed" );
assert . ok ( job . usage ! . outputTokens ! > 0 , "output tokens billed" );
2026-09-11 03:25:09 +03:00
} finally {
2026-09-22 13:55:01 +03:00
researchModelOverride . current = undefined ;
2026-09-11 03:25:09 +03:00
}
});
it ( "aborting a delivering job drops the queued result steer and wakes with the abort note instead" , async () => {
const chat = chatStore . createChat ( "Test: abort delivering job" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , [{ id : "u1" , role : "user" , content : "hello" , timestamp : 1 } as never ]);
2026-09-11 03:25:09 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-abort3" });
trackJob ( job . id );
const result : ResearchResult = { brief : "Done: it works." , sources : [{ title : "s" , url : "https://x.test" }] };
jobStore . updateJob ( job . id , { result , activity : "delivering result" });
// Block the pump's idle gates so the result steer sits QUEUED, not in flight.
assert . ok ( beginTurn ( chat . id ));
const fired : SteerMessage [] = [];
2026-09-26 00:37:46 +03:00
setTurnRunner ( async ( _cid , input ) => {
fired . push (( input as SteerInput ). steer as SteerMessage );
2026-09-11 03:25:09 +03:00
return 200 ;
});
void deliverJobResult ( buildSubagentSteer ( job , result )). catch (() => {
/* dropped by the abort below */
});
await waitFor (() => jobStore . getJob ( job . id ) ! . activity === "delivering result" , 8000 , "queued" );
const outcome = abortJob ( job . id );
assert . ok ( outcome . ok );
assert . equal ( jobStore . getJob ( job . id ) ! . status , "error" );
assert . equal ( jobStore . getJob ( job . id ) ! . error , "Aborted by learner" );
endTurn ( chat . id ); // gates open — whatever is left in the queue fires
await waitFor (() => fired . length > 0 , 8000 , "wake fired" );
assert . equal ( fired . length , 1 , "exactly one delivery — the result steer was dropped" );
2026-09-26 00:37:46 +03:00
assert . equal (( fired [ 0 ] ! . details as { aborted : boolean }). aborted , true , "the abort steer, not the result" );
2026-09-11 03:25:09 +03:00
assert . equal ( jobStore . getJob ( job . id ) ! . status , "error" , "terminal after the wake" );
});
it ( "outcome reasons: not_found for unknown, not_running for settled jobs" , () => {
assert . deepEqual ( abortJob ( "job_does_not_exist" ), { ok : false , reason : "not_found" });
const chat = chatStore . createChat ( "Test: abort settled job" );
trackChat ( chat . id );
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-abort4" });
trackJob ( job . id );
jobStore . updateJob ( job . id , { status : "done" , activity : "delivered" });
assert . deepEqual ( abortJob ( job . id ), { ok : false , reason : "not_running" });
});
});
2026-09-19 13:16:34 +03:00
// ─── 7. crash recovery, LIVENESS-GATED (observability-accuracy wave, 2026-09-19) ──
//
// The sweep (recoverStaleJobs) keys on the abort registry (subagents/aborts.ts
// isJobLive), not record age: a "running" record with a live registration
// belongs to a loop executing in THIS process (hung or slow — never re-run
// beside itself); without one it is a process-death orphan and recovers at
// once. The old 15-minute STALE_JOB_MS heuristic is gone — these tests prove
// immediacy by running the sweep on FRESH records.
describe ( "stale-job recovery sweep (liveness-gated)" , () => {
/** Reset the sweep's 30s throttle — tests fire it directly, repeatedly. */
function resetSweepThrottle () : void {
const g = globalThis as { __lhWakePipeline ?: { lastSweepAt : number } };
if ( g . __lhWakePipeline ) g . __lhWakePipeline . lastSweepAt = 0 ;
}
/** A FRESH "running" record (no loop registration): the orphan shape.
* Freshness is the point — the old code needed 15 minutes of age. */
function orphanJob ( withResult? : ResearchResult ) : { chatId : string ; jobId : string } {
const chat = chatStore . createChat ( "Test: stale sweep" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , [{ id : "u1" , role : "user" , content : "hello" , timestamp : 1 } as never ]);
2026-09-19 13:16:34 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : `call-sweep- ${ createdJobs . length } ` });
trackJob ( job . id );
if ( withResult ) jobStore . updateJob ( job . id , { result : withResult , activity : "delivering result" });
return { chatId : chat.id , jobId : job.id };
}
it ( "a running record with a LIVE loop registration is never recovered — however old (the double-run guard)" , () => {
const { jobId } = orphanJob ();
// Backdate the record well past every plausible in-flight run: age
// alone must NOT trigger recovery anymore.
jobStore . updateJob ( jobId , { updatedAt : new Date ( Date . now () - 30 * 60 _000 ). toISOString () });
registerJobAbort ( jobId , new AbortController ()); // a loop is executing
let respawned = 0 ;
setJobRespawn (() => {
respawned ++ ;
});
resetSweepThrottle ();
recoverStaleJobs ();
assert . equal ( respawned , 0 , "a live loop is provably executing — no respawn beside it" );
assert . equal ( jobStore . getJob ( jobId ) ! . activity , "starting" , "the record is untouched" );
// Hermetic teardown: settle + unregister so later sweeps in THIS suite
// never see this fixture as a fresh orphan.
jobStore . updateJob ( jobId , { status : "done" , activity : "settled (test)" });
unregisterJobAbort ( jobId );
});
it ( "a running record WITHOUT a registration is an orphan: the loop is re-spawned immediately (no age wait)" , () => {
const { jobId } = orphanJob ();
let respawned : string | null = null ;
setJobRespawn (( id ) => {
respawned = id ;
});
resetSweepThrottle ();
recoverStaleJobs ();
assert . equal ( respawned , jobId , "the orphan's loop is re-spawned at once" );
assert . equal ( jobStore . getJob ( jobId ) ! . activity , "recovering: re-running" );
// Hermetic teardown (see above) — the respawned loop is the TEST's
// fake seam, nothing real runs.
jobStore . updateJob ( jobId , { status : "done" , activity : "settled (test)" });
});
it ( "an orphan WITH a stored result is re-delivered: the wake fires and the job flips done" , async () => {
const { jobId } = orphanJob ({ brief : "Done: it works." , sources : [] });
let fired = 0 ;
setTurnRunner ( async () => {
fired ++ ;
return 200 ;
});
resetSweepThrottle ();
recoverStaleJobs ();
await waitFor (() => fired > 0 , 8000 , "recovery wake fired" );
await waitFor (() => jobStore . getJob ( jobId ) ! . status !== "running" , 8000 , "orphan settled after re-delivery" );
assert . equal ( jobStore . getJob ( jobId ) ! . status , "done" );
});
});
2026-09-22 16:58:25 +03:00
// ─── BYOK: the unresolvable-engine refusal (owner, 2026-09-22) ──────────────
describe ( "BYOK job refusal (no factory floor)" , () => {
it ( "runJob with NO resolvable engine lands the job as error — the envelope wording, no phantom run" , async () => {
const chat = chatStore . createChat ( "Test: unconfigured spawn" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , nativeToolMessages ( "hi" , "research" , "call-nc1" , RESEARCH_INPUT ));
2026-09-22 16:58:25 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-nc1" });
trackJob ( job . id );
const delivery = fakeDeliver ();
// NO test override, NO engineFor: the chain (override → slot → floor)
// comes up empty — the runner must refuse honestly, not fall back.
const runner = createJobRunner ({ chatStore , jobStore , deliver : delivery.deliver });
await runner . runJob ( job . id );
const done = jobStore . getJob ( job . id ) ! ;
2026-09-22 17:41:07 +03:00
// The refusal lands through the SAME convention as every other
// failed job (BYOK review fix, Teacher stream 2026-09-22): the kind's
// failure result steers the teacher — a dispatched job is never
// stranded waiting for a result that will never come — and
// finalizeJob flips the record to error after the delivery.
2026-09-22 16:58:25 +03:00
assert . equal ( done . status , "error" , "the job lands error, never a phantom run" );
assert . match ( done . error ?? "" , /No model configured/ );
assert . match ( done . error ?? "" , /missing: cheap/ );
2026-09-22 17:41:07 +03:00
assert . equal ( delivery . steers . length , 1 , "the refusal STEERED — the teacher is not stranded" );
assert . ok (
2026-09-26 00:37:46 +03:00
delivery . steers [ 0 ] ! . content . includes ( "Research failed: No model configured" ),
2026-09-22 17:41:07 +03:00
"the steer carries the kind's failure shape" ,
);
const result = done . result as ResearchResult ;
assert . equal ( result . sources . length , 0 , "the failure result matches the research convention" );
2026-09-22 16:58:25 +03:00
});
});
2026-09-24 22:51:12 +03:00
// ─── provider-key: the key-presence job refusal (owner, 2026-09-25) ─────────
describe ( "provider-key job refusal (key-presence interlude)" , () => {
it ( "an engine whose provider key is gone refuses the spawn — envelope wording, no phantom run" , async () => {
const chat = chatStore . createChat ( "Test: keyless spawn" );
trackChat ( chat . id );
2026-09-26 00:37:46 +03:00
chatStore . saveMessages ( chat . id , nativeToolMessages ( "hi" , "research" , "call-km1" , RESEARCH_INPUT ));
2026-09-24 22:51:12 +03:00
const job = jobStore . createJob ({ kind : "research" , chatId : chat.id , toolCallId : "call-km1" });
trackJob ( job . id );
const delivery = fakeDeliver ();
// The chain RESOLVES (an engine exists) — the KEY is what's missing.
// The injected check is the server seam's stand-in: it sees the
// engine's provider + the kind's slot tier, exactly as wired.
const engine = fauxEngine ([]);
const seen : Array < [ string , ModelSlotName ] > = [];
const runner = createJobRunner ({
chatStore ,
jobStore ,
deliver : delivery.deliver ,
engineFor : () => engine ,
providerKeyMissing : ( provider , tier ) => {
seen . push ([ provider , tier ]);
return providerKeyMissing ( provider , [ tier ]);
},
});
await runner . runJob ( job . id );
// The refusal lands through the SAME convention as every failed job:
// honest record, kind failure shape steered, terminal flip after.
const done = jobStore . getJob ( job . id ) ! ;
assert . equal ( done . status , "error" , "the job lands error, never a phantom run" );
assert . equal ( done . error , providerKeyMissing ( engine . model . provider , [ "cheap" ]). error , "the envelope's error line verbatim" );
assert . deepEqual ( seen , [[ engine . model . provider , "cheap" ]], "the check saw the engine's provider + the researcher's tier" );
assert . equal ( delivery . steers . length , 1 , "the refusal STEERED — the teacher is not stranded" );
assert . ok (
2026-09-26 00:37:46 +03:00
delivery . steers [ 0 ] ! . content . includes ( `Research failed: No ${ engine . model . provider } key is set` ),
2026-09-24 22:51:12 +03:00
"the steer carries the kind's failure shape" ,
);
const result = done . result as ResearchResult ;
assert . equal ( result . sources . length , 0 , "the failure result matches the research convention" );
});
});