diff --git a/packages/commands/src/commands/knowledge/doc-upload.ts b/packages/commands/src/commands/knowledge/doc-upload.ts index 47a55f8..b81e1ca 100644 --- a/packages/commands/src/commands/knowledge/doc-upload.ts +++ b/packages/commands/src/commands/knowledge/doc-upload.ts @@ -149,10 +149,10 @@ export default defineCommand({ endpoint: ragEndpoint(workspaceId, RAG_PATHS.indexJobCreate), request: { indexId: flags.indexId, - // Gotcha (live-verified): job/create requires the nested dataSource shape; - // the public docs' flat documentIds body returns Index.InvalidParameter. - // Omitting sourceType would import the entire data center. - dataSource: { sourceType: "DATA_CENTER_FILE", fileIds: [""] }, + // Live-verified: the field name is docIds (not documentIds as in the + // public docs); omitting sourceType would import the entire data center. + sourceType: "DATA_CENTER_FILE", + docIds: [""], } as unknown, }); } @@ -259,11 +259,10 @@ export default defineCommand({ method: "POST", body: { indexId: flags.indexId, - // Live-verified shape: nested dataSource (the docs' flat documentIds is rejected) - dataSource: { - sourceType: "DATA_CENTER_FILE", - fileIds: uploaded.map((item) => item.fileId), - }, + // Live-verified: the field name is docIds (not documentIds as in the + // public docs); omitting sourceType would import the entire data center. + sourceType: "DATA_CENTER_FILE", + docIds: uploaded.map((item) => item.fileId), }, }); ingestionId = job.data?.ingestionId; 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 795b9b7..b3fb7d5 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 @@ -113,7 +113,7 @@ describe("e2e: knowledge doc upload", () => { expect(data.skipped).toEqual([]); }); - test("--dry-run 带 --index-id 输出 4 步且 job 请求含显式 sourceType", async () => { + test("--dry-run 带 --index-id 输出 4 步且 job 请求含扁平 docIds", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [ "knowledge", "doc", @@ -135,13 +135,15 @@ describe("e2e: knowledge doc upload", () => { expect(data.steps).toHaveLength(4); const jobRequest = data.steps[3]!.request as { indexId?: string; - dataSource?: { sourceType?: string; fileIds?: string[] }; + sourceType?: string; + docIds?: string[]; }; - // Live-verified gotcha: job/create requires the nested dataSource shape, and - // omitting sourceType would import the entire data center - expect(jobRequest.dataSource?.sourceType).toBe("DATA_CENTER_FILE"); + // Live-verified: the field name is docIds (not documentIds as in the + // public docs); omitting sourceType would import the entire data center. + expect(jobRequest.sourceType).toBe("DATA_CENTER_FILE"); + expect(jobRequest.docIds).toEqual([""]); expect(jobRequest.indexId).toBe("idx_test"); - expect(jobRequest).not.toHaveProperty("documentIds"); + expect(jobRequest).not.toHaveProperty("dataSource"); }); test("--file dry-run 展开目录且 skipped 包含不支持的文件", async () => { 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 4a7f2c8..6047464 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 @@ -2,7 +2,7 @@ // (import orchestration) → delete → list to verify removal. import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, join } from "node:path"; import { describe, expect, test } from "vite-plus/test"; import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts"; import { deleteKbWithRetry } from "./journeys/journey-helpers.ts"; @@ -132,7 +132,9 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清 // 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 + // only place the createImportJob step runs live. Using --output json (not --quiet) + // so we can assert final_status and ingestion_id — a regression in the job/create + // request body (e.g. wrong field name) is caught by the server returning HTTP 400. const importFilePath = join(fixtureDir, `chain-import-${Date.now()}.md`); writeFileSync(importFilePath, "# kb chain e2e import fixture\n"); const importUploadRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [ @@ -146,13 +148,47 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: knowledge kb 写链路 (live, 自清 "--wait", "--workspace-id", workspaceId, - "--quiet", + "--output", + "json", ]); expect(importUploadRun.exitCode, importUploadRun.stderr).toBe(0); - const importedFileId = importUploadRun.stdout.trim(); + const importData = parseStdoutJson<{ + files: Array<{ fileId: string }>; + ingestion_id?: string; + final_status?: string; + }>(importUploadRun.stdout); + // These assertions verify the full pipeline completed: ingestion_id proves the + // job was created, and final_status COMPLETED proves parsing finished successfully. + expect(importData.ingestion_id).toBeTruthy(); + expect(importData.final_status).toBe("COMPLETED"); + const importedFileId = importData.files?.[0]?.fileId; expect(importedFileId).toMatch(/^file_/); fixtureFileIds.push(importedFileId); + // 2.8) Verify the imported file is actually visible in the KB — final_status + // COMPLETED only proves the job finished; an independent doc list query + // confirms the file was registered as a document in the index + const importedFileName = basename(importFilePath); + const docListRun = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [ + "knowledge", + "doc", + "list", + "--index-id", + indexId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect(docListRun.exitCode, docListRun.stderr).toBe(0); + const docListData = parseStdoutJson<{ + data?: { rows?: Array<{ doc_name?: string; status?: string }> }; + }>(docListRun.stdout); + const importedDoc = docListData.data?.rows?.find((row) => + row.doc_name?.includes(importedFileName), + ); + expect(importedDoc, `expected doc list to contain file "${importedFileName}"`).toBeTruthy(); + // 3) Delete the base (--yes non-interactive; deleteKbWithRetry retries on // IndexStatusError: readiness can lag briefly even after the import completes) const deleteRun = await deleteKbWithRetry(