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
@@ -1,6 +1,10 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { parseScheduleCreateInput, parseScheduleUpdateInput } from "./shared.js";
import {
compileEveryPresetToCron,
parseScheduleCreateInput,
parseScheduleUpdateInput,
} from "./shared.js";
const baseOptions = {
prompt: "do the thing",
@@ -68,13 +72,9 @@ describe("parseScheduleCreateInput cwd/host validation", () => {
});
describe("parseScheduleCreateInput first-run timing", () => {
test("--every with no run-now flag fires immediately on creation", () => {
test("--every compiles to cron and waits for the next slot", () => {
const input = parseScheduleCreateInput(baseOptions);
expect(input.runOnCreate).toBe(true);
});
test("--every with --no-run-now waits the interval", () => {
const input = parseScheduleCreateInput({ ...baseOptions, runNow: false });
expect(input.cadence).toEqual({ type: "cron", expression: "*/5 * * * *" });
expect(input.runOnCreate).toBe(false);
});
@@ -101,22 +101,9 @@ describe("parseScheduleCreateInput first-run timing", () => {
});
});
test("--every with --run-now is rejected as redundant", () => {
expect(() => parseScheduleCreateInput({ ...baseOptions, runNow: true })).toThrow(
expect.objectContaining({
code: "REDUNDANT_RUN_NOW",
message: expect.stringContaining("--run-now is redundant with --every"),
}),
);
});
test("--cron with --no-run-now is rejected as redundant", () => {
expect(() => parseScheduleCreateInput({ ...baseCron, runNow: false })).toThrow(
expect.objectContaining({
code: "REDUNDANT_NO_RUN_NOW",
message: expect.stringContaining("--no-run-now is redundant with --cron"),
}),
);
test("--run-now is orthogonal to a compiled preset", () => {
const input = parseScheduleCreateInput({ ...baseOptions, runNow: true });
expect(input.runOnCreate).toBe(true);
});
test("--timezone without --cron is rejected", () => {
@@ -159,10 +146,10 @@ describe("parseScheduleUpdateInput", () => {
);
});
test("parses --every cadence", () => {
test("compiles --every cadence to cron", () => {
expect(parseScheduleUpdateInput({ id: "abc", every: "5m" })).toEqual({
id: "abc",
cadence: { type: "every", everyMs: 5 * 60_000 },
cadence: { type: "cron", expression: "*/5 * * * *" },
});
});
@@ -271,3 +258,25 @@ describe("parseScheduleUpdateInput", () => {
).toThrow(expect.objectContaining({ code: "CONFLICTING_EXPIRES" }));
});
});
describe("compileEveryPresetToCron", () => {
test.each([
["1m", "*/1 * * * *"],
["15m", "*/15 * * * *"],
["1h", "0 * * * *"],
["6h", "0 */6 * * *"],
["1d", "0 0 * * *"],
])("compiles %s", (value, expected) => {
expect(compileEveryPresetToCron(value)).toBe(expected);
});
test.each(["30s", "7m", "5h", "2d"])("rejects rolling interval %s", (value) => {
expect(() => compileEveryPresetToCron(value)).toThrow(
expect.objectContaining({ code: "UNREPRESENTABLE_CADENCE" }),
);
});
test.each(["15minutes", "junk15m", "1h-nope"])("rejects malformed preset %s", (value) => {
expect(() => compileEveryPresetToCron(value)).toThrow("Invalid duration format");
});
});