/* * Description: Static and helper regression tests for MXPIC EDA frontend/backend integration contracts. * Inside functions: N/A - assertion-based test/module script. * Developer : Qin Yue @ 2026 * Organization : OptiHK Limited */ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const root = path.resolve(__dirname, '..'); const backendDir = path.join(root, 'backend'); const serverPy = fs.readFileSync(path.join(backendDir, 'server.py'), 'utf8'); const removedInternalPdkPath = ['mxpic', 'PDKs'].join('/'); const removedEdaPdkRootName = 'EDA_' + 'PDK_ROOT'; const removedYmlPathName = 'YML_' + 'PATH'; assert( !fs.existsSync(path.join(backendDir, 'layout_preview.py')), 'EDA backend should not keep the legacy local GDS/SVG preview builder' ); assert( !fs.existsSync(path.join(backendDir, 'pdk_registry.py')), 'EDA backend should not keep the legacy local PDK GDS registry' ); assert( fs.existsSync(path.join(backendDir, 'gds_builder.py')), 'backend/gds_builder.py should build hierarchical GDS from saved project YAML' ); assert( fs.existsSync(path.join(backendDir, 'routed_layout_preview.py')), 'backend/routed_layout_preview.py should create routed SVG previews through mxpic_router' ); assert( fs.existsSync(path.join(backendDir, 'router_dependency.py')), 'backend/router_dependency.py should validate build-time mxpic_router runtime dependencies' ); assert( !serverPy.includes('from router_dependency import require_router_stack') && !serverPy.includes('Verified mxpic_router stack') && !serverPy.includes('require_router_stack()'), 'server startup should not require mxpic_router; only build actions should validate the router stack' ); assert( !serverPy.includes('create_layout_svg_from_gds'), 'save-layout route should not use the legacy gdstk-only preview builder' ); assert( serverPy.includes('create_routed_layout_svg') && !serverPy.includes('if layout_has_links(content):'), 'save-layout route should always use routed preview generation through mxpic_router' ); assert( serverPy.includes('cell_routes_path') && serverPy.includes('write_route_points_sidecar') && serverPy.includes('.routes.yml'), 'save-layout should write a sidecar route-points file for manually drawn link guidance' ); assert( serverPy.includes('svg_url'), 'save-layout response should include an svg_url for the new layout tab' ); assert( serverPy.includes('RouterStackUnavailable') && serverPy.includes('except RouterStackUnavailable as e') && serverPy.includes('"preview_status": preview_status') && serverPy.includes('"preview_error": preview_error'), 'save-layout should still return success and explain skipped SVG preview when mxpic_router is unavailable' ); assert( serverPy.includes("@app.route('/api/projects//cells//layout.svg')"), 'server should expose a route for saved cell SVG previews' ); assert( serverPy.includes("@app.route('/api/build-gds'"), 'server should expose a Build GDS API route' ); assert( serverPy.includes("@app.route('/api/technologies///manifest'"), 'server should expose a technology manifest API route' ); assert( fs.existsSync(path.join(backendDir, 'technology_manifest.py')), 'backend/technology_manifest.py should read generated technology manifests' ); assert( !serverPy.includes(removedEdaPdkRootName) && !serverPy.includes(removedYmlPathName) && !serverPy.includes("'mxpic', 'PDKs'") && !serverPy.includes(removedInternalPdkPath), 'server should not use an internal EDA PDK copy as a separate technology source of truth' ); assert( serverPy.includes('pdks_root = current_pdk_root()') && serverPy.includes('os.path.exists(os.path.join(technology_path, "technology.yml"))'), 'technology list should scan the active role PDK root and only expose folders containing technology.yml' ); assert( serverPy.includes('read_technology_manifest(current_pdk_root()'), 'technology manifest API should read technology.yml from the active role PDK root' ); assert( !fs.existsSync(path.join(root, 'mxpic')), 'mxpic_EDA should not contain a redundant internal technology copy' ); const gdsBuilderPy = fs.readFileSync(path.join(backendDir, 'gds_builder.py'), 'utf8'); assert( gdsBuilderPy.includes('return _build_with_mxpic_router(') && !gdsBuilderPy.includes('return _build_with_gdstk') && !gdsBuilderPy.includes('return _build_with_nazca'), 'Build GDS should use mxpic_router as the only production builder' ); assert( gdsBuilderPy.includes('from router_dependency import require_router_stack') && gdsBuilderPy.includes('require_router_stack()'), 'Build GDS API wrapper should validate mxpic_router only when a GDS build is requested' ); const routedPreviewPy = fs.readFileSync(path.join(backendDir, 'routed_layout_preview.py'), 'utf8'); assert( routedPreviewPy.includes('from router_dependency import require_router_stack') && routedPreviewPy.includes('require_router_stack(require_gdstk=True)'), 'Build Layout preview should validate mxpic_router and gdstk only when preview generation is requested' ); const routerDependencyPy = fs.readFileSync(path.join(backendDir, 'router_dependency.py'), 'utf8'); assert( routerDependencyPy.includes('def require_router_stack') && routerDependencyPy.includes('class RouterStackUnavailable') && routerDependencyPy.includes('require_gdstk: bool = False') && routerDependencyPy.includes('importlib.import_module("nazca")') && routerDependencyPy.includes('mxpic_router.builder') && routerDependencyPy.includes('_import_route_backend'), 'router dependency gate should validate mxpic_router, Nazca, optional gdstk, and route backend imports' ); const routerDir = path.resolve(root, '..', 'mxpic_router', 'mxpic_router'); if (fs.existsSync(routerDir)) { const routerLoaderPy = fs.readFileSync(path.join(routerDir, 'eda_loader.py'), 'utf8'); const routerBuilderPy = fs.readFileSync(path.join(routerDir, 'builder.py'), 'utf8'); assert( routerLoaderPy.includes('pins: Dict[str, PinSpec]') && routerLoaderPy.includes('pin_number') && routerLoaderPy.includes('pitch: float = 10.0') && routerLoaderPy.includes('pins=_pins(element.get("pins"))'), 'mxpic_router loader should parse pins-only layout metadata from exported elements' ); assert( routerBuilderPy.includes('_port_element_pin_entries') && routerBuilderPy.includes('_anchor_element_pin_entries') && routerBuilderPy.includes('_metadata_pins') && routerBuilderPy.includes('link.src_pin') && routerBuilderPy.includes('_NazcaInterconnectRoute') && routerBuilderPy.includes('_import_route_backend'), 'mxpic_router builder should register named element pins and route through pin endpoints' ); } assert( serverPy.includes('def scoped_pdk_root_for_project') && serverPy.includes('read_project_meta(project_name).get("technology")') && serverPy.includes('os.path.join(base_root, foundry, technology)'), 'backend should resolve a project-scoped PDK root from selected foundry/technology' ); assert( serverPy.includes('request.args.get(\'project\')') && serverPy.includes('scoped_pdk_root_for_project(project)'), 'library/component APIs should accept ?project= and search inside the selected technology folder' ); assert( serverPy.includes('__path__') && serverPy.includes('os.path.relpath(root, path_root)'), 'library tree leaves should preserve component paths relative to the role PDK root' ); assert( serverPy.includes('f.lower() != "technology.yml"') && serverPy.includes('component_yml_files'), 'component library scanning should ignore technology.yml so it can descend into technology folders' );