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
116 changes: 79 additions & 37 deletions test/e2e/oracle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,12 @@ describe("predictSection rules", () => {
expect(p.mayWrite).toBe(false);
});

test("exclusion folds before grades and witnesses", () => {
test("exclusion folds before grades and witnesses: the section predicts at NO grade", () => {
// A declared section outside the `sections` allowlist never runs: the
// engine reports it "excluded" before any read, so neither the denied
// grade nor the seeded witness may tighten the prediction.
// grade nor the seeded witness may tighten the prediction, and the grades
// are empty - the section runs at no grade, so preflight and the
// write-granted fold are vacuous over it without recognizing "excluded".
const p = predictSection(
"labels",
meta({
Expand All @@ -477,45 +479,85 @@ describe("predictSection rules", () => {
liveKinds: { labels: "drift-update" },
}),
);
expect([...p.allowed]).toEqual(["excluded"]);
expect(p.mayWrite).toBe(false);
// An undefined allowlist keeps today's behavior: every section runs.
const unrestricted = predictSection("labels", meta({ mode: "check" }));
expect(unrestricted.allowed.has("excluded")).toBe(false);
expect(p).toEqual({
key: "labels",
grades: [],
allowed: new Set(["excluded"]),
mayWrite: false,
});
// An undefined allowlist keeps today's behavior: every section runs, and
// the denied grade with its 403 style reads as a check-mode failure.
const unrestricted = predictSection(
"labels",
meta({ mask: { issues: "none" }, denialStyle: 403, mode: "check" }),
);
expect(unrestricted).toEqual({
key: "labels",
grades: ["none"],
allowed: new Set(["failed"]),
mayWrite: false,
});
});

test("an excluded section does not flip fullyGranted", () => {
// The fixpoint gates (converges / apply_idempotent) quantify over the
// sections that WILL run; a denied-but-excluded section must not block
// them.
const p = predictOutcomes(
test("an excluded NO_READ section in check mode is excluded, not the read-free clean", () => {
// check_suite_preferences makes no request in check mode and is otherwise
// exactly clean; exclusion folds before that rule too, and the read-free
// preflight exemption does not need to see the section since it has no grade.
const p = predictSection(
"check_suite_preferences",
meta({
sections: ["labels", "pages"],
onlySections: ["pages"],
mask: { issues: "none" },
mode: "apply",
policy: "warn",
sections: ["check_suite_preferences", "labels"],
onlySections: ["labels"],
mask: { checks: "none" },
mode: "check",
}),
);
expect(p.fullyGranted).toBe(true);
expect(p.preflightAborts).toBe("no");
expect(p).toEqual({
key: "check_suite_preferences",
grades: [],
allowed: new Set(["excluded"]),
mayWrite: false,
});
});

test("an excluded denied section never arms the preflight barrier", () => {
// Preflight probes only ACTIVE sections, so a permission-denied section
// that the allowlist excludes cannot abort the run.
const p = predictOutcomes(
meta({
sections: ["labels"],
onlySections: ["pages"],
mask: { issues: "none" },
denialStyle: 403,
mode: "apply",
policy: "fail",
}),
);
expect(p.preflightAborts).toBe("no");
expect([...p.allowedExitCodes]).toEqual([0]);
test("an excluded denied section beside an active one: the run follows the active one alone", () => {
// apply + fail + 403 with the excluded section's read denied: preflight
// probes only the active section, so the barrier never arms, the excluded
// section is not write-denied (it writes nothing), and the fixpoint gate
// (fullyGranted) quantifies over the section that WILL run.
const excludedDenied = meta({
sections: ["labels", "pages"],
onlySections: ["pages"],
mask: { issues: "none" },
denialStyle: 403,
mode: "apply",
policy: "fail",
});
expect(predictOutcomes(excludedDenied)).toEqual({
sections: [
{ key: "labels", grades: [], allowed: new Set(["excluded"]), mayWrite: false },
{ key: "pages", grades: ["write"], allowed: new Set(["applied"]), mayWrite: true },
],
allowedExitCodes: new Set([0]),
noWritesInCheck: false,
writeDeniedSections: [],
fullyGranted: true,
preflightAborts: "no",
});

// The control: the same meta with labels ACTIVE reaches the denied read,
// and the barrier aborts the run.
expect(predictOutcomes({ ...excludedDenied, onlySections: undefined })).toEqual({
sections: [
{ key: "labels", grades: ["none"], allowed: new Set(["failed"]), mayWrite: false },
{ key: "pages", grades: ["write"], allowed: new Set(["applied"]), mayWrite: true },
],
allowedExitCodes: new Set([1]),
noWritesInCheck: false,
writeDeniedSections: ["labels"],
fullyGranted: false,
preflightAborts: "yes",
});
});

test("an EMPTY allowlist is unrestricted, mirroring the engine's size > 0 gate", () => {
Expand Down Expand Up @@ -575,8 +617,8 @@ describe("predictSection rules", () => {

test("exclusion folds before the personal-account no-op", () => {
// Both folds precede the grades; an excluded org-only section on a user
// owner is excluded, not applied, and keeps the mask's grades like every
// other excluded section.
// owner is excluded, not applied, and like every other excluded section
// runs at no grade - neither the mask's denial nor the no-op's write grade.
const p = predictSection(
"teams",
meta({
Expand All @@ -589,7 +631,7 @@ describe("predictSection rules", () => {
);
expect(p).toEqual({
key: "teams",
grades: ["none"],
grades: [],
allowed: new Set(["excluded"]),
mayWrite: false,
});
Expand Down
44 changes: 21 additions & 23 deletions test/e2e/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,16 @@ export function effectiveGrades(grant: MaskGrade, gating: ReadGating): readonly
/** The predicted set of outcomes a section may land in, given the run's shape. */
export interface SectionPrediction {
key: SectionKey;
/** The grades the section may run at (effectiveGrades); each contributes to `allowed`. */
/**
* The grades the section may run at (effectiveGrades); each contributes to
* `allowed`. EMPTY for an excluded section (declared, but outside a
* non-empty `sections` allowlist): orchestrate.ts's disposition filter
* classifies it before the preflight list is built and before the section
* loop reads anything, so it runs at no grade, and every fold over these
* grades (preflightDeniable, writeGranted, and through them fullyGranted
* and writeDeniedSections) is vacuous for it by construction - no consumer
* recognizes "excluded" by hand.
*/
grades: readonly MaskGrade[];
/** The outcomes the section is allowed to report; the runner must see one. */
allowed: Set<Outcome>;
Expand All @@ -174,26 +183,23 @@ export function predictSection(key: SectionKey, meta: ScenarioMeta): SectionPred
* Two folds precede the grades because they decide whether the mask is
* consulted at all: exclusion (the section never runs) and the org-only
* no-op on a personal account (the section runs, but never past its
* ungated org probe).
* ungated org probe). Each mints the whole prediction here, grades included.
*/
export function predictSectionAt(
key: SectionKey,
meta: ScenarioMeta,
gating: ReadGating,
): SectionPrediction {
const grant = sectionGrade(key, meta.mask, meta.orgMask ?? meta.mask);
const grades = effectiveGrades(grant, gating);
// A declared section outside the `sections` allowlist never runs: the engine
// reports it "excluded" before any read (orchestrate.ts), so exclusion folds
// before EVERYTHING - grades, denial semantics, and witnesses alike. An
// EMPTY allowlist means unrestricted, mirroring orchestrate.ts's size > 0
// gate, so only a non-empty list excludes.
// Exclusion folds before EVERYTHING - grades, denial semantics, and
// witnesses alike (see SectionPrediction.grades). An EMPTY allowlist means
// unrestricted, mirroring orchestrate.ts's size > 0 gate, so only a
// non-empty list excludes.
if (
meta.onlySections !== undefined &&
meta.onlySections.length > 0 &&
!meta.onlySections.includes(key)
) {
return { key, grades, allowed: new Set(["excluded"]), mayWrite: false };
return { key, grades: [], allowed: new Set(["excluded"]), mayWrite: false };
}
// An org-only section on a personal account no-ops regardless of mask: its
// org probe (declared permission "none", so no mask key gates it) 404s and
Expand All @@ -211,6 +217,8 @@ export function predictSectionAt(
mayWrite: false,
};
}
const grant = sectionGrade(key, meta.mask, meta.orgMask ?? meta.mask);
const grades = effectiveGrades(grant, gating);
const deniedAtGatedRead = gating === "mixed" && grant === "read";
const arms = grades.flatMap((grade) =>
grade === "none" && deniedAtGatedRead
Expand Down Expand Up @@ -349,10 +357,7 @@ export interface RunPrediction {
noWritesInCheck: boolean;
/** Sections whose denied writes must never mutate state (mock rule 4). */
writeDeniedSections: SectionKey[];
/**
* True when every ACTIVE section is write-granted (convergence expected).
* Excluded sections never run, so they do not count against this.
*/
/** True when every section is write-granted (convergence expected). */
fullyGranted: boolean;
/**
* Whether the run aborts at the preflight barrier (apply + fail policy, a denied
Expand Down Expand Up @@ -416,7 +421,7 @@ function foldPreflightAbort(verdicts: readonly PreflightAbort[]): PreflightAbort
return verdicts.includes("possible") ? "possible" : "no";
}

/** True when the section runs write-granted for certain (its only effective grade is write). */
/** True when every effective grade is write: the section runs write-granted for certain. */
function writeGranted(section: SectionPrediction): boolean {
return section.grades.every((grade) => grade === "write");
}
Expand All @@ -427,11 +432,6 @@ function writeGranted(section: SectionPrediction): boolean {
* grades make it "possible": the probe reaches the gated read only for some content.
*/
export function preflightDeniable(section: SectionPrediction, meta: ScenarioMeta): PreflightAbort {
// Preflight only probes ACTIVE sections (orchestrate.ts filters by the
// allowlist first), so an excluded section can never arm the barrier.
if (section.allowed.has("excluded")) {
return "no";
}
if (NO_READ_SECTIONS.has(section.key)) {
// No read endpoints: preflight probes nothing, so the barrier cannot arm.
return "no";
Expand Down Expand Up @@ -475,9 +475,7 @@ export function predictOutcomes(meta: ScenarioMeta): RunPrediction {
allowedExitCodes: exitCodes,
noWritesInCheck: check,
writeDeniedSections: sections.filter((s) => !writeGranted(s) && !s.mayWrite).map((s) => s.key),
// Excluded sections never run, so they cannot break convergence or
// idempotence: fullyGranted quantifies over the sections that WILL run.
fullyGranted: sections.every((s) => s.allowed.has("excluded") || writeGranted(s)),
fullyGranted: sections.every(writeGranted),
preflightAborts,
};
}
Expand Down
Loading