- 薄自建 LLM 网关:OpenAI 兼容适配器(DeepSeek) + instructor 结构化输出 + usage_ledger 记账 + 档位路由 - 记忆服务 assemble:确定性选择(显式+主角+近况) + 渲染卡 + 缓存断点(中性文本) - LangGraph 写章节点 + Postgres checkpointer + SSE 归一(token/done/error) - API:立项 + 写章 draft(SSE) + PUT 自动保存 + 提供商凭据(Fernet 加密/测试连接) - 前端:AppShell + 作品库 + 5 步立项向导 + 写作工作台(流式打字机+自动保存) + 设置页 - M1 E2E:真实 DB + mock 网关零 token 走通闭环
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { createDebouncedSaver, formatSavedAt } from "./autosave";
|
|
|
|
describe("createDebouncedSaver", () => {
|
|
beforeEach(() => vi.useFakeTimers());
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it("saves only once after the debounce window for rapid edits", () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
const saver = createDebouncedSaver(save, 1000);
|
|
saver.schedule("a");
|
|
saver.schedule("ab");
|
|
saver.schedule("abc");
|
|
expect(save).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1000);
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(save).toHaveBeenCalledWith("abc");
|
|
});
|
|
|
|
it("flush triggers the pending save immediately", () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
const saver = createDebouncedSaver(save, 1000);
|
|
saver.schedule("hello");
|
|
saver.flush();
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(save).toHaveBeenCalledWith("hello");
|
|
// No double-fire when the timer would have elapsed.
|
|
vi.advanceTimersByTime(1000);
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("cancel prevents a scheduled save", () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
const saver = createDebouncedSaver(save, 1000);
|
|
saver.schedule("x");
|
|
saver.cancel();
|
|
vi.advanceTimersByTime(2000);
|
|
expect(save).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("each new schedule resets the timer (only last value persists)", () => {
|
|
const save = vi.fn().mockResolvedValue(undefined);
|
|
const saver = createDebouncedSaver(save, 1000);
|
|
saver.schedule("one");
|
|
vi.advanceTimersByTime(800);
|
|
saver.schedule("two");
|
|
vi.advanceTimersByTime(800);
|
|
expect(save).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(200);
|
|
expect(save).toHaveBeenCalledTimes(1);
|
|
expect(save).toHaveBeenCalledWith("two");
|
|
});
|
|
});
|
|
|
|
describe("formatSavedAt", () => {
|
|
it("formats as 保存于 hh:mm zero-padded", () => {
|
|
const d = new Date(2026, 5, 18, 9, 4);
|
|
expect(formatSavedAt(d)).toBe("保存于 09:04");
|
|
});
|
|
});
|