Files
kandev/apps/web/components/task/session-tab-title.test.ts
T
João Salavisa 9381a6a9f7 feat(mcp): session spawning, sibling messaging, and renameable tabs (#1734)
* fix(scripts): launch dev-isolated backend via __backend subcommand

The kandev binary became a bundle launcher with the restart supervisor
(ADR 0019) and exits without KANDEV_BUNDLE_DIR when invoked bare; the
isolated dev instance must run the backend entrypoint directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): session spawning, sibling messaging, and renameable tabs

- spawn_session_kandev spawns an additional agent session on an existing
  task (default: the caller's own) via LaunchSession(IntentStart), with
  optional agent_profile_id (defaults to the spawner's profile) and a
  session name; the initial prompt carries spawner attribution and reply
  instructions.
- message_task_kandev accepts an optional session_id to target a specific
  session, enabling same-task sibling messaging. Explicit targets are
  pinned through dispatch so the idle-path primary-session re-resolution
  cannot reroute a sibling message back to its sender (regression-tested).
- task_sessions gains a name column (migration ordered after the
  agent_execution_id-drop rebuild), a session.rename WS action, and inline
  tab rename UI; custom names take precedence over derived agent/model
  labels across the tab, agents dropdown, mobile list, chat header, and
  dockview panel titles.
- Inter-session messages stamp sender_session_name into metadata so the
  sender badge shows which session sent the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): address review findings on session spawn/rename/messaging

- include name in task-session DTOs (list + full, no omitempty) so
  persisted tab names survive list hydration and clears propagate
- sanitize sender task/session ids embedded in <kandev-system> blocks
- reject cross-workspace spawn targets; require sender_task_id
- truncate session names by runes, not bytes
- distinguish not-found from internal errors in message target lookup
- guard TabRenameInput blur-commit after Enter/Escape resolution
- cap optimistic rename length client-side and toast on rename failure
- add tests: spawn launch-failure and cross-workspace paths, renameSession
  API client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): round-two review fixes for spawn/rename lint and hardening

- extract spawn validation/authorization helpers (cyclop/nestif limits)
- make stripSystemTag replace until stable to block nested tag evasion
- load session before rename so a durable rename can never skip its
  broadcast; surface lookup failures to the caller instead
- drop redundant sessionLauncher nil check (guarded registration)
- add maxLength to the rename input matching the server cap
- add tests: nested sanitizer evasion, spawn rename-failure tolerance

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:26:50 +01:00

111 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { resolveSessionTabTitle } from "./session-tab-title";
const SPARK_MODEL_ID = "gpt-5.3-codex-spark";
const SPARK_MODEL_NAME = "GPT-5.3-Codex-Spark";
const PROFILE_LABEL = "GPT-5.5 (medium)";
const baseArgs = {
agentLabel: PROFILE_LABEL,
activeModelId: null,
currentModelId: null,
snapshotModel: null,
modelOptions: [],
configOptions: [],
};
describe("resolveSessionTabTitle", () => {
it("prefers the user-supplied custom name over every derived title", () => {
expect(
resolveSessionTabTitle({
...baseArgs,
customName: "reviewer",
activeModelId: SPARK_MODEL_ID,
modelOptions: [{ id: SPARK_MODEL_ID, name: SPARK_MODEL_NAME }],
}),
).toBe("reviewer");
});
it("falls back to derived titles when the custom name is empty or absent", () => {
expect(resolveSessionTabTitle({ ...baseArgs, customName: "" })).toBe(PROFILE_LABEL);
expect(resolveSessionTabTitle({ ...baseArgs, customName: null })).toBe(PROFILE_LABEL);
});
it("uses the agent label over live model state when a profile label is available", () => {
expect(
resolveSessionTabTitle({
...baseArgs,
activeModelId: SPARK_MODEL_ID,
modelOptions: [{ id: SPARK_MODEL_ID, name: SPARK_MODEL_NAME }],
}),
).toBe(PROFILE_LABEL);
});
it("includes non-model config selections in the title", () => {
expect(
resolveSessionTabTitle({
...baseArgs,
agentLabel: null,
activeModelId: SPARK_MODEL_ID,
configOptions: [
{
type: "select",
id: "model",
name: "Model",
currentValue: "gpt-5.5",
options: [
{ value: "gpt-5.5", name: "GPT-5.5" },
{ value: SPARK_MODEL_ID, name: SPARK_MODEL_NAME },
],
},
{
type: "select",
id: "effort",
name: "Effort",
currentValue: "high",
options: [
{ value: "medium", name: "Medium" },
{ value: "high", name: "High" },
],
},
],
}),
).toBe(`${SPARK_MODEL_NAME} / High`);
});
it("falls back to the agent label when live model state is unavailable", () => {
expect(resolveSessionTabTitle(baseArgs)).toBe(PROFILE_LABEL);
});
it("falls back to currentModelId when active model id is missing", () => {
expect(
resolveSessionTabTitle({
...baseArgs,
agentLabel: null,
currentModelId: SPARK_MODEL_ID,
modelOptions: [{ id: SPARK_MODEL_ID, name: SPARK_MODEL_NAME }],
}),
).toBe(SPARK_MODEL_NAME);
});
it("falls back to snapshot model when both agent and live model states are unavailable", () => {
expect(
resolveSessionTabTitle({
...baseArgs,
agentLabel: null,
snapshotModel: SPARK_MODEL_ID,
modelOptions: [{ id: SPARK_MODEL_ID, name: SPARK_MODEL_NAME }],
}),
).toBe(SPARK_MODEL_NAME);
});
it("keeps the agent label over the start-time snapshot model", () => {
expect(
resolveSessionTabTitle({
...baseArgs,
snapshotModel: "gpt-5.5",
}),
).toBe(PROFILE_LABEL);
});
});