Files
kandev/apps/web/lib/api/domains/session-api.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

44 lines
1.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
const requestMock = vi.fn();
let clientFactory: () => { request: typeof requestMock } | null = () => ({ request: requestMock });
vi.mock("@/lib/ws/connection", () => ({
getWebSocketClient: () => clientFactory(),
}));
// Imported after the mock so it picks up the mocked module.
const { renameSession } = await import("./session-api");
beforeEach(() => {
requestMock.mockReset();
requestMock.mockResolvedValue({ success: true });
clientFactory = () => ({ request: requestMock });
});
describe("renameSession", () => {
it("sends session.rename with the session id and name", async () => {
await renameSession("sess-1", "reviewer");
expect(requestMock).toHaveBeenCalledWith("session.rename", {
session_id: "sess-1",
name: "reviewer",
});
});
it("passes an empty name through so the custom label can be cleared", async () => {
await renameSession("sess-1", "");
expect(requestMock).toHaveBeenCalledWith("session.rename", { session_id: "sess-1", name: "" });
});
it("throws when the WebSocket client is unavailable", async () => {
clientFactory = () => null;
await expect(renameSession("sess-1", "reviewer")).rejects.toThrow("WebSocket unavailable");
expect(requestMock).not.toHaveBeenCalled();
});
it("propagates backend errors to the caller", async () => {
requestMock.mockRejectedValueOnce(new Error("session not found"));
await expect(renameSession("missing", "x")).rejects.toThrow("session not found");
});
});