Remove repo reports and add recurrence guard
This commit is contained in:
@@ -44,7 +44,6 @@ async function makeFixture({
|
||||
} = {}) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-desired-state-"));
|
||||
await mkdir(path.join(root, "deploy/k8s/base"), { recursive: true });
|
||||
await mkdir(path.join(root, "reports/dev-gate"), { recursive: true });
|
||||
const image = `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`;
|
||||
const catalogImage = `127.0.0.1:5000/hwlab/${serviceId}:${catalogImageTag}`;
|
||||
const workloadImage = `127.0.0.1:5000/hwlab/${serviceId}:${workloadTag}`;
|
||||
|
||||
@@ -20,12 +20,13 @@ import { requireDevCdTransactionForSideEffect } from "./src/dev-cd-transaction-g
|
||||
import { runDevBaseImagePreflight } from "./src/dev-base-image-preflight.mjs";
|
||||
import { probeRegistryCapabilities } from "./src/registry-capabilities.mjs";
|
||||
import { inspectCloudWebDistFreshness } from "../web/hwlab-cloud-web/scripts/dist-contract.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultRegistryPrefix =
|
||||
process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
||||
const defaultBaseImage = process.env.HWLAB_DEV_BASE_IMAGE || null;
|
||||
const defaultReportPath = "reports/dev-gate/dev-artifacts.json";
|
||||
const defaultReportPath = tempReportPath("dev-artifacts.json");
|
||||
const catalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const deployPath = "deploy/deploy.json";
|
||||
|
||||
@@ -103,8 +104,8 @@ function printHelp() {
|
||||
" --registry-prefix PREFIX default: 127.0.0.1:5000/hwlab",
|
||||
" --base-image IMAGE override HWLAB_DEV_BASE_IMAGE for this run; must already be local",
|
||||
" --services LIST comma-separated service IDs; default: all frozen DEV services",
|
||||
" --report PATH default: reports/dev-gate/dev-artifacts.json",
|
||||
" --no-report print JSON without updating the report file"
|
||||
` --report PATH default: ${defaultReportPath}`,
|
||||
" --no-report print JSON without updating the temporary artifact JSON"
|
||||
].join("\n"));
|
||||
}
|
||||
|
||||
@@ -849,7 +850,6 @@ function dockerfile(baseImage, port) {
|
||||
"COPY tools ./tools",
|
||||
"COPY skills ./skills",
|
||||
"COPY deploy ./deploy",
|
||||
"COPY reports ./reports",
|
||||
"RUN mkdir -p /workspace /codex-home && rm -rf /workspace/hwlab && ln -s /app /workspace/hwlab && chmod -R a+rwX /app /workspace /codex-home && test -x /app/node_modules/.bin/codex && /app/node_modules/.bin/codex --version >/tmp/hwlab-codex-version.txt && (test -x /app/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex || test -x /app/node_modules/@openai/codex-linux-arm64/vendor/aarch64-unknown-linux-musl/codex/codex)",
|
||||
`RUN node -e "const fs=require('node:fs'); fs.writeFileSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', Buffer.from('${runtimeScriptBase64()}', 'base64')); fs.chmodSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', 0o755);"`,
|
||||
`EXPOSE ${port}`,
|
||||
@@ -1305,7 +1305,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-artifact-publish.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-artifacts.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-cd/dev-artifacts.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#35",
|
||||
taskId: "dev-artifact-publish",
|
||||
@@ -1417,7 +1417,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
|
||||
}
|
||||
|
||||
async function writeReport(relativePath, report) {
|
||||
const absolutePath = path.join(repoRoot, relativePath);
|
||||
const absolutePath = ensureNotRepoReportsPath(repoRoot, relativePath, "--report");
|
||||
await mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await writeFile(absolutePath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
printSmokeHelp,
|
||||
runDevCloudWorkbenchLayoutSmoke
|
||||
} from "./src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
@@ -98,7 +99,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
const report = args.help
|
||||
? {
|
||||
...printSmokeHelp(),
|
||||
command: "node scripts/dev-cloud-workbench-layout-smoke.mjs [--static|--build|--live --url http://74.48.78.17:16666/] [--report reports/dev-gate/dev-cloud-workbench-layout.json]",
|
||||
command: `node scripts/dev-cloud-workbench-layout-smoke.mjs [--static|--build|--live --url http://74.48.78.17:16666/] [--report ${tempReportPath("dev-cloud-workbench-layout.json")}]`,
|
||||
layoutNotes: [
|
||||
"默认 --static 使用 source/static 本地静态服务。",
|
||||
"--build 会先运行 web/hwlab-cloud-web/scripts/build.mjs,再检查本地 dist 构建产物。",
|
||||
@@ -108,7 +109,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
}
|
||||
: await runDevCloudWorkbenchLayoutSmoke(args);
|
||||
if (args.reportPath && report.status !== "usage") {
|
||||
const reportPath = path.resolve(process.cwd(), args.reportPath);
|
||||
const reportPath = ensureNotRepoReportsPath(repoRoot, path.resolve(process.cwd(), args.reportPath), "--report");
|
||||
report.artifacts = { ...(report.artifacts ?? {}), reportPath };
|
||||
await mkdir(path.dirname(reportPath), { recursive: true });
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
runM3StatusFixtureSmoke,
|
||||
runDevCloudWorkbenchSmoke
|
||||
} from "./src/dev-cloud-workbench-smoke-lib.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
@@ -20,8 +21,8 @@ export function decideSmokeReportWrite(args, report, options = {}) {
|
||||
}
|
||||
const root = options.repoRoot ?? repoRoot;
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const reportPath = path.resolve(cwd, args.reportPath);
|
||||
const liveReportPath = path.resolve(root, "reports/dev-gate/dev-cloud-workbench-live.json");
|
||||
const reportPath = ensureNotRepoReportsPath(root, path.resolve(cwd, args.reportPath), "--report");
|
||||
const liveReportPath = tempReportPath("dev-cloud-workbench-live.json");
|
||||
const displayPath = displayReportPath(reportPath, root);
|
||||
if (report.status === "skip") {
|
||||
return {
|
||||
|
||||
@@ -79,11 +79,11 @@ test("source/default workbench report cannot claim DEV-LIVE and documents the co
|
||||
assert.equal(report.safety.devLive, false);
|
||||
assert.equal(report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --source"), true);
|
||||
assert.equal(
|
||||
report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json"),
|
||||
report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-live.json"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json"),
|
||||
report.validationCommands.includes("node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-live.json"),
|
||||
false
|
||||
);
|
||||
assert.equal(report.checks.find((check) => check.id === "code-agent-long-timeout-contract")?.status, "pass");
|
||||
@@ -131,7 +131,7 @@ test("workbench smoke CLI treats skipped browser evidence as blocked and refuses
|
||||
summary: "Mobile browser smoke skipped because Playwright is unavailable."
|
||||
};
|
||||
const decision = decideSmokeReportWrite(
|
||||
{ reportPath: "reports/dev-gate/dev-cloud-workbench-mobile.json" },
|
||||
{ reportPath: "/tmp/hwlab-dev-gate/dev-cloud-workbench-mobile.json" },
|
||||
report,
|
||||
{ repoRoot: "/repo", cwd: "/repo" }
|
||||
);
|
||||
@@ -148,14 +148,14 @@ test("workbench smoke CLI refuses to overwrite live report with non-live fixture
|
||||
mode: "local-agent-fixture-browser"
|
||||
};
|
||||
const decision = decideSmokeReportWrite(
|
||||
{ reportPath: "reports/dev-gate/dev-cloud-workbench-live.json" },
|
||||
{ reportPath: "/tmp/hwlab-dev-gate/dev-cloud-workbench-live.json" },
|
||||
report,
|
||||
{ repoRoot: "/repo", cwd: "/repo" }
|
||||
);
|
||||
|
||||
assert.equal(decision.status, "blocked");
|
||||
assert.equal(decision.write, false);
|
||||
assert.match(decision.summary, /Refusing to overwrite reports\/dev-gate\/dev-cloud-workbench-live\.json/u);
|
||||
assert.match(decision.summary, /Refusing to overwrite \/tmp\/hwlab-dev-gate\/dev-cloud-workbench-live\.json/u);
|
||||
assert.equal(smokeCliExitCode(report, decision), 2);
|
||||
});
|
||||
|
||||
@@ -165,14 +165,14 @@ test("workbench smoke CLI allows live evidence to write the live report", () =>
|
||||
mode: "live"
|
||||
};
|
||||
const decision = decideSmokeReportWrite(
|
||||
{ reportPath: "reports/dev-gate/dev-cloud-workbench-live.json" },
|
||||
{ reportPath: "/tmp/hwlab-dev-gate/dev-cloud-workbench-live.json" },
|
||||
report,
|
||||
{ repoRoot: "/repo", cwd: "/repo" }
|
||||
);
|
||||
|
||||
assert.equal(decision.status, "pass");
|
||||
assert.equal(decision.write, true);
|
||||
assert.equal(decision.reportPath, "/repo/reports/dev-gate/dev-cloud-workbench-live.json");
|
||||
assert.equal(decision.reportPath, "/tmp/hwlab-dev-gate/dev-cloud-workbench-live.json");
|
||||
assert.equal(smokeCliExitCode(report, decision), 0);
|
||||
});
|
||||
|
||||
@@ -204,10 +204,10 @@ test("dedicated layout smoke CLI supports static build and DEV live shorthand",
|
||||
assert.equal(local.mode, "layout");
|
||||
assert.equal(local.urlExplicit, undefined);
|
||||
|
||||
const build = parseLayoutSmokeArgs(["--build", "--report", "reports/dev-gate/dev-cloud-workbench-layout.json"]);
|
||||
const build = parseLayoutSmokeArgs(["--build", "--report", "/tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json"]);
|
||||
assert.equal(build.mode, "layout");
|
||||
assert.equal(build.build, true);
|
||||
assert.equal(build.reportPath, "reports/dev-gate/dev-cloud-workbench-layout.json");
|
||||
assert.equal(build.reportPath, "/tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json");
|
||||
|
||||
const liveDefault = parseLayoutSmokeArgs(["--live"]);
|
||||
assert.equal(liveDefault.mode, "layout");
|
||||
@@ -843,15 +843,15 @@ test("repo-owned web checks expose source build and DEV live layout smoke gates"
|
||||
assert.equal(rootPackage.scripts["web:check"], "node web/hwlab-cloud-web/scripts/check.mjs");
|
||||
assert.equal(
|
||||
rootPackage.scripts["web:layout"],
|
||||
"node scripts/dev-cloud-workbench-layout-smoke.mjs --static --report reports/dev-gate/dev-cloud-workbench-layout.json"
|
||||
"node scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json"
|
||||
);
|
||||
assert.equal(
|
||||
rootPackage.scripts["web:layout:build"],
|
||||
"node scripts/dev-cloud-workbench-layout-smoke.mjs --build --report reports/dev-gate/dev-cloud-workbench-layout-build.json"
|
||||
"node scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json"
|
||||
);
|
||||
assert.equal(
|
||||
rootPackage.scripts["web:layout:live"],
|
||||
"node scripts/dev-cloud-workbench-layout-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-layout-live.json"
|
||||
"node scripts/dev-cloud-workbench-layout-smoke.mjs --live --url http://74.48.78.17:16666/ --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-live.json"
|
||||
);
|
||||
assert.match(rootPackage.scripts.check, /node --check scripts\/dev-cloud-workbench-layout-smoke\.mjs/u);
|
||||
assert.match(rootPackage.scripts.check, /node web\/hwlab-cloud-web\/scripts\/check\.mjs/u);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
buildReport,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
renderMarkdown,
|
||||
repoRoot
|
||||
} from "./src/dev-evidence-blocker-aggregator.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
function hasFlag(name) {
|
||||
return process.argv.includes(name);
|
||||
@@ -15,11 +15,17 @@ function hasFlag(name) {
|
||||
|
||||
try {
|
||||
const report = await buildReport();
|
||||
const wroteReports = hasFlag("--write-report");
|
||||
const reportFlagIndex = process.argv.indexOf("--report");
|
||||
const markdownFlagIndex = process.argv.indexOf("--markdown-report");
|
||||
const wroteReports = reportFlagIndex !== -1 || markdownFlagIndex !== -1;
|
||||
const jsonPath = reportFlagIndex !== -1
|
||||
? ensureNotRepoReportsPath(repoRoot, process.argv[reportFlagIndex + 1], "--report")
|
||||
: ensureNotRepoReportsPath(repoRoot, tempReportPath("dev-m5-gate-aggregator-v2.json"), "--report");
|
||||
const markdownPath = markdownFlagIndex !== -1
|
||||
? ensureNotRepoReportsPath(repoRoot, process.argv[markdownFlagIndex + 1], "--markdown-report")
|
||||
: ensureNotRepoReportsPath(repoRoot, tempReportPath("dev-m5-gate-aggregator-v2.md"), "--markdown-report");
|
||||
|
||||
if (wroteReports) {
|
||||
const jsonPath = path.join(repoRoot, "reports/dev-gate/dev-m5-gate-aggregator-v2.json");
|
||||
const markdownPath = path.join(repoRoot, "reports/dev-gate/dev-m5-gate-aggregator-v2.md");
|
||||
await writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
await writeFile(markdownPath, renderMarkdown(report));
|
||||
}
|
||||
@@ -32,8 +38,8 @@ try {
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
...formatCheckSummary(report),
|
||||
wrote: [
|
||||
"reports/dev-gate/dev-m5-gate-aggregator-v2.json",
|
||||
"reports/dev-gate/dev-m5-gate-aggregator-v2.md"
|
||||
jsonPath,
|
||||
markdownPath
|
||||
]
|
||||
})}\n`);
|
||||
} else {
|
||||
|
||||
@@ -11,9 +11,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultReportPath = "reports/dev-gate/dev-m3-hardware-loop.json";
|
||||
const defaultReportPath = tempReportPath("dev-m3-hardware-loop.json");
|
||||
const namespace = "hwlab-dev";
|
||||
const issue = "pikasTech/HWLAB#38";
|
||||
const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml";
|
||||
@@ -203,9 +204,7 @@ function relativePath(absolutePath) {
|
||||
}
|
||||
|
||||
function resolveReportPath(value) {
|
||||
const candidate = path.resolve(repoRoot, value ?? defaultReportPath);
|
||||
assert.ok(candidate.startsWith(`${repoRoot}${path.sep}`), "report path must stay inside repository");
|
||||
return candidate;
|
||||
return ensureNotRepoReportsPath(repoRoot, value ?? defaultReportPath, "report path");
|
||||
}
|
||||
|
||||
function currentCommit() {
|
||||
@@ -1404,7 +1403,7 @@ async function runLiveM3Targets(targets) {
|
||||
function baseReport({ commitId, observedAt }) {
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-m3-hardware-loop-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-m3-hardware-loop.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/dev-m3-hardware-loop.json",
|
||||
reportVersion: "v1",
|
||||
issue,
|
||||
taskId: "dev-m3-hardware-loop",
|
||||
@@ -1618,8 +1617,9 @@ async function ensureContractFiles() {
|
||||
}
|
||||
|
||||
async function writeReport(report, reportPath) {
|
||||
await mkdir(path.dirname(reportPath), { recursive: true });
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(repoRoot, reportPath, "report path");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "../internal/agent/runtime.mjs";
|
||||
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
import {
|
||||
collectD601K3sNativeEvidence,
|
||||
collectPublicEntrypoints,
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixedNow = () => "2026-05-22T00:00:00.000Z";
|
||||
const requiredDocs = ["docs/dev-acceptance-matrix.md", "docs/dev-m4-agent-loop.md"];
|
||||
const reportPath = "reports/dev-gate/dev-m4-agent-loop.json";
|
||||
const reportPath = tempReportPath("dev-m4-agent-loop.json");
|
||||
const preflightCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
|
||||
const dryRunCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run";
|
||||
|
||||
@@ -64,7 +65,7 @@ async function readText(relativePath) {
|
||||
}
|
||||
|
||||
async function writeJSON(relativePath, value) {
|
||||
const absolutePath = path.join(repoRoot, relativePath);
|
||||
const absolutePath = ensureNotRepoReportsPath(repoRoot, relativePath, "--report");
|
||||
await mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
@@ -275,7 +276,7 @@ function buildReport(
|
||||
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-m4-agent-loop.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-m4-agent-loop.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#37",
|
||||
taskId: "dev-m4-agent-loop",
|
||||
|
||||
@@ -7,8 +7,8 @@ import { loadMvpGateSummary } from "../internal/mvp-gate/summary.mjs";
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outputPath = path.join(repoRoot, "web/hwlab-cloud-web/gate-summary.mjs");
|
||||
const summary = loadMvpGateSummary(repoRoot);
|
||||
const body = `// Generated from reports/dev-gate/dev-mvp-gate-report.json and fixtures/mvp/m5-e2e/dry-run-plan.json.
|
||||
// Run node scripts/export-web-gate-summary.mjs after changing those fixtures.
|
||||
const body = `// Generated from source:mvp-gate-summary and fixtures/mvp/m5-e2e/dry-run-plan.json.
|
||||
// Run node scripts/export-web-gate-summary.mjs after changing that source summary or fixture.
|
||||
export const gateSummary = ${JSON.stringify(summary, null, 2)};
|
||||
`;
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
ENVIRONMENT_DEV,
|
||||
SERVICE_IDS
|
||||
} from "../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixturePath = path.join(repoRoot, "fixtures/dev-deploy-smoke/dev-deploy-smoke.json");
|
||||
const defaultActiveReportPath = path.join(repoRoot, "reports/dev-gate/dev-m2-deploy-smoke-active.json");
|
||||
const defaultActiveReportPath = tempReportPath("dev-m2-deploy-smoke-active.json");
|
||||
const legacyPublicEndpoint = "http://74.48.78.17:6667";
|
||||
const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
||||
const digestPattern = /^(sha256:[a-f0-9]{64}|not_applicable)$/;
|
||||
@@ -34,6 +35,7 @@ function parseArgs(argv) {
|
||||
const flags = new Set();
|
||||
const options = {
|
||||
reportPath: defaultActiveReportPath,
|
||||
writeReport: false,
|
||||
timeoutMs: 5000
|
||||
};
|
||||
const rest = [];
|
||||
@@ -43,7 +45,8 @@ function parseArgs(argv) {
|
||||
flags.add(arg);
|
||||
index += 1;
|
||||
assert.ok(argv[index], "--report requires a path");
|
||||
options.reportPath = path.resolve(process.cwd(), argv[index]);
|
||||
options.reportPath = argv[index];
|
||||
options.writeReport = true;
|
||||
} else if (arg === "--timeout-ms") {
|
||||
flags.add(arg);
|
||||
index += 1;
|
||||
@@ -292,7 +295,7 @@ function buildActiveReport({ fixture, probes, generatedAt, commitId }) {
|
||||
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-m2-deploy-smoke-active.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/dev-m2-deploy-smoke-active.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#23",
|
||||
taskId: "m2-dev-deploy-smoke",
|
||||
@@ -319,7 +322,7 @@ function buildActiveReport({ fixture, probes, generatedAt, commitId }) {
|
||||
validationCommands: [
|
||||
"node --check scripts/m2-dev-deploy-smoke.mjs",
|
||||
"node scripts/m2-dev-deploy-smoke.mjs --dry-run",
|
||||
"node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --write-report",
|
||||
`node scripts/m2-dev-deploy-smoke.mjs --live --confirm-dev --confirmed-non-production --report ${defaultActiveReportPath}`,
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
@@ -395,8 +398,9 @@ function buildActiveReport({ fixture, probes, generatedAt, commitId }) {
|
||||
}
|
||||
|
||||
async function writeActiveReport(report, reportPath) {
|
||||
await mkdir(path.dirname(reportPath), { recursive: true });
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(repoRoot, reportPath, "--report");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function runLiveSmoke(fixture, options) {
|
||||
@@ -449,7 +453,7 @@ async function main() {
|
||||
process.stdout.write(`[m2-smoke] mode=${isLive ? "live" : "dry-run"} endpoint=${fixture.endpoint}\n`);
|
||||
if (isLive) {
|
||||
const report = await runLiveSmoke(fixture, options);
|
||||
if (flags.has("--write-report")) {
|
||||
if (options.writeReport) {
|
||||
await writeActiveReport(report, options.reportPath);
|
||||
process.stdout.write(`[m2-smoke] wrote ${path.relative(repoRoot, options.reportPath)}\n`);
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
resolveDevArtifactServices,
|
||||
serviceInventoryFromServices
|
||||
} from "./src/dev-artifact-services.mjs";
|
||||
import { tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const catalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const deployPath = "deploy/deploy.json";
|
||||
const workloadsPath = "deploy/k8s/base/workloads.yaml";
|
||||
const defaultPublishReportPath = "reports/dev-gate/dev-artifacts.json";
|
||||
const defaultPublishReportPath = tempReportPath("dev-artifacts.json");
|
||||
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
@@ -49,7 +50,7 @@ function parseArgs(argv) {
|
||||
|
||||
if (!args.help) {
|
||||
assert.notEqual(args.blocked && Boolean(args.publishReportPath), true, "--blocked and --publish-report are mutually exclusive");
|
||||
assert.ok(args.blocked || args.publishReportPath, "choose --blocked or --publish-report reports/dev-gate/dev-artifacts.json");
|
||||
assert.ok(args.blocked || args.publishReportPath, `choose --blocked or --publish-report ${defaultPublishReportPath}`);
|
||||
}
|
||||
|
||||
return args;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const reportsDirName = "reports";
|
||||
const reportsPath = path.join(repoRoot, reportsDirName);
|
||||
|
||||
function gitLines(args) {
|
||||
try {
|
||||
const output = execFileSync("git", args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
}).trim();
|
||||
return output ? output.split(/\r?\n/u).filter(Boolean) : [];
|
||||
} catch (error) {
|
||||
const stderr = error?.stderr?.toString?.().trim();
|
||||
throw new Error(stderr || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function statusReportPaths() {
|
||||
return gitLines(["status", "--porcelain=v1", "--untracked-files=all", "--", reportsDirName])
|
||||
.map((line) => line.slice(3).trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const tracked = gitLines(["ls-files", reportsDirName]);
|
||||
const staged = gitLines(["diff", "--cached", "--name-only", "--", reportsDirName]);
|
||||
const workingTree = statusReportPaths();
|
||||
const exists = existsSync(reportsPath);
|
||||
const failures = [];
|
||||
|
||||
if (exists) failures.push("forbidden repository report directory exists in the worktree");
|
||||
if (tracked.length > 0) failures.push(`tracked reports files: ${tracked.slice(0, 8).join(", ")}`);
|
||||
if (staged.length > 0) failures.push(`staged reports files: ${staged.slice(0, 8).join(", ")}`);
|
||||
if (workingTree.length > 0) failures.push(`working-tree reports files: ${workingTree.slice(0, 8).join(", ")}`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
ok: false,
|
||||
status: "blocked",
|
||||
taskId: "repo-reports-guard",
|
||||
policy: "repository report directories are forbidden; use GitHub issue/PR comments, /tmp, .state, or CI artifacts",
|
||||
failures
|
||||
}, null, 2)}\n`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
status: "pass",
|
||||
taskId: "repo-reports-guard",
|
||||
checked: ["directory", "tracked", "staged", "working-tree"],
|
||||
policy: "repository report directory is absent"
|
||||
})}\n`);
|
||||
}
|
||||
@@ -7,14 +7,15 @@ import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT } from "../../internal/protocol/index.mjs";
|
||||
import { inspectCloudWebDistFreshness } from "../../web/hwlab-cloud-web/scripts/dist-contract.mjs";
|
||||
import { buildDesiredStatePlan } from "./deploy-desired-state-plan.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
const defaultArtifactReport = "reports/dev-gate/dev-artifacts.json";
|
||||
const defaultArtifactReport = tempReportPath("dev-artifacts.json");
|
||||
const defaultArtifactCatalog = "deploy/artifact-catalog.dev.json";
|
||||
const defaultRuntimeReport = "reports/dev-gate/dev-cloud-workbench-live.json";
|
||||
const defaultEdgeReport = "reports/dev-gate/dev-edge-health.json";
|
||||
const defaultOutputReport = "reports/dev-gate/dev-artifact-runtime-readiness.json";
|
||||
const defaultRuntimeReport = tempReportPath("dev-cloud-workbench-live.json");
|
||||
const defaultEdgeReport = tempReportPath("dev-edge-health.json");
|
||||
const defaultOutputReport = tempReportPath("dev-artifact-runtime-readiness.json");
|
||||
const defaultTargetRef = "origin/main";
|
||||
const guardCommand = `node scripts/artifact-runtime-readiness-guard.mjs --target-ref ${defaultTargetRef} --check --no-report`;
|
||||
const expectedBlockedGuardCommand = `${guardCommand} --expect-blocked`;
|
||||
@@ -104,7 +105,7 @@ export function evaluateArtifactRuntimeReadiness({
|
||||
pass: commitsMatch(artifact.sourceCommitId, catalog.commitId),
|
||||
type: "observability_blocker",
|
||||
summary: `Artifact report source=${artifact.shortCommitId}; catalog=${catalog.shortCommitId}.`,
|
||||
nextTask: "Keep reports/dev-gate/dev-artifacts.json and deploy/artifact-catalog.dev.json aligned to the same artifact source."
|
||||
nextTask: `Keep ${defaultArtifactReport} and deploy/artifact-catalog.dev.json aligned to the same artifact source.`
|
||||
});
|
||||
addCheck(checks, {
|
||||
id: "desired-state-internal-valid",
|
||||
@@ -237,7 +238,7 @@ export function buildArtifactRuntimeReadinessReport({
|
||||
});
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-artifact-runtime-readiness.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/dev-artifact-runtime-readiness.json",
|
||||
reportVersion: "v1",
|
||||
status,
|
||||
issue: "pikasTech/HWLAB#164",
|
||||
@@ -528,7 +529,7 @@ export async function runArtifactRuntimeReadinessGuardCli(argv = process.argv.sl
|
||||
});
|
||||
|
||||
if (args.writeReport) {
|
||||
const outputPath = path.join(repoRoot, args.report);
|
||||
const outputPath = ensureNotRepoReportsPath(repoRoot, args.report, "--report");
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
}
|
||||
@@ -1180,7 +1181,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) {
|
||||
return [
|
||||
"node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty",
|
||||
"node scripts/deploy-desired-state-plan.mjs --promotion-commit <origin-main-sha> --check",
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --write-report",
|
||||
`node scripts/dev-deploy-apply.mjs --dry-run --report ${tempReportPath("dev-deploy-report.json")}`,
|
||||
guardCommand
|
||||
];
|
||||
}
|
||||
@@ -1193,7 +1194,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) {
|
||||
];
|
||||
}
|
||||
return [
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --write-report",
|
||||
`node scripts/dev-deploy-apply.mjs --dry-run --report ${tempReportPath("dev-deploy-report.json")}`,
|
||||
"node scripts/artifact-runtime-readiness-guard.mjs --target-ref origin/main --check --live --no-report"
|
||||
];
|
||||
}
|
||||
@@ -1264,8 +1265,6 @@ function parseArgs(argv) {
|
||||
} else if (arg === "--timeout-ms") {
|
||||
args.timeoutMs = Number.parseInt(requireValue(argv, index, arg), 10);
|
||||
index += 1;
|
||||
} else if (arg === "--write-report") {
|
||||
args.writeReport = true;
|
||||
} else if (arg === "--no-report") {
|
||||
args.writeReport = false;
|
||||
} else if (arg === "--no-live") {
|
||||
@@ -1301,7 +1300,7 @@ function requireValue(argv, index, arg) {
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
`usage: ${guardCommand} [--write-report] [--live] [--expect-blocked]`,
|
||||
`usage: ${guardCommand} [--report ${defaultOutputReportPath}] [--live] [--expect-blocked]`,
|
||||
"",
|
||||
"Read-only latest-main DEV artifact/runtime readiness guard.",
|
||||
"",
|
||||
|
||||
@@ -8,11 +8,12 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const namespace = "hwlab-dev";
|
||||
const defaultReportPath = "reports/d601-k3s-readonly-observability.json";
|
||||
const defaultReportPath = tempReportPath("d601-k3s-readonly-observability.json");
|
||||
const issue = "pikasTech/HWLAB#46";
|
||||
const supports = ["#7", "#33", "#36", "#38", "#39", "#42"].map((id) => `pikasTech/HWLAB${id}`);
|
||||
const forbiddenActions = [
|
||||
@@ -604,7 +605,7 @@ function runnerImageFollowUp(blockers) {
|
||||
}
|
||||
|
||||
async function writeReport(report, reportPath) {
|
||||
const absoluteReportPath = path.join(repoRoot, reportPath);
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(repoRoot, reportPath, "--report");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
@@ -628,7 +629,7 @@ async function buildReport(args) {
|
||||
});
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/d601-k3s-readonly-observability-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/d601-k3s-readonly-observability.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/d601-k3s-readonly-observability.json",
|
||||
reportVersion: "v1",
|
||||
reportKind: "d601-k3s-readonly-observability",
|
||||
issue,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
inspectCodeAgentProviderManifestRefs
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
|
||||
import { tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
@@ -16,7 +17,7 @@ const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)
|
||||
const deployPath = "deploy/deploy.json";
|
||||
const catalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const workloadsPath = "deploy/k8s/base/workloads.yaml";
|
||||
const artifactReportPath = "reports/dev-gate/dev-artifacts.json";
|
||||
const artifactReportPath = tempReportPath("dev-artifacts.json");
|
||||
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
|
||||
const mirrorEnvNames = [
|
||||
"HWLAB_COMMIT_ID",
|
||||
@@ -661,7 +662,7 @@ function reportHints(report, desiredCommitId) {
|
||||
path: artifactReportPath,
|
||||
present: true,
|
||||
authoritative: false,
|
||||
note: "report snapshots are contextual evidence only; deploy desired-state remains authoritative in deploy/, not reports/",
|
||||
note: "temporary publish JSON is contextual evidence only; deploy desired-state remains authoritative in deploy/",
|
||||
commitId: reportCommitId,
|
||||
matchesDesiredState: reportCommitId ? commitEquivalent(reportCommitId, desiredCommitId) : null,
|
||||
artifactPublish: {
|
||||
|
||||
@@ -12,24 +12,25 @@ import {
|
||||
redactSensitiveText,
|
||||
resolveDevKubeconfigSelection
|
||||
} from "./dev-deploy-apply.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const defaultNamespace = "hwlab-dev";
|
||||
const defaultLockName = "hwlab-dev-cd-lock";
|
||||
const defaultTtlSeconds = 3600;
|
||||
const defaultReportPath = "reports/dev-gate/dev-cd-apply.json";
|
||||
const defaultReportPath = tempReportPath("dev-cd-apply.json");
|
||||
const d601NativeKubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
|
||||
const artifactCatalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const artifactReportPath = "reports/dev-gate/dev-artifacts.json";
|
||||
const artifactReportPath = tempReportPath("dev-artifacts.json");
|
||||
const artifactDesiredStatePaths = [
|
||||
"deploy/artifact-catalog.dev.json",
|
||||
"deploy/deploy.json",
|
||||
"deploy/k8s/base/workloads.yaml",
|
||||
"reports/dev-gate/dev-artifacts.json"
|
||||
"deploy/k8s/base/workloads.yaml"
|
||||
];
|
||||
const runtimeProvisioningReportPath = "reports/dev-gate/dev-runtime-provisioning-report.json";
|
||||
const runtimeMigrationReportPath = "reports/dev-gate/dev-runtime-migration-report.json";
|
||||
const runtimePostflightReportPath = "reports/dev-gate/dev-runtime-postflight-report.json";
|
||||
const runtimeProvisioningReportPath = tempReportPath("dev-runtime-provisioning-report.json");
|
||||
const runtimeMigrationReportPath = tempReportPath("dev-runtime-migration-report.json");
|
||||
const runtimePostflightReportPath = tempReportPath("dev-runtime-postflight-report.json");
|
||||
const deployApplyReportPath = tempReportPath("dev-deploy-report.json");
|
||||
const runtimeJobReportPrefix = "/tmp/hwlab-dev-runtime-report";
|
||||
const browserLiveUrl = "http://74.48.78.17:16666/health/live";
|
||||
const apiLiveUrl = `${DEV_ENDPOINT}/health/live`;
|
||||
@@ -103,9 +104,6 @@ export function parseArgs(argv) {
|
||||
} else if (arg === "--confirmed-non-production") {
|
||||
args.confirmedNonProduction = true;
|
||||
args.flags.add(arg);
|
||||
} else if (arg === "--write-report") {
|
||||
args.writeReport = true;
|
||||
args.flags.add(arg);
|
||||
} else if (arg === "--break-stale-lock") {
|
||||
args.breakStaleLock = true;
|
||||
args.flags.add(arg);
|
||||
@@ -124,6 +122,7 @@ export function parseArgs(argv) {
|
||||
args.flags.add(arg);
|
||||
} else if (arg === "--report") {
|
||||
args.reportPath = readOption(argv, ++index, arg);
|
||||
args.writeReport = true;
|
||||
args.flags.add(arg);
|
||||
} else if (arg === "--target-ref") {
|
||||
args.targetRef = readOption(argv, ++index, arg);
|
||||
@@ -182,7 +181,7 @@ export function usage() {
|
||||
examples: [
|
||||
"node scripts/dev-cd-apply.mjs --status",
|
||||
"node scripts/dev-cd-apply.mjs --dry-run",
|
||||
"node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report"
|
||||
`node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report ${defaultReportPath}`
|
||||
],
|
||||
options: {
|
||||
"--status": "Read-only status: target ref, deploy.json, current Lease, and public live health.",
|
||||
@@ -195,8 +194,7 @@ export function usage() {
|
||||
"--ttl-seconds SECONDS": "lock TTL; default: 3600",
|
||||
"--owner-task-id ID": "lock holder; default: Code Queue/env/user derived",
|
||||
"--break-stale-lock": "May take over an expired lock only with --confirm-dev.",
|
||||
"--report PATH": `default: ${defaultReportPath}`,
|
||||
"--write-report": "Write the full report to --report.",
|
||||
"--report PATH": `write the full transaction JSON outside the repo; packaged scripts use ${defaultReportPath}`,
|
||||
"--full-output": "Print the full report to stdout instead of the concise summary.",
|
||||
"--skip-runtime-postflight": "Skip the M3/durable runtime postflight while still applying DEV and verifying 16666/16667 health.",
|
||||
"--skip-live-verify": "test-only escape hatch; do not use for DEV acceptance"
|
||||
@@ -326,7 +324,8 @@ async function readDeployJson(repoRoot) {
|
||||
|
||||
async function readJsonFileSummary(repoRoot, relativePath, summarize) {
|
||||
try {
|
||||
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
||||
const absolutePath = path.isAbsolute(relativePath) ? relativePath : path.join(repoRoot, relativePath);
|
||||
const raw = await readFile(absolutePath, "utf8");
|
||||
const json = JSON.parse(raw);
|
||||
return {
|
||||
path: relativePath,
|
||||
@@ -1854,7 +1853,7 @@ function stepBlockerSafeNextAction(stepResult) {
|
||||
if (stepResult.id === "runtime-db-provisioning") return "Repair repo-owned runtime provisioning inputs/SecretRefs without printing Secret values, then rerun DEV CD.";
|
||||
if (stepResult.id === "runtime-db-migration") return "Repair repo-owned runtime migration/schema state, then rerun DEV CD.";
|
||||
if (stepResult.id === "runtime-durable-postflight") return "Inspect the runtime postflight report, repair durable readiness or M3 evidence persistence, then rerun DEV CD after host-controlled rollout if needed.";
|
||||
if (stepResult.id === "dev-deploy-apply") return "Inspect reports/dev-gate/dev-deploy-report.json and rerun the repo-owned DEV CD path after the apply blocker is fixed.";
|
||||
if (stepResult.id === "dev-deploy-apply") return `Inspect ${deployApplyReportPath} and rerun the repo-owned DEV CD path after the apply blocker is fixed.`;
|
||||
return "Fix the reported step blocker and rerun the repo-owned DEV CD transaction.";
|
||||
}
|
||||
|
||||
@@ -2165,8 +2164,8 @@ function buildStatusNextActions({ target, deployBefore, lock, liveVerify }) {
|
||||
}
|
||||
if (actions.length === 0) {
|
||||
actions.push(target.publishRequired === false
|
||||
? "Run --apply --confirm-dev --confirmed-non-production --write-report when ready to apply the published deploy.json promotion without republishing HEAD."
|
||||
: "Run --apply --confirm-dev --confirmed-non-production --write-report when ready to publish/apply DEV.");
|
||||
? `Run --apply --confirm-dev --confirmed-non-production --report ${defaultReportPath} when ready to apply the published deploy.json promotion without republishing HEAD.`
|
||||
: `Run --apply --confirm-dev --confirmed-non-production --report ${defaultReportPath} when ready to publish/apply DEV.`);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
@@ -2229,7 +2228,7 @@ async function runDevCdStatus(args, ctx, stdout) {
|
||||
live: compactLiveReport(liveVerify),
|
||||
liveDelta,
|
||||
nextActions: buildStatusNextActions({ target, deployBefore, lock, liveVerify }),
|
||||
fullOutputHint: "Use --apply --confirm-dev --confirmed-non-production --write-report for the full transaction report; use --full-output only when stdout needs the full JSON."
|
||||
fullOutputHint: `Use --apply --confirm-dev --confirmed-non-production --report ${defaultReportPath} for the full transaction JSON; use --full-output only when stdout needs the full JSON.`
|
||||
};
|
||||
stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
return 0;
|
||||
@@ -2284,7 +2283,7 @@ function summarizeDevCdApplyReport(report) {
|
||||
} : null,
|
||||
blockers: report.blockers ?? [],
|
||||
reportPaths: apply.reportPaths ?? {},
|
||||
fullOutputHint: "Use --full-output to print the full report to stdout. Use --write-report to persist the full report."
|
||||
fullOutputHint: `Use --full-output to print the full report to stdout. Use --report ${defaultReportPath} to persist the full report outside the repo.`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2307,7 +2306,7 @@ function buildReport({
|
||||
}) {
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-cd-apply.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-cd-apply.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-cd/dev-cd-apply.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#274",
|
||||
taskId: "dev-cd-apply",
|
||||
@@ -2357,7 +2356,7 @@ function buildReport({
|
||||
},
|
||||
dryRun: {
|
||||
status: "not_run",
|
||||
commands: ["node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report"],
|
||||
commands: [`node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --report ${deployApplyReportPath}`],
|
||||
evidence: ["The transaction path owns live side effects; dry-run remains a preflight support mode."],
|
||||
summary: "DEV CD mutation requires --apply and the transaction Lease lock."
|
||||
},
|
||||
@@ -2422,7 +2421,7 @@ function buildReport({
|
||||
reportPaths: {
|
||||
transaction: args.writeReport ? args.reportPath : null,
|
||||
artifacts: artifactReportPath,
|
||||
deployApply: "reports/dev-gate/dev-deploy-report.json",
|
||||
deployApply: deployApplyReportPath,
|
||||
runtimeProvisioning: runtimeProvisioningReportPath,
|
||||
runtimeMigration: runtimeMigrationReportPath,
|
||||
runtimePostflight: runtimePostflightReportPath
|
||||
@@ -2544,7 +2543,7 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
type: "contract_blocker",
|
||||
scope: "artifact-boundary",
|
||||
status: "open",
|
||||
summary: "deploy/deploy.json, deploy/artifact-catalog.dev.json, reports/dev-gate/dev-artifacts.json, and the artifact merge parent must resolve to the same promotion commit before apply."
|
||||
summary: "deploy/deploy.json, deploy/artifact-catalog.dev.json, deploy/k8s/base/workloads.yaml, and the artifact merge parent must resolve to the same promotion commit before apply."
|
||||
});
|
||||
throw new DevCdApplyError("artifact desired-state provenance does not match the deploy.json promotion", {
|
||||
code: "artifact-boundary-provenance-mismatch",
|
||||
@@ -2635,7 +2634,7 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
id: "artifact-publish",
|
||||
phase: "publishing",
|
||||
command: process.execPath,
|
||||
args: ["scripts/dev-artifact-publish.mjs", "--publish"],
|
||||
args: ["scripts/dev-artifact-publish.mjs", "--publish", "--report", artifactReportPath],
|
||||
timeoutMs: 60 * 60 * 1000,
|
||||
reportPath: artifactReportPath
|
||||
},
|
||||
@@ -2686,7 +2685,8 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
"--apply",
|
||||
"--confirm-dev",
|
||||
"--confirmed-non-production",
|
||||
"--write-report",
|
||||
"--report",
|
||||
runtimeProvisioningReportPath,
|
||||
"--fail-on-blocked"
|
||||
],
|
||||
timeoutMs: 5 * 60 * 1000,
|
||||
@@ -2702,7 +2702,8 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
"--apply",
|
||||
"--confirm-dev",
|
||||
"--confirmed-non-production",
|
||||
"--write-report",
|
||||
"--report",
|
||||
runtimeMigrationReportPath,
|
||||
"--fail-on-blocked"
|
||||
],
|
||||
timeoutMs: 5 * 60 * 1000,
|
||||
@@ -2717,11 +2718,12 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
"--apply",
|
||||
"--confirm-dev",
|
||||
"--confirmed-non-production",
|
||||
"--write-report",
|
||||
"--report",
|
||||
deployApplyReportPath,
|
||||
...(args.kubeconfig ? ["--kubeconfig", args.kubeconfig] : [])
|
||||
],
|
||||
timeoutMs: 20 * 60 * 1000,
|
||||
reportPath: "reports/dev-gate/dev-deploy-report.json"
|
||||
reportPath: deployApplyReportPath
|
||||
},
|
||||
...(
|
||||
args.skipRuntimePostflight
|
||||
@@ -2737,7 +2739,8 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
"--confirmed-non-production",
|
||||
"--target",
|
||||
"api",
|
||||
"--write-report",
|
||||
"--report",
|
||||
runtimePostflightReportPath,
|
||||
"--fail-on-blocked"
|
||||
],
|
||||
timeoutMs: 2 * 60 * 1000,
|
||||
@@ -2871,7 +2874,7 @@ export async function runDevCdApply(argv, io = {}) {
|
||||
});
|
||||
|
||||
if (args.writeReport) {
|
||||
const absoluteReportPath = path.resolve(ctx.repoRoot, args.reportPath);
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(ctx.repoRoot, args.reportPath, "--report");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ import {
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
|
||||
const tempReportRoot = "/tmp/hwlab-dev-gate";
|
||||
const artifactReportFixturePath = path.join(tempReportRoot, "dev-artifacts.json");
|
||||
|
||||
function tempReport(name) {
|
||||
return path.join(tempReportRoot, name);
|
||||
}
|
||||
|
||||
function stateReport(repoRoot, name) {
|
||||
return path.join(repoRoot, ".state/test-reports", name);
|
||||
}
|
||||
|
||||
function iso(offsetMs = 0) {
|
||||
return new Date(Date.parse("2026-05-23T08:00:00.000Z") + offsetMs).toISOString();
|
||||
@@ -51,7 +61,7 @@ async function makeRepo(options = {}) {
|
||||
const commitId = options.commitId ?? "abc1234";
|
||||
const repoRoot = await fsTempDir();
|
||||
await mkdir(path.join(repoRoot, "deploy/k8s/base"), { recursive: true });
|
||||
await mkdir(path.join(repoRoot, "reports/dev-gate"), { recursive: true });
|
||||
await mkdir(tempReportRoot, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(repoRoot, "deploy/deploy.json"),
|
||||
`${JSON.stringify({
|
||||
@@ -92,7 +102,7 @@ async function makeRepo(options = {}) {
|
||||
publish: {
|
||||
ciPublished: true,
|
||||
registryVerified: true,
|
||||
provenance: "reports/dev-gate/dev-artifacts.json"
|
||||
provenance: "ci-artifact:dev-artifacts.json"
|
||||
},
|
||||
services: [
|
||||
{
|
||||
@@ -121,7 +131,7 @@ async function makeRepo(options = {}) {
|
||||
}, null, 2)}\n`
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repoRoot, "reports/dev-gate/dev-artifacts.json"),
|
||||
artifactReportFixturePath,
|
||||
`${JSON.stringify({
|
||||
reportVersion: "v1",
|
||||
taskId: "dev-artifact-publish",
|
||||
@@ -211,7 +221,7 @@ function makeRunCommand({
|
||||
"deploy/artifact-catalog.dev.json",
|
||||
"deploy/deploy.json",
|
||||
"deploy/k8s/base/workloads.yaml",
|
||||
"reports/dev-gate/dev-artifacts.json"
|
||||
"ci-artifact:dev-artifacts.json"
|
||||
],
|
||||
desiredStatePromotionStatus = "pass",
|
||||
runtimeJobLogSuffix = "",
|
||||
@@ -469,7 +479,7 @@ test("dev-cd status and dry-run are read-only compact JSON", async () => {
|
||||
"--dry-run",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report"
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json")
|
||||
], {
|
||||
repoRoot,
|
||||
env: {},
|
||||
@@ -490,7 +500,7 @@ test("dev-cd status and dry-run are read-only compact JSON", async () => {
|
||||
assert.equal(status.live.summary.checked, 2);
|
||||
assertNoReadOnlySideEffects(commandLog);
|
||||
await assert.rejects(
|
||||
readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"),
|
||||
readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8"),
|
||||
{ code: "ENOENT" }
|
||||
);
|
||||
});
|
||||
@@ -664,7 +674,7 @@ test("dev-cd status degrades artifact merge provenance when catalog/report do no
|
||||
kind: "hwlab-artifact-catalog",
|
||||
commitId: "9999999",
|
||||
artifactState: "published",
|
||||
publish: { ciPublished: true, registryVerified: true, provenance: "reports/dev-gate/dev-artifacts.json" },
|
||||
publish: { ciPublished: true, registryVerified: true, provenance: "ci-artifact:dev-artifacts.json" },
|
||||
services: []
|
||||
}, null, 2)}\n`
|
||||
);
|
||||
@@ -705,7 +715,7 @@ test("dev-cd status degrades artifact merge provenance when catalog/report do no
|
||||
test("apply stops before lock acquisition when artifact merge provenance mismatches deploy-json promotion", async () => {
|
||||
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
||||
await writeFile(
|
||||
path.join(repoRoot, "reports/dev-gate/dev-artifacts.json"),
|
||||
artifactReportFixturePath,
|
||||
`${JSON.stringify({
|
||||
status: "published",
|
||||
commitId: "9999999",
|
||||
@@ -1036,7 +1046,7 @@ test("apply preflight passes with D601 native k3s and required SecretRef keys be
|
||||
"task-preflight-pass",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report",
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json"),
|
||||
"--full-output"
|
||||
], {
|
||||
repoRoot,
|
||||
@@ -1088,7 +1098,7 @@ test("apply preflight blocks missing SecretRef before Lease or side-effect comma
|
||||
"task-missing-secret",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report"
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json")
|
||||
], {
|
||||
repoRoot,
|
||||
env: {},
|
||||
@@ -1105,7 +1115,7 @@ test("apply preflight blocks missing SecretRef before Lease or side-effect comma
|
||||
});
|
||||
|
||||
const summary = JSON.parse(output);
|
||||
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
||||
const writtenReport = JSON.parse(await readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8"));
|
||||
assert.equal(code, 2);
|
||||
assert.equal(summary.status, "blocked");
|
||||
assert.equal(summary.preflight.status, "blocked");
|
||||
@@ -1230,7 +1240,7 @@ test("transaction runs phases, allows internal side-effect env, releases lock, a
|
||||
"task-b",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report",
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json"),
|
||||
"--full-output"
|
||||
], {
|
||||
repoRoot,
|
||||
@@ -1270,10 +1280,10 @@ test("transaction runs phases, allows internal side-effect env, releases lock, a
|
||||
assert.equal(report.devCdApply.liveVerify.summary.checked, 2);
|
||||
assert.equal(report.devCdApply.liveVerify.endpoints.some((endpoint) => endpoint.id === "cloud-web-16666"), true);
|
||||
assert.equal(report.devCdApply.liveVerify.endpoints.some((endpoint) => endpoint.id === "cloud-api-16667"), true);
|
||||
assert.equal(report.devCdApply.reportPaths.transaction, "reports/dev-gate/dev-cd-apply.json");
|
||||
assert.equal(report.devCdApply.reportPaths.runtimeProvisioning, "reports/dev-gate/dev-runtime-provisioning-report.json");
|
||||
assert.equal(report.devCdApply.reportPaths.runtimeMigration, "reports/dev-gate/dev-runtime-migration-report.json");
|
||||
assert.equal(report.devCdApply.reportPaths.runtimePostflight, "reports/dev-gate/dev-runtime-postflight-report.json");
|
||||
assert.equal(report.devCdApply.reportPaths.transaction, stateReport(repoRoot, "dev-cd-apply.json"));
|
||||
assert.equal(report.devCdApply.reportPaths.runtimeProvisioning, tempReport("dev-runtime-provisioning-report.json"));
|
||||
assert.equal(report.devCdApply.reportPaths.runtimeMigration, tempReport("dev-runtime-migration-report.json"));
|
||||
assert.equal(report.devCdApply.reportPaths.runtimePostflight, tempReport("dev-runtime-postflight-report.json"));
|
||||
|
||||
const publishCall = commandLog.find((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs"));
|
||||
const applyCall = commandLog.find((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"));
|
||||
@@ -1309,7 +1319,7 @@ test("transaction runs phases, allows internal side-effect env, releases lock, a
|
||||
commandLog.findIndex((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"))
|
||||
);
|
||||
|
||||
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
||||
const writtenReport = JSON.parse(await readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8"));
|
||||
assert.equal(writtenReport.devCdApply.liveVerify.summary.checked, 2);
|
||||
});
|
||||
|
||||
@@ -1325,7 +1335,7 @@ test("transaction can skip runtime postflight while preserving publish apply and
|
||||
"task-skip-postflight",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report",
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json"),
|
||||
"--full-output",
|
||||
"--skip-runtime-postflight"
|
||||
], {
|
||||
@@ -1403,7 +1413,7 @@ test("transaction blocks with structured runtime postflight blocker when M3 evid
|
||||
"task-postflight-blocked",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report"
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json")
|
||||
], {
|
||||
repoRoot,
|
||||
env: {},
|
||||
@@ -1414,7 +1424,7 @@ test("transaction blocks with structured runtime postflight blocker when M3 evid
|
||||
});
|
||||
|
||||
const summary = JSON.parse(output);
|
||||
const report = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
||||
const report = JSON.parse(await readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8"));
|
||||
const blocker = report.blockers.find((item) => item.scope === "runtime-durable-postflight");
|
||||
assert.equal(code, 2);
|
||||
assert.equal(summary.status, "blocked");
|
||||
@@ -1447,7 +1457,7 @@ test("runtime maintenance job reports are parsed after redacting secret-like log
|
||||
"task-redaction",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report",
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json"),
|
||||
"--full-output"
|
||||
], {
|
||||
repoRoot,
|
||||
@@ -1484,7 +1494,7 @@ test("transaction can apply an already published deploy-json promotion without r
|
||||
"task-promotion",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report",
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json"),
|
||||
"--full-output"
|
||||
], {
|
||||
repoRoot,
|
||||
@@ -1551,7 +1561,7 @@ test("apply stdout is concise by default while write-report keeps the full repor
|
||||
"task-c",
|
||||
"--kubeconfig",
|
||||
"/etc/rancher/k3s/k3s.yaml",
|
||||
"--write-report"
|
||||
"--report", stateReport(repoRoot, "dev-cd-apply.json")
|
||||
], {
|
||||
repoRoot,
|
||||
env: {},
|
||||
@@ -1569,7 +1579,7 @@ test("apply stdout is concise by default while write-report keeps the full repor
|
||||
assert.equal(summary.lock.acquired, true);
|
||||
assert.equal(summary.steps.some((step) => step.id === "dev-deploy-apply"), true);
|
||||
|
||||
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
||||
const writtenReport = JSON.parse(await readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8"));
|
||||
assert.equal(writtenReport.devCdApply.deployJson.before.manifest.environment, "dev");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const transactionEnvNames = [
|
||||
"HWLAB_CD_TRANSACTION_ID",
|
||||
"HWLAB_CD_TRANSACTION_OWNER",
|
||||
@@ -22,7 +24,7 @@ export function devCdTransactionGuardFailure({
|
||||
mode,
|
||||
requiredEnv,
|
||||
acceptedEnv: transactionEnvNames,
|
||||
entrypoint: "node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report",
|
||||
entrypoint: `node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report ${tempReportPath("dev-cd-apply.json")}`,
|
||||
summary: `${script} ${mode} is a DEV CD side-effect step and must run inside scripts/dev-cd-apply.mjs.`,
|
||||
devOnly: true,
|
||||
prodTouched: false,
|
||||
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
classifyCodeAgentBrowserFailure,
|
||||
summarizeCodeAgentPayload
|
||||
} from "./code-agent-response-contract.mjs";
|
||||
import { tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
export { classifyCodeAgentBrowserJourney };
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const domOnlyReportPath = tempReportPath("dev-cloud-workbench-dom-only.json");
|
||||
const liveReportPath = tempReportPath("dev-cloud-workbench-live.json");
|
||||
const webRoot = path.join(repoRoot, "web/hwlab-cloud-web");
|
||||
const defaultLiveUrl = "http://74.48.78.17:16666/";
|
||||
const helpOwner = "codex_1779444232735_1";
|
||||
@@ -1584,11 +1587,11 @@ function baseReport({
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --quick-prompts-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --local-agent-timeout-fixture",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --layout --url http://74.48.78.17:16666/",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-dom-only.json",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json",
|
||||
`node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report ${domOnlyReportPath}`,
|
||||
`node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report ${liveReportPath}`,
|
||||
mode === "dom-only"
|
||||
? "node scripts/validate-dev-gate-report.mjs reports/dev-gate/dev-cloud-workbench-dom-only.json"
|
||||
: "node scripts/validate-dev-gate-report.mjs reports/dev-gate/dev-cloud-workbench-live.json",
|
||||
? `node scripts/validate-dev-gate-report.mjs ${domOnlyReportPath}`
|
||||
: `node scripts/validate-dev-gate-report.mjs ${liveReportPath}`,
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
localSmoke: {
|
||||
@@ -1620,8 +1623,8 @@ function baseReport({
|
||||
"No hardware write API, service restart, PROD access, or secret read is performed."
|
||||
],
|
||||
commands: [
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-dom-only.json",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json"
|
||||
`node scripts/dev-cloud-workbench-smoke.mjs --dom-only --url http://74.48.78.17:16666/ --report ${domOnlyReportPath}`,
|
||||
`node scripts/dev-cloud-workbench-smoke.mjs --live --confirm-dev-live --url http://74.48.78.17:16666/ --report ${liveReportPath}`
|
||||
],
|
||||
summary: liveJourneyPassed
|
||||
? "Deployed 16666 browser journey and same-origin Code Agent chat completed."
|
||||
@@ -1647,10 +1650,10 @@ function baseReport({
|
||||
|
||||
function reportModeId(mode) {
|
||||
return mode === "layout"
|
||||
? "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-layout.json"
|
||||
? "https://hwlab.pikastech.local/dev-gate/dev-cloud-workbench-layout.json"
|
||||
: mode === "dom-only"
|
||||
? "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-dom-only.json"
|
||||
: "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json";
|
||||
? "https://hwlab.pikastech.local/dev-gate/dev-cloud-workbench-dom-only.json"
|
||||
: "https://hwlab.pikastech.local/dev-gate/dev-cloud-workbench-live.json";
|
||||
}
|
||||
|
||||
function addCheck(checks, blockers, id, result, summary, options = {}) {
|
||||
@@ -8441,7 +8444,7 @@ function escapeRegExp(value) {
|
||||
export function printSmokeHelp() {
|
||||
return {
|
||||
status: "usage",
|
||||
command: "node scripts/dev-cloud-workbench-smoke.mjs --source | --layout [--url http://74.48.78.17:16666/] | --mobile | --local-agent-fixture | --quick-prompts-fixture | --local-agent-timeout-fixture | --dom-only --url http://74.48.78.17:16666/ [--report reports/dev-gate/dev-cloud-workbench-dom-only.json] | --live --confirm-dev-live --url http://74.48.78.17:16666/ [--report reports/dev-gate/dev-cloud-workbench-live.json]",
|
||||
command: `node scripts/dev-cloud-workbench-smoke.mjs --source | --layout [--url http://74.48.78.17:16666/] | --mobile | --local-agent-fixture | --quick-prompts-fixture | --local-agent-timeout-fixture | --dom-only --url http://74.48.78.17:16666/ [--report ${domOnlyReportPath}] | --live --confirm-dev-live --url http://74.48.78.17:16666/ [--report ${liveReportPath}]`,
|
||||
notes: [
|
||||
"Default/source mode reads repository files and emits SOURCE-level evidence only; it never calls the live provider and must not claim DEV-LIVE.",
|
||||
"--layout runs desktop and mobile browser geometry/hit-target checks for sidebar collapse/expand; with --url it remains layout-only live evidence and does not claim M3 acceptance.",
|
||||
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
|
||||
import { requireDevCdTransactionForSideEffect } from "./dev-cd-transaction-guard.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const reportPath = "reports/dev-gate/dev-deploy-report.json";
|
||||
const defaultReportPath = tempReportPath("dev-deploy-report.json");
|
||||
const namespace = "hwlab-dev";
|
||||
const healthPath = "/health/live";
|
||||
const defaultD601KubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
|
||||
@@ -73,9 +74,9 @@ const requiredValidationCommands = [
|
||||
"node --check scripts/src/dev-deploy-apply.mjs",
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked"
|
||||
];
|
||||
const applyCommand = "node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report";
|
||||
const dryRunPlanCommand = "node scripts/dev-deploy-apply.mjs --dry-run --write-report";
|
||||
const blockedDryRunCommand = "node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report";
|
||||
const applyCommand = `node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report ${tempReportPath("dev-cd-apply.json")}`;
|
||||
const dryRunPlanCommand = `node scripts/dev-deploy-apply.mjs --dry-run --report ${defaultReportPath}`;
|
||||
const blockedDryRunCommand = `node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --report ${defaultReportPath}`;
|
||||
const forbiddenActions = [
|
||||
"prod-deploy",
|
||||
"secret-read",
|
||||
@@ -93,6 +94,7 @@ export function parseArgs(argv) {
|
||||
const errors = [];
|
||||
let kubeconfig = null;
|
||||
let kubeconfigSpecified = false;
|
||||
let reportPath = defaultReportPath;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
@@ -118,6 +120,27 @@ export function parseArgs(argv) {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--report") {
|
||||
flags.add("--report");
|
||||
const next = argv[index + 1];
|
||||
if (typeof next !== "string" || next.startsWith("--")) {
|
||||
errors.push("--report requires a non-empty path value");
|
||||
} else {
|
||||
reportPath = next;
|
||||
flags.add("--report-output");
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--report=")) {
|
||||
flags.add("--report");
|
||||
reportPath = arg.slice("--report=".length);
|
||||
flags.add("--report-output");
|
||||
if (!reportPath.trim()) {
|
||||
errors.push("--report requires a non-empty path value");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
flags.add(arg);
|
||||
}
|
||||
|
||||
@@ -126,7 +149,8 @@ export function parseArgs(argv) {
|
||||
dryRun: !flags.has("--apply"),
|
||||
expectBlocked: flags.has("--expect-blocked"),
|
||||
skipLiveProbe: flags.has("--skip-live-probe"),
|
||||
writeReport: flags.has("--write-report"),
|
||||
writeReport: flags.has("--report-output"),
|
||||
reportPath,
|
||||
kubeconfig,
|
||||
kubeconfigSpecified,
|
||||
errors,
|
||||
@@ -1052,7 +1076,8 @@ function buildPlanConclusion(status, blockers) {
|
||||
|
||||
function validateArgs(args, blockers) {
|
||||
for (const error of args.errors ?? []) {
|
||||
addBlocker(blockers, "safety_blocker", "kubeconfig-argument", error);
|
||||
const scope = error.startsWith("--report") ? "report-argument" : "kubeconfig-argument";
|
||||
addBlocker(blockers, "safety_blocker", scope, error);
|
||||
}
|
||||
for (const forbidden of ["--prod", "--production", "--read-secret", "--force-push", "--heavyweight-e2e"]) {
|
||||
if (args.flags.has(forbidden)) {
|
||||
@@ -1898,7 +1923,7 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
|
||||
const report = {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-deploy-report.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-cd/dev-deploy-report.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#33",
|
||||
supports: ["pikasTech/HWLAB#164", "pikasTech/HWLAB#143", "pikasTech/HWLAB#99", "pikasTech/HWLAB#63", "pikasTech/HWLAB#50", "pikasTech/HWLAB#7", "pikasTech/HWLAB#33", "pikasTech/HWLAB#17", "pikasTech/HWLAB#32", "pikasTech/HWLAB#30"],
|
||||
@@ -2009,7 +2034,9 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
};
|
||||
|
||||
if (args.writeReport) {
|
||||
await writeFile(path.join(repoRoot, reportPath), `${JSON.stringify(report, null, 2)}\n`);
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(repoRoot, args.reportPath, "--report");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const defaultReportPath = path.join(repoRoot, "reports/dev-gate/dev-edge-health.json");
|
||||
const defaultReportPath = tempReportPath("dev-edge-health.json");
|
||||
const publicHost = "74.48.78.17";
|
||||
const publicPort = 16667;
|
||||
const namespace = "hwlab-dev";
|
||||
@@ -124,12 +125,11 @@ function parseArgs(argv) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--live") {
|
||||
args.live = true;
|
||||
} else if (arg === "--write-report") {
|
||||
args.writeReport = true;
|
||||
} else if (arg === "--report") {
|
||||
index += 1;
|
||||
assert.ok(argv[index], "--report requires a path");
|
||||
args.reportPath = path.resolve(process.cwd(), argv[index]);
|
||||
args.reportPath = argv[index];
|
||||
args.writeReport = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument ${arg}`);
|
||||
}
|
||||
@@ -149,7 +149,7 @@ async function createDevGateReport(edgeHealth) {
|
||||
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-edge-health.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/dev-edge-health.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#36",
|
||||
taskId: "dev-edge-health",
|
||||
@@ -179,7 +179,7 @@ async function createDevGateReport(edgeHealth) {
|
||||
"node --check scripts/dev-edge-health-smoke.mjs",
|
||||
"node --check scripts/src/dev-edge-health-smoke-lib.mjs",
|
||||
"node --test scripts/src/dev-edge-health-smoke-lib.test.mjs",
|
||||
"node scripts/dev-edge-health-smoke.mjs --live --write-report"
|
||||
`node scripts/dev-edge-health-smoke.mjs --live --report ${defaultReportPath}`
|
||||
],
|
||||
localSmoke: {
|
||||
status: "pass",
|
||||
@@ -1202,6 +1202,7 @@ async function maybeWriteReport(report, args) {
|
||||
if (!args.writeReport) {
|
||||
return;
|
||||
}
|
||||
await mkdir(path.dirname(args.reportPath), { recursive: true });
|
||||
await writeFile(args.reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(repoRoot, args.reportPath, "--report");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import test from "node:test";
|
||||
|
||||
import { buildReport } from "./dev-evidence-blocker-aggregator.mjs";
|
||||
|
||||
test("aggregator reports M4 current blocker as durable runtime or skills injection, not stale DB live", async () => {
|
||||
test("aggregator no longer treats deleted repository reports as live evidence", async () => {
|
||||
const report = await buildReport();
|
||||
const m4 = report.milestoneBlockerClassification.find((item) => item.milestone === "M4");
|
||||
const m5 = report.milestoneBlockerClassification.find((item) => item.milestone === "M5");
|
||||
@@ -12,22 +12,19 @@ test("aggregator reports M4 current blocker as durable runtime or skills injecti
|
||||
|
||||
assert.ok(m4, "M4 blocker classification is present");
|
||||
assert.ok(m5, "M5 blocker classification is present");
|
||||
assert.equal(dbLayer.status, "pass");
|
||||
assert.equal(dbLayer.evidenceLevel, "DEV-LIVE");
|
||||
assert.equal(dbCheck.status, "pass");
|
||||
assert.equal(dbCheck.evidenceLevel, "DEV-LIVE");
|
||||
assert.equal(report.overall.status, "blocked");
|
||||
assert.equal(report.sourceReports.status, "removed");
|
||||
assert.equal(dbLayer.status, "blocked");
|
||||
assert.equal(dbLayer.evidenceLevel, "BLOCKED");
|
||||
assert.equal(dbCheck.status, "blocked");
|
||||
assert.equal(dbCheck.evidenceLevel, "BLOCKED");
|
||||
assert.equal(
|
||||
report.blockers.some((blocker) =>
|
||||
["cloud-api-db", "cloud-api-db-health-gate", "cloud-api-db-live", "db-live"].includes(blocker.scope)
|
||||
),
|
||||
false,
|
||||
"DB live blockers must be stale once current DB live evidence is pass"
|
||||
report.blockers.some((blocker) => blocker.scope === "repo-report-directory-removed"),
|
||||
true,
|
||||
"governance blocker must remain explicit"
|
||||
);
|
||||
assert.notEqual(m4.blockerClass, "db-live-readiness");
|
||||
assert.ok(
|
||||
["runtime_durable_adapter_query_blocked", "skills-commit-version-injection"].includes(m4.blockerClass),
|
||||
`unexpected M4 blocker class ${m4.blockerClass}`
|
||||
);
|
||||
assert.equal(m4.blockerClass, "runtime_durable_adapter_query_blocked");
|
||||
assert.match(m4.dependency, /durable runtime adapter|skill commit and version/u);
|
||||
assert.match(m4.nonPromotionReason, /durable agent runtime readiness|skill version injection/u);
|
||||
assert.match(m5.dependency, /runtime durable adapter readiness, skills commit\/version injection/u);
|
||||
|
||||
@@ -17,10 +17,13 @@ import {
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
|
||||
import { probeRegistryCapabilities } from "./registry-capabilities.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const defaultReportPath = "reports/dev-gate/dev-preflight-report.json";
|
||||
const defaultReportPath = tempReportPath("dev-preflight-report.json");
|
||||
const defaultArtifactReportPath = tempReportPath("dev-artifacts.json");
|
||||
const defaultEdgeHealthReportPath = tempReportPath("dev-edge-health.json");
|
||||
const issue = "pikasTech/HWLAB#34";
|
||||
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#35", "#39", "#43", "#48", "#49", "#57", "#66", "#143", "#164"].map(
|
||||
(id) => `pikasTech/HWLAB${id}`
|
||||
@@ -92,7 +95,7 @@ function parseArgs(argv) {
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return "Usage: node scripts/dev-gate-preflight.mjs [--target-ref origin/main] [--report reports/dev-gate/dev-preflight-report.json] [--timeout-ms 5000] [--no-write] [--fail-on-blocked]";
|
||||
return `Usage: node scripts/dev-gate-preflight.mjs [--target-ref origin/main] [--report ${defaultReportPath}] [--timeout-ms 5000] [--no-write] [--fail-on-blocked]`;
|
||||
}
|
||||
|
||||
function commandLine(command, args) {
|
||||
@@ -452,7 +455,7 @@ async function artifactIdentityFor({ deploy, catalog, artifactReport, targetComm
|
||||
publishVerified,
|
||||
refreshCommands: {
|
||||
blocked: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --blocked`,
|
||||
published: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --publish-report reports/dev-gate/dev-artifacts.json`
|
||||
published: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --publish-report ${defaultArtifactReportPath}`
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -536,8 +539,8 @@ async function loadContracts() {
|
||||
|
||||
async function loadOptionalReports() {
|
||||
return {
|
||||
artifactPublish: await readOptionalJson("reports/dev-gate/dev-artifacts.json"),
|
||||
edgeHealth: await readOptionalJson("reports/dev-gate/dev-edge-health.json")
|
||||
artifactPublish: await readOptionalJson(defaultArtifactReportPath),
|
||||
edgeHealth: await readOptionalJson(defaultEdgeHealthReportPath)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -655,8 +658,8 @@ function validateArtifactPublishReport(reporter, artifactReport, artifactIdentit
|
||||
reporter.block({
|
||||
type: "runtime_blocker",
|
||||
scope: "dev-artifact-publish",
|
||||
summary: "reports/dev-gate/dev-artifacts.json is missing, so the preflight has no publish evidence for HWLAB runtime artifacts.",
|
||||
nextTask: "Run the DEV artifact publish workflow and record reports/dev-gate/dev-artifacts.json before real deployment."
|
||||
summary: `${defaultArtifactReportPath} is missing, so the preflight has no publish evidence for HWLAB runtime artifacts.`,
|
||||
nextTask: `Run the DEV artifact publish workflow and keep its temporary JSON at ${defaultArtifactReportPath}; do not commit report output.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -716,7 +719,7 @@ function validateArtifactPublishReport(reporter, artifactReport, artifactIdentit
|
||||
reporter.block({
|
||||
type: "runtime_blocker",
|
||||
scope: "dev-artifact-publish",
|
||||
summary: `reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
|
||||
summary: `${defaultArtifactReportPath} does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
|
||||
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons."
|
||||
});
|
||||
}
|
||||
@@ -775,8 +778,8 @@ function validateEdgeHealthReport(reporter, edgeReport) {
|
||||
reporter.block({
|
||||
type: "network_blocker",
|
||||
scope: "dev-edge-health-report",
|
||||
summary: "reports/dev-gate/dev-edge-health.json is missing, so the preflight has no committed edge/frp health evidence.",
|
||||
nextTask: "Run the read-only DEV edge health smoke and commit reports/dev-gate/dev-edge-health.json before real deployment."
|
||||
summary: `${defaultEdgeHealthReportPath} is missing, so the preflight has no temporary edge/frp health JSON.`,
|
||||
nextTask: `Run the read-only DEV edge health smoke and keep its temporary JSON at ${defaultEdgeHealthReportPath}; do not commit report output.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -785,7 +788,7 @@ function validateEdgeHealthReport(reporter, edgeReport) {
|
||||
const pass = edgeReport.status === "pass" && edgeHealth?.status === "pass";
|
||||
const evidence = [
|
||||
{
|
||||
report: "reports/dev-gate/dev-edge-health.json",
|
||||
report: defaultEdgeHealthReportPath,
|
||||
status: edgeReport.status,
|
||||
classification: edgeHealth?.classification ?? "unknown",
|
||||
publicTcp: edgeHealth?.publicTcp ?? [],
|
||||
@@ -1133,7 +1136,7 @@ function validateRuntimeBoundary(reporter, deploy) {
|
||||
}
|
||||
|
||||
async function writeReport(report, reportPath) {
|
||||
const absoluteReportPath = path.join(repoRoot, reportPath);
|
||||
const absoluteReportPath = ensureNotRepoReportsPath(repoRoot, reportPath, "--report");
|
||||
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
@@ -1145,7 +1148,7 @@ function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIde
|
||||
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-preflight-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-preflight-report.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/dev-preflight-report.json",
|
||||
reportVersion: "v1",
|
||||
reportKind: "dev-gate-preflight",
|
||||
issue,
|
||||
|
||||
@@ -22,10 +22,11 @@ import {
|
||||
} from "../../internal/db/runtime-store.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
|
||||
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const migrationPath = "internal/db/migrations/0001_cloud_core_skeleton.sql";
|
||||
const defaultReportPath = "reports/dev-gate/dev-runtime-migration-report.json";
|
||||
const defaultReportPath = tempReportPath("dev-runtime-migration-report.json");
|
||||
const issue = "pikasTech/HWLAB#164";
|
||||
|
||||
export async function runDevRuntimeMigrationCli(argv = process.argv.slice(2), options = {}) {
|
||||
@@ -167,15 +168,16 @@ export function parseArgs(argv = []) {
|
||||
else if (arg === "--allow-live-db-read") args.allowLiveDbRead = true;
|
||||
else if (arg === "--confirm-dev") args.confirmDev = true;
|
||||
else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true;
|
||||
else if (arg === "--write-report") args.writeReport = true;
|
||||
else if (arg === "--pretty") args.pretty = true;
|
||||
else if (arg === "--fail-on-blocked") args.failOnBlocked = true;
|
||||
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||
else if (arg === "--report") {
|
||||
index += 1;
|
||||
args.reportPath = requireArgValue(argv[index], "--report");
|
||||
args.writeReport = true;
|
||||
} else if (arg.startsWith("--report=")) {
|
||||
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
|
||||
args.writeReport = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
@@ -651,7 +653,7 @@ function isMigrationSslError(error) {
|
||||
}
|
||||
|
||||
async function writeReport(report, reportPath, root) {
|
||||
const absolute = path.resolve(root, reportPath);
|
||||
const absolute = ensureNotRepoReportsPath(root, reportPath, "--report");
|
||||
await mkdir(path.dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
buildM3IoControlSourceReport,
|
||||
runM3IoControlLiveReport
|
||||
} from "./m3-io-control-e2e.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const defaultReportPath = "reports/dev-gate/dev-runtime-postflight-report.json";
|
||||
const defaultReportPath = tempReportPath("dev-runtime-postflight-report.json");
|
||||
const issue = "pikasTech/HWLAB#311";
|
||||
const defaultHealthUrl = `${DEV_ENDPOINT}/health/live`;
|
||||
const defaultV1Url = `${DEV_ENDPOINT}/v1`;
|
||||
@@ -149,7 +150,6 @@ export function parseArgs(argv = []) {
|
||||
else if (arg === "--live") args.live = true;
|
||||
else if (arg === "--confirm-dev") args.confirmDev = true;
|
||||
else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true;
|
||||
else if (arg === "--write-report") args.writeReport = true;
|
||||
else if (arg === "--pretty") args.pretty = true;
|
||||
else if (arg === "--fail-on-blocked") args.failOnBlocked = true;
|
||||
else if (arg === "--target") {
|
||||
@@ -161,8 +161,10 @@ export function parseArgs(argv = []) {
|
||||
} else if (arg === "--report") {
|
||||
index += 1;
|
||||
args.reportPath = requireArgValue(argv[index], "--report");
|
||||
args.writeReport = true;
|
||||
} else if (arg.startsWith("--report=")) {
|
||||
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
|
||||
args.writeReport = true;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
args.help = true;
|
||||
} else {
|
||||
@@ -604,7 +606,7 @@ function sanitizeRedactedValue(value) {
|
||||
}
|
||||
|
||||
async function writeReport(report, reportPath, root) {
|
||||
const absolute = path.resolve(root, reportPath);
|
||||
const absolute = ensureNotRepoReportsPath(root, reportPath, "--report");
|
||||
await mkdir(path.dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
buildPostgresPoolConfig
|
||||
} from "../../internal/db/runtime-store.mjs";
|
||||
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const defaultReportPath = "reports/dev-gate/dev-runtime-provisioning-report.json";
|
||||
const defaultReportPath = tempReportPath("dev-runtime-provisioning-report.json");
|
||||
const issue = "pikasTech/HWLAB#311";
|
||||
|
||||
export const DEV_DB_ADMIN_SECRET_REF = Object.freeze({
|
||||
@@ -189,15 +190,16 @@ export function parseArgs(argv = []) {
|
||||
else if (arg === "--allow-live-db-read") args.allowLiveDbRead = true;
|
||||
else if (arg === "--confirm-dev") args.confirmDev = true;
|
||||
else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true;
|
||||
else if (arg === "--write-report") args.writeReport = true;
|
||||
else if (arg === "--pretty") args.pretty = true;
|
||||
else if (arg === "--fail-on-blocked") args.failOnBlocked = true;
|
||||
else if (arg === "--help" || arg === "-h") args.help = true;
|
||||
else if (arg === "--report") {
|
||||
index += 1;
|
||||
args.reportPath = requireArgValue(argv[index], "--report");
|
||||
args.writeReport = true;
|
||||
} else if (arg.startsWith("--report=")) {
|
||||
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
|
||||
args.writeReport = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
@@ -878,7 +880,7 @@ function isSslError(error) {
|
||||
}
|
||||
|
||||
async function writeReport(report, reportPath, root) {
|
||||
const absolute = path.resolve(root, reportPath);
|
||||
const absolute = ensureNotRepoReportsPath(root, reportPath, "--report");
|
||||
await mkdir(path.dirname(absolute), { recursive: true });
|
||||
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import path from "node:path";
|
||||
|
||||
export const TEMP_REPORT_ROOT = process.env.HWLAB_TEMP_REPORT_ROOT || "/tmp/hwlab-dev-gate";
|
||||
const forbiddenRepoReportDir = "reports";
|
||||
|
||||
export function tempReportPath(...segments) {
|
||||
return path.join(TEMP_REPORT_ROOT, ...segments);
|
||||
}
|
||||
|
||||
export function resolveOutputPath(root, outputPath) {
|
||||
return path.isAbsolute(outputPath) ? outputPath : path.resolve(root, outputPath);
|
||||
}
|
||||
|
||||
export function ensureNotRepoReportsPath(root, outputPath, label = "report path") {
|
||||
const absoluteRoot = path.resolve(root);
|
||||
const absoluteOutput = resolveOutputPath(absoluteRoot, outputPath);
|
||||
const relative = path.relative(absoluteRoot, absoluteOutput).split(path.sep).join("/");
|
||||
if (relative === forbiddenRepoReportDir || relative.startsWith(`${forbiddenRepoReportDir}/`)) {
|
||||
throw new Error(`${label} must not write under the forbidden repository report directory; use ${TEMP_REPORT_ROOT} or .state instead`);
|
||||
}
|
||||
return absoluteOutput;
|
||||
}
|
||||
@@ -26,10 +26,12 @@ import {
|
||||
HWLAB_M3_IO_API_ROUTE,
|
||||
HWLAB_M3_IO_SKILL_NAME
|
||||
} from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const defaultJsonReportPath = "reports/dev-gate/rpt-004-mvp-e2e.json";
|
||||
const defaultMarkdownReportPath = "reports/dev-gate/rpt-004-mvp-e2e.md";
|
||||
const defaultJsonReportPath = tempReportPath("rpt-004-mvp-e2e.json");
|
||||
const defaultMarkdownReportPath = tempReportPath("rpt-004-mvp-e2e.md");
|
||||
const defaultArtifactReportPath = tempReportPath("dev-artifacts.json");
|
||||
const issue = "pikasTech/HWLAB#316";
|
||||
const taskId = "rpt004-mvp-e2e-harness";
|
||||
const reportKind = "rpt-004-mvp-e2e-harness";
|
||||
@@ -214,8 +216,6 @@ export function parseArgs(argv = []) {
|
||||
} else if (arg === "--markdown") {
|
||||
args.markdownPath = readArg(argv, ++index, arg);
|
||||
args.writeReport = true;
|
||||
} else if (arg === "--write-report") {
|
||||
args.writeReport = true;
|
||||
} else if (arg === "--no-write") {
|
||||
args.writeReport = false;
|
||||
} else if (arg === "--pretty") {
|
||||
@@ -612,7 +612,7 @@ function baseReport({ parsed, now, root }) {
|
||||
const liveMutationUsed = parsed.live === true && parsed.allowM3Write === true;
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/rpt-004-mvp-e2e-harness.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/rpt-004-mvp-e2e.json",
|
||||
$id: "https://hwlab.pikastech.local/dev-gate/rpt-004-mvp-e2e.json",
|
||||
reportVersion: "v1",
|
||||
reportKind,
|
||||
issue,
|
||||
@@ -677,7 +677,7 @@ function baseReport({ parsed, now, root }) {
|
||||
"node --test scripts/src/rpt004-mvp-e2e-harness.test.mjs",
|
||||
"node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write",
|
||||
"node scripts/rpt004-mvp-e2e-harness.mjs --live --url http://74.48.78.17:16666/ --api-url http://74.48.78.17:16667/",
|
||||
"node scripts/validate-dev-gate-report.mjs reports/dev-gate/rpt-004-mvp-e2e.json"
|
||||
`node scripts/validate-dev-gate-report.mjs ${defaultJsonReportPath}`
|
||||
],
|
||||
localSmoke: {
|
||||
status: "not_run",
|
||||
@@ -743,7 +743,7 @@ async function buildExpectedArtifactState({ root, expectedCommit, fsJson }) {
|
||||
: (relativePath) => readJsonOptional(root, relativePath);
|
||||
const deploy = await readJson("deploy/deploy.json");
|
||||
const catalog = await readJson("deploy/artifact-catalog.dev.json");
|
||||
const artifactReport = await readJson("reports/dev-gate/dev-artifacts.json");
|
||||
const artifactReport = await readJson(defaultArtifactReportPath);
|
||||
const deployCommit = sanitizeCommit(deploy?.commitId);
|
||||
const catalogCommit = sanitizeCommit(catalog?.commitId);
|
||||
const reportCommit = sanitizeCommit(artifactReport?.artifactPublish?.sourceCommitId ?? artifactReport?.commitId);
|
||||
@@ -1382,8 +1382,8 @@ function compactReport(report) {
|
||||
}
|
||||
|
||||
async function writeReports(report, args, root) {
|
||||
const jsonPath = path.resolve(root, args.reportPath);
|
||||
const markdownPath = path.resolve(root, args.markdownPath);
|
||||
const jsonPath = ensureNotRepoReportsPath(root, args.reportPath, "--report");
|
||||
const markdownPath = ensureNotRepoReportsPath(root, args.markdownPath, "--markdown");
|
||||
report.artifacts = {
|
||||
jsonReportPath: relativeOrAbsolute(jsonPath, root),
|
||||
markdownReportPath: relativeOrAbsolute(markdownPath, root),
|
||||
|
||||
@@ -837,7 +837,7 @@ function fixtureArtifactJson() {
|
||||
return async (relativePath) => {
|
||||
if (relativePath === "deploy/deploy.json") return deploy;
|
||||
if (relativePath === "deploy/artifact-catalog.dev.json") return catalog;
|
||||
if (relativePath === "reports/dev-gate/dev-artifacts.json") return artifactReport;
|
||||
if (relativePath === "/tmp/hwlab-dev-gate/dev-artifacts.json") return artifactReport;
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,946 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const validator = path.join(repoRoot, "scripts/validate-dev-gate-report.mjs");
|
||||
const baseReportPath = path.join(repoRoot, "reports/dev-gate/dev-cloud-workbench-live.json");
|
||||
const baseDomOnlyReportPath = path.join(repoRoot, "reports/dev-gate/dev-cloud-workbench-dom-only.json");
|
||||
const baseAggregatorReportPath = path.join(repoRoot, "reports/dev-gate/dev-m5-gate-aggregator-v2.json");
|
||||
const baseReport = JSON.parse(await readFile(baseReportPath, "utf8"));
|
||||
const baseDomOnlyReport = JSON.parse(await readFile(baseDomOnlyReportPath, "utf8"));
|
||||
const baseAggregatorReport = JSON.parse(await readFile(baseAggregatorReportPath, "utf8"));
|
||||
|
||||
function cloneBaseReport() {
|
||||
return JSON.parse(JSON.stringify(baseReport));
|
||||
}
|
||||
|
||||
function cloneBaseDomOnlyReport() {
|
||||
return JSON.parse(JSON.stringify(baseDomOnlyReport));
|
||||
}
|
||||
|
||||
function cloneBaseAggregatorReport() {
|
||||
return JSON.parse(JSON.stringify(baseAggregatorReport));
|
||||
}
|
||||
|
||||
function currentIdentityFields() {
|
||||
return {
|
||||
generatedAt: "2026-05-23T03:21:02.207Z",
|
||||
sourceIdentity: {
|
||||
status: "observed",
|
||||
source: "git-head",
|
||||
commitId: "7de6edd2c41f54e4265e822d2593fa67057fa41a",
|
||||
shortCommitId: "7de6edd2c41f",
|
||||
ref: "test/d601-dom-only-feedback-validation",
|
||||
worktreeState: "clean",
|
||||
dirty: false,
|
||||
reportCommitId: "7de6edd2c41f",
|
||||
summary: "Source identity was derived from the current worktree or source environment and is separate from the live runtime identity."
|
||||
},
|
||||
runtimeIdentity: {
|
||||
status: "observed",
|
||||
source: "health-live",
|
||||
endpoint: "http://74.48.78.17:16667/health/live",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
environment: "dev",
|
||||
healthStatus: "ok",
|
||||
ready: true,
|
||||
commitId: "7de6edd",
|
||||
commitSource: "runtime-env",
|
||||
imageTag: "7de6edd",
|
||||
observedAt: "2026-05-23T03:21:02.207Z",
|
||||
runtime: {
|
||||
durable: true,
|
||||
ready: true,
|
||||
status: "ok",
|
||||
blocker: "none",
|
||||
connection: {
|
||||
queryAttempted: true,
|
||||
queryResult: "ok"
|
||||
}
|
||||
},
|
||||
readiness: {
|
||||
ready: true,
|
||||
status: "ok",
|
||||
durability: {
|
||||
ready: true,
|
||||
status: "ready",
|
||||
blocker: "none",
|
||||
blockedLayer: "none",
|
||||
queryResult: "ok"
|
||||
}
|
||||
},
|
||||
blockerCodes: [],
|
||||
summary: "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD."
|
||||
},
|
||||
expectedRuntimeIdentity: {
|
||||
status: "observed",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
source: "deploy/artifact-catalog.dev.json+deploy/deploy.json",
|
||||
commitId: "7de6edd",
|
||||
imageTag: "7de6edd",
|
||||
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd",
|
||||
summary: "Expected DEV runtime identity was derived from current source deploy desired state, not from live health."
|
||||
},
|
||||
deploymentIdentity: {
|
||||
status: "pass",
|
||||
expectedCommit: "7de6edd",
|
||||
expectedReportCommitId: "7de6edd2c41f",
|
||||
expectedImageTag: "7de6edd",
|
||||
expectedSource: "deploy/artifact-catalog.dev.json+deploy/deploy.json",
|
||||
observedRuntimeCommit: "7de6edd",
|
||||
observedImageTag: "7de6edd",
|
||||
evidence: [
|
||||
"expected=7de6edd",
|
||||
"expectedImageTag=7de6edd",
|
||||
"runtimeCommit=7de6edd",
|
||||
"imageTag=7de6edd"
|
||||
],
|
||||
summary: "Live API runtime identity matches the current source desired runtime identity."
|
||||
},
|
||||
webAssetIdentity: {
|
||||
status: "pass",
|
||||
assets: [
|
||||
{
|
||||
path: "index.html",
|
||||
status: "match",
|
||||
httpStatus: 200,
|
||||
contentType: "text/html; charset=utf-8",
|
||||
sourceHash: "435edb7da5c539ff",
|
||||
liveHash: "435edb7da5c539ff",
|
||||
sourceLength: 13844,
|
||||
liveLength: 13844
|
||||
},
|
||||
{
|
||||
path: "styles.css",
|
||||
status: "match",
|
||||
httpStatus: 200,
|
||||
contentType: "text/css; charset=utf-8",
|
||||
sourceHash: "95574eda6090e34b",
|
||||
liveHash: "95574eda6090e34b",
|
||||
sourceLength: 21184,
|
||||
liveLength: 21184
|
||||
},
|
||||
{
|
||||
path: "app.mjs",
|
||||
status: "match",
|
||||
httpStatus: 200,
|
||||
contentType: "text/javascript; charset=utf-8",
|
||||
sourceHash: "3dd09b19fec790a4",
|
||||
liveHash: "3dd09b19fec790a4",
|
||||
sourceLength: 67912,
|
||||
liveLength: 67912
|
||||
}
|
||||
],
|
||||
mismatches: [],
|
||||
summary: "Live 16666 primary web assets match the current clean source files."
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function applyCurrentIdentity(report) {
|
||||
const identity = currentIdentityFields();
|
||||
const apiRuntimeReadiness = {
|
||||
status: "pass",
|
||||
healthStatus: "ok",
|
||||
ready: true,
|
||||
runtimeDurable: true,
|
||||
runtimeReady: true,
|
||||
durabilityReady: true,
|
||||
durableBlocked: false,
|
||||
blockerCodes: [],
|
||||
evidence: [
|
||||
"api.status=ok",
|
||||
"ready=true",
|
||||
"runtime.durable=true",
|
||||
"runtime.ready=true",
|
||||
"durability.ready=true"
|
||||
],
|
||||
summary: "Live API health and runtime durability are ready for browser journey evidence."
|
||||
};
|
||||
|
||||
Object.assign(report, {
|
||||
commitId: identity.sourceIdentity.reportCommitId,
|
||||
generatedAt: identity.generatedAt,
|
||||
sourceIdentity: identity.sourceIdentity,
|
||||
runtimeIdentity: identity.runtimeIdentity,
|
||||
expectedRuntimeIdentity: identity.expectedRuntimeIdentity,
|
||||
deploymentIdentity: identity.deploymentIdentity,
|
||||
webAssetIdentity: identity.webAssetIdentity
|
||||
});
|
||||
|
||||
ensureCurrentIdentityChecks(report, identity);
|
||||
|
||||
const runtimeCheck = report.checks.find((check) => check.id === "live-runtime-current-main");
|
||||
if (runtimeCheck) {
|
||||
Object.assign(runtimeCheck, {
|
||||
status: identity.deploymentIdentity.status,
|
||||
summary: identity.deploymentIdentity.summary,
|
||||
evidence: identity.deploymentIdentity.evidence,
|
||||
observations: identity.deploymentIdentity
|
||||
});
|
||||
}
|
||||
|
||||
const apiRuntimeReadinessCheck = report.checks.find((check) => check.id === "live-api-runtime-readiness");
|
||||
if (apiRuntimeReadinessCheck) {
|
||||
Object.assign(apiRuntimeReadinessCheck, {
|
||||
status: apiRuntimeReadiness.status,
|
||||
summary: apiRuntimeReadiness.summary,
|
||||
evidence: apiRuntimeReadiness.evidence,
|
||||
observations: apiRuntimeReadiness
|
||||
});
|
||||
}
|
||||
|
||||
const webAssetCheck = report.checks.find((check) => check.id === "live-web-assets-current-main");
|
||||
if (webAssetCheck) {
|
||||
Object.assign(webAssetCheck, {
|
||||
status: identity.webAssetIdentity.status,
|
||||
summary: identity.webAssetIdentity.summary,
|
||||
evidence: identity.webAssetIdentity.assets.map((asset) => `${asset.path}: ${asset.status}`),
|
||||
observations: identity.webAssetIdentity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCurrentIdentityChecks(report, identity) {
|
||||
const httpIndex = report.checks.findIndex((check) => check.id === "live-http-html");
|
||||
const runtimeCheck = {
|
||||
id: "live-runtime-current-main",
|
||||
status: identity.deploymentIdentity.status,
|
||||
summary: identity.deploymentIdentity.summary,
|
||||
evidence: identity.deploymentIdentity.evidence,
|
||||
observations: identity.deploymentIdentity
|
||||
};
|
||||
const webAssetCheck = {
|
||||
id: "live-web-assets-current-main",
|
||||
status: identity.webAssetIdentity.status,
|
||||
summary: identity.webAssetIdentity.summary,
|
||||
evidence: identity.webAssetIdentity.assets.map((asset) => `${asset.path}: ${asset.status}`),
|
||||
observations: identity.webAssetIdentity
|
||||
};
|
||||
|
||||
upsertCheck(report, runtimeCheck, httpIndex >= 0 ? httpIndex : 0);
|
||||
const updatedHttpIndex = report.checks.findIndex((check) => check.id === "live-http-html");
|
||||
upsertCheck(report, webAssetCheck, updatedHttpIndex >= 0 ? updatedHttpIndex + 1 : 1);
|
||||
}
|
||||
|
||||
function upsertCheck(report, nextCheck, insertIndex) {
|
||||
const existingIndex = report.checks.findIndex((check) => check.id === nextCheck.id);
|
||||
if (existingIndex >= 0) {
|
||||
report.checks[existingIndex] = nextCheck;
|
||||
return;
|
||||
}
|
||||
report.checks.splice(insertIndex, 0, nextCheck);
|
||||
}
|
||||
|
||||
function acceptedDomOnlyReport() {
|
||||
const report = cloneBaseDomOnlyReport();
|
||||
applyCurrentIdentity(report);
|
||||
report.status = "pass";
|
||||
report.mode = "dom-only";
|
||||
report.evidenceLevel = "DEV-LIVE-DOM-READONLY";
|
||||
report.devLive = false;
|
||||
report.blockers = [];
|
||||
report.devPreconditions.status = "pass";
|
||||
report.devPreconditions.summary = "Deployed browser DOM was inspected read-only; Code Agent chat was not posted.";
|
||||
report.safety.liveMode = "dom-only-readonly-no-code-agent-post";
|
||||
report.safety.codeAgentBrowserJourneySkipped = true;
|
||||
report.safety.codeAgentPostSent = false;
|
||||
return report;
|
||||
}
|
||||
|
||||
function acceptedCodeAgentResponse() {
|
||||
return {
|
||||
status: "completed",
|
||||
provider: "openai-responses",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/openai-responses",
|
||||
traceId: "trc_completed",
|
||||
hasReply: true,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
function acceptedLiveReport() {
|
||||
const report = cloneBaseReport();
|
||||
applyCurrentIdentity(report);
|
||||
report.status = "pass";
|
||||
report.mode = "live";
|
||||
report.evidenceLevel = "DEV-LIVE-BROWSER";
|
||||
report.devLive = true;
|
||||
report.blockers = [];
|
||||
report.devPreconditions.status = "pass";
|
||||
report.devPreconditions.summary = "Deployed 16666 browser journey and same-origin Code Agent chat completed.";
|
||||
report.safety.liveMode = "browser-user-journey-with-code-agent-post";
|
||||
delete report.safety.codeAgentBrowserJourneySkipped;
|
||||
delete report.safety.codeAgentPostSent;
|
||||
report.safety.retainedApiFields = [
|
||||
"status",
|
||||
"provider",
|
||||
"model",
|
||||
"backend",
|
||||
"traceId",
|
||||
"hasReply",
|
||||
"error.code",
|
||||
"error.missingEnv",
|
||||
"error.providerStatus"
|
||||
];
|
||||
report.safety.statement = "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance.";
|
||||
|
||||
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
|
||||
const response = acceptedCodeAgentResponse();
|
||||
Object.assign(journey, {
|
||||
status: "pass",
|
||||
summary: "Deployed browser journey opened the workbench, sent a Code Agent message, and observed a completed UI response.",
|
||||
evidence: [
|
||||
"POST /v1/agent/chat HTTP 200",
|
||||
"status=completed",
|
||||
"provider=openai-responses",
|
||||
"model=gpt-5.5",
|
||||
"traceId=trc_completed",
|
||||
"uiStatus=DEV-LIVE 回复",
|
||||
"blocker=none"
|
||||
],
|
||||
observations: {
|
||||
request: {
|
||||
method: "POST",
|
||||
status: 200,
|
||||
urlPath: "/v1/agent/chat"
|
||||
},
|
||||
response,
|
||||
classification: {
|
||||
status: "pass"
|
||||
},
|
||||
ui: {
|
||||
agentChatStatus: "DEV-LIVE 回复",
|
||||
inputCleared: true,
|
||||
completedMessageVisible: true,
|
||||
failedMessageVisible: false
|
||||
},
|
||||
networkEvents: [
|
||||
{
|
||||
method: "POST",
|
||||
status: 200,
|
||||
urlPath: "/v1/agent/chat",
|
||||
body: JSON.parse(JSON.stringify(response))
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
function fullAcceptanceReport() {
|
||||
return acceptedLiveReport();
|
||||
}
|
||||
|
||||
function preflightBlockedReport() {
|
||||
const report = cloneBaseReport();
|
||||
report.status = "blocked";
|
||||
report.evidenceLevel = "BLOCKED";
|
||||
report.devLive = false;
|
||||
report.generatedAt = report.runtimeIdentity.observedAt;
|
||||
report.expectedRuntimeIdentity = {
|
||||
status: "observed",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
source: "deploy/artifact-catalog.dev.json+deploy/deploy.json",
|
||||
commitId: "c7de474",
|
||||
imageTag: "c7de474",
|
||||
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:c7de474",
|
||||
summary: "Expected DEV runtime identity was derived from current source deploy desired state, not from live health."
|
||||
};
|
||||
report.deploymentIdentity = {
|
||||
status: "pass",
|
||||
expectedCommit: "c7de474",
|
||||
expectedImageTag: "c7de474",
|
||||
expectedSource: "deploy/artifact-catalog.dev.json+deploy/deploy.json",
|
||||
observedRuntimeCommit: "c7de474",
|
||||
observedImageTag: "c7de474",
|
||||
summary: "Live API runtime identity matches the current source desired runtime identity.",
|
||||
evidence: [
|
||||
"expected=c7de474",
|
||||
"expectedImageTag=c7de474",
|
||||
"runtimeCommit=c7de474",
|
||||
"imageTag=c7de474"
|
||||
]
|
||||
};
|
||||
report.webAssetIdentity = {
|
||||
status: "blocked",
|
||||
assets: [
|
||||
{
|
||||
path: "index.html",
|
||||
status: "mismatch",
|
||||
httpStatus: 200,
|
||||
contentType: "text/html; charset=utf-8",
|
||||
sourceHash: "435edb7da5c539ff",
|
||||
liveHash: "924f9d57e0bb61be",
|
||||
sourceLength: 13844,
|
||||
liveLength: 11375
|
||||
}
|
||||
],
|
||||
mismatches: ["index.html"],
|
||||
summary: "Deployment drift: live 16666 primary web assets differ from current source (index.html)."
|
||||
};
|
||||
report.checks = [
|
||||
{
|
||||
id: "live-runtime-current-main",
|
||||
status: report.deploymentIdentity.status,
|
||||
summary: report.deploymentIdentity.summary,
|
||||
evidence: report.deploymentIdentity.evidence,
|
||||
observations: report.deploymentIdentity
|
||||
},
|
||||
report.checks.find((check) => check.id === "live-http-html"),
|
||||
{
|
||||
id: "live-web-assets-current-main",
|
||||
status: report.webAssetIdentity.status,
|
||||
summary: report.webAssetIdentity.summary,
|
||||
evidence: ["index.html: mismatch"],
|
||||
observations: report.webAssetIdentity
|
||||
},
|
||||
{
|
||||
id: "live-code-agent-browser-journey",
|
||||
status: "blocked",
|
||||
summary: "Browser Code Agent journey was not posted because deployed runtime/assets do not match current source identity.",
|
||||
evidence: [
|
||||
"POST /v1/agent/chat not sent",
|
||||
"runtimeIdentity=pass",
|
||||
"webAssetIdentity=blocked"
|
||||
],
|
||||
observations: {
|
||||
request: {
|
||||
method: "POST",
|
||||
status: "not_sent",
|
||||
urlPath: "/v1/agent/chat"
|
||||
},
|
||||
response: {
|
||||
status: "failed",
|
||||
provider: "not_observed",
|
||||
model: "not_observed",
|
||||
backend: "not_observed",
|
||||
traceId: "not_observed",
|
||||
hasReply: false,
|
||||
error: {
|
||||
code: "deployment_identity_preflight",
|
||||
missingEnv: []
|
||||
}
|
||||
},
|
||||
classification: {
|
||||
status: "blocked",
|
||||
reason: "deployment_identity_preflight",
|
||||
codeAgentBrowserJourneySkipped: true
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
report.blockers = [
|
||||
{
|
||||
type: "observability_blocker",
|
||||
scope: "live-web-assets-current-main",
|
||||
status: "open",
|
||||
summary: report.webAssetIdentity.summary
|
||||
},
|
||||
{
|
||||
type: "observability_blocker",
|
||||
scope: "live-code-agent-browser-journey",
|
||||
status: "open",
|
||||
summary: "Browser Code Agent journey was not posted because deployed runtime/assets do not match current source identity."
|
||||
}
|
||||
];
|
||||
report.devPreconditions.status = "blocked";
|
||||
report.devPreconditions.summary = "Deployed browser journey is blocked; see checks and blockers.";
|
||||
report.safety.liveMode = "deployment-identity-preflight";
|
||||
report.safety.codeAgentBrowserJourneySkipped = true;
|
||||
report.safety.retainedApiFields = ["runtime.commitId", "runtime.imageTag", "web asset hash/status"];
|
||||
report.safety.statement = "Live smoke blocked before the browser chat journey because deployed runtime/assets do not match the current source identity; it did not post Code Agent chat, call hardware write APIs, restart services, or read secret values.";
|
||||
return report;
|
||||
}
|
||||
|
||||
async function withReport(report, fn) {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "hwlab-dev-report-guard-"));
|
||||
const reportPath = path.join(directory, "report.json");
|
||||
try {
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
return await fn(reportPath);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function runValidator(reportPath) {
|
||||
return spawnSync(process.execPath, [validator, reportPath], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8"
|
||||
});
|
||||
}
|
||||
|
||||
async function assertRejected(report, expectedMessage) {
|
||||
await withReport(report, async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.notEqual(result.status, 0, "validator should reject stale deployed evidence");
|
||||
assert.match(`${result.stdout}\n${result.stderr}`, expectedMessage);
|
||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /sk-[A-Za-z0-9._-]{8,}/u);
|
||||
});
|
||||
}
|
||||
|
||||
test("accepts the sanitized deployed workbench evidence contract", async () => {
|
||||
await withReport(cloneBaseReport(), async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts full workbench acceptance only when API health and runtime durability are ready", async () => {
|
||||
await withReport(fullAcceptanceReport(), async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects full workbench acceptance when API health is degraded", async () => {
|
||||
const report = fullAcceptanceReport();
|
||||
report.runtimeIdentity.healthStatus = "degraded";
|
||||
report.checks.find((check) => check.id === "live-api-runtime-readiness").observations.healthStatus = "degraded";
|
||||
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
|
||||
});
|
||||
|
||||
test("rejects full workbench acceptance when runtime durability query is blocked", async () => {
|
||||
const report = fullAcceptanceReport();
|
||||
Object.assign(report.runtimeIdentity.runtime, {
|
||||
durable: false,
|
||||
ready: false,
|
||||
status: "degraded",
|
||||
blocker: "runtime_durable_adapter_query_blocked",
|
||||
connection: {
|
||||
queryAttempted: true,
|
||||
queryResult: "query_blocked"
|
||||
}
|
||||
});
|
||||
Object.assign(report.runtimeIdentity.readiness.durability, {
|
||||
ready: false,
|
||||
status: "blocked",
|
||||
blocker: "runtime_durable_adapter_query_blocked",
|
||||
blockedLayer: "durability_query",
|
||||
queryResult: "query_blocked"
|
||||
});
|
||||
report.runtimeIdentity.blockerCodes = ["runtime_durable_adapter_query_blocked"];
|
||||
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
|
||||
});
|
||||
|
||||
test("rejects degraded workbench evidence with full-acceptance wording", async () => {
|
||||
const report = acceptedLiveReport();
|
||||
report.status = "degraded";
|
||||
report.evidenceLevel = "DEV-LIVE-BROWSER-DEGRADED";
|
||||
report.devLive = false;
|
||||
report.devPreconditions.status = "degraded";
|
||||
Object.assign(report.runtimeIdentity, {
|
||||
healthStatus: "degraded",
|
||||
ready: false
|
||||
});
|
||||
Object.assign(report.runtimeIdentity.runtime, {
|
||||
durable: false,
|
||||
ready: false,
|
||||
status: "degraded",
|
||||
blocker: "runtime_durable_adapter_query_blocked",
|
||||
connection: {
|
||||
queryAttempted: true,
|
||||
queryResult: "query_blocked"
|
||||
}
|
||||
});
|
||||
Object.assign(report.runtimeIdentity.readiness.durability, {
|
||||
ready: false,
|
||||
status: "blocked",
|
||||
blocker: "runtime_durable_adapter_query_blocked",
|
||||
blockedLayer: "durability_query",
|
||||
queryResult: "query_blocked"
|
||||
});
|
||||
report.runtimeIdentity.blockerCodes = ["runtime_durable_adapter_query_blocked"];
|
||||
const readiness = report.checks.find((check) => check.id === "live-api-runtime-readiness");
|
||||
Object.assign(readiness, {
|
||||
status: "degraded",
|
||||
summary: "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance.",
|
||||
evidence: [
|
||||
"api.status=degraded",
|
||||
"ready=false",
|
||||
"runtime.durable=false",
|
||||
"runtime.ready=false",
|
||||
"durability.ready=false",
|
||||
"runtime_durable_adapter_query_blocked",
|
||||
"blocker=runtime_durable_adapter_query_blocked"
|
||||
],
|
||||
observations: {
|
||||
status: "degraded",
|
||||
healthStatus: "degraded",
|
||||
ready: false,
|
||||
runtimeDurable: false,
|
||||
runtimeReady: false,
|
||||
durabilityReady: false,
|
||||
durableBlocked: true,
|
||||
blockerCodes: ["runtime_durable_adapter_query_blocked"],
|
||||
evidence: [
|
||||
"api.status=degraded",
|
||||
"ready=false",
|
||||
"runtime.durable=false",
|
||||
"runtime.ready=false",
|
||||
"durability.ready=false",
|
||||
"runtime_durable_adapter_query_blocked",
|
||||
"blocker=runtime_durable_adapter_query_blocked"
|
||||
],
|
||||
summary: "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
|
||||
}
|
||||
});
|
||||
report.reportLifecycle.summary = "Current deployed browser user journey report for the 16666 Cloud Workbench; deployed UI usable in degraded/read-only mode and does not substitute for M3/M4/M5 DEV-LIVE acceptance.";
|
||||
report.devPreconditions.summary = "Deployed UI usable in degraded/read-only mode; M3/M4/M5 DEV-LIVE accepted.";
|
||||
report.safety.statement = "Degraded/read-only mode only; Full DEV-LIVE accepted.";
|
||||
await assertRejected(report, /degraded API\/runtime report must not claim full M3\/M4\/M5 DEV-LIVE acceptance/);
|
||||
});
|
||||
|
||||
test("accepts preflight-blocked workbench evidence when Code Agent POST is marked not_sent", async () => {
|
||||
await withReport(preflightBlockedReport(), async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects preflight-blocked workbench evidence without not_sent Code Agent journey proof", async () => {
|
||||
const report = preflightBlockedReport();
|
||||
report.checks = report.checks.filter((check) => check.id !== "live-code-agent-browser-journey");
|
||||
report.blockers = report.blockers.filter((blocker) => blocker.scope !== "live-code-agent-browser-journey");
|
||||
await assertRejected(report, /checks missing live-code-agent-browser-journey/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence when runtime identity is not observed", async () => {
|
||||
const report = acceptedDomOnlyReport();
|
||||
Object.assign(report.runtimeIdentity, {
|
||||
status: "not_observed",
|
||||
serviceId: "not_observed",
|
||||
environment: "not_observed",
|
||||
healthStatus: "not_observed",
|
||||
commitId: "not_observed",
|
||||
commitSource: "not_observed",
|
||||
imageTag: "not_observed",
|
||||
reason: "test fixture"
|
||||
});
|
||||
await assertRejected(report, /runtimeIdentity\.status/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence when runtime identity is stale", async () => {
|
||||
const report = acceptedDomOnlyReport();
|
||||
report.runtimeIdentity.observedAt = "2026-05-22T12:00:00.000Z";
|
||||
await assertRejected(report, /runtimeIdentity\.observedAt must be fresh/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence that carries an allowed legacy port annotation", async () => {
|
||||
const report = acceptedDomOnlyReport();
|
||||
report.deprecatedEndpoints = [
|
||||
{
|
||||
endpoint: "http://74.48.78.17:6666",
|
||||
status: "deprecated",
|
||||
activeGreenEligible: false
|
||||
}
|
||||
];
|
||||
report.checks.find((check) => check.id === "live-http-html").evidence.push("legacy probe http://74.48.78.17:6666/");
|
||||
await assertRejected(report, /deprecated public endpoint evidence/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence with a hard-coded runtime commit source", async () => {
|
||||
const report = acceptedDomOnlyReport();
|
||||
report.runtimeIdentity.commitSource = "hard-coded";
|
||||
await assertRejected(report, /runtimeIdentity\.commitSource/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence that mixes provider_unavailable blockers", async () => {
|
||||
const report = acceptedLiveReport();
|
||||
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
|
||||
journey.observations.networkEvents[0].body.error = {
|
||||
code: "provider_unavailable",
|
||||
missingEnv: ["OPENAI_API_KEY"]
|
||||
};
|
||||
await assertRejected(report, /provider_unavailable, OPENAI_API_KEY, or providerStatus blockers/);
|
||||
});
|
||||
|
||||
function applyProviderBlockedJourney(report, { uiLabel = "发送失败" } = {}) {
|
||||
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
|
||||
report.status = "blocked";
|
||||
report.evidenceLevel = "BLOCKED";
|
||||
report.devLive = false;
|
||||
report.devPreconditions.status = "blocked";
|
||||
report.devPreconditions.summary = "Deployed browser journey is blocked; see checks and blockers.";
|
||||
report.blockers = [
|
||||
{
|
||||
type: "runtime_blocker",
|
||||
scope: "live-code-agent-browser-journey",
|
||||
status: "open",
|
||||
summary: "Code Agent provider returned provider_unavailable with provider HTTP 502."
|
||||
}
|
||||
];
|
||||
report.safety.retainedApiFields = [
|
||||
"status",
|
||||
"provider",
|
||||
"model",
|
||||
"backend",
|
||||
"traceId",
|
||||
"hasReply",
|
||||
"error.code",
|
||||
"error.missingEnv",
|
||||
"error.providerStatus"
|
||||
];
|
||||
journey.status = "blocked";
|
||||
journey.summary = "Deployed browser journey reached Code Agent, but the provider returned a blocked/unavailable state instead of a completed reply.";
|
||||
journey.evidence = [
|
||||
"POST /v1/agent/chat HTTP 200",
|
||||
"status=failed",
|
||||
"provider=openai-responses",
|
||||
"model=gpt-5.5",
|
||||
"traceId=trc_provider_502",
|
||||
`uiStatus=${uiLabel}`,
|
||||
"blocker=provider-upstream"
|
||||
];
|
||||
journey.observations.request.status = 200;
|
||||
journey.observations.response = {
|
||||
status: "failed",
|
||||
provider: "openai-responses",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/openai-responses",
|
||||
traceId: "trc_provider_502",
|
||||
hasReply: false,
|
||||
error: {
|
||||
code: "provider_unavailable",
|
||||
missingEnv: [],
|
||||
providerStatus: 502
|
||||
}
|
||||
};
|
||||
journey.observations.classification = {
|
||||
status: "blocked",
|
||||
blocker: "provider-upstream",
|
||||
providerStatus: 502,
|
||||
reason: "Code Agent HTTP non-2xx status 502 means the upstream response is blocked, not completed"
|
||||
};
|
||||
journey.observations.ui.agentChatStatus = uiLabel;
|
||||
journey.observations.ui.inputCleared = false;
|
||||
journey.observations.ui.completedMessageVisible = false;
|
||||
journey.observations.ui.failedMessageVisible = true;
|
||||
journey.observations.networkEvents = [
|
||||
{
|
||||
method: "POST",
|
||||
status: 200,
|
||||
urlPath: "/v1/agent/chat",
|
||||
body: journey.observations.response
|
||||
}
|
||||
];
|
||||
return report;
|
||||
}
|
||||
|
||||
test("accepts blocked workbench evidence for provider HTTP 502 without promoting completion", async () => {
|
||||
const report = applyProviderBlockedJourney(acceptedLiveReport());
|
||||
|
||||
await withReport(report, async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts sanitized 服务受阻 provider blocker wording only as blocked evidence", async () => {
|
||||
const report = applyProviderBlockedJourney(acceptedLiveReport(), { uiLabel: "服务受阻" });
|
||||
|
||||
await withReport(report, async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects sanitized 服务受阻 provider blocker evidence promoted to full workbench acceptance", async () => {
|
||||
const report = fullAcceptanceReport();
|
||||
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
|
||||
journey.status = "pass";
|
||||
journey.summary = "Incorrectly promoted provider blocker evidence.";
|
||||
journey.evidence = [
|
||||
"POST /v1/agent/chat HTTP 200",
|
||||
"status=failed",
|
||||
"provider=openai-responses",
|
||||
"model=gpt-5.5",
|
||||
"traceId=trc_provider_502",
|
||||
"uiStatus=服务受阻",
|
||||
"blocker=provider-upstream"
|
||||
];
|
||||
journey.observations.response = {
|
||||
status: "failed",
|
||||
provider: "openai-responses",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/openai-responses",
|
||||
traceId: "trc_provider_502",
|
||||
hasReply: false,
|
||||
error: {
|
||||
code: "provider_unavailable",
|
||||
missingEnv: [],
|
||||
providerStatus: 502
|
||||
}
|
||||
};
|
||||
journey.observations.classification = {
|
||||
status: "blocked",
|
||||
blocker: "provider-upstream",
|
||||
providerStatus: 502,
|
||||
reason: "Code Agent HTTP non-2xx status 502 means the upstream response is blocked, not completed"
|
||||
};
|
||||
journey.observations.ui.agentChatStatus = "服务受阻";
|
||||
journey.observations.ui.inputCleared = false;
|
||||
journey.observations.ui.completedMessageVisible = false;
|
||||
journey.observations.ui.failedMessageVisible = true;
|
||||
journey.observations.networkEvents = [
|
||||
{
|
||||
method: "POST",
|
||||
status: 200,
|
||||
urlPath: "/v1/agent/chat",
|
||||
body: journey.observations.response
|
||||
}
|
||||
];
|
||||
|
||||
await assertRejected(report, /response\.status|provider_unavailable, OPENAI_API_KEY, or providerStatus blockers/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence that hides providerStatus 502 in network events", async () => {
|
||||
const report = acceptedLiveReport();
|
||||
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
|
||||
journey.observations.networkEvents[0].body.error = {
|
||||
code: null,
|
||||
missingEnv: [],
|
||||
providerStatus: 502
|
||||
};
|
||||
await assertRejected(report, /providerStatus blockers/);
|
||||
});
|
||||
|
||||
test("rejects accepted workbench evidence that uses the synthetic gpt-5 placeholder model", async () => {
|
||||
const report = acceptedLiveReport();
|
||||
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
|
||||
journey.evidence = journey.evidence.map((item) => item === "model=gpt-5.5" ? "model=gpt-5" : item);
|
||||
journey.observations.response.model = "gpt-5";
|
||||
journey.observations.networkEvents[0].body.model = "gpt-5";
|
||||
await assertRejected(report, /synthetic placeholder model/);
|
||||
});
|
||||
|
||||
test("accepts the current blocked M5 aggregator non-promotion contract", async () => {
|
||||
await withReport(cloneBaseAggregatorReport(), async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects aggregator DB acceptance promoted from frontend route evidence", async () => {
|
||||
const report = cloneBaseAggregatorReport();
|
||||
const m4Source = report.sourceReports.devM4Agent;
|
||||
m4Source.path = "reports/dev-gate/dev-edge-health.json";
|
||||
const check = report.dod.checks.find((item) => item.id === "cloud-api-db-ready");
|
||||
Object.assign(check, {
|
||||
status: "pass",
|
||||
evidenceLevel: "DEV-LIVE",
|
||||
summary: "Frontend route HTTP 200 proves DB ready."
|
||||
});
|
||||
Object.assign(report.currentDevLayering.dbLive, {
|
||||
status: "pass",
|
||||
evidenceLevel: "DEV-LIVE",
|
||||
summary: "Promoted from active frontend/API route evidence."
|
||||
});
|
||||
await assertRejected(report, /DB DEV-LIVE acceptance source reports/);
|
||||
});
|
||||
|
||||
test("rejects aggregator stale DB runtime-env blocker when DB live is pass", async () => {
|
||||
const report = cloneBaseAggregatorReport();
|
||||
report.blockers.push({
|
||||
id: "dev-gate-preflight:cloud-api-db-health-gate",
|
||||
priority: "P1",
|
||||
type: "runtime_blocker",
|
||||
scope: "cloud-api-db-health-gate",
|
||||
status: "open",
|
||||
source: "reports/dev-gate/dev-preflight-report.json",
|
||||
sourceIssue: "pikasTech/HWLAB#34",
|
||||
summary: "cloud-api DB runtime env is not ready; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE.",
|
||||
unblockOrder: 5,
|
||||
unblocks: ["pikasTech/HWLAB#34", "pikasTech/HWLAB#33", "pikasTech/HWLAB#39"],
|
||||
rationale: "Stale DB runtime-env blocker fixture."
|
||||
});
|
||||
await assertRejected(report, /stale DB live\/runtime-env blocker/);
|
||||
});
|
||||
|
||||
test("rejects aggregator M3 acceptance promoted from source evidence", async () => {
|
||||
const report = cloneBaseAggregatorReport();
|
||||
const sourceEvidence = report.evidence.find((item) =>
|
||||
item.milestone === "M3" && item.category === "hardware-loop-cardinality"
|
||||
);
|
||||
sourceEvidence.level = "DEV-LIVE";
|
||||
report.levels["DEV-LIVE"].push({
|
||||
milestone: "M3",
|
||||
issue: sourceEvidence.issue,
|
||||
taskId: sourceEvidence.taskId,
|
||||
lifecycleState: "active",
|
||||
status: sourceEvidence.status,
|
||||
category: sourceEvidence.category,
|
||||
reportPath: sourceEvidence.reportPath,
|
||||
summary: sourceEvidence.summary
|
||||
});
|
||||
await assertRejected(report, /M3 DEV-LIVE evidence must come from hardware-loop-live/);
|
||||
});
|
||||
|
||||
test("rejects aggregator M4 acceptance promoted from dry-run evidence", async () => {
|
||||
const report = cloneBaseAggregatorReport();
|
||||
const dryRunEvidence = report.evidence.find((item) =>
|
||||
item.milestone === "M4" && item.category === "agent-loop-dry-run"
|
||||
);
|
||||
dryRunEvidence.level = "DEV-LIVE";
|
||||
report.levels["DEV-LIVE"].push({
|
||||
milestone: "M4",
|
||||
issue: dryRunEvidence.issue,
|
||||
taskId: dryRunEvidence.taskId,
|
||||
lifecycleState: "active",
|
||||
status: dryRunEvidence.status,
|
||||
category: dryRunEvidence.category,
|
||||
reportPath: dryRunEvidence.reportPath,
|
||||
summary: dryRunEvidence.summary
|
||||
});
|
||||
await assertRejected(report, /M4 DEV-LIVE evidence must come from agent-loop-live-preflight/);
|
||||
});
|
||||
|
||||
test("rejects aggregator M5 acceptance promoted from fixture evidence", async () => {
|
||||
const report = cloneBaseAggregatorReport();
|
||||
const fixtureEvidence = {
|
||||
milestone: "M5",
|
||||
issue: "pikasTech/HWLAB#39",
|
||||
taskId: "dev-mvp-gate-report",
|
||||
reportPath: "reports/dev-gate/dev-mvp-gate-report.json",
|
||||
commitId: "fixture",
|
||||
lifecycleState: "active",
|
||||
level: "DEV-LIVE",
|
||||
status: "pass",
|
||||
category: "fixture-mvp-e2e",
|
||||
evidence: ["fixture transcript marked green"],
|
||||
commands: ["node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run"],
|
||||
summary: "Fixture evidence was incorrectly promoted to M5 DEV-LIVE."
|
||||
};
|
||||
report.evidence.push(fixtureEvidence);
|
||||
report.levels["DEV-LIVE"].push({
|
||||
milestone: fixtureEvidence.milestone,
|
||||
issue: fixtureEvidence.issue,
|
||||
taskId: fixtureEvidence.taskId,
|
||||
lifecycleState: fixtureEvidence.lifecycleState,
|
||||
status: fixtureEvidence.status,
|
||||
category: fixtureEvidence.category,
|
||||
reportPath: fixtureEvidence.reportPath,
|
||||
summary: fixtureEvidence.summary
|
||||
});
|
||||
await assertRejected(report, /M5 DEV-LIVE evidence must come from mvp-e2e-live/);
|
||||
});
|
||||
|
||||
test("rejects aggregator frontend fact that claims promotion authority", async () => {
|
||||
const report = cloneBaseAggregatorReport();
|
||||
report.latestFrontendDevFact.promotesM3M4M5 = true;
|
||||
await assertRejected(report, /latestFrontendDevFact\.promotesM3M4M5 must be false/);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,10 @@ import {
|
||||
findForbiddenActiveDeprecatedEndpoints,
|
||||
REPORT_LIFECYCLE_VERSION
|
||||
} from "../internal/dev-report-lifecycle.mjs";
|
||||
import { tempReportPath } from "./src/report-paths.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const reportPath = path.join(repoRoot, "reports/dev-gate/dev-m2-deploy-smoke-active.json");
|
||||
const reportPath = tempReportPath("dev-m2-deploy-smoke-active.json");
|
||||
|
||||
function assertString(value, label) {
|
||||
assert.equal(typeof value, "string", `${label} must be a string`);
|
||||
|
||||
@@ -216,14 +216,14 @@ function assertDurableRuntimeRunbook(value) {
|
||||
"Still blocked:",
|
||||
"No full M3, M4, or M5 acceptance is allowed while runtime durability is blocked",
|
||||
"node scripts/dev-runtime-provisioning.mjs --check",
|
||||
"node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --write-report",
|
||||
"node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --write-report",
|
||||
"node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
"node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
"node scripts/dev-runtime-migration.mjs --check",
|
||||
"node scripts/dev-runtime-migration.mjs --dry-run --write-report",
|
||||
"node scripts/dev-runtime-migration.mjs --dry-run --allow-live-db-read --confirm-dev --write-report",
|
||||
"node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --write-report",
|
||||
"node scripts/dev-runtime-migration.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"node scripts/dev-runtime-migration.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"node scripts/dev-runtime-postflight.mjs --check",
|
||||
"node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --write-report"
|
||||
"node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --report /tmp/hwlab-dev-gate/dev-runtime-postflight-report.json"
|
||||
]) {
|
||||
assertIncludes(value, expected, label);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user