Menu
popagent
publicLatest change 6726bbcb1e8a68b297a0e59825c4d913d4b9aab8 - Run autonomous improvements continuously by Ólafur Búi Ólafsson
import { describe, expect, test } from "bun:test";
import type {
AgentSchedule,
AgentSettings,
AgentWorkspace,
AutonomySettings,
} from "./api-types";
import {
SELF_UPDATE_PROMPT,
SELF_UPDATE_SCHEDULE_NAME,
SelfUpdateScheduler,
} from "./self-update-scheduler";
function autonomy(overrides: Partial<AutonomySettings> = {}): AutonomySettings {
return {
enabled: true,
reflectionIntervalMs: 3_600_000,
batchSize: 10,
maxAttempts: 3,
autoApplyStrategies: false,
autoCreateSkills: false,
selfUpdateEnabled: true,
selfUpdateCron: "0 3 * * *",
idleImprovementEnabled: false,
idleDeploymentEnabled: false,
idleWorkspaceIds: [],
updatedAt: new Date(0).toISOString(),
...overrides,
};
}
function workspace(id: string): AgentWorkspace {
return {
id,
name: id,
repositoryPath: id,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
};
}
function schedule(
workspaceId: string,
overrides: Partial<AgentSchedule> = {},
): AgentSchedule {
return {
id: crypto.randomUUID(),
sessionId: null,
source: "self-update",
workspaceId,
name: SELF_UPDATE_SCHEDULE_NAME,
prompt: SELF_UPDATE_PROMPT,
model: "test/model",
maxAttempts: 3,
cron: "0 3 * * *",
timezone: "UTC",
enabled: true,
nextRunAt: new Date().toISOString(),
lastRunAt: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
class FakeAutonomySettings {
listeners = new Set<(settings: AutonomySettings) => void>();
constructor(public current: AutonomySettings) {}
async get(): Promise<AutonomySettings> {
return this.current;
}
subscribe(listener: (settings: AutonomySettings) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}
class FakeTaskSchedules {
schedules: AgentSchedule[] = [];
upsertCalls = 0;
async upsertSelfUpdateSchedule(input: {
workspaceId: string;
name: string;
prompt: string;
model: string;
maxAttempts: number;
cron: string;
enabled: boolean;
}): Promise<AgentSchedule> {
this.upsertCalls++;
const index = this.schedules.findIndex((item) =>
item.source === "self-update" && item.workspaceId === input.workspaceId
);
if (index < 0) {
const created = schedule(input.workspaceId, input);
this.schedules.push(created);
return created;
}
const updated = {
...this.schedules[index]!,
...input,
updatedAt: new Date().toISOString(),
};
this.schedules[index] = updated;
return updated;
}
async disableSelfUpdateSchedules(exceptWorkspaceIds: string[] = []): Promise<void> {
for (const item of this.schedules) {
if (item.source === "self-update" && !exceptWorkspaceIds.includes(item.workspaceId)) {
item.enabled = false;
item.nextRunAt = null;
}
}
}
}
const writableAgents = {
async get(): Promise<Pick<AgentSettings, "workspaceAccess" | "delegationEnabled">> {
return { workspaceAccess: "read-write", delegationEnabled: true };
},
};
describe("SelfUpdateScheduler", () => {
test("keeps one stable autonomous schedule per writable workspace and cadence", async () => {
const tasks = new FakeTaskSchedules();
const settings = new FakeAutonomySettings(autonomy({ idleImprovementEnabled: true, idleWorkspaceIds: ["writable"] }));
const workspaces = {
async list() { return [workspace("writable")]; },
async resolveRepository(id: string) { return { workspace: workspace(id), path: `/registered/${id}` }; },
};
const scheduler = new SelfUpdateScheduler({
tasks,
settings,
workspaces,
agents: writableAgents,
isWritableRepository: async () => true,
});
await Promise.all([scheduler.reconcile(), scheduler.reconcile()]);
expect(tasks.schedules).toHaveLength(1);
expect(tasks.schedules[0]).toEqual(expect.objectContaining({
source: "self-update",
workspaceId: "writable",
name: SELF_UPDATE_SCHEDULE_NAME,
cron: "0 3 * * *",
maxAttempts: 3,
enabled: true,
}));
const stableId = tasks.schedules[0]!.id;
settings.current = autonomy({ maxAttempts: 5, selfUpdateCron: "30 4 * * 1", idleImprovementEnabled: true, idleWorkspaceIds: ["writable"] });
await scheduler.reconcile();
expect(tasks.schedules).toHaveLength(1);
expect(tasks.schedules[0]).toEqual(expect.objectContaining({
id: stableId,
cron: "30 4 * * 1",
maxAttempts: 5,
}));
});
test("does not claim a user schedule with the reserved display name", async () => {
const tasks = new FakeTaskSchedules();
const userSchedule = schedule("writable", { source: "user" });
tasks.schedules.push(userSchedule);
const scheduler = new SelfUpdateScheduler({
tasks,
settings: new FakeAutonomySettings(autonomy({ idleImprovementEnabled: true, idleWorkspaceIds: ["writable"] })),
workspaces: {
async list() { return [workspace("writable")]; },
async resolveRepository(id: string) { return { workspace: workspace(id), path: `/registered/${id}` }; },
},
agents: writableAgents,
isWritableRepository: async () => true,
});
await scheduler.reconcile();
expect(tasks.schedules).toHaveLength(2);
expect(tasks.schedules.find((item) => item.id === userSchedule.id)).toEqual(userSchedule);
expect(tasks.schedules.filter((item) => item.source === "self-update")).toHaveLength(1);
});
test("skips read-only and invalid registered workspaces", async () => {
const tasks = new FakeTaskSchedules();
tasks.schedules.push(schedule("read-only"), schedule("invalid"));
const workspaces = {
async list() { return [workspace("writable"), workspace("read-only"), workspace("invalid")]; },
async resolveRepository(id: string) {
if (id === "invalid") throw new Error("invalid workspace");
return { workspace: workspace(id), path: `/registered/${id}` };
},
};
const scheduler = new SelfUpdateScheduler({
tasks,
settings: new FakeAutonomySettings(autonomy({ idleImprovementEnabled: true, idleWorkspaceIds: ["writable", "read-only", "invalid"] })),
workspaces,
agents: writableAgents,
isWritableRepository: async (path) => path.endsWith("/writable"),
});
await scheduler.reconcile();
expect(tasks.schedules.filter((item) => item.enabled).map((item) => item.workspaceId)).toEqual(["writable"]);
expect(tasks.schedules).toHaveLength(3);
});
test("disabling autonomy disables only self-update-owned schedules", async () => {
const tasks = new FakeTaskSchedules();
tasks.schedules.push(
schedule("writable"),
schedule("writable", { source: "user" }),
);
const scheduler = new SelfUpdateScheduler({
tasks,
settings: new FakeAutonomySettings(autonomy({ selfUpdateEnabled: false })),
workspaces: {
async list() { return [workspace("writable")]; },
async resolveRepository(id: string) { return { workspace: workspace(id), path: `/registered/${id}` }; },
},
agents: writableAgents,
isWritableRepository: async () => true,
});
await scheduler.reconcile();
expect(tasks.schedules.find((item) => item.source === "self-update")?.enabled).toBe(false);
expect(tasks.schedules.find((item) => item.source === "user")?.enabled).toBe(true);
});
test("requires writable supervisor and implementer plus an eligible reviewer", async () => {
const tasks = new FakeTaskSchedules();
const scheduler = new SelfUpdateScheduler({
tasks,
settings: new FakeAutonomySettings(autonomy({ idleImprovementEnabled: true, idleWorkspaceIds: ["writable"] })),
workspaces: {
async list() { return [workspace("writable")]; },
async resolveRepository(id: string) { return { workspace: workspace(id), path: `/registered/${id}` }; },
},
agents: {
async get(id) {
return id === "reviewer"
? { workspaceAccess: "read-only" as const, delegationEnabled: false }
: { workspaceAccess: "read-write" as const, delegationEnabled: true };
},
},
isWritableRepository: async () => true,
});
await scheduler.reconcile();
expect(tasks.schedules).toHaveLength(0);
});
test("serializes concurrent starts and stop during the initial reconcile", async () => {
const gate = Promise.withResolvers<void>();
let gets = 0;
const settings = new FakeAutonomySettings(autonomy());
settings.get = async () => {
gets++;
await gate.promise;
return settings.current;
};
const tasks = new FakeTaskSchedules();
const scheduler = new SelfUpdateScheduler({
tasks,
settings,
workspaces: {
async list() { return [workspace("writable")]; },
async resolveRepository(id: string) { return { workspace: workspace(id), path: `/registered/${id}` }; },
},
agents: writableAgents,
reconcileIntervalMs: 60_000,
isWritableRepository: async () => true,
});
const firstStart = scheduler.start();
const secondStart = scheduler.start();
expect(firstStart).toBe(secondStart);
await Promise.resolve();
const stopping = scheduler.stop();
gate.resolve();
await Promise.all([firstStart, secondStart, stopping]);
expect(gets).toBe(1);
expect(settings.listeners.size).toBe(0);
await scheduler.start();
expect(gets).toBe(2);
expect(settings.listeners.size).toBe(1);
await scheduler.stop();
expect(settings.listeners.size).toBe(0);
});
test("generated work requires bounded implementation, independent verification, and a contained commit", () => {
expect(SELF_UPDATE_PROMPT).toContain("Delegate independent verification");
expect(SELF_UPDATE_PROMPT).toContain("prepared autonomous/self-update branch");
expect(SELF_UPDATE_PROMPT).toContain("Never acquire credentials");
expect(SELF_UPDATE_PROMPT).toContain("clean no-op");
expect(SELF_UPDATE_PROMPT).toContain("Server-owned policy performs optional publish and deployment");
});
});