mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
fix: expose SONA stats and normalize ESM exports
This commit is contained in:
+2
-1
@@ -59,7 +59,8 @@
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/preinstall.cjs || true",
|
||||
"postinstall": "node scripts/postinstall.cjs",
|
||||
"build": "tsc && npm run build:cli && npm run build:mcp",
|
||||
"build": "tsc && npm run build:esm && npm run build:cli && npm run build:mcp",
|
||||
"build:esm": "node scripts/fix-esm-specifiers.mjs",
|
||||
"build:cli": "node scripts/build-cli.mjs",
|
||||
"build:mcp": "node scripts/build-mcp.mjs",
|
||||
"prepublishOnly": "node scripts/sync-agents.cjs && node scripts/prepare-assets.cjs",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Normalize relative ESM specifiers in TypeScript's emitted JavaScript.
|
||||
*
|
||||
* The source tree historically uses bundler-style extensionless imports. The
|
||||
* CLI/MCP bundles resolve those, but package exports point at raw `dist/` ESM,
|
||||
* where Node requires an explicit file or directory index. This post-emit pass
|
||||
* keeps source churn contained while making the published artifact valid ESM.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const SPECIFIER_RE = /(from\s*|import\s*\(\s*|import\s+)(['"])(\.{1,2}\/[^'"\n]+)\2/g;
|
||||
const ALIAS_SPECIFIER_RE = /(from\s*|import\s*\(\s*|import\s+)(['"])(@(shared|kernel|domains|coordination|adapters|integrations)\/[^'"\n]+)\2/g;
|
||||
|
||||
export function resolveRelativeSpecifier(importerPath, specifier) {
|
||||
const suffixIndex = specifier.search(/[?#]/);
|
||||
const cleanSpecifier = suffixIndex === -1 ? specifier : specifier.slice(0, suffixIndex);
|
||||
const suffix = suffixIndex === -1 ? '' : specifier.slice(suffixIndex);
|
||||
if (/\.(?:[cm]?js|json|node)$/.test(cleanSpecifier)) return specifier;
|
||||
|
||||
const resolved = path.resolve(path.dirname(importerPath), cleanSpecifier);
|
||||
if (fs.existsSync(`${resolved}.js`)) return `${cleanSpecifier}.js${suffix}`;
|
||||
if (fs.existsSync(path.join(resolved, 'index.js'))) return `${cleanSpecifier}/index.js${suffix}`;
|
||||
if (fs.existsSync(`${resolved}.json`)) return `${cleanSpecifier}.json${suffix}`;
|
||||
return specifier;
|
||||
}
|
||||
|
||||
export function normalizeEmittedESM(distDir) {
|
||||
const files = listJavaScriptFiles(distDir);
|
||||
let rewrittenFiles = 0;
|
||||
let rewrittenSpecifiers = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const original = fs.readFileSync(file, 'utf8');
|
||||
let normalized = original.replace(
|
||||
SPECIFIER_RE,
|
||||
(match, prefix, quote, specifier) => {
|
||||
const resolved = resolveRelativeSpecifier(file, specifier);
|
||||
if (resolved === specifier) return match;
|
||||
rewrittenSpecifiers++;
|
||||
return `${prefix}${quote}${resolved}${quote}`;
|
||||
},
|
||||
);
|
||||
normalized = normalized.replace(
|
||||
ALIAS_SPECIFIER_RE,
|
||||
(match, prefix, quote, specifier, alias) => {
|
||||
const target = path.join(distDir, alias, specifier.slice(alias.length + 2));
|
||||
const targetExists = fs.existsSync(target)
|
||||
|| fs.existsSync(`${target}.js`)
|
||||
|| fs.existsSync(path.join(target, 'index.js'))
|
||||
|| fs.existsSync(`${target}.json`);
|
||||
if (!targetExists) return match;
|
||||
let relative = path.relative(path.dirname(file), target).split(path.sep).join('/');
|
||||
if (!relative.startsWith('.')) relative = `./${relative}`;
|
||||
const resolved = resolveRelativeSpecifier(file, relative);
|
||||
rewrittenSpecifiers++;
|
||||
return `${prefix}${quote}${resolved}${quote}`;
|
||||
},
|
||||
);
|
||||
if (normalized !== original) {
|
||||
fs.writeFileSync(file, normalized);
|
||||
rewrittenFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
return { scannedFiles: files.length, rewrittenFiles, rewrittenSpecifiers };
|
||||
}
|
||||
|
||||
function listJavaScriptFiles(root) {
|
||||
if (!fs.existsSync(root)) return [];
|
||||
const files = [];
|
||||
const pending = [root];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
const resolved = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) pending.push(resolved);
|
||||
else if (entry.isFile() && entry.name.endsWith('.js')) files.push(resolved);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
const distDir = path.resolve(process.argv[2] ?? 'dist');
|
||||
const result = normalizeEmittedESM(distDir);
|
||||
console.log(
|
||||
`Normalized ESM specifiers: ${result.rewrittenSpecifiers} in `
|
||||
+ `${result.rewrittenFiles}/${result.scannedFiles} emitted files`,
|
||||
);
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
import {
|
||||
PersistentSONAEngine,
|
||||
createPersistentSONAEngine,
|
||||
type PersistedSONAStats,
|
||||
} from '../../integrations/ruvector/sona-persistence.js';
|
||||
import {
|
||||
type QESONAPattern,
|
||||
@@ -802,6 +803,13 @@ export class LearningOptimizationCoordinator
|
||||
return this.sona!.getStats();
|
||||
}
|
||||
|
||||
/** Return persisted SONA aggregates without mutating or reconstructing the store. */
|
||||
async getSONAPersistedStats(): Promise<PersistedSONAStats> {
|
||||
this.ensureInitialized();
|
||||
this.ensureSONAAvailable();
|
||||
return this.sona!.getPersistedStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all QESONA patterns.
|
||||
*
|
||||
|
||||
@@ -484,6 +484,9 @@ export interface ILearningOptimizationCoordinator {
|
||||
*/
|
||||
getLearningDashboard(): Promise<Result<LearningDashboard>>;
|
||||
|
||||
/** Return read-only statistics from the persisted SONA pattern store. */
|
||||
getSONAPersistedStats(): Promise<import('../../integrations/ruvector/sona-persistence.js').PersistedSONAStats>;
|
||||
|
||||
/**
|
||||
* Export learned models
|
||||
*/
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
ProductionIntelService,
|
||||
ProductionIntelConfig,
|
||||
} from './services/index.js';
|
||||
import type { PersistedSONAStats } from '../../integrations/ruvector/sona-persistence.js';
|
||||
|
||||
/**
|
||||
* Plugin configuration options
|
||||
@@ -67,6 +68,8 @@ export interface LearningOptimizationAPI {
|
||||
// Availability check
|
||||
/** Check if SONA pattern engine is available (may be false if init failed) */
|
||||
isSONAAvailable(): boolean;
|
||||
/** Read-only persisted SONA statistics; null when SONA initialization failed. */
|
||||
getSONAPersistedStats(): Promise<PersistedSONAStats | null>;
|
||||
|
||||
// Coordinator methods
|
||||
runLearningCycle(domain: DomainName): Promise<Result<LearningCycleReport>>;
|
||||
@@ -184,6 +187,10 @@ export class LearningOptimizationPlugin extends BaseDomainPlugin {
|
||||
const api: LearningOptimizationExtendedAPI = {
|
||||
// Availability check
|
||||
isSONAAvailable: () => this.coordinator?.isSONAAvailable() ?? false,
|
||||
getSONAPersistedStats: async () => {
|
||||
if (!this.coordinator?.isSONAAvailable()) return null;
|
||||
return this.coordinator.getSONAPersistedStats();
|
||||
},
|
||||
|
||||
// Coordinator methods
|
||||
runLearningCycle: this.runLearningCycle.bind(this),
|
||||
|
||||
@@ -81,7 +81,7 @@ export const SONA_PATTERNS_SCHEMA = `
|
||||
/**
|
||||
* SONA Fisher matrices table schema - stores EWC++ state per domain
|
||||
*/
|
||||
export const SONA_FISHER_SCHEMA = `
|
||||
export const SONA_FISHER_SCHEMA = `
|
||||
-- SONA Fisher Information Matrices (Task 2.2: EWC++ Persistence)
|
||||
CREATE TABLE IF NOT EXISTS sona_fisher_matrices (
|
||||
domain TEXT PRIMARY KEY,
|
||||
@@ -96,9 +96,22 @@ export const SONA_FISHER_SCHEMA = `
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
`;
|
||||
|
||||
/** Read-only aggregate over the persisted SONA pattern store. */
|
||||
export interface PersistedSONAStats {
|
||||
totalPatterns: number;
|
||||
uniqueTypes: number;
|
||||
uniqueDomains: number;
|
||||
avgConfidence: number;
|
||||
avgUsage: number;
|
||||
totalSuccesses: number;
|
||||
totalFailures: number;
|
||||
byType: Record<string, number>;
|
||||
byDomain: Record<string, number>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
@@ -196,7 +209,7 @@ interface SONAPatternRow {
|
||||
*
|
||||
* Optionally integrates with RuVector server for cross-process pattern sharing.
|
||||
*/
|
||||
export class PersistentSONAEngine {
|
||||
export class PersistentSONAEngine {
|
||||
private readonly baseEngine: QESONA;
|
||||
private readonly config: Required<PersistentSONAConfig>;
|
||||
private persistence: UnifiedPersistenceManager | null = null;
|
||||
@@ -955,17 +968,7 @@ export class PersistentSONAEngine {
|
||||
/**
|
||||
* Get persisted statistics from SQLite
|
||||
*/
|
||||
async getPersistedStats(): Promise<{
|
||||
totalPatterns: number;
|
||||
uniqueTypes: number;
|
||||
uniqueDomains: number;
|
||||
avgConfidence: number;
|
||||
avgUsage: number;
|
||||
totalSuccesses: number;
|
||||
totalFailures: number;
|
||||
byType: Record<string, number>;
|
||||
byDomain: Record<string, number>;
|
||||
}> {
|
||||
async getPersistedStats(): Promise<PersistedSONAStats> {
|
||||
this.ensureInitialized();
|
||||
|
||||
const statsStmt = this.prepared.get('getStats');
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
OptimizationObjective as DomainObjective,
|
||||
Constraint,
|
||||
} from '../../../domains/learning-optimization/interfaces.js';
|
||||
import type { LearningOptimizationAPI } from '../../../domains/learning-optimization/plugin.js';
|
||||
import type { PersistedSONAStats } from '../../../integrations/ruvector/sona-persistence.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -107,6 +109,8 @@ export interface PatternResult {
|
||||
topPatterns: LearnedPattern[];
|
||||
avgConfidence: number;
|
||||
avgSuccessRate: number;
|
||||
/** Read-only persisted SONA visibility when an initialized fleet is available. */
|
||||
sona?: SONASnapshot;
|
||||
}
|
||||
|
||||
export interface DashboardResult {
|
||||
@@ -117,6 +121,13 @@ export interface DashboardResult {
|
||||
topPerformingDomains: DomainName[];
|
||||
learningTrend: TrendPoint[];
|
||||
recentMilestones: Milestone[];
|
||||
/** Read-only persisted SONA visibility when an initialized fleet is available. */
|
||||
sona?: SONASnapshot;
|
||||
}
|
||||
|
||||
export interface SONASnapshot {
|
||||
available: boolean;
|
||||
stats: PersistedSONAStats | null;
|
||||
}
|
||||
|
||||
export interface TrendPoint {
|
||||
@@ -522,6 +533,7 @@ export class LearningOptimizeTool extends MCPToolBase<LearningOptimizeParams, Le
|
||||
context: MCPToolContext
|
||||
): Promise<PatternResult> {
|
||||
const { learningCoordinator } = await this.getServices(context);
|
||||
const sona = await this.getSONASnapshot(context);
|
||||
|
||||
this.emitStream(context, {
|
||||
status: 'analyzing',
|
||||
@@ -540,6 +552,7 @@ export class LearningOptimizeTool extends MCPToolBase<LearningOptimizeParams, Le
|
||||
topPatterns: [],
|
||||
avgConfidence: 0,
|
||||
avgSuccessRate: 0,
|
||||
sona,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -563,11 +576,13 @@ export class LearningOptimizeTool extends MCPToolBase<LearningOptimizeParams, Le
|
||||
topPatterns,
|
||||
avgConfidence: stats.avgConfidence,
|
||||
avgSuccessRate: stats.avgSuccessRate,
|
||||
sona,
|
||||
};
|
||||
}
|
||||
|
||||
private async executeDashboard(context: MCPToolContext): Promise<DashboardResult> {
|
||||
const { learningCoordinator, transferSpecialist } = await this.getServices(context);
|
||||
const sona = await this.getSONASnapshot(context);
|
||||
|
||||
this.emitStream(context, {
|
||||
status: 'aggregating',
|
||||
@@ -665,8 +680,25 @@ export class LearningOptimizeTool extends MCPToolBase<LearningOptimizeParams, Le
|
||||
topPerformingDomains: topPerformingDomains.length > 0 ? topPerformingDomains : ['test-generation'],
|
||||
learningTrend,
|
||||
recentMilestones,
|
||||
sona,
|
||||
};
|
||||
}
|
||||
|
||||
/** Read SONA through the initialized domain API; never construct a second engine here. */
|
||||
private async getSONASnapshot(context: MCPToolContext): Promise<SONASnapshot> {
|
||||
const api = context.kernel?.getDomainAPI<LearningOptimizationAPI>('learning-optimization');
|
||||
if (!api?.isSONAAvailable()) return { available: false, stats: null };
|
||||
|
||||
try {
|
||||
return { available: true, stats: await api.getSONAPersistedStats() };
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to read persisted SONA statistics', {
|
||||
requestId: context.requestId,
|
||||
error: toErrorMessage(error),
|
||||
});
|
||||
return { available: false, stats: null };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
describe('packed package native ESM contracts', () => {
|
||||
const packageRoot = process.cwd();
|
||||
const tempRoot = path.join(packageRoot, 'node_modules', '.cache', `aqe-pack-esm-${process.pid}`);
|
||||
const unpackedRoot = path.join(tempRoot, 'package');
|
||||
let packageJson: {
|
||||
exports: Record<string, { import?: string }>;
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
// Arrange a real packed artifact under this checkout so its dependencies
|
||||
// resolve from the existing node_modules tree without network access.
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(tempRoot, { recursive: true });
|
||||
const packOutput = execFileSync(
|
||||
'npm',
|
||||
['pack', '--json', '--ignore-scripts', '--pack-destination', tempRoot],
|
||||
{ cwd: packageRoot, encoding: 'utf8' },
|
||||
);
|
||||
const [{ filename }] = JSON.parse(packOutput) as Array<{ filename: string }>;
|
||||
execFileSync('tar', ['-xzf', path.join(tempRoot, filename), '-C', tempRoot]);
|
||||
packageJson = JSON.parse(fs.readFileSync(path.join(unpackedRoot, 'package.json'), 'utf8'));
|
||||
}, 30_000);
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should_importEveryDeclaredExport_when_loadedByNativeNodeESM', () => {
|
||||
for (const [exportName, target] of Object.entries(packageJson.exports)) {
|
||||
if (!target.import) continue;
|
||||
expectNativeImport(target.import, `package export ${exportName}`, exportName === './cli');
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
it('should_importRuVectorBarrel_when_loadedByNativeNodeESM', () => {
|
||||
expectNativeImport('./dist/integrations/ruvector/index.js', 'RuVector barrel');
|
||||
});
|
||||
|
||||
it('should_importCoordinatorGNN_when_loadedByNativeNodeESM', () => {
|
||||
expectNativeImport(
|
||||
'./dist/domains/code-intelligence/coordinator-gnn.js',
|
||||
'code-intelligence coordinator GNN',
|
||||
);
|
||||
});
|
||||
|
||||
function expectNativeImport(
|
||||
relativeTarget: string,
|
||||
label: string,
|
||||
executableEntry = false,
|
||||
): void {
|
||||
const url = pathToFileURL(path.resolve(unpackedRoot, relativeTarget)).href;
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`await import(${JSON.stringify(url)});`,
|
||||
...(executableEntry ? ['--', '--version'] : []),
|
||||
],
|
||||
{ cwd: unpackedRoot, encoding: 'utf8', timeout: 30_000 },
|
||||
);
|
||||
expect(result.status, `${label}: ${result.stderr || result.stdout}`).toBe(0);
|
||||
}
|
||||
});
|
||||
@@ -32,6 +32,17 @@ vi.mock('../../../../src/integrations/ruvector/sona-persistence', () => ({
|
||||
createPattern: vi.fn(),
|
||||
adaptPattern: vi.fn().mockResolvedValue({ patterns: [], adapted: false }),
|
||||
getStats: vi.fn().mockReturnValue({ totalPatterns: 0, typeBreakdown: {}, domainBreakdown: {} }),
|
||||
getPersistedStats: vi.fn().mockResolvedValue({
|
||||
totalPatterns: 3,
|
||||
uniqueTypes: 2,
|
||||
uniqueDomains: 2,
|
||||
avgConfidence: 0.8,
|
||||
avgUsage: 4,
|
||||
totalSuccesses: 7,
|
||||
totalFailures: 1,
|
||||
byType: { successful: 2, optimization: 1 },
|
||||
byDomain: { 'test-generation': 2, 'quality-assessment': 1 },
|
||||
}),
|
||||
getAllPatterns: vi.fn().mockReturnValue([]),
|
||||
getPatternsByType: vi.fn().mockReturnValue([]),
|
||||
getPatternsByDomain: vi.fn().mockReturnValue([]),
|
||||
@@ -178,6 +189,19 @@ describe('LearningOptimizationPlugin', () => {
|
||||
expect(api).toHaveProperty('importModels');
|
||||
});
|
||||
|
||||
it('should_returnPersistedSONAStats_when_engineIsAvailable', async () => {
|
||||
// Arrange
|
||||
const api = plugin.getAPI<{
|
||||
getSONAPersistedStats: () => Promise<{ totalPatterns: number } | null>;
|
||||
}>();
|
||||
|
||||
// Act
|
||||
const stats = await api.getSONAPersistedStats();
|
||||
|
||||
// Assert
|
||||
expect(stats?.totalPatterns).toBe(3);
|
||||
});
|
||||
|
||||
it('should return API with pattern learning methods', () => {
|
||||
const api = plugin.getAPI<Record<string, unknown>>();
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { LearningOptimizeTool } from '../../../../src/mcp/tools/learning-optimization/optimize.js';
|
||||
import type { MemoryBackend, QEKernel } from '../../../../src/kernel/interfaces.js';
|
||||
|
||||
const PERSISTED_STATS = {
|
||||
totalPatterns: 5,
|
||||
uniqueTypes: 2,
|
||||
uniqueDomains: 2,
|
||||
avgConfidence: 0.82,
|
||||
avgUsage: 3.4,
|
||||
totalSuccesses: 9,
|
||||
totalFailures: 2,
|
||||
byType: { successful: 4, optimization: 1 },
|
||||
byDomain: { 'test-generation': 3, 'quality-assessment': 2 },
|
||||
};
|
||||
|
||||
describe('LearningOptimizeTool SONA visibility', () => {
|
||||
let tool: LearningOptimizeTool;
|
||||
let memory: MemoryBackend;
|
||||
let kernel: QEKernel;
|
||||
const getSONAPersistedStats = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
tool = new LearningOptimizeTool();
|
||||
getSONAPersistedStats.mockReset().mockResolvedValue(PERSISTED_STATS);
|
||||
memory = {
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
set: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
search: vi.fn().mockResolvedValue([]),
|
||||
list: vi.fn().mockResolvedValue([]),
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as MemoryBackend;
|
||||
kernel = {
|
||||
getDomainAPI: vi.fn().mockReturnValue({
|
||||
isSONAAvailable: () => true,
|
||||
getSONAPersistedStats,
|
||||
}),
|
||||
} as unknown as QEKernel;
|
||||
});
|
||||
|
||||
it('should_includeReadOnlySONAStats_when_patternsActionUsesInitializedKernel', async () => {
|
||||
// Act
|
||||
const result = await tool.invoke({ action: 'patterns' }, { memory, kernel });
|
||||
|
||||
// Assert
|
||||
expect(result.data?.patternResult?.sona).toEqual({ available: true, stats: PERSISTED_STATS });
|
||||
});
|
||||
|
||||
it('should_includeReadOnlySONAStats_when_dashboardActionUsesInitializedKernel', async () => {
|
||||
// Act
|
||||
const result = await tool.invoke({ action: 'dashboard' }, { memory, kernel });
|
||||
|
||||
// Assert
|
||||
expect(result.data?.dashboardResult?.sona).toEqual({ available: true, stats: PERSISTED_STATS });
|
||||
});
|
||||
|
||||
it('should_reportSONAUnavailable_when_fleetKernelIsAbsent', async () => {
|
||||
// Act
|
||||
const result = await tool.invoke({ action: 'patterns' }, { memory });
|
||||
|
||||
// Assert
|
||||
expect(result.data?.patternResult?.sona).toEqual({ available: false, stats: null });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { normalizeEmittedESM } from '../../../scripts/fix-esm-specifiers.mjs';
|
||||
|
||||
describe('normalizeEmittedESM', () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should_addExplicitTargets_when_emittedImportsAreExtensionless', () => {
|
||||
// Arrange
|
||||
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aqe-esm-specifiers-'));
|
||||
tempDirs.push(distDir);
|
||||
fs.mkdirSync(path.join(distDir, 'feature'));
|
||||
fs.writeFileSync(path.join(distDir, 'dep.js'), 'export const dep = true;\n');
|
||||
fs.writeFileSync(path.join(distDir, 'feature', 'index.js'), 'export const feature = true;\n');
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, 'index.js'),
|
||||
"export { dep } from './dep';\nexport { feature } from './feature';\n",
|
||||
);
|
||||
|
||||
// Act
|
||||
normalizeEmittedESM(distDir);
|
||||
|
||||
// Assert
|
||||
expect(fs.readFileSync(path.join(distDir, 'index.js'), 'utf8')).toBe(
|
||||
"export { dep } from './dep.js';\nexport { feature } from './feature/index.js';\n",
|
||||
);
|
||||
});
|
||||
|
||||
it('should_preserveSpecifier_when_itAlreadyHasAnExtension', () => {
|
||||
// Arrange
|
||||
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aqe-esm-specifiers-'));
|
||||
tempDirs.push(distDir);
|
||||
fs.writeFileSync(path.join(distDir, 'dep.js'), 'export const dep = true;\n');
|
||||
fs.writeFileSync(path.join(distDir, 'index.js'), "export { dep } from './dep.js';\n");
|
||||
|
||||
// Act
|
||||
normalizeEmittedESM(distDir);
|
||||
|
||||
// Assert
|
||||
expect(fs.readFileSync(path.join(distDir, 'index.js'), 'utf8')).toBe(
|
||||
"export { dep } from './dep.js';\n",
|
||||
);
|
||||
});
|
||||
|
||||
it('should_appendRuntimeExtension_when_sourceBasenameContainsDots', () => {
|
||||
// Arrange
|
||||
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aqe-esm-specifiers-'));
|
||||
tempDirs.push(distDir);
|
||||
fs.writeFileSync(path.join(distDir, 'e2e-step.types.js'), 'export const step = true;\n');
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, 'index.js'),
|
||||
"export { step } from './e2e-step.types';\n",
|
||||
);
|
||||
|
||||
// Act
|
||||
normalizeEmittedESM(distDir);
|
||||
|
||||
// Assert
|
||||
expect(fs.readFileSync(path.join(distDir, 'index.js'), 'utf8')).toBe(
|
||||
"export { step } from './e2e-step.types.js';\n",
|
||||
);
|
||||
});
|
||||
|
||||
it('should_rewriteTypeScriptPathAliases_toRelativeRuntimeTargets', () => {
|
||||
// Arrange
|
||||
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aqe-esm-specifiers-'));
|
||||
tempDirs.push(distDir);
|
||||
fs.mkdirSync(path.join(distDir, 'shared'));
|
||||
fs.mkdirSync(path.join(distDir, 'domains', 'feature'), { recursive: true });
|
||||
fs.writeFileSync(path.join(distDir, 'shared', 'error-utils.js'), 'export const ok = true;\n');
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, 'domains', 'feature', 'index.js'),
|
||||
"export { ok } from '@shared/error-utils.js';\n",
|
||||
);
|
||||
|
||||
// Act
|
||||
normalizeEmittedESM(distDir);
|
||||
|
||||
// Assert
|
||||
expect(fs.readFileSync(path.join(distDir, 'domains', 'feature', 'index.js'), 'utf8')).toBe(
|
||||
"export { ok } from '../../shared/error-utils.js';\n",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user