Files
PaperClipAI/tests/runner-e2e/selectors.ts
T
DottaandPaperclip 6d46851920 test(e2e): add attended GitHub review bot journey (#17457)
## Thinking Path

> - Paperclip manages agents and the work they perform.
> - Product E2E checks verify real user paths through those agents.
> - A recorded GitHub review-bot walkthrough had no repeatable
registered case.
> - App setup, automatic triggering, publication, and task completion
need separate evidence.
> - This pull request adds an explicit attended journey to the existing
catalog and report tools.
> - Reusable setup profiles and independent assertions preserve the same
outcome across compatible targets.

## Linked Issues or Issue Description

Refs: #17262. This case remains an explicit attended attached-target
flow. It does not replace the shared deployment-target or
paid-operations framework.

**What existing behavior does this improve?**

The Product E2E catalog can now select and retain evidence for GitHub
review-bot setup and its first automatic review.

**Current behavior**

Recorded walkthroughs cannot be rerun as a fixed registered case. A
published review alone can hide a failed run or unfinished task.

**Proposed behavior**

Create and install an owned App, save automatic new-PR settings, create
a defective PR, and require correlated review delivery, an inline
finding, a consistent check, a successful run, and a Done task. Retain
failures and confirm owned-resource cleanup.

**Reason and benefit**

The case makes a useful external integration reproducible without
treating setup, unit checks, or report rendering as proof of a completed
live journey.

## What Changed

- Add two explicit attended cells for an existing configured agent in
local or Daytona execution.
- Declare hosting, account, connection, and credential prerequisites
through private setup bindings.
- Add fixed browser controls, ownership checks, independent grading, and
cleanup.
- Package declared screenshots through the existing sanitizer, manifest,
validator, and dashboard.
- Reject operations-managed execution until budgeted routing is
qualified.
- Document hosted QA credential setup and evidence limits.

## Verification

- Focused catalog, oracle, report, connection, and ownership checks: 127
tests passed.
- Inherited-environment preflight regression failed against the previous
commit and passed after the fix. Actual-run environment verification
remains required.
- Harness TypeScript check and full workspace typecheck passed. Full
build passed. The local full-test attempt was interrupted after repeated
embedded PostgreSQL startup failures; its exit code of zero does not
establish a pass; Exact-head Linux CI passed the full test, typecheck,
build, Runner, serialized, browser, and canary gates on
`abbddeade848edefc8de75f11cc9008b2cd52764`.
- A real hosted attempt published a rated review and inline finding. It
failed when its native session timed out before the task reached Done.
Cleanup passed. Full live qualification remains open.
- Credential scan found no known credential values in the authored files
or retained report corpus.

## Risks

- The full case creates external test resources and uses the configured
provider. It requires an explicit selection and current task authority.
It is excluded from default and CI matrices.
- Time and run limits are not a hard dollar cap. Missing dollar cost
stays unknown. Fixture setup does not weaken budget policy.
- Fresh personal OAuth, unattended authentication, runtime skill-read
proof, and cross-host qualification are separate claims.
- The selected live fixture needs environment/lifecycle recovery before
it can adopt a longer provider deadline.

## Model Used

OpenAI GPT-6 family through Codex, with reasoning, code execution, and
browser tools. The exact deployment model ID and context-window size are
not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used, including the available version
and capability details
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have described the issue in-PR following the enhancement
template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
ticket ID
- [x] I have run focused tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented risks
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open findings
- [x] I will address all Greptile and reviewer comments before merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-24 12:16:26 -07:00

240 lines
7.5 KiB
TypeScript

import { runnerMatrix } from "./catalog.js";
import type { MatrixExecution, MatrixJob } from "./types.js";
export interface RunnerSelectorOptions {
all: boolean;
list: boolean;
matrixJson: boolean;
installedStartupOnly?: boolean;
ids: string[];
suites: string[];
groups: string[];
profiles: string[];
environments: string[];
cases: string[];
headed: boolean;
ui: boolean;
debug: boolean;
maxParallel: number;
maxAutomaticRetries: number;
connectionConfig?: string;
githubReviewConfig?: string;
provisionGitHubRepository?: boolean;
}
export class RunnerSelectorError extends Error {}
function valueFor(args: string[], index: number, flag: string): string {
const value = args[index + 1];
if (!value || value.startsWith("--"))
throw new RunnerSelectorError(`${flag} requires a value`);
return value;
}
export function parseRunnerSelectors(
rawArgs: readonly string[],
): RunnerSelectorOptions {
const args = rawArgs.filter((value) => value !== "--");
const options: RunnerSelectorOptions = {
all: false,
list: false,
matrixJson: false,
ids: [],
suites: [],
groups: [],
profiles: [],
environments: [],
cases: [],
headed: false,
ui: false,
debug: false,
maxParallel: Number(process.env.PAPERCLIP_E2E_MAX_PARALLEL ?? "1"),
maxAutomaticRetries: 1,
};
for (let index = 0; index < args.length; index += 1) {
const flag = args[index];
if (flag === "--all") options.all = true;
else if (flag === "--list") options.list = true;
else if (flag === "--matrix-json") options.matrixJson = true;
else if (flag === "--installed-startup-only") options.installedStartupOnly = true;
else if (flag === "--headed") options.headed = true;
else if (flag === "--ui") options.ui = true;
else if (flag === "--debug") options.debug = true;
else if (flag === "--connection-config") {
options.connectionConfig = valueFor(args, index, flag);
index += 1;
}
else if (flag === "--github-review-config") {
options.githubReviewConfig = valueFor(args, index, flag);
index += 1;
}
else if (flag === "--provision-github-repository") options.provisionGitHubRepository = true;
else if (flag === "--max-parallel") {
const value = valueFor(args, index, flag);
index += 1;
options.maxParallel = Number(value);
} else if (flag === "--max-automatic-retries") {
const value = valueFor(args, index, flag);
index += 1;
options.maxAutomaticRetries = Number(value);
} else if (
[
"--id",
"--suite",
"--group",
"--profile",
"--environment",
"--case",
].includes(flag)
) {
const value = valueFor(args, index, flag);
index += 1;
if (flag === "--id") options.ids.push(value);
else if (flag === "--suite") options.suites.push(value);
else if (flag === "--group") options.groups.push(value);
else if (flag === "--profile") options.profiles.push(value);
else if (flag === "--environment") options.environments.push(value);
else options.cases.push(value);
} else {
throw new RunnerSelectorError(`Unknown runner E2E argument: ${flag}`);
}
}
if (!Number.isInteger(options.maxParallel) || options.maxParallel < 1) {
throw new RunnerSelectorError("--max-parallel must be a positive integer");
}
if (![0, 1].includes(options.maxAutomaticRetries)) {
throw new RunnerSelectorError(
"--max-automatic-retries must be 0 or 1",
);
}
const hasDimensions =
options.suites.length +
options.groups.length +
options.profiles.length +
options.environments.length +
options.cases.length >
0;
if (options.ids.length > 0 && (hasDimensions || options.all)) {
throw new RunnerSelectorError(
"--id is exclusive with --all and dimension filters",
);
}
if (options.all && hasDimensions) {
throw new RunnerSelectorError("--all is exclusive with dimension filters");
}
if (
!options.list &&
!options.matrixJson &&
!options.all &&
options.ids.length === 0 &&
!hasDimensions
) {
throw new RunnerSelectorError(
"Billable runner E2E runs require --all or an explicit selector",
);
}
return options;
}
function assertKnown(
label: string,
selected: readonly string[],
known: Set<string>,
) {
const unknown = selected.filter((value) => !known.has(value));
if (unknown.length > 0)
throw new RunnerSelectorError(`Unknown ${label}: ${unknown.join(", ")}`);
}
export function selectRunnerExecutions(
options: RunnerSelectorOptions,
matrix: readonly MatrixExecution[] = runnerMatrix,
): MatrixExecution[] {
const knownGroups = new Set(matrix.flatMap((execution) => execution.groups));
assertKnown(
"suite",
options.suites,
new Set(matrix.map((execution) => execution.suite.id)),
);
assertKnown("group", options.groups, knownGroups);
assertKnown(
"profile",
options.profiles,
new Set(matrix.map((execution) => execution.profile.id)),
);
assertKnown(
"environment",
options.environments,
new Set(matrix.map((execution) => execution.environment.id)),
);
assertKnown(
"case",
options.cases,
new Set(matrix.map((execution) => execution.task.id)),
);
assertKnown(
"execution id",
options.ids,
new Set(matrix.map((execution) => execution.id)),
);
const selected = matrix.filter((execution) => {
if (options.ids.length > 0) return options.ids.includes(execution.id);
if (execution.suite.manualOnly && !options.suites.includes(execution.suite.id) && !options.list) return false;
if (
options.all ||
(options.list &&
options.groups.length === 0 &&
options.suites.length === 0 &&
options.profiles.length === 0 &&
options.environments.length === 0 &&
options.cases.length === 0)
)
return true;
return (
(options.suites.length === 0 ||
options.suites.includes(execution.suite.id)) &&
options.groups.every((group) => execution.groups.includes(group)) &&
(options.profiles.length === 0 ||
options.profiles.includes(execution.profile.id)) &&
(options.environments.length === 0 ||
options.environments.includes(execution.environment.id)) &&
(options.cases.length === 0 || options.cases.includes(execution.task.id))
);
});
if (selected.length === 0)
throw new RunnerSelectorError(
"Runner E2E selectors matched zero executions",
);
return selected;
}
export function buildMatrixJobs(
executions: readonly MatrixExecution[],
): MatrixJob[] {
if (executions.some(e => ["provider_connection", "github_review_journey"].includes(e.task.flow))) throw new RunnerSelectorError("Attended connection journeys require explicit private setup profiles; they are not admitted to paid CI matrix jobs yet.");
return executions
.map((execution) => ({
executionId: execution.id,
suiteId: execution.suite.id,
profileId: execution.profile.id,
credentialName: execution.profile.credential,
environmentId: execution.environment.id,
caseId: execution.task.id,
timeoutMinutes: Math.max(
execution.environment.id === "daytona" ? 40 : 25,
Math.ceil(
(2 *
(execution.task.attemptTimeoutMs[execution.environment.id] +
90_000) +
5 * 60_000) /
60_000,
),
),
needsDaytona: execution.environment.id === "daytona",
}))
.sort((left, right) => left.executionId.localeCompare(right.executionId));
}