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
10 changes: 10 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,16 @@ review:
# quiet | normal | detailed. Default: null/normal (byte-identical).
# comment_verbosity: normal

# How a `@gittensory generate-tests` result is delivered, once features.e2eTests is on (#4197, part of
# the #4189 E2E-test-generation epic). `comment` (default, null/absent) posts the generated test as a
# reply comment only. `commit` pushes it as a real commit onto the PR's own head branch instead -- a
# bigger blast radius, so it stays opt-in even with e2eTests already enabled. `commit` mode is ALSO
# blocked at runtime (regardless of this setting) for a PR whose author is a confirmed Gittensor miner,
# to protect the externally-computed score from ever including a maintainer-authored line the miner
# didn't write themselves.
# comment | commit. Default: null/comment (byte-identical -- no write access to any PR branch).
# e2e_test_delivery: comment

# Inline-comment layer toggles (#1956 / #1958). Bool | null. Default: null/false — byte-identical.
# Requires operator flag GITTENSORY_REVIEW_INLINE_COMMENTS + cutover allowlist + review.inline_comments: true.
# inline_comments: false
Expand Down
10 changes: 10 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,16 @@ review:
# quiet | normal | detailed. Default: null/normal (byte-identical).
# comment_verbosity: normal

# How a `@gittensory generate-tests` result is delivered, once features.e2eTests is on (#4197, part of
# the #4189 E2E-test-generation epic). `comment` (default, null/absent) posts the generated test as a
# reply comment only. `commit` pushes it as a real commit onto the PR's own head branch instead -- a
# bigger blast radius, so it stays opt-in even with e2eTests already enabled. `commit` mode is ALSO
# blocked at runtime (regardless of this setting) for a PR whose author is a confirmed Gittensor miner,
# to protect the externally-computed score from ever including a maintainer-authored line the miner
# didn't write themselves.
# comment | commit. Default: null/comment (byte-identical -- no write access to any PR branch).
# e2e_test_delivery: comment

# Inline-comment layer toggles (#1956 / #1958). Bool | null. Default: null/false — byte-identical.
# Requires operator flag GITTENSORY_REVIEW_INLINE_COMMENTS + cutover allowlist + review.inline_comments: true.
# inline_comments: false
Expand Down
25 changes: 22 additions & 3 deletions packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,15 @@ export type FocusManifestReviewConfig = {
* (default, absent) ⇒ byte-identical to today. Net-new vs the changed-files-summary (#1957) and effort-score
* (#1955) knobs. (#2047) */
commentVerbosity: CommentVerbosity | null;
/** `review.e2e_test_delivery` (#4197, part of the #4189 epic): how a `@gittensory generate-tests` result is
* delivered once `features.e2eTests` is on. `"comment"` (default, null/absent) posts the generated test as
* a reply comment only — no write access to the PR branch. `"commit"` pushes it as a real commit onto the
* PR's own head branch (git/trees -> git/commits -> a ref UPDATE, mirroring `repo-doc-pr.ts`'s write
* chokepoint) — a materially bigger blast radius, so it stays opt-in per repo even with e2eTests already
* on. `"commit"` mode is additionally blocked at runtime (regardless of this config) for a PR whose author
* is a confirmed Gittensor miner, to protect the external, upstream-computed score from ever including a
* maintainer-authored line the miner didn't write themselves — see `src/github/e2e-test-commit.ts`. */
e2eTestDelivery: E2eTestDeliveryMode | null;
/** `review.path_instructions`: per-path natural-language guidance handed to the AI reviewer when the PR's
* changed files match the glob. Empty (default) ⇒ byte-identical reviewer prompt. Also consumed by
* AI-generated E2E test coverage (`resolveE2eTestGenInstructions` in `ai-e2e-test-gen.ts`, #4200) when
Expand Down Expand Up @@ -591,6 +600,10 @@ export type LinkedIssueSatisfactionMode = (typeof LINKED_ISSUE_SATISFACTION_MODE
export const COMMENT_VERBOSITY_LEVELS = ["quiet", "normal", "detailed"] as const;
export type CommentVerbosity = (typeof COMMENT_VERBOSITY_LEVELS)[number];

/** `review.e2e_test_delivery` modes (#4197). `comment` = today's behavior (same as unset). */
export const E2E_TEST_DELIVERY_MODES = ["comment", "commit"] as const;
export type E2eTestDeliveryMode = (typeof E2E_TEST_DELIVERY_MODES)[number];

/** One `review.labeling_rules[]` entry: a non-reserved `label` plus the deterministic `when` criteria that must ALL
* match for it to fire. A rule always has at least one criterion (enforced at parse). */
export type LabelingRule = {
Expand Down Expand Up @@ -936,7 +949,7 @@ const EMPTY_MANIFEST: FocusManifest = {
publicNotes: [],
gate: { ...EMPTY_GATE_CONFIG },
settings: {},
review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null },
review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null },
features: { ...EMPTY_FEATURES_CONFIG },
contentLane: { ...EMPTY_CONTENT_LANE_CONFIG },
repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG },
Expand Down Expand Up @@ -966,7 +979,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo
warnings,
gate: { ...EMPTY_GATE_CONFIG },
settings: {},
review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null },
review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null },
features: { ...EMPTY_FEATURES_CONFIG },
contentLane: { ...EMPTY_CONTENT_LANE_CONFIG },
repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG },
Expand Down Expand Up @@ -1990,7 +2003,7 @@ function parsePublicSafeText(value: JsonValue | undefined, field: string, warnin
* throws; invalid/unsafe values are dropped with warnings.
*/
function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestReviewConfig {
const empty: FocusManifestReviewConfig = { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null };
const empty: FocusManifestReviewConfig = { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null };
if (value === undefined || value === null) return empty;
if (typeof value !== "object" || Array.isArray(value)) {
warnings.push(`Manifest field "review" must be a mapping; ignoring it.`);
Expand Down Expand Up @@ -2050,6 +2063,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo
);
const maxFindings = parseMaxFindingsConfig(r.max_findings, warnings);
const commentVerbosity = normalizeOptionalEnum(r.comment_verbosity, "review.comment_verbosity", COMMENT_VERBOSITY_LEVELS, warnings);
const e2eTestDelivery = normalizeOptionalEnum(r.e2e_test_delivery, "review.e2e_test_delivery", E2E_TEST_DELIVERY_MODES, warnings);
const pathInstructions = parseReviewPathInstructions(r.path_instructions, warnings);
const instructions = parsePublicSafeText(r.instructions, "review.instructions", warnings);
const excludePaths = parseReviewExcludePaths(r.exclude_paths, warnings);
Expand Down Expand Up @@ -2082,6 +2096,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo
minFindingSeverity !== null ||
maxFindingsPresent(maxFindings) ||
commentVerbosity !== null ||
e2eTestDelivery !== null ||
pathInstructions.length > 0 ||
instructions !== null ||
excludePaths.length > 0 ||
Expand Down Expand Up @@ -2120,6 +2135,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo
minFindingSeverity,
maxFindings,
commentVerbosity,
e2eTestDelivery,
pathInstructions,
instructions,
excludePaths,
Expand Down Expand Up @@ -2216,6 +2232,7 @@ function computeReviewConfigPresent(review: Omit<FocusManifestReviewConfig, "pre
review.minFindingSeverity !== null ||
maxFindingsPresent(review.maxFindings) ||
review.commentVerbosity !== null ||
review.e2eTestDelivery !== null ||
review.pathInstructions.length > 0 ||
review.instructions !== null ||
review.excludePaths.length > 0 ||
Expand Down Expand Up @@ -2260,6 +2277,7 @@ export function overlayReviewConfig(
minFindingSeverity: pickOverlayNullable(override.minFindingSeverity, base.minFindingSeverity),
maxFindings: overlayMaxFindingsConfig(base.maxFindings, override.maxFindings),
commentVerbosity: pickOverlayNullable(override.commentVerbosity, base.commentVerbosity),
e2eTestDelivery: pickOverlayNullable(override.e2eTestDelivery, base.e2eTestDelivery),
pathInstructions: override.pathInstructions.length > 0 ? [...override.pathInstructions] : [...base.pathInstructions],
instructions: pickOverlayNullable(override.instructions, base.instructions),
excludePaths: pickOverlayStringList(override.excludePaths, base.excludePaths),
Expand Down Expand Up @@ -2765,6 +2783,7 @@ export function reviewConfigToJson(review: FocusManifestReviewConfig): JsonValue
out.max_findings = maxFindings;
}
if (review.commentVerbosity !== null) out.comment_verbosity = review.commentVerbosity;
if (review.e2eTestDelivery !== null) out.e2e_test_delivery = review.e2eTestDelivery;
if (review.instructions !== null) out.instructions = review.instructions;
if (review.pathInstructions.length > 0) out.path_instructions = review.pathInstructions.map((entry) => ({ path: entry.path, instructions: entry.instructions }));
if (review.excludePaths.length > 0) out.exclude_paths = [...review.excludePaths];
Expand Down
102 changes: 102 additions & 0 deletions src/github/e2e-test-commit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// E2E test-generation commit delivery (#4197, part of the #4189 epic). Pushes an AI-generated test file as a
// real commit onto an EXISTING PR's own head branch — reusing the SAME installation-token write chokepoint
// (`makeInstallationOctokit`) and git/trees -> git/commits pattern as `repo-doc-pr.ts`, but updating an
// existing ref (`PATCH git/refs/{ref}`) instead of creating a new branch/PR.
//
// Deliberately narrower in scope than repo-doc-pr.ts: this writes to SOMEONE ELSE'S branch (the PR author's),
// not a branch gittensory itself owns, so it carries a materially bigger blast radius — see #4195's
// maintainer-only authorization tier and the miner-scoring safeguard below, both required before this is ever
// invoked for real.
//
// SCORING-INTEGRITY SAFEGUARD (#4201): gittensory does not compute the authoritative Gittensor score itself —
// it is computed by external validators reading the merged PR directly from GitHub. A commit this module
// pushes onto a CONFIRMED MINER's PR branch would be indistinguishable, to that external validator, from a
// line the miner wrote themselves, inflating their apparent contribution. `isMinerAuthoredBranch` must be
// checked by the CALLER before invoking `commitE2eTestToPrBranch` for a confirmed-miner PR — this module does
// not re-check it itself (the caller already resolved miner status while authorizing the command, so
// re-deriving it here would be a redundant, easy-to-drift second source of truth).
import { githubErrorStatus, withInstallationTokenRetry } from "./app";
import { githubRateLimitAdmissionKeyForInstallation, makeInstallationOctokit } from "./client";
import { errorMessage, repoParts } from "../utils/json";
import type { AgentActionMode } from "../settings/agent-execution";

export type E2eTestCommitResult =
| { status: "committed"; commitSha: string; htmlUrl: string }
| { status: "declined"; reason: string }
| { status: "error"; reason: string };

/** Default path for a generated test file — clearly labeled and namespaced by PR number so repeat
* invocations on the same PR overwrite the same file rather than accumulating duplicates. A maintainer who
* wants a different location can move the file after it lands; this module has no per-repo convention to
* read (that is a possible future enhancement, not required for this first delivery mode). */
export function defaultE2eTestFilePath(prNumber: number): string {
return `e2e/gittensory-pr-${prNumber}.spec.ts`;
}

/**
* Push a generated test file as a new commit onto an EXISTING PR's head branch. Fail-safe on every expected
* failure mode (never a `"live"` mode, no write access / fork without "Allow edits by maintainers", the
* branch moved since this pass started) — those all return `{ status: "declined", reason }`, not a thrown
* error. A genuinely unexpected failure (network, auth) returns `{ status: "error", reason }` instead, so a
* caller can tell "this could never have worked" apart from "something broke and should be retried/reported".
*/
export async function commitE2eTestToPrBranch(
env: Env,
args: {
installationId: number;
repoFullName: string;
prNumber: number;
headRef: string;
headSha: string;
testSource: string;
actor: string;
mode: AgentActionMode;
testFilePath?: string | undefined;
},
): Promise<E2eTestCommitResult> {
if (args.mode !== "live") return { status: "declined", reason: `commit not pushed: action mode is "${args.mode}"` };
const { owner, name: repo } = repoParts(args.repoFullName);
const path = args.testFilePath?.trim() || defaultE2eTestFilePath(args.prNumber);
const message = `test: add AI-generated E2E test\n\nGenerated-by: gittensory (invoked by @${args.actor})`;
try {
return await withInstallationTokenRetry(env, args.installationId, async (token) => {
const octokit = makeInstallationOctokit(env, token, args.mode, githubRateLimitAdmissionKeyForInstallation(args.installationId));

const headCommit = await octokit.request("GET /repos/{owner}/{repo}/git/commits/{commit_sha}", { owner, repo, commit_sha: args.headSha });
const baseTreeSha = (headCommit.data as { tree: { sha: string } }).tree.sha;

const tree = await octokit.request("POST /repos/{owner}/{repo}/git/trees", {
owner,
repo,
base_tree: baseTreeSha,
tree: [{ path, mode: "100644", type: "blob", content: args.testSource }],
});
const treeSha = (tree.data as { sha: string }).sha;

const commit = await octokit.request("POST /repos/{owner}/{repo}/git/commits", {
owner,
repo,
message,
tree: treeSha,
parents: [args.headSha],
});
const commitSha = (commit.data as { sha: string }).sha;

// A ref UPDATE (not create) against the PR's own existing head branch — the one structural difference
// from repo-doc-pr.ts's new-branch flow. `force: false` (the default) so a genuinely concurrent push to
// the same branch surfaces as a 422/409 (handled below) rather than silently discarding it.
await octokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", { owner, repo, ref: `heads/${args.headRef}`, sha: commitSha });

return { status: "committed", commitSha, htmlUrl: `https://github.com/${args.repoFullName}/commit/${commitSha}` };
});
} catch (error) {
const status = githubErrorStatus(error);
if (status === 403 || status === 404) {
return { status: "declined", reason: 'no write access to the PR branch (a fork PR needs "Allow edits by maintainers" enabled, or the installation lacks contents:write)' };
}
if (status === 422 || status === 409) {
return { status: "declined", reason: "the PR branch moved since this pass started (ref update rejected) — try the command again" };
}
return { status: "error", reason: errorMessage(error, "unknown error committing the generated test") };
}
}
Loading