@@ -3,35 +3,61 @@ import assert from 'node:assert/strict';
33import fs from 'node:fs' ;
44import os from 'node:os' ;
55import path from 'node:path' ;
6+ import { spawnSync } from 'node:child_process' ;
7+ import { fileURLToPath } from 'node:url' ;
68import { buildInventory } from '../lib/inventory.mjs' ;
79import { compileContext } from '../lib/context.mjs' ;
810import { databaseStats , indexRepository } from '../lib/indexer.mjs' ;
9- import { generate } from '../lib/pipeline.mjs' ;
11+ import { audit , generate } from '../lib/pipeline.mjs' ;
1012import { projectPaths , readJson , writeJson } from '../lib/core.mjs' ;
1113
14+ const testDir = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
15+ const cli = path . resolve ( testDir , '..' , 'bin' , 'docgen-v2.mjs' ) ;
16+
1217function fixture ( ) {
1318 const root = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'docgen-v2-' ) ) ; const p = projectPaths ( root ) ;
1419 fs . mkdirSync ( path . join ( root , 'src' ) , { recursive : true } ) ;
1520 fs . mkdirSync ( path . dirname ( p . config ) , { recursive : true } ) ;
1621 writeJson ( p . project , { schemaVersion : '2.0' , kitVersion : '2.0.0' } ) ;
17- writeJson ( p . config , { schemaVersion : '2.0' , ignore : { binary : { enabled : true , maxTextFileBytes : 1024 * 1024 } } , context : { maxTokens : { default : 4000 , generate : 4000 } } , execution : { generationBatchSize : 2 } , audit : { llmEnabled : false } } ) ;
22+ writeJson ( p . config , { schemaVersion : '2.0' , projectName : 'Fixture' , ignore : { useGitignore : true , useDocgenignore : true , binary : { enabled : true , maxTextFileBytes : 1024 * 1024 } } , context : { maxTokens : { default : 4000 , generate : 4000 } } , execution : { generationBatchSize : 2 , maxPlannedPages : 30 } , audit : { llmEnabled : false } } ) ;
1823 writeJson ( p . state , { schemaVersion : '2.0' , kitVersion : '2.0.0' , stages : { } , pages : { } } ) ;
1924 return root ;
2025}
2126
27+ function catalogFixture ( root ) {
28+ const p = projectPaths ( root ) ;
29+ fs . writeFileSync ( path . join ( root , 'src' , 'Resource.java' ) , '@Path("/quotes")\nclass Resource { @POST void create() {} }\n' ) ;
30+ indexRepository ( root , { force : true } ) ;
31+ fs . mkdirSync ( p . model , { recursive : true } ) ;
32+ writeJson ( path . join ( p . model , 'catalogs.json' ) , { schemaVersion : '2.0' , endpoints : [ { id : 'create-quote' , name : 'Create quote' , statement : 'Creates a quote.' , classification : 'FACT' , confidence : 1 , method : 'POST' , path : '/quotes' , evidence : [ { path : 'src/Resource.java' , startLine : 1 } ] } ] , messageHandlers : [ ] , externalDependencies : [ ] , dataStores : [ ] , scheduledJobs : [ ] } ) ;
33+ writeJson ( p . plan , { schemaVersion : '2.0' , pages : [ { id : 'endpoint-catalog' , title : 'Endpoint Catalog' , summary : 'HTTP API reference.' , category : 'api' , mode : 'reference' , type : 'reference' , order : 1 , audience : [ 'engineer' ] , coverageTags : [ 'endpoint-catalog' ] , query : 'endpoints' , requiredSections : [ ] , relatedPages : [ ] } ] } ) ;
34+ return p ;
35+ }
36+
2237test ( 'inventory excludes binary and docgenignore paths' , ( ) => {
2338 const root = fixture ( ) ;
2439 fs . writeFileSync ( path . join ( root , 'src' , 'Resource.java' ) , '@Path("/quotes")\nclass Resource { @GET void get() {} }\n' ) ;
2540 fs . writeFileSync ( path . join ( root , 'secret.txt' ) , 'ignore me' ) ;
2641 fs . writeFileSync ( path . join ( root , 'image.png' ) , Buffer . from ( [ 0x89 , 0x50 , 0x4e , 0x47 , 0 , 1 ] ) ) ;
2742 fs . writeFileSync ( path . join ( root , '.docgenignore' ) , 'secret.txt\n' ) ;
28- const inv = buildInventory ( root , { force : true } ) ;
29- assert ( inv . files . some ( ( x ) => x . path === 'src/Resource.java' ) ) ;
30- assert ( ! inv . files . some ( ( x ) => x . path === 'secret.txt' ) ) ;
31- assert ( inv . excluded . some ( ( x ) => x . path === 'image.png' ) ) ;
43+ const inv = buildInventory ( root ) ;
44+ assert ( inv . files . some ( ( item ) => item . path === 'src/Resource.java' ) ) ;
45+ assert ( ! inv . files . some ( ( item ) => item . path === 'secret.txt' ) ) ;
46+ assert ( inv . excluded . some ( ( item ) => item . path === 'image.png' ) ) ;
47+ } ) ;
48+
49+ test ( 'non-git inventory respects nested gitignore files' , ( ) => {
50+ const root = fixture ( ) ;
51+ fs . mkdirSync ( path . join ( root , 'src' , 'generated' ) , { recursive : true } ) ;
52+ fs . writeFileSync ( path . join ( root , 'src' , 'generated' , '.gitignore' ) , '*.txt\n' ) ;
53+ fs . writeFileSync ( path . join ( root , 'src' , 'generated' , 'secret.txt' ) , 'ignored' ) ;
54+ fs . writeFileSync ( path . join ( root , 'src' , 'generated' , 'public.java' ) , 'class Public {}' ) ;
55+ const inv = buildInventory ( root ) ;
56+ assert ( ! inv . files . some ( ( item ) => item . path === 'src/generated/secret.txt' ) ) ;
57+ assert ( inv . files . some ( ( item ) => item . path === 'src/generated/public.java' ) ) ;
3258} ) ;
3359
34- test ( 'index is incremental and extracts repository facts' , ( ) => {
60+ test ( 'index is incremental and extracts source chunks and facts' , ( ) => {
3561 const root = fixture ( ) ;
3662 fs . writeFileSync ( path . join ( root , 'src' , 'Resource.java' ) , '@Path("/quotes")\nclass Resource { @POST void create() {} }\n' ) ;
3763 const first = indexRepository ( root , { force : true } ) ;
@@ -41,6 +67,7 @@ test('index is incremental and extracts repository facts', () => {
4167 assert . equal ( second . changedFiles , 0 ) ;
4268 assert . equal ( second . unchangedFiles > 0 , true ) ;
4369 assert . equal ( stats . facts > 0 , true ) ;
70+ assert . equal ( stats . sourceChunks > 0 , true ) ;
4471} ) ;
4572
4673test ( 'context compiler stays within configured budget' , ( ) => {
@@ -53,17 +80,34 @@ test('context compiler stays within configured budget', () => {
5380 assert ( payload . omissions . facts > 0 ) ;
5481} ) ;
5582
56- test ( 'reference catalog page is rendered without a provider call' , async ( ) => {
57- const root = fixture ( ) ; const p = projectPaths ( root ) ;
58- fs . writeFileSync ( path . join ( root , 'src' , 'Resource.java' ) , '@Path("/quotes")\nclass Resource { @POST void create() {} }\n' ) ;
59- indexRepository ( root , { force : true } ) ;
60- fs . mkdirSync ( p . model , { recursive : true } ) ;
61- writeJson ( path . join ( p . model , 'catalogs.json' ) , { schemaVersion : '2.0' , endpoints : [ { id : 'create-quote' , name : 'Create quote' , method : 'POST' , path : '/quotes' , evidence : [ { path : 'src/Resource.java' , startLine : 1 } ] } ] , messageHandlers : [ ] , externalDependencies : [ ] , dataStores : [ ] , scheduledJobs : [ ] } ) ;
62- writeJson ( p . plan , { schemaVersion : '2.0' , pages : [ { id : 'endpoint-catalog' , title : 'Endpoint Catalog' , summary : 'HTTP API reference.' , category : 'api' , mode : 'reference' , type : 'reference' , order : 1 , audience : [ 'engineer' ] , coverageTags : [ 'endpoint-catalog' ] , query : 'endpoints' , requiredSections : [ ] , relatedPages : [ ] } ] } ) ;
63- const result = await generate ( root ) ;
64- assert . equal ( result . providerPages , 0 ) ;
83+ test ( 'reference catalog page is deterministic, traced, auditable, and reusable' , async ( ) => {
84+ const root = fixture ( ) ; const p = catalogFixture ( root ) ;
85+ const first = await generate ( root ) ; const second = await generate ( root ) ;
86+ assert . equal ( first . providerPages , 0 ) ; assert . equal ( second . providerPages , 0 ) ;
6587 const output = fs . readFileSync ( path . join ( root , 'docs' , 'api' , 'endpoint-catalog.md' ) , 'utf8' ) ;
66- assert . match ( output , / P O S T / ) ;
67- assert . match ( output , / \/ q u o t e s / ) ;
88+ assert . match ( output , / P O S T / ) ; assert . match ( output , / \/ q u o t e s / ) ;
89+ const trace = readJson ( path . join ( p . traceability , 'pages' , 'endpoint-catalog.json' ) ) ;
90+ assert . equal ( trace . claims . length , 1 ) ; assert . equal ( trace . claims [ 0 ] . classification , 'FACT' ) ; assert . equal ( trace . claims [ 0 ] . evidence [ 0 ] . path , 'src/Resource.java' ) ;
91+ const quality = await audit ( root ) ; assert . equal ( quality . pass , true ) ;
6892 assert . equal ( fs . existsSync ( path . join ( p . telemetry , 'provider-runs.jsonl' ) ) , false ) ;
6993} ) ;
94+
95+ test ( 'deterministic audit rejects FACT evidence outside inventory' , async ( ) => {
96+ const root = fixture ( ) ; const p = catalogFixture ( root ) ; await generate ( root ) ;
97+ const file = path . join ( p . traceability , 'pages' , 'endpoint-catalog.json' ) ; const trace = readJson ( file ) ; trace . claims [ 0 ] . evidence = [ { path : 'ignored/secret.java' } ] ; writeJson ( file , trace ) ;
98+ await assert . rejects ( ( ) => audit ( root ) , / Q u a l i t y f a i l e d / ) ;
99+ } ) ;
100+
101+ test ( 'v1 migration preserves docs and ignore policy while archiving workflow state' , ( ) => {
102+ const root = fixture ( ) ; const p = projectPaths ( root ) ;
103+ writeJson ( p . config , { schemaVersion : '1.6' , projectName : 'Migrated' , commandCode : { executable : 'custom-cmdc' , model : 'cheap-model' } , ignore : { useGitignore : true , binary : { maxTextFileBytes : 123456 } } } ) ;
104+ writeJson ( p . project , { schemaVersion : '1.0' , kitVersion : '1.0.0' } ) ;
105+ fs . mkdirSync ( path . join ( root , 'docs' ) , { recursive : true } ) ; fs . writeFileSync ( path . join ( root , 'docs' , 'keep.md' ) , '# Keep\n' ) ; fs . writeFileSync ( path . join ( root , '.docgenignore' ) , 'private/**\n' ) ;
106+ fs . mkdirSync ( path . join ( p . base , 'evidence' ) , { recursive : true } ) ; fs . writeFileSync ( path . join ( p . base , 'evidence' , 'legacy.json' ) , '{}' ) ;
107+ const run = spawnSync ( process . execPath , [ cli , 'migrate' ] , { cwd : root , encoding : 'utf8' } ) ;
108+ assert . equal ( run . status , 0 , run . stderr ) ;
109+ assert . equal ( fs . readFileSync ( path . join ( root , 'docs' , 'keep.md' ) , 'utf8' ) , '# Keep\n' ) ;
110+ assert . equal ( fs . readFileSync ( path . join ( root , '.docgenignore' ) , 'utf8' ) , 'private/**\n' ) ;
111+ const next = readJson ( p . config ) ; assert . equal ( next . schemaVersion , '2.0' ) ; assert . equal ( next . projectName , 'Migrated' ) ; assert . equal ( next . commandCode . executable , 'custom-cmdc' ) ; assert . equal ( next . ignore . binary . maxTextFileBytes , 123456 ) ;
112+ const marker = readJson ( p . project ) ; assert . match ( marker . migrationBackup , / ^ \. d o c g e n \/ m i g r a t i o n - b a c k u p \/ / ) ; assert ( fs . existsSync ( path . join ( root , marker . migrationBackup , 'evidence' , 'legacy.json' ) ) ) ;
113+ } ) ;
0 commit comments