Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
areAgentGuiWorkbenchStatesEqual,
|
|
createAgentGuiWorkbenchNodeStateSource,
|
|
createDefaultAgentGuiWorkbenchNodeState,
|
|
migrateLegacyAgentGuiWorkbenchState,
|
|
normalizeAgentGuiWorkbenchNodeState,
|
|
normalizeAgentGuiWorkbenchState,
|
|
projectAgentGuiWorkbenchState
|
|
} from "./state.ts";
|
|
|
|
describe("agent gui workbench state", () => {
|
|
it("normalizes providers and partial runtime data", () => {
|
|
expect(
|
|
normalizeAgentGuiWorkbenchNodeState({ provider: "claude-code" }).provider
|
|
).toBe("claude-code");
|
|
expect(
|
|
normalizeAgentGuiWorkbenchNodeState({
|
|
provider: "unsupported"
|
|
} as never).provider
|
|
).toBe("unsupported");
|
|
expect(
|
|
normalizeAgentGuiWorkbenchNodeState({
|
|
composerOverrides: { model: "gpt-5" },
|
|
composerOverridesByAgentTargetId: {
|
|
"target-a": { model: "target-model" }
|
|
},
|
|
composerOverridesByProvider: {
|
|
openclaw: { permissionModeId: "read-only" },
|
|
unsupported: { model: "ignored" }
|
|
} as never,
|
|
lastActiveAgentSessionId: "session-1",
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
" target-a ": " session-1 ",
|
|
empty: " "
|
|
},
|
|
provider: "openclaw"
|
|
})
|
|
).toEqual({
|
|
...createDefaultAgentGuiWorkbenchNodeState("openclaw"),
|
|
composerOverrides: { model: "gpt-5" },
|
|
composerOverridesByAgentTargetId: {
|
|
"target-a": { model: "target-model" }
|
|
},
|
|
composerOverridesByProvider: {
|
|
openclaw: { permissionModeId: "read-only" }
|
|
},
|
|
lastActiveAgentSessionId: "session-1",
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
"target-a": "session-1"
|
|
}
|
|
});
|
|
});
|
|
|
|
it("projects only persisted workbench state", () => {
|
|
expect(
|
|
projectAgentGuiWorkbenchState({
|
|
...createDefaultAgentGuiWorkbenchNodeState("hermes"),
|
|
composerOverrides: { permissionModeId: "read-only" },
|
|
composerOverridesByAgentTargetId: {
|
|
"target-a": { model: "target-model" }
|
|
},
|
|
composerOverridesByProvider: {
|
|
hermes: { model: "hermes-2.5-pro" }
|
|
},
|
|
conversationCount: 3,
|
|
conversationRailCollapsed: true,
|
|
conversationRailWidthPx: 360.4,
|
|
agentTargetId: "shared-agent:agent-1",
|
|
lastActiveAgentSessionId: "session-1",
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
"shared-agent:agent-1": "session-1"
|
|
}
|
|
})
|
|
).toEqual({
|
|
agentTargetId: "shared-agent:agent-1",
|
|
conversationRailCollapsed: true,
|
|
conversationRailWidthPx: 360,
|
|
lastActiveAgentSessionId: "session-1",
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
"shared-agent:agent-1": "session-1"
|
|
}
|
|
});
|
|
});
|
|
|
|
it("migrates legacy provider target ids to agent target selection", () => {
|
|
const providerTargetRef = {
|
|
kind: "shared-agent",
|
|
provider: "codex" as const,
|
|
sharedAgentId: "agent-1"
|
|
};
|
|
|
|
expect(
|
|
normalizeAgentGuiWorkbenchNodeState(
|
|
migrateLegacyAgentGuiWorkbenchState({
|
|
provider: "codex",
|
|
providerTargetId: "shared-agent:agent-1",
|
|
providerTargetRef
|
|
})
|
|
)
|
|
).toMatchObject({
|
|
agentTargetId: "shared-agent:agent-1",
|
|
provider: "codex"
|
|
});
|
|
|
|
expect(
|
|
normalizeAgentGuiWorkbenchNodeState(
|
|
migrateLegacyAgentGuiWorkbenchState({
|
|
provider: "codex",
|
|
providerTargetId: "shared-agent:agent-1",
|
|
providerTargetRef: {
|
|
...providerTargetRef,
|
|
provider: "claude-code"
|
|
}
|
|
})
|
|
)
|
|
).toMatchObject({
|
|
provider: "codex",
|
|
agentTargetId: "shared-agent:agent-1"
|
|
});
|
|
});
|
|
|
|
it("compares persisted workbench state", () => {
|
|
expect(
|
|
areAgentGuiWorkbenchStatesEqual(
|
|
normalizeAgentGuiWorkbenchState({
|
|
composerOverrides: { permissionModeId: "auto" },
|
|
composerOverridesByAgentTargetId: {
|
|
"target-a": { model: "target-model" }
|
|
},
|
|
composerOverridesByProvider: {
|
|
codex: { model: "gpt-5" }
|
|
},
|
|
conversationRailCollapsed: false,
|
|
lastActiveAgentSessionId: "session-1"
|
|
}),
|
|
normalizeAgentGuiWorkbenchState({
|
|
lastActiveAgentSessionId: "session-1",
|
|
provider: "hermes"
|
|
})
|
|
)
|
|
).toBe(true);
|
|
expect(
|
|
areAgentGuiWorkbenchStatesEqual(
|
|
normalizeAgentGuiWorkbenchState({
|
|
lastActiveAgentSessionId: null,
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
"local:codex": "session-1"
|
|
}
|
|
}),
|
|
normalizeAgentGuiWorkbenchState({
|
|
lastActiveAgentSessionId: null,
|
|
lastActiveAgentSessionIdByAgentTargetId: {
|
|
"local:codex": "session-2"
|
|
}
|
|
})
|
|
)
|
|
).toBe(false);
|
|
expect(
|
|
areAgentGuiWorkbenchStatesEqual(
|
|
normalizeAgentGuiWorkbenchState(
|
|
migrateLegacyAgentGuiWorkbenchState({
|
|
lastActiveAgentSessionId: "session-1",
|
|
providerTargetId: "legacy-target"
|
|
})
|
|
),
|
|
normalizeAgentGuiWorkbenchState({
|
|
lastActiveAgentSessionId: "session-1",
|
|
agentTargetId: "legacy-target"
|
|
})
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it("uses instance launch state only until node state is written", () => {
|
|
const source = createAgentGuiWorkbenchNodeStateSource({
|
|
workspaceId: "workspace-1"
|
|
});
|
|
let notified = 0;
|
|
const unsubscribe =
|
|
source.externalStateSource.subscribeNodeState?.(
|
|
{
|
|
instanceId: "agent-gui:hermes",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
},
|
|
() => {
|
|
notified += 1;
|
|
}
|
|
) ?? (() => undefined);
|
|
|
|
source.writeNodeState({
|
|
instanceId: "agent-gui:hermes",
|
|
state: {
|
|
agentTargetId: "daemon-hermes",
|
|
conversationRailCollapsed: true,
|
|
conversationRailWidthPx: 360,
|
|
lastActiveAgentSessionId: "session-1"
|
|
},
|
|
typeId: "agent-gui"
|
|
});
|
|
|
|
expect(notified).toBe(1);
|
|
expect(
|
|
source.externalStateSource.getNodeState({
|
|
instanceId: "agent-gui:hermes",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
})
|
|
).toEqual({
|
|
agentTargetId: "daemon-hermes",
|
|
conversationRailCollapsed: true,
|
|
conversationRailWidthPx: 360,
|
|
lastActiveAgentSessionId: "session-1"
|
|
});
|
|
|
|
source.writeNodeState({
|
|
instanceId: "agent-gui:hermes",
|
|
nodeId: "node-1",
|
|
state: {
|
|
agentTargetId: "daemon-hermes-2",
|
|
conversationRailCollapsed: false,
|
|
conversationRailWidthPx: 420,
|
|
lastActiveAgentSessionId: "session-2"
|
|
},
|
|
typeId: "agent-gui"
|
|
});
|
|
unsubscribe();
|
|
|
|
expect(notified).toBe(2);
|
|
expect(
|
|
source.externalStateSource.getNodeState({
|
|
instanceId: "agent-gui:hermes",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
})
|
|
).toEqual({
|
|
agentTargetId: "daemon-hermes-2",
|
|
conversationRailCollapsed: false,
|
|
conversationRailWidthPx: 420,
|
|
lastActiveAgentSessionId: "session-2"
|
|
});
|
|
expect(
|
|
source.externalStateSource.getSnapshotNodeState?.({
|
|
instanceId: "agent-gui:hermes",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
})
|
|
).toMatchObject({
|
|
agentTargetId: "daemon-hermes-2",
|
|
lastActiveAgentSessionId: "session-2"
|
|
});
|
|
expect(
|
|
source.readNodeState({
|
|
instanceId: "agent-gui:hermes",
|
|
typeId: "agent-gui"
|
|
})
|
|
).toBeNull();
|
|
});
|
|
|
|
it("isolates multi-instance node state by workbench node id", () => {
|
|
const source = createAgentGuiWorkbenchNodeStateSource({
|
|
workspaceId: "workspace-1"
|
|
});
|
|
let notified = 0;
|
|
const unsubscribe =
|
|
source.externalStateSource.subscribeNodeState?.(
|
|
{
|
|
instanceId: "agent-gui",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
},
|
|
() => {
|
|
notified += 1;
|
|
}
|
|
) ?? (() => undefined);
|
|
|
|
source.writeNodeState({
|
|
instanceId: "agent-gui",
|
|
nodeId: "node-1",
|
|
state: {
|
|
lastActiveAgentSessionId: "session-1"
|
|
},
|
|
typeId: "agent-gui"
|
|
});
|
|
source.writeNodeState({
|
|
instanceId: "agent-gui",
|
|
nodeId: "node-2",
|
|
state: {
|
|
lastActiveAgentSessionId: "session-2"
|
|
},
|
|
typeId: "agent-gui"
|
|
});
|
|
source.writeNodeState({
|
|
instanceId: "agent-gui",
|
|
nodeId: "node-2",
|
|
state: {
|
|
lastActiveAgentSessionId: "session-2"
|
|
},
|
|
typeId: "agent-gui"
|
|
});
|
|
unsubscribe();
|
|
|
|
expect(notified).toBe(2);
|
|
expect(
|
|
source.externalStateSource.getNodeState({
|
|
instanceId: "agent-gui",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
})
|
|
).toMatchObject({
|
|
lastActiveAgentSessionId: "session-1"
|
|
});
|
|
expect(
|
|
source.externalStateSource.getNodeState({
|
|
instanceId: "agent-gui",
|
|
nodeId: "node-2",
|
|
typeId: "agent-gui",
|
|
workspaceId: "workspace-1"
|
|
})
|
|
).toMatchObject({
|
|
lastActiveAgentSessionId: "session-2"
|
|
});
|
|
});
|
|
|
|
it("preserves node state identity for an equal write", () => {
|
|
const source = createAgentGuiWorkbenchNodeStateSource({
|
|
workspaceId: "workspace-1"
|
|
});
|
|
const request = {
|
|
instanceId: "agent-gui:hermes",
|
|
nodeId: "node-1",
|
|
typeId: "agent-gui" as const,
|
|
workspaceId: "workspace-1"
|
|
};
|
|
const state = { lastActiveAgentSessionId: "session-1" };
|
|
|
|
source.writeNodeState({ ...request, state });
|
|
const first = source.externalStateSource.getNodeState(request);
|
|
source.writeNodeState({ ...request, state: { ...state } });
|
|
|
|
expect(source.externalStateSource.getNodeState(request)).toBe(first);
|
|
});
|
|
|
|
it("locates a node launch instanceId by the session it is showing", () => {
|
|
const source = createAgentGuiWorkbenchNodeStateSource({
|
|
workspaceId: "workspace-1"
|
|
});
|
|
|
|
// Instance identity stays opaque; the live session binding is written
|
|
// under a node-scoped state key.
|
|
source.writeNodeState({
|
|
instanceId: "agent-gui:instance:abc123",
|
|
nodeId: "node-1",
|
|
state: { lastActiveAgentSessionId: "session-xyz" },
|
|
typeId: "agent-gui"
|
|
});
|
|
|
|
expect(source.findInstanceIdByAgentSessionId("session-xyz")).toBe(
|
|
"agent-gui:instance:abc123"
|
|
);
|
|
expect(source.findInstanceIdByAgentSessionId(" session-xyz ")).toBe(
|
|
"agent-gui:instance:abc123"
|
|
);
|
|
expect(source.findInstanceIdByAgentSessionId("other-session")).toBeNull();
|
|
expect(source.findInstanceIdByAgentSessionId("")).toBeNull();
|
|
});
|
|
});
|