Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/dispatcher/src/grant-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ export const RUN_GRANTS: Readonly<Record<string, RunGrant>> = {
// installation token (ADR-0006), so no API host is admitted.
"pr-review": { profiles: ["public-repo-read"], rollout: "legacy" },
"spec-drift-pr": { profiles: JS_BUILD, rollout: "legacy" },
// Narrow for the same reason as `org-spec-audit`, and the run is written to
// keep it that way: every measurement script is read-only — no install, no
// build, no network — so a container that could reach a registry would be a
// grant wider than any use. That bound is what makes `size` report the
// dependency surface rather than built bundle bytes; the honest grant and the
// honest measurement are the same decision.
"improve-pr": { profiles: ["public-repo-read"], rollout: "legacy" },
// Narrower than its sibling on purpose: the sweep only clones and reads with
// `git`, never installs or builds, and its single write is the Worker-side
// draft PR. No `js-install`.
Expand Down
1 change: 1 addition & 0 deletions apps/dispatcher/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ describe("GET /health", () => {
"deploy-smoke",
"email-otp-login",
"finops-audit",
"improve-pr",
"matrix-fanout",
"offload-test",
"org-spec-audit",
Expand Down
2 changes: 2 additions & 0 deletions apps/dispatcher/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
refreshFixtures,
releaseNotes,
selfHealPr,
improvePr,
specDriftPr,
triageIssues,
triagePrs,
Expand All @@ -46,6 +47,7 @@ const RUN_REGISTRY: Record<string, Run<unknown, unknown>> = {
[productDemo.name]: productDemo as Run<unknown, unknown>,
[playwrightDemo.name]: playwrightDemo as Run<unknown, unknown>,
[prReview.name]: prReview as Run<unknown, unknown>,
[improvePr.name]: improvePr as Run<unknown, unknown>,
[specDriftPr.name]: specDriftPr as Run<unknown, unknown>,
// The other half of the internal audit: `spec-drift-pr` proposes the drift a
// machine can fix, this sweeps the estate for what needs a human answer and
Expand Down
139 changes: 139 additions & 0 deletions runs/improve-pr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Run-level unit tests for `improve-pr` — drive the run against the in-memory
// test runtime (`makeCFRuntimeTest`) with seeded config + sandbox + model fakes.
// No CF, no Docker, no model provider.

import { it } from "@effect/vitest";
import { Effect, Exit } from "effect";
import { describe, expect } from "vitest";
import { makeCFRuntimeTest } from "@fractalboxdev/flare-dispatch-core/testing";
import type { ModelCompletionResult } from "@fractalboxdev/flare-dispatch-core";
import { improvePr } from "./improve-pr";

const firedAt = Date.UTC(2026, 5, 3); // 2026-06-03
const input = { firedAt, dimension: "ci-speed" } as const;

/** A tools-mode model result returning the `propose_improvements` payload. */
const report = (findings: unknown[]): ModelCompletionResult => ({
toolCalls: [
{
name: "propose_improvements",
arguments: { summary: "measured the pipeline", findings },
},
],
text: "",
});

const finding = (over: Record<string, unknown> = {}) => ({
title: "Cache the install step",
location: ".github/workflows/ci.yml",
measured: "0 steps use a cache action across 3 workflows",
saving: "~90s per run",
cost: "one cache key to invalidate, and a stale-cache failure mode",
percentImprovement: 40,
proposal: "Add actions/cache keyed on the lockfile hash.",
...over,
});

const baseConfig = {
"improve.repos": "owner/name",
"improve.workers-ai.model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
};

/** A measurement with content — enough to pass the deterministic exit. */
const measured = { exitCode: 0, stdout: "=== workflow files ===\nname: CI\n" };
const sandboxProgram = { "workflows": measured };

describe("improve-pr", () => {
it.effect("opens a draft PR carrying the report when a finding clears the floor", () => {
const { layer, handles } = makeCFRuntimeTest({
config: baseConfig,
sandboxProgram,
modelGateway: { responses: [report([finding()])] },
});

return Effect.gen(function* () {
const out = yield* improvePr.run(input);
expect(out.reposScanned).toBe(1);
expect(out.prsOpened).toBe(1);
expect(out.belowFloor).toBe(0);

const calls = handles.github.openDraftPullRequestCalls;
expect(calls).toHaveLength(1);
expect(calls[0]!.headBranch).toBe("flare-dispatch/improve-ci-speed-2026-06-03");
expect(calls[0]!.files[0]!.path).toBe("improvements/ci-speed/2026-06-03.md");
// The cost is not optional decoration — it has to reach the artifact.
expect(calls[0]!.files[0]!.content).toContain("stale-cache failure mode");
}).pipe(Effect.provide(layer));
});

it.effect("proposes nothing when every finding is below the materiality floor", () => {
const { layer, handles } = makeCFRuntimeTest({
config: baseConfig,
sandboxProgram,
modelGateway: { responses: [report([finding({ percentImprovement: 2 })])] },
});

return Effect.gen(function* () {
const out = yield* improvePr.run(input);
expect(out.prsOpened).toBe(0);
expect(out.belowFloor).toBe(1);
expect(out.reposClean).toBe(1);
expect(handles.github.openDraftPullRequestCalls).toHaveLength(0);
}).pipe(Effect.provide(layer));
});

it.effect("honours an operator-lowered materiality floor", () => {
const { layer, handles } = makeCFRuntimeTest({
config: { ...baseConfig, "improve.materiality-floor": "1" },
sandboxProgram,
modelGateway: { responses: [report([finding({ percentImprovement: 2 })])] },
});

return Effect.gen(function* () {
const out = yield* improvePr.run(input);
expect(out.prsOpened).toBe(1);
expect(handles.github.openDraftPullRequestCalls).toHaveLength(1);
}).pipe(Effect.provide(layer));
});

it.effect("never calls the model when the measurement is empty", () => {
// The deterministic exit (§7 fence 1): a repo the dimension cannot measure
// costs nothing, and must not reach a model to discover that.
const { layer, handles } = makeCFRuntimeTest({
config: baseConfig,
sandboxProgram: { workflows: { exitCode: 0, stdout: "" } },
modelGateway: { responses: [report([finding()])] },
});

return Effect.gen(function* () {
const out = yield* improvePr.run(input);
expect(out.reposClean).toBe(1);
expect(out.prsOpened).toBe(0);
expect(handles.github.openDraftPullRequestCalls).toHaveLength(0);
}).pipe(Effect.provide(layer));
});

it.effect("is a no-op when improve.repos is unset", () => {
const { layer, handles } = makeCFRuntimeTest({ config: {} });
return Effect.gen(function* () {
const out = yield* improvePr.run(input);
expect(out.reposScanned).toBe(0);
expect(handles.github.openDraftPullRequestCalls).toHaveLength(0);
}).pipe(Effect.provide(layer));
});

it.effect("fails when the backend model is unconfigured", () => {
const { layer } = makeCFRuntimeTest({ config: { "improve.repos": "owner/name" } });
return Effect.gen(function* () {
const exit = yield* Effect.exit(improvePr.run(input));
expect(Exit.isFailure(exit)).toBe(true);
}).pipe(Effect.provide(layer));
});

it("declares no schedule, so no cron can arm it by accident", () => {
// §6 staggers one dimension per weekday and arming that is a product
// decision. A `schedules` entry appearing here without a wrangler cron (or
// vice versa) is the drift `cron-parity.test.ts` exists to catch.
expect(improvePr.schedules).toBeUndefined();
});
});
Loading
Loading