Files
roast_command_center/test/cupping.test.js
T
snowspeederandClaude Fable 5 385474ecef
Test and deploy / test-and-deploy (push) Successful in 1m8s
Removable reference curves, audit fixes across pages, shaded Academy art
The planner's Artisan .alog drawer now shows what's attached with a
"Remove reference curve" control (re-rendered per drawer open), so adding
a reference curve is no longer a one-way door; /api/alog shares the 8 MB
body cap so real-sized logs parse instead of failing with "bad_request".

Deep-evaluation fixes: editing a brew of an archived bean no longer
silently detaches the bean; the roasts pending-review poll no longer
wipes in-progress after-roast edits; roasters gain an Edit (rename/model)
action; the gear page refuses to autosave over a failed load; duplicating
a plan carries its custom name; cupping sessions can attach a plan after
creation (ownership-checked PUT + selector); admin user deletion also
refreshes plans/audit; cupping cup-count subtitle stays live; roasts
error-row colspan corrected. Regression tests cover the new cupping PUT
and the /api/alog body cap.

Academy scenes drop the flat paper-cutout look: shared defs provide
radial-gradient shading on every bean/half-bean/particle, flame gradients
with radiant halos, soft ground shadows, and a warm-lit stage background;
fill-shift animations now ride a partial-opacity tint overlay so shading
survives the color change.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-11 16:59:28 -04:00

263 lines
9.7 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import { setup, signup } from "./helpers.js";
import { computeTotalScore, SCORE_ATTRS, TICK_ATTRS } from "../shared/cupping.js";
async function bootstrapPlan(agent, csrf) {
const r = await agent
.post("/api/plans")
.set("x-csrf-token", csrf)
.send({ plan: { fields: { 0.1: "Test coffee" } } });
return r.body.plan.id;
}
test("cupping: auth and CSRF are required on every route", async () => {
const { app } = await setup();
const anon = request.agent(app);
assert.equal((await anon.get("/api/cupping")).status, 401);
assert.equal((await anon.post("/api/cupping").send({})).status, 401);
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
assert.equal((await agent.post("/api/cupping").send({})).status, 403);
const created = await agent.post("/api/cupping").set("x-csrf-token", csrf).send({});
assert.equal(created.status, 201);
assert.equal(
(await agent.put(`/api/cupping/${created.body.session.id}`).send({ data: {} })).status,
403,
);
assert.equal((await agent.delete(`/api/cupping/${created.body.session.id}`)).status, 403);
});
test("cupping: create (with and without a linked plan), list, and ownership isolation", async () => {
const { app } = await setup();
const first = request.agent(app);
const second = request.agent(app);
const { csrf: firstCsrf } = await signup(first, "[email protected]");
await signup(second, "[email protected]");
const planId = await bootstrapPlan(first, firstCsrf);
const linked = await first
.post("/api/cupping")
.set("x-csrf-token", firstCsrf)
.send({ roastPlanId: planId, cupCount: 4 });
assert.equal(linked.status, 201);
assert.equal(linked.body.session.roastPlanId, planId);
assert.equal(linked.body.session.data.cup_count, 4);
assert.equal(linked.body.session.totalScore, 0);
const unlinked = await first.post("/api/cupping").set("x-csrf-token", firstCsrf).send({});
assert.equal(unlinked.status, 201);
assert.equal(unlinked.body.session.roastPlanId, null);
assert.equal((await first.get("/api/cupping")).body.sessions.length, 2);
assert.equal((await second.get("/api/cupping")).body.sessions.length, 0);
assert.equal((await second.get(`/api/cupping/${linked.body.session.id}`)).status, 404);
const filtered = await first.get(`/api/cupping?plan=${planId}`);
assert.equal(filtered.body.sessions.length, 1);
assert.equal(filtered.body.sessions[0].id, linked.body.session.id);
});
test("cupping: creating against another user's plan is refused", async () => {
const { app } = await setup();
const owner = request.agent(app);
const attacker = request.agent(app);
const { csrf: ownerCsrf } = await signup(owner, "[email protected]");
const { csrf: attackerCsrf } = await signup(attacker, "[email protected]");
const planId = await bootstrapPlan(owner, ownerCsrf);
const attempt = await attacker
.post("/api/cupping")
.set("x-csrf-token", attackerCsrf)
.send({ roastPlanId: planId });
assert.equal(attempt.status, 404);
});
test("cupping: server always recomputes the total score and ignores a client-supplied value", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
const session = (
await agent.post("/api/cupping").set("x-csrf-token", csrf).send({ cupCount: 5 })
).body.session;
const scores = Object.fromEntries(SCORE_ATTRS.map((a) => [a, 8]));
const ticks = Object.fromEntries(TICK_ATTRS.map((a) => [a, 5]));
const expected = computeTotalScore(scores, ticks, 1, 1, 5);
const saved = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({
data: {
cup_count: 5,
scores,
ticks,
taint_cups: 1,
fault_cups: 1,
flavor_tags: ["fruity.berry.blueberry"],
notes: "Bright and clean.",
total_score: 999999, // must be discarded — the server recomputes it
},
});
assert.equal(saved.status, 200);
assert.equal(saved.body.session.totalScore, expected);
assert.notEqual(expected, 999999);
const reloaded = await agent.get(`/api/cupping/${session.id}`);
assert.equal(reloaded.body.session.totalScore, expected);
assert.deepEqual(reloaded.body.session.data.flavor_tags, ["fruity.berry.blueberry"]);
});
test("cupping: coerceSession validation errors surface as 400s", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
const session = (await agent.post("/api/cupping").set("x-csrf-token", csrf).send({})).body
.session;
const badFlavor = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: { flavor_tags: ["Not A Valid Id!"] } });
assert.equal(badFlavor.status, 400);
assert.equal(badFlavor.body.code, "bad_session");
const dedupedTags = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: { flavor_tags: Array.from({ length: 33 }, () => "other.chemical.rubber") } });
// 33 identical tags dedupe to 1, so this specific input should NOT throw — verifies dedup
// happens before the >32 check.
assert.equal(dedupedTags.status, 200);
assert.deepEqual(dedupedTags.body.session.data.flavor_tags, ["other.chemical.rubber"]);
// Build 33 genuinely distinct valid-shaped ids by walking the real taxonomy, to trip the
// actual >32-after-dedup limit.
const { FLAVOR_TAXONOMY } = await import("../shared/cupping.js");
const manyValid = [];
for (const [famId, fam] of Object.entries(FLAVOR_TAXONOMY)) {
for (const [subId, descriptors] of Object.entries(fam.subgroups)) {
for (const d of descriptors) manyValid.push(`${famId}.${subId}.${d}`);
}
}
assert.ok(manyValid.length > 32);
const overLimit = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: { flavor_tags: manyValid } });
assert.equal(overLimit.status, 400);
const badStage = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: { stage_marks: [{ stage: "not_a_real_stage", elapsed_sec: 1 }] } });
assert.equal(badStage.status, 400);
const notObject = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: "nope" });
assert.equal(notObject.status, 400);
});
test("cupping: scores clamp/snap and ticks clamp to cup count", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
const session = (
await agent.post("/api/cupping").set("x-csrf-token", csrf).send({ cupCount: 3 })
).body.session;
const saved = await agent
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({
data: {
cup_count: 3,
scores: { flavor: 11, acidity: 7.13, body: -1 },
ticks: { uniformity: 99 },
},
});
assert.equal(saved.status, 200);
assert.equal(saved.body.session.data.scores.flavor, 10);
assert.equal(saved.body.session.data.scores.acidity, 7.25);
assert.equal(saved.body.session.data.scores.body, 0);
assert.equal(saved.body.session.data.ticks.uniformity, 3);
});
test("cupping: delete removes the session, and deleting the linked plan nulls roastPlanId instead of deleting the session", async () => {
const { app } = await setup();
const agent = request.agent(app);
const { csrf } = await signup(agent, "[email protected]");
const planId = await bootstrapPlan(agent, csrf);
const session = (
await agent
.post("/api/cupping")
.set("x-csrf-token", csrf)
.send({ roastPlanId: planId })
).body.session;
await agent.delete(`/api/plans/${planId}`).set("x-csrf-token", csrf);
const afterPlanDelete = await agent.get(`/api/cupping/${session.id}`);
assert.equal(afterPlanDelete.status, 200);
assert.equal(afterPlanDelete.body.session.roastPlanId, null);
assert.equal(
(await agent.delete(`/api/cupping/${session.id}`).set("x-csrf-token", csrf)).status,
200,
);
assert.equal((await agent.get(`/api/cupping/${session.id}`)).status, 404);
});
test("cupping: a plan can be attached after creation, with ownership enforced", async () => {
const { app } = await setup();
const owner = request.agent(app);
const attacker = request.agent(app);
const { csrf } = await signup(owner, "[email protected]");
const { csrf: attackerCsrf } = await signup(attacker, "[email protected]");
const planId = await bootstrapPlan(owner, csrf);
const foreignPlanId = await bootstrapPlan(attacker, attackerCsrf);
const session = (
await owner.post("/api/cupping").set("x-csrf-token", csrf).send({ cupCount: 3 })
).body.session;
assert.equal(session.roastPlanId, null);
// A PUT without roastPlanId leaves the link untouched
const noTouch = await owner
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: session.data });
assert.equal(noTouch.status, 200);
assert.equal(noTouch.body.session.roastPlanId, null);
// Attaching my own plan works
const linked = await owner
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: session.data, roastPlanId: planId });
assert.equal(linked.status, 200);
assert.equal(linked.body.session.roastPlanId, planId);
// Someone else's plan is refused and the link stays intact
const cross = await owner
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: session.data, roastPlanId: foreignPlanId });
assert.equal(cross.status, 404);
assert.equal(
(await owner.get(`/api/cupping/${session.id}`)).body.session.roastPlanId,
planId,
);
// Explicit null detaches
const detached = await owner
.put(`/api/cupping/${session.id}`)
.set("x-csrf-token", csrf)
.send({ data: session.data, roastPlanId: null });
assert.equal(detached.status, 200);
assert.equal(detached.body.session.roastPlanId, null);
});