40 lines
3.4 KiB
JavaScript
40 lines
3.4 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import vm from 'node:vm';
|
|
import ts from 'typescript';
|
|
|
|
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;
|
|
const resourceModule = { exports: {} };
|
|
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
|
const resources = resourceModule.exports.resources;
|
|
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)]) : [];
|
|
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`); }
|
|
}
|
|
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');
|
|
for (const failure of failures) console.log(failure);
|
|
if (failures.length) process.exitCode = 1;
|