Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const ALLOWED_RUNTIME_DEPS = new Set([
"@opentelemetry/api",
"@suaveplan/error",
"@suaveplan/events",
"@suaveplan/opentelemetry",
"@suaveplan/types",
"tslib",
"zod",
]);
describe("dependency graph", () => {
it("runtime dependencies are restricted to the declared allowlist", {
timeout: 5000,
}, async () => {
const pkg = JSON.parse(
await readFile(join(__dirname, "..", "package.json"), "utf8")
) as { dependencies: Record<string, string> };
const declared = new Set(Object.keys(pkg.dependencies));
const extras = [...declared].filter(
(d) => !ALLOWED_RUNTIME_DEPS.has(d)
);
expect(extras).toEqual([]);
});
});

View File

@@ -0,0 +1,163 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { BitSet } from "../src/bitset/bitset.js";
import { defineQuery } from "../src/query/query.js";
import { defineComponent } from "../src/schema/schema.js";
import { createWorld } from "../src/world/world.js";
const MAX_BITS = 128;
const MAX_ENTITIES = 32;
function bitsetFromBits(size: number, bits: ReadonlyArray<number>): BitSet {
const set = new BitSet(size);
for (const b of bits) set.set(b);
return set;
}
function bruteContainsAll(a: BitSet, b: BitSet, size: number): boolean {
for (let i = 0; i < size; i++) {
if (b.has(i) && !a.has(i)) return false;
}
return true;
}
function bruteIntersects(a: BitSet, b: BitSet, size: number): boolean {
for (let i = 0; i < size; i++) {
if (a.has(i) && b.has(i)) return true;
}
return false;
}
describe("property invariants", () => {
it("bitset algebra matches brute-force on random masks", {
timeout: 10000,
}, () => {
fc.assert(
fc.property(
fc.integer({ min: 1, max: MAX_BITS }),
fc.array(fc.nat(MAX_BITS - 1), { maxLength: 16 }),
fc.array(fc.nat(MAX_BITS - 1), { maxLength: 16 }),
(rawSize, aBits, bBits) => {
const size = Math.max(rawSize, ...aBits, ...bBits, 0) + 1;
const a = bitsetFromBits(size, aBits);
const b = bitsetFromBits(size, bBits);
expect(a.containsAll(b)).toBe(bruteContainsAll(a, b, size));
expect(a.intersects(b)).toBe(b.intersects(a));
expect(a.intersects(b)).toBe(bruteIntersects(a, b, size));
}
),
{ numRuns: 100 }
);
});
it("query result equals brute-force scan on random worlds", {
timeout: 30000,
}, () => {
const Position = defineComponent("Position", { x: 0 });
const Velocity = defineComponent("Velocity", { dx: 0 });
const Disabled = defineComponent("Disabled", { d: true });
const schemas = [Position, Velocity, Disabled] as const;
fc.assert(
fc.property(
fc.array(fc.tuple(fc.boolean(), fc.boolean(), fc.boolean()), {
maxLength: MAX_ENTITIES,
}),
fc.subarray([Position, Velocity, Disabled], {
minLength: 1,
maxLength: 3,
}),
fc.subarray([Position, Velocity, Disabled], {
minLength: 0,
maxLength: 3,
}),
(entitySpec, include, exclude) => {
const world = createWorld();
for (const s of schemas) world.registerComponent(s);
const entities: number[] = [];
for (const flags of entitySpec) {
const entity = world.spawn();
entities.push(entity);
for (let i = 0; i < schemas.length; i++) {
if (flags[i] === true) {
world.addComponent(entity, schemas[i]!);
}
}
}
const descriptor = defineQuery(
include,
exclude.length > 0 ? exclude : undefined
);
const queried = [...world.query(descriptor)].sort(
(a, b) => a - b
);
const expected = entities
.filter((e) => {
for (const s of include) {
if (!world.hasComponent(e, s)) return false;
}
for (const s of exclude) {
if (world.hasComponent(e, s)) return false;
}
return true;
})
.sort((a, b) => a - b);
expect(queried).toEqual(expected);
}
),
{ numRuns: 50 }
);
});
it("entity count tracks spawn / despawn monotonically", {
timeout: 10000,
}, () => {
fc.assert(
fc.property(
fc.array(fc.boolean(), { maxLength: MAX_ENTITIES }),
(ops) => {
const world = createWorld();
const live: number[] = [];
for (const op of ops) {
if (op || live.length === 0) {
live.push(world.spawn());
} else {
const last = live.pop();
if (last !== undefined) world.despawn(last);
}
}
expect(world.entityCount).toBe(live.length);
}
),
{ numRuns: 100 }
);
});
it("snapshot then restore preserves observable state", {
timeout: 15000,
}, () => {
const Position = defineComponent("Position", { x: 0, y: 0 });
fc.assert(
fc.property(
fc.array(fc.record({ x: fc.integer(), y: fc.integer() }), {
maxLength: MAX_ENTITIES,
}),
(positions) => {
const a = createWorld();
a.registerComponent(Position);
for (const p of positions) {
const entity = a.spawn();
a.addComponent(entity, Position, p);
}
const snap = a.snapshot();
const b = createWorld();
b.registerComponent(Position);
b.restore(snap);
expect(b.entityCount).toBe(a.entityCount);
expect(b.archetypeCount).toBe(a.archetypeCount);
}
),
{ numRuns: 30 }
);
});
});

View File

@@ -0,0 +1,52 @@
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const SRC_ROOT = join(__dirname, "..", "src");
const BANNED_PATTERNS = [
/\bfrom\s+["']node:/,
/\bfrom\s+["']fs["']/,
/\bfrom\s+["']path["']/,
/\bfrom\s+["']crypto["']/,
/\brequire\(["']node:/,
/\bprocess\.[a-zA-Z]/,
/\bBuffer\.[a-zA-Z]/,
/\bwindow\./,
/\bdocument\./,
/\bnavigator\./,
];
async function* walk(dir: string): AsyncGenerator<string> {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
yield* walk(full);
} else if (
entry.isFile() &&
entry.name.endsWith(".ts") &&
!entry.name.endsWith(".test.ts") &&
!entry.name.endsWith(".spec.ts") &&
!entry.name.endsWith(".bench.ts")
) {
yield full;
}
}
}
describe("purity guards", () => {
it("built dist references no Node or browser globals", {
timeout: 5000,
}, async () => {
const offenders: Array<{ file: string; pattern: string }> = [];
for await (const file of walk(SRC_ROOT)) {
const source = await readFile(file, "utf8");
for (const pattern of BANNED_PATTERNS) {
if (pattern.test(source)) {
offenders.push({ file, pattern: pattern.source });
}
}
}
expect(offenders).toEqual([]);
});
});

View File

@@ -0,0 +1,40 @@
/**
* Test setup for @suaveplan/ecs.
*
* Installs a real in-memory OpenTelemetry MeterProvider BEFORE any source
* module imports so the meter resolved on first call binds to it. Also
* provides a `webcrypto`-backed `globalThis.crypto` for bare-Node runs.
*/
import { webcrypto } from "node:crypto";
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
type WritableGlobal = { crypto?: Crypto };
const g = globalThis as WritableGlobal;
if (g.crypto === undefined) {
g.crypto = webcrypto as unknown as Crypto;
}
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;
}