252 lines
9.1 KiB
TypeScript
252 lines
9.1 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { NextRequest } from "next/server";
|
|
|
|
let runtimeRoot = "";
|
|
let openAiOnboardRoute: (typeof import("../src/app/api/v1/accounts/onboard/openai-api/route"))["POST"];
|
|
let aliyunQwenOnboardRoute: (typeof import("../src/app/api/v1/accounts/onboard/aliyun-qwen/route"))["POST"];
|
|
let masterNodeOnboardRoute: (typeof import("../src/app/api/v1/accounts/onboard/master-node/route"))["POST"];
|
|
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
|
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
|
let AUTH_SESSION_COOKIE: string;
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-ai-onboard-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
|
|
const [openAiModule, aliyunModule, masterNodeModule, data, auth] = await Promise.all([
|
|
import("../src/app/api/v1/accounts/onboard/openai-api/route.ts"),
|
|
import("../src/app/api/v1/accounts/onboard/aliyun-qwen/route.ts"),
|
|
import("../src/app/api/v1/accounts/onboard/master-node/route.ts"),
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
]);
|
|
|
|
openAiOnboardRoute = openAiModule.POST;
|
|
aliyunQwenOnboardRoute = aliyunModule.POST;
|
|
masterNodeOnboardRoute = masterNodeModule.POST;
|
|
createAuthSession = data.createAuthSession;
|
|
readState = data.readState;
|
|
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) {
|
|
await rm(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function createAuthedJsonRequest(url: string, body: Record<string, unknown>) {
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
return new NextRequest(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
test("POST /api/v1/accounts/onboard/openai-api creates a primary openai account and activates it", async () => {
|
|
await setup();
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async (input, init) => {
|
|
if (typeof input === "string" && input === "https://api.openai.com/v1/responses") {
|
|
assert.equal(init?.method, "POST");
|
|
return new Response(JSON.stringify({ output_text: "连接正常" }), {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-request-id": "req-onboard-openai",
|
|
},
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const response = await openAiOnboardRoute(
|
|
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/onboard/openai-api", {
|
|
label: "主 GPT",
|
|
displayName: "OpenAI 平台账号",
|
|
accountIdentifier: "sk-proj-demo",
|
|
model: "gpt-5.4",
|
|
apiKey: "sk-live-demo-123456",
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
accountId: string;
|
|
message: string;
|
|
activeIdentity: { accountId: string; provider: string; displayName: string };
|
|
};
|
|
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.accountId, "openai-api-primary");
|
|
assert.equal(payload.activeIdentity.accountId, "openai-api-primary");
|
|
assert.equal(payload.activeIdentity.provider, "openai_api");
|
|
assert.match(payload.message, /已登录/);
|
|
assert.match(payload.message, /当前主控/);
|
|
|
|
const state = await readState();
|
|
const account = state.aiAccounts.find((item) => item.accountId === "openai-api-primary");
|
|
assert.ok(account, "expected openai primary account to be created");
|
|
assert.equal(account?.provider, "openai_api");
|
|
assert.equal(account?.role, "primary");
|
|
assert.equal(account?.isActive, true);
|
|
assert.equal(account?.status, "ready");
|
|
assert.equal(account?.displayName, "OpenAI 平台账号");
|
|
assert.equal(account?.apiKey, "sk-live-demo-123456");
|
|
assert.match(account?.apiKeyMasked ?? "", /\.\.\./);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("POST /api/v1/accounts/onboard/master-node upserts a master node account and returns bound-device guidance", async () => {
|
|
await setup();
|
|
|
|
const response = await masterNodeOnboardRoute(
|
|
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/onboard/master-node", {
|
|
label: "主 GPT",
|
|
displayName: "Mac 上的 Master Codex Node",
|
|
accountIdentifier: "17600003315",
|
|
nodeId: "mac-studio",
|
|
nodeLabel: "Mac Studio",
|
|
model: "gpt-5.4",
|
|
setActive: true,
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
accountId: string;
|
|
message: string;
|
|
validation: { status: string; message: string };
|
|
};
|
|
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.accountId, "master-codex-primary");
|
|
assert.match(payload.message, /当前主控|已绑定/);
|
|
assert.equal(payload.validation.status, "ready");
|
|
assert.match(payload.validation.message, /不在手机里直接登录/);
|
|
assert.match(payload.validation.message, /Mac Studio|绑定设备/);
|
|
|
|
const state = await readState();
|
|
const account = state.aiAccounts.find((item) => item.accountId === "master-codex-primary");
|
|
assert.ok(account, "expected master node primary account");
|
|
assert.equal(account?.provider, "master_codex_node");
|
|
assert.equal(account?.displayName, "Mac 上的 Master Codex Node");
|
|
assert.equal(account?.nodeId, "mac-studio");
|
|
assert.equal(account?.nodeLabel, "Mac Studio");
|
|
assert.equal(account?.isActive, true);
|
|
});
|
|
|
|
test("POST /api/v1/accounts/onboard/openai-api returns a clear network guidance when OpenAI is unreachable", async () => {
|
|
await setup();
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async () => {
|
|
const error = new TypeError("fetch failed");
|
|
(error as TypeError & { cause?: { code?: string; message?: string } }).cause = {
|
|
code: "ENETUNREACH",
|
|
message: "connect ENETUNREACH api.openai.com",
|
|
};
|
|
throw error;
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const response = await openAiOnboardRoute(
|
|
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/onboard/openai-api", {
|
|
label: "主 GPT",
|
|
displayName: "OpenAI 平台账号",
|
|
accountIdentifier: "sk-proj-demo",
|
|
model: "gpt-5.4",
|
|
apiKey: "sk-live-demo-123456",
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.status, 400);
|
|
const payload = (await response.json()) as { ok: boolean; message: string };
|
|
assert.equal(payload.ok, false);
|
|
assert.match(payload.message, /无法访问 api\.openai\.com|无法连接 OpenAI API/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("POST /api/v1/accounts/onboard/aliyun-qwen creates a backup aliyun qwen account", async () => {
|
|
await setup();
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async (input, init) => {
|
|
if (typeof input === "string" && input === "https://dashscope.aliyuncs.com/compatible-mode/v1/responses") {
|
|
assert.equal(init?.method, "POST");
|
|
return new Response(JSON.stringify({ output_text: "连接正常" }), {
|
|
status: 200,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-request-id": "req-onboard-aliyun",
|
|
},
|
|
});
|
|
}
|
|
throw new Error(`unexpected fetch: ${String(input)}`);
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const response = await aliyunQwenOnboardRoute(
|
|
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/onboard/aliyun-qwen", {
|
|
label: "阿里备用",
|
|
displayName: "阿里百炼备用账号",
|
|
accountIdentifier: "dashscope-demo",
|
|
model: "qwen3.5-plus",
|
|
apiKey: "sk-aliyun-demo-123456",
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
accountId: string;
|
|
message: string;
|
|
account: { provider: string; role: string; model: string; isActive: boolean };
|
|
};
|
|
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.accountId, "aliyun-qwen-backup");
|
|
assert.equal(payload.account.provider, "aliyun_qwen_api");
|
|
assert.equal(payload.account.role, "backup");
|
|
assert.equal(payload.account.model, "qwen3.5-plus");
|
|
assert.equal(payload.account.isActive, false);
|
|
assert.match(payload.message, /备用/);
|
|
assert.match(payload.message, /阿里|百炼/);
|
|
|
|
const state = await readState();
|
|
const account = state.aiAccounts.find((item) => item.accountId === "aliyun-qwen-backup");
|
|
assert.ok(account, "expected aliyun backup account to be created");
|
|
assert.equal(account?.provider, "aliyun_qwen_api");
|
|
assert.equal(account?.role, "backup");
|
|
assert.equal(account?.model, "qwen3.5-plus");
|
|
assert.equal(account?.apiKey, "sk-aliyun-demo-123456");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|