Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
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([
"@hono/zod-openapi",
"@opentelemetry/api",
"@suaveplan/api-plugin",
"@suaveplan/api-schemas",
"@suaveplan/error",
"@suaveplan/types",
"hono",
"tslib",
"yaml",
"zod",
]);
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",
"registry",
"route-factory",
"schema-registry",
"merger",
"plugin-integration",
"emitter",
"namespacing",
"strictness",
"endpoints",
];
describe("dep-graph", () => {
it("dependencies match the 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);
}
});
});

View File

@@ -0,0 +1,136 @@
import { OpenAPIHono, z } from "@hono/zod-openapi";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { definePluginRoutes } from "../../src/plugin-integration/plugin-integration.js";
import {
createOpenAPIRegistry,
DEFAULT_SPEC_PATH,
} from "../../src/registry/registry.js";
import {
GET,
POST,
successResponse,
} from "../../src/route-factory/route-factory.js";
describe("full doc roundtrip — real OpenAPIHono + real registry + 3 plugins", () => {
it("registers across 3 plugins and the served doc matches buildDocument()", {
timeout: 30000,
}, async () => {
const app = new OpenAPIHono();
const registry = createOpenAPIRegistry({
info: { title: "All-in-One", version: "1.0.0" },
});
// Plugin: users
const listUsers = GET("/users")
.tags("users")
.operationId("listUsers")
.response(successResponse(z.array(z.object({ id: z.string() }))))
.build();
const createUser = POST("/users")
.tags("users")
.operationId("createUser")
.body(z.object({ email: z.string().email() }))
.response(
successResponse(z.object({ id: z.string(), email: z.string() }))
)
.build();
definePluginRoutes(
[
{
route: listUsers,
handler: (c) =>
c.json({ success: true as const, data: [] }, 200),
},
{
route: createUser,
handler: (c) =>
c.json(
{
success: true as const,
data: { id: "1", email: "u@e.com" },
},
200
),
},
],
{ app, registry, pluginName: "users" }
);
// Plugin: projects
const listProjects = GET("/projects")
.tags("projects")
.operationId("listProjects")
.response(successResponse(z.array(z.object({ id: z.string() }))))
.build();
definePluginRoutes(
[
{
route: listProjects,
handler: (c) =>
c.json({ success: true as const, data: [] }, 200),
},
],
{ app, registry, pluginName: "projects" }
);
// Plugin: health
const health = GET("/health")
.tags("ops")
.operationId("getHealth")
.response(successResponse(z.object({ ok: z.boolean() })))
.build();
definePluginRoutes(
[
{
route: health,
handler: (c) =>
c.json(
{
success: true as const,
data: { ok: true },
},
200
),
},
],
{ app, registry, pluginName: "health" }
);
// Connect the registry to the app
registry.applyTo(app);
// Assert registry-built doc contains every route
const built = registry.buildDocument();
expect(Object.keys(built.paths).sort()).toEqual([
"/health",
"/projects",
"/users",
]);
expect(built.paths["/users"]?.get?.operationId).toBe("listUsers");
expect(built.paths["/users"]?.post?.operationId).toBe("createUser");
// Tag set carries through
expect(built.tags?.map((t) => t.name).sort()).toEqual([
"ops",
"projects",
"users",
]);
// Plugin-name introspection
expect(
registry
.getRoutesByPlugin("users")
.map((r) => r.path)
.sort()
).toEqual(["/users", "/users"]);
expect(
registry.getRoutesByPlugin("projects").map((r) => r.path)
).toEqual(["/projects"]);
// Served doc must equal buildDocument() byte-for-byte
const res = await app.request(DEFAULT_SPEC_PATH);
expect(res.status).toBe(200);
const served = (await res.json()) as unknown;
expect(served).toEqual(built);
});
});

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { mergeOpenAPIDocuments } from "../src/merger/merger.js";
import type { OpenAPIDocument } from "../src/types/openapi.js";
const operationArb = fc.record({
responses: fc.constant({}),
});
const pathItemArb = fc.dictionary(
fc.constantFrom("get", "post", "put", "patch", "delete"),
operationArb,
{ minKeys: 1, maxKeys: 3 }
);
const pathsArb = fc.dictionary(
fc.string({ minLength: 1, maxLength: 10 }).map((s) => `/${s}`),
pathItemArb,
{ minKeys: 0, maxKeys: 5 }
);
const docArb = pathsArb.map<OpenAPIDocument>((paths) => ({
openapi: "3.1.0",
info: { title: "T", version: "1.0.0" },
paths,
}));
describe("mergeOpenAPIDocuments — associativity property", () => {
it("merge(a, b, c) === merge(merge(a, b), c) for arbitrary path sets", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(docArb, docArb, docArb, (a, b, c) => {
const left = mergeOpenAPIDocuments(a, b, c);
const right = mergeOpenAPIDocuments(
mergeOpenAPIDocuments(a, b),
c
);
expect(right.paths).toEqual(left.paths);
}),
{ numRuns: 200 }
);
});
it("merging with empty extras is idempotent on paths", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(docArb, (a) => {
const m = mergeOpenAPIDocuments(a, {}, {}, {});
expect(m.paths).toEqual(a.paths);
}),
{ numRuns: 100 }
);
});
});

View File

@@ -0,0 +1,34 @@
/**
* Test-environment setup for @suaveplan/api-openapi.
*
* Wires a real in-memory OpenTelemetry `MeterProvider` BEFORE any source
* module is imported. Tests that inspect emitted metrics can read them
* from `testInMemoryMetricExporter`. Node ≥18 exposes `crypto.subtle`
* and `crypto.getRandomValues` globally, so no crypto polyfill needed.
*/
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;
}