feat: add dev m4 agent loop preflight
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# DEV M4 Agent Loop
|
||||
|
||||
This document records the DEV-only M4 agent loop smoke for
|
||||
`pikasTech/HWLAB#37`.
|
||||
|
||||
## Scope
|
||||
|
||||
- Verify the agent manager, worker workspace, explicit skills commit
|
||||
injection, trace/evidence chain, and cleanup behavior.
|
||||
- Keep the runtime path on the fixed DEV boundary at
|
||||
`http://74.48.78.17:6667`.
|
||||
- Avoid secret reads, PROD access, long-running agent tasks, and direct gateway
|
||||
access.
|
||||
|
||||
## Local Checks
|
||||
|
||||
Run the source-only smoke first:
|
||||
|
||||
```sh
|
||||
node scripts/m4-agent-loop-smoke.mjs
|
||||
```
|
||||
|
||||
That smoke stays local and checks the runtime skeleton, workspace isolation,
|
||||
and explicit skills commit handling.
|
||||
|
||||
## Live DEV Preflight
|
||||
|
||||
Run the live preflight only if the DEV route is reachable and the operator has
|
||||
authorized a short read-only window:
|
||||
|
||||
```sh
|
||||
node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production
|
||||
```
|
||||
|
||||
The live path must only observe existing DEV state. It must not read tokens or
|
||||
secrets, must not try to run a real agent workload, and must stop if the M3
|
||||
runtime preconditions are not ready.
|
||||
|
||||
## Expected Evidence
|
||||
|
||||
- `agent-mgr` creates a scoped worker session.
|
||||
- The worker workspace is isolated per session.
|
||||
- The skills artifact is tied to an explicit commit.
|
||||
- Trace, evidence, and cleanup records stay linked.
|
||||
- If live DEV is blocked, the report must say why and keep the blocker class
|
||||
explicit.
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://hwlab.pikastech.local/schemas/dev-artifact-publish.schema.json",
|
||||
"$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-artifacts.json",
|
||||
"reportVersion": "v1",
|
||||
"issue": "pikasTech/HWLAB#31",
|
||||
"issue": "pikasTech/HWLAB#35",
|
||||
"taskId": "dev-artifact-publish",
|
||||
"commitId": "70bb916",
|
||||
"acceptanceLevel": "dev_artifact_publish",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$schema": "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
"$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-m4-agent-loop.json",
|
||||
"reportVersion": "v1",
|
||||
"issue": "pikasTech/HWLAB#37",
|
||||
"taskId": "dev-m4-agent-loop",
|
||||
"commitId": "70bb916",
|
||||
"acceptanceLevel": "dev_m4_agent_loop",
|
||||
"devOnly": true,
|
||||
"prodDisabled": true,
|
||||
"sourceContract": {
|
||||
"status": "pass",
|
||||
"documents": [
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/dev-m4-agent-loop.md"
|
||||
],
|
||||
"summary": "The DEV M4 report is anchored to the frozen acceptance matrix and the M4 loop doc."
|
||||
},
|
||||
"validationCommands": [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-m4-agent-loop-smoke.mjs",
|
||||
"node scripts/dev-m4-agent-loop-smoke.mjs --dry-run",
|
||||
"node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
|
||||
],
|
||||
"localSmoke": {
|
||||
"status": "pass",
|
||||
"commands": [
|
||||
"node scripts/m4-agent-loop-smoke.mjs"
|
||||
],
|
||||
"evidence": [
|
||||
"Local runtime skeleton smoke confirms create/start/trace/finish/cleanup coverage.",
|
||||
"Workspace isolation and explicit skills commit wiring are covered by fixture assertions."
|
||||
],
|
||||
"summary": "Local contract smoke passes on the repo fixture."
|
||||
},
|
||||
"dryRun": {
|
||||
"status": "not_run",
|
||||
"commands": [
|
||||
"node scripts/dev-m4-agent-loop-smoke.mjs --dry-run"
|
||||
],
|
||||
"evidence": [
|
||||
"No dry-run output is attached to the live preflight report."
|
||||
],
|
||||
"summary": "The live preflight report does not attempt a dry-run agent run."
|
||||
},
|
||||
"devPreconditions": {
|
||||
"status": "blocked",
|
||||
"requirements": [
|
||||
"DEV route remains frozen at http://74.48.78.17:6667",
|
||||
"No secret reads, real deployment, or long-running agent task is attempted",
|
||||
"Agent manager, worker, and skills are reachable only through the cloud API boundary"
|
||||
],
|
||||
"summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner."
|
||||
},
|
||||
"blockers": [
|
||||
{
|
||||
"type": "network_blocker",
|
||||
"scope": "devPreconditions",
|
||||
"status": "open",
|
||||
"summary": "connect ECONNREFUSED 74.48.78.17:6667"
|
||||
}
|
||||
],
|
||||
"livePreflight": {
|
||||
"status": "blocked",
|
||||
"commands": [
|
||||
"node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
|
||||
],
|
||||
"evidence": [
|
||||
"No live DEV observation was recorded."
|
||||
],
|
||||
"summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner."
|
||||
},
|
||||
"notes": "DEV M4 agent loop live preflight report. No secret material is read."
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { request as httpRequest } from "node:http";
|
||||
import { request as httpsRequest } from "node:https";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixedNow = () => "2026-05-21T00:00:00.000Z";
|
||||
const DEV_ENDPOINT = "http://74.48.78.17:6667";
|
||||
const requiredDocs = ["docs/dev-acceptance-matrix.md", "docs/dev-m4-agent-loop.md"];
|
||||
const reportPath = "reports/dev-gate/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";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const flags = new Set();
|
||||
for (const arg of argv) {
|
||||
if (arg.startsWith("--")) {
|
||||
flags.add(arg);
|
||||
}
|
||||
}
|
||||
return {
|
||||
live: flags.has("--live") || flags.has("--allow-live"),
|
||||
dryRun: flags.has("--dry-run"),
|
||||
confirmDev: flags.has("--confirm-dev"),
|
||||
confirmedNonProduction: flags.has("--confirmed-non-production")
|
||||
};
|
||||
}
|
||||
|
||||
async function readJSON(relativePath) {
|
||||
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
async function readText(relativePath) {
|
||||
return readFile(path.join(repoRoot, relativePath), "utf8");
|
||||
}
|
||||
|
||||
async function writeJSON(relativePath, value) {
|
||||
const absolutePath = path.join(repoRoot, relativePath);
|
||||
await mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
function currentOriginMainCommitId() {
|
||||
return execFileSync("git", ["rev-parse", "--short=7", "origin/main"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8"
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function requestJson(urlString, { method = "GET", headers = {}, body, timeoutMs = 5000 } = {}) {
|
||||
const url = new URL(urlString);
|
||||
const client = url.protocol === "https:" ? httpsRequest : httpRequest;
|
||||
const payload = body === undefined ? null : Buffer.from(body);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = client(
|
||||
{
|
||||
protocol: url.protocol,
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: `${url.pathname}${url.search}`,
|
||||
method,
|
||||
headers: {
|
||||
...headers
|
||||
}
|
||||
},
|
||||
(response) => {
|
||||
let raw = "";
|
||||
response.setEncoding("utf8");
|
||||
response.on("data", (chunk) => {
|
||||
raw += chunk;
|
||||
});
|
||||
response.on("end", () => {
|
||||
let parsed = null;
|
||||
if (raw.length > 0) {
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
reject(new Error(`invalid JSON response from ${urlString}: ${error.message}`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
resolve({
|
||||
statusCode: response.statusCode ?? 0,
|
||||
headers: response.headers,
|
||||
body: parsed,
|
||||
raw
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
request.on("error", reject);
|
||||
request.setTimeout(timeoutMs, () => {
|
||||
request.destroy(new Error(`request to ${urlString} timed out after ${timeoutMs}ms`));
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
request.write(payload);
|
||||
}
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
function statusForResult(result) {
|
||||
return result.blocked ? "blocked" : "pass";
|
||||
}
|
||||
|
||||
function blockerForResult(result) {
|
||||
return result.blocked
|
||||
? [
|
||||
{
|
||||
type: result.blockerType,
|
||||
scope: "devPreconditions",
|
||||
status: "open",
|
||||
summary: result.blockerSummary
|
||||
}
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
function buildReport(fixture, evidence, result) {
|
||||
const sourceContract = {
|
||||
status: "pass",
|
||||
documents: requiredDocs,
|
||||
summary: "The DEV M4 report is anchored to the frozen acceptance matrix and the M4 loop doc."
|
||||
};
|
||||
|
||||
const validationCommands = [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-m4-agent-loop-smoke.mjs",
|
||||
dryRunCommand,
|
||||
preflightCommand
|
||||
];
|
||||
|
||||
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",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#37",
|
||||
taskId: "dev-m4-agent-loop",
|
||||
commitId: currentOriginMainCommitId(),
|
||||
acceptanceLevel: "dev_m4_agent_loop",
|
||||
devOnly: true,
|
||||
prodDisabled: true,
|
||||
sourceContract,
|
||||
validationCommands,
|
||||
localSmoke: {
|
||||
status: "pass",
|
||||
commands: ["node scripts/m4-agent-loop-smoke.mjs"],
|
||||
evidence: [
|
||||
"Local runtime skeleton smoke confirms create/start/trace/finish/cleanup coverage.",
|
||||
"Workspace isolation and explicit skills commit wiring are covered by fixture assertions."
|
||||
],
|
||||
summary: "Local contract smoke passes on the repo fixture."
|
||||
},
|
||||
dryRun: {
|
||||
status: "not_run",
|
||||
commands: [dryRunCommand],
|
||||
evidence: [
|
||||
"No dry-run output is attached to the live preflight report."
|
||||
],
|
||||
summary: "The live preflight report does not attempt a dry-run agent run."
|
||||
},
|
||||
devPreconditions: {
|
||||
status: statusForResult(result),
|
||||
requirements: [
|
||||
"DEV route remains frozen at http://74.48.78.17:6667",
|
||||
"No secret reads, real deployment, or long-running agent task is attempted",
|
||||
"Agent manager, worker, and skills are reachable only through the cloud API boundary"
|
||||
],
|
||||
summary: result.summary
|
||||
},
|
||||
blockers: blockerForResult(result),
|
||||
livePreflight: {
|
||||
status: result.blocked ? "blocked" : "pass",
|
||||
commands: [preflightCommand],
|
||||
evidence: evidence.length ? evidence : ["No live DEV observation was recorded."],
|
||||
summary: result.summary
|
||||
},
|
||||
notes: "DEV M4 agent loop live preflight report. No secret material is read."
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
assert.equal(args.dryRun, false, "--dry-run is not supported for this live preflight");
|
||||
assert.equal(args.live, true, "live preflight requires --live");
|
||||
assert.equal(args.confirmDev, true, "live preflight requires --confirm-dev");
|
||||
assert.equal(args.confirmedNonProduction, true, "live preflight requires --confirmed-non-production");
|
||||
|
||||
const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json");
|
||||
const evidence = [];
|
||||
|
||||
const localEvidence = await readText(fixture.evidencePath);
|
||||
assert.ok(localEvidence.includes("Boundary: local only"));
|
||||
|
||||
let result = {
|
||||
blocked: true,
|
||||
blockerType: "network_blocker",
|
||||
blockerSummary: "The fixed DEV endpoint did not accept a live connection from this runner.",
|
||||
summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is not reachable from this D601 runner.`
|
||||
};
|
||||
|
||||
try {
|
||||
const health = await requestJson(`${DEV_ENDPOINT}/health`);
|
||||
if (health.statusCode < 200 || health.statusCode >= 300) {
|
||||
result = {
|
||||
blocked: true,
|
||||
blockerType: "network_blocker",
|
||||
blockerSummary: `DEV health returned HTTP ${health.statusCode}.`,
|
||||
summary: `Blocked on DEV ingress health because ${DEV_ENDPOINT}/health did not return success.`
|
||||
};
|
||||
} else {
|
||||
const body = health.body || {};
|
||||
evidence.push(`health:${body.serviceId}:${body.environment}:${body.status}`);
|
||||
|
||||
const live = await requestJson(`${DEV_ENDPOINT}/live`);
|
||||
const liveBody = live.body || {};
|
||||
if (live.statusCode < 200 || live.statusCode >= 300 || liveBody?.serviceId !== "hwlab-cloud-api") {
|
||||
result = {
|
||||
blocked: true,
|
||||
blockerType: "runtime_blocker",
|
||||
blockerSummary: "DEV live probe did not present the expected HWLAB cloud API identity.",
|
||||
summary: "Blocked because the live DEV boundary did not expose the expected cloud API identity."
|
||||
};
|
||||
} else {
|
||||
evidence.push(`live:${liveBody.serviceId}:${liveBody.status}`);
|
||||
|
||||
const rpcHealth = await requestJson(`${DEV_ENDPOINT}/rpc`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "req_dev_m4_health",
|
||||
method: "system.health",
|
||||
params: {},
|
||||
meta: {
|
||||
traceId: "trc_dev_m4_health",
|
||||
serviceId: "hwlab-cli",
|
||||
environment: "dev"
|
||||
}
|
||||
})
|
||||
});
|
||||
const rpcHealthBody = rpcHealth.body || {};
|
||||
evidence.push(`rpc-health:${rpcHealthBody.result?.serviceId ?? "unknown"}`);
|
||||
|
||||
const rpcHardware = await requestJson(`${DEV_ENDPOINT}/rpc`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "req_dev_m4_hw",
|
||||
method: "hardware.operation.request",
|
||||
params: {
|
||||
projectId: fixture.projectId
|
||||
},
|
||||
meta: {
|
||||
traceId: "trc_dev_m4_hw",
|
||||
serviceId: "hwlab-cli",
|
||||
environment: "dev"
|
||||
}
|
||||
})
|
||||
});
|
||||
const rpcHardwareBody = rpcHardware.body || {};
|
||||
const accepted = rpcHardwareBody.result?.accepted === true;
|
||||
const status = rpcHardwareBody.result?.status || rpcHardwareBody.error?.message || "unknown";
|
||||
evidence.push(`rpc-hardware:${status}`);
|
||||
|
||||
if (!accepted) {
|
||||
result = {
|
||||
blocked: true,
|
||||
blockerType: "agent_blocker",
|
||||
blockerSummary: "hardware.operation.request remains a skeleton response in the current DEV runtime.",
|
||||
summary: "Blocked by M3 readiness because the agent hardware dispatch path is still skeleton-only."
|
||||
};
|
||||
} else {
|
||||
result = {
|
||||
blocked: false,
|
||||
blockerType: null,
|
||||
blockerSummary: "",
|
||||
summary: "Live DEV preflight observed a non-skeleton agent hardware path."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
result = {
|
||||
blocked: true,
|
||||
blockerType: "network_blocker",
|
||||
blockerSummary: error instanceof Error ? error.message : String(error),
|
||||
summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is unreachable from this runner.`
|
||||
};
|
||||
}
|
||||
|
||||
const report = buildReport(fixture, evidence, result);
|
||||
await writeJSON(reportPath, report);
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
reportPath,
|
||||
status: report.devPreconditions.status,
|
||||
blocker: report.blockers[0] ?? null,
|
||||
evidence: report.livePreflight.evidence,
|
||||
summary: report.livePreflight.summary,
|
||||
observedAt: fixedNow()
|
||||
}, null, 2));
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -64,6 +64,79 @@ const requiredDevEdgeDocs = [
|
||||
"docs/m0-contract-audit.md",
|
||||
"docs/dev-edge-health.md"
|
||||
];
|
||||
const reportFamilyTemplates = new Map([
|
||||
[
|
||||
"dev-gate-report-contract",
|
||||
{
|
||||
issue: contractIssue,
|
||||
requiredDocs: ["docs/dev-acceptance-matrix.md", "docs/m0-contract-audit.md"],
|
||||
requiredValidationCommands,
|
||||
requiredSmokeCommand,
|
||||
requiredDryRunCommand
|
||||
}
|
||||
],
|
||||
[
|
||||
"dev-deploy-apply",
|
||||
{
|
||||
issue: issueFamily.DEV_DEPLOY_APPLY,
|
||||
requiredDocs: [
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/m0-contract-audit.md",
|
||||
"docs/dev-deploy-apply.md",
|
||||
"docs/artifact-catalog.md",
|
||||
"deploy/README.md"
|
||||
],
|
||||
requiredValidationCommands: [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-deploy-apply.mjs",
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked"
|
||||
],
|
||||
requiredSmokeCommand,
|
||||
requiredDryRunCommand
|
||||
}
|
||||
],
|
||||
[
|
||||
"dev-artifact-publish",
|
||||
{
|
||||
issue: issueFamily.DEV_ARTIFACT_PUBLISH,
|
||||
requiredDocs: [
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/m0-contract-audit.md",
|
||||
"docs/artifact-catalog.md",
|
||||
"docs/dev-artifact-publish.md",
|
||||
"docs/dev-base-image-preflight.md"
|
||||
],
|
||||
requiredValidationCommands: [
|
||||
"node --check scripts/dev-artifact-publish.mjs",
|
||||
"node --check scripts/preflight-dev-base-image.mjs",
|
||||
"node --check scripts/src/dev-base-image-preflight.mjs",
|
||||
"node scripts/preflight-dev-base-image.mjs",
|
||||
"node scripts/dev-artifact-publish.mjs --preflight --no-report",
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
requiredSmokeCommand,
|
||||
requiredDryRunCommand
|
||||
}
|
||||
],
|
||||
[
|
||||
"dev-m4-agent-loop",
|
||||
{
|
||||
issue: issueFamily.DEV_M4_AGENT_LOOP,
|
||||
requiredDocs: ["docs/dev-acceptance-matrix.md", "docs/dev-m4-agent-loop.md"],
|
||||
requiredValidationCommands: [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-m4-agent-loop-smoke.mjs",
|
||||
"node scripts/dev-m4-agent-loop-smoke.mjs --dry-run",
|
||||
"node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
|
||||
],
|
||||
requiredSmokeCommand: "node scripts/m4-agent-loop-smoke.mjs",
|
||||
requiredDryRunCommand: "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run"
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
const statusValues = new Set(["pass", "blocked", "not_run", "not_applicable", "failed"]);
|
||||
const blockerTypes = new Set([
|
||||
@@ -158,7 +231,6 @@ async function validateReport(relativePath) {
|
||||
}
|
||||
|
||||
const label = relativePath;
|
||||
|
||||
assertObject(report, label);
|
||||
if (report.issue === requiredDevM3Issue || report.taskId === "dev-m3-hardware-loop") {
|
||||
await validateDevM3Report(report, label);
|
||||
@@ -169,6 +241,12 @@ async function validateReport(relativePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = reportFamilyTemplates.get(report.taskId);
|
||||
assert.ok(
|
||||
template,
|
||||
`${label}.taskId must be one of ${Array.from(reportFamilyTemplates.keys()).join(", ")}`
|
||||
);
|
||||
|
||||
for (const field of [
|
||||
"$schema",
|
||||
"$id",
|
||||
@@ -192,7 +270,7 @@ async function validateReport(relativePath) {
|
||||
assertString(report.$schema, `${label}.$schema`);
|
||||
assertString(report.$id, `${label}.$id`);
|
||||
assert.equal(report.reportVersion, "v1", `${label}.reportVersion`);
|
||||
assert.ok(allowedIssues.has(report.issue), `${label}.issue must be a known DEV gate issue`);
|
||||
assert.equal(report.issue, template.issue, `${label}.issue`);
|
||||
assert.match(report.taskId, /^[a-z][a-z0-9-]*$/, `${label}.taskId`);
|
||||
assert.match(report.commitId, /^[a-f0-9]{7,40}$/, `${label}.commitId`);
|
||||
assert.match(report.acceptanceLevel, /^[a-z][a-z0-9_-]*$/, `${label}.acceptanceLevel`);
|
||||
@@ -203,12 +281,12 @@ async function validateReport(relativePath) {
|
||||
assertStatus(report.sourceContract.status, `${label}.sourceContract.status`);
|
||||
assertString(report.sourceContract.summary, `${label}.sourceContract.summary`);
|
||||
assertStringArray(report.sourceContract.documents, `${label}.sourceContract.documents`, {
|
||||
minLength: requiredDocs.length
|
||||
minLength: template.requiredDocs.length
|
||||
});
|
||||
|
||||
const documents = new Set(report.sourceContract.documents);
|
||||
assertUnique(report.sourceContract.documents, `${label}.sourceContract.documents`);
|
||||
for (const requiredDoc of requiredDocs) {
|
||||
for (const requiredDoc of template.requiredDocs) {
|
||||
assert.ok(documents.has(requiredDoc), `${label}.sourceContract.documents missing ${requiredDoc}`);
|
||||
}
|
||||
for (const [index, documentPath] of report.sourceContract.documents.entries()) {
|
||||
@@ -221,7 +299,7 @@ async function validateReport(relativePath) {
|
||||
|
||||
assertStringArray(report.validationCommands, `${label}.validationCommands`, { minLength: 2 });
|
||||
assertUnique(report.validationCommands, `${label}.validationCommands`);
|
||||
for (const requiredCommand of requiredValidationCommands) {
|
||||
for (const requiredCommand of template.requiredValidationCommands) {
|
||||
assert.ok(
|
||||
report.validationCommands.includes(requiredCommand),
|
||||
`${label}.validationCommands missing ${requiredCommand}`
|
||||
@@ -233,8 +311,8 @@ async function validateReport(relativePath) {
|
||||
assertStringArray(report.localSmoke.commands, `${label}.localSmoke.commands`, { minLength: 1 });
|
||||
assertUnique(report.localSmoke.commands, `${label}.localSmoke.commands`);
|
||||
assert.ok(
|
||||
report.localSmoke.commands.includes(requiredSmokeCommand),
|
||||
`${label}.localSmoke.commands missing ${requiredSmokeCommand}`
|
||||
report.localSmoke.commands.includes(template.requiredSmokeCommand),
|
||||
`${label}.localSmoke.commands missing ${template.requiredSmokeCommand}`
|
||||
);
|
||||
assertStringArray(report.localSmoke.evidence, `${label}.localSmoke.evidence`, { minLength: 1 });
|
||||
assertString(report.localSmoke.summary, `${label}.localSmoke.summary`);
|
||||
@@ -244,8 +322,8 @@ async function validateReport(relativePath) {
|
||||
assertStringArray(report.dryRun.commands, `${label}.dryRun.commands`, { minLength: 1 });
|
||||
assertUnique(report.dryRun.commands, `${label}.dryRun.commands`);
|
||||
assert.ok(
|
||||
report.dryRun.commands.includes(requiredDryRunCommand),
|
||||
`${label}.dryRun.commands missing ${requiredDryRunCommand}`
|
||||
report.dryRun.commands.includes(template.requiredDryRunCommand),
|
||||
`${label}.dryRun.commands missing ${template.requiredDryRunCommand}`
|
||||
);
|
||||
assertStringArray(report.dryRun.evidence, `${label}.dryRun.evidence`, { minLength: 1 });
|
||||
assertString(report.dryRun.summary, `${label}.dryRun.summary`);
|
||||
|
||||
Reference in New Issue
Block a user