S3.8: office people show real agent state: running, queued, cooling down, blocked or idle, from sessions, task holds and pending approvals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 13:51:04 -04:00
1 parent 202418f81c
commit ccd698602e
15 files changed
+164 -69

No files matched your search

+2 -1
View File
@@ -154,7 +154,8 @@ test("the list width, office people and chart values all work from the keyboard
await page.getByRole("button", { name: "Office", exact: true }).click();
await expect(page.getByRole("img", { name: /People here: 1./ })).toBeVisible();
const people = page.getByRole("combobox", { name: "Show a person" });
await people.selectOption({ label: "CEO, Idle" });
// The only person is the CEO, listed after the empty choice with whatever state their real work gives them.
await people.selectOption({ index: 1 });
await expect(page.getByRole("dialog", { name: "Agent CEO" })).toBeVisible();
await people.selectOption({ label: "Show a person…" });
await expect(page.getByRole("dialog", { name: "Agent CEO" })).toHaveCount(0);
+16
View File
@@ -3,6 +3,7 @@ import { mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { buildFloorPlan } from "../../src/renderer/src/pages/office/floor-plan";
import { cameraFor } from "../../src/renderer/src/pages/office/simulation";
import { isoFrame, isoPixelSize, toIso } from "../../src/renderer/src/pages/office/iso";
@@ -78,6 +79,21 @@ test("the office floor opens agent details on click", async () => {
await page.keyboard.press("Escape");
await expect(card).not.toBeVisible();
// The office shows real state: a blocked task makes its agent blocked, with the reason on the card.
const db = new DatabaseSync(join(profileDir, "agentcompany.db"));
const blocked = db.prepare("update tasks set blocked_reason = 'needs the staging key' where assignee_agent_id = (select id from agents where name = 'CEO') and status <> 'done'").run();
db.close();
expect(blocked.changes).toBeGreaterThan(0);
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed", version: Number.MAX_SAFE_INTEGER });
});
await expect(page.getByLabel("Show a person").locator("option", { hasText: "CEO, Blocked" })).toHaveCount(1);
await page.getByLabel("Show a person").selectOption({ label: "CEO, Blocked" });
await expect(card.getByText("Blocked: needs the staging key")).toBeVisible();
await expect(card.getByText("Waiting on you.")).toBeVisible();
await page.keyboard.press("Escape");
await expect(card).not.toBeVisible();
// Clicking empty corridor floor selects nothing.
const corridor = onScreen(plan, box!, ratio, { x: 500, y: 320 }, 0);
await page.mouse.click(corridor.x, corridor.y);
+35
View File
@@ -0,0 +1,35 @@
import { expect, test } from "vitest";
import { officeStatus } from "../../src/renderer/src/pages/office/agent-state";
import type { TaskHold } from "../../src/shared/domain";
const blocked: TaskHold = { reason: "Blocked: needs the staging key", waitingOn: "operator", resumesAt: null };
const cooling: TaskHold = { reason: "Provider is cooling down", waitingOn: "provider", resumesAt: "2026-10-09T14:00:00.000Z" };
const task = (hold: TaskHold | null, status = "in_progress", assigneeAgentId: string | null = "agt_a", id = "tsk_1") => ({ id, assigneeAgentId, status, hold });
const pending = (taskId: string, state = "pending") => ({ sourceEntityType: "task", sourceEntityId: taskId, state });
const none = { runs: [], tasks: [], approvals: [] };
test("each state comes from real records", () => {
expect(officeStatus("agt_a", none)).toEqual({ state: "idle", hold: null });
expect(officeStatus("agt_a", { ...none, runs: [{ agentId: "agt_a", state: "queued" }] }).state).toBe("queued");
expect(officeStatus("agt_a", { ...none, runs: [{ agentId: "agt_a", state: "running" }] }).state).toBe("running");
expect(officeStatus("agt_a", { ...none, tasks: [task(cooling)] })).toEqual({ state: "cooldown", hold: cooling });
expect(officeStatus("agt_a", { ...none, tasks: [task(blocked)] })).toEqual({ state: "blocked", hold: blocked });
// A pending approval on the agent's open task blocks the agent before any hold is written.
expect(officeStatus("agt_a", { ...none, tasks: [task(null)], approvals: [pending("tsk_1")] })).toEqual({ state: "blocked", hold: { reason: "Waiting for your approval.", waitingOn: "operator", resumesAt: null } });
});
test("running beats blocked, blocked beats cooldown, cooldown beats queued", () => {
const queued = { agentId: "agt_a", state: "queued" };
expect(officeStatus("agt_a", { ...none, runs: [{ agentId: "agt_a", state: "running" }], tasks: [task(blocked)] }).state).toBe("running");
expect(officeStatus("agt_a", { ...none, runs: [queued], tasks: [task(cooling), task(blocked)] }).state).toBe("blocked");
expect(officeStatus("agt_a", { ...none, runs: [queued], tasks: [task(cooling)], approvals: [pending("tsk_1")] }).state).toBe("blocked");
expect(officeStatus("agt_a", { ...none, runs: [queued], tasks: [task(cooling)] }).state).toBe("cooldown");
});
test("only this agent's open work counts", () => {
expect(officeStatus("agt_a", { ...none, runs: [{ agentId: "agt_b", state: "running" }] }).state).toBe("idle");
expect(officeStatus("agt_a", { ...none, tasks: [task(blocked, "in_progress", "agt_b")] }).state).toBe("idle");
expect(officeStatus("agt_a", { ...none, tasks: [task(blocked, "done")] }).state).toBe("idle");
expect(officeStatus("agt_a", { ...none, tasks: [task(null, "in_progress", "agt_b")], approvals: [pending("tsk_1")] }).state).toBe("idle");
expect(officeStatus("agt_a", { ...none, tasks: [task(null)], approvals: [pending("tsk_1", "approved")] }).state).toBe("idle");
});
+1 -1
View File
@@ -84,7 +84,7 @@ describe("office depth order", () => {
test("every piece of the scene sorts by a footprint no longer than 32 on either side, so a person beside a long block is never painted over", () => {
const plan = buildFloorPlan(12);
const items = sceneDrawables(isoFrame(plan.world), plan, "winter", new Set(), 0);
const items = sceneDrawables(isoFrame(plan.world), plan, "winter", new Map(), 0);
const oversized = items.filter((item) => item.foot.w > 32 || item.foot.h > 32).map((item) => item.foot);
expect(oversized).toEqual([]);
// A person at the left basin sorts after both the counter piece and the basin in front of them.
+9 -8
View File
@@ -1,10 +1,11 @@
import { describe, expect, test } from "vitest";
import { OfficeSimulation, cameraFor, type MoveInput } from "../../src/renderer/src/pages/office/simulation";
import type { AgentActivity, OfficeAgentInput } from "../../src/renderer/src/pages/office/office-types";
import type { OfficeAgentInput } from "../../src/renderer/src/pages/office/office-types";
import type { OfficeState } from "../../src/renderer/src/pages/office/agent-state";
const IDLE: MoveInput = { up: false, down: false, left: false, right: false };
function agentInput(id: string, roleLevel = "staff", activity: AgentActivity = "idle"): OfficeAgentInput {
function agentInput(id: string, roleLevel = "staff", state: OfficeState = "idle"): OfficeAgentInput {
return {
id,
name: `Agent ${id}`,
@@ -12,7 +13,7 @@ function agentInput(id: string, roleLevel = "staff", activity: AgentActivity = "
title: "Engineer",
department: "Engineering",
roleLevel,
activity,
status: { state, hold: null },
taskTitle: null,
};
}
@@ -121,10 +122,10 @@ describe("office simulation", () => {
expect(agent.carrying).toBe("coffee");
});
test("agents with a run, thinking or busy never leave their desk", () => {
for (const activity of ["active", "thinking", "busy"] as const) {
test("agents running, queued, cooling down or blocked never leave their desk", () => {
for (const state of ["running", "queued", "cooldown", "blocked"] as const) {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("ceo", "c_suite", activity), agentInput("a", "staff", activity)]);
sim.sync([agentInput("ceo", "c_suite", state), agentInput("a", "staff", state)]);
for (let i = 0; i < 6000; i++) {
sim.step(16, IDLE);
for (const agent of sim.agents) expect(agent.behavior.kind).toBe("seated");
@@ -144,7 +145,7 @@ describe("office simulation", () => {
sim.sync([agentInput("a")]);
expect(sim.agents[0].behavior.kind === "walking" && sim.agents[0].behavior.destination).toBe("amenity");
sim.sync([agentInput("a", "staff", "active")]);
sim.sync([agentInput("a", "staff", "running")]);
const working = sim.agents[0];
expect(working.behavior.kind === "walking" && working.behavior.destination).toBe("seat");
for (let i = 0; i < 6000 && working.behavior.kind !== "seated"; i++) {
@@ -157,7 +158,7 @@ describe("office simulation", () => {
test("only idle agents gather around the table, and anyone with a run stays at their desk", () => {
const sim = new OfficeSimulation(() => 0);
sim.sync([agentInput("a"), agentInput("b"), agentInput("ceo", "c_suite", "active")]);
sim.sync([agentInput("a"), agentInput("b"), agentInput("ceo", "c_suite", "running")]);
// Nobody has settled yet: a meeting cannot be convened.
expect(sim.conveneMeeting()).toBe(0);
for (const agent of sim.agents) agent.settledMs = 30_000;