AkurAI Build
Menu

popagent

public

Latest change 7da2fe74493c9dcbac24958ec97be58b6ccf8e29 - Require complete autonomous delegation sequence 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");
    expect(SELF_UPDATE_PROMPT).toContain("Delegate repository inspection to the researcher");
    expect(SELF_UPDATE_PROMPT).toContain("even when the result is a no-op");
  });
});