@@ -17,7 +17,7 @@ import {
1717 KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES ,
1818 KNOWLEDGE_KB_DELETE_ROUTES ,
1919} from "../topic-routes.ts" ;
20- import { deleteKbWithRetry } from "./journeys/journey-helpers.ts" ;
20+ import { deleteKbWithRetry , pollUntil } from "./journeys/journey-helpers.ts" ;
2121
2222interface DryRunBody {
2323 endpoint ?: string ;
@@ -854,6 +854,21 @@ describe.skipIf(!isKbAdminE2EReady())(
854854 ( ) => {
855855 const workspaceId = process . env . BAILIAN_WORKSPACE_ID ! ;
856856
857+ test ( "file get 不存在的 file-id: 服务端错误原样透传 (非零退出)" , async ( ) => {
858+ const { stderr, exitCode } = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
859+ "knowledge" ,
860+ "file" ,
861+ "get" ,
862+ "--file-id" ,
863+ "file_nonexistent_e2e_0000" ,
864+ "--workspace-id" ,
865+ workspaceId ,
866+ ] ) ;
867+ expect ( exitCode ) . not . toBe ( 0 ) ;
868+ // Verified live: HTTP 200 + InvalidParameter with a "cant find" message
869+ expect ( stderr ) . toMatch ( / c a n t f i n d | I n v a l i d P a r a m e t e r / i) ;
870+ } , 60_000 ) ;
871+
857872 test ( "collection get/create live: 幂等复用固定名测试集合" , async ( ) => {
858873 // collection has no delete API — reuse a fixed name idempotently instead of creating
859874 // new ones (the legacy name e2e-test-connector is kept: that idempotent resource
@@ -1521,8 +1536,25 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
15211536 const categoryId = categoryAddRun . stdout . trim ( ) ;
15221537 expect ( categoryId ) . toMatch ( / ^ c a t e _ / ) ;
15231538
1539+ // ── empty-result contract: a freshly created category has no files —
1540+ // text mode prints the friendly empty line and exits 0 (empty ≠ error) ──
1541+ const emptyCategoryListRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
1542+ "knowledge" ,
1543+ "file" ,
1544+ "list" ,
1545+ "--category-id" ,
1546+ categoryId ,
1547+ "--workspace-id" ,
1548+ workspaceId ,
1549+ ] ) ;
1550+ expect ( emptyCategoryListRun . exitCode , emptyCategoryListRun . stderr ) . toBe ( 0 ) ;
1551+ expect ( emptyCategoryListRun . stdout ) . toMatch ( / N o f i l e s f o u n d \. / ) ;
1552+
15241553 const fixtureDir = mkdtempSync ( join ( tmpdir ( ) , "param-e2e-" ) ) ;
1525- const filePath = join ( fixtureDir , `param-${ Date . now ( ) } .md` ) ;
1554+ // Server contract (verified live): listFile fileName filters by the exact
1555+ // file name WITHOUT extension — keep the stem for the --name assertion below
1556+ const fileStem = `param-${ Date . now ( ) } ` ;
1557+ const filePath = join ( fixtureDir , `${ fileStem } .md` ) ;
15261558 writeFileSync ( filePath , "# e2e param file\n" ) ;
15271559 const uploadRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
15281560 "knowledge" ,
@@ -1541,21 +1573,30 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
15411573 let indexId : string | undefined ;
15421574
15431575 try {
1544- // ── P2: file list --name (模糊匹配) ──
1545- const fileListByNameRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
1546- "knowledge" ,
1547- "file" ,
1548- "list" ,
1549- "--category-id" ,
1550- categoryId ,
1551- "--name" ,
1552- "param-" ,
1553- "--workspace-id" ,
1554- workspaceId ,
1555- "--quiet" ,
1556- ] ) ;
1557- expect ( fileListByNameRun . exitCode , fileListByNameRun . stderr ) . toBe ( 0 ) ;
1558- expect ( fileListByNameRun . stdout . trim ( ) . split ( "\n" ) ) . toContain ( fileId ) ;
1576+ // ── P2: file list --name —— exact match on the extension-less file name
1577+ // (e.g. a.md → pass a); fuzzy/partial keywords return an empty set.
1578+ // Poll to absorb list-visibility lag right after upload ──
1579+ const fileListByNamePoll = await pollUntil (
1580+ ( ) =>
1581+ runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
1582+ "knowledge" ,
1583+ "file" ,
1584+ "list" ,
1585+ "--category-id" ,
1586+ categoryId ,
1587+ "--name" ,
1588+ fileStem ,
1589+ "--workspace-id" ,
1590+ workspaceId ,
1591+ "--quiet" ,
1592+ ] ) ,
1593+ ( run ) => run . exitCode === 0 && run . stdout . trim ( ) . split ( "\n" ) . includes ( fileId ) ,
1594+ { timeoutMs : 60_000 , intervalMs : 5_000 } ,
1595+ ) ;
1596+ expect (
1597+ fileListByNamePoll . satisfied ,
1598+ `file list --name 未包含 ${ fileId } (attempts=${ fileListByNamePoll . attempts } )` ,
1599+ ) . toBe ( true ) ;
15591600
15601601 // ── P2: file list --file-id ──
15611602 const fileListByFileIdRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
@@ -1638,7 +1679,9 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
16381679 ] ) ;
16391680 expect ( listByParentRun . exitCode , listByParentRun . stderr ) . toBe ( 0 ) ;
16401681
1641- // ── P3: category list --collection-id (no-error verification) ──
1682+ // ── P3: category list --collection-id — an unknown collection id is rejected
1683+ // by the server (Invalid connectorId, verified live) and the error passes
1684+ // through verbatim with a non-zero exit ──
16421685 const listByCollectionRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
16431686 "knowledge" ,
16441687 "category" ,
@@ -1649,7 +1692,8 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
16491692 workspaceId ,
16501693 "--quiet" ,
16511694 ] ) ;
1652- expect ( listByCollectionRun . exitCode , listByCollectionRun . stderr ) . toBe ( 0 ) ;
1695+ expect ( listByCollectionRun . exitCode , "不存在的 collection id 应被服务端拒绝" ) . not . toBe ( 0 ) ;
1696+ expect ( listByCollectionRun . stderr ) . toMatch ( / I n v a l i d c o n n e c t o r I d | I n v a l i d P a r a m e t e r / i) ;
16531697
16541698 // ── create kb for chunk tests ──
16551699 const kbCreateRun = await runCommandE2e ( KNOWLEDGE_KB_DELETE_ROUTES , [
@@ -1744,6 +1788,45 @@ describe.skipIf(!isKbAdminE2EReady())("e2e: chunk/category/file 参数补全 (li
17441788 expect ( node . metadata ?. doc_id ) . toContain ( fileId ) ;
17451789 }
17461790
1791+ // ── empty-result contract: a page far past the end returns no chunks —
1792+ // text mode prints the friendly empty line and exits 0 (empty ≠ error) ──
1793+ const chunkListEmptyPageRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
1794+ "knowledge" ,
1795+ "chunk" ,
1796+ "list" ,
1797+ "--index-id" ,
1798+ indexId ,
1799+ "--page-number" ,
1800+ "99" ,
1801+ "--workspace-id" ,
1802+ workspaceId ,
1803+ ] ) ;
1804+ expect ( chunkListEmptyPageRun . exitCode , chunkListEmptyPageRun . stderr ) . toBe ( 0 ) ;
1805+ expect ( chunkListEmptyPageRun . stdout ) . toMatch ( / N o c h u n k s f o u n d \. / ) ;
1806+
1807+ // ── contract: updating a nonexistent chunk id — the server currently
1808+ // reports success (silent upsert-like semantics, verified live). The CLI
1809+ // passes the server result through without an existence pre-check; this
1810+ // assertion pins the known server behavior so a future server-side change
1811+ // (rejecting unknown ids) surfaces here immediately ──
1812+ const chunkUpdateMissingRun = await runCommandE2e ( KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES , [
1813+ "knowledge" ,
1814+ "chunk" ,
1815+ "update" ,
1816+ "--index-id" ,
1817+ indexId ,
1818+ "--chunk-id" ,
1819+ "chunk_nonexistent_e2e_0000" ,
1820+ "--doc-id" ,
1821+ fileId ,
1822+ "--content" ,
1823+ "content for a chunk id that should not exist" ,
1824+ "--workspace-id" ,
1825+ workspaceId ,
1826+ ] ) ;
1827+ expect ( chunkUpdateMissingRun . exitCode , chunkUpdateMissingRun . stderr ) . toBe ( 0 ) ;
1828+ expect ( chunkUpdateMissingRun . stdout ) . toMatch ( / u p d a t e d : c h u n k _ n o n e x i s t e n t _ e 2 e _ 0 0 0 0 / ) ;
1829+
17471830 // ── P4: chunk update --content-file ──
17481831 if ( addedChunkId ) {
17491832 const updateContentFile = join ( fixtureDir , `chunk-update-${ Date . now ( ) } .txt` ) ;
0 commit comments