@@ -850,6 +850,122 @@ describe('renderFileDescription — #14455: a tag WITH a payload is rewritten, n
850850 } ) ;
851851} ) ;
852852
853+ /**
854+ * #16962 — the caption's fence is a PRECONDITION, and the generator asserts it.
855+ *
856+ * `EXAMPLE_CAPTION` promotes `@example CAPTION` to a bold lead-in because the
857+ * contract says a fence follows. Nothing checked, and #15440 is what that cost:
858+ * two module headers captioned a listing, wrote its rows as bare prose, and the
859+ * rows reached two customer-facing reference pages as one run-on paragraph —
860+ * consecutive non-blank lines are one markdown paragraph, and the docs site
861+ * loads no `remark-breaks`.
862+ *
863+ * ⛔ The refusal is NOT a detector for "prose that is really a table". This
864+ * module's own header rejects that shape-sniffing and so does the card. The
865+ * question asked here is only the one the contract already states: is there a
866+ * block beneath the caption? An author who wants those words as prose writes
867+ * them without the tag.
868+ */
869+ describe ( 'renderFileDescription — #16962: a caption with no block beneath it is refused, not published' , ( ) => {
870+ const ctx = { fromCategory : 'api' , sourcePathToDocsRoute : ( ) => null , sectionLevel : PAGE_SECTION_LEVEL } ;
871+
872+ const moduleBlock = ( ...body : string [ ] ) : string =>
873+ [ '/**' , ...body . map ( l => ( l === '' ? ' *' : ` * ${ l } ` ) ) , ' */' , '' , "import { z } from 'zod';" , '' ] . join ( '\n' ) ;
874+
875+ it ( 'refuses the #15440 shape — a caption over rows written as bare prose' , ( ) => {
876+ // `api/automation-api` and `api/package-api`, reduced to the shape they
877+ // shipped. Before the assertion this rendered `**Endpoints**` followed by
878+ // one paragraph reading `GET /api/automation … POST /api/automation …`.
879+ expect ( ( ) =>
880+ renderFileDescription (
881+ moduleBlock (
882+ 'Automation API Protocol' ,
883+ '' ,
884+ '@example Endpoints' ,
885+ 'GET /api/automation - list' ,
886+ 'POST /api/automation - create' ,
887+ ) ,
888+ ctx ,
889+ ) ,
890+ ) . toThrow ( / ` @ e x a m p l e E n d p o i n t s ` w i t h n o c o d e b l o c k b e n e a t h i t / ) ;
891+ } ) ;
892+
893+ it ( 'names the source fix, because the source is where the fix goes' , ( ) => {
894+ // The renderer cannot repair this and must not try — the same reason the
895+ // heading-depth refusal points at the file header rather than clamping.
896+ expect ( ( ) => renderFileDescription ( moduleBlock ( '@example Endpoints' , 'GET /api/x' ) , ctx ) ) . toThrow (
897+ / F e n c e t h e b l o c k i n t h e s o u r c e ' s o w n f i l e h e a d e r / ,
898+ ) ;
899+ } ) ;
900+
901+ it ( 'refuses a caption that ends the block, with nothing at all beneath it' , ( ) => {
902+ // The other orphan shape, and the one a "next line is not a fence" test
903+ // written with an off-by-one would sail past.
904+ expect ( ( ) => renderFileDescription ( moduleBlock ( 'Automation API Protocol' , '' , '@example Endpoints' ) , ctx ) ) . toThrow (
905+ / n o c o d e b l o c k b e n e a t h i t / ,
906+ ) ;
907+ } ) ;
908+
909+ it ( 'refuses a caption whose next block is another tag rather than a fence' , ( ) => {
910+ // A run of tags is the arrangement `withTagBlocksSeparated` exists for, so
911+ // the caption is followed by a blank line here whatever the source wrote.
912+ // Skipping blanks must not be mistaken for finding a block.
913+ expect ( ( ) =>
914+ renderFileDescription (
915+ moduleBlock ( 'Automation API Protocol' , '' , '@example Endpoints' , '@see https://example.invalid/api' ) ,
916+ ctx ,
917+ ) ,
918+ ) . toThrow ( / n o c o d e b l o c k b e n e a t h i t / ) ;
919+ } ) ;
920+
921+ it ( 'accepts the fenced form — the twelve captions in the corpus keep rendering' , ( ) => {
922+ const out = renderFileDescription (
923+ moduleBlock ( 'Automation API Protocol' , '' , '@example Endpoints' , '```' , 'GET /api/automation' , '```' ) ,
924+ ctx ,
925+ ) ;
926+ expect ( out ) . toContain ( '**Endpoints**\n```' ) ;
927+ } ) ;
928+
929+ it ( 'accepts a blank line between the caption and its fence' , ( ) => {
930+ // Markdown puts the fence under the bold line either way, and the sources
931+ // write both spellings — refusing this one would reject correct pages.
932+ const out = renderFileDescription (
933+ moduleBlock ( '@example Endpoints' , '' , '```' , 'GET /api/automation' , '```' ) ,
934+ ctx ,
935+ ) ;
936+ expect ( out ) . toContain ( '**Endpoints**' ) ;
937+ expect ( out ) . toContain ( 'GET /api/automation' ) ;
938+ } ) ;
939+
940+ it ( 'accepts an INDENTED block, which reaches the page as a fence anyway' , ( ) => {
941+ // `data/date-macros` and `data/context-tokens` write examples this way and
942+ // the render loop re-emits them fenced. Judged by KIND, so a caption above
943+ // one captions a fence by the time a reader sees it.
944+ const out = renderFileDescription ( moduleBlock ( '@example Macros' , '' , ' value: 1' ) , ctx ) ;
945+ expect ( out ) . toContain ( '**Macros**' ) ;
946+ expect ( out ) . toContain ( '```\nvalue: 1\n```' ) ;
947+ } ) ;
948+
949+ it ( 'ignores an `@example CAPTION` shown INSIDE a fence — that is an author illustrating the tag' , ( ) => {
950+ // Judged on the same classification the rewrite is, so a header teaching the
951+ // convention is not refused for demonstrating the broken form. A refusal
952+ // written over raw text instead of over `kind` would reject this file's own
953+ // documentation.
954+ const out = renderFileDescription (
955+ moduleBlock ( 'How a module header captions an example:' , '' , '```md' , '@example Endpoints' , 'GET /api/x' , '```' ) ,
956+ ctx ,
957+ ) ;
958+ expect ( out ) . toContain ( '@example Endpoints' ) ;
959+ } ) ;
960+
961+ it ( 'ignores a mid-sentence mention, the same UNTRIMMED test the rewrite uses' , ( ) => {
962+ // `MODULE_MARKER`'s rule, and the reason the two can share one pattern: only
963+ // a line that OPENS with the tag is a tag.
964+ const out = renderFileDescription ( moduleBlock ( 'Write `@example Foo` above a fence to caption it.' ) , ctx ) ;
965+ expect ( out ) . toContain ( '@example Foo' ) ;
966+ } ) ;
967+ } ) ;
968+
853969/**
854970 * #5553 — the block is rendered as the markdown it was written as.
855971 *
0 commit comments