mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 19:06:06 +02:00
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>
69 lines
2.1 KiB
TypeScript
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("");
|
|
});
|
|
});
|