feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
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>
This commit is contained in:
191
apps/web/lib/settings/kimiOauth.test.ts
Normal file
191
apps/web/lib/settings/kimiOauth.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
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("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user