release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createOpenApi, generateApiArtifacts, inspectApi } from "../src/api-command.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () =>
|
||||
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
|
||||
);
|
||||
async function fixture() {
|
||||
const root = join(tmpdir(), `wrnexus-api-${crypto.randomUUID()}`);
|
||||
roots.push(root);
|
||||
await mkdir(join(root, "app", "api", "users"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "app", "api", "users", "[id].ts"),
|
||||
"export async function GET(){}\nexport const PATCH = () => {};\n",
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("API and SDK generation", () => {
|
||||
test("derives methods, paths and OpenAPI operations from file routes", async () => {
|
||||
const operations = inspectApi(await fixture());
|
||||
expect(operations.map((operation) => `${operation.method} ${operation.path}`)).toEqual([
|
||||
"GET /api/users/{id}",
|
||||
"PATCH /api/users/{id}",
|
||||
]);
|
||||
expect(createOpenApi(operations).openapi).toBe("3.1.0");
|
||||
});
|
||||
test("emits docs, Postman, examples and all requested SDK languages", async () => {
|
||||
const root = await fixture();
|
||||
const result = generateApiArtifacts(root, ["typescript", "javascript", "java", "go", "python"]);
|
||||
expect(result.files).toHaveLength(9);
|
||||
expect(
|
||||
JSON.parse(await readFile(join(root, "generated/api/openapi.json"), "utf8")).paths[
|
||||
"/api/users/{id}"
|
||||
].get.operationId,
|
||||
).toBe("getUsersId");
|
||||
expect(await readFile(join(root, "generated/api/sdk/python/wrnexus-api.py"), "utf8")).toContain(
|
||||
"class WrnexusApi",
|
||||
);
|
||||
});
|
||||
test("extracts webhook prose and schemas into OpenAPI 3.1 webhooks", async () => {
|
||||
const root = await fixture();
|
||||
await mkdir(join(root, "app", "api", "webhooks"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "app", "api", "webhooks", "payment.ts"),
|
||||
`
|
||||
export const webhook = defineWebhook({
|
||||
event: "payment.completed",
|
||||
summary: "Payment completed",
|
||||
description: "Sent after settlement.",
|
||||
payloadSchema: "#/components/schemas/Payment",
|
||||
signatureHeader: "x-payment-signature"
|
||||
});
|
||||
export const POST = () => new Response("ok");
|
||||
`,
|
||||
);
|
||||
const spec = createOpenApi(inspectApi(root)) as any;
|
||||
expect(spec.webhooks["payment.completed"].post.description).toBe("Sent after settlement.");
|
||||
expect(
|
||||
spec.webhooks["payment.completed"].post.requestBody.content["application/json"].schema.$ref,
|
||||
).toBe("#/components/schemas/Payment");
|
||||
expect(spec.webhooks["payment.completed"].post.parameters[0].name).toBe("x-payment-signature");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { compatibilityReport, upgradeCompatibility } from "../src/compatibility-command.ts";
|
||||
|
||||
test("compatibility upgrade is backed up, current, and idempotent", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-compatibility-"));
|
||||
const file = join(root, "wrnexus.config.ts");
|
||||
writeFileSync(file, `export default { port: 3000 };\n`);
|
||||
const first = upgradeCompatibility(root);
|
||||
expect(first.changed).toBe(true);
|
||||
expect(readFileSync(first.backup, "utf8")).toContain("port: 3000");
|
||||
expect(readFileSync(file, "utf8")).toContain('compatibilityDate: "2026-08-02"');
|
||||
expect(upgradeCompatibility(root).changed).toBe(false);
|
||||
expect((await compatibilityReport(root)).needsUpgrade).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { runContractsCommand } from "../src/contracts-command.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () =>
|
||||
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
|
||||
);
|
||||
|
||||
async function fixture(): Promise<string> {
|
||||
const root = join(tmpdir(), `wrnexus-contracts-${crypto.randomUUID()}`);
|
||||
roots.push(root);
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "wrnexus.contracts.json"),
|
||||
JSON.stringify({
|
||||
format: 1,
|
||||
contracts: [
|
||||
{
|
||||
kind: "queue",
|
||||
name: "mail",
|
||||
version: 1,
|
||||
consumers: ["worker"],
|
||||
payload: {
|
||||
type: "object",
|
||||
fields: { to: { type: "string", rules: [] } },
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("contracts command", () => {
|
||||
test("snapshots and checks compatible contracts", async () => {
|
||||
const root = await fixture();
|
||||
expect((await runContractsCommand(root, "snapshot")).ok).toBe(true);
|
||||
expect((await runContractsCommand(root, "check")).ok).toBe(true);
|
||||
});
|
||||
|
||||
test("returns a failed result for breaking changes", async () => {
|
||||
const root = await fixture();
|
||||
await runContractsCommand(root, "snapshot");
|
||||
await writeFile(
|
||||
join(root, "wrnexus.contracts.json"),
|
||||
JSON.stringify({ format: 1, contracts: [] }),
|
||||
);
|
||||
const result = await runContractsCommand(root, "check");
|
||||
expect(result).toMatchObject({ ok: false, issueCount: 1 });
|
||||
});
|
||||
|
||||
test("requires an explicit baseline", async () => {
|
||||
const root = await fixture();
|
||||
await expect(runContractsCommand(root, "check")).rejects.toThrow("WRN-CONTRACT-BASELINE");
|
||||
});
|
||||
});
|
||||
@@ -22,8 +22,10 @@ test("scaffoldApp creates a comprehensive .gitignore", () => {
|
||||
".wrnexus/",
|
||||
".env.*",
|
||||
"!.env.example",
|
||||
"!.env.*.example",
|
||||
"*.log",
|
||||
"*.db",
|
||||
"uploads/",
|
||||
"coverage/",
|
||||
"mobile/android/",
|
||||
".vscode/",
|
||||
@@ -47,6 +49,79 @@ test("scaffoldApp includes production build and start scripts", () => {
|
||||
expect(pkg.scripts.build).toBe("wrnexus build .");
|
||||
expect(pkg.scripts.start).toBe("bun dist/server.js");
|
||||
expect(pkg.scripts.production).toBe("bun run build && bun run start");
|
||||
expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
|
||||
expect(pkg.scripts.test).toBe("wrnexus test .");
|
||||
expect(pkg.scripts.check).toBe(
|
||||
"bun run typecheck && bun run lint && bun run test && bun run format:check",
|
||||
);
|
||||
} finally {
|
||||
rmSync(parent, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("scaffoldApp includes the complete v0.8 configuration and starter structure", () => {
|
||||
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
|
||||
const root = join(parent, "complete-app");
|
||||
|
||||
try {
|
||||
scaffoldApp(root, "complete-app");
|
||||
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||
|
||||
for (const packageName of [
|
||||
"@wrnexus/auth",
|
||||
"@wrnexus/captcha",
|
||||
"@wrnexus/db",
|
||||
"@wrnexus/encryption",
|
||||
"@wrnexus/i18n",
|
||||
"@wrnexus/image",
|
||||
"@wrnexus/jwt",
|
||||
"@wrnexus/observability",
|
||||
"@wrnexus/realtime",
|
||||
"@wrnexus/security",
|
||||
"@wrnexus/store",
|
||||
"@wrnexus/ui",
|
||||
"@wrnexus/uploader",
|
||||
"@wrnexus/validation",
|
||||
]) {
|
||||
expect(pkg.dependencies[packageName]).toBe(currentCliVersion());
|
||||
}
|
||||
|
||||
for (const block of [
|
||||
"plugins:",
|
||||
"imports:",
|
||||
"types:",
|
||||
"stores:",
|
||||
"compatibility:",
|
||||
"performance:",
|
||||
"observability:",
|
||||
"tenancy:",
|
||||
"build:",
|
||||
"navigation:",
|
||||
"devToolbar:",
|
||||
"theme:",
|
||||
"i18n:",
|
||||
"db:",
|
||||
"databases:",
|
||||
"storage:",
|
||||
"realtime:",
|
||||
"profiles:",
|
||||
]) {
|
||||
expect(config).toContain(block);
|
||||
}
|
||||
|
||||
for (const relative of [
|
||||
".env.example",
|
||||
".env.test.example",
|
||||
"app/locales/en.json",
|
||||
"app/db/migrations/0001_init.sql",
|
||||
"app/db/seed.ts",
|
||||
"app/schemas/contact.ts",
|
||||
"app/realtime/chat.ts",
|
||||
"test/smoke.test.ts",
|
||||
]) {
|
||||
expect(existsSync(join(root, relative))).toBe(true);
|
||||
}
|
||||
} finally {
|
||||
rmSync(parent, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEPLOY_TARGETS, generateDeployment } from "../src/deploy.ts";
|
||||
|
||||
describe("deployment presets", () => {
|
||||
for (const target of DEPLOY_TARGETS) {
|
||||
test(`generates ${target}`, () => {
|
||||
const root = mkdtempSync(join(tmpdir(), `wrnexus-${target}-`));
|
||||
const files = generateDeployment(root, target);
|
||||
expect(files).toContain(".env.production.example");
|
||||
expect(readFileSync(join(root, "deploy/README.md"), "utf8")).toContain("/readyz");
|
||||
expect(generateDeployment(root, target)).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("Kubernetes includes probes, limits and release migration", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-k8s-"));
|
||||
generateDeployment(root, "kubernetes");
|
||||
const manifest = readFileSync(join(root, "deploy/kubernetes.yaml"), "utf8");
|
||||
expect(manifest).toContain("readinessProbe");
|
||||
expect(manifest).toContain("kind: Job");
|
||||
expect(manifest).toContain("resources:");
|
||||
});
|
||||
|
||||
test("rejects unknown targets", () => {
|
||||
expect(() => generateDeployment(".", "unknown")).toThrow("WRN-DEPLOY-TARGET");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { inspectProject } from "../src/doctor.ts";
|
||||
import { inspectProject, repairProject } from "../src/doctor.ts";
|
||||
|
||||
test("doctor reports a healthy minimal project", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-doctor-"));
|
||||
@@ -19,3 +19,29 @@ test("doctor returns actionable missing-project checks", () => {
|
||||
expect(checks.find((check) => check.name === "package.json")?.ok).toBe(false);
|
||||
expect(checks.find((check) => check.name === "app/pages")?.detail).toBe("Create app/pages");
|
||||
});
|
||||
|
||||
test("doctor --fix applies safe repairs and is idempotent", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-doctor-fix-"));
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "app",
|
||||
dependencies: { "@wrnexus/core": "^0.8.0", "@wrnexus/router": "^0.7.0" },
|
||||
wrnexus: { version: "0.7.0" },
|
||||
}),
|
||||
);
|
||||
mkdirSync(join(root, "app", "components"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "app", "components", "Greeting.wrn"),
|
||||
'component Greeting { props { name:string="World" } view { <p>{name}</p> } }',
|
||||
);
|
||||
|
||||
const repairs = repairProject(root);
|
||||
expect(repairs.map(({ name }) => name)).toContain("app/pages");
|
||||
expect(repairs.map(({ name }) => name)).toContain("configuration");
|
||||
expect(existsSync(join(root, "wrnexus.config.ts"))).toBe(true);
|
||||
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
expect(manifest.dependencies["@wrnexus/router"]).toBe("^0.8.0");
|
||||
expect(manifest.wrnexus.version).toBe("0.8.0");
|
||||
expect(repairProject(root)).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { explainBuildDecision } from "../src/explain.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () =>
|
||||
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
|
||||
);
|
||||
|
||||
async function fixture(): Promise<string> {
|
||||
const root = join(tmpdir(), `wrnexus-explain-${crypto.randomUUID()}`);
|
||||
roots.push(root);
|
||||
await mkdir(join(root, "dist"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "dist", "build-report.json"),
|
||||
JSON.stringify({
|
||||
frameworkVersion: "0.8.0",
|
||||
adapter: "edge",
|
||||
measurements: { routeJsBytes: 12 },
|
||||
budgetViolations: [],
|
||||
assets: [{ file: "server.js", bytes: 12 }],
|
||||
routes: [
|
||||
{
|
||||
kind: "page",
|
||||
path: "/users/[id]",
|
||||
source: "app/pages/users/[id].wrn",
|
||||
execution: "authenticated-ssr",
|
||||
canPrerender: false,
|
||||
needsClientRuntime: true,
|
||||
needsServerRuntime: true,
|
||||
hydrationStrategy: "visible",
|
||||
reasons: ["client interactivity", "authentication required"],
|
||||
cachePolicy: { strategy: "stale-while-revalidate", ttl: "30s" },
|
||||
requiredPermission: "users.read",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("causal build explanations", () => {
|
||||
test("explains route execution and hydration from persisted compiler evidence", async () => {
|
||||
const root = await fixture();
|
||||
const route = explainBuildDecision(root, "route", "/users/[id]");
|
||||
expect(route.summary).toContain("authenticated-ssr");
|
||||
expect(route.reasons).toContain("authentication required");
|
||||
expect(explainBuildDecision(root, "hydration", "users/[id]").summary).toContain("visible");
|
||||
});
|
||||
|
||||
test("explains build and bundle measurements", async () => {
|
||||
const root = await fixture();
|
||||
expect(explainBuildDecision(root, "build").reasons).toContain(
|
||||
"all configured performance budgets pass",
|
||||
);
|
||||
expect(explainBuildDecision(root, "bundle").reasons[0]).toBe("server.js: 12 bytes");
|
||||
});
|
||||
|
||||
test("explains cache and permission decisions", async () => {
|
||||
const root = await fixture();
|
||||
expect(explainBuildDecision(root, "cache", "/users/[id]").summary).toContain(
|
||||
"stale-while-revalidate",
|
||||
);
|
||||
expect(explainBuildDecision(root, "permission", "users.read").reasons[0]).toContain(
|
||||
"security.permission",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses stable diagnostics for missing evidence", () => {
|
||||
expect(() => explainBuildDecision("missing", "build")).toThrow("WRN-EXPLAIN-NO-BUILD");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { runI18nCommand } from "../src/i18n-command.ts";
|
||||
|
||||
test("i18n extract and validate audit native WRN translation keys", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-i18n-cli-"));
|
||||
mkdirSync(join(root, "app/pages"), { recursive: true });
|
||||
mkdirSync(join(root, "app/locales"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "app/pages/index.wrn"),
|
||||
`page Home { view { <h1>{t:home.title}</h1> } }`,
|
||||
);
|
||||
writeFileSync(join(root, "app/locales/en.json"), JSON.stringify({ home: { title: "Home" } }));
|
||||
writeFileSync(
|
||||
join(root, "app/locales/mr.json"),
|
||||
JSON.stringify({ home: { title: "मुख्यपृष्ठ" } }),
|
||||
);
|
||||
expect(runI18nCommand(root, "extract")).toBe(true);
|
||||
expect(existsSync(join(root, ".wrnexus/i18n-keys.json"))).toBe(true);
|
||||
expect(runI18nCommand(root, "validate")).toBe(true);
|
||||
});
|
||||
test("i18n validate fails missing locale keys", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-i18n-cli-"));
|
||||
mkdirSync(join(root, "app/pages"), { recursive: true });
|
||||
mkdirSync(join(root, "app/locales"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "app/pages/index.wrn"),
|
||||
`page Home { view { <h1>{t:home.title}</h1> } }`,
|
||||
);
|
||||
writeFileSync(join(root, "app/locales/en.json"), JSON.stringify({ home: { title: "Home" } }));
|
||||
writeFileSync(join(root, "app/locales/es.json"), JSON.stringify({}));
|
||||
expect(runI18nCommand(root, "validate")).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { runPluginCliCommand } from "../src/plugin-command.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
delete (globalThis as Record<string, unknown>).__pluginCommandArgs;
|
||||
});
|
||||
|
||||
test("application plugins can register executable CLI commands", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-command-"));
|
||||
roots.push(root);
|
||||
writeFileSync(
|
||||
join(root, "wrnexus.config.ts"),
|
||||
`export default {
|
||||
plugins: [{
|
||||
name: "command-test",
|
||||
cliCommands: [{
|
||||
name: "greet",
|
||||
run(args) { globalThis.__pluginCommandArgs = args }
|
||||
}]
|
||||
}]
|
||||
};
|
||||
`,
|
||||
);
|
||||
expect(await runPluginCliCommand(root, "greet", ["Ada"])).toBe(true);
|
||||
expect((globalThis as Record<string, unknown>).__pluginCommandArgs).toEqual(["Ada"]);
|
||||
expect(await runPluginCliCommand(root, "missing", [])).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { productionEntry, runPreview } from "../src/preview.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () =>
|
||||
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
|
||||
);
|
||||
|
||||
describe("production preview", () => {
|
||||
test("refuses to approximate a missing production build", () => {
|
||||
expect(() => productionEntry("missing-preview-root")).toThrow("WRN-PREVIEW-NO-BUILD");
|
||||
});
|
||||
|
||||
test("executes the exact dist server with production environment", async () => {
|
||||
const root = join(tmpdir(), `wrnexus-preview-${crypto.randomUUID()}`);
|
||||
roots.push(root);
|
||||
await mkdir(join(root, "dist"), { recursive: true });
|
||||
await writeFile(
|
||||
join(root, "dist", "server.js"),
|
||||
"console.log(process.env.NODE_ENV + ':' + process.env.PORT)",
|
||||
);
|
||||
expect(productionEntry(root)).toBe(join(root, "dist", "server.js"));
|
||||
const child = runPreview(root, { port: 4100, stdio: "pipe" });
|
||||
const output = await new Response(child.stdout as never).text();
|
||||
expect(await new Promise<number | null>((resolve) => child.on("exit", resolve))).toBe(0);
|
||||
expect(output.trim()).toBe("production:4100");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { generateReproductionReport } from "../src/report.ts";
|
||||
|
||||
test("report bundles actionable diagnostics while redacting secrets and user/internal data", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-report-"));
|
||||
mkdirSync(join(root, "app/pages"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({ dependencies: { "@wrnexus/core": "0.8.0" } }),
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "wrnexus.config.ts"),
|
||||
`export default { apiKey: "sk_secretsecretsecret", endpoint: "https://internal.police.local/api" }`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app/pages/index.wrn"),
|
||||
`page Home { view { <p>officer@example.com</p> } }`,
|
||||
);
|
||||
const result = generateReproductionReport(root, {
|
||||
file: "app/pages/index.wrn",
|
||||
error: "token=wrn_supersecrettoken at 10.0.0.1",
|
||||
output: ".wrnexus/report-test",
|
||||
});
|
||||
const all = readFileSync(result.reportFile, "utf8") + readFileSync(result.sourceFile!, "utf8");
|
||||
expect(all).toContain("frameworkVersion");
|
||||
expect(all).toContain("diagnostics");
|
||||
expect(all).not.toContain("sk_secretsecretsecret");
|
||||
expect(all).not.toContain("officer@example.com");
|
||||
expect(all).not.toContain("internal.police.local");
|
||||
expect(all).not.toContain("10.0.0.1");
|
||||
});
|
||||
test("report rejects traversal inputs and outputs", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-report-"));
|
||||
writeFileSync(join(root, "package.json"), "{}");
|
||||
expect(() => generateReproductionReport(root, { file: "../secret" })).toThrow("WRN-REPORT-FILE");
|
||||
expect(() => generateReproductionReport(root, { output: "../outside" })).toThrow(
|
||||
"WRN-REPORT-OUTPUT",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { validateRuntimeCapabilities } from "../src/build.ts";
|
||||
|
||||
test("production targets fail before bundling incompatible application imports", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-edge-build-"));
|
||||
mkdirSync(join(root, "app", "api"), { recursive: true });
|
||||
writeFileSync(join(root, "app", "api", "files.ts"), `import fs from "node:fs";`);
|
||||
expect(() => validateRuntimeCapabilities(root, "edge")).toThrow(/WRN-RUNTIME-CAPABILITY/);
|
||||
expect(() => validateRuntimeCapabilities(root, "worker")).toThrow(/filesystem/);
|
||||
expect(() => validateRuntimeCapabilities(root, "bun")).not.toThrow();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { runSecurityCommand, securityAudit, securityHeaders } from "../src/security-command.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () =>
|
||||
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
|
||||
);
|
||||
|
||||
async function fixture(config = "export default {};"): Promise<string> {
|
||||
const root = join(tmpdir(), `wrnexus-security-${crypto.randomUUID()}`);
|
||||
roots.push(root);
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(join(root, "wrnexus.config.ts"), config);
|
||||
return root;
|
||||
}
|
||||
|
||||
describe("security command", () => {
|
||||
test("audits secure framework defaults against mapped ASVS controls", async () => {
|
||||
const report = await securityAudit(await fixture());
|
||||
expect(report.passed).toBe(true);
|
||||
expect(report.version).toBe("ASVS 5.0.0");
|
||||
expect(report.checks.every((check) => check.asvs.length > 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("reports deliberately disabled headers", async () => {
|
||||
const report = await securityAudit(
|
||||
await fixture("export default { security: { headers: false } };"),
|
||||
);
|
||||
expect(report.passed).toBe(false);
|
||||
expect(report.checks.find((check) => check.id === "SEC-HEADERS")?.passed).toBe(false);
|
||||
});
|
||||
|
||||
test("prints the effective production headers", async () => {
|
||||
const headers = await securityHeaders(await fixture());
|
||||
expect(headers["content-security-policy"]).toContain("nonce-audit-nonce");
|
||||
expect(headers["strict-transport-security"]).toContain("max-age=");
|
||||
expect(headers["x-content-type-options"]).toBe("nosniff");
|
||||
});
|
||||
|
||||
test("rejects credentialed wildcard CORS and unknown commands", async () => {
|
||||
const root = await fixture(
|
||||
'export default { security: { cors: { origin: "*", credentials: true } } };',
|
||||
);
|
||||
expect((await securityAudit(root)).passed).toBe(false);
|
||||
await expect(runSecurityCommand(root, "unknown")).rejects.toThrow("WRN-SECURITY-COMMAND");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import {
|
||||
createLocalServicesHandler,
|
||||
ensureLocalCertificate,
|
||||
startLocalServices,
|
||||
} from "../src/services.ts";
|
||||
|
||||
describe("local production service simulator", () => {
|
||||
test("simulates bounded mail, cache, storage, auth and health APIs", async () => {
|
||||
const handler = createLocalServicesHandler();
|
||||
expect((await handler(new Request("http://local/healthz"))).status).toBe(200);
|
||||
expect(
|
||||
(
|
||||
await handler(
|
||||
new Request("http://local/mail", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ to: "u@test", subject: "Welcome" }),
|
||||
}),
|
||||
)
|
||||
).status,
|
||||
).toBe(202);
|
||||
expect(await (await handler(new Request("http://local/mail"))).json()).toHaveLength(1);
|
||||
await handler(
|
||||
new Request("http://local/cache", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ key: "user", value: 1 }),
|
||||
}),
|
||||
);
|
||||
expect(await (await handler(new Request("http://local/cache"))).json()).toHaveProperty(
|
||||
"user.value",
|
||||
1,
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await handler(
|
||||
new Request("http://local/storage?key=file.txt", { method: "POST", body: "hello" }),
|
||||
)
|
||||
).status,
|
||||
).toBe(201);
|
||||
expect(await (await handler(new Request("http://local/storage/file.txt"))).text()).toBe(
|
||||
"hello",
|
||||
);
|
||||
expect(
|
||||
(
|
||||
await handler(
|
||||
new Request("http://local/auth", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: "u@test" }),
|
||||
}),
|
||||
)
|
||||
).status,
|
||||
).toBe(201);
|
||||
});
|
||||
test("rejects unsafe storage keys and oversized declared JSON", async () => {
|
||||
const handler = createLocalServicesHandler();
|
||||
expect(
|
||||
(
|
||||
await handler(
|
||||
new Request("http://local/storage?key=../secret", { method: "POST", body: "bad" }),
|
||||
)
|
||||
).status,
|
||||
).toBe(400);
|
||||
const response = await handler(
|
||||
new Request("http://local/mail", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: "x".repeat(300_000) }),
|
||||
}),
|
||||
);
|
||||
expect(response.status).toBe(413);
|
||||
});
|
||||
test("generates and safely reuses a localhost HTTPS certificate", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-services-cert-"));
|
||||
const first = await ensureLocalCertificate(root);
|
||||
const certificate = new X509Certificate(first.cert);
|
||||
expect(certificate.subjectAltName).toContain("DNS:localhost");
|
||||
expect(certificate.subjectAltName).toContain("IP Address:127.0.0.1");
|
||||
expect(statSync(first.keyFile).size).toBeGreaterThan(100);
|
||||
const second = await ensureLocalCertificate(root);
|
||||
expect(second.reused).toBe(true);
|
||||
expect(second.cert).toBe(first.cert);
|
||||
});
|
||||
test("serves the simulator over generated HTTPS", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-services-tls-"));
|
||||
const server = await startLocalServices({ appRoot: root, port: 0, hostname: "127.0.0.1" });
|
||||
try {
|
||||
const response = await fetch(`https://127.0.0.1:${server.port}/healthz`, {
|
||||
tls: { rejectUnauthorized: false },
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toHaveProperty("service", "wrnexus-local-services");
|
||||
} finally {
|
||||
server.stop(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createTestPlan } from "../src/test.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () =>
|
||||
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
|
||||
);
|
||||
async function fixture(): Promise<string> {
|
||||
const root = join(tmpdir(), `wrnexus-test-command-${crypto.randomUUID()}`);
|
||||
roots.push(root);
|
||||
await mkdir(join(root, "test", "component"), { recursive: true });
|
||||
await writeFile(join(root, "test", "math.unit.test.ts"), "export {};\n");
|
||||
await writeFile(join(root, "test", "component", "card.test.ts"), "export {};\n");
|
||||
await writeFile(join(root, "test", "home.a11y.test.ts"), "export {};\n");
|
||||
await writeFile(join(root, "package.json"), "{}");
|
||||
return root;
|
||||
}
|
||||
describe("test command planning", () => {
|
||||
test("discovers the requested Bun test level", async () => {
|
||||
const root = await fixture();
|
||||
expect(createTestPlan(root, ["unit"]).files).toHaveLength(1);
|
||||
expect(createTestPlan(root, ["component"]).files[0]).toContain("card.test.ts");
|
||||
expect(createTestPlan(root, ["accessibility"]).files[0]).toContain("a11y.test.ts");
|
||||
});
|
||||
test("delegates browser and visual suites to Playwright", async () => {
|
||||
const root = await fixture();
|
||||
await writeFile(join(root, "playwright.config.ts"), "export default {};\n");
|
||||
expect(createTestPlan(root, ["browser"]).args).toContain("playwright");
|
||||
expect(createTestPlan(root, ["visual"]).args).toContain("@visual");
|
||||
const matrix = createTestPlan(root, [
|
||||
"browser",
|
||||
"--browsers=chromium,firefox",
|
||||
"--shard=2/3",
|
||||
"--install-browsers",
|
||||
]);
|
||||
expect(matrix.args).toContain("firefox");
|
||||
expect(matrix.args).toContain("--shard=2/3");
|
||||
expect(matrix.args).toContain("--reporter=line,html");
|
||||
expect(matrix.setup?.args).toEqual(["x", "playwright", "install", "chromium", "firefox"]);
|
||||
});
|
||||
test("deterministically shards convention-based suites", async () => {
|
||||
const root = await fixture();
|
||||
await writeFile(join(root, "test", "second.unit.test.ts"), "export {};\n");
|
||||
expect(createTestPlan(root, ["unit", "--shard=1/2"]).files).toHaveLength(1);
|
||||
expect(() => createTestPlan(root, ["unit", "--shard=3/2"])).toThrow("WRN-TEST-SHARD");
|
||||
});
|
||||
test("keeps the unfiltered legacy command", async () => {
|
||||
const plan = createTestPlan(await fixture(), ["--watch"]);
|
||||
expect(plan.level).toBeUndefined();
|
||||
expect(plan.args).toEqual(["test", "--watch"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { checkApplication, generateApplicationTypes } from "../src/types.ts";
|
||||
import { inspectComponent } from "../src/inspect.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-types-"));
|
||||
roots.push(root);
|
||||
for (const dir of ["pages/users", "components", "api", "realtime", "queues", "locales"])
|
||||
mkdirSync(join(root, "app", dir), { recursive: true });
|
||||
writeFileSync(join(root, ".env.example"), "PUBLIC_API_URL=https://example.test\n");
|
||||
writeFileSync(join(root, "app/pages/index.wrn"), "page Home { view { <h1>Home</h1> } }\n");
|
||||
writeFileSync(
|
||||
join(root, "app/pages/users/[id].wrn"),
|
||||
"page User { props { id: string } view { <p>{id}</p> } }\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app/components/Button.wrn"),
|
||||
"component Button { props { label: string } outputs { press(event: MouseEvent) } view { <button>{label}</button> } }\n",
|
||||
);
|
||||
writeFileSync(join(root, "app/api/users.ts"), "export default () => new Response('ok');\n");
|
||||
writeFileSync(join(root, "app/realtime/chat.ts"), "export default {};\n");
|
||||
writeFileSync(join(root, "app/queues/email.ts"), "export default {};\n");
|
||||
writeFileSync(join(root, "app/locales/en.json"), JSON.stringify({ common: { save: "Save" } }));
|
||||
return root;
|
||||
}
|
||||
|
||||
test("generate types emits application-wide deterministic contracts", () => {
|
||||
const root = fixture();
|
||||
const result = generateApplicationTypes(root);
|
||||
const output = readFileSync(join(root, result.file), "utf8");
|
||||
expect(existsSync(join(root, "app/routes.gen.ts"))).toBe(true);
|
||||
expect(output).toContain('type RouteName = "index" | "users.id"');
|
||||
expect(output).toContain('type EnvironmentKey = "PUBLIC_API_URL"');
|
||||
expect(output).toContain('type TranslationKey = "common.save"');
|
||||
expect(output).toContain('type QueueName = "email"');
|
||||
expect(output).toContain('"Button": { props: { "label": string }');
|
||||
expect(output).toContain("interface ApiContracts");
|
||||
expect(output).toContain('"/api/users": { default: ApiContract<');
|
||||
expect(output).toContain("interface RealtimeMessages");
|
||||
expect(output).toContain('"/realtime/chat": RealtimeMessage<');
|
||||
expect(output).toContain("interface QueuePayloads");
|
||||
expect(output).toContain('"email": QueuePayload<');
|
||||
});
|
||||
|
||||
test("application checker validates every wrn source", () => {
|
||||
expect(checkApplication(fixture()).filter((item) => item.category === "error")).toEqual([]);
|
||||
}, 15_000);
|
||||
|
||||
test("component inspection exposes its typed public contract", () => {
|
||||
const value = inspectComponent(fixture(), "button") as { name: string; props: unknown[] };
|
||||
expect(value.name).toBe("Button");
|
||||
expect(value.props).toEqual([{ name: "label", type: "string", required: true }]);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { migrateV060WrnSource } from "../src/update.ts";
|
||||
import { formatCurrentWrnSource, migrateV060WrnSource } from "../src/update.ts";
|
||||
|
||||
const report = () => ({
|
||||
changedAutomatically: [],
|
||||
@@ -30,4 +30,16 @@ describe("v0.6 source migration", () => {
|
||||
expect(first).toContain("output.confirm({ ok: true })");
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
test("uses the canonical framework formatter idempotently", () => {
|
||||
const source = `page Home {
|
||||
view {
|
||||
<button type="button" class="one two three four five six seven eight nine ten eleven twelve" @click='save()'>Save</button>
|
||||
}
|
||||
}`;
|
||||
const formatted = formatCurrentWrnSource(source);
|
||||
|
||||
expect(formatted).toContain("<button\n");
|
||||
expect(formatCurrentWrnSource(formatted)).toBe(formatted);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,3 +286,56 @@ test("0.4 migration removes manual CAPTCHA runtime wiring and archives copied as
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("0.8 migration modernizes every WRN source with imports and a review report", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-update-current-source-"));
|
||||
mkdirSync(join(root, "app", "components"), { recursive: true });
|
||||
mkdirSync(join(root, "app", "layouts"), { recursive: true });
|
||||
mkdirSync(join(root, "app", "pages"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "source-app",
|
||||
dependencies: { "@wrnexus/core": "^0.7.0" },
|
||||
wrnexus: { version: "0.7.0" },
|
||||
}),
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app", "components", "Notice.wrn"),
|
||||
'component Notice {\r\n props { label = "Ready" count = 1 } \r\n view { <p>{label}</p> }\r\n}',
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app", "layouts", "shell.wrn"),
|
||||
"layout Shell {\n view { <main><slot /></main> }\n}\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, "app", "pages", "index.wrn"),
|
||||
'page Home {\n layout = "shell"\n view { <Notice label={"Updated"} /> <Missing /> }\n}\n',
|
||||
);
|
||||
|
||||
try {
|
||||
updateApp(root, "0.8.0", false);
|
||||
const first = readFileSync(join(root, "app", "pages", "index.wrn"), "utf8");
|
||||
expect(first).toContain('import Notice from "@/components/Notice.wrn"');
|
||||
expect(first).toContain('import Shell from "@/layouts/shell.wrn"');
|
||||
expect(first).toContain("layout = Shell");
|
||||
expect(first).toContain("label='{\"Updated\"}'");
|
||||
expect(first.endsWith("\n")).toBe(true);
|
||||
|
||||
const component = readFileSync(join(root, "app", "components", "Notice.wrn"), "utf8");
|
||||
expect(component).toContain('props {\n label = "Ready"\n count = 1\n }');
|
||||
expect(component).not.toContain("\r");
|
||||
|
||||
const reportPath = join(root, ".wrnexus", "migrations", "0.8.0-source-modernization.json");
|
||||
const report = JSON.parse(readFileSync(reportPath, "utf8"));
|
||||
expect(report.changedFiles).toContain("app/pages/index.wrn");
|
||||
expect(report.unresolvedImports).toContain(
|
||||
"app/pages/index.wrn: component 'Missing' could not be resolved",
|
||||
);
|
||||
|
||||
updateApp(root, "0.8.0", false);
|
||||
expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toBe(first);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,8 +47,14 @@ test("workspace templates pin the running framework release", () => {
|
||||
expect(files["README.md"]).toContain("http://127.0.0.1:3000");
|
||||
expect(files["README.md"]).toContain("internal gateway targets");
|
||||
expect(JSON.parse(files["package.json"]!).scripts.production).toBe("wrnexus production");
|
||||
expect(JSON.parse(files["package.json"]!).scripts.check).toContain("typecheck");
|
||||
expect(JSON.parse(files["package.json"]!).scripts.check).toContain("format:check");
|
||||
expect(files["wrnexus.workspace.ts"]).toContain('runtime: "development"');
|
||||
expect(files["wrnexus.workspace.ts"]).toContain("hmr: false");
|
||||
expect(files[".env.example"]).toContain("REDIS_URL");
|
||||
expect(files["eslint.config.js"]).toContain("typescript-eslint");
|
||||
expect(files["tsconfig.json"]).toContain('"strict": true');
|
||||
expect(files[".vscode/extensions.json"]).toContain("wrnexus.wrnexus");
|
||||
});
|
||||
|
||||
test("production workspace detects default and named SQL migrations", () => {
|
||||
|
||||
Reference in New Issue
Block a user