Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/lab/automation/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,18 @@ export async function enqueueManualLabRun(
return { state: next, value: run };
});
if (!created) return null;
// Manual execution is independent of automation enablement/layer toggles.
await runDispatchBatch(configDir, { manualRunId: created.runId, abortSignal });
// Manual execution can run without activation or a scheduler, so it must own a shutdown
// hook for the lifetime of its dispatch instead of relying on either of those paths.
const detachShutdownHook = registerOptionalShutdownHook(
`lab-automation-manual:${created.runId}`,
requestLabAutomationShutdown,
);
try {
// Manual execution is independent of automation enablement/layer toggles.
await runDispatchBatch(configDir, { manualRunId: created.runId, abortSignal });
} finally {
detachShutdownHook();
}
return loadLabAutomationState(configDir).runs.find((row) => row.runId === created.runId) ?? created;
}

Expand All @@ -496,4 +506,4 @@ export function cancelLabAutomationRun(runId: string, configDir?: string): boole
}
return { state: next, value: true };
});
}
}
42 changes: 42 additions & 0 deletions tests/lab-automation-review-regressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import type {
import { LabAutomationError } from "../src/lab/automation/types";
import { LAB_AUTOMATION_HARD_MAX } from "../src/lab/automation/constants";
import { createHostIssuedLabRouteExecutor } from "../src/lib/lab-live-host";
import {
resetOptionalShutdownHooksForTests,
runOptionalShutdownHooks,
} from "../src/lib/optional-shutdown-hooks";
import { readInstallationSalt } from "../src/lab/subject/installation-salt";
import type { NormalizedObservation } from "../src/lab/conformance/types";
import {
Expand Down Expand Up @@ -149,6 +153,7 @@ afterEach(() => {
requestLabAutomationShutdown();
stopLabAutomationScheduler();
resetLabAutomationSchedulerStateForTests();
resetOptionalShutdownHooksForTests();
setLabAutomationDispatchDeps({});
resetCompatibilityVersionCacheForTests();
delete process.env.OPENCODEX_HOME;
Expand Down Expand Up @@ -239,6 +244,43 @@ describe("CL-08 independent review regressions", () => {
expect(result?.state).not.toBe("running");
});

test("manual run remains cancellable by shutdown after its activation lease is released", async () => {
const home = tempHome();
prepareHome(home);
const config = providerConfig();
const plan = planManualLabRun({
evidenceLayer: "live_route_compatibility",
scenarioId: "responses-core.live.basic-turn",
providerName: "fixture-provider",
modelId: "fixture-model",
config,
configDir: home,
});
let startedResolve!: () => void;
const started = new Promise<void>((resolve) => { startedResolve = resolve; });
const release = setLabAutomationDispatchDeps({
configDir: home,
loadConfig: () => config,
resolve: fixtureDnsResolve(),
routeExecutor: createHostIssuedLabRouteExecutor(async (input) => {
startedResolve();
if (!input.signal.aborted) {
await new Promise<void>((resolve) => input.signal.addEventListener("abort", () => resolve(), { once: true }));
}
return passObservation();
}),
});

const manualRun = enqueueManualLabRun(plan, home);
await started;
release();
runOptionalShutdownHooks();

const result = await manualRun;
expect(result?.state).toBe("cancelled");
expect(result?.terminalCode).toBe("cancelled");
});

test("disabling the live layer cancels previously queued scheduled live work", async () => {
const home = tempHome();
prepareHome(home);
Expand Down
Loading