Ground Bean Bro in displayed temperatures, stream replies, and add activity calendar
Test and deploy / test-and-deploy (push) Successful in 1m19s

This commit is contained in:
snowspeeder committed 2026-09-07 06:38:16 -04:00
1 parent a72aec9f86
commit d96ed22de1
16 files changed
+551 -484

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import request from "supertest";
import {setup,signup} from "./helpers.js";
test("activity calendar is authenticated, validates dates, and only links owned events",async()=>{
const {app,agent,db}=await setup();const {csrf}=await signup(agent,"[email protected]");
const other=request.agent(app);await signup(other,"[email protected]");
const owner=(await db.query("SELECT id FROM users WHERE email=$1",["[email protected]"])).rows[0].id;
const privateId=(await db.query("SELECT id FROM users WHERE email=$1",["[email protected]"])).rows[0].id;
const p=(await db.query("INSERT INTO roast_plans(user_id,name,plan,created_at) VALUES($1,'Burundi',$2,'2026-09-05T12:00:00Z') RETURNING id",[owner,{fields:{"0.1":"Burundi"}}])).rows[0];
await db.query("INSERT INTO actual_roasts(user_id,roast_plan_id,filename,original_content,parsed,created_at) VALUES($1,$2,'batch.alog','{}',$3,'2026-10-01')",[owner,p.id,{roast:{title:"Roasted earlier",roastDate:"2026-09-04"}}]);
await db.query("INSERT INTO cupping_sessions(user_id,roast_plan_id,data,created_at) VALUES($1,$2,$3,'2026-09-07')",[owner,p.id,{ritual_started_at_iso:"2026-09-06T12:00:00Z"}]);
await db.query("INSERT INTO brews(user_id,method,brewed_at) VALUES($1,'v60','2026-09-01T02:00:00Z')",[owner]);
await db.query("INSERT INTO brews(user_id,method,brewed_at) VALUES($1,'PRIVATE','2026-09-04')",[privateId]);
const path="/api/activity?month=2026-09&timeZone=America%2FNew_York";
assert.equal((await request(app).get(path)).status,401);
assert.equal((await agent.get("/api/activity?month=2026-13")).status,400);
assert.equal((await agent.get("/api/activity?month=2026-09&timeZone=nope")).status,400);
const result=await agent.get(path);assert.equal(result.status,200);
assert.deepEqual(result.body.events.map(e=>e.kind),["roast","plan","cupping"]);
assert.equal(result.body.events[0].date,"2026-09-04");
assert.equal(result.body.events[0].planHref,"/app?plan="+p.id);
assert.equal((await agent.get("/api/activity?month=2026-08&timeZone=America%2FNew_York")).body.events[0].kind,"brew");
assert.equal((await agent.get("/home")).status,200);assert.equal((await request(app).get("/home")).status,302);
assert.equal((await agent.get("/coffees")).headers.location,"/home");
assert.equal((await agent.get("/")).headers.location,"/home");
});
test("Bean Bro streaming preserves auth, CSRF, final suggestion validation and errors",async()=>{
const {app,agent}=await setup({}, {runPlanChat:async({onText,messages})=>{
onText?.("First crack: 392°F");
if(messages[0].content==="fail")throw new Error("Provider disconnected");
return {reply:"First crack: 392°F, illustrative.",suggestions:[{fields:{"bad":"secret","4.4":"1:20"},reason:"Test",evidenceRoastIds:["private"]}]};
}});
const {csrf}=await signup(agent,"[email protected]");
const body={plan:{fields:{"1.4":"8:45"}},messages:[{role:"user",content:"FC?"}]};
assert.equal((await request(app).post("/api/plan-chat").set("accept","text/event-stream").send(body)).status,401);
assert.equal((await agent.post("/api/plan-chat").set("accept","text/event-stream").send(body)).status,403);
const response=await agent.post("/api/plan-chat").set("x-csrf-token",csrf).set("accept","text/event-stream").send(body);
assert.equal(response.status,200);assert.match(response.headers["content-type"],/text\/event-stream/);
assert.match(response.text,/event: reply/);assert.match(response.text,/392°F/);assert.match(response.text,/event: done/);
assert.ok(!response.text.includes("private"));assert.ok(!response.text.includes("secret"));
body.messages[0].content="fail";
const failed=await agent.post("/api/plan-chat").set("x-csrf-token",csrf).set("accept","text/event-stream").send(body);
assert.match(failed.text,/event: error/);assert.match(failed.text,/Provider disconnected/);assert.ok(!failed.text.includes("event: done"));
});
+36
View File
@@ -0,0 +1,36 @@
import test from "node:test";
import assert from "node:assert/strict";
import {buildPlanChatContext,partialReply} from "../server/plan-chat.js";
import {blankPlan} from "../shared/fields.js";
import {buildPlanCurve} from "../shared/curve.js";
import {computeLedger} from "../shared/ledger.js";
test("Bean Bro sees the displayed 392°F first crack even with blank raw temperatures",()=>{
const p=blankPlan();Object.assign(p.fields,{"1.4":"8:45","3.2":"-0:30","4.3":"1:38","4.1":"light"});
const ctx=buildPlanChatContext(p);
const fc=ctx.displayedMilestones.find(p=>p.event==="fc");
assert.equal(fc.tempF,392);assert.equal(fc.time,"8:45");assert.equal(fc.temperatureSource,"illustrative");
assert.equal(ctx.computedLedger.timingBreakdown.development.processAdjustmentS,-30);
assert.equal(ctx.computedLedger.developmentS,75);
for(const point of buildPlanCurve(p,computeLedger(p))) {
const item=ctx.displayedMilestones.find(i=>i.event===point.key);
assert.equal(item.tempC,point.tempC);assert.equal(item.timeS,point.timeS);
}
});
test("Bean Bro distinguishes entered targets, preheat and actual measurements without machine scaling",()=>{
const p=blankPlan();p.fields["1.4"]="8:45";p.fields["4.4"]="1:20";
p.temps.fc.tempC="195";p.preheat.tempC="175";p.planActual.fc.actualBt="171.111111";p.planActual.fc.actualTime="7:05";
const ctx=buildPlanChatContext(p);
assert.equal(ctx.displayedMilestones.find(p=>p.event==="fc").tempF,383);
assert.equal(ctx.displayedMilestones.find(p=>p.event==="fc").temperatureSource,"entered");
assert.equal(ctx.preheatSetpointF,347);assert.equal(ctx.recordedMilestones.find(p=>p.event==="fc").actualBtF,340);
assert.equal(buildPlanChatContext(blankPlan()).displayedMilestones.some(p=>p.event==="fc"),false);
});
test("streaming JSON decoder exposes only reply text and handles split escapes",()=>{
const text='The "FC" is 392°F.\nUse actual crack.';
const raw=JSON.stringify({reply:text,suggestions:[{fields:{"4.4":"1:20"}}]});
for(let i=0;i<=raw.length;i++) assert.ok(text.startsWith(partialReply(raw.slice(0,i))));
assert.equal(partialReply(raw),text);
assert.equal(partialReply('{"reply":"392\\u00'),"392");
assert.equal(partialReply('{"reply":"392\\u00b0F"}'),"392°F");
});
+71 -17
View File
@@ -613,7 +613,7 @@ test(
},
);
test(
"browser: coffee history compares measured curves and quick tasting persists",
"browser: quick tasting persists and removed coffee history redirects to calendar",
browserOptions,
async (t) => {
const { page, base, create, db, errors } = await session(t);
@@ -666,22 +666,12 @@ test(
"coffee:" + warningPlan().fields["0.1"].toLowerCase(),
),
);
await page.getByRole("heading", { name: "Your favorite batch" }).waitFor();
await page
.getByRole("checkbox", { name: "Compare Batch 1", exact: true })
.check();
await page
.getByRole("checkbox", { name: "Compare Batch 2", exact: true })
.check();
await page
.getByRole("button", { name: "Compare selected batches" })
.click();
await page
.getByRole("img", {
name: "Measured roast curves, Fahrenheit versus time",
})
.waitFor();
assert.equal(await page.locator(".comparison-chart polyline").count(), 2);
await page.getByRole("heading", { name: "Activity calendar", exact:true }).waitFor();
await page.getByRole("heading", { name: "Activity log", exact:true }).waitFor();
assert.equal(new URL(page.url()).pathname,"/home");
await page.goto(base + "/roasts?roast=" + first);
await page.locator('#after-form [name="favorite"]').waitFor();
assert.equal(await page.locator('#after-form [name="favorite"]').inputValue(),"yes");
assert.deepEqual(errors, []);
},
);
@@ -761,3 +751,67 @@ test("browser: deleting a plan confirms in-page, supports retry, and preserves r
assert.deepEqual(remaining.plan_snapshot,plan);
assert.deepEqual(errors,[]);
});
test("browser: Bean Bro displays live text before completion and calendar links to activity",browserOptions,async(t)=>{
let release;
const gate=new Promise(r=>{release=r;});t.after(()=>release());
const {page,base,create,db,errors}=await session(t,{runPlanChat:async({onText})=>{
onText?.("First crack is shown at 392°F");
await gate;
return {reply:"First crack is shown at 392°F as an illustrative reference.",suggestions:[],model:"test:stream"};
}});
const saved=await create(warningPlan());
await page.goto(base+"/app?plan="+saved.id);
await page.locator("#chat-input").fill("What is the planned first crack temperature?");
await page.locator("#chat-send").click();
await page.locator("#chat-thread .assistant").filter({hasText:"First crack is shown at 392°F"}).waitFor();
assert.equal(await page.locator("#chat-send").isDisabled(),true);
assert.equal(await page.locator(".proposal-card").count(),0);
await page.screenshot({path:"/tmp/bean-bro-streaming.png"});
release();
await page.waitForFunction(()=>!document.getElementById("chat-send").disabled);
await page.locator("#chat-thread .assistant").filter({hasText:"illustrative reference"}).waitFor();
const owner=(await db.query("SELECT user_id FROM roast_plans WHERE id=$1",[saved.id])).rows[0].user_id;
const roast=(await db.query("INSERT INTO actual_roasts(user_id,roast_plan_id,filename,original_content,parsed,evaluation_status) VALUES($1,$2,'Burundi.alog','{}',$3,'failed') RETURNING id",[owner,saved.id,{roast:{title:"Burundi · Light & fruity",roastDate:"2026-09-05"},curve:[],derived:{}}])).rows[0];
await db.query("INSERT INTO cupping_sessions(user_id,roast_plan_id,data,created_at) VALUES($1,$2,$3,'2026-09-07')",[owner,saved.id,{}]);
const brew=(await db.query("INSERT INTO brews(user_id,method,brewed_at) VALUES($1,'v60','2026-09-08T12:00:00Z') RETURNING id",[owner])).rows[0];
await page.goto(base+"/home?month=2026-09");
await page.getByRole("heading",{name:"Activity log",exact:true}).waitFor();
assert.equal(await page.getByRole("link",{name:"Coffee history",exact:true}).count(),0);
assert.equal(await page.getByRole("link",{name:"Today",exact:true}).count(),0);
await page.screenshot({path:"/tmp/activity-calendar-desktop.png",fullPage:true,animations:"disabled"});
await page.getByRole("combobox",{name:"Activity type"}).selectOption("roast");
assert.equal(await page.locator(".calendar-agenda .calendar-log-row").count(),1);
await page.getByRole("combobox",{name:"Activity type"}).selectOption("");
await page.setViewportSize({width:390,height:844});
await page.reload();
await page.getByRole("heading",{name:"Activity log",exact:true}).waitFor();
await page.screenshot({path:"/tmp/activity-calendar-mobile.png",fullPage:true});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);
await page.locator('.calendar-agenda a[href="/roasts?roast='+roast.id+'"]').click();
await page.getByRole("heading",{name:"Quick taste",exact:true}).waitFor();
await page.goto(base+"/brews?brew="+brew.id);
await page.getByRole("heading",{name:"Edit brew",exact:true}).waitFor();
assert.deepEqual(errors,[]);
});
test("browser: an interrupted Bean Bro stream keeps partial text, restores the question and allows retry",browserOptions,async(t)=>{
let attempt=0;
const {page,base,create,errors}=await session(t,{runPlanChat:async({onText})=>{
onText?.("First crack is shown at 392°F.");
if(++attempt===1)throw new Error("Provider disconnected");
return {reply:"First crack is shown at 392°F; this is illustrative.",suggestions:[]};
}});
const saved=await create(warningPlan());
await page.goto(base+"/app?plan="+saved.id);
await page.locator("#chat-input").fill("Explain first crack");
await page.locator("#chat-send").click();
await page.locator("#chat-thread").getByText(/Response interrupted/).waitFor();
assert.equal(await page.locator("#chat-input").inputValue(),"Explain first crack");
assert.equal(await page.locator("#chat-send").isDisabled(),false);
assert.match(await page.locator("#chat-thread").innerText(),/392°F/);
await page.locator("#chat-send").click();
await page.locator("#chat-thread .assistant").filter({hasText:"this is illustrative"}).waitFor();
assert.equal(await page.locator("#chat-thread .user").count(),1);
assert.deepEqual(errors,[]);
});
+1 -1
View File
@@ -38,7 +38,7 @@ test("workspace groups own coffee history and surfaces tasting, inventory and ex
assert.ok(result.body.actions.some((a) => a.kind === "taste"));
assert.ok(result.body.actions.some((a) => a.kind === "experiment"));
assert.equal((await request(app).get("/api/workspace")).status, 401);
for (const route of ["/home", "/coffees"]) {
for (const route of ["/home"]) {
assert.equal((await agent.get(route)).status, 200);
assert.equal((await request(app).get(route)).status, 302);
}