2026-07-27 10:25:04 +08:00
import { execFileSync } from 'node:child_process' ;
2026-07-27 00:18:42 +08:00
import fs from 'node:fs' ;
import path from 'node:path' ;
2026-07-27 09:56:33 +08:00
import vm from 'node:vm' ;
import ts from 'typescript' ;
2026-07-27 10:25:04 +08:00
const backendDirectory = path . resolve ( '../..' , 'backend/api' ) ;
const manifest = JSON . parse ( execFileSync ( 'go' , [ 'run' , './cmd/resource-contract' ] , { cwd : backendDirectory , encoding : 'utf8' } ) ) ;
let responseShapeVerified = true ;
try {
execFileSync ( 'go' , [ 'test' , '-count=1' , './internal/logic/platform' , '-run' , '^TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID$' ] , { cwd : backendDirectory , stdio : 'pipe' } ) ;
} catch {
responseShapeVerified = false ;
}
const compiled = ts . transpileModule ( fs . readFileSync ( 'src/api/resources.ts' , 'utf8' ) , { compilerOptions : { module : ts . ModuleKind . CommonJS , target : ts . ScriptTarget . ES2020 } } ) . outputText ;
2026-07-27 09:56:33 +08:00
const resourceModule = { exports : { } } ;
vm . runInNewContext ( compiled , { module : resourceModule , exports : resourceModule . exports } ) ;
const resources = resourceModule . exports . resources ;
2026-07-27 09:44:08 +08:00
const files = ( dir ) => fs . existsSync ( dir ) ? fs . readdirSync ( dir , { withFileTypes : true } ) . flatMap ( ( entry ) => entry . isDirectory ( ) ? files ( path . join ( dir , entry . name ) ) : [ path . join ( dir , entry . name ) ] ) : [ ] ;
2026-07-27 10:25:04 +08:00
const failures = [ ] ;
if ( ! responseShapeVerified ) failures . push ( 'backend list response shape is not identity-only' ) ;
const expected = manifest . resources ;
if ( resources . length !== expected . length ) failures . push ( ` catalogue count: ${ resources . length } / ${ expected . length } ` ) ;
for ( const contract of expected ) {
const resource = resources . find ( ( item ) => item . name === contract . name ) ;
const pageKind = contract . name === 'ec_category' ? 'tree' : contract . pageKind ;
if ( ! resource || resource . mode !== contract . mode || resource . pageKind !== pageKind ) { failures . push ( ` ${ contract . name } : contract mismatch ` ) ; continue ; }
if ( ! /^[\u4e00-\u9fff]/ . test ( resource . title ) || resource . fields . some ( ( field ) => ! /^[\u4e00-\u9fff]/ . test ( field . label ) ) ) failures . push ( ` ${ contract . name } : missing Chinese business labels ` ) ;
if ( resource . fields . length === 0 || resource . fields . some ( ( field ) => field . key === 'id' || field . key . endsWith ( '_id' ) ) ) failures . push ( ` ${ contract . name } : invalid allowlist ` ) ;
const method = resource . mode === 'append_only' ? 'POST' : 'GET' ;
if ( ! manifest . routes . some ( ( route ) => route . method === method && route . path === resource . resource ) ) failures . push ( ` ${ contract . name } : resource route mismatch ` ) ;
if ( resource . mode === 'readonly' ) { const view = files ( 'src/views' ) . find ( ( file ) => file . includes ( resource . name ) && file . endsWith ( 'ListPage.vue' ) ) ; if ( ! view || ! fs . readFileSync ( view , 'utf8' ) . includes ( 'ReadOnlyListPage' ) ) failures . push ( ` ${ contract . name } : readonly UI ` ) ; }
2026-07-27 00:18:42 +08:00
}
2026-07-27 10:25:04 +08:00
if ( JSON . stringify ( resources . map ( ( item ) => item . name ) . sort ( ) ) !== JSON . stringify ( expected . map ( ( item ) => item . name ) . sort ( ) ) ) failures . push ( 'catalogue names differ from backend ExpectedResources' ) ;
for ( const name of [ 'ec_category' , 'platform_menu' ] ) { const view = files ( 'src/views' ) . find ( ( file ) => file . includes ( name ) && file . endsWith ( 'TreePage.vue' ) ) ; if ( ! view ) failures . push ( ` ${ name } : tree page missing ` ) ; }
const tree = fs . readFileSync ( 'src/views/shared/TreePage.vue' , 'utf8' ) ;
if ( ! tree . includes ( 'parent_identity' ) || ! tree . includes ( 'canWrite' ) || ! tree . includes ( 'resourceApi.create' ) || ! tree . includes ( 'resourceApi.update' ) ) failures . push ( 'tree: writable identity actions missing' ) ;
2026-07-27 09:44:08 +08:00
for ( const failure of failures ) console . log ( failure ) ;
if ( failures . length ) process . exitCode = 1 ;