summaryrefslogtreecommitdiffhomepage
path: root/src/features/named-agents/logic/options.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-08-20 19:36:12 +0900
committerAdam Malczewski <[email protected]>2026-08-20 19:36:12 +0900
commitfb7eb3105e1fbbdefdad9348f9132815b5bbe52d (patch)
treedeb95e97a917424ea67470b168b91349a41d1c08 /src/features/named-agents/logic/options.test.ts
parent63588c3c69b32d180dc78a2cc187cc8aa22ad7e0 (diff)
downloaddispatch-web-fb7eb3105e1fbbdefdad9348f9132815b5bbe52d.tar.gz
dispatch-web-fb7eb3105e1fbbdefdad9348f9132815b5bbe52d.zip
feat(named-agents): sidebar view, synthetic provider, auto-save, snapshot semantics
Implement the frontend of the Named Agents feature (plan §5-§9): - New features/named-agents module: pure logic (types re-exported from @dispatch/wire + transport-contract, validation, provider/model options, snapshot helpers, network normalizers, auto-save status model) + an injected auto-save controller (name debounce, immediate provider/model save, monotonic stale-response guard) + the collapsible sidebar card view. - Store: /named-agents CRUD + boot seed; selectModel(model, snapshot?) persists the raw model WITH the namedAgent snapshot; draft promotion carries it; refreshModel restores ModelResponse.namedAgent. - Chat store setNamedAgent (sends namedAgent on chat.send); Tab.snapshot + setSnapshot (retained across rename/remove, persisted with tabs). - Model selector: synthetic Named Agents provider ALWAYS first; selecting one resolves to the raw model + records a snapshot; a retained snapshot of a renamed/removed agent still displays as unavailable (plan §7). - Composition root wires the view (viewKinds + loaded-modules manifest) + feeds the selector. Tests: logic + controller + selector + app-store CRUD (1246 pass), typecheck 0, biome clean (only pre-existing config infos pending a future migrate), build OK.
Diffstat (limited to 'src/features/named-agents/logic/options.test.ts')
-rw-r--r--src/features/named-agents/logic/options.test.ts72
1 files changed, 72 insertions, 0 deletions
diff --git a/src/features/named-agents/logic/options.test.ts b/src/features/named-agents/logic/options.test.ts
new file mode 100644
index 0000000..8b774ab
--- /dev/null
+++ b/src/features/named-agents/logic/options.test.ts
@@ -0,0 +1,72 @@
+import { describe, expect, it } from "vitest";
+import {
+ fullModelFromSelection,
+ modelFromFull,
+ modelsForProvider,
+ providerFromModel,
+ providerLabel,
+ providerOptions,
+} from "./options";
+
+const MODELS = [
+ "opencode-2/gpt-5",
+ "opencode-1/claude-sonnet-4.5",
+ "umans/umans-glm-5.2",
+ "opencode-10/defiant",
+ "umans/umans-gpt-5",
+ "hyper/fast-1",
+] as const;
+
+describe("providerFromModel / modelFromFull", () => {
+ it("splits a <provider>/<model> on the first slash", () => {
+ expect(providerFromModel("opencode-1/claude-sonnet-4.5")).toBe("opencode-1");
+ expect(modelFromFull("opencode-1/claude-sonnet-4.5")).toBe("claude-sonnet-4.5");
+ });
+
+ it("treats a slashless name as all-provider", () => {
+ expect(providerFromModel("umans")).toBe("umans");
+ expect(modelFromFull("umans")).toBe("");
+ });
+});
+
+describe("providerOptions", () => {
+ it("sorts numbered OpenCode credentials numerically before other providers", () => {
+ expect(providerOptions([...MODELS])).toEqual([
+ "opencode-1",
+ "opencode-2",
+ "opencode-10",
+ "umans",
+ "hyper",
+ ]);
+ });
+
+ it("keeps first-seen order and dedupes", () => {
+ expect(providerOptions(["b/x", "a/y", "b/z"])).toEqual(["b", "a"]);
+ });
+});
+
+describe("modelsForProvider", () => {
+ it("returns only the model suffixes under the provider, in order", () => {
+ expect(modelsForProvider([...MODELS], "umans")).toEqual(["umans-glm-5.2", "umans-gpt-5"]);
+ expect(modelsForProvider([...MODELS], "hyper")).toEqual(["fast-1"]);
+ });
+
+ it("returns [] for an unknown provider", () => {
+ expect(modelsForProvider([...MODELS], "missing")).toEqual([]);
+ });
+});
+
+describe("providerLabel", () => {
+ it("labels numbered OpenCode providers, leaves others unchanged", () => {
+ expect(providerLabel("opencode-2")).toBe("OpenCode #2");
+ expect(providerLabel("umans")).toBe("umans");
+ });
+});
+
+describe("fullModelFromSelection", () => {
+ it("recombines a provider + model suffix", () => {
+ expect(fullModelFromSelection("opencode-1", "claude-sonnet-4.5")).toBe(
+ "opencode-1/claude-sonnet-4.5",
+ );
+ });
+});