Files
maka/scripts/generate-astryx-surface-inventory.test.mjs
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

229 lines
9.7 KiB
JavaScript

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { after, describe, test } from 'node:test';
import {
analyzeTsx,
assertAstryxComponentSet,
loadAstryxComponents,
loadMakaUiBarrel,
parseAstryxDistComponents,
} from './generate-astryx-surface-inventory.mjs';
const tmpRoots = [];
/** Materialize a fake repo root (`packages/ui/src/*`) from a {relPath: source} map. */
function fixtureRepo(files) {
const repoRoot = mkdtempSync(join(tmpdir(), 'astryx-inv-'));
tmpRoots.push(repoRoot);
for (const [rel, contents] of Object.entries(files)) {
const abs = join(repoRoot, rel);
mkdirSync(join(abs, '..'), { recursive: true });
writeFileSync(abs, contents);
}
return repoRoot;
}
after(() => {
for (const dir of tmpRoots) rmSync(dir, { force: true, recursive: true });
});
describe('Astryx component set derivation (#3868)', () => {
test('is derived from @astryxdesign/core with the resolved version', () => {
const { version, components } = loadAstryxComponents();
assert.match(version, /^\d+\.\d+\.\d+/);
assert.ok(components instanceof Set);
// Components the old 47-name hand-list omitted — the drift #3868 fixes.
for (const name of ['Timestamp', 'ChatMessage', 'ChatMessageMetadata', 'Thumbnail']) {
assert.ok(components.has(name), `expected derived set to include ${name}`);
}
// Fail-closed floor: a partial parse must not slip through.
assert.ok(components.size >= 150, `expected >=150 components, got ${components.size}`);
// Type/prop exports are not components.
assert.ok(!components.has('ButtonProps'));
assert.ok(!components.has('BaseProps'));
});
});
describe('Astryx declaration parsing is fail-closed (#3868)', () => {
test('traverses a genuine multi-level `export *` barrel graph', () => {
// dist/index.d.ts --export*--> Group --export*--> Sub --export*--> Leaf,
// where the component is only declared in the deepest file. A one-layer
// parser would stop at Group and silently omit Deep; the recursive walk
// (relative to each file's own dir) must reach it.
const repoRoot = fixtureRepo({
'dist/index.d.ts': ["export { Direct } from './Direct';", "export * from './Group';"].join(
'\n',
),
'dist/Group/index.d.ts': "export * from './Sub';\n",
'dist/Group/Sub/index.d.ts': "export * from './Leaf';\n",
'dist/Group/Sub/Leaf/index.d.ts': 'export declare const Deep: unknown;\n',
});
const set = parseAstryxDistComponents(join(repoRoot, 'dist'));
assert.ok(set.has('Direct'), 'top-level named re-export resolves');
assert.ok(set.has('Deep'), 'component behind a 3-level barrel graph resolves');
});
test('resolves a flat `Dir.d.ts` barrel target alongside directory barrels', () => {
const repoRoot = fixtureRepo({
'dist/index.d.ts': "export * from './Flat';\n",
'dist/Flat.d.ts': 'export declare function FlatOne(): null;\n',
});
const set = parseAstryxDistComponents(join(repoRoot, 'dist'));
assert.ok(set.has('FlatOne'));
});
test('is cycle-safe when barrels re-export each other', () => {
// A ⇄ B mutual `export *` must terminate, not overflow the stack.
const repoRoot = fixtureRepo({
'dist/index.d.ts': "export * from './A';\n",
'dist/A/index.d.ts': ["export * from '../B';", 'export declare const FromA: unknown;'].join(
'\n',
),
'dist/B/index.d.ts': ["export * from '../A';", 'export declare const FromB: unknown;'].join(
'\n',
),
});
const set = parseAstryxDistComponents(join(repoRoot, 'dist'));
assert.ok(set.has('FromA'));
assert.ok(set.has('FromB'));
});
test('throws on an unresolved barrel target', () => {
const repoRoot = fixtureRepo({ 'dist/index.d.ts': "export * from './Missing';\n" });
assert.throws(() => parseAstryxDistComponents(join(repoRoot, 'dist')), /unresolved/i);
});
test('throws on an unresolved target nested below the first barrel layer', () => {
const repoRoot = fixtureRepo({
'dist/index.d.ts': "export * from './Group';\n",
'dist/Group/index.d.ts': "export * from './Gone';\n",
});
assert.throws(() => parseAstryxDistComponents(join(repoRoot, 'dist')), /unresolved/i);
});
test('rejects a partial parse (too few components / missing a key component)', () => {
assert.throws(() => assertAstryxComponentSet(new Set(['Button']), '0.5.0'), /incomplete/i);
const bigButMissingKey = new Set(Array.from({ length: 200 }, (_, i) => `Widget${i}`));
assert.throws(() => assertAstryxComponentSet(bigButMissingKey, '0.5.0'), /missing/i);
});
});
describe('@maka/ui barrel resolution (#3868)', () => {
test('derives the Astryx re-export map from the real barrel', () => {
const { reexports } = loadMakaUiBarrel();
// Auto-derived (replaces a hand-written 9-name list) — much broader coverage.
assert.ok(reexports.size >= 20, `expected a broad re-export map, got ${reexports.size}`);
// A direct re-export maps to its canonical Astryx name.
assert.equal(reexports.get('Selector'), 'Selector');
assert.equal(reexports.get('Button'), 'Button');
assert.equal(reexports.get('CommandPalette'), 'CommandPalette');
});
test('resolves nested export graphs and flags only local public shadows', () => {
const repoRoot = fixtureRepo({
'packages/ui/src/index.ts': [
"export { Button, Badge as UiBadge } from '@astryxdesign/core';",
"export * from './local.js';",
'export function Card() { return null; }', // local public shadow: Card is an Astryx component
'export function MyWidget() { return null; }', // not an Astryx name → not a shadow
].join('\n'),
'packages/ui/src/local.ts': [
"export { Timestamp } from '@astryxdesign/core';",
'export function helperThing() { return null; }',
].join('\n'),
});
const { reexports, shadowsByFile } = loadMakaUiBarrel(repoRoot);
// Direct + aliased + transitive (through export *) re-exports all resolve.
assert.equal(reexports.get('Button'), 'Button');
assert.equal(reexports.get('UiBadge'), 'Badge');
assert.equal(reexports.get('Timestamp'), 'Timestamp');
// Only the locally-defined public export whose name shadows Astryx is flagged.
assert.equal(shadowsByFile.get('packages/ui/src/index.ts')?.has('Card'), true);
assert.equal(shadowsByFile.get('packages/ui/src/index.ts')?.has('MyWidget'), false);
});
test('fails closed on an unresolved relative barrel target', () => {
const repoRoot = fixtureRepo({
'packages/ui/src/index.ts': "export * from './does-not-exist.js';\n",
});
assert.throws(() => loadMakaUiBarrel(repoRoot), /unresolved/i);
});
});
describe('analyzeTsx severity (#3868)', () => {
const astryxComponents = loadAstryxComponents().components;
const base = { astryxComponents, makaUiReexports: new Map(), shadowNames: null };
test('counts a rendered Astryx component in the "Astryx used" column', () => {
const src = `
import { Timestamp } from '@astryxdesign/core';
export function Row() { return <Timestamp value={0} />; }
`;
const result = analyzeTsx('packages/ui/src/row.tsx', src, base);
assert.match(result.astryx, /Timestamp/);
assert.equal(result.severity, 'aligned');
});
test('counts an Astryx component imported via the @maka/ui re-export map', () => {
const src = `
import { Selector } from '@maka/ui';
export function Row() { return <Selector value="x" />; }
`;
const result = analyzeTsx('apps/desktop/src/renderer/row.tsx', src, {
...base,
makaUiReexports: new Map([['Selector', 'Selector']]),
});
assert.match(result.astryx, /Selector/);
assert.equal(result.severity, 'aligned');
});
test('flags a public export that shadows an Astryx component', () => {
const src = 'export function Button() { return null; }\n';
const result = analyzeTsx('packages/ui/src/button.tsx', src, {
...base,
shadowNames: new Set(['Button']),
});
assert.equal(result.severity, 'reimplementation');
assert.match(result.gaps, /public export `Button` shadows Astryx component/);
});
test('does not flag a private local component that merely shares a name', () => {
// No shadowNames for this file (it is not a public @maka/ui export).
const src = `
export function ModuleHubSelector() {
function Selector() { return null; }
return <Selector />;
}
`;
const result = analyzeTsx('packages/ui/src/module-hub-selector.tsx', src, base);
assert.equal(result.severity, 'aligned');
assert.doesNotMatch(result.gaps, /shadows/);
});
test('still flags a raw interactive control as a blocker', () => {
const src = 'export function Bad() { return <button type="button">x</button>; }\n';
const result = analyzeTsx('packages/ui/src/bad.tsx', src, base);
assert.equal(result.severity, 'blocker');
});
});