feat(orchestrator): enforce Page Data validation gate
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user