Initial Commit
This commit is contained in:
119
packages/api-stack/api/api-plugin/tests/dep-graph.test.ts
Normal file
119
packages/api-stack/api/api-plugin/tests/dep-graph.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
const PKG_ROOT = new URL("..", import.meta.url).pathname;
|
||||
const PKG_JSON = JSON.parse(
|
||||
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
|
||||
) as {
|
||||
dependencies: Record<string, string>;
|
||||
exports: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const RUNTIME_ALLOWLIST = new Set([
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/events",
|
||||
"@suaveplan/types",
|
||||
"tslib",
|
||||
"zod",
|
||||
"@opentelemetry/api",
|
||||
]);
|
||||
|
||||
const SRC_ROOT = join(PKG_ROOT, "src");
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = join(dir, entry);
|
||||
const st = statSync(p);
|
||||
if (st.isDirectory()) out.push(...walk(p));
|
||||
else out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const FEATURE_MODULES = [
|
||||
"error-codes",
|
||||
"types",
|
||||
"topo-sort",
|
||||
"service-registry",
|
||||
"context",
|
||||
"hook-executor",
|
||||
"define",
|
||||
"registry",
|
||||
];
|
||||
|
||||
describe("dep-graph", () => {
|
||||
it("dependencies match the universal allowlist", { timeout: 5000 }, () => {
|
||||
for (const dep of Object.keys(PKG_JSON.dependencies)) {
|
||||
expect(RUNTIME_ALLOWLIST.has(dep)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("subpath exports cover every feature module", { timeout: 5000 }, () => {
|
||||
const exports = PKG_JSON.exports;
|
||||
for (const m of FEATURE_MODULES) {
|
||||
expect(exports[`./${m}`]).toBeDefined();
|
||||
}
|
||||
expect(exports["."]).toBeDefined();
|
||||
});
|
||||
|
||||
it("every implementation .ts has a sibling .md", { timeout: 5000 }, () => {
|
||||
const files = walk(SRC_ROOT).filter(
|
||||
(f) =>
|
||||
f.endsWith(".ts") &&
|
||||
!f.endsWith(".test.ts") &&
|
||||
!f.endsWith(".types.ts") &&
|
||||
!f.endsWith(".bench.ts") &&
|
||||
!f.endsWith(".spec-conformance.test.ts") &&
|
||||
!/[/\\]index\.ts$/.test(f) &&
|
||||
!/[/\\]src[/\\]index\.ts$/.test(f)
|
||||
);
|
||||
for (const f of files) {
|
||||
const md = f.replace(/\.ts$/, ".md");
|
||||
const has = (() => {
|
||||
try {
|
||||
statSync(md);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
expect({ file: f, hasMd: has }).toEqual({
|
||||
file: f,
|
||||
hasMd: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("no biome-ignore suppressions in src", { timeout: 5000 }, () => {
|
||||
const files = walk(SRC_ROOT).filter((f) => f.endsWith(".ts"));
|
||||
for (const f of files) {
|
||||
const text = readFileSync(f, "utf8");
|
||||
expect(text.includes("biome-ignore")).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("no direct vitest/bun:test imports", { timeout: 5000 }, () => {
|
||||
const files = walk(SRC_ROOT)
|
||||
.concat(walk(join(PKG_ROOT, "tests")))
|
||||
.filter((f) => f.endsWith(".ts"));
|
||||
const banned =
|
||||
/from\s+['"](vitest|bun:test)['"]|require\(\s*['"](vitest|bun:test)['"]\s*\)/;
|
||||
for (const f of files) {
|
||||
const text = readFileSync(f, "utf8");
|
||||
if (banned.test(text)) {
|
||||
throw new Error(`${f} imports vitest or bun:test directly`);
|
||||
}
|
||||
}
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("no `export *` in feature barrels", { timeout: 5000 }, () => {
|
||||
for (const m of FEATURE_MODULES) {
|
||||
const idx = join(SRC_ROOT, m, "index.ts");
|
||||
const text = readFileSync(idx, "utf8");
|
||||
expect(text.includes("export *")).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
67
packages/api-stack/api/api-plugin/tests/property.test.ts
Normal file
67
packages/api-stack/api/api-plugin/tests/property.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import fc from "fast-check";
|
||||
import { topologicalSort } from "../src/topo-sort/topo-sort.js";
|
||||
import type { PluginNode } from "../src/types/api-plugin.js";
|
||||
|
||||
/** Generate an acyclic dep graph: each node's deps only point to earlier ids. */
|
||||
const acyclicGraphArb = fc
|
||||
.array(fc.string({ minLength: 1, maxLength: 4 }), {
|
||||
minLength: 1,
|
||||
maxLength: 20,
|
||||
})
|
||||
.map((rawIds) => {
|
||||
const ids = Array.from(new Set(rawIds));
|
||||
return ids;
|
||||
})
|
||||
.filter((ids) => ids.length > 0)
|
||||
.chain((ids) =>
|
||||
fc
|
||||
.tuple(
|
||||
...ids.map((_, i) =>
|
||||
fc.subarray(ids.slice(0, i), { maxLength: i })
|
||||
)
|
||||
)
|
||||
.map((depsPerNode) =>
|
||||
ids.map<PluginNode<null>>((id, i) => ({
|
||||
id,
|
||||
data: null,
|
||||
dependencies: depsPerNode[i] ?? [],
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
describe("topologicalSort — property tests", () => {
|
||||
it("sorted order satisfies dep invariant", { timeout: 30000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(acyclicGraphArb, (nodes) => {
|
||||
const { sorted } = topologicalSort(nodes);
|
||||
const positions = new Map<string, number>();
|
||||
sorted.forEach((n, i) => positions.set(n.id, i));
|
||||
for (const node of sorted) {
|
||||
for (const dep of node.dependencies) {
|
||||
const depPos = positions.get(dep);
|
||||
const nodePos = positions.get(node.id);
|
||||
// dep must appear before node (if dep is in the graph)
|
||||
if (depPos !== undefined && nodePos !== undefined) {
|
||||
expect(depPos).toBeLessThan(nodePos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
|
||||
it("sort is deterministic across repeated calls", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(acyclicGraphArb, (nodes) => {
|
||||
const a = topologicalSort(nodes).sorted.map((n) => n.id);
|
||||
const b = topologicalSort(nodes).sorted.map((n) => n.id);
|
||||
expect(a).toEqual(b);
|
||||
}),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
});
|
||||
39
packages/api-stack/api/api-plugin/tests/setup/polyfills.ts
Normal file
39
packages/api-stack/api/api-plugin/tests/setup/polyfills.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Test-environment setup for @suaveplan/api-plugin.
|
||||
*
|
||||
* Wires a real in-memory OpenTelemetry `MeterProvider` BEFORE any
|
||||
* source module is imported, so the meter resolved at module load
|
||||
* is the real provider's meter — not the noop fallback. Tests that
|
||||
* inspect emitted metrics (registry spans/counters) can read them
|
||||
* out of `testInMemoryMetricExporter`.
|
||||
*
|
||||
* Node ≥18 already exposes `crypto.subtle` and `crypto.getRandomValues`
|
||||
* globally, so no crypto polyfill is required. Universal: no
|
||||
* `node:fs`, no `process.cwd()`.
|
||||
*/
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import {
|
||||
AggregationTemporality,
|
||||
InMemoryMetricExporter,
|
||||
MeterProvider,
|
||||
PeriodicExportingMetricReader,
|
||||
} from "@opentelemetry/sdk-metrics";
|
||||
|
||||
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
|
||||
AggregationTemporality.CUMULATIVE
|
||||
);
|
||||
|
||||
const reader = new PeriodicExportingMetricReader({
|
||||
exporter: testInMemoryMetricExporter,
|
||||
exportIntervalMillis: 60_000,
|
||||
});
|
||||
|
||||
export const testMeterProvider = new MeterProvider({ readers: [reader] });
|
||||
metrics.setGlobalMeterProvider(testMeterProvider);
|
||||
|
||||
export async function flushTestMetrics() {
|
||||
await testMeterProvider.forceFlush();
|
||||
const collected = testInMemoryMetricExporter.getMetrics();
|
||||
testInMemoryMetricExporter.reset();
|
||||
return collected;
|
||||
}
|
||||
Reference in New Issue
Block a user