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:
Mohamed Boudra
2026-07-18 14:04:48 +02:00
committed by GitHub
parent 98f6611362
commit ffe76a7e57
66 changed files with 2791 additions and 924 deletions
@@ -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",