Files
multica/apps/mobile/data/agent-task-schema.test.ts
a2d819ea40 MUL-6962 fix(daemon): give environment preparation failures their own failure reason (#7915)
* fix(daemon): give environment preparation failures their own failure reason

A failed execenv.Prepare / Reuse is a filesystem error on the daemon's own
host, but handleTask routed it through taskfailure.Classify — a classifier
written to read agent and provider output — so a full disk, a denied
permission or a directory held open by another process all landed in
agent_error.*: the catchall today, provider_server_error on the report that
opened the issue and sent hours of diagnosis at an LLM vendor.

The daemon now tags those errors structurally (environmentSetupError) and
reports the new platform-side reason environment_prepare_failed. The four
existing sentinels still win: task identity, the prepare budget, skill
downloads and the OpenClaw CLI deadline each name a specific cause inside the
same phase.

Installed daemons update on their own cadence, and they report a non-empty
reason that FailTask deliberately leaves alone, so NormalizeDaemonReason
upgrades any agent_error.* value whose error text opens with the prepare /
reuse wrapper. That witness proves the run died before an agent existed, so
every label in that namespace is wrong by construction.

Not retryable and resume-safe: a full disk reproduces identically on the next
attempt, and no session was touched.

Closes #7913

Co-authored-by: multica-agent <[email protected]>

* fix(mobile): parse failure_reason as an open string, and match copy to real scope

Review follow-up on #7915.

AgentTaskSchema pinned failure_reason to the six pre-MUL-1949 coarse values
with `.catch("")`, so every refined reason was erased to undefined before any
surface read it. run-row's badge map was widened in MUL-5370 and has been
unreachable ever since: agent_error.*, skill_bundle_unavailable and the new
environment_prepare_failed all rendered a bare "Failed". The field is an open
string on the wire (packages/core/types/agent.ts) and in the chat message
schema beside it; the task schema now matches, and the badge map moved to
lib/run-failure-badge.ts so mobile's node-only vitest lane can cover the
lookup a component test cannot reach.

execenv.Prepare is not only filesystem work — it also writes and validates the
per-provider local config (Codex home, Hermes overlay, Cursor MCP, and an
OpenClaw config it fails closed on), so a malformed local config lands on this
reason too. The Go docs, the chat copy in four locales and the docs table said
"disk, permission, directory in use", which would send the reader past half
the real causes — the same misdirection this reason exists to end. They now
point at the raw error first and name the config case, with regression tests
for a non-filesystem preparation failure on both the daemon and the legacy
wire path.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-02 14:45:41 +08:00

55 lines
2.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { runFailureBadgeLabel } from "@/lib/run-failure-badge";
import { AgentTaskSchema } from "./schemas";
function parseFailedTask(failure_reason: unknown) {
return AgentTaskSchema.parse({
id: "task-1",
status: "failed",
failure_reason,
});
}
describe("AgentTaskSchema failure_reason (#7913 regression)", () => {
// The field was a closed six-value enum with `.catch("")`, so every refined
// reason the backend has written since MUL-1949 was erased to undefined
// before any surface could read it. The badge map was widened in MUL-5370
// and has been unreachable ever since — the parser, not the map, was the
// reason a failed run said only "Failed".
it("keeps a refined reason the backend classifies today", () => {
expect(parseFailedTask("agent_error.provider_network").failure_reason).toBe(
"agent_error.provider_network",
);
expect(parseFailedTask("environment_prepare_failed").failure_reason).toBe(
"environment_prepare_failed",
);
});
it("keeps a reason newer than this build", () => {
// The taxonomy grows on the backend's cadence and installed builds lag it.
// Dropping an unknown value here would re-create the dead-map bug the next
// time a reason lands, which is the whole point of parsing it open.
expect(parseFailedTask("some_future_reason").failure_reason).toBe(
"some_future_reason",
);
});
it("still normalizes the not-failed sentinel to undefined", () => {
// Go's `omitempty` sends "" on a task that did not fail. Downstream truthy
// checks depend on that becoming undefined rather than an empty string.
expect(parseFailedTask("").failure_reason).toBeUndefined();
expect(parseFailedTask(undefined).failure_reason).toBeUndefined();
expect(parseFailedTask(42).failure_reason).toBeUndefined();
});
it("hands the run row a value its badge map can name", () => {
// The end-to-end shape of the bug: parse, then look up. Either half alone
// passed while the run row still rendered a bare "Failed".
const task = parseFailedTask("environment_prepare_failed");
expect(runFailureBadgeLabel(task.failure_reason)).toBe(
"Environment setup failed",
);
});
});