🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* PR-C Deprecation Warning System — Tests
|
||||
*
|
||||
* Run: node --test test/architecture/deprecation-warning.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { warnOnce, resetWarnings, getWarningCount, warnMany, runDeprecationCheck } from "../../scripts/lib/deprecation-warning.mjs";
|
||||
|
||||
const CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
||||
const TEST_DIR = join(homedir(), ".openclaw", "workspace", "test", "architecture");
|
||||
const TMP_CONFIG = join(homedir(), ".openclaw", "openclaw-prc-test.json");
|
||||
|
||||
beforeEach(() => {
|
||||
resetWarnings();
|
||||
});
|
||||
|
||||
describe("PR-C: Deprecation Warning System", () => {
|
||||
// ─── warnOnce utility tests ──────────────────────────────
|
||||
describe("warnOnce utility", () => {
|
||||
it("warn-01: same key only warns once", () => {
|
||||
const r1 = warnOnce("test.key1", "first message");
|
||||
const r2 = warnOnce("test.key1", "second message");
|
||||
const r3 = warnOnce("test.key1", "third message");
|
||||
|
||||
assert.equal(r1, true, "first call must emit");
|
||||
assert.equal(r2, false, "second call must be suppressed");
|
||||
assert.equal(r3, false, "third call must be suppressed");
|
||||
assert.equal(getWarningCount(), 1, "only 1 unique key warned");
|
||||
});
|
||||
|
||||
it("warn-02: different keys both warn", () => {
|
||||
const r1 = warnOnce("test.key1", "msg1");
|
||||
const r2 = warnOnce("test.key2", "msg2");
|
||||
|
||||
assert.equal(r1, true);
|
||||
assert.equal(r2, true);
|
||||
assert.equal(getWarningCount(), 2);
|
||||
});
|
||||
|
||||
it("warn-03: force option bypasses deduplication", () => {
|
||||
warnOnce("test.force", "first");
|
||||
const r = warnOnce("test.force", "second", { force: true });
|
||||
|
||||
assert.equal(r, true, "force must emit even if already warned");
|
||||
assert.equal(getWarningCount(), 1, "key count unchanged (same key)");
|
||||
});
|
||||
|
||||
it("warn-04: resetWarnings clears all state", () => {
|
||||
warnOnce("test.a", "a");
|
||||
warnOnce("test.b", "b");
|
||||
assert.equal(getWarningCount(), 2);
|
||||
|
||||
resetWarnings();
|
||||
assert.equal(getWarningCount(), 0);
|
||||
|
||||
// Can warn again after reset
|
||||
const r = warnOnce("test.a", "a again");
|
||||
assert.equal(r, true);
|
||||
assert.equal(getWarningCount(), 1);
|
||||
});
|
||||
|
||||
it("warn-05: warnMany deduplicates across batch", () => {
|
||||
const emitted = warnMany([
|
||||
{ key: "a", message: "msg a" },
|
||||
{ key: "b", message: "msg b" },
|
||||
{ key: "a", message: "msg a again" }, // duplicate
|
||||
]);
|
||||
|
||||
assert.equal(emitted.length, 2, "only 2 unique warnings emitted");
|
||||
assert.equal(getWarningCount(), 2);
|
||||
});
|
||||
|
||||
it("warn-06: runDeprecationCheck catches errors", () => {
|
||||
const result = runDeprecationCheck("test-check", () => {
|
||||
return [{ key: "test.ok", message: "all good" }];
|
||||
});
|
||||
assert.equal(result.name, "test-check");
|
||||
assert.equal(result.warnings.length, 1);
|
||||
|
||||
const errResult = runDeprecationCheck("bad-check", () => {
|
||||
throw new Error("simulated failure");
|
||||
});
|
||||
assert.equal(errResult.warnings[0].key, "bad-check");
|
||||
assert.ok(errResult.warnings[0].message.includes("simulated failure"));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Config detection tests ─────────────────────────────
|
||||
describe("config detection", () => {
|
||||
it("detect-01: real config loads without crashing", () => {
|
||||
assert.ok(existsSync(CONFIG_PATH), "openclaw.json must exist");
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
assert.ok(typeof config === "object");
|
||||
assert.ok(config.gateway || config.agents, "config must have expected sections");
|
||||
});
|
||||
|
||||
it("detect-02: active-memory blocking mode detected", () => {
|
||||
// Read the actual config to check current state
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const activeCfg = config?.plugins?.entries?.["active-memory"];
|
||||
|
||||
if (activeCfg) {
|
||||
const mode = activeCfg.config?.mode;
|
||||
console.log(` [DETECT] active-memory mode: "${mode || "default"}"`);
|
||||
|
||||
if (mode === "blocking") {
|
||||
const r = warnOnce("active-memory.blocking",
|
||||
"active-memory blocking mode is deprecated. Set mode to 'precompute'.");
|
||||
assert.equal(r, true);
|
||||
}
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-03: dreaming REM phase detected", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const memoryCoreCfg = config?.plugins?.entries?.["memory-core"];
|
||||
|
||||
if (memoryCoreCfg?.config?.dreaming) {
|
||||
const dreaming = memoryCoreCfg.config.dreaming;
|
||||
const remEnabled = dreaming?.phases?.rem?.enabled;
|
||||
console.log(` [DETECT] dreaming REM enabled: ${remEnabled ?? "not configured"}`);
|
||||
|
||||
if (remEnabled === true) {
|
||||
warnOnce("dreaming.rem-phase",
|
||||
"Dreaming REM phase is deprecated. Will be replaced by Collect/Promote.");
|
||||
}
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-04: memory-wiki plugin reference detectable", () => {
|
||||
const wikiPath = join(
|
||||
"/opt/homebrew/lib/node_modules/openclaw",
|
||||
"dist/extensions/memory-wiki/openclaw.plugin.json"
|
||||
);
|
||||
const exists = existsSync(wikiPath);
|
||||
console.log(` [DETECT] memory-wiki plugin: ${exists ? "present" : "not found"}`);
|
||||
|
||||
if (exists) {
|
||||
const plugin = JSON.parse(readFileSync(wikiPath, "utf8"));
|
||||
assert.equal(plugin.id, "memory-wiki");
|
||||
warnOnce("memory-wiki.plugin",
|
||||
"memory-wiki plugin is legacy. Tools remain available during migration.");
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-05: QMD reference in config detectable", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const qmdRefs = [];
|
||||
|
||||
// Check memory search
|
||||
if (config?.agents?.defaults?.memorySearch?.provider === "qmd") {
|
||||
qmdRefs.push("memorySearch.provider=qmd");
|
||||
}
|
||||
if (config?.agents?.defaults?.memorySearch?.backend === "qmd") {
|
||||
qmdRefs.push("memorySearch.backend=qmd");
|
||||
}
|
||||
|
||||
// Check active-memory
|
||||
const activeQmd = config?.plugins?.entries?.["active-memory"]?.config?.qmd;
|
||||
if (activeQmd?.searchMode) {
|
||||
qmdRefs.push("active-memory.qmd.searchMode");
|
||||
}
|
||||
|
||||
console.log(` [DETECT] QMD references found: ${qmdRefs.length}`);
|
||||
for (const ref of qmdRefs) {
|
||||
console.log(` • ${ref}`);
|
||||
warnOnce(`qmd.${ref}`, "QMD memory engine is legacy. Migrate to Builtin MemoryCore.");
|
||||
}
|
||||
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-06: Honcho/LanceDB plugins detectable", () => {
|
||||
const extDir = "/opt/homebrew/lib/node_modules/openclaw/dist/extensions";
|
||||
const legacyPlugins = ["memory-honcho", "memory-lancedb"];
|
||||
let found = 0;
|
||||
|
||||
for (const name of legacyPlugins) {
|
||||
// Check if directory or plugin reference exists
|
||||
// These are NOT in the current install, which is expected (they're optional)
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const enabled = config?.plugins?.entries?.[name]?.enabled;
|
||||
console.log(` [DETECT] ${name}: ${enabled === true ? "enabled" : "not enabled"}`);
|
||||
|
||||
if (enabled === true) {
|
||||
warnOnce(`${name}.plugin`, `${name} memory plugin is legacy. Data readable during migration.`);
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` [DETECT] legacy memory plugins enabled: ${found}`);
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
|
||||
it("detect-07: commitments auto-infer detectable", () => {
|
||||
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
||||
const autoInfer = config?.commitments?.autoInfer;
|
||||
console.log(` [DETECT] commitments auto-infer: ${autoInfer ?? "not configured"}`);
|
||||
|
||||
if (autoInfer === true) {
|
||||
warnOnce("commitments.auto-infer",
|
||||
"Commitments auto-infer is deprecated. Use explicit cron tasks.");
|
||||
}
|
||||
assert.ok(true, "check completed without crash");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Integration tests ──────────────────────────────────
|
||||
describe("integration", () => {
|
||||
it("integ-01: check-deprecations script runs without error", () => {
|
||||
const scriptPath = join(import.meta.dirname, "../../scripts/check-deprecations.mjs");
|
||||
const result = spawnSync("node", [scriptPath], {
|
||||
encoding: "utf8",
|
||||
timeout: 15000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
console.log(" [INTEG] check-deprecations exit code:", result.status);
|
||||
console.log(" [INTEG] check-deprecations stdout:",
|
||||
result.stdout?.slice(0, 400) || "(empty)");
|
||||
|
||||
if (result.stderr) {
|
||||
console.log(" [INTEG] stderr:", result.stderr.slice(0, 200));
|
||||
}
|
||||
|
||||
// Script should never crash — always exit 0
|
||||
assert.equal(result.status, 0,
|
||||
"check-deprecations must exit 0 even with warnings");
|
||||
});
|
||||
|
||||
it("integ-02: deprecation warnings do NOT cause process exit", () => {
|
||||
// warnOnce writes to console.warn — it should never throw
|
||||
assert.doesNotThrow(() => {
|
||||
warnOnce("test.integration", "This is a test deprecation warning");
|
||||
}, "warnOnce must never throw");
|
||||
|
||||
assert.equal(getWarningCount(), 1);
|
||||
});
|
||||
|
||||
it("integ-03: all 8 deprecation categories checkable", () => {
|
||||
const categories = [
|
||||
"active-memory.blocking",
|
||||
"dreaming.rem-phase",
|
||||
"dreaming.dream-diary",
|
||||
"qmd.reference",
|
||||
"honcho.plugin",
|
||||
"lancedb.plugin",
|
||||
"memory-wiki.plugin",
|
||||
"commitments.auto-infer",
|
||||
];
|
||||
|
||||
// Each category should have a defined key pattern
|
||||
for (const cat of categories) {
|
||||
assert.ok(typeof cat === "string" && cat.length > 0,
|
||||
`category ${cat} must be a non-empty string`);
|
||||
}
|
||||
|
||||
console.log(` [INTEG] ${categories.length} deprecation categories defined`);
|
||||
assert.equal(categories.length, 8, "must cover all 8 deprecation categories");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* PR-1 Freeze Architecture Test
|
||||
*
|
||||
* Run: node --test test/architecture/freeze.test.mjs
|
||||
*
|
||||
* Tests:
|
||||
* 1. architecture-freeze-v2.md exists
|
||||
* 2. Document contains core/adapter/legacy/experimental sections
|
||||
* 3. Document contains 90-day legacy policy
|
||||
* 4. check-deprecations output includes replacement info
|
||||
* 5. check-deprecations output includes migration window
|
||||
* 6. production-check still passes
|
||||
* 7. guard still passes
|
||||
* 8. baseline still passes
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const ROOT = __dirname;
|
||||
const SCRIPTS = join(ROOT, "scripts");
|
||||
const DOCS = join(ROOT, "docs");
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────
|
||||
|
||||
function getFileSize(path) {
|
||||
try { return statSync(path).size; } catch { return 0; }
|
||||
}
|
||||
|
||||
describe("PR-1: Freeze Architecture", () => {
|
||||
|
||||
// ─── Test 1 ──────────────────────────────────────────
|
||||
it("architecture-freeze-v2.md exists and is non-empty", () => {
|
||||
const path = join(DOCS, "architecture-freeze-v2.md");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
const size = getFileSize(path);
|
||||
assert.ok(size > 1000, `File too small: ${size} bytes (expected >1000)`);
|
||||
});
|
||||
|
||||
// ─── Test 2 ──────────────────────────────────────────
|
||||
it("document contains core / adapter / legacy / experimental sections", () => {
|
||||
const content = readFileSync(join(DOCS, "architecture-freeze-v2.md"), "utf8");
|
||||
|
||||
const required = [
|
||||
"Core Module",
|
||||
"Adapter Module",
|
||||
"Legacy Module",
|
||||
"Experimental Module",
|
||||
];
|
||||
|
||||
for (const section of required) {
|
||||
assert.ok(content.includes(section),
|
||||
`Missing section: "${section}"`);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 3 ──────────────────────────────────────────
|
||||
it("document contains 90-day legacy migration policy", () => {
|
||||
const content = readFileSync(join(DOCS, "architecture-freeze-v2.md"), "utf8");
|
||||
|
||||
assert.ok(content.includes("90-day") || content.includes("90-Day") ||
|
||||
content.includes("90 day") || content.includes("Migration Window"),
|
||||
"Missing 90-day migration policy reference");
|
||||
|
||||
assert.ok(content.includes("2026-09-04"),
|
||||
"Missing removal target date 2026-09-04");
|
||||
});
|
||||
|
||||
// ─── Test 4 ──────────────────────────────────────────
|
||||
it("check-deprecations output includes replacement info", { timeout: 30000 }, () => {
|
||||
const result = spawnSync("node", [join(SCRIPTS, "check-deprecations.mjs")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 25000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-1000));
|
||||
|
||||
// Deprecation check always exits 0 (never blocks on deprecation count)
|
||||
assert.equal(result.status, 0, `Script crashed (exit ${result.status})`);
|
||||
|
||||
// Must contain replacement info in enhanced output
|
||||
assert.ok(
|
||||
output.includes("Replacement:") || output.includes("replacement"),
|
||||
"Missing replacement info in deprecation output"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Test 5 ──────────────────────────────────────────
|
||||
it("check-deprecations output includes migration window", { timeout: 30000 }, () => {
|
||||
const result = spawnSync("node", [join(SCRIPTS, "check-deprecations.mjs")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 25000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
|
||||
// Must contain migration window
|
||||
assert.ok(
|
||||
output.includes("Migration Window") || output.includes("migrationWindow") ||
|
||||
output.includes("Migration Timeline") || output.includes("Removal Target"),
|
||||
"Missing migration window info in deprecation output"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Test 6 ──────────────────────────────────────────
|
||||
it("production-check still passes after freeze", { timeout: 120000 }, () => {
|
||||
const result = spawnSync("bash", [join(SCRIPTS, "production-check.sh")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 110000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-800));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Production check failed after freeze (exit ${result.status}):\n${output.slice(-500)}`);
|
||||
assert.ok(output.includes("PRODUCTION CHECK PASSED") ||
|
||||
output.includes("Overall:"),
|
||||
"Production check summary not found");
|
||||
});
|
||||
|
||||
// ─── Test 7 ──────────────────────────────────────────
|
||||
it("architecture guard still passes after freeze", { timeout: 60000 }, () => {
|
||||
const result = spawnSync("bash", [join(SCRIPTS, "guard-all.sh")], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 55000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-500));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Guard failed after freeze (exit ${result.status}):\n${output.slice(-300)}`);
|
||||
});
|
||||
|
||||
// ─── Test 8 ──────────────────────────────────────────
|
||||
it("baseline tests still pass after freeze", { timeout: 60000 }, () => {
|
||||
const result = spawnSync("node", ["--test", "test/baseline/test-*.test.mjs"], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 55000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Baseline tests failed after freeze (exit ${result.status})`);
|
||||
assert.ok(output.includes("fail 0") || !output.includes("fail"),
|
||||
"Baseline tests have failures");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* PR-B Architecture Guard — Unified Test
|
||||
* Runs all 6 guards as node:test sub-tests.
|
||||
*
|
||||
* Run: node --test test/architecture/guard.test.mjs
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const SCRIPTS_DIR = join(__dirname, "scripts");
|
||||
|
||||
function runGuard(name, script, expectExit0 = true) {
|
||||
const result = spawnSync("node", [script], {
|
||||
cwd: __dirname,
|
||||
encoding: "utf8",
|
||||
timeout: 30000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
|
||||
return {
|
||||
name,
|
||||
exitCode: result.status,
|
||||
signal: result.signal,
|
||||
output: output.slice(-500), // Keep last 500 chars for report
|
||||
passed: result.status === 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe("PR-B: Architecture Guard", () => {
|
||||
it("Guard-01: Memory backend count ≤ allowed", () => {
|
||||
const r = runGuard("Memory Backend", join(SCRIPTS_DIR, "guard-memory-backend.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 1 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-02: Runtime core count ≤ 3", () => {
|
||||
const r = runGuard("Runtime Core", join(SCRIPTS_DIR, "guard-runtime-core.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 2 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-03: Tool path inventory (soft)", () => {
|
||||
const r = runGuard("Tool Path", join(SCRIPTS_DIR, "guard-tool-path.mjs"));
|
||||
console.log(r.output);
|
||||
// Soft guard — informational only
|
||||
assert.ok(r.exitCode === 0 || r.exitCode === null,
|
||||
`Guard 3 unexpected error (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-04: MEMORY.md direct write paths unchanged", () => {
|
||||
const r = runGuard("MEMORY.md Write", join(SCRIPTS_DIR, "guard-memory-write.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 4 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-05: Tool trace coverage (soft)", () => {
|
||||
const r = runGuard("Tool Trace", join(SCRIPTS_DIR, "guard-tool-trace.mjs"));
|
||||
console.log(r.output);
|
||||
// Soft guard — informational only
|
||||
assert.ok(r.exitCode === 0 || r.exitCode === null,
|
||||
`Guard 5 unexpected error (exit ${r.exitCode})`);
|
||||
});
|
||||
|
||||
it("Guard-06: Dreaming phase count ≤ 3", () => {
|
||||
const r = runGuard("Dreaming Phase", join(SCRIPTS_DIR, "guard-dreaming-phase.mjs"));
|
||||
console.log(r.output);
|
||||
assert.equal(r.exitCode, 0, `Guard 6 failed (exit ${r.exitCode})`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* PR-D Production Check — Architecture Test
|
||||
*
|
||||
* Run: node --test test/architecture/production-check.test.mjs
|
||||
*
|
||||
* Tests:
|
||||
* 1. production-check.sh file exists
|
||||
* 2. production-check.sh is executable
|
||||
* 3. smoke-test.mjs file exists
|
||||
* 4. package.json contains production-check script
|
||||
* 5. Smoke test runs independently
|
||||
* 6. production-check runs end-to-end and exits 0
|
||||
*/
|
||||
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { existsSync, statSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { execSync, spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const ROOT = __dirname;
|
||||
const SCRIPTS = join(ROOT, "scripts");
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────
|
||||
|
||||
function isExecutable(path) {
|
||||
try {
|
||||
const mode = statSync(path).mode;
|
||||
// Check owner execute bit
|
||||
return (mode & 0o100) !== 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────
|
||||
|
||||
describe("PR-D: Production Check Architecture", () => {
|
||||
// Test 1
|
||||
it("production-check.sh file exists", () => {
|
||||
const path = join(SCRIPTS, "production-check.sh");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
});
|
||||
|
||||
// Test 2
|
||||
it("production-check.sh is executable", () => {
|
||||
const path = join(SCRIPTS, "production-check.sh");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
assert.ok(isExecutable(path), `Not executable: ${path}. Run: chmod +x ${path}`);
|
||||
});
|
||||
|
||||
// Test 3
|
||||
it("smoke-test.mjs file exists", () => {
|
||||
const path = join(SCRIPTS, "smoke-test.mjs");
|
||||
assert.ok(existsSync(path), `Missing: ${path}`);
|
||||
});
|
||||
|
||||
// Test 4
|
||||
it("package.json contains production-check, smoke, check:deprecations, test:baseline, guard scripts", () => {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
||||
const scripts = pkg.scripts || {};
|
||||
|
||||
assert.ok(typeof scripts["production-check"] === "string",
|
||||
"Missing scripts.production-check in package.json");
|
||||
assert.ok(typeof scripts["smoke"] === "string",
|
||||
"Missing scripts.smoke in package.json");
|
||||
assert.ok(typeof scripts["check:deprecations"] === "string",
|
||||
"Missing scripts.check:deprecations in package.json");
|
||||
assert.ok(typeof scripts["test:baseline"] === "string",
|
||||
"Missing scripts.test:baseline in package.json");
|
||||
assert.ok(typeof scripts["guard"] === "string",
|
||||
"Missing scripts.guard in package.json");
|
||||
});
|
||||
|
||||
// Test 5
|
||||
it("smoke test runs independently and exits 0", { timeout: 30000 }, () => {
|
||||
const script = join(SCRIPTS, "smoke-test.mjs");
|
||||
const result = spawnSync("node", [script], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 25000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-800));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Smoke test failed (exit ${result.status}):\n${output.slice(-500)}`);
|
||||
assert.ok(output.includes("Smoke test PASSED") || output.includes("PASS"),
|
||||
`Expected "Smoke test PASSED" in output:\n${output.slice(-300)}`);
|
||||
});
|
||||
|
||||
// Test 6
|
||||
it("production-check runs end-to-end and exits 0", { timeout: 120000 }, () => {
|
||||
const script = join(SCRIPTS, "production-check.sh");
|
||||
const result = spawnSync("bash", [script], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
timeout: 110000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
|
||||
const output = result.stdout + result.stderr;
|
||||
console.log(output.slice(-1200));
|
||||
|
||||
assert.equal(result.status, 0,
|
||||
`Production check failed (exit ${result.status}):\n${output.slice(-800)}`);
|
||||
|
||||
// Verify all 6 steps ran
|
||||
const steps = [
|
||||
"Step 1: Environment Check",
|
||||
"Step 2: Baseline Tests",
|
||||
"Step 3: Architecture Guards",
|
||||
"Step 4: Deprecation Check",
|
||||
"Step 5: Smoke Test",
|
||||
"Step 6: Summary Report",
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
assert.ok(output.includes(step),
|
||||
`Missing step in output: "${step}"`);
|
||||
}
|
||||
|
||||
assert.ok(output.includes("Overall:"),
|
||||
"Missing Overall summary");
|
||||
assert.ok(output.includes("Duration:"),
|
||||
"Missing Duration in summary");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user