diff --git a/backend/src/tht/tht-runner.ts b/backend/src/tht/tht-runner.ts index e14d6f7d..c93409b6 100644 --- a/backend/src/tht/tht-runner.ts +++ b/backend/src/tht/tht-runner.ts @@ -26,6 +26,15 @@ export interface SessionDocument { content: string; } +export interface OllamaEnsureResult { + ok: boolean; + stage?: string; + error?: string; + server?: string; + model?: string; + model_name?: string; +} + export class ThtRunner { constructor(private cfg: ThtConfig) {} @@ -129,4 +138,19 @@ export class ThtRunner { unarchive(id: string) { return this.ok(["session", "unarchive", id]); } deleteSession(id: string) { return this.ok(["session", "delete", id]); } documents(id: string) { return this.json(["session", "documents", id, "--json"]); } + + async ollamaEnsure(workspace: string, timeoutSec: number): Promise { + const { code, stdout, stderr } = await this.run( + ["ollama", "ensure", "--json", "--timeout", String(timeoutSec)], + workspace, + ); + let parsed: Partial = {}; + try { parsed = JSON.parse(stdout.trim() || "{}"); } catch { /* leave {} */ } + if (code === 0) return { ok: true, ...parsed }; + return { + ok: false, + stage: parsed.stage, + error: parsed.error ?? (stderr.trim() || `tht ollama ensure exit ${code}`), + }; + } } diff --git a/backend/test/tht-runner.test.ts b/backend/test/tht-runner.test.ts index a4f1d7e5..39c3f709 100644 --- a/backend/test/tht-runner.test.ts +++ b/backend/test/tht-runner.test.ts @@ -114,3 +114,28 @@ test("documents parses the JSON array", async () => { const docs = await r.documents("sid"); expect(docs[0].key).toBe("question"); }); + +test("ollamaEnsure builds argv with --json --timeout and the workspace -c", async () => { + let calledArgs: string[] = []; + let calledWs: string | undefined; + const r = new ThtRunner({ thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml" }); + r.run = async (args, ws) => { calledArgs = args; calledWs = ws; return { code: 0, stdout: '{"ok":true,"server":"up","model":"warmed","model_name":"m"}', stderr: "" }; }; + const res = await r.ollamaEnsure("psd", 60); + expect(calledArgs).toEqual(["ollama", "ensure", "--json", "--timeout", "60"]); + expect(calledWs).toBe("psd"); + expect(res).toEqual({ ok: true, server: "up", model: "warmed", model_name: "m" }); +}); + +test("ollamaEnsure maps a non-zero exit to ok:false with stage/error from stdout JSON", async () => { + const r = new ThtRunner({ thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml" }); + r.run = async () => ({ code: 1, stdout: '{"ok":false,"stage":"model","error":"missing"}', stderr: "" }); + expect(await r.ollamaEnsure("psd", 60)).toEqual({ ok: false, stage: "model", error: "missing" }); +}); + +test("ollamaEnsure falls back to stderr when stdout is not JSON on failure", async () => { + const r = new ThtRunner({ thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml" }); + r.run = async () => ({ code: 1, stdout: "", stderr: "boom" }); + const res = await r.ollamaEnsure("psd", 60); + expect(res.ok).toBe(false); + expect(res.error).toContain("boom"); +});