Files
multica/packages/core/drafts/create-draft-store.test.ts
Naiyuan Qing e5c6e732b3 feat(drafts): unified draft lifecycle + upload ownership inversion (MUL-5181)
Unify how every composer preserves unsent work, sends, and handles uploads.

L1 foundation (packages/core/drafts):
- createDraftStore factory + self-registering cleanup-registry replacing the
  hand-maintained WORKSPACE_SCOPED_KEYS list; register-all-drafts guarantees
  registration completeness. Fixes the confirmed cross-user draft leak
  (persistence + in-memory) on logout / workspace delete.

L3 send paradigm:
- useComposerSubmit: one await-then-render contract (lock/spin, keep-on-fail,
  clear-on-success, single-flight, submit-time upload-gate), adopted by
  comment/reply/edit, create-issue, quick-create, and chat.

Per-surface:
- Comment/Reply/Edit: attachments moved into the persisted draft.
- Create Issue: draft split into shared/manual/agent/activeMode with
  non-destructive mode switching + migration for old flat drafts.
- Chat: optimistic send converted to await-then-render (kept server-driven
  cancel restore_to_input); chat draft keys registered for cleanup.

L2 upload coordinator (ownership inversion, Linear-validated shape):
- upload-coordinator + DraftUpload placeholder: uploads owned by a module
  coordinator that outlives the component, state persisted in the draft;
  AbortController + abort-on-logout; interrupted-on-reload. Comment surface
  fully wired. Create-issue/chat upload wiring is a documented residual.

Verified: core + views typecheck clean; core 1064 + views 2928 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 13:40:50 +08:00

69 lines
2.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { createDraftStore } from "./create-draft-store";
import {
resetAllRegisteredDrafts,
__clearDraftCleanupRegistryForTest,
__getRegisteredDraftKeysForTest,
} from "./cleanup-registry";
interface Sample {
title: string;
tags: string[];
}
const EMPTY: Sample = { title: "", tags: [] };
beforeEach(() => {
__clearDraftCleanupRegistryForTest();
});
describe("createDraftStore", () => {
it("merges patches and reports meaningful drafts via hasMeaningful", () => {
const useStore = createDraftStore<Sample>({
storageKey: "t_merge",
emptyData: EMPTY,
hasMeaningful: (d) => !!d.title || d.tags.length > 0,
workspaceScoped: false,
});
expect(useStore.getState().hasDraft()).toBe(false);
useStore.getState().setDraft({ title: "hi" });
expect(useStore.getState().draft).toEqual({ title: "hi", tags: [] });
expect(useStore.getState().hasDraft()).toBe(true);
});
it("clearDraft resets to a fresh empty draft that does not share nested references", () => {
const useStore = createDraftStore<Sample>({
storageKey: "t_clear",
emptyData: EMPTY,
hasMeaningful: (d) => d.tags.length > 0,
workspaceScoped: false,
});
useStore.getState().setDraft({ tags: ["a"] });
useStore.getState().clearDraft();
expect(useStore.getState().draft.tags).toEqual([]);
// Mutating the cleared array must not corrupt EMPTY for the next clear.
useStore.getState().draft.tags.push("leak");
useStore.getState().setDraft({ tags: ["b"] });
useStore.getState().clearDraft();
expect(useStore.getState().draft.tags).toEqual([]);
});
it("self-registers for cleanup and reset wipes in-memory state", () => {
const useStore = createDraftStore<Sample>({
storageKey: "t_register",
emptyData: EMPTY,
hasMeaningful: (d) => !!d.title,
workspaceScoped: false,
});
expect(__getRegisteredDraftKeysForTest()).toContain("t_register");
useStore.getState().setDraft({ title: "leaky" });
resetAllRegisteredDrafts();
expect(useStore.getState().draft.title).toBe("");
});
});