test(knowledge): 补全知识库相关命令参数并增加E2E测试覆盖

- 添加知识库列表、创建、删除及文件删除等命令路由
- 新增知识块、分类、文件相关参数的端到端测试,覆盖文件列表、分类列表、块新增更新及分页等功能
- 增加对知识文档状态上传、等待、轮询参数的实时测试及自清理逻辑
- 新增知识文档列表分页、过滤参数的E2E测试覆盖
- 扩展知识文档上传命令的轮询间隔参数测试,验证无错误
- 补充知识库删除命令的轮询间隔参数传递测试
- 增强知识库列表的分页、名称过滤测试用例
- 丰富知识服务命令的参数全覆盖测试,包括创建、更新、部署、删除及多版本描述等功能
- 添加知识检索命令的重新排序指令与过时参数的实时测试覆盖
This commit is contained in:
zeyu.fz
2026-08-11 13:17:34 +08:00
parent e292b20d4b
commit 1b568e8d37
9 changed files with 1132 additions and 2 deletions
@@ -1498,6 +1498,358 @@ describe.skipIf(!isKbAdminE2EReady())(
},
);
// Live parameter coverage: file-list 4 params + category-list 2 params +
// chunk add/update --content-file + chunk list --doc-id. All in one self-cleaning
// chain to minimize API calls.
describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (live, 自清理)", () => {
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
test("file list(name/file-id/max-result/next-token) + category list(parent-id/collection-id) + chunk add/update --content-file + chunk list --doc-id", async () => {
// ── setup: create category → upload file ──
const categoryName = `e2e-param-${Date.now() % 100000000}`;
const categoryAddRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"category",
"add",
"--name",
categoryName,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(categoryAddRun.exitCode, categoryAddRun.stderr).toBe(0);
const categoryId = categoryAddRun.stdout.trim();
expect(categoryId).toMatch(/^cate_/);
const fixtureDir = mkdtempSync(join(tmpdir(), "param-e2e-"));
const filePath = join(fixtureDir, `param-${Date.now()}.md`);
writeFileSync(filePath, "# e2e param file\n");
const uploadRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"doc",
"upload",
"--file",
filePath,
"--category-id",
categoryId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(uploadRun.exitCode, uploadRun.stderr).toBe(0);
const fileId = uploadRun.stdout.trim();
let indexId: string | undefined;
try {
// ── P2: file list --name (模糊匹配) ──
const fileListByNameRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"list",
"--category-id",
categoryId,
"--name",
"param-",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(fileListByNameRun.exitCode, fileListByNameRun.stderr).toBe(0);
expect(fileListByNameRun.stdout.trim().split("\n")).toContain(fileId);
// ── P2: file list --file-id ──
const fileListByFileIdRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"list",
"--category-id",
categoryId,
"--file-id",
fileId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(fileListByFileIdRun.exitCode, fileListByFileIdRun.stderr).toBe(0);
expect(fileListByFileIdRun.stdout.trim().split("\n")).toContain(fileId);
// ── P2: file list --max-result 1 ──
const fileListMaxResultRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"list",
"--category-id",
categoryId,
"--max-result",
"1",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(fileListMaxResultRun.exitCode, fileListMaxResultRun.stderr).toBe(0);
const maxResultLines = fileListMaxResultRun.stdout.trim().split("\n").filter(Boolean);
expect(maxResultLines.length).toBeLessThanOrEqual(1);
// ── P2: file list --next-token (if paginated) ──
const fileListJsonRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"list",
"--category-id",
categoryId,
"--max-result",
"1",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(fileListJsonRun.exitCode, fileListJsonRun.stderr).toBe(0);
const fileListData = parseStdoutJson<{
data?: { nextToken?: string };
}>(fileListJsonRun.stdout);
const fileNextToken = fileListData.data?.nextToken;
if (fileNextToken) {
const fileListNextTokenRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"list",
"--category-id",
categoryId,
"--next-token",
fileNextToken,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(fileListNextTokenRun.exitCode, fileListNextTokenRun.stderr).toBe(0);
}
// ── P3: category list --parent-id (using existing categoryId) ──
const listByParentRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"category",
"list",
"--parent-id",
categoryId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listByParentRun.exitCode, listByParentRun.stderr).toBe(0);
// ── P3: category list --collection-id (no-error verification) ──
const listByCollectionRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"category",
"list",
"--collection-id",
"col_nonexistent_e2e",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listByCollectionRun.exitCode, listByCollectionRun.stderr).toBe(0);
// ── create kb for chunk tests ──
const kbCreateRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
"knowledge",
"create",
"--name",
`e2e-pk-${Date.now() % 100000000}`.slice(0, 20),
"--doc-id",
fileId,
"--workspace-id",
workspaceId,
"--wait",
"--quiet",
]);
expect(kbCreateRun.exitCode, kbCreateRun.stderr).toBe(0);
indexId = kbCreateRun.stdout.trim().split("\n")[0]!;
expect(indexId).toBeTruthy();
try {
// ── P4: chunk add --content-file ──
const chunkContentFile = join(fixtureDir, `chunk-${Date.now()}.txt`);
const chunkMarker = `e2e chunk content file marker ${Date.now()}`;
writeFileSync(chunkContentFile, chunkMarker);
const chunkAddFileRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"chunk",
"add",
"--index-id",
indexId,
"--doc-id",
fileId,
"--content-file",
chunkContentFile,
"--workspace-id",
workspaceId,
]);
expect(chunkAddFileRun.exitCode, chunkAddFileRun.stderr).toBe(0);
// chunk list read-back: verify content came from file
const chunkListAfterAddRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"chunk",
"list",
"--index-id",
indexId,
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(chunkListAfterAddRun.exitCode, chunkListAfterAddRun.stderr).toBe(0);
const chunkListAfterAddData = parseStdoutJson<{
data?: {
nodes?: Array<{
metadata?: { _id?: string; content?: string };
text?: string;
}>;
};
}>(chunkListAfterAddRun.stdout);
const addedChunk = chunkListAfterAddData.data?.nodes?.find(
(node) => (node.metadata?.content ?? node.text) === chunkMarker,
);
expect(addedChunk?.metadata?.content ?? addedChunk?.text).toBe(chunkMarker);
const addedChunkId = addedChunk?.metadata?._id;
expect(addedChunkId).toBeTruthy();
// ── P6: chunk list --doc-id ──
const chunkListByDocRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"chunk",
"list",
"--index-id",
indexId,
"--doc-id",
fileId,
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(chunkListByDocRun.exitCode, chunkListByDocRun.stderr).toBe(0);
const chunkListByDocData = parseStdoutJson<{
data?: {
nodes?: Array<{
metadata?: { doc_id?: string };
}>;
};
}>(chunkListByDocRun.stdout);
// All returned chunks should belong to the specified doc
for (const node of chunkListByDocData.data?.nodes ?? []) {
expect(node.metadata?.doc_id).toContain(fileId);
}
// ── P4: chunk update --content-file ──
if (addedChunkId) {
const updateContentFile = join(fixtureDir, `chunk-update-${Date.now()}.txt`);
const updateMarker = `e2e chunk update content file marker ${Date.now()}`;
writeFileSync(updateContentFile, updateMarker);
const chunkUpdateFileRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"chunk",
"update",
"--index-id",
indexId,
"--chunk-id",
addedChunkId,
"--doc-id",
fileId,
"--content-file",
updateContentFile,
"--workspace-id",
workspaceId,
]);
expect(chunkUpdateFileRun.exitCode, chunkUpdateFileRun.stderr).toBe(0);
// chunk list read-back: verify content changed
const chunkListAfterUpdateRun = await runCommandE2e(
KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES,
[
"knowledge",
"chunk",
"list",
"--index-id",
indexId,
"--workspace-id",
workspaceId,
"--output",
"json",
],
);
expect(chunkListAfterUpdateRun.exitCode, chunkListAfterUpdateRun.stderr).toBe(0);
const chunkListAfterUpdateData = parseStdoutJson<{
data?: {
nodes?: Array<{
metadata?: { _id?: string; content?: string };
text?: string;
}>;
};
}>(chunkListAfterUpdateRun.stdout);
const updatedChunk = chunkListAfterUpdateData.data?.nodes?.find(
(node) => node.metadata?._id === addedChunkId,
);
expect(updatedChunk?.metadata?.content ?? updatedChunk?.text).toBe(updateMarker);
}
} finally {
// cleanup kb (retry for IndexStatusError)
if (indexId) {
let cleanupRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
"knowledge",
"delete",
"--index-id",
indexId,
"--yes",
"--workspace-id",
workspaceId,
]);
for (let retry = 0; retry < 5 && cleanupRun.exitCode !== 0; retry++) {
if (!cleanupRun.stderr.includes("IndexStatusError")) break;
await new Promise((resolve) => setTimeout(resolve, 10_000));
cleanupRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
"knowledge",
"delete",
"--index-id",
indexId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
}
}
} finally {
// cleanup file + category
await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"delete",
"--file-id",
fileId,
"--yes",
"--workspace-id",
workspaceId,
]);
await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"category",
"delete",
"--category-id",
categoryId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
// kb create --wait adds a full import phase — generous timeout
}, 600_000);
});
interface ChunkListNodes {
data?: { nodes?: Array<{ metadata?: Record<string, unknown> }> };
}
@@ -1771,4 +2123,103 @@ describe.skipIf(!isOssImportE2EReady())("e2e: doc import-oss live (授权 bucket
expect(customDetail.data.connectorName).toBe(connectorName);
expect(customDetail.data.connectorType).toBe("FILE");
}, 60_000);
test("import-oss --category-id --tag → file list + file get 读回验证", async () => {
// P5: verify --category-id and --tag params land on the server via read-back
const categoryName = `e2e-oss-cat-${Date.now() % 100000000}`;
const categoryAddRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"category",
"add",
"--name",
categoryName,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(categoryAddRun.exitCode, categoryAddRun.stderr).toBe(0);
const categoryId = categoryAddRun.stdout.trim();
expect(categoryId).toMatch(/^cate_/);
try {
const importRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"doc",
"import-oss",
"--bucket",
ossBucket,
"--region",
ossRegion,
"--oss-key",
ossKey,
"--category-id",
categoryId,
"--tag",
"e2e-oss-tag",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(importRun.exitCode, importRun.stderr).toBe(0);
const fileId = importRun.stdout.trim();
expect(fileId).toMatch(/^file_/);
try {
// file list --category-id read-back: file is under the category
const fileListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"list",
"--category-id",
categoryId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(fileListRun.exitCode, fileListRun.stderr).toBe(0);
expect(fileListRun.stdout.trim().split("\n")).toContain(fileId);
// file get read-back: tags contain "e2e-oss-tag"
const fileGetRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"get",
"--file-id",
fileId,
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(fileGetRun.exitCode, fileGetRun.stderr).toBe(0);
const fileDetail = parseStdoutJson<{
data: { category?: string; tags?: unknown };
}>(fileGetRun.stdout);
expect(fileDetail.data.category).toBe(categoryId);
expect(JSON.stringify(fileDetail.data.tags ?? "")).toContain("e2e-oss-tag");
} finally {
await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"file",
"delete",
"--file-id",
fileId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
} finally {
await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
"knowledge",
"category",
"delete",
"--category-id",
categoryId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
}, 120_000);
});
@@ -93,5 +93,25 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge doc list (live)", () => {
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ code: string }>(stdout);
expect(data.code).toBe("Success");
// P6: --page-number + --page-size
const pagedRun = await runCommandE2e(KNOWLEDGE_DOC_LIST_ROUTES, [
"knowledge",
"doc",
"list",
"--index-id",
firstIndexId,
"--page-number",
"1",
"--page-size",
"10",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(pagedRun.exitCode, pagedRun.stderr).toBe(0);
const pagedData = parseStdoutJson<{ code: string }>(pagedRun.stdout);
expect(pagedData.code).toBe("Success");
});
});
@@ -1,5 +1,8 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
// Live coverage depends on a real job_id produced by doc upload; it is exercised
@@ -93,3 +96,139 @@ describe("e2e: knowledge doc status", () => {
expect(data.endpoint).toMatch(/page_size=50/);
});
});
// Live: --wait + --poll-interval on a real job_id produced by upload --index-id --wait
describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge doc status (live, 自清理)", () => {
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
test("status --wait --poll-interval → 返回 COMPLETED", async () => {
const fixtureDir = mkdtempSync(join(tmpdir(), "doc-status-e2e-"));
const filePathA = join(fixtureDir, `status-a-${Date.now()}.md`);
writeFileSync(filePathA, "# doc status e2e fixture A\n");
// 1) Upload file A to data center
const uploadRunA = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"doc",
"upload",
"--file",
filePathA,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(uploadRunA.exitCode, uploadRunA.stderr).toBe(0);
const fileIdA = uploadRunA.stdout.trim();
expect(fileIdA).toMatch(/^file_/);
let indexId = "";
let fileIdB = "";
try {
// 2) Create a throwaway base (gives us an index_id)
const createRun = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"create",
"--name",
`e2e-st-${Date.now() % 100000000}`,
"--doc-id",
fileIdA,
"--workspace-id",
workspaceId,
"--wait",
"--quiet",
]);
expect(createRun.exitCode, createRun.stderr).toBe(0);
indexId = createRun.stdout.trim().split("\n")[0]!;
expect(indexId).toBeTruthy();
// 3) Upload file B to the base (--index-id --wait --output json) → ingestion_id
const filePathB = join(fixtureDir, `status-b-${Date.now()}.md`);
writeFileSync(filePathB, "# doc status e2e fixture B\n");
const uploadRunB = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"doc",
"upload",
"--file",
filePathB,
"--index-id",
indexId,
"--wait",
"--poll-interval",
"3",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(uploadRunB.exitCode, uploadRunB.stderr).toBe(0);
const importData = parseStdoutJson<{
files: Array<{ fileId: string }>;
ingestion_id?: string;
final_status?: string;
}>(uploadRunB.stdout);
const jobId = importData.ingestion_id;
expect(jobId).toBeTruthy();
fileIdB = importData.files?.[0]?.fileId ?? "";
// 4) status --wait --poll-interval: verify COMPLETED
// (upload --wait already ensured completion, so this should return immediately)
const statusRun = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"doc",
"status",
"--index-id",
indexId,
"--job-id",
jobId!,
"--wait",
"--poll-interval",
"3",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(statusRun.exitCode, statusRun.stderr).toBe(0);
const statusData = parseStdoutJson<{
data?: { final_status?: string };
}>(statusRun.stdout);
expect(statusData.data?.final_status).toBe("COMPLETED");
} finally {
// Cleanup base + data-center files
if (fileIdB) {
await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"file",
"delete",
"--file-id",
fileIdB,
"--yes",
"--workspace-id",
workspaceId,
]);
}
if (indexId) {
await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"delete",
"--index-id",
indexId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
"knowledge",
"file",
"delete",
"--file-id",
fileIdA,
"--yes",
"--workspace-id",
workspaceId,
]);
}
// kb create --wait adds a full import phase — generous timeout
}, 600_000);
});
@@ -240,4 +240,57 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge doc upload (live)", () =>
expect(fileDeleteRun.exitCode, fileDeleteRun.stderr).toBe(0);
}
}, 120_000);
test("upload --index-id --wait --poll-interval 3 (不报错验证)", async () => {
// P6: --poll-interval — local behavior param, verify no error
// Grab a real index id from the workspace
const listRun = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
"knowledge",
"list",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listRun.exitCode, listRun.stderr).toBe(0);
const indexId = listRun.stdout.trim().split("\n")[0];
if (!indexId) return;
const pollFilePath = join(fixtureDir, `poll-${Date.now()}.md`);
writeFileSync(pollFilePath, "# e2e poll interval fixture\n");
const uploadRun = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
"knowledge",
"doc",
"upload",
"--file",
pollFilePath,
"--index-id",
indexId,
"--wait",
"--poll-interval",
"3",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(uploadRun.exitCode, uploadRun.stderr).toBe(0);
// Cleanup the uploaded file
const importData = parseStdoutJson<{
files: Array<{ fileId: string }>;
}>(uploadRun.stdout);
const pollFileId = importData.files?.[0]?.fileId;
if (pollFileId) {
await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
"knowledge",
"file",
"delete",
"--file-id",
pollFileId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
}, 120_000);
});
@@ -106,6 +106,8 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清
"--workspace-id",
workspaceId,
"--wait",
"--poll-interval",
"3",
"--quiet",
]);
expect(createRun.exitCode, createRun.stderr).toBe(0);
@@ -124,4 +124,51 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb list (live)", () => {
const lines = quietRun.stdout.trim().split("\n").filter(Boolean);
expect(lines.length).toBe(rows.length);
});
test("--name 过滤 + --page-number/--page-size 分页", async () => {
// P6: --name
const jsonRun = await runCommandE2e(KNOWLEDGE_KB_LIST_ROUTES, [
"knowledge",
"list",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(jsonRun.exitCode, jsonRun.stderr).toBe(0);
const allRows = parseStdoutJson<KbListResponse>(jsonRun.stdout).data.rows;
if (allRows.length === 0) return;
const firstName = allRows[0]!.name;
const nameRun = await runCommandE2e(KNOWLEDGE_KB_LIST_ROUTES, [
"knowledge",
"list",
"--name",
firstName,
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(nameRun.exitCode, nameRun.stderr).toBe(0);
const nameData = parseStdoutJson<KbListResponse>(nameRun.stdout);
expect(nameData.data.rows.some((row) => row.name === firstName)).toBe(true);
// P6: --page-number + --page-size
const pagedRun = await runCommandE2e(KNOWLEDGE_KB_LIST_ROUTES, [
"knowledge",
"list",
"--page-number",
"1",
"--page-size",
"1",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(pagedRun.exitCode, pagedRun.stderr).toBe(0);
const pagedData = parseStdoutJson<KbListResponse>(pagedRun.stdout);
expect(pagedData.data.rows.length).toBeLessThanOrEqual(1);
});
});
@@ -668,3 +668,356 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge service 生命周期 (live
}
}, 300_000);
});
// Live parameter coverage: every write parameter gets a read-back assertion
// via `service get --output json`. Read-back fields are confirmed present in
// the get response by the existing lifecycle test and printDetail().
describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge service 参数全覆盖 (live, 自清理)", () => {
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
test("create --description → update(name/model/policy×2/version-desc) → list(status×3/scene/name/index-id) → deploy → published update --version-desc → delete", async () => {
const serviceName = `e2e-svc-param-${Date.now() % 100000000}`;
let agentId: string | undefined;
try {
// ── P6: service-create --description ──
const createRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"create",
"--name",
serviceName,
"--scene",
"chat",
"--description",
"e2e param coverage",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(createRun.exitCode, createRun.stderr).toBe(0);
agentId = createRun.stdout.trim().split("\n").pop()!;
expect(agentId).toMatch(/^aid-/);
// get 读回验证 --description (agent_desc)
const createGetRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"get",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(createGetRun.exitCode, createGetRun.stderr).toBe(0);
const createGetData = parseStdoutJson<{
data?: { agent_desc?: string };
}>(createGetRun.stdout);
expect(createGetData.data?.agent_desc).toBe("e2e param coverage");
// ── P1: list --status draft 验证新建 agent 可见 ──
const listDraftRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
"--scene",
"chat",
"--status",
"draft",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listDraftRun.exitCode, listDraftRun.stderr).toBe(0);
expect(listDraftRun.stdout.trim().split("\n")).toContain(agentId);
// ── P1: list --scene search (live 合法值) ──
const listSearchSceneRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
"--scene",
"search",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listSearchSceneRun.exitCode, listSearchSceneRun.stderr).toBe(0);
// ── P1: list --name 精确过滤 ──
const listNameRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
"--scene",
"chat",
"--name",
serviceName,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listNameRun.exitCode, listNameRun.stderr).toBe(0);
expect(listNameRun.stdout.trim().split("\n")).toContain(agentId);
// ── P1: list --index-id (不报错验证; 无 kb fixture) ──
const listIndexIdRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
"--scene",
"search",
"--index-id",
"idx_nonexistent_e2e",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listIndexIdRun.exitCode, listIndexIdRun.stderr).toBe(0);
// ── P0 组1: update --name --version-desc --model → get 读回 3 个 scalar ──
const newName = `${serviceName}-renamed`;
const updateScalarRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"update",
"--agent-id",
agentId,
"--name",
newName,
"--version-desc",
"beta-v1",
"--model",
"qwen-plus",
"--workspace-id",
workspaceId,
]);
expect(updateScalarRun.exitCode, updateScalarRun.stderr).toBe(0);
const scalarGetRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"get",
"--agent-id",
agentId,
"--agent-version",
"beta",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(scalarGetRun.exitCode, scalarGetRun.stderr).toBe(0);
const scalarGetData = parseStdoutJson<{
data?: {
agent_name?: string;
agent_details?: Array<{
agent_version_desc?: string;
agent_config?: { agent_model?: string };
}>;
};
}>(scalarGetRun.stdout);
expect(scalarGetData.data?.agent_name).toBe(newName);
expect(scalarGetData.data?.agent_details?.[0]?.agent_config?.agent_model).toBe("qwen-plus");
expect(scalarGetData.data?.agent_details?.[0]?.agent_version_desc).toBe("beta-v1");
// ── P0 组2: update --policy turbo → get 读回 ──
const updateTurboRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"update",
"--agent-id",
agentId,
"--policy",
"turbo",
"--workspace-id",
workspaceId,
]);
expect(updateTurboRun.exitCode, updateTurboRun.stderr).toBe(0);
const turboGetRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"get",
"--agent-id",
agentId,
"--agent-version",
"beta",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(turboGetRun.exitCode, turboGetRun.stderr).toBe(0);
const turboData = parseStdoutJson<{
data?: {
agent_details?: Array<{
agent_config?: { agent_policy?: string };
}>;
};
}>(turboGetRun.stdout);
expect(turboData.data?.agent_details?.[0]?.agent_config?.agent_policy).toBe("turbo");
// ── P0 组3: update --policy agentic → get 读回 ──
// 注: --max-llm-calls 10 仅作为 agentic 切换的前置条件,不做读回验证
const updateAgenticRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"update",
"--agent-id",
agentId,
"--policy",
"agentic",
"--max-llm-calls",
"10",
"--workspace-id",
workspaceId,
]);
expect(updateAgenticRun.exitCode, updateAgenticRun.stderr).toBe(0);
const agenticGetRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"get",
"--agent-id",
agentId,
"--agent-version",
"beta",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(agenticGetRun.exitCode, agenticGetRun.stderr).toBe(0);
const agenticData = parseStdoutJson<{
data?: {
agent_details?: Array<{
agent_config?: { agent_policy?: string };
}>;
};
}>(agenticGetRun.stdout);
expect(agenticData.data?.agent_details?.[0]?.agent_config?.agent_policy).toBe("agentic");
// ── deploy → P1: list --status deployed ──
const deployRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"deploy",
"--agent-id",
agentId,
"--yes",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(deployRun.exitCode, deployRun.stderr).toBe(0);
const listDeployedRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
"--scene",
"chat",
"--status",
"deployed",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listDeployedRun.exitCode, listDeployedRun.stderr).toBe(0);
expect(listDeployedRun.stdout.trim().split("\n")).toContain(agentId);
// ── P0 组4: update --agent-version 1 --version-desc (published version) ──
const updatePublishedRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"update",
"--agent-id",
agentId,
"--agent-version",
"1",
"--version-desc",
"published-v1-desc",
"--workspace-id",
workspaceId,
]);
expect(updatePublishedRun.exitCode, updatePublishedRun.stderr).toBe(0);
// get --agent-version 1 读回验证 --version-desc
const publishedGetRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"get",
"--agent-id",
agentId,
"--agent-version",
"1",
"--workspace-id",
workspaceId,
"--output",
"json",
]);
expect(publishedGetRun.exitCode, publishedGetRun.stderr).toBe(0);
const publishedData = parseStdoutJson<{
data?: {
agent_details?: Array<{ agent_version_desc?: string }>;
};
}>(publishedGetRun.stdout);
expect(publishedData.data?.agent_details?.[0]?.agent_version_desc).toBe("published-v1-desc");
// ── delete → P1: list --status deleted ──
const deleteRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"delete",
"--agent-id",
agentId,
"--yes",
"--workspace-id",
workspaceId,
]);
expect(deleteRun.exitCode, deleteRun.stderr).toBe(0);
const listDeletedRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"list",
"--scene",
"chat",
"--status",
"deleted",
"--agent-id",
agentId,
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listDeletedRun.exitCode, listDeletedRun.stderr).toBe(0);
// 软验证: deleted 状态下 agent 应出现在列表中;
// 若服务端不返回 deleted 列表则仅验证不报错
if (listDeletedRun.stdout.trim()) {
expect(listDeletedRun.stdout.trim().split("\n")).toContain(agentId);
}
} finally {
// 兜底清理
if (agentId) {
await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
"knowledge",
"service",
"delete",
"--agent-id",
agentId,
"--yes",
"--workspace-id",
workspaceId,
]);
}
}
}, 300_000);
});
@@ -1,5 +1,10 @@
import { describe, expect, test } from "vite-plus/test";
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
import {
isDashScopeE2EReady,
isKbAdminE2EReady,
parseStdoutJson,
runCommandE2e,
} from "../helpers.ts";
import { KNOWLEDGE_ROUTES } from "../topic-routes.ts";
// ---- Types ----
@@ -187,3 +192,57 @@ describe("e2e: knowledge retrieve dry-run", () => {
expect(data.request?.rerank?.[0]?.rerank_instruct).toBe("按相关性排序");
});
});
// Live: --rerank-instruct and --top-k (deprecated) on a real index
describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge retrieve 参数补全 (live)", () => {
const workspaceId = process.env.BAILIAN_WORKSPACE_ID!;
test("retrieve --rerank-instruct + --top-k (不报错验证)", async () => {
// Grab a real index id from the workspace
const listRun = await runCommandE2e(KNOWLEDGE_ROUTES, [
"knowledge",
"list",
"--workspace-id",
workspaceId,
"--quiet",
]);
expect(listRun.exitCode, listRun.stderr).toBe(0);
const indexId = listRun.stdout.trim().split("\n")[0];
if (!indexId) return;
// P6: --rerank-instruct (live)
const rerankInstructRun = await runCommandE2e(KNOWLEDGE_ROUTES, [
"knowledge",
"retrieve",
"--index-id",
indexId,
"--query",
"e2e test",
"--rerank",
"--rerank-model",
"qwen3-rerank-hybrid",
"--rerank-mode",
"custom",
"--rerank-instruct",
"按相关性排序",
"--output",
"json",
]);
expect(rerankInstructRun.exitCode, rerankInstructRun.stderr).toBe(0);
// P6: --top-k (deprecated, verify still accepted)
const topKRun = await runCommandE2e(KNOWLEDGE_ROUTES, [
"knowledge",
"retrieve",
"--index-id",
indexId,
"--query",
"e2e test",
"--top-k",
"10",
"--output",
"json",
]);
expect(topKRun.exitCode, topKRun.stderr).toBe(0);
}, 60_000);
});
@@ -39,6 +39,7 @@ export const KNOWLEDGE_ROUTES: E2eRouteExports = {
"knowledge retrieve": "knowledgeRetrieve",
"knowledge search": "knowledgeSearch",
"knowledge chat": "knowledgeChat",
"knowledge list": "knowledgeKbList", // live: grab a real index id for retrieve
};
export const KNOWLEDGE_SEARCH_ROUTES: E2eRouteExports = {
@@ -201,11 +202,16 @@ export const KNOWLEDGE_DOC_LIST_ROUTES: E2eRouteExports = {
export const KNOWLEDGE_DOC_STATUS_ROUTES: E2eRouteExports = {
"knowledge doc status": "knowledgeDocStatus",
"knowledge doc upload": "knowledgeDocUpload", // live: produce a real job_id
"knowledge create": "knowledgeKbCreate", // live: create a throwaway base
"knowledge delete": "knowledgeKbDelete", // live: cleanup the throwaway base
"knowledge file delete": "knowledgeFileDelete", // live: cleanup data-center files
};
export const KNOWLEDGE_DOC_UPLOAD_ROUTES: E2eRouteExports = {
"knowledge doc upload": "knowledgeDocUpload",
"knowledge file delete": "knowledgeFileDelete", // live cleanup of data-center files
"knowledge list": "knowledgeKbList", // live: grab a real index id for --index-id --wait
};
export const KNOWLEDGE_KB_CREATE_ROUTES: E2eRouteExports = {