Initial Commit
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
const PKG_ROOT = path.resolve(__dirname, "..");
|
||||
const SRC_ROOT = path.join(PKG_ROOT, "src");
|
||||
|
||||
async function walk(dir: string): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
out.push(...(await walk(p)));
|
||||
} else {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isExempt(filePath: string): boolean {
|
||||
const base = path.basename(filePath);
|
||||
return (
|
||||
base.endsWith(".test.ts") ||
|
||||
base.endsWith(".types.ts") ||
|
||||
base === "index.ts"
|
||||
);
|
||||
}
|
||||
|
||||
describe("dep-graph: package shape", () => {
|
||||
it("every implementation .ts has a sibling .md", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const files = await walk(SRC_ROOT);
|
||||
const tsFiles = files.filter((f) => f.endsWith(".ts") && !isExempt(f));
|
||||
const missing: string[] = [];
|
||||
for (const ts of tsFiles) {
|
||||
const md = ts.replace(/\.ts$/, ".md");
|
||||
try {
|
||||
await fs.stat(md);
|
||||
} catch {
|
||||
missing.push(path.relative(PKG_ROOT, ts));
|
||||
}
|
||||
}
|
||||
expect(
|
||||
missing,
|
||||
`missing sibling .md for: ${missing.join(", ")}`
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("no source file uses `export *` (feature-barrel rule)", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const files = await walk(SRC_ROOT);
|
||||
const tsFiles = files.filter((f) => f.endsWith(".ts"));
|
||||
const offending: string[] = [];
|
||||
for (const f of tsFiles) {
|
||||
const content = await fs.readFile(f, "utf-8");
|
||||
if (/^\s*export\s*\*/m.test(content)) {
|
||||
offending.push(path.relative(PKG_ROOT, f));
|
||||
}
|
||||
}
|
||||
expect(offending).toEqual([]);
|
||||
});
|
||||
|
||||
it("no test file imports runner primitives directly from vitest or bun:test", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const files = await walk(SRC_ROOT);
|
||||
const tests = files.filter((f) => f.endsWith(".test.ts"));
|
||||
const offending: string[] = [];
|
||||
for (const f of tests) {
|
||||
const content = await fs.readFile(f, "utf-8");
|
||||
if (/from\s+["'](?:vitest|bun:test)["']/.test(content)) {
|
||||
offending.push(path.relative(PKG_ROOT, f));
|
||||
}
|
||||
}
|
||||
expect(offending).toEqual([]);
|
||||
});
|
||||
|
||||
it("no source file contains banned patterns", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const files = await walk(SRC_ROOT);
|
||||
const tsFiles = files.filter(
|
||||
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts")
|
||||
);
|
||||
const banned = [
|
||||
"@deprecated",
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"ts-ignore",
|
||||
"ts-expect-error",
|
||||
".skip",
|
||||
".only",
|
||||
"console.log",
|
||||
"biome-ignore",
|
||||
];
|
||||
const hits: string[] = [];
|
||||
for (const f of tsFiles) {
|
||||
const content = await fs.readFile(f, "utf-8");
|
||||
for (const b of banned) {
|
||||
if (content.includes(b)) {
|
||||
hits.push(`${path.relative(PKG_ROOT, f)}: ${b}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(hits).toEqual([]);
|
||||
});
|
||||
|
||||
it("package.json declares subpath exports for every command + prompt + run-function + otel + error-codes module", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const pkg = JSON.parse(
|
||||
await fs.readFile(path.join(PKG_ROOT, "package.json"), "utf-8")
|
||||
) as { exports: Record<string, unknown> };
|
||||
const required = [
|
||||
".",
|
||||
"./error-codes/error-codes",
|
||||
"./otel/instruments",
|
||||
"./run-function",
|
||||
"./prompt/prompt",
|
||||
"./commands/keys/keys",
|
||||
"./commands/files/files",
|
||||
"./commands/files/serve-file",
|
||||
"./commands/license/license",
|
||||
];
|
||||
for (const r of required) {
|
||||
expect(
|
||||
pkg.exports[r],
|
||||
`missing subpath export: ${r}`
|
||||
).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("package.json declares all 18 bin entries", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const pkg = JSON.parse(
|
||||
await fs.readFile(path.join(PKG_ROOT, "package.json"), "utf-8")
|
||||
) as { bin: Record<string, string> };
|
||||
const required = [
|
||||
"suaveplan",
|
||||
"license",
|
||||
"create-root-keys",
|
||||
"create-server-keys",
|
||||
"roll-server-keys",
|
||||
"create-root-keys-chain",
|
||||
"create-server-keys-chain",
|
||||
"roll-server-keys-chain",
|
||||
"verify-key-chain",
|
||||
"view-keys",
|
||||
"view-vault",
|
||||
"view-public-key-file",
|
||||
"view-chain-public",
|
||||
"prepare-file",
|
||||
"serve-file",
|
||||
"validate-file",
|
||||
"get-file-stats",
|
||||
"validate-packet",
|
||||
];
|
||||
for (const r of required) {
|
||||
expect(pkg.bin[r], `missing bin: ${r}`).toBeDefined();
|
||||
}
|
||||
expect(Object.keys(pkg.bin)).toHaveLength(18);
|
||||
// Each declared bin must have a matching .ts file in src/bin.
|
||||
for (const r of required) {
|
||||
const expected = path.join(
|
||||
SRC_ROOT,
|
||||
"bin",
|
||||
r === "suaveplan" ? "index.ts" : `${r}.ts`
|
||||
);
|
||||
await expect(fs.stat(expected)).resolves.toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import {
|
||||
prepareFileCommand,
|
||||
validateFileCommand,
|
||||
} from "../../src/commands/files/files.js";
|
||||
import { serveFileCommand } from "../../src/commands/files/serve-file.js";
|
||||
import { createRootKeysCommand } from "../../src/commands/keys/keys.js";
|
||||
|
||||
const PW = "intpw1234";
|
||||
|
||||
function tmp(label: string): string {
|
||||
return path.join(
|
||||
tmpdir(),
|
||||
`sk-int-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
);
|
||||
}
|
||||
|
||||
describe("integration: prepare → validate → serve round-trip", () => {
|
||||
let dir = "";
|
||||
let input = "";
|
||||
let originalBytes = new Uint8Array();
|
||||
const preparedDir = (): string => path.join(dir, "prepared");
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = tmp("rt");
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await createRootKeysCommand(PW, { output: dir });
|
||||
input = path.join(dir, "data.bin");
|
||||
// 1 MiB + 1 byte to exercise multi-packet and an odd tail packet.
|
||||
const size = 1_048_577;
|
||||
originalBytes = new Uint8Array(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
originalBytes[i] = (i * 31 + 7) & 0xff;
|
||||
}
|
||||
await fs.writeFile(input, originalBytes);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("prepares + validates + serves a binary file byte-identical to the input", {
|
||||
timeout: 120000,
|
||||
}, async () => {
|
||||
const manifest = await prepareFileCommand(
|
||||
input,
|
||||
path.join(dir, "root.vault"),
|
||||
PW,
|
||||
{ output: preparedDir(), packetSize: 256 * 1024 }
|
||||
);
|
||||
expect(manifest.totalPackets).toBe(5);
|
||||
expect(manifest.originalSize).toBe(originalBytes.byteLength);
|
||||
|
||||
const validation = await validateFileCommand(preparedDir());
|
||||
expect(validation.isComplete).toBe(true);
|
||||
expect(validation.validPackets).toBe(manifest.totalPackets);
|
||||
expect(validation.invalidPackets).toEqual([]);
|
||||
|
||||
const reassembledPath = path.join(dir, "out.bin");
|
||||
const result = await serveFileCommand(preparedDir(), {
|
||||
output: reassembledPath,
|
||||
});
|
||||
expect(result.bytesWritten).toBe(originalBytes.byteLength);
|
||||
const reassembled = await fs.readFile(reassembledPath);
|
||||
expect(reassembled.byteLength).toBe(originalBytes.byteLength);
|
||||
// Byte-identical comparison.
|
||||
for (let i = 0; i < originalBytes.byteLength; i++) {
|
||||
if (reassembled[i] !== originalBytes[i]) {
|
||||
throw new Error(
|
||||
`byte mismatch at offset ${i}: expected ${originalBytes[i]} got ${reassembled[i]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { ed25519 } from "@suaveplan/crypto-curve";
|
||||
import { type ChainLevel, TlvChainCodec } from "@suaveplan/crypto-keys";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import fc from "fast-check";
|
||||
import { prepareFileCommand } from "../src/commands/files/files.js";
|
||||
import { serveFileCommand } from "../src/commands/files/serve-file.js";
|
||||
import { createRootKeysCommand } from "../src/commands/keys/keys.js";
|
||||
|
||||
function arbBytes(min: number, max: number): fc.Arbitrary<Uint8Array> {
|
||||
return fc
|
||||
.array(fc.integer({ min: 0, max: 255 }), {
|
||||
minLength: min,
|
||||
maxLength: max,
|
||||
})
|
||||
.map((arr) => Uint8Array.from(arr));
|
||||
}
|
||||
|
||||
describe("property: TLV chain codec round-trip", () => {
|
||||
const codec = new TlvChainCodec();
|
||||
|
||||
// Build a syntactically-valid ChainLevel by deriving an ed25519
|
||||
// signerPublicKey from a random seed; signature/payload are arbitrary
|
||||
// byte arrays — the codec does not validate the cryptographic
|
||||
// relationship, only the byte layout.
|
||||
function arbChainLevel(): fc.Arbitrary<ChainLevel> {
|
||||
return fc
|
||||
.record({
|
||||
seed: arbBytes(32, 32),
|
||||
payload: arbBytes(0, 256),
|
||||
signature: arbBytes(0, 64),
|
||||
})
|
||||
.map(({ seed, payload, signature }) => {
|
||||
const signerPublicKey = ed25519.getPublicKey(seed);
|
||||
return {
|
||||
algorithm: "ed25519",
|
||||
signerPublicKey,
|
||||
payload,
|
||||
signature,
|
||||
} satisfies ChainLevel;
|
||||
});
|
||||
}
|
||||
|
||||
it("decode(encode(levels)) is byte-equal to the input for any depth in [1, 5]", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(arbChainLevel(), { minLength: 1, maxLength: 5 }),
|
||||
(levels) => {
|
||||
const encoded = codec.encode(levels);
|
||||
const decoded = codec.decode(encoded);
|
||||
expect(decoded.length).toBe(levels.length);
|
||||
for (let i = 0; i < levels.length; i++) {
|
||||
const a = levels[i];
|
||||
const b = decoded[i];
|
||||
if (!a || !b) throw new Error("missing level");
|
||||
expect(b.algorithm).toBe(a.algorithm);
|
||||
expect(Array.from(b.signerPublicKey)).toEqual(
|
||||
Array.from(a.signerPublicKey)
|
||||
);
|
||||
expect(Array.from(b.payload)).toEqual(
|
||||
Array.from(a.payload)
|
||||
);
|
||||
expect(Array.from(b.signature)).toEqual(
|
||||
Array.from(a.signature)
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("property: packet prepare → serve round-trip", () => {
|
||||
let dir = "";
|
||||
let vaultPath = "";
|
||||
const PW = "proppw1234";
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = path.join(
|
||||
tmpdir(),
|
||||
`sk-prop-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await createRootKeysCommand(PW, { output: dir });
|
||||
vaultPath = path.join(dir, "root.vault");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Each fast-check run decrypts the vault (Argon2id) inside
|
||||
// prepareFileCommand, which dominates wall-clock time at ~5s/run.
|
||||
// The chain codec property above runs at 200 numRuns; here we use a
|
||||
// smaller `numRuns` so the suite stays under the package's 30s
|
||||
// testTimeout while still exercising a meaningful spread of file
|
||||
// sizes and packet sizes.
|
||||
it("serve(prepare(file)) is byte-identical for arbitrary file bytes (1..10 KiB) and packet sizes", {
|
||||
timeout: 300000,
|
||||
}, async () => {
|
||||
let runIndex = 0;
|
||||
await fc.assert(
|
||||
fc.asyncProperty(
|
||||
arbBytes(1, 10 * 1024),
|
||||
fc.integer({ min: 64, max: 4096 }),
|
||||
async (fileBytes, packetSize) => {
|
||||
const label = `run-${runIndex++}`;
|
||||
const inputPath = path.join(dir, `${label}.bin`);
|
||||
const preparedDir = path.join(dir, `${label}-prepared`);
|
||||
const outputPath = path.join(dir, `${label}-out.bin`);
|
||||
await fs.writeFile(inputPath, fileBytes);
|
||||
await prepareFileCommand(inputPath, vaultPath, PW, {
|
||||
output: preparedDir,
|
||||
packetSize,
|
||||
});
|
||||
const result = await serveFileCommand(preparedDir, {
|
||||
output: outputPath,
|
||||
});
|
||||
expect(result.bytesWritten).toBe(fileBytes.byteLength);
|
||||
const reassembled = await fs.readFile(outputPath);
|
||||
expect(reassembled.byteLength).toBe(fileBytes.byteLength);
|
||||
for (let i = 0; i < fileBytes.byteLength; i++) {
|
||||
if (reassembled[i] !== fileBytes[i]) {
|
||||
throw new Error(
|
||||
`byte mismatch at offset ${i} (run=${label})`
|
||||
);
|
||||
}
|
||||
}
|
||||
await fs.rm(inputPath, { force: true });
|
||||
await fs.rm(preparedDir, { recursive: true, force: true });
|
||||
await fs.rm(outputPath, { force: true });
|
||||
}
|
||||
),
|
||||
{ numRuns: 10 }
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Test-environment setup for @suaveplan/server-keys.
|
||||
*
|
||||
* Wires a real in-memory OpenTelemetry `MeterProvider` BEFORE any source
|
||||
* module is imported, so the meter resolved by `instruments.ts` at module
|
||||
* load is the real provider's meter — not the noop fallback. Tests that
|
||||
* inspect emitted metrics flush via `flushTestMetrics`.
|
||||
*
|
||||
* Node ≥18 already exposes `crypto.subtle` and `crypto.getRandomValues`
|
||||
* globally, so no crypto polyfill is required.
|
||||
*/
|
||||
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);
|
||||
|
||||
/** Force-flush the test meter provider and return the captured snapshots. */
|
||||
export async function flushTestMetrics() {
|
||||
await testMeterProvider.forceFlush();
|
||||
const collected = testInMemoryMetricExporter.getMetrics();
|
||||
testInMemoryMetricExporter.reset();
|
||||
return collected;
|
||||
}
|
||||
Reference in New Issue
Block a user