AkurAI Build
Menu

popagent

public

Latest change a976edbfb97c44a0e4fbac194e5e94b27b53f1f8 - Create follow-up tasks autonomously from completed scheduled reports by AkurAI Build

import { describe, expect, test } from "bun:test";
import type {
  AgentSchedule,
  AgentSettings,
  AgentWorkspace,
  AutonomySettings,
} from "./api-types";
import {
  BUILD_MAINTENANCE_MODEL,
  BUILD_MAINTENANCE_PROMPT,
  BUILD_MAINTENANCE_SCHEDULE_NAME,
  SELF_UPDATE_MODEL,
  SELF_UPDATE_PROMPT,
  SELF_UPDATE_SCHEDULE_NAME,
  SelfUpdateScheduler,
} from "./self-update-scheduler";

test("system schedules use the configured cloud model instead of Titan-local inference", () => {
  expect(SELF_UPDATE_MODEL).toBe("cc/claude-sonnet-5");
  expect(BUILD_MAINTENANCE_MODEL).toBe("cc/claude-sonnet-5");
});

function autonomy(overrides: Partial<AutonomySettings> = {}): AutonomySettings {
  return {
    enabled: true,
    reflectionIntervalMs: 3_600_000,
    batchSize: 10,
    maxAttempts: 3,
    autoApplyStrategies: false,
    autoCreateSkills: false,
    autoRetainFacts: 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,
    followUps: false,
    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;
    }
    return this.schedules[index]!;
  }
  async upsertBuildMaintenanceSchedule(input: {
    workspaceId: string;
    name: string;
    prompt: string;
    model: string;
    maxAttempts: number;
    cron: string;
    enabled: boolean;
  }): Promise<AgentSchedule> {
    const index = this.schedules.findIndex((item) =>
      item.source === "build-maintenance" && item.workspaceId === input.workspaceId
    );
    if (index < 0) {
      const created = schedule(input.workspaceId, { ...input, source: "build-maintenance" });
      this.schedules.push(created);
      return created;
    }
    return this.schedules[index]!;
  }

  async disableBuildMaintenanceSchedules(exceptWorkspaceIds: string[] = []): Promise<void> {
    for (const item of this.schedules) {
      if (item.source === "build-maintenance" && !exceptWorkspaceIds.includes(item.workspaceId)) {
        item.enabled = false;
        item.nextRunAt = null;
      }
    }
  }

  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("creates one default autonomous schedule and preserves operator timing", 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: "0 3 * * *",
      maxAttempts: 3,
    }));
  });

  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("keeps a stable Build maintenance schedule beside self-update schedules", async () => {
    const tasks = new FakeTaskSchedules();
    const settings = new FakeAutonomySettings(autonomy({
      idleImprovementEnabled: true,
      idleWorkspaceIds: ["build"],
    }));
    const buildWorkspace = { ...workspace("build"), name: "AkurAI-Build", repositoryPath: "akurai-build" };
    const scheduler = new SelfUpdateScheduler({
      tasks,
      settings,
      workspaces: {
        async list() { return [buildWorkspace]; },
        async resolveRepository() { return { workspace: buildWorkspace, path: "/registered/akurai-build" }; },
      },
      agents: writableAgents,
      isWritableRepository: async () => true,
    });

    await scheduler.reconcile();
    const first = tasks.schedules.find((item) => item.source === "build-maintenance");
    expect(first).toEqual(expect.objectContaining({
      source: "build-maintenance",
      workspaceId: "build",
      name: BUILD_MAINTENANCE_SCHEDULE_NAME,
      prompt: BUILD_MAINTENANCE_PROMPT,
    }));
    expect(tasks.schedules.filter((item) => item.source === "self-update")).toHaveLength(1);

    await scheduler.reconcile();
    expect(tasks.schedules.filter((item) => item.source === "build-maintenance")).toHaveLength(1);
    expect(tasks.schedules.find((item) => item.source === "build-maintenance")?.id).toBe(first?.id);
  });
  test("requires concrete inspection evidence before a generated no-op", () => {
    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("A clean checkout alone is never sufficient no-op evidence");
    expect(SELF_UPDATE_PROMPT).toContain("paths and symbols inspected");
    expect(SELF_UPDATE_PROMPT).toContain("candidates considered");
    expect(SELF_UPDATE_PROMPT).toContain("Delegate bounded inspection to the Researcher");
    expect(SELF_UPDATE_PROMPT).toContain("Server-owned policy performs optional publish and deployment");
    expect(BUILD_MAINTENANCE_PROMPT).toContain("Build Maintainer");
    expect(BUILD_MAINTENANCE_PROMPT).toContain("autonomous/build-maintenance/");
    expect(BUILD_MAINTENANCE_PROMPT).toContain("Never advance main");
  });
});