Move After-the-Roast onto actual roasts; add Artisan-importable updated .alog download
Test and deploy / test-and-deploy (push) Successful in 56s
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:
co-authored by
Claude Fable 5
parent
9b6ea9880e
commit
4a3d192c2c
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user