Files
roast_command_center/test/roasts.test.js
T
Shane MaynardandClaude Fable 5 eb82263ead
Test and deploy / test-and-deploy (push) Successful in 1m39s
Add finished-roast .alog uploads with Pi agent deep review and /roasts history
- POST /api/roasts stores the original .alog verbatim (multiple per plan),
  parses it server-side, and kicks off an async zero-tool Pi agent review
  (grade, highlights, concerns, next-batch suggestions, plan-vs-actual)
- /roasts page: table of historical actual roasts with review summaries;
  row detail renders the BT curve as SVG with the plan curve overlaid,
  shows the full review, and offers original-.alog backup download,
  re-evaluate, and delete
- Planner's Reference curve drawer gains a multi-file finished-roast
  uploader that attaches to the open (synced) plan
- Failed reviews record the error on the row and are retryable via
  POST /api/roasts/:id/evaluate (e.g. once a model is configured)

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 20:18:22 -04:00

241 lines
8.4 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { setup, signup } from "./helpers.js";
// Minimal but realistic Artisan .alog: 21 samples at 30s intervals, milestones marked via
// timeindex (charge, dry end, first crack, drop).
function makeAlog(title = "Test Roast") {
const timex = [];
const temp1 = [];
const temp2 = [];
for (let i = 0; i <= 20; i++) {
const t = i * 30;
timex.push(t);
temp1.push(200 + i * 2);
// BT: drop to a turning point then climb
temp2.push(i < 3 ? 180 - i * 30 : 90 + (i - 3) * 7);
}
return JSON.stringify({
title,
roastdate: "08.08.2026",
roastertype: "Hottop KN-8828B-2K+",
mode: "C",
weight: [250, 212, "g"],
timex,
temp1,
temp2,
timeindex: [1, 8, 14, 0, 0, 0, 20, 0],
});
}
async function pollStatus(agent, id, want, tries = 100) {
for (let i = 0; i < tries; i++) {
const res = await agent.get(`/api/roasts/${id}`);
if (res.body.roast?.evaluationStatus === want) return res.body.roast;
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error(`evaluation never reached status "${want}"`);
}
function evaluationStub() {
const calls = [];
let impl = async () => ({
summary: "Clean roast, slightly long drying.",
grade: "good",
highlights: ["DTR 20% in band"],
concerns: [],
suggestions: ["Charge 5C hotter"],
planComparison: "Tracked the plan within 20s.",
});
const stub = (parsed, plan) => {
calls.push({ parsed, plan });
return impl(parsed, plan);
};
stub.calls = calls;
stub.setImpl = (fn) => {
impl = fn;
};
return stub;
}
test("upload, evaluate, list, detail, download, delete", async () => {
const stub = evaluationStub();
const { agent } = await setup({}, { evaluateRoast: stub });
const { csrf } = await signup(agent, "[email protected]");
const plan = await agent
.post("/api/plans")
.set("x-csrf-token", csrf)
.send({ plan: { fields: { 0.1: "Kenya AA" } } });
assert.equal(plan.status, 201);
const planId = plan.body.plan.id;
// Upload attached to the plan
const up1 = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ roastPlanId: planId, filename: "roast-1.alog", content: makeAlog("Roast one") });
assert.equal(up1.status, 201);
assert.equal(up1.body.roast.evaluationStatus, "pending");
assert.equal(up1.body.roast.roast.title, "Roast one");
assert.equal(up1.body.roast.derived.dtrPct > 0, true);
// The async deep evaluation lands on the row
const done = await pollStatus(agent, up1.body.roast.id, "done");
assert.equal(done.evaluation.grade, "good");
assert.equal(done.evaluation.summary, "Clean roast, slightly long drying.");
// The stub was handed the linked plan for plan-vs-actual comparison
assert.equal(stub.calls[0].plan.fields["0.1"], "Kenya AA");
// A second upload against the same plan is allowed (multiple actuals per plan)
const up2 = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ roastPlanId: planId, filename: "roast-2.alog", content: makeAlog("Roast two") });
assert.equal(up2.status, 201);
await pollStatus(agent, up2.body.roast.id, "done");
// List shows both, newest first, with plan title but no heavy payloads
const list = await agent.get("/api/roasts");
assert.equal(list.body.roasts.length, 2);
assert.equal(list.body.roasts[0].planTitle, "Kenya AA");
assert.equal(list.body.roasts[0].parsed, undefined);
const filtered = await agent.get(`/api/roasts?plan=${planId}`);
assert.equal(filtered.body.roasts.length, 2);
// Detail includes full parsed curve, evaluation, and the linked plan
const detail = await agent.get(`/api/roasts/${up1.body.roast.id}`);
assert.equal(detail.status, 200);
assert.equal(detail.body.roast.parsed.curve.length > 0, true);
assert.equal(detail.body.roast.parsed.milestones.some((m) => m.key === "fc"), true);
assert.equal(detail.body.roast.plan.fields["0.1"], "Kenya AA");
// Download returns the original bytes as an attachment
const download = await agent.get(`/api/roasts/${up1.body.roast.id}/download`);
assert.equal(download.status, 200);
assert.match(download.headers["content-disposition"], /attachment; filename="roast-1.alog"/);
assert.equal(download.body.toString("utf8"), makeAlog("Roast one"));
// Delete
const del = await agent
.delete(`/api/roasts/${up1.body.roast.id}`)
.set("x-csrf-token", csrf);
assert.equal(del.status, 200);
assert.equal((await agent.get("/api/roasts")).body.roasts.length, 1);
});
test("upload without a plan, garbage rejected, csrf required", async () => {
const stub = evaluationStub();
const { agent } = await setup({}, { evaluateRoast: stub });
const { csrf } = await signup(agent, "[email protected]");
const up = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ filename: "standalone.alog", content: makeAlog("No plan") });
assert.equal(up.status, 201);
assert.equal(up.body.roast.roastPlanId, null);
const done = await pollStatus(agent, up.body.roast.id, "done");
assert.equal(done.plan, null);
assert.equal(stub.calls[0].plan, null);
const bad = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ filename: "junk.alog", content: "definitely not an alog {{{" });
assert.equal(bad.status, 422);
assert.equal(bad.body.code, "unparseable_alog");
const empty = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ filename: "x.alog" });
assert.equal(empty.status, 400);
const noCsrf = await agent
.post("/api/roasts")
.send({ filename: "x.alog", content: makeAlog() });
assert.equal(noCsrf.status, 403);
});
test("failed evaluation records the error and can be retried", async () => {
const stub = evaluationStub();
stub.setImpl(async () => {
const err = new Error("No model available from ~/.pi/agent config.");
err.code = "no_model";
throw err;
});
const { agent } = await setup({}, { evaluateRoast: stub });
const { csrf } = await signup(agent, "[email protected]");
const up = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ filename: "r.alog", content: makeAlog() });
assert.equal(up.status, 201);
const failed = await pollStatus(agent, up.body.roast.id, "failed");
assert.match(failed.evaluationError, /no_model/);
// Model becomes available; retry succeeds
stub.setImpl(async () => ({
summary: "Recovered.",
grade: "fair",
highlights: [],
concerns: [],
suggestions: [],
planComparison: null,
}));
const retry = await agent
.post(`/api/roasts/${up.body.roast.id}/evaluate`)
.set("x-csrf-token", csrf);
assert.equal(retry.status, 200);
const done = await pollStatus(agent, up.body.roast.id, "done");
assert.equal(done.evaluation.summary, "Recovered.");
assert.equal(done.evaluationError, null);
});
test("/roasts shell page requires a session and is never served by filename", async () => {
const { agent } = await setup({}, { evaluateRoast: evaluationStub() });
const anonymous = await agent.get("/roasts");
assert.equal(anonymous.status, 302);
assert.equal(anonymous.headers.location, "/login");
assert.equal((await agent.get("/roasts.html")).status, 302);
await signup(agent, "[email protected]");
const page = await agent.get("/roasts");
assert.equal(page.status, 200);
assert.match(page.text, /Roast history/);
});
test("roasts are private to their owner", async () => {
const stub = evaluationStub();
const { app, agent } = await setup({}, { evaluateRoast: stub });
const { csrf } = await signup(agent, "[email protected]");
const up = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ filename: "mine.alog", content: makeAlog("Private") });
assert.equal(up.status, 201);
const id = up.body.roast.id;
const request = (await import("supertest")).default;
const stranger = request.agent(app);
const { csrf: strangerCsrf } = await signup(stranger, "[email protected]");
assert.equal((await stranger.get("/api/roasts")).body.roasts.length, 0);
assert.equal((await stranger.get(`/api/roasts/${id}`)).status, 404);
assert.equal((await stranger.get(`/api/roasts/${id}/download`)).status, 404);
assert.equal(
(await stranger.delete(`/api/roasts/${id}`).set("x-csrf-token", strangerCsrf)).status,
404,
);
// A stranger's plan id can't be attached to my upload either
const plan = await agent
.post("/api/plans")
.set("x-csrf-token", csrf)
.send({ plan: { fields: {} } });
const crossPlan = await stranger
.post("/api/roasts")
.set("x-csrf-token", strangerCsrf)
.send({ roastPlanId: plan.body.plan.id, filename: "x.alog", content: makeAlog() });
assert.equal(crossPlan.status, 404);
});