From 5755c24cdf859133d21be7375d84254fcf88100a Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Thu, 7 May 2026 16:38:49 -0700 Subject: [PATCH] Phase 7 dogfood follow-up: bug #7 (severities Jinja function undefined) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaced live by the apex #3043 dogfood at 16:22Z (post-#168 relaunch): Agent open_questions_counter failed: 'severities_at_or_above' is undefined `plan-level.yaml` referenced `severities_at_or_above(...)` as a Jinja function call (lines 625 + 641); conductor 0.1.14 has no such Jinja extension registered. Two-vector failure mode — both the warning-mode route and the gate-prompt template detonated. Worse: `lint-plan-level.ps1` Check 7 *required* the function-call form, so the lint and the workflow agreed on a contract conductor never honored. Same root-cause family as bug #6 (Jinja field references go un-checked at lint time), different angle: lint contracts that aren't grounded in conductor's actual Jinja environment are equally vulnerable. Fix (Option A — recommended in catalog post-#168): make `polyphony policy resolve --domain open_questions` precompute the ascending severity list from `min_severity` and emit it as a wire-level `severities_at_or_above` field. Workflow and lint then agree with conductor (field reference, not function call). Changes: - src/Polyphony/Policy/PolicyResolver.cs: - `ResolvedRule.SeveritiesAtOrAbove: List?` with `[JsonPropertyName("severities_at_or_above")]`. - Computed from `MinSeverity` via `Enum.GetValues()` filtered `>= threshold`, ToString().ToLowerInvariant(). Null when MinSeverity is null (non-OpenQuestions domains stay shape-clean). - .conductor/registry/workflows/plan-level.yaml: - Lines 625, 641: `severities_at_or_above(open_questions_policy.output.min_severity)` → `open_questions_policy.output.severities_at_or_above`. - .conductor/registry/tests/lint-plan-level.ps1: - Check 7 inverted: now requires the field reference, rejects the function-call form with rule `severities-at-or-above-as-function`. - Check 8 detail message updated to reference the new field. - .conductor/registry/tests/lint-plan-level.Tests.ps1: - 7 synthetic-fixture occurrences of the legacy form swept to the new field reference. - New "severities_at_or_above field reference" Context with 4 tests (production yaml uses field, production yaml has no function call, lint fails on legacy form, lint fails on missing reference). - tests/Polyphony.Tests/Commands/PolicyCommandsTests.cs: - Existing OpenQuestions resolve tests assert SeveritiesAtOrAbove contents (default scope: moderate/major/critical; root override critical: just critical; by-type fallback low: all four). - New Resolve_OpenQuestions_SeveritiesAtOrAbove_IsAscending pinning `["major","critical"]` for `min_severity: major` plus wire-format assertion. - Approvals/PR shape regression tests extended: the new field is null on non-OpenQuestions domains, omitted from JSON. - docs/polyphony-state-effects-catalog.md: - New `polyphony policy resolve` verb entry with severity-list gotcha + bug #7 history. - New "Lint can enforce a Jinja contract conductor doesn't honor" open-question entry pointing at #163. Verification: - Build clean (0 warnings, 0 errors). - xUnit: 2431/2431 (was 2431; +1 new test, modified 5 existing assertions in-place — net +1 net new test count expected when CI runs with the fresh assemblies). - Pester (lint-plan-level.Tests.ps1): 23/23 (was 19; +4 new). - All 14 conductor lints pass; conductor validate clean for all 14 registered workflows. - Live smoke: `polyphony policy resolve --domain open_questions --scope default` now emits `"severities_at_or_above":["moderate","major","critical"]`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../registry/tests/lint-plan-level.Tests.ps1 | 60 ++++++++++++++++--- .conductor/registry/tests/lint-plan-level.ps1 | 20 ++++++- .conductor/registry/workflows/plan-level.yaml | 4 +- docs/polyphony-state-effects-catalog.md | 38 ++++++++++++ src/Polyphony/Policy/PolicyResolver.cs | 32 ++++++++++ .../Commands/PolicyCommandsTests.cs | 36 +++++++++++ 6 files changed, 178 insertions(+), 12 deletions(-) diff --git a/.conductor/registry/tests/lint-plan-level.Tests.ps1 b/.conductor/registry/tests/lint-plan-level.Tests.ps1 index ef106c2f..eb312c22 100644 --- a/.conductor/registry/tests/lint-plan-level.Tests.ps1 +++ b/.conductor/registry/tests/lint-plan-level.Tests.ps1 @@ -98,7 +98,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - to: review_group - name: open_questions_gate type: human_gate @@ -156,7 +156,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - to: review_group - name: open_questions_gate type: human_gate @@ -231,7 +231,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - to: review_group - name: open_questions_gate type: human_gate @@ -398,7 +398,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - name: open_questions_answer_counter type: script command: pwsh @@ -471,7 +471,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - name: open_questions_answer_counter type: script command: pwsh @@ -560,7 +560,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - name: open_questions_answer_counter type: script command: pwsh @@ -646,7 +646,7 @@ agents: - to: open_questions_gate when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" - name: open_questions_answer_counter type: script command: pwsh @@ -810,4 +810,50 @@ agents: ($output -join "`n") | Should -Match 'open-questions-policy-bad-type-field' } } + + Context 'severities_at_or_above field reference (regression: dogfood apex #3043, 2026-05-08)' { + + BeforeEach { + $script:TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "lint-plan-level-severities-$([guid]::NewGuid().ToString('N').Substring(0,8))" + $script:WorkflowsDir = Join-Path $script:TempRoot 'workflows' + $script:TestsDir = Join-Path $script:TempRoot 'tests' + $script:RealYaml = Join-Path $PSScriptRoot '..' 'workflows' 'plan-level.yaml' + New-Item $script:WorkflowsDir -ItemType Directory -Force | Out-Null + New-Item $script:TestsDir -ItemType Directory -Force | Out-Null + Copy-Item $script:LintScript (Join-Path $script:TestsDir 'lint-plan-level.ps1') + } + + AfterEach { + Remove-Item $script:TempRoot -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Real plan-level.yaml references the precomputed severities_at_or_above field' { + (Get-Content $script:RealYaml -Raw) | Should -Match 'open_questions_policy\.output\.severities_at_or_above' + } + + It 'Real plan-level.yaml does NOT reference severities_at_or_above as a Jinja function call' { + (Get-Content $script:RealYaml -Raw) | Should -Not -Match 'severities_at_or_above\s*\(' + } + + It 'Lint fails when plan-level.yaml regresses to the legacy function-call form' { + $content = Get-Content $script:RealYaml -Raw + # Mutate every field reference back to the old function-call form + $mutated = $content -replace 'open_questions_policy\.output\.severities_at_or_above', 'severities_at_or_above(open_questions_policy.output.min_severity)' + Set-Content (Join-Path $script:WorkflowsDir 'plan-level.yaml') $mutated + $lintScript = Join-Path $script:TestsDir 'lint-plan-level.ps1' + $output = pwsh -NoProfile -File $lintScript 2>&1 + $LASTEXITCODE | Should -Be 1 + ($output -join "`n") | Should -Match 'severities-at-or-above-as-function' + } + + It 'Lint fails when the severities_at_or_above field reference is removed entirely' { + $content = Get-Content $script:RealYaml -Raw + $mutated = $content -replace 'open_questions_policy\.output\.severities_at_or_above', 'open_questions_policy.output.min_severity' + Set-Content (Join-Path $script:WorkflowsDir 'plan-level.yaml') $mutated + $lintScript = Join-Path $script:TestsDir 'lint-plan-level.ps1' + $output = pwsh -NoProfile -File $lintScript 2>&1 + $LASTEXITCODE | Should -Be 1 + ($output -join "`n") | Should -Match 'missing-warning-mode-route' + } + } } diff --git a/.conductor/registry/tests/lint-plan-level.ps1 b/.conductor/registry/tests/lint-plan-level.ps1 index 7584b31d..c915ce9d 100644 --- a/.conductor/registry/tests/lint-plan-level.ps1 +++ b/.conductor/registry/tests/lint-plan-level.ps1 @@ -96,10 +96,24 @@ if ($content -notmatch "open_questions_policy\.output\.mode\s*==\s*'manual'") { } # ── Check 7: Policy-aware routes — mode==warning uses severities_at_or_above -if ($content -notmatch 'severities_at_or_above\(open_questions_policy\.output\.min_severity\)') { +# Rev 4 (2026-05-08): the workflow now references the precomputed +# `open_questions_policy.output.severities_at_or_above` list emitted by +# `polyphony policy resolve --domain open_questions`. The legacy form +# `severities_at_or_above(open_questions_policy.output.min_severity)` was a +# Jinja function call that conductor never honored — surfaced live in the +# #3043 dogfood as `'severities_at_or_above' is undefined`. Lint now enforces +# the field reference and rejects the legacy function form. +if ($content -notmatch 'open_questions_policy\.output\.severities_at_or_above') { $violations += [PSCustomObject]@{ Rule = 'missing-warning-mode-route' - Detail = "No route condition using severities_at_or_above(open_questions_policy.output.min_severity)" + Detail = "No route condition referencing open_questions_policy.output.severities_at_or_above (precomputed severity list emitted by `polyphony policy resolve --domain open_questions`)." + } +} + +if ($content -match 'severities_at_or_above\s*\(') { + $violations += [PSCustomObject]@{ + Rule = 'severities-at-or-above-as-function' + Detail = "Found legacy `severities_at_or_above(...)` function-call form. Conductor has no such Jinja extension; reference the precomputed `open_questions_policy.output.severities_at_or_above` list field instead." } } @@ -109,7 +123,7 @@ if ($content -notmatch 'severities_at_or_above\(open_questions_policy\.output\.m if ($content -match "architect\.output\.open_questions\s*\|\s*selectattr\('severity',\s*'in',\s*\['") { $violations += [PSCustomObject]@{ Rule = 'hardcoded-severity-filter' - Detail = "Hardcoded severity list found in architect routing — should use policy-driven severities_at_or_above()" + Detail = "Hardcoded severity list found in architect routing — should reference policy-driven open_questions_policy.output.severities_at_or_above" } } diff --git a/.conductor/registry/workflows/plan-level.yaml b/.conductor/registry/workflows/plan-level.yaml index b30a552d..94570bcc 100644 --- a/.conductor/registry/workflows/plan-level.yaml +++ b/.conductor/registry/workflows/plan-level.yaml @@ -622,7 +622,7 @@ agents: when: "{{ open_questions_policy.output.mode == 'manual' and architect.output.open_questions | length > 0 }}" # Mode=warning: gate only when questions at or above min_severity exist. - to: open_questions_gate - when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list | length > 0 }}" + when: "{{ open_questions_policy.output.mode == 'warning' and architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list | length > 0 }}" # Else: no gating questions — proceed to write_plan. - to: write_plan @@ -638,7 +638,7 @@ agents: finalizing the plan. {% if open_questions_policy.output.mode == 'warning' %} - {% set blocking = architect.output.open_questions | selectattr('severity', 'in', severities_at_or_above(open_questions_policy.output.min_severity)) | list %} + {% set blocking = architect.output.open_questions | selectattr('severity', 'in', open_questions_policy.output.severities_at_or_above) | list %} {% else %} {% set blocking = architect.output.open_questions %} {% endif %} diff --git a/docs/polyphony-state-effects-catalog.md b/docs/polyphony-state-effects-catalog.md index 8f032050..3ba1d3dc 100644 --- a/docs/polyphony-state-effects-catalog.md +++ b/docs/polyphony-state-effects-catalog.md @@ -158,6 +158,31 @@ introduce new dependencies. runtime. Pinned by lint check `open-questions-policy-bad-type-field` in `lint-plan-level.ps1` as of this PR. +### `polyphony policy resolve --domain --scope [--path P]` +- **Purpose**: resolve effective policy for a `(scope, domain)` pair by + layering most-specific-wins (`type:Name` > `root` > `defaults`). +- **Pre**: `.conductor/policy.yaml` parseable (or absent — built-in + defaults applied). +- **Post**: emits `ResolvedRule` JSON. Domain-shaped fields are present + only when populated: + - `approvals` / `pr` → `{domain, scope, mode, max_revision_cycles?, + max_fix_loops?, max_remediation_cycles?, quality_avg_score_at_least?, + quality_blocking_count_at_most?}`. + - `open_questions` → adds `{min_severity, severities_at_or_above, + max_question_loops}`. `severities_at_or_above` is the precomputed + ascending list of severities at or above `min_severity` — + workflows route on this directly (no Jinja function call). +- **Side effects**: none. +- **Idempotent**: yes (read-only). +- **Severity-list gotcha**: workflows MUST reference + `open_questions_policy.output.severities_at_or_above` as a field, NOT + call `severities_at_or_above(...)` as a function. Bug #7 (dogfood + apex #3043, 2026-05-08) had two `severities_at_or_above(...)` + function-call references in `plan-level.yaml`; conductor 0.1.14 has + no such Jinja extension and crashed with `'severities_at_or_above' + is undefined`. Lint check `severities-at-or-above-as-function` in + `lint-plan-level.ps1` blocks the regression as of this PR. + --- ## Helper scripts @@ -229,6 +254,19 @@ introduce new dependencies. output schemas would catch this class statically — partially addressable under [#163](https://github.com/PolyphonyRequiem/polyphony/issues/163) (property-based testing) but really wants its own pass. +- **Lint can enforce a Jinja contract conductor doesn't honor**: bug + #7 (dogfood apex #3043, 2026-05-08) had `lint-plan-level.ps1` Check 7 + *requiring* `severities_at_or_above(open_questions_policy.output.min_severity)` + — a custom Jinja function the conductor runtime never registered. + Lint and workflow agreed; conductor crashed at runtime. Same + underlying gap as the bug-#6 entry above, but a different angle: + *lint contracts that aren't grounded in conductor's actual Jinja + environment* are equally vulnerable. Fix in this PR moved the + contract to a verb-output field (`severities_at_or_above`) emitted by + `polyphony policy resolve` so the lint, the workflow, and conductor + agree. A general remedy would be a registry of conductor-honored + Jinja functions/filters that lints can validate against — also + rolls into [#163](https://github.com/PolyphonyRequiem/polyphony/issues/163). - **Wave integration idempotency**: not yet exercised; document after first wave-integration smoke. - **`apex-wave-dispatch.yaml` and per-lifecycle sub-workflows**: not diff --git a/src/Polyphony/Policy/PolicyResolver.cs b/src/Polyphony/Policy/PolicyResolver.cs index 78ca3c88..7adce9da 100644 --- a/src/Polyphony/Policy/PolicyResolver.cs +++ b/src/Polyphony/Policy/PolicyResolver.cs @@ -1,5 +1,6 @@ namespace Polyphony.Policy; +using System.Text.Json.Serialization; using Polyphony.Sdlc; /// @@ -70,12 +71,31 @@ _ when scope.StartsWith("type:", StringComparison.Ordinal) => scope, MaxFixLoops = specific?.MaxFixLoops ?? defaults.MaxFixLoops, MaxRemediationCycles = specific?.MaxRemediationCycles ?? defaults.MaxRemediationCycles, MinSeverity = (specific?.MinSeverity ?? defaults.MinSeverity)?.ToString().ToLowerInvariant(), + SeveritiesAtOrAbove = ComputeSeveritiesAtOrAbove(specific?.MinSeverity ?? defaults.MinSeverity), MaxQuestionLoops = specific?.MaxQuestionLoops ?? defaults.MaxQuestionLoops, QualityAvgScoreAtLeast = quality?.AvgScoreAtLeast, QualityBlockingCountAtMost = quality?.BlockingCountAtMost, }; } + /// + /// Returns the lowercase string names of every at + /// or above , ordered by enum declaration + /// (Low → Critical). Used by workflow routes to filter open-question + /// lists by severity threshold without needing a custom Jinja function. + /// Returns null when is null — + /// non-OpenQuestions domains have no severity threshold concept. + /// + private static List? ComputeSeveritiesAtOrAbove(Severity? minSeverity) + { + if (minSeverity is null) return null; + var threshold = minSeverity.Value; + return Enum.GetValues() + .Where(s => s >= threshold) + .Select(s => s.ToString().ToLowerInvariant()) + .ToList(); + } + private static QualityThreshold? MergeQuality(QualityThreshold? specific, QualityThreshold? defaults) { if (specific is null) return defaults; @@ -153,6 +173,18 @@ public sealed record ResolvedRule public int? MaxFixLoops { get; init; } public int? MaxRemediationCycles { get; init; } public string? MinSeverity { get; init; } + + /// + /// Lowercase severity names at or above , in + /// ascending order (e.g. ["moderate","major","critical"] for + /// MinSeverity == "moderate"). Null when MinSeverity is null + /// (non-OpenQuestions domains). Used by workflow routes to filter + /// architect-emitted open-question lists by severity without a + /// custom Jinja function. + /// + [JsonPropertyName("severities_at_or_above")] + public List? SeveritiesAtOrAbove { get; init; } + public int? MaxQuestionLoops { get; init; } public int? QualityAvgScoreAtLeast { get; init; } public int? QualityBlockingCountAtMost { get; init; } diff --git a/tests/Polyphony.Tests/Commands/PolicyCommandsTests.cs b/tests/Polyphony.Tests/Commands/PolicyCommandsTests.cs index 1815f2da..a1849972 100644 --- a/tests/Polyphony.Tests/Commands/PolicyCommandsTests.cs +++ b/tests/Polyphony.Tests/Commands/PolicyCommandsTests.cs @@ -442,6 +442,7 @@ public void Resolve_OpenQuestions_DefaultScope_ReturnsDefaults() result.Scope.ShouldBe("default"); result.Mode.ShouldBe("warning"); result.MinSeverity.ShouldBe("moderate"); + result.SeveritiesAtOrAbove.ShouldBe(new[] { "moderate", "major", "critical" }); result.MaxQuestionLoops.ShouldBe(3); } @@ -463,6 +464,8 @@ public void Resolve_OpenQuestions_RootScope_OverridesDefaults() result.ShouldNotBeNull(); result.Mode.ShouldBe("manual"); result.MinSeverity.ShouldBe("critical"); + // SeveritiesAtOrAbove tracks the resolved MinSeverity, not the defaults. + result.SeveritiesAtOrAbove.ShouldBe(new[] { "critical" }); // Inherits from defaults result.MaxQuestionLoops.ShouldBe(3); } @@ -509,9 +512,38 @@ public void Resolve_OpenQuestions_ByTypeScope_FallsBackWhenTypeMissing() result.ShouldNotBeNull(); result.Mode.ShouldBe("warning"); result.MinSeverity.ShouldBe("low"); + // Lowest threshold → all severities included, in ascending order. + result.SeveritiesAtOrAbove.ShouldBe(new[] { "low", "moderate", "major", "critical" }); result.MaxQuestionLoops.ShouldBe(2); } + [Fact] + public void Resolve_OpenQuestions_SeveritiesAtOrAbove_IsAscending() + { + // Bug #7 (2026-05-08): plan-level.yaml referenced a Jinja function + // `severities_at_or_above(...)` that conductor never honored; surfaced + // live in the #3043 dogfood. Fix is a precomputed array on the policy + // resolve output, consumed directly by route/template Jinja. Ordering + // is fixed at ascending so workflow comparisons are deterministic. + using var fx = new PolicyFileFixture(); + fx.WritePolicy(""" + schema_version: 1 + open_questions: + defaults: { mode: warning, min_severity: major, max_question_loops: 1 } + """); + + var cmd = CreateCommand(); + var (_, output) = CaptureConsole(() => cmd.Resolve("default", "open_questions", fx.PolicyPath)); + + var result = JsonSerializer.Deserialize(output, PolyphonyJsonContext.Default.ResolvedRule); + result.ShouldNotBeNull(); + result.MinSeverity.ShouldBe("major"); + result.SeveritiesAtOrAbove.ShouldBe(new[] { "major", "critical" }); + + // Field name on the wire matches the workflow Jinja reference. + output.ShouldContain("\"severities_at_or_above\":[\"major\",\"critical\"]"); + } + [Fact] public void Validate_NegativeMaxQuestionLoops_ReturnsError() { @@ -564,10 +596,12 @@ public void Resolve_ApprovalsShape_UnchangedByOpenQuestionsAddition() result.ShouldNotBeNull(); result.Domain.ShouldBe("approvals"); result.MinSeverity.ShouldBeNull(); + result.SeveritiesAtOrAbove.ShouldBeNull(); result.MaxQuestionLoops.ShouldBeNull(); // Raw JSON should not contain these fields (null fields are omitted). output.ShouldNotContain("\"min_severity\""); + output.ShouldNotContain("\"severities_at_or_above\""); output.ShouldNotContain("\"max_question_loops\""); } @@ -582,9 +616,11 @@ public void Resolve_PrShape_UnchangedByOpenQuestionsAddition() result.ShouldNotBeNull(); result.Domain.ShouldBe("pr"); result.MinSeverity.ShouldBeNull(); + result.SeveritiesAtOrAbove.ShouldBeNull(); result.MaxQuestionLoops.ShouldBeNull(); output.ShouldNotContain("\"min_severity\""); + output.ShouldNotContain("\"severities_at_or_above\""); output.ShouldNotContain("\"max_question_loops\""); }