2026-05-09 09:02:29 +08:00
/**
* Unit tests for the docs route text generation functions.
*
* Tests the isPublic, generateLlmsTxt, and generateLlmsFullTxt helpers
* that produce llms.txt and llms-full.txt content from the OpenAPI spec.
*/
import { describe , expect , it , vi } from "vitest" ;
// ── Mocks ───────────────────────────────────────────────────────────────
vi . mock ( "../../../apps/api/src/db/index.js" , () => ({
db : {},
2026-06-13 10:15:23 +08:00
pool : {},
closeDb : async () => {},
2026-05-09 09:02:29 +08:00
schema : {},
}));
// ── Reproduce helper functions and types from docs.ts ───────────────────
interface PathOperation {
tags? : string [];
summary? : string ;
description? : string ;
security? : Array < Record < string , string [] >>;
parameters? : Array < { name : string ; in : string ; required? : boolean ; schema ?: { type : string } } > ;
requestBody ?: { content : Record < string , { schema ?: SchemaObject }> };
responses? : Record < string , { description ?: string }>;
}
interface SchemaObject {
type ?: string ;
properties? : Record < string , SchemaObject >;
required? : string [];
description? : string ;
}
interface OpenAPISpec {
info : { title : string ; version : string ; description? : string };
tags? : Array < { name : string ; description? : string } > ;
paths : Record < string , Record < string , PathOperation >>;
}
function isPublic ( op : PathOperation ) : boolean {
return Array . isArray ( op . security ) && op . security . length === 0 ;
}
function generateLlmsTxt ( spec : OpenAPISpec ) : string {
const lines : string [] = [];
lines . push ( `# ${ spec . info . title } ` );
lines . push ( "" );
lines . push (
2026-06-16 18:04:52 +08:00
"> Self-hosted file processing API with 157 tools across image, video, audio, document, and data. Convert, compress, edit, transcribe, OCR, and more." ,
2026-05-09 09:02:29 +08:00
);
lines . push ( "" );
lines . push ( "## Docs" );
lines . push ( "- [Interactive API Reference](/api/docs): Full interactive API documentation" );
lines . push ( "- [OpenAPI Spec](/api/v1/openapi.yaml): OpenAPI 3.1 specification (YAML)" );
lines . push (
"- [Full API Docs (LLM-friendly)](/llms-full.txt): Complete API documentation in plain text" ,
);
lines . push ( "" );
lines . push ( "## API Sections" );
for ( const tag of spec . tags || []) {
const count = Object . values ( spec . paths ). reduce (( n , methods ) => {
return n + Object . values ( methods ). filter (( op ) => op . tags ? .[ 0 ] === tag . name ). length ;
}, 0 );
lines . push ( `- ${ tag . name } ( ${ count } endpoints): ${ tag . description || "" } ` );
}
lines . push ( "" );
lines . push ( "## Authentication" );
lines . push ( "- Session token via `POST /api/auth/login` -> `Authorization: Bearer <token>`" );
lines . push ( "- API key (prefixed `si_`) -> `Authorization: Bearer si_...`" );
return lines . join ( "\n" );
}
function generateLlmsFullTxt ( spec : OpenAPISpec ) : string {
const lines : string [] = [];
lines . push ( `# ${ spec . info . title } v ${ spec . info . version } ` );
lines . push ( "" );
if ( spec . info . description ) {
lines . push ( spec . info . description . trim ());
lines . push ( "" );
}
const tagGroups = new Map < string , Array < { method : string ; path : string ; op : PathOperation }> > ();
for ( const [ path , methods ] of Object . entries ( spec . paths )) {
for ( const [ method , op ] of Object . entries ( methods )) {
const tag = op . tags ? .[ 0 ] || "Other" ;
if ( ! tagGroups . has ( tag )) tagGroups . set ( tag , []);
tagGroups . get ( tag ) ? . push ({ method : method.toUpperCase (), path , op });
}
}
const tagOrder = ( spec . tags || []). map (( t ) => t . name );
const allTags = [... new Set ([... tagOrder , ... tagGroups . keys ()])];
for ( const tag of allTags ) {
const endpoints = tagGroups . get ( tag );
if ( ! endpoints ) continue ;
const tagInfo = spec . tags ? . find (( t ) => t . name === tag );
lines . push ( `## ${ tag } ` );
if ( tagInfo ? . description ) lines . push ( ` ${ tagInfo . description } ` );
lines . push ( "" );
for ( const { method , path , op } of endpoints ) {
const auth = isPublic ( op ) ? "(public)" : "(auth required)" ;
lines . push ( `### ${ method } ${ path } ${ auth } ` );
if ( op . summary ) lines . push ( `** ${ op . summary } **` );
if ( op . description ) lines . push ( op . description . trim ());
lines . push ( "" );
if ( op . parameters ? . length ) {
lines . push ( "**Parameters:**" );
for ( const p of op . parameters ) {
lines . push (
`- \` ${ p . name } \` ( ${ p . in }${ p . required ? ", required" : "" } ) — ${ p . schema ? . type || "string" } ` ,
);
}
lines . push ( "" );
}
if ( op . requestBody ) {
const contentType = Object . keys ( op . requestBody . content )[ 0 ];
const schema = op . requestBody . content [ contentType ] ? . schema ;
lines . push ( `**Request:** \` ${ contentType } \`` );
if ( schema ? . properties ) {
for ( const [ name , prop ] of Object . entries ( schema . properties )) {
const required = schema . required ? . includes ( name ) ? " (required)" : "" ;
const desc = prop . description ? ` — ${ prop . description . split ( "\n" )[ 0 ] } ` : "" ;
lines . push ( `- \` ${ name } \` ${ required } : ${ prop . type || "string" }${ desc } ` );
}
}
lines . push ( "" );
}
if ( op . responses ) {
lines . push ( "**Responses:**" );
for ( const [ code , res ] of Object . entries ( op . responses )) {
lines . push ( `- \` ${ code } \` — ${ res . description || "" } ` );
}
lines . push ( "" );
}
}
}
return lines . join ( "\n" );
}
// ── Tests ───────────────────────────────────────────────────────────────
describe ( "docs route logic" , () => {
describe ( "isPublic" , () => {
it ( "returns true for empty security array" , () => {
expect ( isPublic ({ security : [] })). toBe ( true );
});
it ( "returns false for non-empty security array" , () => {
expect ( isPublic ({ security : [{ bearerAuth : [] }] })). toBe ( false );
});
it ( "returns false for undefined security" , () => {
expect ( isPublic ({})). toBe ( false );
});
it ( "returns false for null-like security" , () => {
expect ( isPublic ({ security : undefined })). toBe ( false );
});
});
describe ( "generateLlmsTxt" , () => {
const minimalSpec : OpenAPISpec = {
info : { title : "SnapOtter API" , version : "1.0.0" },
tags : [
{ name : "Tools" , description : "Image processing tools" },
{ name : "Auth" , description : "Authentication" },
],
paths : {
"/api/v1/tools/resize" : {
post : { tags : [ "Tools" ], summary : "Resize image" },
},
"/api/auth/login" : {
post : { tags : [ "Auth" ], summary : "Login" , security : [] },
},
},
};
it ( "starts with the API title" , () => {
const result = generateLlmsTxt ( minimalSpec );
expect ( result ). toContain ( "# SnapOtter API" );
});
it ( "includes the Docs section" , () => {
const result = generateLlmsTxt ( minimalSpec );
expect ( result ). toContain ( "## Docs" );
expect ( result ). toContain ( "[Interactive API Reference]" );
expect ( result ). toContain ( "[OpenAPI Spec]" );
});
it ( "lists API sections with endpoint counts" , () => {
const result = generateLlmsTxt ( minimalSpec );
expect ( result ). toContain ( "Tools (1 endpoints): Image processing tools" );
expect ( result ). toContain ( "Auth (1 endpoints): Authentication" );
});
it ( "includes authentication section" , () => {
const result = generateLlmsTxt ( minimalSpec );
expect ( result ). toContain ( "## Authentication" );
});
it ( "handles spec with no tags" , () => {
const specNoTags : OpenAPISpec = {
info : { title : "Test API" , version : "1.0.0" },
paths : {},
};
const result = generateLlmsTxt ( specNoTags );
expect ( result ). toContain ( "# Test API" );
expect ( result ). toContain ( "## API Sections" );
});
it ( "handles spec with empty paths" , () => {
const specEmpty : OpenAPISpec = {
info : { title : "Empty API" , version : "0.1.0" },
tags : [{ name : "Tools" , description : "desc" }],
paths : {},
};
const result = generateLlmsTxt ( specEmpty );
expect ( result ). toContain ( "Tools (0 endpoints): desc" );
});
});
describe ( "generateLlmsFullTxt" , () => {
const fullSpec : OpenAPISpec = {
info : {
title : "SnapOtter API" ,
version : "2.0.0" ,
description : "Image processing API" ,
},
tags : [{ name : "Tools" , description : "Processing tools" }],
paths : {
"/api/v1/tools/resize" : {
post : {
tags : [ "Tools" ],
summary : "Resize an image" ,
description : "Resize to specified dimensions" ,
parameters : [
{ name : "width" , in : "query" , required : true , schema : { type : "integer" } },
],
requestBody : {
content : {
"multipart/form-data" : {
schema : {
properties : {
file : { type : "string" , description : "Image file to process" },
},
required : [ "file" ],
},
},
},
},
responses : {
"200" : { description : "Successful resize" },
"400" : { description : "Invalid input" },
},
},
},
"/api/health" : {
get : {
summary : "Health check" ,
security : [],
},
},
},
};
it ( "includes title with version" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "# SnapOtter API v2.0.0" );
});
it ( "includes the description" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "Image processing API" );
});
it ( "groups endpoints by tag" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "## Tools" );
});
it ( "marks auth-required endpoints" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "POST /api/v1/tools/resize (auth required)" );
});
it ( "marks public endpoints" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "GET /api/health (public)" );
});
it ( "includes parameters section" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "**Parameters:**" );
expect ( result ). toContain ( "`width` (query, required)" );
});
it ( "includes request body section" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "**Request:** `multipart/form-data`" );
});
it ( "includes responses section" , () => {
const result = generateLlmsFullTxt ( fullSpec );
expect ( result ). toContain ( "**Responses:**" );
expect ( result ). toContain ( "`200`" );
expect ( result ). toContain ( "`400`" );
});
it ( "handles spec with no description" , () => {
const specNoDesc : OpenAPISpec = {
info : { title : "No Desc API" , version : "1.0.0" },
paths : {},
};
const result = generateLlmsFullTxt ( specNoDesc );
expect ( result ). toContain ( "# No Desc API v1.0.0" );
});
it ( "puts untagged endpoints under 'Other'" , () => {
const specNoTag : OpenAPISpec = {
info : { title : "Test" , version : "1.0.0" },
paths : {
"/health" : { get : { summary : "Health" } },
},
};
const result = generateLlmsFullTxt ( specNoTag );
expect ( result ). toContain ( "## Other" );
});
});
});