Make workspace, agent, and schedule automation consistent (#2186)
* feat(workspaces): align agent and schedule automation Make workspace identity the shared placement boundary for MCP and CLI, while caller identity determines parentage. Keep heartbeats minimal and cron-based without changing legacy rolling interval semantics. * test(cli): expect canonical schedule cadence * fix(automation): preserve workspace and schedule compatibility * fix(automation): preserve compatibility edges * fix(workspaces): align MCP lifecycle resolution * fix(workspaces): preserve creation intent * fix(workspaces): preserve branch and schedule identity
This commit is contained in:
@@ -113,6 +113,26 @@ function scheduleOnHost(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function heartbeatOnHost(cadence: ScheduleSummary["cadence"]): TestSchedule {
|
||||
return {
|
||||
id: "heartbeat-host-a",
|
||||
serverId: "host-a",
|
||||
serverName: "Host A",
|
||||
name: "Babysit",
|
||||
prompt: "Check status",
|
||||
cadence,
|
||||
target: { type: "agent", agentId: "agent-1" },
|
||||
status: "active",
|
||||
createdAt: "2026-07-01T00:00:00.000Z",
|
||||
updatedAt: "2026-07-01T00:00:00.000Z",
|
||||
nextRunAt: "2026-07-02T00:00:00.000Z",
|
||||
lastRunAt: null,
|
||||
pausedAt: null,
|
||||
expiresAt: null,
|
||||
maxRuns: null,
|
||||
};
|
||||
}
|
||||
|
||||
function providerSnapshot(models: AgentModelDefinition[]): { entries: ProviderSnapshotEntry[] } {
|
||||
return {
|
||||
entries: [
|
||||
@@ -389,7 +409,7 @@ describe("schedule form model", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes interval cadences to cron cadences when opening the form", () => {
|
||||
it("displays a representable legacy interval without submitting a cadence change", () => {
|
||||
const form = open({
|
||||
mode: "edit",
|
||||
schedule: scheduleOnHost({
|
||||
@@ -412,9 +432,12 @@ describe("schedule form model", () => {
|
||||
expression: "* * * * *",
|
||||
timezone: "Europe/Madrid",
|
||||
});
|
||||
form.setName("Renamed without touching cadence");
|
||||
|
||||
expect(form.getState().submitCadence).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves an edited schedule's interval cadence until the cadence changes", () => {
|
||||
it("does not rewrite an unrepresentable legacy interval until cadence changes", () => {
|
||||
const originalCadence = { type: "every" as const, everyMs: 90 * 60_000 };
|
||||
const form = open({
|
||||
mode: "edit",
|
||||
@@ -441,7 +464,7 @@ describe("schedule form model", () => {
|
||||
|
||||
form.setName("Renamed without touching cadence");
|
||||
|
||||
expect(form.getState().submitCadence).toEqual(originalCadence);
|
||||
expect(form.getState().submitCadence).toBeUndefined();
|
||||
|
||||
form.setCadence({
|
||||
type: "cron",
|
||||
@@ -456,6 +479,28 @@ describe("schedule form model", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires a cron choice before updating a legacy heartbeat", () => {
|
||||
const form = open({
|
||||
mode: "edit",
|
||||
schedule: heartbeatOnHost({ type: "every", everyMs: 90 * 60_000 }),
|
||||
defaults: {
|
||||
serverId: null,
|
||||
projectTargets: PROJECT_TARGETS,
|
||||
preferences: {},
|
||||
timezone: "Europe/Madrid",
|
||||
},
|
||||
});
|
||||
|
||||
expect(form.getState()).toMatchObject({ targetKind: "agent", canSubmit: false });
|
||||
|
||||
form.setCadence({ type: "cron", expression: "0 9 * * *", timezone: "Europe/Madrid" });
|
||||
|
||||
expect(form.getState()).toMatchObject({
|
||||
submitCadence: { type: "cron", expression: "0 9 * * *", timezone: "Europe/Madrid" },
|
||||
canSubmit: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("clears provider selection while resolving a different project", () => {
|
||||
const form = open({
|
||||
mode: "create",
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface ScheduleFormProjectOption {
|
||||
}
|
||||
|
||||
export type ScheduleFormTargetKind = "agent" | "new-agent";
|
||||
type CronCadence = Extract<ScheduleCadence, { type: "cron" }>;
|
||||
type ProviderResolutionStatus = "idle" | "pending" | "complete";
|
||||
|
||||
export interface ScheduleFormState {
|
||||
@@ -86,7 +87,7 @@ export interface ScheduleFormState {
|
||||
prompt: string;
|
||||
maxRuns: string;
|
||||
cadence: ScheduleCadence;
|
||||
submitCadence: ScheduleCadence;
|
||||
submitCadence: CronCadence | undefined;
|
||||
hosts: ScheduleFormHost[];
|
||||
projectOptions: ScheduleFormProjectOption[];
|
||||
selectedServerId: string | null;
|
||||
@@ -436,13 +437,10 @@ function formatInitialMaxRuns(schedule: ScheduleFormSnapshot["schedule"]): strin
|
||||
}
|
||||
|
||||
function resolveInitialSubmitCadence(
|
||||
snapshot: ScheduleFormSnapshot,
|
||||
initialCadence: ScheduleCadence,
|
||||
): ScheduleCadence {
|
||||
if (snapshot.mode === "edit" && snapshot.schedule) {
|
||||
return snapshot.schedule.cadence;
|
||||
}
|
||||
return initialCadence;
|
||||
schedule: ScheduleFormSnapshot["schedule"],
|
||||
initialCadence: CronCadence,
|
||||
): CronCadence | undefined {
|
||||
return schedule ? undefined : initialCadence;
|
||||
}
|
||||
|
||||
function resolveInitialIsolation(input: {
|
||||
@@ -547,12 +545,12 @@ function resolveDisclosure(state: ScheduleFormState): ScheduleDisclosureState {
|
||||
}
|
||||
|
||||
function resolveCanSubmit(state: ScheduleFormState): boolean {
|
||||
if (state.targetKind === "agent") {
|
||||
return state.submitCadence !== undefined;
|
||||
}
|
||||
if (state.prompt.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (state.targetKind === "agent") {
|
||||
return true;
|
||||
}
|
||||
const hasWorkingDir = state.workingDir.trim().length > 0;
|
||||
const hasMatchedProject = state.selectedProjectOptionId.trim().length > 0;
|
||||
if (state.mode === "create" && !hasMatchedProject) {
|
||||
@@ -657,7 +655,7 @@ function buildInitialState(snapshot: ScheduleFormSnapshot): ScheduleFormState {
|
||||
prompt: snapshot.schedule?.prompt ?? "",
|
||||
maxRuns: formatInitialMaxRuns(snapshot.schedule),
|
||||
cadence: initialCadence,
|
||||
submitCadence: resolveInitialSubmitCadence(snapshot, initialCadence),
|
||||
submitCadence: resolveInitialSubmitCadence(snapshot.schedule, initialCadence),
|
||||
hosts: [...snapshot.hosts],
|
||||
projectOptions: buildProjectOptions(snapshot.defaults.projectTargets, selectedServerId),
|
||||
selectedServerId,
|
||||
|
||||
Reference in New Issue
Block a user