M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
192 lines
5.0 KiB
TypeScript
192 lines
5.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { JobView, PollState } from "@/lib/jobs/job";
|
|
import {
|
|
authOpenUrl,
|
|
connectPhase,
|
|
formatExpiresAt,
|
|
jobConnected,
|
|
KIMI_CODE_MODEL,
|
|
KIMI_CODE_PROVIDER,
|
|
toConnectionStatus,
|
|
toDeviceDisplay,
|
|
} from "./kimiOauth";
|
|
|
|
const poll = (over: Partial<PollState>): PollState => ({
|
|
status: "polling",
|
|
progress: 0,
|
|
job: null,
|
|
error: null,
|
|
...over,
|
|
});
|
|
|
|
const job = (over: Partial<JobView>): JobView => ({
|
|
id: "j1",
|
|
kind: "kimi_oauth",
|
|
status: "done",
|
|
progress: 100,
|
|
result: null,
|
|
error: null,
|
|
...over,
|
|
});
|
|
|
|
describe("KIMI_CODE constants", () => {
|
|
it("uses the OAuth provider + coding model names", () => {
|
|
expect(KIMI_CODE_PROVIDER).toBe("kimi-code");
|
|
expect(KIMI_CODE_MODEL).toBe("kimi-for-coding");
|
|
});
|
|
});
|
|
|
|
describe("toDeviceDisplay", () => {
|
|
it("maps start response fields", () => {
|
|
const d = toDeviceDisplay({
|
|
job_id: "abc",
|
|
user_code: "WXYZ-1234",
|
|
verification_uri: "https://auth.kimi.com/device",
|
|
verification_uri_complete: "https://auth.kimi.com/device?code=WXYZ-1234",
|
|
expires_in: 600,
|
|
interval: 5,
|
|
});
|
|
expect(d).toEqual({
|
|
userCode: "WXYZ-1234",
|
|
verificationUri: "https://auth.kimi.com/device",
|
|
verificationUriComplete: "https://auth.kimi.com/device?code=WXYZ-1234",
|
|
expiresIn: 600,
|
|
interval: 5,
|
|
});
|
|
});
|
|
|
|
it("defaults missing complete uri to null", () => {
|
|
const d = toDeviceDisplay({
|
|
job_id: "abc",
|
|
user_code: "CODE",
|
|
verification_uri: "https://auth.kimi.com/device",
|
|
verification_uri_complete: null,
|
|
expires_in: 600,
|
|
interval: 5,
|
|
});
|
|
expect(d.verificationUriComplete).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("authOpenUrl", () => {
|
|
it("prefers the complete uri when present", () => {
|
|
expect(
|
|
authOpenUrl({
|
|
userCode: "C",
|
|
verificationUri: "https://auth.kimi.com/device",
|
|
verificationUriComplete: "https://auth.kimi.com/device?code=C",
|
|
expiresIn: 600,
|
|
interval: 5,
|
|
}),
|
|
).toBe("https://auth.kimi.com/device?code=C");
|
|
});
|
|
|
|
it("falls back to the plain uri", () => {
|
|
expect(
|
|
authOpenUrl({
|
|
userCode: "C",
|
|
verificationUri: "https://auth.kimi.com/device",
|
|
verificationUriComplete: null,
|
|
expiresIn: 600,
|
|
interval: 5,
|
|
}),
|
|
).toBe("https://auth.kimi.com/device");
|
|
});
|
|
});
|
|
|
|
describe("toConnectionStatus", () => {
|
|
it("narrows connected + expires_at", () => {
|
|
expect(
|
|
toConnectionStatus({ connected: true, expires_at: "2026-07-01T00:00:00Z" }),
|
|
).toEqual({ connected: true, expiresAt: "2026-07-01T00:00:00Z" });
|
|
});
|
|
|
|
it("defaults non-string expires_at to null", () => {
|
|
expect(toConnectionStatus({ connected: false })).toEqual({
|
|
connected: false,
|
|
expiresAt: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("jobConnected", () => {
|
|
it("true only when result.connected === true (no token expected)", () => {
|
|
expect(jobConnected(job({ result: { connected: true, provider: "kimi-code" } }))).toBe(
|
|
true,
|
|
);
|
|
expect(jobConnected(job({ result: { connected: false } }))).toBe(false);
|
|
expect(jobConnected(job({ result: null }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("connectPhase", () => {
|
|
it("not started + not connected → idle", () => {
|
|
expect(
|
|
connectPhase({ started: false, connected: false, poll: poll({}) }),
|
|
).toBe("idle");
|
|
});
|
|
|
|
it("not started + connected (status query) → connected", () => {
|
|
expect(
|
|
connectPhase({ started: false, connected: true, poll: poll({}) }),
|
|
).toBe("connected");
|
|
});
|
|
|
|
it("started + polling → awaiting", () => {
|
|
expect(
|
|
connectPhase({
|
|
started: true,
|
|
connected: false,
|
|
poll: poll({ status: "polling" }),
|
|
}),
|
|
).toBe("awaiting");
|
|
});
|
|
|
|
it("started + done + job.connected → connected", () => {
|
|
expect(
|
|
connectPhase({
|
|
started: true,
|
|
connected: false,
|
|
poll: poll({
|
|
status: "done",
|
|
job: job({ result: { connected: true, provider: "kimi-code" } }),
|
|
}),
|
|
}),
|
|
).toBe("connected");
|
|
});
|
|
|
|
it("started + done but job not connected → error", () => {
|
|
expect(
|
|
connectPhase({
|
|
started: true,
|
|
connected: false,
|
|
poll: poll({ status: "done", job: job({ result: { connected: false } }) }),
|
|
}),
|
|
).toBe("error");
|
|
});
|
|
|
|
it("started + poll error (expired/denied/network) → error", () => {
|
|
expect(
|
|
connectPhase({
|
|
started: true,
|
|
connected: false,
|
|
poll: poll({ status: "error", error: "授权已过期" }),
|
|
}),
|
|
).toBe("error");
|
|
});
|
|
});
|
|
|
|
describe("formatExpiresAt", () => {
|
|
it("returns null for null/invalid input", () => {
|
|
expect(formatExpiresAt(null)).toBeNull();
|
|
expect(formatExpiresAt("not-a-date")).toBeNull();
|
|
});
|
|
|
|
it("formats a valid ISO timestamp to a non-empty string", () => {
|
|
const out = formatExpiresAt("2026-07-01T08:30:00Z");
|
|
expect(typeof out).toBe("string");
|
|
expect(out).not.toBe("");
|
|
});
|
|
});
|