First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
agentStoreReducer,
|
||||
buildNewSessionAgentPatch,
|
||||
getFilteredAgents,
|
||||
initialAgentStoreState,
|
||||
type AgentStoreSeed,
|
||||
} from "@/features/agents/state/store";
|
||||
|
||||
describe("agent store", () => {
|
||||
it("hydrates agents with defaults and selection", () => {
|
||||
const seed: AgentStoreSeed = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
};
|
||||
const next = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: [seed],
|
||||
});
|
||||
expect(next.loading).toBe(false);
|
||||
expect(next.selectedAgentId).toBe("agent-1");
|
||||
expect(next.agents).toHaveLength(1);
|
||||
expect(next.agents[0].status).toBe("idle");
|
||||
expect(next.agents[0].thinkingLevel).toBe("high");
|
||||
expect(next.agents[0].sessionCreated).toBe(false);
|
||||
expect(next.agents[0].outputLines).toEqual([]);
|
||||
});
|
||||
|
||||
it("hydrates agents with a requested selection when present", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
];
|
||||
const next = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
selectedAgentId: " agent-2 ",
|
||||
});
|
||||
expect(next.selectedAgentId).toBe("agent-2");
|
||||
});
|
||||
|
||||
it("keeps existing selection when requested selection is invalid", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
];
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "selectAgent",
|
||||
agentId: "agent-2",
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
selectedAgentId: "missing-agent",
|
||||
});
|
||||
expect(state.selectedAgentId).toBe("agent-2");
|
||||
});
|
||||
|
||||
it("builds a patch that resets runtime state for a session reset", () => {
|
||||
const seed: AgentStoreSeed = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:studio:old-session",
|
||||
};
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: [seed],
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: {
|
||||
status: "running",
|
||||
awaitingUserInput: true,
|
||||
hasUnseenActivity: true,
|
||||
outputLines: ["> hello", "response"],
|
||||
lastResult: "response",
|
||||
lastDiff: "diff",
|
||||
runId: "run-1",
|
||||
streamText: "live",
|
||||
thinkingTrace: "thinking",
|
||||
latestOverride: "override",
|
||||
latestOverrideKind: "heartbeat",
|
||||
lastAssistantMessageAt: 1700000000000,
|
||||
lastActivityAt: 1700000000001,
|
||||
latestPreview: "preview",
|
||||
lastUserMessage: "hello",
|
||||
draft: "draft",
|
||||
historyLoadedAt: 1700000000002,
|
||||
},
|
||||
});
|
||||
|
||||
const agent = state.agents.find((entry) => entry.agentId === "agent-1")!;
|
||||
const patch = buildNewSessionAgentPatch(agent);
|
||||
|
||||
expect(patch.sessionKey).toBe("agent:agent-1:studio:old-session");
|
||||
expect(patch.status).toBe("idle");
|
||||
expect(patch.sessionCreated).toBe(true);
|
||||
expect(patch.sessionSettingsSynced).toBe(true);
|
||||
expect(patch.outputLines).toEqual([]);
|
||||
expect(patch.streamText).toBeNull();
|
||||
expect(patch.thinkingTrace).toBeNull();
|
||||
expect(patch.lastResult).toBeNull();
|
||||
expect(patch.lastDiff).toBeNull();
|
||||
expect(patch.historyLoadedAt).toBeNull();
|
||||
expect(patch.lastUserMessage).toBeNull();
|
||||
expect(patch.runId).toBeNull();
|
||||
expect(patch.runStartedAt).toBeNull();
|
||||
expect(patch.latestPreview).toBeNull();
|
||||
expect(patch.latestOverride).toBeNull();
|
||||
expect(patch.latestOverrideKind).toBeNull();
|
||||
expect(patch.lastAssistantMessageAt).toBeNull();
|
||||
expect(patch.awaitingUserInput).toBe(false);
|
||||
expect(patch.hasUnseenActivity).toBe(false);
|
||||
expect(patch.draft).toBe("");
|
||||
});
|
||||
|
||||
it("preserves_session_created_state_across_hydration", () => {
|
||||
const seed: AgentStoreSeed = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
};
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: [seed],
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: { sessionCreated: true },
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "hydrateAgents",
|
||||
agents: [seed],
|
||||
});
|
||||
expect(state.agents[0]?.sessionCreated).toBe(true);
|
||||
});
|
||||
|
||||
it("resets_runtime_state_when_session_key_changes_on_hydration", () => {
|
||||
const initialSeed: AgentStoreSeed = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:studio:legacy",
|
||||
};
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: [initialSeed],
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: {
|
||||
sessionCreated: true,
|
||||
outputLines: ["> old"],
|
||||
lastResult: "old result",
|
||||
runId: "run-1",
|
||||
},
|
||||
});
|
||||
|
||||
const nextSeed: AgentStoreSeed = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
};
|
||||
state = agentStoreReducer(state, {
|
||||
type: "hydrateAgents",
|
||||
agents: [nextSeed],
|
||||
});
|
||||
const next = state.agents[0];
|
||||
expect(next?.sessionKey).toBe("agent:agent-1:main");
|
||||
expect(next?.sessionCreated).toBe(false);
|
||||
expect(next?.outputLines).toEqual([]);
|
||||
expect(next?.lastResult).toBeNull();
|
||||
expect(next?.runId).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps_transcript_references_for_non_transcript_agent_updates", () => {
|
||||
const seed: AgentStoreSeed = {
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
};
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: [seed],
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: { outputLines: ["> hello", "response"] },
|
||||
});
|
||||
|
||||
const beforeDraftUpdate = state.agents[0];
|
||||
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: { draft: "x" },
|
||||
});
|
||||
const afterDraftUpdate = state.agents[0];
|
||||
|
||||
expect(afterDraftUpdate.outputLines).toBe(beforeDraftUpdate.outputLines);
|
||||
expect(afterDraftUpdate.transcriptEntries).toBe(beforeDraftUpdate.transcriptEntries);
|
||||
expect(afterDraftUpdate.transcriptSequenceCounter).toBe(beforeDraftUpdate.transcriptSequenceCounter);
|
||||
});
|
||||
|
||||
it("tracks_unseen_activity_for_non_selected_agents", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
];
|
||||
const hydrated = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
});
|
||||
const withActivity = agentStoreReducer(hydrated, {
|
||||
type: "markActivity",
|
||||
agentId: "agent-2",
|
||||
at: 1700000000000,
|
||||
});
|
||||
const second = withActivity.agents.find((agent) => agent.agentId === "agent-2");
|
||||
expect(second?.hasUnseenActivity).toBe(true);
|
||||
expect(second?.lastActivityAt).toBe(1700000000000);
|
||||
|
||||
const selected = agentStoreReducer(withActivity, {
|
||||
type: "selectAgent",
|
||||
agentId: "agent-2",
|
||||
});
|
||||
const cleared = selected.agents.find((agent) => agent.agentId === "agent-2");
|
||||
expect(cleared?.hasUnseenActivity).toBe(false);
|
||||
});
|
||||
|
||||
it("filters_agents_by_status_and_approvals", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-3",
|
||||
name: "Agent Three",
|
||||
sessionKey: "agent:agent-3:main",
|
||||
},
|
||||
];
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: { status: "idle", awaitingUserInput: true },
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-2",
|
||||
patch: { status: "running" },
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-3",
|
||||
patch: { status: "error" },
|
||||
});
|
||||
|
||||
expect(getFilteredAgents(state, "all").map((agent) => agent.agentId)).toEqual([
|
||||
"agent-2",
|
||||
"agent-1",
|
||||
"agent-3",
|
||||
]);
|
||||
expect(getFilteredAgents(state, "running").map((agent) => agent.agentId)).toEqual([
|
||||
"agent-2",
|
||||
]);
|
||||
expect(getFilteredAgents(state, "approvals").map((agent) => agent.agentId)).toEqual([
|
||||
"agent-1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("clears_unseen_indicator_on_focus", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
];
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "markActivity",
|
||||
agentId: "agent-2",
|
||||
at: 1700000000100,
|
||||
});
|
||||
|
||||
const before = state.agents.find((agent) => agent.agentId === "agent-2");
|
||||
expect(before?.hasUnseenActivity).toBe(true);
|
||||
|
||||
state = agentStoreReducer(state, {
|
||||
type: "selectAgent",
|
||||
agentId: "agent-2",
|
||||
});
|
||||
const after = state.agents.find((agent) => agent.agentId === "agent-2");
|
||||
expect(after?.hasUnseenActivity).toBe(false);
|
||||
});
|
||||
|
||||
it("sorts_filtered_agents_by_latest_assistant_message", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-3",
|
||||
name: "Agent Three",
|
||||
sessionKey: "agent:agent-3:main",
|
||||
},
|
||||
];
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: { status: "running", lastAssistantMessageAt: 200 },
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-2",
|
||||
patch: { status: "running", lastAssistantMessageAt: 500 },
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-3",
|
||||
patch: { status: "running", lastAssistantMessageAt: 300 },
|
||||
});
|
||||
|
||||
expect(getFilteredAgents(state, "all").map((agent) => agent.agentId)).toEqual([
|
||||
"agent-2",
|
||||
"agent-3",
|
||||
"agent-1",
|
||||
]);
|
||||
expect(getFilteredAgents(state, "running").map((agent) => agent.agentId)).toEqual([
|
||||
"agent-2",
|
||||
"agent-3",
|
||||
"agent-1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("prioritizes_running_agents_in_all_filter_even_without_assistant_reply", () => {
|
||||
const seeds: AgentStoreSeed[] = [
|
||||
{
|
||||
agentId: "agent-1",
|
||||
name: "Agent One",
|
||||
sessionKey: "agent:agent-1:main",
|
||||
},
|
||||
{
|
||||
agentId: "agent-2",
|
||||
name: "Agent Two",
|
||||
sessionKey: "agent:agent-2:main",
|
||||
},
|
||||
];
|
||||
let state = agentStoreReducer(initialAgentStoreState, {
|
||||
type: "hydrateAgents",
|
||||
agents: seeds,
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-1",
|
||||
patch: { status: "idle", lastAssistantMessageAt: 900 },
|
||||
});
|
||||
state = agentStoreReducer(state, {
|
||||
type: "updateAgent",
|
||||
agentId: "agent-2",
|
||||
patch: { status: "running", runStartedAt: 1000, lastAssistantMessageAt: null },
|
||||
});
|
||||
|
||||
expect(getFilteredAgents(state, "all").map((agent) => agent.agentId)).toEqual([
|
||||
"agent-2",
|
||||
"agent-1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user