feat: add web sentinel session invariance checks
This commit is contained in:
@@ -1654,6 +1654,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
});
|
||||
}
|
||||
let promptIndex = 0;
|
||||
const sessionInvarianceChecks = sessionInvarianceChecksByRound(scenario);
|
||||
for (const item of arrayAt(scenario, "commandSequence").map(record)) {
|
||||
const type = stringAt(item, "type");
|
||||
const repeat = Math.max(1, typeof item.repeat === "number" && Number.isFinite(item.repeat) ? Math.trunc(item.repeat) : 1);
|
||||
@@ -1711,6 +1712,21 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
warnings: mergeWarnings(Array.isArray(waitResult.warnings) ? waitResult.warnings : [], elapsedWarnings()),
|
||||
}));
|
||||
}
|
||||
const invariantResult = runQuickVerifySessionInvarianceChecks(state, observerId, sessionInvarianceChecks.get(promptIndex) ?? [], deadline, promptIndex, steps);
|
||||
if (invariantResult.ok !== true) {
|
||||
return recordQuickVerify(state, finalizeQuickVerifyFailure(state, {
|
||||
runId,
|
||||
scenarioId,
|
||||
reason,
|
||||
observerId,
|
||||
promptIndex,
|
||||
steps,
|
||||
failure: text(invariantResult.failure ?? "observe-session-invariance-check-failed"),
|
||||
promptSource: prompts.summary,
|
||||
elapsedMs: elapsedMs(),
|
||||
warnings: elapsedWarnings(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1756,6 +1772,65 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
|
||||
});
|
||||
}
|
||||
|
||||
function sessionInvarianceChecksByRound(scenario: Record<string, unknown>): Map<number, Record<string, unknown>[]> {
|
||||
const checks = new Map<number, Record<string, unknown>[]>();
|
||||
const items = Array.isArray(scenario.sessionInvarianceChecks) ? scenario.sessionInvarianceChecks.map(record) : [];
|
||||
for (const item of items) {
|
||||
const afterRound = typeof item.afterRound === "number" && Number.isInteger(item.afterRound) ? item.afterRound : null;
|
||||
if (afterRound === null || afterRound < 0) continue;
|
||||
const list = checks.get(afterRound) ?? [];
|
||||
list.push(item);
|
||||
checks.set(afterRound, list);
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
|
||||
function runQuickVerifySessionInvarianceChecks(
|
||||
state: SentinelCicdState,
|
||||
observerId: string,
|
||||
checks: readonly Record<string, unknown>[],
|
||||
deadline: number,
|
||||
promptIndex: number,
|
||||
steps: Record<string, unknown>[],
|
||||
): Record<string, unknown> {
|
||||
for (const check of checks) {
|
||||
const checkId = nonEmptyString(check.id) ?? `after-round-${promptIndex}`;
|
||||
const commands: { readonly type: string; readonly enabled: boolean }[] = [
|
||||
{ type: "refreshCurrentSession", enabled: check.refreshCurrent === true },
|
||||
{ type: "switchAwayAndBack", enabled: check.switchAwayAndBack === true },
|
||||
{ type: "assertSessionInvariant", enabled: check.assertSessionInvariant !== false },
|
||||
];
|
||||
for (const command of commands) {
|
||||
if (!command.enabled) continue;
|
||||
const args = [
|
||||
"web-probe", "observe", "command", observerId,
|
||||
"--node", state.spec.nodeId,
|
||||
"--lane", state.spec.lane,
|
||||
"--type", command.type,
|
||||
"--after-round", String(promptIndex),
|
||||
"--wait-ms", "55000",
|
||||
"--command-timeout-seconds", String(remainingSeconds(deadline, 55)),
|
||||
];
|
||||
const severity = nonEmptyString(check.severity);
|
||||
const findingId = nonEmptyString(check.findingId);
|
||||
const expectedSentinelRange = nonEmptyString(check.expectedSentinelRange);
|
||||
const alternateSessionStrategy = nonEmptyString(check.alternateSessionStrategy);
|
||||
if (severity !== null) args.push("--severity", severity);
|
||||
if (findingId !== null) args.push("--finding-id", findingId);
|
||||
if (expectedSentinelRange !== null) args.push("--expected-sentinel-range", expectedSentinelRange);
|
||||
if (command.type === "switchAwayAndBack" && alternateSessionStrategy !== null) args.push("--alternate-session-strategy", alternateSessionStrategy);
|
||||
if (command.type === "assertSessionInvariant" && check.requireComposerReady === true) args.push("--require-composer-ready");
|
||||
args.push(check.blocking === true ? "--blocking" : "--non-blocking");
|
||||
const result = runChildCli(args, remainingSeconds(deadline, 60));
|
||||
steps.push({ phase: `observe-session-invariance-${command.type}`, ok: result.ok, promptIndex, checkId, result: result.result });
|
||||
if (!result.ok) {
|
||||
return { ok: false, failure: `observe-session-invariance-${command.type}-failed`, checkId, promptIndex, valuesRedacted: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ok: true, promptIndex, checkCount: checks.length, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
||||
readonly runId: string;
|
||||
readonly scenarioId: string;
|
||||
@@ -2172,7 +2247,7 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
|
||||
"let artifactCount=0; let screenshot=null;",
|
||||
"function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}",
|
||||
"walk(stateDir);",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220)};});",
|
||||
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
|
||||
"const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});",
|
||||
"console.log(JSON.stringify({ok:!!report,reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));",
|
||||
"NODE",
|
||||
@@ -2438,6 +2513,7 @@ function readPromptSetForScenario(scenario: Record<string, unknown>): { ok: true
|
||||
summary: {
|
||||
...summary,
|
||||
promptCount: parsed.length,
|
||||
promptMarkers: parsed.map((item) => Array.from(new Set(Array.from(item.matchAll(/\bsentinel-(?:0[1-9]|10)\b/giu)).map((match) => match[0].toLowerCase())))),
|
||||
promptTextHashes: parsed.map((item) => `sha256:${createHash("sha256").update(item).digest("hex").slice(0, 16)}`),
|
||||
promptTextBytes: parsed.map((item) => Buffer.byteLength(item)),
|
||||
valuesRedacted: true,
|
||||
|
||||
Reference in New Issue
Block a user