Files
horus-3d/tests/unit/mutationLifecycleWorkflow.integration.test.ts
T
Luke The Dev 4fa4f13558 First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
2026-03-19 23:14:04 -05:00

281 lines
8.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
buildConfigMutationFailureMessage,
buildMutationSideEffectCommands,
buildQueuedMutationBlock,
resolveConfigMutationPostRunEffects,
resolveConfigMutationStatusLine,
resolveMutationStartGuard,
runConfigMutationWorkflow,
} from "@/features/agents/operations/mutationLifecycleWorkflow";
import { shouldStartNextConfigMutation } from "@/features/agents/operations/configMutationGatePolicy";
describe("mutationLifecycleWorkflow integration", () => {
it("page create handler uses shared start guard and queued block shape", () => {
const denied = resolveMutationStartGuard({
status: "disconnected",
hasCreateBlock: false,
hasRenameBlock: false,
hasDeleteBlock: false,
});
expect(denied).toEqual({ kind: "deny", reason: "not-connected" });
const allowed = resolveMutationStartGuard({
status: "connected",
hasCreateBlock: false,
hasRenameBlock: false,
hasDeleteBlock: false,
});
expect(allowed).toEqual({ kind: "allow" });
const queued = buildQueuedMutationBlock({
kind: "create-agent",
agentId: "",
agentName: "Agent One",
startedAt: 42,
});
expect(queued).toEqual({
kind: "create-agent",
agentId: "",
agentName: "Agent One",
phase: "queued",
startedAt: 42,
sawDisconnect: false,
});
});
it("delete workflow maps awaiting-restart outcome to awaiting-restart block phase", async () => {
const result = await runConfigMutationWorkflow(
{ kind: "delete-agent", isLocalGateway: false },
{
executeMutation: async () => undefined,
shouldAwaitRemoteRestart: async () => true,
}
);
const effects = resolveConfigMutationPostRunEffects(result);
expect(effects).toEqual({
shouldReloadAgents: false,
shouldClearBlock: false,
awaitingRestartPatch: {
phase: "awaiting-restart",
sawDisconnect: false,
},
});
});
it("rename workflow maps completed outcome to load-and-clear flow", async () => {
const result = await runConfigMutationWorkflow(
{ kind: "rename-agent", isLocalGateway: false },
{
executeMutation: async () => undefined,
shouldAwaitRemoteRestart: async () => false,
}
);
const effects = resolveConfigMutationPostRunEffects(result);
let didLoadAgents = false;
let block: { phase: string; sawDisconnect: boolean } | null = {
phase: "mutating",
sawDisconnect: false,
};
if (effects.shouldReloadAgents) {
didLoadAgents = true;
}
if (effects.shouldClearBlock) {
block = null;
}
expect(didLoadAgents).toBe(true);
expect(block).toBeNull();
expect(effects.awaitingRestartPatch).toBeNull();
});
it("page rename and delete handlers share lifecycle guard plus post-run transitions", async () => {
const blocked = resolveMutationStartGuard({
status: "connected",
hasCreateBlock: true,
hasRenameBlock: false,
hasDeleteBlock: false,
});
expect(blocked).toEqual({
kind: "deny",
reason: "create-block-active",
});
const allowed = resolveMutationStartGuard({
status: "connected",
hasCreateBlock: false,
hasRenameBlock: false,
hasDeleteBlock: false,
});
expect(allowed).toEqual({ kind: "allow" });
const executeMutation = vi.fn(async () => undefined);
const renameCompleted = await runConfigMutationWorkflow(
{
kind: "rename-agent",
isLocalGateway: false,
},
{
executeMutation,
shouldAwaitRemoteRestart: async () => false,
}
);
expect(buildMutationSideEffectCommands({ disposition: renameCompleted.disposition })).toEqual([
{ kind: "reload-agents" },
{ kind: "clear-mutation-block" },
{ kind: "set-mobile-pane", pane: "chat" },
]);
const deleteAwaitingRestart = await runConfigMutationWorkflow(
{
kind: "delete-agent",
isLocalGateway: false,
},
{
executeMutation,
shouldAwaitRemoteRestart: async () => true,
}
);
expect(buildMutationSideEffectCommands({ disposition: deleteAwaitingRestart.disposition })).toEqual(
[{ kind: "patch-mutation-block", patch: { phase: "awaiting-restart", sawDisconnect: false } }]
);
expect(executeMutation).toHaveBeenCalledTimes(2);
});
it("uses typed mutation commands for lifecycle side effects instead of inline branching", async () => {
const commandLog: string[] = [];
const runCommands = async (
disposition: "completed" | "awaiting-restart"
): Promise<{ phase: string; sawDisconnect: boolean } | null> => {
let block: { phase: string; sawDisconnect: boolean } | null = {
phase: "mutating",
sawDisconnect: false,
};
for (const command of buildMutationSideEffectCommands({ disposition })) {
if (command.kind === "reload-agents") {
commandLog.push("reload");
continue;
}
if (command.kind === "clear-mutation-block") {
commandLog.push("clear");
block = null;
continue;
}
if (command.kind === "set-mobile-pane") {
commandLog.push(`pane:${command.pane}`);
continue;
}
commandLog.push(`patch:${command.patch.phase}`);
block = {
phase: command.patch.phase,
sawDisconnect: command.patch.sawDisconnect,
};
}
return block;
};
const completedBlock = await runCommands("completed");
const awaitingBlock = await runCommands("awaiting-restart");
expect(completedBlock).toBeNull();
expect(awaitingBlock).toEqual({
phase: "awaiting-restart",
sawDisconnect: false,
});
expect(commandLog).toEqual(["reload", "clear", "pane:chat", "patch:awaiting-restart"]);
});
it("workflow errors clear block and set page error message", async () => {
let block: { phase: string; sawDisconnect: boolean } | null = {
phase: "mutating",
sawDisconnect: false,
};
let errorMessage: string | null = null;
try {
await runConfigMutationWorkflow(
{ kind: "rename-agent", isLocalGateway: false },
{
executeMutation: async () => {
throw new Error("rename exploded");
},
shouldAwaitRemoteRestart: async () => false,
}
);
} catch (error) {
block = null;
errorMessage = buildConfigMutationFailureMessage({
kind: "rename-agent",
error,
});
}
expect(block).toBeNull();
expect(errorMessage).toBe("rename exploded");
});
it("preserves queue gating when restart block is active", () => {
expect(
shouldStartNextConfigMutation({
status: "connected",
hasRunningAgents: false,
nextMutationRequiresIdleAgents: false,
hasActiveMutation: false,
hasRestartBlockInProgress: true,
queuedCount: 1,
})
).toBe(false);
expect(
shouldStartNextConfigMutation({
status: "connected",
hasRunningAgents: false,
nextMutationRequiresIdleAgents: false,
hasActiveMutation: false,
hasRestartBlockInProgress: false,
queuedCount: 1,
})
).toBe(true);
});
it("preserves lock-status text behavior across queued, mutating, and awaiting-restart phases", () => {
expect(
resolveConfigMutationStatusLine({
block: { phase: "queued", sawDisconnect: false },
status: "connected",
})
).toBe("Waiting for active runs to finish");
expect(
resolveConfigMutationStatusLine({
block: { phase: "mutating", sawDisconnect: false },
status: "connected",
})
).toBe("Submitting config change");
expect(
resolveConfigMutationStatusLine({
block: { phase: "awaiting-restart", sawDisconnect: false },
status: "connected",
})
).toBe("Waiting for gateway to restart");
expect(
resolveConfigMutationStatusLine({
block: { phase: "awaiting-restart", sawDisconnect: true },
status: "disconnected",
})
).toBe("Gateway restart in progress");
expect(
resolveConfigMutationStatusLine({
block: { phase: "awaiting-restart", sawDisconnect: true },
status: "connected",
})
).toBe("Gateway is back online, syncing agents");
});
});