Merge branch 'main' into issue/ope-51-ask_user-upgrades

This commit is contained in:
Rohit P
2026-08-01 09:45:03 -07:00
75 changed files with 7547 additions and 191 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ test("run_shell → full card: description title, command preview, stays-on-this
// The mocked proposal has no description → plain "Run a command" title; the command is
// the preview; the reason still renders; the scope note replaces the old badge.
await expect(page.getByText("Run a command").last()).toBeVisible();
await expect(page.getByText("stays on this Mac").last()).toBeVisible();
await expect(page.getByText("stays on this computer").last()).toBeVisible();
await expect(page.getByText("The coworker wants to run a command.").first()).toBeVisible();
await expect(page.getByRole("button", { name: "Always allow this command" }).last()).toBeVisible();
await expect(page.getByText(/local action/)).toHaveCount(0);
+34
View File
@@ -57,3 +57,37 @@ test("approval: Deny skips the tool and the agent says so", async ({ page }) =>
await page.getByRole("button", { name: "Deny" }).last().click();
await expect(page.getByText("Understood — skipped the command.")).toBeVisible();
});
test("long user pastes clamp with a more…/less… toggle", async ({ page }) => {
await page.goto("/");
const box = page.getByPlaceholder(/Ask the coworker/);
await expect(box).toBeVisible();
const tail = "END-OF-PASTE-MARKER";
const paste =
"reply OK. " + "lorem ipsum dolor sit amet consectetur ".repeat(60) + tail; // ~2.4k chars
await box.fill(paste);
await page.getByRole("button", { name: "Send" }).click();
// Clamped: the bubble shows the head but not the tail, plus the toggle.
const more = page.getByRole("button", { name: "more…" });
await expect(more).toBeVisible();
const bubble = page.locator(".bubble-user").last();
await expect(bubble).toContainText("reply OK.");
await expect(bubble).not.toContainText(tail);
// Expand → full text + "less…"; collapse → clamped again.
await more.click();
await expect(bubble).toContainText(tail);
const less = page.getByRole("button", { name: "less…" });
await expect(less).toBeVisible();
await less.click();
await expect(bubble).not.toContainText(tail);
// Short messages never show the control.
await expect(page.getByText("Echo:").first()).toBeVisible();
await box.fill("short follow-up");
await page.getByRole("button", { name: "Send" }).click();
await expect(page.getByText("short follow-up", { exact: true }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "more…" })).toHaveCount(1); // still only the paste's
});
+80
View File
@@ -0,0 +1,80 @@
// OPE-27 — auto-compaction GUI: the Settings card's two overrides + summarizer-model
// pin POST through, and the "context compacted" divider renders inline mid-session
// (driven by the fixtures' scripted `compacted` event) without touching the transcript.
import { expect } from "@playwright/test";
import { test } from "./fixtures";
test("Settings: Context compaction card edits threshold, cap, and summarizer model", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByRole("button", { name: "Models", exact: true }).click();
const card = page.getByTestId("compaction-card");
await expect(card).toBeVisible();
await expect(card.getByText("Context compaction")).toBeVisible();
// Defaults render when the backend doesn't send the fields (older-backend robustness).
await expect(card.getByTestId("compaction-threshold")).toHaveValue("80");
await expect(card.getByTestId("compaction-cap")).toHaveValue("250000");
await expect(card.getByTestId("compaction-model")).toHaveValue("");
// Threshold edits POST as a fraction, clamped to 1095%.
const [req] = await Promise.all([
page.waitForRequest(
(r) => r.url().endsWith("/v1/settings/compaction") && r.method() === "POST",
),
card.getByTestId("compaction-threshold").fill("70"),
]);
expect(req.postDataJSON()).toEqual({ compaction_threshold_pct: 0.7 });
const [req2] = await Promise.all([
page.waitForRequest(
(r) => r.url().endsWith("/v1/settings/compaction") && r.method() === "POST",
),
card.getByTestId("compaction-cap").fill("100000"),
]);
expect(req2.postDataJSON()).toEqual({ compaction_cap_tokens: 100000 });
// Summarizer pin: the picker offers the session-default plus the configured models.
const [req3] = await Promise.all([
page.waitForRequest(
(r) => r.url().endsWith("/v1/settings/compaction") && r.method() === "POST",
),
card.getByTestId("compaction-model").selectOption("gpt-4o-mini"),
]);
expect(req3.postDataJSON()).toEqual({ compaction_model: "gpt-4o-mini" });
});
test("the compacted divider renders mid-session and the transcript stays intact", async ({
page,
}) => {
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
const box = page.getByPlaceholder(/Ask the coworker/);
// An earlier exchange that must survive the compaction marker (transcript intact).
await box.fill("remember the launch date");
await box.press("Enter");
await expect(page.getByText("Echo: remember the launch date").first()).toBeVisible({
timeout: 10_000,
});
await box.fill("compact the context");
await box.press("Enter");
// The transient signal shows while the summarizer runs, then yields to the divider.
await expect(page.getByText("Compacting context…").first()).toBeVisible({
timeout: 10_000,
});
await expect(
page.getByText("Context compacted — earlier turns were summarized").first(),
).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("Compacting context…")).toHaveCount(0);
await expect(
page.getByText("Still on it — continuing where I left off.").first(),
).toBeVisible();
// Outbound-only: everything before the divider is still on screen.
await expect(page.getByText("Echo: remember the launch date").first()).toBeVisible();
});
+100 -3
View File
@@ -158,7 +158,7 @@ const CONNECTORS = {
// MCP-BACKED connectors (§42): vendor-hosted MCP + local OAuth, pinned tool subset.
// monday is one-click ONLY (no manual fields); jira also has a manual token path
// (two-mode modal). Neither needs cloud sign-in.
{ name: "monday", title: "monday.com", icon: "▦", blurb: "Read boards and items, track work, create items and post updates.", aliases: ["project management", "tasks", "boards"], auth: "oauth", two_way: false, channels: false, available: true, brand_color: "#6161ff", logo: "monday", mcp: true, fields: [], instructions: ["One click connects via monday.com sign-in in your browser.", "Sign-in is fully local — tokens stay on this Mac."], connected: false, account: null, enabled: false, allowed_users: [], tools: [{ name: "mcp__monday__get_board_info", label: "Read board", kind: "read", description: "Read a board's columns and groups.", enabled: true, requires_approval: false }, { name: "mcp__monday__create_item", label: "Create item", kind: "write", description: "Create an item on a board.", enabled: true, requires_approval: true }], managed: false, managed_profile: false },
{ name: "monday", title: "monday.com", icon: "▦", blurb: "Read boards and items, track work, create items and post updates.", aliases: ["project management", "tasks", "boards"], auth: "oauth", two_way: false, channels: false, available: true, brand_color: "#6161ff", logo: "monday", mcp: true, fields: [], instructions: ["One click connects via monday.com sign-in in your browser.", "Sign-in is fully local — tokens stay on this computer."], connected: false, account: null, enabled: false, allowed_users: [], tools: [{ name: "mcp__monday__get_board_info", label: "Read board", kind: "read", description: "Read a board's columns and groups.", enabled: true, requires_approval: false }, { name: "mcp__monday__create_item", label: "Create item", kind: "write", description: "Create an item on a board.", enabled: true, requires_approval: true }], managed: false, managed_profile: false },
{ name: "jira", title: "Jira", icon: "◆", blurb: "Search, summarize, create, and update issues.", aliases: ["issues", "tickets", "atlassian"], auth: "api_token", two_way: false, channels: false, available: true, brand_color: "#0052cc", logo: "jira", mcp: true, fields: [{ key: "base_url", label: "Atlassian site URL", secret: false, required: true, help: "", placeholder: "" }, { key: "email", label: "Account email", secret: false, required: true, help: "", placeholder: "" }, { key: "api_token", label: "API token", secret: true, required: true, help: "", placeholder: "" }], instructions: [], connected: false, account: null, enabled: false, allowed_users: [], tools: [], managed: false, managed_profile: false },
],
};
@@ -552,6 +552,14 @@ export async function mockApi(page: import("@playwright/test").Page) {
// Per-session unattended flag — mutable so the composer's "Send to Inbox" toggle persists and
// the app reads it back (which is what gates parking approvals to the Inbox vs an inline card).
const unattended: Record<string, boolean> = {};
// Skills (SKILLS-SPEC) — mutable folder-is-truth mirror: Settings CRUD, the enabled flag,
// staged uploads (stage → preview → confirm), and the composer's per-session menu all
// round-trip through this one list.
const skills: any[] = [
{ name: "weekly-report", description: "Monday status report", instructions: "1. Collect updates\n2. Write it up", scope: "global", source: "local", enabled: true, path: "/state/skills/weekly-report", files: 0 },
{ name: "html-to-markdown", description: "Convert an HTML document or fragment to clean markdown.", instructions: "Convert the given HTML to markdown, preserving structure.", scope: "global", source: "uploaded", enabled: true, path: "/state/skills/html-to-markdown", files: 2 },
];
let stagedSkill: any = null;
// Fresh cloud sign-in state per test (module state outlives a page).
Object.assign(CLOUD_STATE, {
@@ -585,7 +593,12 @@ export async function mockApi(page: import("@playwright/test").Page) {
const msg = JSON.parse(String(raw));
if (msg.type === "user_message") {
hadTurn = true;
send("turn_start", { input: msg.text });
// Force-run (SKILLS-SPEC §6): like the real server, TURN_START ships the user's
// literal "/name …" line as `display` so the client dedupes on what the user sees.
send("turn_start", {
input: msg.text,
...(msg.skill ? { display: `/${msg.skill}${msg.text ? ` ${msg.text}` : ""}` } : {}),
});
if (/run a tool/i.test(msg.text)) {
pendingTool = "run_shell";
send("tool_proposed", { name: "run_shell", arguments: { command: "ls" } });
@@ -681,6 +694,18 @@ export async function mockApi(page: import("@playwright/test").Page) {
}, 120);
return;
}
// Auto-compaction (OPE-27): the server signals `compacting` (the transient
// spinner label), summarizes for a beat, then emits the marker and the turn
// continues normally — the divider must render inline.
if (/compact the context/i.test(msg.text)) {
send("compacting", {});
setTimeout(() => {
send("compacted", { text: "Context compacted — earlier turns were summarized" });
send("assistant_message", { text: "Still on it — continuing where I left off." });
send("turn_done");
}, 400);
return;
}
// A turn that dies on a provider error; the follow-up {type:"retry"} recovers.
if (/fail the turn/i.test(msg.text)) {
send("error", { error: "model unreachable" });
@@ -709,10 +734,11 @@ export async function mockApi(page: import("@playwright/test").Page) {
send("assistant_delta", { text: msg.text });
// Echo the model the message carried — pins the model-per-message contract (the
// composer's visible model must ride on every user_message; 2026-07-04 fix).
// Same for `skill`: the force-run pick must ride as its OWN FIELD, never as text.
// `usage` mirrors the real engine's assistant_message sidecar (OPE-42): fixed
// counts per turn so the usage-chip specs can assert exact accumulation.
send("assistant_message", {
text: `Echo: ${msg.text} [model=${msg.model || "none"}]`,
text: `Echo: ${msg.text} [model=${msg.model || "none"}]${msg.skill ? ` [skill=${msg.skill}]` : ""}`,
usage: {
model: msg.model || "anthropic:claude-opus-4-8",
input: 1_000,
@@ -824,8 +850,79 @@ export async function mockApi(page: import("@playwright/test").Page) {
return json(i >= 0 ? sessions[i] : PINNED_SESSION);
}
// Skills (SKILLS-SPEC §5/§6). Order matters: upload/confirm before the {name} regexes.
if (/\/v1\/sessions\/[^/]+\/skills$/.test(p)) {
// The composer's live menu: every Settings-enabled skill (§4 — disabled = invisible).
return json({
skills: skills
.filter((s) => s.enabled)
.map((s) => ({ name: s.name, description: s.description, scope: s.scope, enabled: true })),
});
}
if (p.endsWith("/v1/skills/upload/confirm") && m === "POST") {
const b = req.postDataJSON() || {};
if (!stagedSkill || b.token !== stagedSkill.token)
return json({ ok: false, error: "Upload expired — pick the file again." });
skills.push({
name: stagedSkill.name, description: stagedSkill.description,
instructions: stagedSkill.instructions, scope: "global", source: "uploaded",
enabled: true, path: `/state/skills/${stagedSkill.name}`, files: stagedSkill.files.length,
});
stagedSkill = null;
return json({ ok: true });
}
if (p.endsWith("/v1/skills/upload") && m === "POST") {
// Stage → preview; nothing lands until confirm. Fixed parse (the mock reads no zips).
stagedSkill = {
token: "stage-1", name: "greet", description: "says hello",
instructions: "Say hello warmly.", files: ["notes.txt"],
};
return json({ ok: true, ...stagedSkill });
}
{
const mr = p.match(/\/v1\/skills\/([^/]+)\/reveal$/);
if (mr && m === "POST") {
return json(
skills.some((s) => s.name === decodeURIComponent(mr[1]))
? { ok: true }
: { ok: false, error: `Unknown skill: ${decodeURIComponent(mr[1])}` },
);
}
}
if (/\/v1\/skills\/[^/]+$/.test(p) && (m === "PATCH" || m === "DELETE")) {
const name = decodeURIComponent(p.split("/").pop()!);
const i = skills.findIndex((s) => s.name === name);
if (i < 0) return json({ ok: false, error: `Unknown skill: ${name}` });
if (m === "DELETE") {
skills.splice(i, 1);
return json({ ok: true });
}
const b = req.postDataJSON() || {};
if (typeof b.enabled === "boolean") skills[i].enabled = b.enabled;
if (typeof b.description === "string") skills[i].description = b.description;
if (typeof b.instructions === "string") skills[i].instructions = b.instructions;
return json({ ok: true });
}
if (p.endsWith("/v1/skills") && m === "POST") {
const b = req.postDataJSON() || {};
if (!b.name || !(b.instructions || "").trim())
return json({ ok: false, error: "Skill name and instructions are required." });
if (skills.some((s) => s.name === b.name))
return json({ ok: false, error: `A skill named '${b.name}' already exists in that scope.` });
skills.push({
name: b.name, description: b.description || "", instructions: b.instructions,
scope: "global", source: "local", enabled: true, path: `/state/skills/${b.name}`, files: 0,
});
return json({ ok: true });
}
if (p.endsWith("/v1/skills")) return json({ skills });
if (p.endsWith("/v1/health")) return json(HEALTH);
if (p.endsWith("/v1/settings")) return json(SETTINGS);
if (p.endsWith("/v1/settings/context-bar") && m === "POST") {
Object.assign(SETTINGS, req.postDataJSON());
return json({ ok: true, context_bar: SETTINGS.context_bar });
}
if (p.endsWith("/v1/settings/pdf") && m === "POST") {
Object.assign(SETTINGS, req.postDataJSON());
return json({
+30
View File
@@ -0,0 +1,30 @@
import { test, expect } from "./fixtures";
// SKILLS-SPEC §9 journey 4 — the "/" force-run: popup pick inserts the inline `/name `
// prefix, the send carries the skill as its OWN WebSocket field (never as message text),
// and the transcript shows ONE truthful bubble with exactly what the user typed.
test("skills-forcerun: popup pick → inline /name → skill rides the frame → one bubble", async ({ page }) => {
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
// "/" opens the popup; picking inserts the inline prefix (no chip) and keeps focus.
const box = page.getByPlaceholder(/Ask the coworker/);
await box.fill("/");
await expect(page.getByTestId("skill-popup")).toBeVisible();
await page.getByText("/weekly-report").click();
await expect(box).toHaveValue("/weekly-report ");
await box.type("cover last week");
await box.press("Enter");
// ONE user bubble, showing the literal line the user typed — never the model-facing
// "load this skill…" framing (§6: the _display contract).
await expect(page.getByText("/weekly-report cover last week")).toHaveCount(1);
await expect(page.getByText(/Use the skill/)).toHaveCount(0);
// The fake agent echoes what actually rode the wire: text WITHOUT the prefix, and the
// skill as its own field.
await expect(page.getByText(/\[skill=weekly-report\]/)).toBeVisible();
await expect(page.getByText(/Echo: cover last week/)).toBeVisible();
});
+39
View File
@@ -0,0 +1,39 @@
import { test, expect } from "./fixtures";
// SKILLS-SPEC §9 journey 2 — liveness from the session's seat: the composer's "/" popup is
// the live "what can my worker use right now" view. A skill created in Settings is offered;
// a disabled one vanishes. Hermetic: the popup reads /v1/sessions/{id}/skills from fixtures.
test("skills-session: new skill offered in '/', disabled one absent", async ({ page }) => {
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
// The seeded menu: both enabled skills offered on "/".
const box = page.getByPlaceholder(/Ask the coworker/);
await box.fill("/");
await expect(page.getByTestId("skill-popup")).toBeVisible();
await expect(page.getByText("/weekly-report")).toBeVisible();
await expect(page.getByText("/html-to-markdown")).toBeVisible();
await box.fill(""); // close the popup
// Settings round-trip: create one skill, disable another.
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByRole("button", { name: "Skills", exact: true }).click();
await page.getByRole("button", { name: /Add skill/ }).click();
await page.getByText("Write it myself").click();
await page.getByLabel("Name").fill("fresh-skill");
await page.getByLabel("Instructions").fill("Do the fresh thing.");
await page.getByRole("button", { name: "Save skill" }).click();
await expect(page.getByRole("status")).toContainText("fresh-skill");
await page.getByLabel("weekly-report enabled").click();
await expect(page.getByRole("status")).toContainText("turned off everywhere");
// Back in the session: the popup reflects the new state — created offered, disabled gone.
await page.getByText("Draft the launch note").first().click();
await box.fill("/");
await expect(page.getByTestId("skill-popup")).toBeVisible();
await expect(page.getByText("/fresh-skill")).toBeVisible();
await expect(page.getByText("/weekly-report")).toHaveCount(0);
await expect(page.getByText("/html-to-markdown")).toBeVisible(); // untouched one persists
});
+65
View File
@@ -0,0 +1,65 @@
import { test, expect } from "./fixtures";
// SKILLS-SPEC §9 journey 1 — Settings ▸ Skills as the management home: create through the
// Add-skill menu, edit in place, disable with the amber clean-slate banner, and the
// rich-skill folder chip. Hermetic: every /v1 call lands in fixtures.ts.
const openSkills = async (page: import("@playwright/test").Page) => {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByRole("button", { name: "Skills", exact: true }).click();
};
test("skills-settings: create via the menu → name-first banner; edit persists", async ({ page }) => {
await openSkills(page);
// The seeded rows render; the rich one wears its folder chip; the list is the page
// (no standing add-surfaces).
await expect(page.getByText("weekly-report")).toBeVisible();
await expect(page.getByText("uploaded")).toBeVisible();
await expect(page.getByTitle("Show folder")).toContainText("2 files");
await expect(page.getByText("Start a conversation")).toHaveCount(0);
// Add skill ▾ → the three doors, then Write it myself.
await page.getByRole("button", { name: /Add skill/ }).click();
await expect(page.getByText("Import a file")).toBeVisible();
await expect(page.getByText("Create with OpenWorker")).toBeVisible();
await page.getByText("Write it myself").click();
await page.getByLabel("Name").fill("greet-warmly");
await page.getByLabel("Description").fill("Greets people warmly");
await page.getByLabel("Instructions").fill("Always greet warmly.");
await page.getByRole("button", { name: "Save skill" }).click();
// Name-first teal confirmation (§7) + the new row.
const status = page.getByRole("status");
await expect(status).toContainText("greet-warmly");
await expect(status).toContainText("can now use it in every conversation");
await expect(page.getByText("Greets people warmly")).toBeVisible();
// Edit: pencil prefills, name locked, save PATCHes through to the re-fetched list.
await page.getByTitle("Edit").first().click();
const name = page.getByLabel("Name");
await expect(name).toBeDisabled();
await page.getByLabel("Description").fill("Monday status report, sharper");
await page.getByRole("button", { name: "Save skill" }).click();
await expect(page.getByText("Monday status report, sharper")).toBeVisible();
});
test("skills-settings: disable → amber everywhere/clean-slate banner; delete is two-step", async ({ page }) => {
await openSkills(page);
await page.getByLabel("weekly-report enabled").click();
const status = page.getByRole("status");
await expect(status).toContainText("weekly-report");
await expect(status).toContainText("turned off everywhere");
await expect(status).toContainText("start a new one for a completely clean slate");
// Two-step delete: arm, confirm, row gone, banner names the skill.
await page.getByLabel("Delete html-to-markdown").click();
await expect(page.getByText("html-to-markdown")).toBeVisible(); // armed ≠ deleted
await page.getByText("Confirm delete").click();
await expect(page.getByText("html-to-markdown")).toHaveCount(1); // only the banner remains
await expect(page.getByRole("status")).toContainText("removed");
});
+37
View File
@@ -0,0 +1,37 @@
import { test, expect } from "./fixtures";
// SKILLS-SPEC §9 journey 3 — import with the mandatory review gate: the preview installs
// NOTHING; confirm installs and the row wears the `uploaded` provenance badge. Hermetic:
// stage/confirm round-trip through fixtures.ts state.
test("skills-upload: preview installs nothing → confirm → uploaded badge", async ({ page }) => {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByRole("button", { name: "Skills", exact: true }).click();
// Add skill ▾ → Import a file → straight to the (hidden) picker.
await page.getByRole("button", { name: /Add skill/ }).click();
await page.getByText("Import a file").click();
await page.getByLabel("Upload a skill archive").setInputFiles({
name: "greet.zip",
mimeType: "application/zip",
buffer: Buffer.from("PKfake"),
});
// The mandatory review screen: everything parsed, nothing installed yet.
await expect(page.getByText("Review before installing")).toBeVisible();
await expect(page.getByText("says hello")).toBeVisible();
await expect(page.getByText("Say hello warmly.")).toBeVisible();
await expect(page.getByText(/notes\.txt/)).toBeVisible();
await expect(page.getByText("greet", { exact: true })).toHaveCount(1); // preview only, no row
await page.getByRole("button", { name: "Install skill" }).click();
// Installed: teal name-first banner, a real row with the provenance badge + folder chip.
const status = page.getByRole("status");
await expect(status).toContainText("greet");
await expect(status).toContainText("can now use it in every conversation");
await expect(page.getByText("greet", { exact: true })).toHaveCount(2); // banner + the new row
await expect(page.getByText("uploaded")).toHaveCount(2); // html-to-markdown + greet
});
+35 -2
View File
@@ -20,7 +20,8 @@ test("usage chip appears after a turn and opens the breakdown popover", async ({
timeout: 10_000,
});
// Chip shows the session total (1k + 200 + 8k + 800 = 10k).
// Default: no bar (owner ask 2026-07-30) — the chip states the session total
// (1k + 200 + 8k + 800 = 10k). The bar is opt-in via Settings.
const chip = page.getByTestId("usage-chip");
await expect(chip).toContainText("10k");
@@ -58,9 +59,41 @@ test("usage resets on a new session", async ({ page }) => {
const box = page.getByPlaceholder(/Ask the coworker/);
await box.fill("hello");
await box.press("Enter");
await expect(page.getByTestId("usage-chip")).toContainText("10k", { timeout: 10_000 });
await expect(page.getByTestId("usage-chip")).toBeVisible({ timeout: 10_000 });
// " New session" wipes the transcript — and the usage accumulation with it.
await page.getByRole("button", { name: /New session/ }).first().click();
await expect(page.getByTestId("usage-chip")).toHaveCount(0);
});
test("Settings toggle turns the context bar on; default is the session total", async ({ page }) => {
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
const box = page.getByPlaceholder(/Ask the coworker/);
await box.fill("hello");
await box.press("Enter");
const chip = page.getByTestId("usage-chip");
await expect(chip).toContainText("10k", { timeout: 10_000 }); // default: total, no bar
// Turn the bar ON in Settings -> General.
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await expect(page.getByTestId("context-bar-toggle")).not.toBeChecked();
const [req] = await Promise.all([
page.waitForRequest(
(r) => r.url().endsWith("/v1/settings/context-bar") && r.method() === "POST",
),
page.getByTestId("context-bar-toggle").check(),
]);
expect(req.postDataJSON()).toEqual({ context_bar: true });
// Reload so the app re-reads settings: the chip is now the fill bar, not a number.
await page.goto("/");
await page.getByText("Draft the launch note").first().click();
await page.getByPlaceholder(/Ask the coworker/).fill("hello");
await page.getByPlaceholder(/Ask the coworker/).press("Enter");
const bar = page.getByTestId("usage-chip");
await expect(bar).toBeVisible({ timeout: 10_000 });
await expect(bar).not.toContainText("10k");
await expect(bar).toHaveAttribute("title", /Context window 5% full/);
});