feat: add proactive report gate lock evaluator
This commit is contained in:
@@ -28,23 +28,110 @@ const scenarios = [
|
||||
},
|
||||
{
|
||||
name: 'missing next report condition',
|
||||
input: {},
|
||||
expected: {},
|
||||
input: {
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
firstReportTrigger: 'when delegated scan returns or at 10 minutes, whichever comes first',
|
||||
fallbackState: 'paused',
|
||||
reportMode: 'watchdog',
|
||||
},
|
||||
expected: {
|
||||
gateRequired: true,
|
||||
gateStatus: 'fail',
|
||||
reasonIncludes: 'missing next proactive report condition',
|
||||
requiredEvidenceKey: 'nextReportCondition',
|
||||
allowedResponseModesIncludes: 'non_silent_follow_up',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'missing fallback state',
|
||||
input: {},
|
||||
expected: {},
|
||||
input: {
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
firstReportTrigger: 'when delegated scan returns',
|
||||
nextReportCondition: 'report again after verifier output or blocker-state change',
|
||||
reportMode: 'watchdog',
|
||||
},
|
||||
expected: {
|
||||
gateRequired: true,
|
||||
gateStatus: 'fail',
|
||||
reasonIncludes: 'missing fallback state for stalled reporting',
|
||||
requiredEvidenceKey: 'fallbackState',
|
||||
allowedResponseModesIncludes: 'non_silent_follow_up',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'checkpoint-only spoof is insufficient',
|
||||
input: {
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
externalizedCheckpointPath: 'checkpoints/task-123.json',
|
||||
checkpointTrigger: 'when subagent returns',
|
||||
},
|
||||
expected: {
|
||||
gateRequired: true,
|
||||
gateStatus: 'fail',
|
||||
reasonIncludes: 'checkpoint path alone does not satisfy proactive report binding',
|
||||
requiredEvidenceKey: 'firstReportTrigger',
|
||||
allowedResponseModesIncludes: 'non_silent_follow_up',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'valid proactive report binding',
|
||||
input: {},
|
||||
expected: {},
|
||||
input: {
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
firstReportTrigger: 'when delegated scan returns or at 10 minutes, whichever comes first',
|
||||
nextReportCondition: 'report again only after new verifier output or blocker-state change',
|
||||
fallbackState: 'blocked',
|
||||
reportMode: 'watchdog',
|
||||
ownerVisibleIfStalled: true,
|
||||
},
|
||||
expected: {
|
||||
gateRequired: true,
|
||||
gateStatus: 'pass',
|
||||
reasonIncludes: 'proactive report binding is complete for silent progression',
|
||||
allowedResponseModesIncludes: 'silent_continuation',
|
||||
requiredEvidenceLength: 0,
|
||||
reportBindingStatus: 'bound',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'non-silent long-task is not gated',
|
||||
input: {
|
||||
classification: 'long_task',
|
||||
silentCandidate: false,
|
||||
},
|
||||
expected: {
|
||||
gateRequired: false,
|
||||
gateStatus: 'not_applicable',
|
||||
reasonIncludes: 'not a silent progression candidate',
|
||||
allowedResponseModesIncludes: 'direct_reply',
|
||||
requiredEvidenceLength: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'owner decision + button-path handoff',
|
||||
input: {},
|
||||
expected: {},
|
||||
input: {
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
firstReportTrigger: 'when delegated scan returns or at 10 minutes, whichever comes first',
|
||||
nextReportCondition: 'report again only after new verifier output or blocker-state change',
|
||||
fallbackState: 'waiting_user',
|
||||
reportMode: 'button_path',
|
||||
ownerVisibleIfStalled: true,
|
||||
needsOwnerDecision: true,
|
||||
handoffMode: 'button_path',
|
||||
},
|
||||
expected: {
|
||||
gateRequired: true,
|
||||
gateStatus: 'pass',
|
||||
reasonIncludes: 'owner decision flow preserves button-path handoff',
|
||||
allowedResponseModesIncludes: 'button_path',
|
||||
disallowedResponseMode: 'plain_text_closure',
|
||||
requiredEvidenceLength: 0,
|
||||
reportBindingStatus: 'bound',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -99,6 +186,13 @@ function assertScenario(output, expected) {
|
||||
);
|
||||
}
|
||||
|
||||
if (expected.disallowedResponseMode) {
|
||||
assert.ok(
|
||||
!output.allowedResponseModes.includes(expected.disallowedResponseMode),
|
||||
`expected allowedResponseModes to exclude: ${expected.disallowedResponseMode}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof expected.requiredEvidenceLength === 'number') {
|
||||
assert.equal(
|
||||
output.requiredEvidence.length,
|
||||
@@ -113,6 +207,10 @@ function assertScenario(output, expected) {
|
||||
`expected requiredEvidence to include key: ${expected.requiredEvidenceKey}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (expected.reportBindingStatus) {
|
||||
assert.equal(output.reportBindingStatus, expected.reportBindingStatus, 'reportBindingStatus mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
@@ -132,6 +230,7 @@ for (const scenario of scenarios) {
|
||||
reasons: output.reasons,
|
||||
requiredEvidenceKeys: output.requiredEvidence.map((entry) => entry.evidenceKey),
|
||||
allowedResponseModes: output.allowedResponseModes,
|
||||
reportBindingStatus: output.reportBindingStatus,
|
||||
assertion: 'pass',
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user