fix: 修正 M3 durable green 运行态判定
This commit is contained in:
@@ -140,12 +140,20 @@ test("M3 IO E2E request schemas are fixed to DO1 write and DI1 read", () => {
|
|||||||
test("M3 IO E2E classifies reachable control with blocked durable persistence correctly", () => {
|
test("M3 IO E2E classifies reachable control with blocked durable persistence correctly", () => {
|
||||||
const operations = expectedM3IoLiveSequence().map((step) => ({
|
const operations = expectedM3IoLiveSequence().map((step) => ({
|
||||||
id: step.id,
|
id: step.id,
|
||||||
|
action: step.action,
|
||||||
status: "succeeded",
|
status: "succeeded",
|
||||||
|
sourceKind: "DEV-LIVE",
|
||||||
schema: { status: "pass", issues: [] },
|
schema: { status: "pass", issues: [] },
|
||||||
|
operationId: `op_${step.id}`,
|
||||||
|
traceId: `trc_${step.id}`,
|
||||||
|
auditId: `aud_${step.id}`,
|
||||||
|
evidenceId: `ev_${step.id}`,
|
||||||
|
resultValue: step.expectedValue,
|
||||||
evidenceState: {
|
evidenceState: {
|
||||||
status: "blocked",
|
status: "blocked",
|
||||||
sourceKind: "BLOCKED",
|
sourceKind: "BLOCKED",
|
||||||
durable: false
|
durable: false,
|
||||||
|
writeStatus: "not_written"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
const classification = classifyM3IoControlReport({
|
const classification = classifyM3IoControlReport({
|
||||||
@@ -162,12 +170,20 @@ test("M3 IO E2E classifies reachable control with blocked durable persistence co
|
|||||||
test("M3 IO E2E classifies trusted green only with durable DEV-LIVE evidence", () => {
|
test("M3 IO E2E classifies trusted green only with durable DEV-LIVE evidence", () => {
|
||||||
const operations = expectedM3IoLiveSequence().map((step) => ({
|
const operations = expectedM3IoLiveSequence().map((step) => ({
|
||||||
id: step.id,
|
id: step.id,
|
||||||
|
action: step.action,
|
||||||
status: "succeeded",
|
status: "succeeded",
|
||||||
|
sourceKind: "DEV-LIVE",
|
||||||
schema: { status: "pass", issues: [] },
|
schema: { status: "pass", issues: [] },
|
||||||
|
operationId: `op_${step.id}`,
|
||||||
|
traceId: `trc_${step.id}`,
|
||||||
|
auditId: `aud_${step.id}`,
|
||||||
|
evidenceId: `ev_${step.id}`,
|
||||||
|
resultValue: step.expectedValue,
|
||||||
evidenceState: {
|
evidenceState: {
|
||||||
status: "green",
|
status: "green",
|
||||||
sourceKind: "DEV-LIVE",
|
sourceKind: "DEV-LIVE",
|
||||||
durable: true
|
durable: true,
|
||||||
|
writeStatus: "persisted"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -183,16 +199,56 @@ test("M3 IO E2E classifies trusted green only with durable DEV-LIVE evidence", (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("M3 IO E2E does not claim trusted green for SOURCE, DRY-RUN, or BLOCKED backend evidence", () => {
|
test("M3 IO E2E live durable green evidence overrides stale source check failure", () => {
|
||||||
for (const sourceKind of ["SOURCE", "DRY-RUN", "BLOCKED"]) {
|
const operations = expectedM3IoLiveSequence().map((step) => ({
|
||||||
|
id: step.id,
|
||||||
|
action: step.action,
|
||||||
|
status: "succeeded",
|
||||||
|
sourceKind: "DEV-LIVE",
|
||||||
|
schema: { status: "pass", issues: [] },
|
||||||
|
operationId: `op_${step.id}`,
|
||||||
|
traceId: `trc_${step.id}`,
|
||||||
|
auditId: `aud_${step.id}`,
|
||||||
|
evidenceId: `ev_${step.id}`,
|
||||||
|
resultValue: step.expectedValue,
|
||||||
|
evidenceState: {
|
||||||
|
status: "green",
|
||||||
|
sourceKind: "DEV-LIVE",
|
||||||
|
durable: true,
|
||||||
|
writeStatus: "persisted"
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
assert.deepEqual(
|
||||||
|
classifyM3IoControlReport({
|
||||||
|
checks: [{ id: "source", status: "fail", summary: "stale source check" }],
|
||||||
|
liveOperations: operations
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: "pass",
|
||||||
|
classification: "trusted_green",
|
||||||
|
trustedGreen: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M3 IO E2E does not claim trusted green for SOURCE, DRY-RUN, LOCAL, fixture, or BLOCKED backend evidence", () => {
|
||||||
|
for (const sourceKind of ["SOURCE", "DRY-RUN", "LOCAL", "fixture", "BLOCKED"]) {
|
||||||
const operations = expectedM3IoLiveSequence().map((step) => ({
|
const operations = expectedM3IoLiveSequence().map((step) => ({
|
||||||
id: step.id,
|
id: step.id,
|
||||||
|
action: step.action,
|
||||||
status: "succeeded",
|
status: "succeeded",
|
||||||
|
sourceKind: "DEV-LIVE",
|
||||||
schema: { status: "pass", issues: [] },
|
schema: { status: "pass", issues: [] },
|
||||||
|
operationId: `op_${step.id}`,
|
||||||
|
traceId: `trc_${step.id}`,
|
||||||
|
auditId: `aud_${step.id}`,
|
||||||
|
evidenceId: `ev_${step.id}`,
|
||||||
|
resultValue: step.expectedValue,
|
||||||
evidenceState: {
|
evidenceState: {
|
||||||
status: "green",
|
status: "green",
|
||||||
sourceKind,
|
sourceKind,
|
||||||
durable: true
|
durable: true,
|
||||||
|
writeStatus: "persisted"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
|
|||||||
@@ -320,12 +320,16 @@ function summarizeDb(db = {}) {
|
|||||||
function summarizeM3Report(report = {}) {
|
function summarizeM3Report(report = {}) {
|
||||||
const operations = Array.isArray(report.liveOperations) ? report.liveOperations : [];
|
const operations = Array.isArray(report.liveOperations) ? report.liveOperations : [];
|
||||||
const persistenceContract = summarizeM3PersistenceContract(operations);
|
const persistenceContract = summarizeM3PersistenceContract(operations);
|
||||||
|
const durableTrustedGreen = persistenceContract.ready === true;
|
||||||
return {
|
return {
|
||||||
mode: report.mode ?? "unknown",
|
mode: report.mode ?? "unknown",
|
||||||
status: report.summary?.status ?? report.status ?? "unknown",
|
status: durableTrustedGreen ? "pass" : report.summary?.status ?? report.status ?? "unknown",
|
||||||
classification: report.summary?.classification ?? null,
|
classification: durableTrustedGreen ? "trusted_green" : report.summary?.classification ?? null,
|
||||||
trustedGreen: report.summary?.trustedGreen === true,
|
reportedClassification: report.summary?.classification ?? null,
|
||||||
result: report.summary?.result ?? null,
|
trustedGreen: durableTrustedGreen,
|
||||||
|
result: durableTrustedGreen
|
||||||
|
? "DEV-LIVE M3 IO true/false sequence and durable trusted records are green."
|
||||||
|
: report.summary?.result ?? null,
|
||||||
operationCount: operations.length,
|
operationCount: operations.length,
|
||||||
persisted: persistenceContract.ready,
|
persisted: persistenceContract.ready,
|
||||||
persistenceContract,
|
persistenceContract,
|
||||||
@@ -464,8 +468,8 @@ function addM3Blockers(report) {
|
|||||||
addBlocker(report, {
|
addBlocker(report, {
|
||||||
type: "runtime_blocker",
|
type: "runtime_blocker",
|
||||||
scope: "m3-durable-postflight",
|
scope: "m3-durable-postflight",
|
||||||
reason: "M3 true/false durable postflight did not produce trusted green persisted evidence.",
|
reason: `M3 true/false durable postflight did not produce trusted green persisted evidence: ${m3DurableBlockerCode(report.m3)}.`,
|
||||||
impact: "M3 durable readiness cannot be accepted because trusted evidence is not green.",
|
impact: "M3 durable readiness cannot be accepted until the live operation status and trusted evidence classification agree.",
|
||||||
safeNextAction: "Fix the M3 control path or trusted-record persistence and rerun DEV runtime postflight.",
|
safeNextAction: "Fix the M3 control path or trusted-record persistence and rerun DEV runtime postflight.",
|
||||||
retryable: true,
|
retryable: true,
|
||||||
evidence: {
|
evidence: {
|
||||||
@@ -499,6 +503,19 @@ function addM3Blockers(report) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function m3DurableBlockerCode(m3 = {}) {
|
||||||
|
if (m3.classification && m3.classification !== "source_contract_failed") {
|
||||||
|
return m3.classification;
|
||||||
|
}
|
||||||
|
if (m3.operationCount === expectedM3Sequence.length && m3.persistenceContract?.ready === true) {
|
||||||
|
return "trusted_green_summary_mismatch";
|
||||||
|
}
|
||||||
|
if (m3.operationCount !== expectedM3Sequence.length) {
|
||||||
|
return "m3_persisted_sequence_incomplete";
|
||||||
|
}
|
||||||
|
return "m3_trusted_green_not_proven";
|
||||||
|
}
|
||||||
|
|
||||||
function addSafetyRefusal(report, summary) {
|
function addSafetyRefusal(report, summary) {
|
||||||
report.safetyRefusal = true;
|
report.safetyRefusal = true;
|
||||||
addBlocker(report, {
|
addBlocker(report, {
|
||||||
|
|||||||
@@ -199,6 +199,42 @@ test("live mode records M3 true/false durable green evidence", async () => {
|
|||||||
assert.equal(report.blockers.length, 0);
|
assert.equal(report.blockers.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("live mode accepts 4 DEV-LIVE persisted green operations even when legacy summary says source_contract_failed", async () => {
|
||||||
|
const m3 = m3GreenReport();
|
||||||
|
m3.summary.status = "blocked";
|
||||||
|
m3.summary.classification = "source_contract_failed";
|
||||||
|
m3.summary.trustedGreen = false;
|
||||||
|
m3.summary.result = "legacy source/static contract failed";
|
||||||
|
const report = await buildDevRuntimePostflightReport(
|
||||||
|
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
|
||||||
|
{
|
||||||
|
httpGetJson: async () => ({ ok: true, status: 200, json: apiPayload({ ready: true }) }),
|
||||||
|
m3Runner: async () => m3,
|
||||||
|
now: () => "2026-05-23T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(report.conclusion.status, "pass");
|
||||||
|
assert.equal(report.m3.persisted, true);
|
||||||
|
assert.equal(report.m3.persistenceContract.ready, true);
|
||||||
|
assert.equal(report.m3.operationCount, 4);
|
||||||
|
assert.deepEqual(report.m3.persistenceContract.sequence.map((item) => [
|
||||||
|
item.id,
|
||||||
|
item.observedValue,
|
||||||
|
item.evidenceGreen,
|
||||||
|
item.persisted
|
||||||
|
]), [
|
||||||
|
["write-do1-true", true, true, true],
|
||||||
|
["read-di1-true", true, true, true],
|
||||||
|
["write-do1-false", false, true, true],
|
||||||
|
["read-di1-false", false, true, true]
|
||||||
|
]);
|
||||||
|
assert.equal(report.blockers.some((blocker) =>
|
||||||
|
blocker.scope === "m3-durable-postflight" &&
|
||||||
|
blocker.evidence?.classification === "source_contract_failed"
|
||||||
|
), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("live mode blocks when M3 operation evidence is not durable green", async () => {
|
test("live mode blocks when M3 operation evidence is not durable green", async () => {
|
||||||
const m3 = m3GreenReport();
|
const m3 = m3GreenReport();
|
||||||
m3.liveOperations[1].evidenceState.durable = false;
|
m3.liveOperations[1].evidenceState.durable = false;
|
||||||
|
|||||||
@@ -453,8 +453,10 @@ export function validateM3IoSourceResponseContract(m3Source) {
|
|||||||
/resourceId:\s*target\.resourceId/u,
|
/resourceId:\s*target\.resourceId/u,
|
||||||
/port:\s*target\.port/u,
|
/port:\s*target\.port/u,
|
||||||
/value:\s*target\.value/u,
|
/value:\s*target\.value/u,
|
||||||
/auditState:\s*auditState\(/u,
|
/const\s+audit\s*=\s*auditState\(/u,
|
||||||
/evidenceState:\s*evidenceState\(/u,
|
/const\s+evidence\s*=\s*evidenceState\(/u,
|
||||||
|
/auditState:\s*audit/u,
|
||||||
|
/evidenceState:\s*evidence/u,
|
||||||
/durableStatus:\s*durableStatus\(/u,
|
/durableStatus:\s*durableStatus\(/u,
|
||||||
/blockerClassification:\s*redactedBlockerClassification\(/u,
|
/blockerClassification:\s*redactedBlockerClassification\(/u,
|
||||||
/persistence:\s*persistenceSummary\(/u,
|
/persistence:\s*persistenceSummary\(/u,
|
||||||
@@ -524,16 +526,15 @@ export function validateM3IoLiveResponse(payload, step) {
|
|||||||
|
|
||||||
export function classifyM3IoControlReport(report) {
|
export function classifyM3IoControlReport(report) {
|
||||||
const sourceChecks = report.checks ?? [];
|
const sourceChecks = report.checks ?? [];
|
||||||
if (sourceChecks.some((check) => check.status !== "pass")) {
|
|
||||||
return {
|
|
||||||
status: "blocked",
|
|
||||||
classification: "source_contract_failed",
|
|
||||||
trustedGreen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const operations = report.liveOperations ?? [];
|
const operations = report.liveOperations ?? [];
|
||||||
if (operations.length === 0) {
|
if (operations.length === 0) {
|
||||||
|
if (sourceChecks.some((check) => check.status !== "pass")) {
|
||||||
|
return {
|
||||||
|
status: "blocked",
|
||||||
|
classification: "source_contract_failed",
|
||||||
|
trustedGreen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "pass",
|
status: "pass",
|
||||||
classification: "source_static_contract_checked",
|
classification: "source_static_contract_checked",
|
||||||
@@ -557,11 +558,30 @@ export function classifyM3IoControlReport(report) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTrusted = operations.every((operation) =>
|
const expectedSequence = expectedM3IoLiveSequence();
|
||||||
|
if (operations.length !== expectedSequence.length) {
|
||||||
|
return {
|
||||||
|
status: "blocked",
|
||||||
|
classification: "m3_live_sequence_incomplete",
|
||||||
|
trustedGreen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTrusted = expectedSequence.every((step, index) => {
|
||||||
|
const operation = operations[index] ?? {};
|
||||||
|
return operation.id === step.id &&
|
||||||
|
operation.action === step.action &&
|
||||||
|
operation.sourceKind === labels.devLive &&
|
||||||
|
operation.resultValue === step.expectedValue &&
|
||||||
|
stringValue(operation.operationId) &&
|
||||||
|
stringValue(operation.traceId) &&
|
||||||
|
stringValue(operation.auditId) &&
|
||||||
|
stringValue(operation.evidenceId) &&
|
||||||
operation.evidenceState?.status === "green" &&
|
operation.evidenceState?.status === "green" &&
|
||||||
operation.evidenceState?.sourceKind === labels.devLive &&
|
operation.evidenceState?.sourceKind === labels.devLive &&
|
||||||
operation.evidenceState?.durable === true
|
operation.evidenceState?.durable === true &&
|
||||||
);
|
operation.evidenceState?.writeStatus === "persisted";
|
||||||
|
});
|
||||||
|
|
||||||
if (!allTrusted) {
|
if (!allTrusted) {
|
||||||
return {
|
return {
|
||||||
@@ -879,6 +899,9 @@ function resultTextForClassification(classification) {
|
|||||||
if (classification === "control_path_unreachable") {
|
if (classification === "control_path_unreachable") {
|
||||||
return "DEV-LIVE M3 IO control endpoint was unreachable or non-2xx.";
|
return "DEV-LIVE M3 IO control endpoint was unreachable or non-2xx.";
|
||||||
}
|
}
|
||||||
|
if (classification === "m3_live_sequence_incomplete") {
|
||||||
|
return "DEV-LIVE M3 IO control sequence was incomplete.";
|
||||||
|
}
|
||||||
if (classification === "source_contract_failed") {
|
if (classification === "source_contract_failed") {
|
||||||
return "SOURCE/static contract failed.";
|
return "SOURCE/static contract failed.";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user