Skip to content
Open
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
343 changes: 343 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3025,6 +3025,349 @@ describe("OrchestratorMcpService provider resolution", () => {
}),
);
});
describe("the model of a scheduled task", () => {
const projectId = ProjectId.make("project:scheduled-model");
const existing: ScheduledTask = {
id: ScheduledTaskId.make("scheduled-task:model"),
title: "Nightly",
prompt: "Check the build",
enabled: false,
schedule: { type: "interval", everyMs: 3_600_000 },
projectId,
threadId: null,
workspaceStrategy: { type: "root" },
modelSelection: {
instanceId: codexInstanceId,
model: "gpt-5",
options: [{ id: "reasoningEffort", value: "low" }],
},
runtimeMode: "approval-required",
interactionMode: "default",
createdBy: "agent",
creationSource: "mcp",
createdAt: "2026-10-05T00:00:00.000Z",
updatedAt: "2026-10-05T00:00:00.000Z",
nextRunAt: null,
lastRunAt: null,
lastRunStatus: "never",
lastRunError: null,
runCount: 0,
};
const client: McpInvocationScope = {
environmentId: EnvironmentId.make("environment:scheduled-model"),
requestNamespace: "client:scheduled-model",
thread: undefined,
client: { sessionId: "scheduled-model", label: "Claude Code", access: "approval-required" },
capabilities: new Set(["orchestration"]),
issuedAt: 1,
};
const codex: ServerProvider = {
...providerSnapshot({
instanceId: codexInstanceId,
driver: ProviderDriverKind.make("codex"),
}),
supportedRuntimeModes: ["approval-required", "auto-accept-edits"],
models: ["gpt-5", "gpt-5-mini"].map((slug) => ({
slug,
name: slug,
isCustom: false,
capabilities: {
optionDescriptors: [
{
id: "reasoningEffort",
label: "Reasoning",
type: "select",
options: [
{ id: "low", label: "Low" },
{ id: "high", label: "High" },
],
},
{
id: "serviceTier",
label: "Speed",
type: "select",
options: [
{ id: "default", label: "Default" },
{ id: "priority", label: "Fast" },
],
},
],
},
})),
};
const bound: ScheduledTask = {
...existing,
threadId: ThreadId.make("thread:scheduled-model-bound"),
};
const service = (
saved: Ref.Ref<ReadonlyArray<ScheduledTaskUpsertInput>>,
stored: ScheduledTask = existing,
enabledForProject = true,
defaultModelSelection: ScheduledTask["modelSelection"] | null = existing.modelSelection,
) =>
OrchestratorMcpService.layer.pipe(
Layer.provide(Layer.mock(ThreadLaunchService.ThreadLaunchService)({})),
Layer.provide(
Layer.mock(GitVcsDriver.GitVcsDriver)({
resolvePrimaryRemoteName: () => Effect.succeed("origin"),
resolveDefaultBranchName: () => Effect.succeed("main"),
resolveCommit: () => Effect.succeed({ commitSha: "abc123" }),
}),
),
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
NodeCrypto.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadShell: () => Effect.succeed(null),
isProviderInstanceEnabledForProject: () => Effect.succeed(enabledForProject),
}),
ServerSettings.layerTest(),
providerRegistryLayer([codex]),
adapterRegistryLayer([codexInstanceId]),
Layer.mock(ProjectService.ProjectService)({
getById: () =>
Effect.succeedSome({
id: projectId,
workspaceRoot: "/repo/scheduled-model",
defaultModelSelection,
} as never),
}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({
list: () => Effect.succeed({ tasks: [stored] }),
upsert: (input) =>
Ref.update(saved, (inputs) => [...inputs, input]).pipe(
Effect.as({
task: {
...stored,
threadId: input.threadId ?? null,
modelSelection: input.modelSelection,
},
}),
),
}),
),
),
);
const savedInputs = <E>(
run: (mcp: OrchestratorMcpService.OrchestratorMcpServiceShape) => Effect.Effect<unknown, E>,
stored: ScheduledTask = existing,
defaultModelSelection?: ScheduledTask["modelSelection"] | null,
) =>
Effect.gen(function* () {
const saved = yield* Ref.make<ReadonlyArray<ScheduledTaskUpsertInput>>([]);
const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
Effect.provide(service(saved, stored, true, defaultModelSelection)),
);
yield* run(mcp);
return yield* Ref.get(saved);
});
const savedSelections = <E>(
run: (mcp: OrchestratorMcpService.OrchestratorMcpServiceShape) => Effect.Effect<unknown, E>,
stored: ScheduledTask = existing,
defaultModelSelection?: ScheduledTask["modelSelection"] | null,
) =>
savedInputs(run, stored, defaultModelSelection).pipe(
Effect.map((inputs) => inputs.map((input) => input.modelSelection)),
);

it.effect("saves a new task with the target's model and options", () =>
Effect.gen(function* () {
const saved = yield* savedSelections((mcp) =>
mcp.scheduleTask(client, {
projectId,
prompt: "Check the build",
schedule: { type: "interval", everyMs: 3_600_000 },
enabled: false,
target: {
model: "gpt-5-mini",
options: [{ id: "serviceTier", value: "priority" }],
},
}),
);
assert.deepEqual(saved, [
{
instanceId: codexInstanceId,
model: "gpt-5-mini",
options: [{ id: "serviceTier", value: "priority" }],
},
]);
}),
);

it.effect("takes a complete target when the project has no default model", () =>
Effect.gen(function* () {
const saved = yield* savedSelections(
(mcp) =>
mcp.scheduleTask(client, {
projectId,
prompt: "Check the build",
schedule: { type: "interval", everyMs: 3_600_000 },
target: { providerInstanceId: codexInstanceId, model: "gpt-5-mini" },
}),
existing,
null,
);
assert.deepEqual(saved, [{ instanceId: codexInstanceId, model: "gpt-5-mini" }]);
}),
);

it.effect("steps the runtime mode down to one the target's provider offers", () =>
Effect.gen(function* () {
const fullAccess: McpInvocationScope = {
...client,
client: { sessionId: "scheduled-model", label: "Claude Code", access: "full-access" },
};
const stored: ScheduledTask = { ...existing, runtimeMode: "full-access" };
const target = { providerInstanceId: codexInstanceId };
const saved = yield* savedInputs(
(mcp) =>
Effect.all([
mcp.scheduleTask(fullAccess, {
projectId,
prompt: "Check the build",
schedule: { type: "interval", everyMs: 3_600_000 },
target,
}),
mcp.updateScheduledTask(fullAccess, { scheduledTaskId: stored.id, target }),
mcp.updateScheduledTask(fullAccess, { scheduledTaskId: stored.id, prompt: "Again" }),
]),
stored,
);
assert.deepEqual(
saved.map((input) => input.runtimeMode),
["auto-accept-edits", "auto-accept-edits", "full-access"],
);
}),
);

it.effect("merges an update's options into the stored selection", () =>
Effect.gen(function* () {
const saved = yield* savedSelections((mcp) =>
mcp.updateScheduledTask(client, {
scheduledTaskId: existing.id,
target: { options: [{ id: "serviceTier", value: "priority" }] },
}),
);
assert.deepEqual(saved, [
{
instanceId: codexInstanceId,
model: "gpt-5",
options: [
{ id: "reasoningEffort", value: "low" },
{ id: "serviceTier", value: "priority" },
],
},
]);
}),
);

it.effect("keeps the stored selection when an update has no target", () =>
Effect.gen(function* () {
const saved = yield* savedSelections((mcp) =>
mcp.updateScheduledTask(client, { scheduledTaskId: existing.id, prompt: "Check again" }),
);
assert.deepEqual(saved, [existing.modelSelection]);
}),
);

it.effect("reports the saved model and options", () =>
Effect.gen(function* () {
const saved = yield* Ref.make<ReadonlyArray<ScheduledTaskUpsertInput>>([]);
const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
Effect.provide(service(saved)),
);
const listed = yield* mcp.listScheduledTasks(client, { projectId });
assert.deepEqual(listed.tasks[0]?.target, {
providerInstanceId: codexInstanceId,
model: "gpt-5",
options: [{ id: "reasoningEffort", value: "low" }],
});
}),
);

it.effect("refuses a target for a bound task, whose runs use the thread's model", () =>
Effect.gen(function* () {
const saved = yield* Ref.make<ReadonlyArray<ScheduledTaskUpsertInput>>([]);
const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
Effect.provide(service(saved, bound)),
);
const error = yield* mcp
.updateScheduledTask(client, {
scheduledTaskId: bound.id,
target: { model: "gpt-5-mini" },
})
.pipe(Effect.flip);
assert.equal(error.code, "invalid_request");
assert.deepEqual(yield* Ref.get(saved), []);
const listed = yield* mcp.listScheduledTasks(client, { projectId });
assert.isUndefined(listed.tasks[0]?.target);
}),
);

it.effect("takes a target in the update that unbinds a task", () =>
Effect.gen(function* () {
const saved = yield* savedSelections(
(mcp) =>
mcp.updateScheduledTask(client, {
scheduledTaskId: bound.id,
bindToCurrentThread: false,
target: { model: "gpt-5-mini" },
}),
bound,
);
assert.deepEqual(saved, [{ instanceId: codexInstanceId, model: "gpt-5-mini" }]);
}),
);

it.effect("refuses a provider the project has disabled, which every run would fail on", () =>
Effect.gen(function* () {
const saved = yield* Ref.make<ReadonlyArray<ScheduledTaskUpsertInput>>([]);
const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
Effect.provide(service(saved, existing, false)),
);
const error = yield* mcp
.updateScheduledTask(client, {
scheduledTaskId: existing.id,
target: { providerInstanceId: codexInstanceId },
})
.pipe(Effect.flip);
assert.equal(error.code, "provider_unavailable");
assert.deepEqual(yield* Ref.get(saved), []);
}),
);

it.effect.each([
{ name: "an unknown model", target: { model: "gpt-0" }, code: "model_unavailable" },
{
name: "an unknown option value",
target: { options: [{ id: "serviceTier", value: "turbo" }] },
code: "invalid_request",
},
] as const)("refuses $name and saves nothing", ({ target, code }) =>
Effect.gen(function* () {
const saved = yield* Ref.make<ReadonlyArray<ScheduledTaskUpsertInput>>([]);
const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe(
Effect.provide(service(saved)),
);
const created = yield* mcp
.scheduleTask(client, {
projectId,
prompt: "Check the build",
schedule: { type: "interval", everyMs: 3_600_000 },
target,
})
.pipe(Effect.flip);
const updated = yield* mcp
.updateScheduledTask(client, { scheduledTaskId: existing.id, target })
.pipe(Effect.flip);
assert.deepEqual([created.code, updated.code], [code, code]);
assert.deepEqual(yield* Ref.get(saved), []);
}),
);
});
it.effect(
"reports the child run's selection after a restart and the thread selection before launch",
() =>
Expand Down
Loading
Loading