From 12e7a22195734f86a571b902f5396b08e656e6cb Mon Sep 17 00:00:00 2001 From: "zeyu.fz" Date: Mon, 10 Aug 2026 17:37:21 +0800 Subject: [PATCH] =?UTF-8?q?test(knowledge):=20=E5=A2=9E=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E7=9B=B8=E5=85=B3=E5=91=BD=E4=BB=A4=E7=9A=84=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E8=AF=BB=E5=9B=9E=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 knowledge doc delete 命令中添加异步删除的轮询验证,确保文档从服务器彻底移除 - 为 knowledge doc tag 添加标签设置后,独立调用 file get 验证标签正确应用 - 在知识库更新操作后,通过 info 命令独立验证更新是否成功保存 - 在文件删除和类别删除后,通过独立列表命令验证资源确实被清除 - 对知识块更新及排除标记修改,添加通过列表接口的内容验证步骤 - 对知识服务代理删除操作后,增加独立查询接口确保代理已彻底删除 - 补充 doc delete 备注,明确 doc_id 与 fileId 的区别及异步删除机制说明 - 增加 e2e 路由映射中缺失的 knowledge info 和 knowledge file get 命令支持 --- .../src/commands/knowledge/doc-delete.ts | 2 + .../knowledge-chunk-category-file.e2e.test.ts | 149 +++++++++++++++++- .../knowledge/knowledge-doc-tag.e2e.test.ts | 36 +++++ .../knowledge/knowledge-kb-delete.e2e.test.ts | 25 ++- .../knowledge/knowledge-service.e2e.test.ts | 19 +++ packages/commands/tests/e2e/topic-routes.ts | 2 + skills/bailian-cli/reference/knowledge.md | 2 + 7 files changed, 231 insertions(+), 4 deletions(-) diff --git a/packages/commands/src/commands/knowledge/doc-delete.ts b/packages/commands/src/commands/knowledge/doc-delete.ts index 86bcd3a..1b8681a 100644 --- a/packages/commands/src/commands/knowledge/doc-delete.ts +++ b/packages/commands/src/commands/knowledge/doc-delete.ts @@ -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: [ diff --git a/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts index 2ae7914..d7fb529 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-chunk-category-file.e2e.test.ts @@ -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_"), 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, [ diff --git a/packages/commands/tests/e2e/knowledge/knowledge-doc-tag.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-doc-tag.e2e.test.ts index 0ce4a8c..64b77e7 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-doc-tag.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-doc-tag.e2e.test.ts @@ -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", diff --git a/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts index 9a4423f..935b889 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-kb-delete.e2e.test.ts @@ -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) diff --git a/packages/commands/tests/e2e/knowledge/knowledge-service.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-service.e2e.test.ts index f2967d4..a371ad2 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-service.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-service.e2e.test.ts @@ -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); }); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 7994e5e..6bea875 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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 }; diff --git a/skills/bailian-cli/reference/knowledge.md b/skills/bailian-cli/reference/knowledge.md index 044b684..05b54c7 100644 --- a/skills/bailian-cli/reference/knowledge.md +++ b/skills/bailian-cli/reference/knowledge.md @@ -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