mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
test(qa): derive flow deadline ownership structurally
This commit is contained in:
@@ -24,12 +24,6 @@ const MATRIX_ISOLATED_ALLOWBOTS_ADMISSION_SCENARIOS = [
|
||||
"matrix-allowbots-true-unmentioned-open-room",
|
||||
] as const;
|
||||
|
||||
const MATRIX_PROVIDER_DEADLINE_SCENARIOS = new Set([
|
||||
"matrix-e2ee-thread-follow-up",
|
||||
"matrix-inbound-edit-no-duplicate-trigger",
|
||||
"matrix-thread-nested-reply-shape",
|
||||
]);
|
||||
|
||||
function readModuleBinding(
|
||||
scenario: ReturnType<typeof readQaBootstrapScenarioCatalog>["scenarios"][number],
|
||||
) {
|
||||
@@ -99,9 +93,6 @@ describe("Matrix QA Lab scenario flows", () => {
|
||||
}
|
||||
expect(scenario.execution.channel, scenario.id).toBe("matrix");
|
||||
expect(scenario.execution.retryCount, scenario.id).toBe(0);
|
||||
if (!MATRIX_PROVIDER_DEADLINE_SCENARIOS.has(scenario.id)) {
|
||||
expect(scenario.execution.timeoutMs, scenario.id).toBeGreaterThan(0);
|
||||
}
|
||||
expect(scenario.execution.flow?.steps.at(-1)?.detailsExpr, scenario.id).toBe(
|
||||
"result.details ?? (result.artifacts ? JSON.stringify(result.artifacts, null, 2) : undefined)",
|
||||
);
|
||||
@@ -126,11 +117,16 @@ describe("Matrix QA Lab scenario flows", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses provider-owned deadlines for model-driven multi-phase Matrix flows", () => {
|
||||
for (const scenarioId of MATRIX_PROVIDER_DEADLINE_SCENARIOS) {
|
||||
const execution = requireFlowScenario(readQaScenarioById(scenarioId)).execution;
|
||||
expect(execution.timeoutMs, scenarioId).toBeUndefined();
|
||||
it("keeps provider-owned deadlines outside the scenario lifecycle", () => {
|
||||
let providerDeadlineScenarioCount = 0;
|
||||
for (const scenario of scenarios) {
|
||||
if (scenario.execution.kind !== "flow" || scenario.execution.deadlineOwner !== "provider") {
|
||||
continue;
|
||||
}
|
||||
providerDeadlineScenarioCount += 1;
|
||||
expect(scenario.execution.timeoutMs, scenario.id).toBeUndefined();
|
||||
}
|
||||
expect(providerDeadlineScenarioCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("applies the portable thread override through Matrix flow preparation", () => {
|
||||
|
||||
@@ -97,9 +97,19 @@ const qaFlowScenarioExecutionSchema = z
|
||||
suiteIsolation: z.literal("isolated").optional(),
|
||||
isolationReason: z.string().trim().min(1).optional(),
|
||||
transportPolicy: qaScenarioTransportPolicySchema.optional(),
|
||||
deadlineOwner: z.literal("provider").optional(),
|
||||
config: qaScenarioConfigSchema.optional(),
|
||||
})
|
||||
.extend(qaScenarioModuleFlow.executionShape);
|
||||
.extend(qaScenarioModuleFlow.executionShape)
|
||||
.superRefine((execution, ctx) => {
|
||||
if (execution.deadlineOwner === "provider" && execution.timeoutMs !== undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["timeoutMs"],
|
||||
message: "provider-owned deadlines cannot declare a scenario timeout",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const qaTestFileScenarioExecutionBaseSchema = z.object({
|
||||
summary: z.string().trim().min(1).optional(),
|
||||
|
||||
@@ -501,6 +501,57 @@ describe("qa suite runtime flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves the lifecycle deadline unset when the scenario delegates timeout ownership", async () => {
|
||||
vi.useFakeTimers();
|
||||
const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
try {
|
||||
const env = createQaSuiteRuntimeFlowTestEnv();
|
||||
const scenario = makeQaSuiteTestScenario("provider-owned-timeout", { config: {} });
|
||||
if (scenario.execution.kind !== "flow") {
|
||||
throw new Error("expected flow scenario");
|
||||
}
|
||||
createQaScenarioRuntimeApi.mockImplementationOnce(
|
||||
(params: { deps: { runScenario: typeof runQaSuiteScenarioSteps } }) => ({
|
||||
runScenario: params.deps.runScenario,
|
||||
}),
|
||||
);
|
||||
runScenarioFlow.mockImplementationOnce(async (params) => {
|
||||
const api = params.api as { runScenario: typeof runQaSuiteScenarioSteps };
|
||||
return await api.runScenario("Provider-owned timeout", [
|
||||
{
|
||||
name: "Complete under the provider deadline",
|
||||
run: async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 10_000);
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const pending = runQaSuiteScenarioDefinition({
|
||||
env,
|
||||
scenario,
|
||||
runScenario: runQaSuiteScenarioSteps,
|
||||
splitModelRef: (raw) => parseModelRef(raw, "openai"),
|
||||
formatErrorMessage: (error) => String(error),
|
||||
liveTurnTimeoutMs: () => 60_000,
|
||||
resolveQaLiveTurnTimeoutMs: () => 60_000,
|
||||
constants: qaSuiteRuntimeFlowTestConstants,
|
||||
});
|
||||
expect(timeoutSpy).toHaveBeenCalledTimes(1);
|
||||
expect(timeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), 10_000);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
|
||||
expect(await pending).toMatchObject({ status: "pass" });
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
} finally {
|
||||
timeoutSpy.mockRestore();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("caps and disposes the lifecycle watchdog without advancing the maximum timer", async () => {
|
||||
vi.useFakeTimers();
|
||||
const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
|
||||
@@ -8,6 +8,7 @@ scenario:
|
||||
execution:
|
||||
kind: flow
|
||||
channel: matrix
|
||||
deadlineOwner: provider
|
||||
retryCount: 0
|
||||
config:
|
||||
matrixConfigOverrides:
|
||||
|
||||
@@ -8,6 +8,7 @@ scenario:
|
||||
execution:
|
||||
kind: flow
|
||||
channel: matrix
|
||||
deadlineOwner: provider
|
||||
retryCount: 0
|
||||
|
||||
flow:
|
||||
|
||||
@@ -8,6 +8,7 @@ scenario:
|
||||
execution:
|
||||
kind: flow
|
||||
channel: matrix
|
||||
deadlineOwner: provider
|
||||
retryCount: 0
|
||||
|
||||
flow:
|
||||
|
||||
Reference in New Issue
Block a user