Skip to content

Commit 2ab91d3

Browse files
committed
fix(review): persist merge train settings paths
1 parent ef3257a commit 2ab91d3

5 files changed

Lines changed: 13 additions & 3 deletions

File tree

packages/gittensory-engine/src/focus-manifest.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ export type FocusManifestSettings = Partial<
323323
| "reviewEvasionProtection"
324324
| "reviewEvasionLabel"
325325
| "reviewEvasionComment"
326+
| "mergeTrainMode"
326327
>
327328
> & {
328329
// `typeLabels`/`linkedIssueLabelPropagation`/`linkedIssueHardRules` are declared PARTIAL here (not via the `Pick<RepositorySettings,
@@ -1867,6 +1868,8 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[])
18671868
}
18681869
const reviewEvasionComment = normalizeOptionalBoolean(r.reviewEvasionComment, "settings.reviewEvasionComment", warnings);
18691870
if (reviewEvasionComment !== null) out.reviewEvasionComment = reviewEvasionComment;
1871+
const mergeTrainMode = normalizeOptionalEnum(r.mergeTrainMode, "settings.mergeTrainMode", ["off", "audit", "enforce"] as const, warnings);
1872+
if (mergeTrainMode !== null) out.mergeTrainMode = mergeTrainMode;
18701873
return out;
18711874
}
18721875

packages/gittensory-engine/src/types/manifest-deps-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,9 @@ export type RepositorySettings = {
479479
/** Review-evasion protection: whether to post the public explanation comment before the enforcement close.
480480
* Default true. */
481481
reviewEvasionComment?: boolean | undefined;
482+
/** Merge-train FIFO gate (#selfhost-merge-train): `"off"` keeps current behavior, `"audit"` logs would-hold
483+
* decisions, and `"enforce"` defers a merge behind a still-viable older sibling. */
484+
mergeTrainMode?: "off" | "audit" | "enforce" | undefined;
482485
/** Config-driven before/after screenshot-table gate (#2006): a DETERMINISTIC check (no AI, zero hallucination
483486
* risk) that a contributor visual/frontend PR's body contains a markdown table with before/after image
484487
* markup, scoped to the repo's configured labels/paths (`whenLabels`/`whenPaths`, OR-matched). Off by

src/api/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,7 @@ const maintainerSettingsSchema = z
729729
manifestPolicyGateMode: z.enum(["off", "advisory", "block"]),
730730
selfAuthoredLinkedIssueGateMode: z.enum(["off", "advisory", "block"]),
731731
linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]),
732+
mergeTrainMode: z.enum(["off", "audit", "enforce"]),
732733
firstTimeContributorGrace: z.boolean(),
733734
slopGateMode: z.enum(["off", "advisory", "block"]),
734735
slopGateMinScore: z.number().int().min(0).max(100).nullable(),

test/integration/api.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2448,7 +2448,7 @@ describe("api routes", () => {
24482448
// #2267: qualityGateMode: "block" is downgraded to "advisory" on write — readiness/quality can never
24492449
// hard-block a PR, so the dashboard/API save path can't persist a value implying enforcement it doesn't
24502450
// have. slopGateMode: "block" is a DIFFERENT, legitimately-blockable dimension and is left untouched.
2451-
body: JSON.stringify({ gateCheckMode: "enabled", slopGateMode: "block", slopGateMinScore: 55, qualityGateMode: "block", autonomy: { merge: "auto_with_approval", deploy: "auto" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" }, agentPaused: true, agentDryRun: true }),
2451+
body: JSON.stringify({ gateCheckMode: "enabled", slopGateMode: "block", slopGateMinScore: 55, qualityGateMode: "block", mergeTrainMode: "enforce", autonomy: { merge: "auto_with_approval", deploy: "auto" }, autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" }, agentPaused: true, agentDryRun: true }),
24522452
},
24532453
ownerEnv,
24542454
);
@@ -2462,6 +2462,7 @@ describe("api routes", () => {
24622462
slopGateMode: "block",
24632463
slopGateMinScore: 55,
24642464
qualityGateMode: "advisory", // #2267: downgraded, not persisted as "block"
2465+
mergeTrainMode: "enforce",
24652466
autonomy: { merge: "auto_with_approval" }, // unknown action class dropped by the DB normalizer
24662467
autoMaintain: { requireApprovals: 2, mergeMethod: "rebase" },
24672468
agentPaused: true, // #776 kill-switch

test/unit/focus-manifest.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ describe(".gittensory.yml.example field-exhaustiveness (#1670)", () => {
342342
reviewEvasionProtection: "reviewEvasionProtection:",
343343
reviewEvasionLabel: "reviewEvasionLabel:",
344344
reviewEvasionComment: "reviewEvasionComment:",
345+
mergeTrainMode: "mergeTrainMode:",
345346
typeLabels: "typeLabels:",
346347
linkedIssueLabelPropagation: "linkedIssueLabelPropagation:",
347348
linkedIssueHardRules: "linkedIssueHardRules:",
@@ -1786,13 +1787,14 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
17861787

17871788
it("drops invalid settings values with warnings and keeps the valid ones", () => {
17881789
const m = parseFocusManifest({
1789-
settings: { commentMode: "loud", qualityGateMinScore: "high", autoLabelEnabled: "yes", gittensorLabel: " ", publicSurface: "comment_only" },
1790+
settings: { commentMode: "loud", qualityGateMinScore: "high", autoLabelEnabled: "yes", gittensorLabel: " ", mergeTrainMode: "later", publicSurface: "comment_only" },
17901791
});
17911792
expect(m.settings).toEqual({ publicSurface: "comment_only" });
17921793
expect(m.warnings.some((w) => /settings\.commentMode/.test(w))).toBe(true);
17931794
expect(m.warnings.some((w) => /settings\.qualityGateMinScore/.test(w))).toBe(true);
17941795
expect(m.warnings.some((w) => /settings\.autoLabelEnabled/.test(w))).toBe(true);
17951796
expect(m.warnings.some((w) => /settings\.gittensorLabel/.test(w))).toBe(true);
1797+
expect(m.warnings.some((w) => /settings\.mergeTrainMode/.test(w))).toBe(true);
17961798
});
17971799

17981800
it("ignores a non-mapping settings block and treats a settings-only manifest as present", () => {
@@ -1815,7 +1817,7 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
18151817
});
18161818

18171819
it("round-trips settings through settingsOverrideToJson and serializes empty as null", () => {
1818-
const original = parseFocusManifest({ settings: { commentMode: "all_prs", qualityGateMinScore: 40 } });
1820+
const original = parseFocusManifest({ settings: { commentMode: "all_prs", qualityGateMinScore: 40, mergeTrainMode: "audit" } });
18191821
const reparsed = parseFocusManifest({ settings: settingsOverrideToJson(original.settings) });
18201822
expect(reparsed.settings).toEqual(original.settings);
18211823
expect(settingsOverrideToJson(parseFocusManifest({}).settings)).toBeNull();

0 commit comments

Comments
 (0)