Files
zeyu.fz d24104f7dc feat(kb-dsh): 接入仓库工程约定并改名为公开包 bailian-kb-dsh
包名 @ali/bailian-kb-dsh → bailian-kb-dsh(公开 npm):package.json name +
cordis.patch.yml insert.name + tsdown PLUGIN_ID 三处同步(漏一处即 dsh 运行时崩)。

产物 lib/ → dist/(本仓 .gitignore 忽略 dist 不忽略 lib),连带 main/types/
exports/files/tsdown outDir 同步;.gitignore 补 *.tsbuildinfo。

依赖接 catalog(yaml/typescript/@types/node/vite-plus);测试导入 vitest →
vite-plus/test(全仓统一约定,消掉唯一的 vitest 依赖漂移)。

tsconfig 拆三件套:tsconfig.json 纯类型检查覆盖 src+tests(供 oxlint 自动发现,
含 jsx/DOM),tsconfig.build.json 产出 node 半,tsconfig.web.json 隔离检查 web 半。
补齐 tests 从未被类型检查暴露的一处 partial 输入类型错误。

根 vite.config.ts 新增两条 override:web 半 no-restricted-imports 把 tsdown 构建期
的 bundle purity gate 提前到 lint 期;全包放开 _ 前缀的 no-unused-vars。

文档:新增 docs/agents/dsh-plugin.md,AGENTS.md 项目地图/版本锁步例外/分层边界/
场景索引同步,packages.mjs 注释说明故意不进发布白名单。

格式化(单引号无分号 → 双引号加分号)由 pre-commit 的 vp check --fix 自动完成,
无法单独成 commit,一并纳入。

全仓 vp check 0 error;插件 13 文件 85 测试全绿;build + typecheck 通过。
2026-08-24 11:02:44 +08:00

125 lines
4.5 KiB
TypeScript

import { describe, expect, it, vi } from "vite-plus/test";
import type { ServiceListResponse } from "../src/api-types.js";
import type { KbClient } from "../src/client.js";
import { listServices } from "../src/services.js";
/** A page of `count` rows, all deployed unless overridden. */
function page(
count: number,
total: number,
overrides: Record<string, unknown> = {},
): ServiceListResponse {
return {
code: "Success",
data: {
total_count: total,
rows: Array.from({ length: count }, (_row, index) => ({
agent_id: `aid-${index}`,
agent_name: `service-${index}`,
agent_status: "deployed",
modify_time: "2026-08-20T10:00:00",
...overrides,
})),
},
};
}
/** A client whose postJson is driven by a queue of per-call responses or errors. */
function clientReturning(...outcomes: (ServiceListResponse | Error)[]): {
client: KbClient;
postJson: ReturnType<typeof vi.fn>;
} {
const postJson = vi.fn(async () => {
const next = outcomes.shift();
if (next === undefined) throw new Error("unexpected extra request");
if (next instanceof Error) throw next;
return next;
});
return { client: { postJson } as unknown as KbClient, postJson };
}
describe("listServices", () => {
it("queries both scenes for deployed services only and tags each entry with its scene", async () => {
const { client, postJson } = clientReturning(page(1, 1), page(1, 1));
const result = await listServices(client);
expect(postJson.mock.calls[0]?.[1]).toMatchObject({
agent_scene: "search",
// Verified server-side: this filter is honored and excludes drafts.
agent_status: "deployed",
page_number: 1,
page_size: 100,
});
expect(postJson.mock.calls[1]?.[1]).toMatchObject({ agent_scene: "chat" });
expect(result.entries.map((e) => e.scene)).toEqual(["search", "chat"]);
expect(result.total).toBe(2);
expect(result.truncated).toBe(false);
expect(result.failedScenes).toEqual([]);
});
it("stops at a short page without asking for another", async () => {
// 3 rows on a 100-row page is the last page; a second request would be waste.
const { client, postJson } = clientReturning(page(3, 3), page(0, 0));
const result = await listServices(client);
expect(postJson).toHaveBeenCalledTimes(2); // one per scene, not one per page
expect(result.entries).toHaveLength(3);
});
it("caps at two pages per scene and reports the shortfall as truncated", async () => {
// A workspace claiming 500 rows: fetch 200, flag the rest as unfetched.
const { client, postJson } = clientReturning(page(100, 500), page(100, 500), page(0, 0));
const result = await listServices(client);
// 2 pages for search + 1 short page for chat: the cap holds.
expect(postJson).toHaveBeenCalledTimes(3);
expect(result.entries).toHaveLength(200);
expect(result.truncated).toBe(true);
});
it("keeps one scene when the other fails instead of losing the whole list", async () => {
const { client } = clientReturning(page(2, 2), new Error("chat scene exploded"));
const result = await listServices(client);
expect(result.entries).toHaveLength(2);
expect(result.entries.every((e) => e.scene === "search")).toBe(true);
expect(result.failedScenes).toEqual(["chat"]);
});
it("reports both scenes as failed without throwing", async () => {
const { client } = clientReturning(new Error("down"), new Error("down"));
const result = await listServices(client);
expect(result.entries).toEqual([]);
expect(result.failedScenes).toEqual(["search", "chat"]);
});
it("drops rows without an agent_id and never reads pipeline_list", async () => {
// pipeline_list is unreliable in production (missing names, sometimes empty),
// so entries must not carry any knowledge-base label derived from it.
const { client } = clientReturning(
{
code: "Success",
data: {
total_count: 2,
rows: [
{ agent_name: "no id", agent_status: "deployed" },
{
agent_id: "aid-1",
agent_name: "ok",
agent_status: "deployed",
pipeline_list: [{ pipeline_id: "p1" }],
},
],
},
},
page(0, 0),
);
const result = await listServices(client);
expect(result.entries).toHaveLength(1);
expect(result.entries[0]).toEqual({
agent_id: "aid-1",
agent_name: "ok",
scene: "search",
status: "deployed",
});
expect(JSON.stringify(result.entries)).not.toContain("p1");
});
});