Files
agentic-mobile-control/cloud-console/src/taskProgress.test.ts
T
q792602257andClaude Opus 4.6 ec261d57c2 feat: surface task execution progress across Host Agent and Cloud
Host Agent now persists step-level execution detail locally (via a real
TaskMetadataStore/Timeline wired into TaskRunner) and reports a bounded
in-progress snapshot piggybacked on lease renewal. Cloud persists that
snapshot per active assignment and exposes it through the existing task
list/detail query path; Cloud Console renders it as a live badge. Host
Agent's local console gains authenticated, read-only task list and
detail/timeline pages (same-origin, server-rendered) with inlined
screenshots.

Also fixes a pre-existing gap in the shared Timeline: the actual
per-step LLM prompt is now recorded instead of the task goal, benefiting
both Runtime and Host Agent consoles. When a host uses the cloud planner
transport, each decide call's prompt and resulting tool decision are
durably logged in a new planner_decision_log table (with bounded
retention) and browsable from Cloud Console; direct-transport hosts
explicitly surface a "not reported" state.

Includes Alembic migrations 0008 (progress columns on scheduled_tasks)
and 0009 (planner_decision_log), bounded Host-Agent-local retention,
dual-backend repository parity, and Vitest + pytest coverage. Task 6.5
(manual end-to-end device verification) remains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 12:47:49 +08:00

132 lines
4.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { formatTaskProgress } from "./taskProgress";
import type { TaskListItem } from "./types";
function makeTask(
overrides: Partial<TaskListItem> & Pick<TaskListItem, "status">,
): TaskListItem {
return {
id: "task-a",
goal: null,
workflow_definition_id: null,
assigned_device_id: null,
assigned_host_id: null,
attempt_count: 0,
failure_reason: null,
target_host_id: null,
target_device_id: null,
created_at: "2026-07-14T00:00:00+00:00",
progress_step_index: null,
progress_step_status: null,
progress_summary: null,
progress_updated_at: null,
...overrides,
};
}
describe("formatTaskProgress", () => {
it("renders step index, status, and summary for an in-progress dispatched task", () => {
const task = makeTask({
status: "dispatched",
progress_step_index: 2,
progress_step_status: "running",
progress_summary: "tapping button",
});
expect(formatTaskProgress(task)).toBe("step 2 \u2014 running: tapping button");
});
it("renders step index and status when summary is missing", () => {
const task = makeTask({
status: "assigned",
progress_step_index: 0,
progress_step_status: "starting",
progress_summary: null,
});
expect(formatTaskProgress(task)).toBe("step 0 \u2014 starting");
});
it("returns null when the task is dispatched but no progress has been reported", () => {
const task = makeTask({
status: "dispatched",
progress_step_index: null,
progress_step_status: null,
progress_summary: null,
});
expect(formatTaskProgress(task)).toBeNull();
});
it("returns null for a dispatched task with undefined progress fields", () => {
const task = makeTask({ status: "dispatched" });
// Fields default to null via makeTask; simulate the undefined case.
delete (task as Partial<TaskListItem>).progress_step_index;
delete (task as Partial<TaskListItem>).progress_step_status;
expect(formatTaskProgress(task)).toBeNull();
});
it("hides stale progress when the task has reached a terminal status (done)", () => {
const task = makeTask({
status: "done",
progress_step_index: 5,
progress_step_status: "running",
progress_summary: "stale snapshot before terminal",
});
expect(formatTaskProgress(task)).toBeNull();
});
it("hides stale progress when the task has reached a terminal status (failed)", () => {
const task = makeTask({
status: "failed",
progress_step_index: 3,
progress_step_status: "running",
progress_summary: "stale snapshot before failure",
});
expect(formatTaskProgress(task)).toBeNull();
});
it("hides progress for a queued task that has not started executing", () => {
const task = makeTask({
status: "queued",
progress_step_index: 1,
progress_step_status: "running",
progress_summary: "should not show while queued",
});
expect(formatTaskProgress(task)).toBeNull();
});
it("ignores a missing step status even when step index is present", () => {
const task = makeTask({
status: "dispatched",
progress_step_index: 1,
progress_step_status: null,
progress_summary: "partial snapshot",
});
expect(formatTaskProgress(task)).toBeNull();
});
it("truncates a long summary with an ellipsis", () => {
const longSummary = "x".repeat(200);
const task = makeTask({
status: "dispatched",
progress_step_index: 1,
progress_step_status: "running",
progress_summary: longSummary,
});
const formatted = formatTaskProgress(task);
expect(formatted).not.toBeNull();
const summaryPart = formatted!.split(": ").slice(1).join(": ");
expect(summaryPart.length).toBeLessThan(longSummary.length);
expect(summaryPart.endsWith("\u2026")).toBe(true);
});
it("trims surrounding whitespace before truncating or rendering", () => {
const task = makeTask({
status: "dispatched",
progress_step_index: 1,
progress_step_status: "running",
progress_summary: " hello world ",
});
expect(formatTaskProgress(task)).toBe("step 1 \u2014 running: hello world");
});
});