Ground Bean Bro in displayed temperatures, stream replies, and add activity calendar
Test and deploy / test-and-deploy (push) Successful in 1m19s
Test and deploy / test-and-deploy (push) Successful in 1m19s
This commit is contained in:
1 parent
a72aec9f86
commit
d96ed22de1
16 files changed
+551
-484
No files matched your search
@@ -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"));
|
||||
});
|
||||
@@ -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
@@ -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,[]);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user