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
573 lines
17 KiB
TypeScript
573 lines
17 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { AgentActivityComposerOptions } from "@tutti-os/agent-activity-core";
|
|
import type { AgentGUINodeData } from "../../../types";
|
|
import {
|
|
composerModelChoiceHistoryTargetId,
|
|
composerTargetDataForConversation,
|
|
effectiveComposerSettingsFromOptions,
|
|
enforceComposerModelBindingForHomeDefaults,
|
|
isForegroundModelOptionsLoading,
|
|
nodeDataMatchesComposerTarget,
|
|
reconcileOptimisticComposerTarget,
|
|
resolvePresentedComposerSettings,
|
|
sanitizeComposerSettingsForOptions,
|
|
verifyComposerModelAgainstNativeOptions
|
|
} from "./agentGuiController.composerPresentation";
|
|
|
|
describe("composer target presentation", () => {
|
|
const selectedTarget = {
|
|
agentTargetId: "local:codex",
|
|
provider: "codex" as const,
|
|
targetId: "local:codex",
|
|
data: {
|
|
provider: "codex" as const,
|
|
agentTargetId: "local:codex",
|
|
lastActiveAgentSessionId: null
|
|
}
|
|
};
|
|
|
|
it("fails model history closed for unresolved active targets", () => {
|
|
expect(
|
|
composerModelChoiceHistoryTargetId({
|
|
activeConversationId: null,
|
|
target: { agentTargetId: null, targetId: "local:codex" }
|
|
})
|
|
).toBe("local:codex");
|
|
expect(
|
|
composerModelChoiceHistoryTargetId({
|
|
activeConversationId: "session-1",
|
|
target: { agentTargetId: null, targetId: "local:codex" }
|
|
})
|
|
).toBeNull();
|
|
expect(
|
|
composerModelChoiceHistoryTargetId({
|
|
activeConversationId: "session-1",
|
|
target: { agentTargetId: "agent:ccs", targetId: "legacy:ccs" }
|
|
})
|
|
).toBe("agent:ccs");
|
|
});
|
|
|
|
it("keeps the submitted target until the host node projection catches up", () => {
|
|
const staleNodeData: AgentGUINodeData = {
|
|
provider: "codex",
|
|
lastActiveAgentSessionId: null
|
|
};
|
|
const optimisticTarget = {
|
|
agentSessionId: "session-new",
|
|
target: selectedTarget
|
|
};
|
|
|
|
expect(
|
|
composerTargetDataForConversation({
|
|
activeConversationId: "session-new",
|
|
activeSessionTarget: {
|
|
...selectedTarget,
|
|
agentSessionId: "session-new"
|
|
},
|
|
data: staleNodeData,
|
|
optimisticTarget,
|
|
selectedTarget
|
|
})
|
|
).toBe(selectedTarget);
|
|
expect(nodeDataMatchesComposerTarget(staleNodeData, selectedTarget)).toBe(
|
|
false
|
|
);
|
|
const echoedNodeData = {
|
|
...staleNodeData,
|
|
agentTargetId: "local:codex"
|
|
};
|
|
expect(nodeDataMatchesComposerTarget(echoedNodeData, selectedTarget)).toBe(
|
|
true
|
|
);
|
|
expect(
|
|
composerTargetDataForConversation({
|
|
activeConversationId: "session-new",
|
|
activeSessionTarget: {
|
|
...selectedTarget,
|
|
agentSessionId: "session-new"
|
|
},
|
|
data: echoedNodeData,
|
|
optimisticTarget,
|
|
selectedTarget
|
|
}).data
|
|
).toBe(echoedNodeData);
|
|
expect(
|
|
reconcileOptimisticComposerTarget({
|
|
activeConversationId: "session-new",
|
|
data: staleNodeData,
|
|
optimisticTarget
|
|
})
|
|
).toBe(optimisticTarget);
|
|
expect(
|
|
reconcileOptimisticComposerTarget({
|
|
activeConversationId: "session-new",
|
|
data: echoedNodeData,
|
|
optimisticTarget
|
|
})
|
|
).toBeNull();
|
|
expect(
|
|
reconcileOptimisticComposerTarget({
|
|
activeConversationId: "session-other",
|
|
data: staleNodeData,
|
|
optimisticTarget
|
|
})
|
|
).toBeNull();
|
|
});
|
|
|
|
it("keeps the submitted target reference while the canonical session is pending", () => {
|
|
expect(
|
|
composerTargetDataForConversation({
|
|
activeConversationId: "session-new",
|
|
activeSessionTarget: null,
|
|
data: selectedTarget.data,
|
|
optimisticTarget: {
|
|
agentSessionId: "session-new",
|
|
target: selectedTarget
|
|
},
|
|
selectedTarget
|
|
})
|
|
).toBe(selectedTarget);
|
|
});
|
|
|
|
it("uses the active session agent target when node data lacks the target id", () => {
|
|
const staleNodeData: AgentGUINodeData = {
|
|
provider: "acp:hermes",
|
|
lastActiveAgentSessionId: "session-1"
|
|
};
|
|
|
|
expect(
|
|
composerTargetDataForConversation({
|
|
activeConversationId: "session-1",
|
|
activeSessionTarget: {
|
|
agentTargetId: "extension:hermes",
|
|
agentSessionId: "session-1",
|
|
provider: "acp:hermes"
|
|
},
|
|
data: staleNodeData,
|
|
optimisticTarget: null,
|
|
selectedTarget
|
|
})
|
|
).toMatchObject({
|
|
agentTargetId: "extension:hermes",
|
|
provider: "acp:hermes",
|
|
targetId: "extension:hermes",
|
|
data: {
|
|
agentTargetId: "extension:hermes",
|
|
provider: "acp:hermes"
|
|
}
|
|
});
|
|
});
|
|
|
|
it("keeps the active session target identity atomic while node data catches up", () => {
|
|
const staleNodeData: AgentGUINodeData = {
|
|
agentTargetId: "local:opencode",
|
|
provider: "opencode",
|
|
lastActiveAgentSessionId: null
|
|
};
|
|
|
|
expect(
|
|
composerTargetDataForConversation({
|
|
activeConversationId: "session-codex",
|
|
activeSessionTarget: {
|
|
agentTargetId: "local:codex",
|
|
agentSessionId: "session-codex",
|
|
provider: "codex"
|
|
},
|
|
data: staleNodeData,
|
|
optimisticTarget: null,
|
|
selectedTarget
|
|
})
|
|
).toMatchObject({
|
|
agentTargetId: "local:codex",
|
|
provider: "codex",
|
|
targetId: "local:codex",
|
|
data: {
|
|
agentTargetId: "local:codex",
|
|
provider: "codex"
|
|
}
|
|
});
|
|
});
|
|
|
|
it("waits when the Engine target belongs to a different session", () => {
|
|
expect(
|
|
composerTargetDataForConversation({
|
|
activeConversationId: "session-new",
|
|
activeSessionTarget: {
|
|
agentSessionId: "session-old",
|
|
agentTargetId: "local:opencode",
|
|
provider: "opencode"
|
|
},
|
|
data: selectedTarget.data,
|
|
optimisticTarget: null,
|
|
selectedTarget
|
|
})
|
|
).toMatchObject({
|
|
agentTargetId: null,
|
|
targetId: "__active_session_loading__"
|
|
});
|
|
});
|
|
|
|
it("treats live model discovery as foreground loading only without usable cached options", () => {
|
|
expect(
|
|
isForegroundModelOptionsLoading({
|
|
modelOptionsLoading: true,
|
|
selection: { currentValue: "gpt-5", options: [] },
|
|
supportsModel: true
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isForegroundModelOptionsLoading({
|
|
modelOptionsLoading: true,
|
|
selection: {
|
|
currentValue: "gpt-5",
|
|
options: [{ value: "gpt-5", label: "GPT-5" }]
|
|
},
|
|
supportsModel: true
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("fills missing optimistic values from effective pre-session settings", () => {
|
|
const preloaded = effectiveComposerSettingsFromOptions({
|
|
provider: "codex",
|
|
capabilities: null,
|
|
models: [],
|
|
reasoningEfforts: [],
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: false,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1,
|
|
effectiveSettings: {
|
|
model: "gpt-5.3-codex",
|
|
reasoningEffort: "high",
|
|
permissionModeId: "full-access"
|
|
}
|
|
});
|
|
|
|
expect(
|
|
resolvePresentedComposerSettings({
|
|
sessionSettings: null,
|
|
optimisticSettings: { model: null, planMode: false },
|
|
preloadedSettings: preloaded,
|
|
homeSettings: { reasoningEffort: "medium", browserUse: true }
|
|
})
|
|
).toMatchObject({
|
|
model: "gpt-5.3-codex",
|
|
reasoningEffort: "high",
|
|
planMode: false,
|
|
browserUse: true,
|
|
permissionModeId: "full-access"
|
|
});
|
|
});
|
|
|
|
it("keeps every explicit optimistic default above a stale effective-settings snapshot", () => {
|
|
expect(
|
|
resolvePresentedComposerSettings({
|
|
sessionSettings: null,
|
|
optimisticSettings: {
|
|
model: "new-model",
|
|
permissionModeId: "full-access",
|
|
reasoningEffort: "high",
|
|
speed: "fast"
|
|
},
|
|
preloadedSettings: {
|
|
model: "old-model",
|
|
permissionModeId: "ask",
|
|
reasoningEffort: "low",
|
|
speed: "standard"
|
|
},
|
|
homeSettings: {}
|
|
})
|
|
).toMatchObject({
|
|
model: "new-model",
|
|
permissionModeId: "full-access",
|
|
reasoningEffort: "high",
|
|
speed: "fast"
|
|
});
|
|
});
|
|
|
|
it("does not turn an absent pre-session boolean into an explicit override", () => {
|
|
const preloaded = effectiveComposerSettingsFromOptions({
|
|
provider: "codex",
|
|
capabilities: null,
|
|
models: [],
|
|
reasoningEfforts: [],
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: false,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1,
|
|
effectiveSettings: { model: "gpt-5.3-codex" }
|
|
});
|
|
|
|
expect(preloaded?.planMode).toBeUndefined();
|
|
expect(
|
|
resolvePresentedComposerSettings({
|
|
sessionSettings: null,
|
|
optimisticSettings: null,
|
|
preloadedSettings: preloaded,
|
|
homeSettings: { planMode: true }
|
|
}).planMode
|
|
).toBe(true);
|
|
});
|
|
|
|
it("applies descriptor-backed model catalog authority without provider checks", () => {
|
|
const settings = { model: "stale-model", reasoningEffort: "high" };
|
|
const options = {
|
|
provider: "any-provider",
|
|
capabilities: null,
|
|
models: [{ value: "current-model", label: "Current" }],
|
|
reasoningEfforts: [{ value: "high", label: "High" }],
|
|
reasoningConfigurable: true,
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: false,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1
|
|
};
|
|
|
|
expect(sanitizeComposerSettingsForOptions(settings, options).model).toBe(
|
|
"stale-model"
|
|
);
|
|
expect(
|
|
sanitizeComposerSettingsForOptions(settings, {
|
|
...options,
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: true,
|
|
refreshModelOptionsAfterSettings: false,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
}
|
|
}).model
|
|
).toBeNull();
|
|
});
|
|
|
|
it("clears a remembered effort when the selected model advertises no reasoning variants", () => {
|
|
const settings = { model: "opencode/big-pickle", reasoningEffort: "high" };
|
|
const options = {
|
|
provider: "opencode",
|
|
capabilities: null,
|
|
models: [{ value: "opencode/big-pickle", label: "Big Pickle" }],
|
|
reasoningEfforts: [],
|
|
reasoningConfigurable: true,
|
|
reasoningOptionsByModel: {
|
|
"opencode/big-pickle": { defaultValue: null, options: [] }
|
|
},
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: true,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1
|
|
};
|
|
|
|
expect(sanitizeComposerSettingsForOptions(settings, options)).toMatchObject(
|
|
{
|
|
model: "opencode/big-pickle",
|
|
reasoningEffort: null
|
|
}
|
|
);
|
|
});
|
|
|
|
it("uses the selected model reasoning default when the provider-level config reflects another model", () => {
|
|
const settings = {
|
|
model: "openai/gpt-5.6-sol-pro",
|
|
reasoningEffort: null
|
|
};
|
|
const options = {
|
|
provider: "opencode",
|
|
capabilities: null,
|
|
models: [{ value: "openai/gpt-5.6-sol-pro", label: "GPT-5.6 Sol Pro" }],
|
|
reasoningEfforts: [],
|
|
reasoningConfigurable: false,
|
|
reasoningOptionsByModel: {
|
|
"openai/gpt-5.6-sol-pro": {
|
|
defaultValue: "high",
|
|
options: [
|
|
{ value: "off", label: "Off" },
|
|
{ value: "low", label: "Low" },
|
|
{ value: "high", label: "High" }
|
|
]
|
|
}
|
|
},
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: true,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1
|
|
};
|
|
|
|
expect(sanitizeComposerSettingsForOptions(settings, options)).toMatchObject(
|
|
{
|
|
model: "openai/gpt-5.6-sol-pro",
|
|
reasoningEffort: "high"
|
|
}
|
|
);
|
|
});
|
|
|
|
it("clears reasoning effort when the target does not advertise it", () => {
|
|
const settings = { model: "gpt-5.2", reasoningEffort: "high" };
|
|
const cleared = sanitizeComposerSettingsForOptions(settings, {
|
|
provider: "cursor",
|
|
capabilities: null,
|
|
models: [{ value: "gpt-5.2", label: "gpt-5.2" }],
|
|
reasoningEfforts: [],
|
|
reasoningConfigurable: false,
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: true,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: false,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1
|
|
});
|
|
|
|
expect(cleared.model).toBe("gpt-5.2");
|
|
expect(cleared.reasoningEffort).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("composer model binding enforcement", () => {
|
|
const options = (
|
|
models: string[],
|
|
overrides: Partial<AgentActivityComposerOptions> = {}
|
|
): AgentActivityComposerOptions => ({
|
|
provider: "codex",
|
|
capabilities: null,
|
|
models: models.map((value) => ({ value, label: value })),
|
|
reasoningEfforts: [],
|
|
speeds: [],
|
|
skills: [],
|
|
behavior: {
|
|
collapseModelOptionsToLatest: false,
|
|
modelOptionsAuthoritative: false,
|
|
refreshModelOptionsAfterSettings: false,
|
|
prewarmDraftSession: false,
|
|
planModeExclusiveWithPermissionMode: false
|
|
},
|
|
loadedAtUnixMs: 1,
|
|
...overrides
|
|
});
|
|
|
|
it("classifies bare model ids against the native list", () => {
|
|
expect(verifyComposerModelAgainstNativeOptions("m", null)).toBe(
|
|
"unverifiable"
|
|
);
|
|
expect(verifyComposerModelAgainstNativeOptions("m", options([]))).toBe(
|
|
"unverifiable"
|
|
);
|
|
expect(
|
|
verifyComposerModelAgainstNativeOptions(
|
|
"m",
|
|
options(["m", "other"], { modelOptionsLoading: true })
|
|
)
|
|
).toBe("unverifiable");
|
|
expect(
|
|
verifyComposerModelAgainstNativeOptions("m", options(["m", "other"]))
|
|
).toBe("verified");
|
|
expect(
|
|
verifyComposerModelAgainstNativeOptions("m", options(["other"]))
|
|
).toBe("rejected");
|
|
});
|
|
|
|
it("excludes requested-origin entries from catalog testimony", () => {
|
|
// Warm-catalog append: the daemon adds the requested model to a settled
|
|
// multi-entry list with a provenance marker. The marked entry must not
|
|
// verify the model — only the raw catalog counts.
|
|
const warm = options(["gpt-5.3-codex", "gpt-5.6-sol"], {
|
|
effectiveSettings: { model: "x-ai/grok-4.5" }
|
|
});
|
|
warm.models.push({
|
|
value: "x-ai/grok-4.5",
|
|
label: "x-ai/grok-4.5",
|
|
requested: true
|
|
});
|
|
expect(verifyComposerModelAgainstNativeOptions("x-ai/grok-4.5", warm)).toBe(
|
|
"rejected"
|
|
);
|
|
expect(verifyComposerModelAgainstNativeOptions("gpt-5.6-sol", warm)).toBe(
|
|
"verified"
|
|
);
|
|
// A list made solely of requested-origin entries has no catalog at all.
|
|
const requestedOnly = options([]);
|
|
requestedOnly.models.push({
|
|
value: "x-ai/grok-4.5",
|
|
label: "x-ai/grok-4.5",
|
|
requested: true
|
|
});
|
|
expect(
|
|
verifyComposerModelAgainstNativeOptions("x-ai/grok-4.5", requestedOnly)
|
|
).toBe("unverifiable");
|
|
});
|
|
|
|
it("treats a selected-model-only echo of the effective settings as unverifiable", () => {
|
|
// The daemon's bootstrap composer options mirror the requested settings
|
|
// as the sole model option (composerSelectedModelOptions); the list is
|
|
// seeded from the very value under verification and proves nothing.
|
|
const echo = options(["x-ai/grok-4.5"], {
|
|
effectiveSettings: { model: "x-ai/grok-4.5" }
|
|
});
|
|
expect(verifyComposerModelAgainstNativeOptions("x-ai/grok-4.5", echo)).toBe(
|
|
"unverifiable"
|
|
);
|
|
// A genuine single-entry catalog (no effective-settings mirror) still
|
|
// counts as testimony.
|
|
expect(
|
|
verifyComposerModelAgainstNativeOptions(
|
|
"x-ai/grok-4.5",
|
|
options(["x-ai/grok-4.5"])
|
|
)
|
|
).toBe("verified");
|
|
});
|
|
|
|
it("home defaults drop only positively rejected bare models", () => {
|
|
const bare = { model: "x-ai/grok-4.5", modelPlanId: null };
|
|
expect(
|
|
enforceComposerModelBindingForHomeDefaults(
|
|
bare,
|
|
options(["gpt-5.3-codex"])
|
|
)
|
|
).toMatchObject({ model: null, modelPlanId: null });
|
|
// Unverifiable windows must not destroy a stored default.
|
|
expect(enforceComposerModelBindingForHomeDefaults(bare, null)).toBe(bare);
|
|
expect(enforceComposerModelBindingForHomeDefaults(bare, options([]))).toBe(
|
|
bare
|
|
);
|
|
expect(
|
|
enforceComposerModelBindingForHomeDefaults(
|
|
{ model: "x-ai/grok-4.5", modelPlanId: "mp-relay" },
|
|
options(["gpt-5.3-codex"])
|
|
)
|
|
).toMatchObject({ model: "x-ai/grok-4.5", modelPlanId: "mp-relay" });
|
|
});
|
|
});
|