mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
test(knowledge): 增加文档相关命令的独立读回验证
- 在 knowledge doc delete 命令中添加异步删除的轮询验证,确保文档从服务器彻底移除 - 为 knowledge doc tag 添加标签设置后,独立调用 file get 验证标签正确应用 - 在知识库更新操作后,通过 info 命令独立验证更新是否成功保存 - 在文件删除和类别删除后,通过独立列表命令验证资源确实被清除 - 对知识块更新及排除标记修改,添加通过列表接口的内容验证步骤 - 对知识服务代理删除操作后,增加独立查询接口确保代理已彻底删除 - 补充 doc delete 备注,明确 doc_id 与 fileId 的区别及异步删除机制说明 - 增加 e2e 路由映射中缺失的 knowledge info 和 knowledge file get 命令支持
This commit is contained in:
@@ -42,6 +42,8 @@ export default defineCommand({
|
||||
flags: DOC_DELETE_FLAGS,
|
||||
notes: [
|
||||
"Removes documents from the knowledge base index only; the source files remain in the data center.",
|
||||
"Use the doc_id from `knowledge doc list --quiet`, not the fileId from `knowledge doc upload`. For documents created via `knowledge create --doc-id`, the doc_id equals the fileId; for documents imported via `knowledge doc upload --index-id`, the doc_id may include a workspace suffix.",
|
||||
"Deletion is asynchronous: the server returns Success immediately, but the document may still appear in `knowledge doc list` for up to ~30s until the change propagates.",
|
||||
"The output lists the ids actually deleted as reported by the server.",
|
||||
],
|
||||
exampleArgs: [
|
||||
|
||||
@@ -1072,6 +1072,20 @@ describe.skipIf(!isKbAdminE2EReady())(
|
||||
workspaceId,
|
||||
]);
|
||||
expect(deleteRun.exitCode, deleteRun.stderr).toBe(0);
|
||||
|
||||
// Verify the file is actually gone from the data center via read-back
|
||||
const postFileListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"file",
|
||||
"list",
|
||||
"--category-id",
|
||||
categoryId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--quiet",
|
||||
]);
|
||||
expect(postFileListRun.exitCode, postFileListRun.stderr).toBe(0);
|
||||
expect(postFileListRun.stdout.trim()).not.toContain(fileId);
|
||||
} finally {
|
||||
// Clean up the self-created category (doubles as live coverage of category delete)
|
||||
const categoryDeleteRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
@@ -1085,6 +1099,20 @@ describe.skipIf(!isKbAdminE2EReady())(
|
||||
workspaceId,
|
||||
]);
|
||||
expect(categoryDeleteRun.exitCode, categoryDeleteRun.stderr).toBe(0);
|
||||
|
||||
// Verify the category is actually gone from the server via read-back
|
||||
const postCategoryListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"category",
|
||||
"list",
|
||||
"--name",
|
||||
categoryName,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--quiet",
|
||||
]);
|
||||
expect(postCategoryListRun.exitCode, postCategoryListRun.stderr).toBe(0);
|
||||
expect(postCategoryListRun.stdout.trim()).not.toContain(categoryId);
|
||||
}
|
||||
// kb create --wait adds a full import phase — generous timeout
|
||||
}, 600_000);
|
||||
@@ -1176,6 +1204,31 @@ describe.skipIf(!isKbAdminE2EReady())(
|
||||
]);
|
||||
expect(updateRun.exitCode, updateRun.stderr).toBe(0);
|
||||
|
||||
// Verify the content update landed on the server via independent read-back
|
||||
const updateVerifyRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"chunk",
|
||||
"list",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(updateVerifyRun.exitCode, updateVerifyRun.stderr).toBe(0);
|
||||
const updateVerifyData = parseStdoutJson<{
|
||||
data?: {
|
||||
nodes?: Array<{ metadata?: { _id?: string; content?: string }; text?: string }>;
|
||||
};
|
||||
}>(updateVerifyRun.stdout);
|
||||
const updatedChunk = updateVerifyData.data?.nodes?.find(
|
||||
(node) => node.metadata?._id === targetChunkId,
|
||||
);
|
||||
expect(updatedChunk?.metadata?.content ?? updatedChunk?.text).toBe(
|
||||
"e2e updated chunk content",
|
||||
);
|
||||
|
||||
// update toggling --exclude only (no content — exercises the fetchChunkContent read-back path live)
|
||||
const excludeRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
@@ -1193,6 +1246,29 @@ describe.skipIf(!isKbAdminE2EReady())(
|
||||
]);
|
||||
expect(excludeRun.exitCode, excludeRun.stderr).toBe(0);
|
||||
|
||||
// Verify the exclude flag landed on the server via independent read-back
|
||||
const excludeVerifyRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"chunk",
|
||||
"list",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(excludeVerifyRun.exitCode, excludeVerifyRun.stderr).toBe(0);
|
||||
const excludeVerifyData = parseStdoutJson<{
|
||||
data?: {
|
||||
nodes?: Array<{ metadata?: { _id?: string; is_displayed_chunk_content?: boolean } }>;
|
||||
};
|
||||
}>(excludeVerifyRun.stdout);
|
||||
const excludedChunk = excludeVerifyData.data?.nodes?.find(
|
||||
(node) => node.metadata?._id === targetChunkId,
|
||||
);
|
||||
expect(excludedChunk?.metadata?.is_displayed_chunk_content).toBe(false);
|
||||
|
||||
// kb stats on this base doubles as live coverage of the monitor endpoint
|
||||
// (the only indices endpoint not touched by the chain)
|
||||
const statsRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
@@ -1298,8 +1374,47 @@ describe.skipIf(!isKbAdminE2EReady())(
|
||||
),
|
||||
);
|
||||
|
||||
// Verify the chunks are actually gone from the server via independent read-back
|
||||
const postDeleteListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"chunk",
|
||||
"list",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--quiet",
|
||||
]);
|
||||
expect(postDeleteListRun.exitCode, postDeleteListRun.stderr).toBe(0);
|
||||
expect(postDeleteListRun.stdout.trim()).toBe("");
|
||||
|
||||
// doc delete verifies document-level delete semantics (only unlinks from this
|
||||
// base, unlike file delete)
|
||||
// base, unlike file delete). The --doc-id must be the full doc_id from doc
|
||||
// list (e.g. "file_xxx_<workspaceId>"), NOT the bare fileId ("file_xxx") —
|
||||
// passing the bare fileId returns Success but does not actually remove the
|
||||
// document.
|
||||
const preDocListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"list",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(preDocListRun.exitCode, preDocListRun.stderr).toBe(0);
|
||||
const preDocListData = parseStdoutJson<{
|
||||
data?: { rows?: Array<{ doc_id?: string; doc_name?: string }> };
|
||||
}>(preDocListRun.stdout);
|
||||
const docRow = preDocListData.data?.rows?.find((row) => row.doc_id?.startsWith(fileId));
|
||||
expect(
|
||||
docRow,
|
||||
`expected doc list to contain a doc_id starting with "${fileId}"`,
|
||||
).toBeTruthy();
|
||||
const fullDocId = docRow!.doc_id!;
|
||||
|
||||
const docDeleteRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
@@ -1307,13 +1422,41 @@ describe.skipIf(!isKbAdminE2EReady())(
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--doc-id",
|
||||
fileId,
|
||||
fullDocId,
|
||||
"--yes",
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
]);
|
||||
expect(docDeleteRun.exitCode, docDeleteRun.stderr).toBe(0);
|
||||
expect(docDeleteRun.stdout).toMatch(new RegExp(fileId));
|
||||
expect(docDeleteRun.stdout).toMatch(new RegExp(fullDocId));
|
||||
|
||||
// Verify the document is actually gone from the server via independent read-back.
|
||||
// doc delete is asynchronous — the server returns Success immediately but the
|
||||
// document disappears from doc list after a propagation delay (same pattern as
|
||||
// IndexStatusError on kb delete). Poll until the doc_id is gone or timeout.
|
||||
let docGone = false;
|
||||
for (let retry = 0; retry < 6; retry++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
||||
const postDocListRun = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"list",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--quiet",
|
||||
]);
|
||||
expect(postDocListRun.exitCode, postDocListRun.stderr).toBe(0);
|
||||
if (!postDocListRun.stdout.trim().includes(fullDocId)) {
|
||||
docGone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(
|
||||
docGone,
|
||||
`doc delete returned Success but the document is still in doc list after 60s`,
|
||||
).toBe(true);
|
||||
} finally {
|
||||
// Clean up the throwaway base + data-center file (same IndexStatusError retry as the kb delete chain)
|
||||
await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
|
||||
@@ -218,6 +218,22 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge doc tag (live)", () => {
|
||||
const data = parseStdoutJson<{ code: string }>(tagRun.stdout);
|
||||
expect(data.code).toBe("Success");
|
||||
|
||||
// Verify the tag actually landed on the server via an independent read-back
|
||||
const tagGetRun = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
"knowledge",
|
||||
"file",
|
||||
"get",
|
||||
"--file-id",
|
||||
fileId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(tagGetRun.exitCode, tagGetRun.stderr).toBe(0);
|
||||
const tagGetDetail = parseStdoutJson<{ data: { tags?: string[] } }>(tagGetRun.stdout);
|
||||
expect(tagGetDetail.data?.tags ?? []).toContain("e2e-tag");
|
||||
|
||||
// overwrite mode replaces the tag set live (append above covered the default)
|
||||
const overwriteRun = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
"knowledge",
|
||||
@@ -238,6 +254,26 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge doc tag (live)", () => {
|
||||
const overwriteData = parseStdoutJson<{ code: string }>(overwriteRun.stdout);
|
||||
expect(overwriteData.code).toBe("Success");
|
||||
|
||||
// Verify overwrite replaced the tag set — old tag gone, new tag present
|
||||
const overwriteGetRun = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
"knowledge",
|
||||
"file",
|
||||
"get",
|
||||
"--file-id",
|
||||
fileId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(overwriteGetRun.exitCode, overwriteGetRun.stderr).toBe(0);
|
||||
const overwriteGetDetail = parseStdoutJson<{ data: { tags?: string[] } }>(
|
||||
overwriteGetRun.stdout,
|
||||
);
|
||||
const overwriteTags = overwriteGetDetail.data?.tags ?? [];
|
||||
expect(overwriteTags).toContain("e2e-tag-final");
|
||||
expect(overwriteTags).not.toContain("e2e-tag");
|
||||
|
||||
// Clean up the uploaded data-center file
|
||||
const fileDeleteRun = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
"knowledge",
|
||||
|
||||
@@ -113,13 +113,14 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清
|
||||
expect(indexId).toBeTruthy();
|
||||
|
||||
// 2.5) Live update coverage: name / description / rerank threshold in one call
|
||||
const updatedName = `e2e-upd-${Date.now() % 100000000}`;
|
||||
const updateRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"update",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--name",
|
||||
`e2e-upd-${Date.now() % 100000000}`,
|
||||
updatedName,
|
||||
"--description",
|
||||
"e2e chain updated description",
|
||||
"--rerank-min-score",
|
||||
@@ -130,6 +131,28 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清
|
||||
expect(updateRun.exitCode, updateRun.stderr).toBe(0);
|
||||
expect(updateRun.stdout).toMatch(/updated/);
|
||||
|
||||
// 2.6) Verify the update actually landed on the server — not just that the
|
||||
// command returned Success, but that a fresh read-back shows the new values
|
||||
const infoRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"info",
|
||||
"--index-id",
|
||||
indexId,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(infoRun.exitCode, infoRun.stderr).toBe(0);
|
||||
const infoData = parseStdoutJson<{
|
||||
name?: string;
|
||||
description?: string;
|
||||
rerankMinScore?: number;
|
||||
}>(infoRun.stdout);
|
||||
expect(infoData.name).toBe(updatedName);
|
||||
expect(infoData.description).toBe("e2e chain updated description");
|
||||
expect(infoData.rerankMinScore).toBe(0.3);
|
||||
|
||||
// 2.7) Live coverage of the upload → import orchestration (doc upload --index-id --wait):
|
||||
// the journeys always upload bare files and import via kb create, so this is the
|
||||
// only place the createImportJob step runs live. Using --output json (not --quiet)
|
||||
|
||||
@@ -647,5 +647,24 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge service 生命周期 (live
|
||||
]);
|
||||
expect(deleteRun.exitCode, deleteRun.stderr).toBe(0);
|
||||
}
|
||||
|
||||
// 6.5) Verify both agents are gone from the server — not just that delete
|
||||
// returned Success, but that a fresh list filtered by agent_id finds nothing
|
||||
for (const idToVerify of [copiedAgentId, agentId]) {
|
||||
const postDeleteListRun = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
"knowledge",
|
||||
"service",
|
||||
"list",
|
||||
"--scene",
|
||||
"chat",
|
||||
"--agent-id",
|
||||
idToVerify,
|
||||
"--workspace-id",
|
||||
workspaceId,
|
||||
"--quiet",
|
||||
]);
|
||||
expect(postDeleteListRun.exitCode, postDeleteListRun.stderr).toBe(0);
|
||||
expect(postDeleteListRun.stdout.trim()).toBe("");
|
||||
}
|
||||
}, 300_000);
|
||||
});
|
||||
|
||||
@@ -220,6 +220,7 @@ export const KNOWLEDGE_KB_DELETE_ROUTES: E2eRouteExports = {
|
||||
"knowledge delete": "knowledgeKbDelete",
|
||||
"knowledge create": "knowledgeKbCreate", // live self-cleaning chain
|
||||
"knowledge update": "knowledgeKbUpdate", // live update step in the chain
|
||||
"knowledge info": "knowledgeKbInfo", // live: verify update landed on the server
|
||||
"knowledge list": "knowledgeKbList",
|
||||
"knowledge doc upload": "knowledgeDocUpload",
|
||||
"knowledge doc list": "knowledgeDocList", // live: verify imported file is visible in the KB
|
||||
@@ -233,6 +234,7 @@ export const KNOWLEDGE_DOC_DELETE_ROUTES: E2eRouteExports = {
|
||||
export const KNOWLEDGE_DOC_TAG_ROUTES: E2eRouteExports = {
|
||||
"knowledge doc tag": "knowledgeDocTag",
|
||||
"knowledge doc upload": "knowledgeDocUpload", // live uploads first to grab a real fileId
|
||||
"knowledge file get": "knowledgeFileGet", // live: verify tags landed on the server
|
||||
"knowledge file delete": "knowledgeFileDelete", // live cleanup of data-center files
|
||||
};
|
||||
|
||||
|
||||
@@ -502,6 +502,8 @@ bl knowledge delete --index-id idx-xxx --yes
|
||||
#### Notes
|
||||
|
||||
- Removes documents from the knowledge base index only; the source files remain in the data center.
|
||||
- Use the doc_id from `knowledge doc list --quiet`, not the fileId from `knowledge doc upload`. For documents created via `knowledge create --doc-id`, the doc_id equals the fileId; for documents imported via `knowledge doc upload --index-id`, the doc_id may include a workspace suffix.
|
||||
- Deletion is asynchronous: the server returns Success immediately, but the document may still appear in `knowledge doc list` for up to ~30s until the change propagates.
|
||||
- The output lists the ids actually deleted as reported by the server.
|
||||
|
||||
#### Examples
|
||||
|
||||
Reference in New Issue
Block a user