229 lines
7.9 KiB
JavaScript
229 lines
7.9 KiB
JavaScript
import { execFile } from "node:child_process";
|
|
import { lstat, mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const workspaceRoot = process.cwd();
|
|
const evidenceRoot = path.join(workspaceRoot, "docs/evidence/devtool-import");
|
|
const gameVersionId = "simulation-fixture-v1";
|
|
const target = "wechat_minigame";
|
|
const projectPath = `dist/packages/${gameVersionId}/${target}`;
|
|
const outPath = `docs/evidence/devtool-import/${target}-${gameVersionId}.json`;
|
|
const blockerPath = path.join(evidenceRoot, `${target}-${gameVersionId}.blocker.json`);
|
|
const evidencePath = path.join(workspaceRoot, outPath);
|
|
const logPath = `docs/evidence/devtool-import/${target}-${gameVersionId}.manual.log`;
|
|
const blockerLogPath = path.join(evidenceRoot, `${target}-${gameVersionId}.blocked.log`);
|
|
const outsideRoots = [];
|
|
const swappedPaths = [];
|
|
|
|
beforeEach(async () => {
|
|
await rm(path.join(workspaceRoot, "dist/packages/simulation-fixture-v1"), { recursive: true, force: true });
|
|
await restoreSwappedPaths();
|
|
await cleanGeneratedEvidenceFiles();
|
|
await execFileAsync("node", ["scripts/build-minigame.mjs", "--target", "all", "--fixture", "simulation"], {
|
|
cwd: workspaceRoot,
|
|
maxBuffer: 1024 * 1024 * 8
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(path.join(workspaceRoot, "dist/packages/simulation-fixture-v1"), { recursive: true, force: true });
|
|
await restoreSwappedPaths();
|
|
await cleanGeneratedEvidenceFiles();
|
|
for (const outsideRoot of outsideRoots.splice(0)) {
|
|
await rm(outsideRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("smoke-devtool-import CLI", () => {
|
|
it("fails closed and writes a blocker when manual passed evidence is not explicitly allowed", async () => {
|
|
await assert.rejects(
|
|
execFileAsync(
|
|
"node",
|
|
[
|
|
"scripts/smoke-devtool-import.mjs",
|
|
"--target",
|
|
target,
|
|
"--game-version-id",
|
|
gameVersionId,
|
|
"--project",
|
|
projectPath,
|
|
"--out",
|
|
outPath
|
|
],
|
|
{ cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 8 }
|
|
),
|
|
(error) => {
|
|
assert.equal(error.code, 1);
|
|
const output = JSON.parse(error.stdout);
|
|
assert.equal(output.status, "NO_GO");
|
|
assert.equal(output.result, "blocked");
|
|
assert.equal(output.blockerPath, `docs/evidence/devtool-import/${target}-${gameVersionId}.blocker.json`);
|
|
return true;
|
|
}
|
|
);
|
|
|
|
const blocker = JSON.parse(await readFile(blockerPath, "utf8"));
|
|
assert.equal(blocker.result, "blocked");
|
|
assert.equal(blocker.reasonCode, "DEVTOOL_UNAVAILABLE");
|
|
});
|
|
|
|
it("fails closed before writing default blocker log when evidence root is a symlink", async () => {
|
|
const outsideRoot = await mkdtemp(path.join(tmpdir(), "huijing-s5-smoke-devtool-outside-"));
|
|
outsideRoots.push(outsideRoot);
|
|
await swapPathForSymlink(evidenceRoot, outsideRoot);
|
|
|
|
await assert.rejects(
|
|
execFileAsync(
|
|
"node",
|
|
[
|
|
"scripts/smoke-devtool-import.mjs",
|
|
"--target",
|
|
target,
|
|
"--game-version-id",
|
|
gameVersionId,
|
|
"--project",
|
|
projectPath,
|
|
"--out",
|
|
outPath
|
|
],
|
|
{ cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 8 }
|
|
),
|
|
(error) => {
|
|
assert.equal(error.code, 1);
|
|
const output = JSON.parse(error.stdout);
|
|
assert.equal(output.status, "FAIL");
|
|
assert.equal(output.reasonCode, "DEVTOOL_IMPORT_PATH_SYMLINK_UNSUPPORTED");
|
|
assert.equal(output.path, "evidenceRoot");
|
|
return true;
|
|
}
|
|
);
|
|
|
|
await assert.rejects(readFile(path.join(outsideRoot, `${target}-${gameVersionId}.blocked.log`), "utf8"), { code: "ENOENT" });
|
|
await assert.rejects(readFile(path.join(outsideRoot, `${target}-${gameVersionId}.blocker.json`), "utf8"), { code: "ENOENT" });
|
|
});
|
|
|
|
it("fails closed before writing default blocker log when evidence root parent is a symlink", async () => {
|
|
const outsideRoot = await mkdtemp(path.join(tmpdir(), "huijing-s5-smoke-devtool-parent-outside-"));
|
|
outsideRoots.push(outsideRoot);
|
|
const symlinkParent = path.join(workspaceRoot, "docs/evidence");
|
|
await swapPathForSymlink(symlinkParent, outsideRoot);
|
|
|
|
await assert.rejects(
|
|
execFileAsync(
|
|
"node",
|
|
[
|
|
"scripts/smoke-devtool-import.mjs",
|
|
"--target",
|
|
target,
|
|
"--game-version-id",
|
|
gameVersionId,
|
|
"--project",
|
|
projectPath,
|
|
"--out",
|
|
outPath
|
|
],
|
|
{ cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 8 }
|
|
),
|
|
(error) => {
|
|
assert.equal(error.code, 1);
|
|
const output = JSON.parse(error.stdout);
|
|
assert.equal(output.status, "FAIL");
|
|
assert.equal(output.reasonCode, "DEVTOOL_IMPORT_PATH_SYMLINK_UNSUPPORTED");
|
|
assert.equal(output.path, "evidenceRoot");
|
|
return true;
|
|
}
|
|
);
|
|
|
|
await assert.rejects(readFile(path.join(outsideRoot, `devtool-import/${target}-${gameVersionId}.blocked.log`), "utf8"), { code: "ENOENT" });
|
|
await assert.rejects(readFile(path.join(outsideRoot, `devtool-import/${target}-${gameVersionId}.blocker.json`), "utf8"), { code: "ENOENT" });
|
|
});
|
|
|
|
it("records passed evidence only with explicit audited manual evidence inputs", async () => {
|
|
await mkdir(evidenceRoot, { recursive: true });
|
|
await writeFile(path.join(workspaceRoot, logPath), "manual devtool import passed\n", "utf8");
|
|
|
|
const { stdout } = await execFileAsync(
|
|
"node",
|
|
[
|
|
"scripts/smoke-devtool-import.mjs",
|
|
"--target",
|
|
target,
|
|
"--game-version-id",
|
|
gameVersionId,
|
|
"--project",
|
|
projectPath,
|
|
"--out",
|
|
outPath,
|
|
"--allow-manual-passed-evidence",
|
|
"--tool-name",
|
|
"WeChat DevTools",
|
|
"--tool-version",
|
|
"1.06.2504010",
|
|
"--log",
|
|
logPath
|
|
],
|
|
{ cwd: workspaceRoot, maxBuffer: 1024 * 1024 * 8 }
|
|
);
|
|
const output = JSON.parse(stdout);
|
|
|
|
assert.equal(output.status, "PASS");
|
|
assert.equal(output.result, "passed");
|
|
assert.equal(output.evidencePath, outPath);
|
|
|
|
const evidence = JSON.parse(await readFile(evidencePath, "utf8"));
|
|
assert.equal(evidence.result, "passed");
|
|
assert.equal(evidence.target, target);
|
|
assert.equal(evidence.toolName, "WeChat DevTools");
|
|
assert.equal(evidence.importedProjectPath, projectPath);
|
|
});
|
|
});
|
|
|
|
async function cleanGeneratedEvidenceFiles() {
|
|
await rm(blockerPath, { force: true });
|
|
await rm(blockerLogPath, { force: true });
|
|
await rm(evidencePath, { force: true });
|
|
await rm(path.join(workspaceRoot, logPath), { force: true });
|
|
}
|
|
|
|
async function swapPathForSymlink(targetPath, outsideRoot) {
|
|
const backupPath = await mkdtemp(path.join(tmpdir(), "huijing-s5-smoke-devtool-backup-"));
|
|
await rm(backupPath, { recursive: true, force: true });
|
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
|
|
let hasBackup = true;
|
|
try {
|
|
await rename(targetPath, backupPath);
|
|
} catch (error) {
|
|
if (error?.code !== "ENOENT") throw error;
|
|
hasBackup = false;
|
|
}
|
|
|
|
await symlink(outsideRoot, targetPath);
|
|
swappedPaths.push({ targetPath, backupPath, hasBackup });
|
|
}
|
|
|
|
async function restoreSwappedPaths() {
|
|
for (const swap of swappedPaths.splice(0).reverse()) {
|
|
try {
|
|
const stats = await lstat(swap.targetPath);
|
|
if (stats.isSymbolicLink()) {
|
|
await rm(swap.targetPath, { force: true });
|
|
}
|
|
} catch (error) {
|
|
if (error?.code !== "ENOENT") throw error;
|
|
}
|
|
|
|
if (swap.hasBackup) {
|
|
await mkdir(path.dirname(swap.targetPath), { recursive: true });
|
|
await rename(swap.backupPath, swap.targetPath);
|
|
} else {
|
|
await rm(swap.backupPath, { recursive: true, force: true });
|
|
}
|
|
}
|
|
}
|