Move After-the-Roast onto actual roasts; add Artisan-importable updated .alog download
Test and deploy / test-and-deploy (push) Successful in 56s

One system: post-roast observations (weights, colour, DTR, cup notes,
'one change next batch', disproof) now live on the uploaded roast
(actual_roasts.after, migration 011), edited on the roast detail view
with weights/DTR prefilled from the .alog itself. The planner's screen
section is removed (the print worksheet keeps its hand-fill copy), and
the lot 'last refine' suggestion reads the note from both the new home
and legacy plans, newest wins.

Every roast now downloads two ways: the untouched original .alog, and
an updated supplemental copy with the app's data written back as valid
Artisan fields (weight, beans, roastingnotes incl. the LLM review,
cuppingnotes incl. linked cupping score/flavors) — serialized as a
Python literal so Artisan's ast.literal_eval re-imports it.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-09 08:35:11 -04:00
co-authored by Claude Fable 5
parent 9b6ea9880e
commit 4a3d192c2c
12 changed files with 395 additions and 161 deletions
+88
View File
@@ -206,6 +206,94 @@ test("/roasts shell page requires a session and is never served by filename", as
assert.match(page.text, /Roast history/);
});
test("after-the-roast lives on the actual roast and flows into the updated .alog", 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": "After test coffee" }, inventory: {} } });
const up = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ roastPlanId: plan.body.plan.id, filename: "after.alog", content: makeAlog("After roast") });
assert.equal(up.status, 201);
const id = up.body.roast.id;
// Save observations; junk body rejected
const put = await agent.put(`/api/roasts/${id}`).set("x-csrf-token", csrf).send({
after: {
greenIn: "250",
out: "212",
colour: "62/78",
oneChange: "first crack +0:30",
cupNotes: "papery, thin",
method: "V60 1:16",
nonsense: "dropped",
},
});
assert.equal(put.status, 200);
assert.equal(put.body.after.oneChange, "first crack +0:30");
assert.equal(put.body.after.nonsense, undefined);
assert.equal(
(await agent.put(`/api/roasts/${id}`).set("x-csrf-token", csrf).send({ after: "x" })).status,
400,
);
const detail = (await agent.get(`/api/roasts/${id}`)).body.roast;
assert.equal(detail.after.colour, "62/78");
// Original download unchanged; updated download is a Python literal with the app's data
const original = await agent.get(`/api/roasts/${id}/download`);
assert.equal(original.body.toString("utf8"), makeAlog("After roast"));
const updated = await agent.get(`/api/roasts/${id}/download?variant=updated`);
assert.equal(updated.status, 200);
assert.match(updated.headers["content-disposition"], /after-updated\.alog/);
const text = updated.body.toString("utf8");
assert.match(text, /"weight": \[250, 212, "g"\]/);
assert.match(text, /One change next batch: first crack \+0:30/);
assert.match(text, /papery, thin/);
assert.match(text, /"beans": "After test coffee"/);
// Round-trips through our own Artisan parser (same tolerant path Artisan's literal_eval uses)
const { parseAlog } = await import("../server/alog.js");
const reparsed = parseAlog(text, "after-updated.alog");
assert.equal(reparsed.roast.weightInG, 250);
assert.equal(reparsed.roast.title, "After roast");
// No JSON booleans/nulls leak into the Python literal
assert.equal(/\b(true|false|null)\b/.test(text), false);
});
test("last-refine reads the one-change note from actual roasts (new home) and legacy plans", async () => {
const stub = evaluationStub();
const { agent } = await setup({}, { evaluateRoast: stub });
const { csrf } = await signup(agent, "[email protected]");
const lot = (
await agent
.post("/api/inventory")
.set("x-csrf-token", csrf)
.send({ origin: "Kenya", initialWeightG: 1000 })
).body.lot;
const plan = (
await agent
.post("/api/plans")
.set("x-csrf-token", csrf)
.send({ plan: { fields: { "0.1": "Lot plan" }, inventory: { lotId: lot.id } } })
).body.plan;
// Note stored on the actual roast — the new home
const up = await agent
.post("/api/roasts")
.set("x-csrf-token", csrf)
.send({ roastPlanId: plan.id, filename: "r.alog", content: makeAlog("R") });
await agent
.put(`/api/roasts/${up.body.roast.id}`)
.set("x-csrf-token", csrf)
.send({ after: { oneChange: "development +0:15" } });
const refine = await agent.get(`/api/inventory/${lot.id}/last-refine`);
assert.equal(refine.status, 200);
assert.equal(refine.body.refine.oneChange, "development +0:15");
assert.equal(refine.body.refine.planTitle, "Lot plan");
});
test("roasts are private to their owner", async () => {
const stub = evaluationStub();
const { app, agent } = await setup({}, { evaluateRoast: stub });