feat(orchestrator): enforce Page Data validation gate

This commit is contained in:
john
2026-07-25 14:27:07 +08:00
parent 417aa9c78b
commit 721db19a92
10 changed files with 432 additions and 43 deletions
+175
View File
@@ -609,6 +609,181 @@ test('Page Data validation observation failure stays isolated from the Native re
assert.equal(failure.code, 'VALIDATION_OBSERVER_UNAVAILABLE');
});
test('enforced Page Data validation waits for Orchestrator passed before Native success', async () => {
const pool = createFakePool();
const validationStatuses = [];
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-page-data-gate-pass' };
},
async submitSessionReplyForUser() {},
},
observeWorkflowRun: async () => ({ observed: true }),
observeWorkflowValidation: async (input) => {
validationStatuses.push(pool.runs.get(input.runId)?.status);
assert.deepEqual(
input.observation.checks
.filter((check) => [
'agent_run_completion',
'page_data_binding',
'page_data_storage_policy',
].includes(check.id))
.map((check) => [check.id, check.status]),
[
['agent_run_completion', 'passed'],
['page_data_binding', 'passed'],
['page_data_storage_policy', 'passed'],
],
);
return {
observed: true,
engine: 'langgraph',
mode: 'shadow',
configVersion: 12,
validation: {
kind: 'page-data-delivery',
verdict: 'passed',
},
};
},
syncUserPagesOnSuccess: async () => ({
pageDataBind: { errors: [] },
pageDataRelativePaths: [],
}),
validateRunDeliverables: async () => ({ errors: [] }),
enforcePageDataWorkflowValidation: true,
retryDelaysMs: [],
});
const run = await gateway.createRun('user-page-data-gate-pass', {
requestId: 'req-page-data-gate-pass',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '解释 Page Data API' }],
metadata: {
displayText: '解释 Page Data API',
memindRun: { pgRequired: true },
},
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'succeeded');
assert.deepEqual(validationStatuses, ['running']);
assert.ok(pool.events.some(
(event) => event.runId === run.id
&& event.eventType === 'workflow_validation_gate_passed',
));
assert.equal(pool.events.some(
(event) => event.runId === run.id
&& event.eventType === 'workflow_validation_observation_completed',
), false);
});
test('enforced Page Data validation fails closed on an inconclusive verdict', async () => {
const pool = createFakePool();
let validationCalls = 0;
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-page-data-gate-inconclusive' };
},
async submitSessionReplyForUser() {},
},
observeWorkflowRun: async () => ({ observed: true }),
observeWorkflowValidation: async () => {
validationCalls += 1;
return {
observed: true,
engine: 'langgraph',
mode: 'shadow',
validation: {
kind: 'page-data-delivery',
verdict: 'inconclusive',
},
};
},
enforcePageDataWorkflowValidation: true,
retryDelaysMs: [],
});
const run = await gateway.createRun('user-page-data-gate-inconclusive', {
requestId: 'req-page-data-gate-inconclusive',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '解释 Page Data API' }],
metadata: {
displayText: '解释 Page Data API',
memindRun: { pgRequired: true },
},
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
assert.equal(validationCalls, 1);
assert.match(pool.runs.get(run.id).error_message, /did not pass: inconclusive/);
const failed = pool.events.find(
(event) => event.runId === run.id
&& event.eventType === 'workflow_validation_gate_failed',
);
assert.ok(failed);
assert.equal(
JSON.parse(failed.dataJson).code,
'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE',
);
});
test('enforced Page Data validation fails closed without retrying when Orchestrator is unavailable', async () => {
const pool = createFakePool();
let validationCalls = 0;
const gateway = createAgentRunGateway({
pool,
userAuth: {},
tkmindProxy: {
async startSessionForUser() {
return { id: 'session-page-data-gate-unavailable' };
},
async submitSessionReplyForUser() {},
},
observeWorkflowRun: async () => ({ observed: true }),
observeWorkflowValidation: async () => {
validationCalls += 1;
throw Object.assign(new Error('orchestrator unavailable'), {
code: 'ORCHESTRATOR_UNAVAILABLE',
});
},
enforcePageDataWorkflowValidation: true,
retryDelaysMs: [1, 1, 1],
});
const run = await gateway.createRun('user-page-data-gate-unavailable', {
requestId: 'req-page-data-gate-unavailable',
userMessage: {
role: 'user',
content: [{ type: 'text', text: '解释 Page Data API' }],
metadata: {
displayText: '解释 Page Data API',
memindRun: { pgRequired: true },
},
},
});
await waitFor(() => pool.runs.get(run.id)?.status === 'failed');
assert.equal(validationCalls, 1);
assert.equal(pool.runs.get(run.id).attempts, 1);
const failed = pool.events.find(
(event) => event.runId === run.id
&& event.eventType === 'workflow_validation_gate_failed',
);
const failure = JSON.parse(failed.dataJson);
assert.equal(failure.code, 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE');
assert.equal(failure.upstreamCode, 'ORCHESTRATOR_UNAVAILABLE');
});
test('agent run starts a session and marks submitted reply as succeeded', async () => {
const pool = createFakePool();
const submitted = [];