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 d7fb529..bf42afc 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 @@ -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 }> }; } @@ -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); }); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-doc-list.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-doc-list.e2e.test.ts index 13eb100..3fb530d 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-doc-list.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-doc-list.e2e.test.ts @@ -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"); }); }); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts index 41b5269..be76781 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-doc-status.e2e.test.ts @@ -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); +}); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-doc-upload.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-doc-upload.e2e.test.ts index b3fb7d5..51270a0 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-doc-upload.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-doc-upload.e2e.test.ts @@ -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); }); 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 935b889..d19bce9 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 @@ -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); diff --git a/packages/commands/tests/e2e/knowledge/knowledge-kb-list.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge-kb-list.e2e.test.ts index 855358c..af1063b 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-kb-list.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-kb-list.e2e.test.ts @@ -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(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(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(pagedRun.stdout); + expect(pagedData.data.rows.length).toBeLessThanOrEqual(1); + }); }); 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 a371ad2..1d35d57 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge-service.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge-service.e2e.test.ts @@ -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); +}); diff --git a/packages/commands/tests/e2e/knowledge/knowledge.e2e.test.ts b/packages/commands/tests/e2e/knowledge/knowledge.e2e.test.ts index 33e59f1..160a4ed 100644 --- a/packages/commands/tests/e2e/knowledge/knowledge.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge/knowledge.e2e.test.ts @@ -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); +}); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 6bea875..84f81b4 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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 = {