2026-08-17 13:49:11 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import test from "node:test";
|
|
|
|
|
|
2026-08-24 21:43:00 +00:00
|
|
|
import {
|
|
|
|
|
AsyncLocalStorageOperationContextStore,
|
|
|
|
|
type IModuleServer,
|
|
|
|
|
type ModuleParams,
|
|
|
|
|
SequelizeTransactionContext,
|
|
|
|
|
} from "@erp/core/api";
|
|
|
|
|
import { Audit } from "@repo/rdx-audit";
|
|
|
|
|
import { Result } from "@repo/rdx-utils";
|
2026-08-17 13:49:11 +00:00
|
|
|
|
|
|
|
|
import { resetModelsRegistry } from "./model-loader";
|
2026-08-31 18:51:27 +00:00
|
|
|
import { initModules, registerModule, resetModuleRegistry, startModules } from "./module-loader";
|
2026-08-17 13:49:11 +00:00
|
|
|
import { listServices, resetServiceRegistry } from "./service-registry";
|
|
|
|
|
|
|
|
|
|
type FakeDatabase = {
|
|
|
|
|
sync: () => Promise<void>;
|
|
|
|
|
models: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function buildModuleParams(): ModuleParams & {
|
|
|
|
|
config: { database: { syncMode: string } };
|
|
|
|
|
database: FakeDatabase;
|
|
|
|
|
logger: Console;
|
|
|
|
|
} {
|
|
|
|
|
return {
|
|
|
|
|
getService: () => {
|
|
|
|
|
throw new Error("Base params getService should not be called directly in tests.");
|
|
|
|
|
},
|
|
|
|
|
config: {
|
|
|
|
|
database: {
|
|
|
|
|
syncMode: "none",
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-08-31 18:51:27 +00:00
|
|
|
warmup: { timeoutMs: 10_000, strict: false },
|
|
|
|
|
databaseSyncMode: "none",
|
2026-08-17 13:49:11 +00:00
|
|
|
database: {
|
|
|
|
|
sync: async () => undefined,
|
|
|
|
|
models: {},
|
|
|
|
|
},
|
|
|
|
|
logger: console,
|
2026-08-24 21:43:00 +00:00
|
|
|
operationContext: new AsyncLocalStorageOperationContextStore(),
|
|
|
|
|
transactionContext: new SequelizeTransactionContext(),
|
|
|
|
|
audit: new Audit({
|
|
|
|
|
source: { type: "test", id: "module-loader" },
|
|
|
|
|
contextProvider: { get: () => null },
|
|
|
|
|
store: { append: async () => Result.ok() },
|
|
|
|
|
}),
|
2026-08-17 13:49:11 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetModuleTestState(): void {
|
|
|
|
|
resetServiceRegistry();
|
|
|
|
|
resetModuleRegistry();
|
|
|
|
|
resetModelsRegistry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("module-loader permite dependencias externas declaradas", async (t) => {
|
|
|
|
|
resetModuleTestState();
|
|
|
|
|
t.after(resetModuleTestState);
|
|
|
|
|
|
|
|
|
|
let receivedService = "";
|
|
|
|
|
|
|
|
|
|
const catalogsModule: IModuleServer = {
|
|
|
|
|
name: "catalogs",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
setup: () => ({
|
|
|
|
|
services: {
|
|
|
|
|
general: {
|
|
|
|
|
name: "catalogs-service",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const companiesModule: IModuleServer = {
|
|
|
|
|
name: "companies",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
dependencies: ["catalogs"],
|
|
|
|
|
start: ({ getService }) => {
|
|
|
|
|
const service = getService<{ name: string }>("catalogs:general");
|
|
|
|
|
receivedService = service.name;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
registerModule(catalogsModule);
|
|
|
|
|
registerModule(companiesModule);
|
|
|
|
|
|
|
|
|
|
await initModules(buildModuleParams());
|
2026-08-31 18:51:27 +00:00
|
|
|
await startModules(buildModuleParams());
|
2026-08-17 13:49:11 +00:00
|
|
|
|
|
|
|
|
assert.equal(receivedService, "catalogs-service");
|
|
|
|
|
assert.deepEqual(listServices().sort(), ["catalogs:general"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("module-loader falla cuando una dependencia usada no está declarada", async (t) => {
|
|
|
|
|
resetModuleTestState();
|
|
|
|
|
t.after(resetModuleTestState);
|
|
|
|
|
|
|
|
|
|
registerModule({
|
|
|
|
|
name: "catalogs",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
setup: () => ({
|
|
|
|
|
services: {
|
|
|
|
|
general: {
|
|
|
|
|
name: "catalogs-service",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
registerModule({
|
|
|
|
|
name: "companies",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
start: ({ getService }) => {
|
|
|
|
|
getService("catalogs:general");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-10 11:26:33 +00:00
|
|
|
await assert.rejects(async () => {
|
|
|
|
|
await initModules(buildModuleParams());
|
|
|
|
|
await startModules(buildModuleParams());
|
|
|
|
|
}, /without declaring dependency on "catalogs"/i);
|
2026-08-17 13:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("module-loader sigue detectando ciclos reales entre módulos", async (t) => {
|
|
|
|
|
resetModuleTestState();
|
|
|
|
|
t.after(resetModuleTestState);
|
|
|
|
|
|
|
|
|
|
registerModule({
|
|
|
|
|
name: "companies",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
dependencies: ["catalogs"],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
registerModule({
|
|
|
|
|
name: "catalogs",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
dependencies: ["companies"],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await assert.rejects(() => initModules(buildModuleParams()), /cyclic dependency detected/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("module-loader rechaza la autoreferencia explícita por nombre de módulo", async (t) => {
|
|
|
|
|
resetModuleTestState();
|
|
|
|
|
t.after(resetModuleTestState);
|
|
|
|
|
|
|
|
|
|
registerModule({
|
|
|
|
|
name: "companies",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
setup: () => ({
|
|
|
|
|
services: {
|
|
|
|
|
general: {
|
|
|
|
|
name: "companies-service",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
start: ({ getService }) => {
|
|
|
|
|
getService("companies:general");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-10 11:26:33 +00:00
|
|
|
await assert.rejects(async () => {
|
|
|
|
|
await initModules(buildModuleParams());
|
|
|
|
|
await startModules(buildModuleParams());
|
|
|
|
|
}, /without declaring dependency on "companies"/i);
|
2026-08-17 13:49:11 +00:00
|
|
|
});
|