forgedock 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/bin/engine/outcome-codes.mjs +13 -0
  2. package/bin/engine/phases.mjs +54 -4
  3. package/bin/engine/runlog.mjs +10 -2
  4. package/bin/engine/telemetry-seam.mjs +3 -0
  5. package/bin/engine/telemetry.test.mjs +3 -2
  6. package/bin/engine-cli.mjs +32 -2
  7. package/bin/engine.mjs +139 -7
  8. package/bin/runner.mjs +69 -9
  9. package/bin/stats/aggregate.mjs +7 -3
  10. package/bin/stats/cache.mjs +7 -3
  11. package/bin/stats/github-source.mjs +136 -28
  12. package/bin/stats/index.mjs +46 -9
  13. package/bin/stats/render.mjs +1 -1
  14. package/bin/stats/stats.test.mjs +201 -40
  15. package/commands/review-pr-staging.md +4 -4
  16. package/commands/review-pr.md +25 -15
  17. package/commands/work-on/build/architect.md +53 -0
  18. package/commands/work-on/build/implement.md +19 -0
  19. package/commands/work-on/build/validate.md +320 -28
  20. package/commands/work-on/build.md +66 -9
  21. package/commands/work-on/investigate.md +7 -0
  22. package/commands/work-on/remediate.md +32 -13
  23. package/commands/work-on/review.md +32 -19
  24. package/docs/CONFIG.md +89 -2
  25. package/docs/OUTCOME-CODES.md +5 -1
  26. package/docs/STATS.md +11 -4
  27. package/docs/TELEMETRY.md +5 -2
  28. package/docs/WORK-ON-RUNTIME.md +18 -0
  29. package/package.json +2 -2
  30. package/packages/protocol/src/types.js +1 -1
  31. package/scripts/acceptance-red-green.sh +90 -0
  32. package/scripts/acceptance-red-green.test.sh +74 -0
  33. package/scripts/cap-findings.test.sh +2 -0
  34. package/scripts/check-contract-scope.test.sh +2 -0
  35. package/scripts/check-edge-cases.sh +143 -0
  36. package/scripts/check-edge-cases.test.sh +175 -0
  37. package/scripts/check-spec-markers.sh +2 -0
  38. package/scripts/check-test-tmp-cleanup.sh +56 -0
  39. package/scripts/check-test-tmp-cleanup.test.sh +69 -0
  40. package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
  41. package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
  42. package/scripts/ci-local/adapters/buildkite.mjs +58 -0
  43. package/scripts/ci-local/adapters/circleci.mjs +101 -0
  44. package/scripts/ci-local/adapters/common.mjs +115 -0
  45. package/scripts/ci-local/adapters/explicit.mjs +21 -0
  46. package/scripts/ci-local/adapters/github-actions.mjs +349 -0
  47. package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
  48. package/scripts/ci-local/adapters/portability.mjs +104 -0
  49. package/scripts/ci-local/adapters/task-runners.mjs +67 -0
  50. package/scripts/ci-local/cli.mjs +50 -0
  51. package/scripts/ci-local/lib.mjs +371 -0
  52. package/scripts/ci-local/plan.mjs +186 -0
  53. package/scripts/ci-local/run.mjs +345 -0
  54. package/scripts/ci-local/scoping.mjs +508 -0
  55. package/scripts/ci-local.sh +97 -0
  56. package/scripts/ci-local.test.sh +919 -0
  57. package/scripts/classify-finding.sh +32 -6
  58. package/scripts/classify-finding.test.sh +23 -0
  59. package/scripts/classify-test-run.sh +69 -0
  60. package/scripts/classify-test-run.test.sh +90 -0
  61. package/scripts/close-parent-c3.test.sh +2 -0
  62. package/scripts/consumer-sweep.sh +569 -0
  63. package/scripts/consumer-sweep.test.sh +357 -0
  64. package/scripts/detect-append-registries.sh +106 -0
  65. package/scripts/detect-append-registries.test.sh +64 -0
  66. package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
  67. package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
  68. package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
  69. package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
  70. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
  71. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
  72. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
  73. package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
  74. package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
  75. package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
  76. package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
  77. package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
  78. package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
  79. package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
  80. package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
  81. package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
  82. package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
  83. package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
  84. package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
  85. package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
  86. package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
  87. package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
  88. package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
  89. package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
  90. package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
  91. package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
  92. package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
  93. package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
  94. package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
  95. package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
  96. package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
  97. package/scripts/forge-root.test.sh +1 -1
  98. package/scripts/lib/telemetry.sh +1 -0
  99. package/scripts/remediate-approved-head.test.sh +2 -0
  100. package/scripts/remediate-basesync.test.sh +1 -1
  101. package/scripts/review-acceptance-gate.sh +2 -2
  102. package/scripts/review-acceptance-gate.test.sh +20 -0
  103. package/scripts/review-delta.sh +8 -1
  104. package/scripts/review-delta.test.sh +19 -0
  105. package/scripts/review-finalize.sh +193 -41
  106. package/scripts/review-finalize.test.sh +211 -4
  107. package/scripts/review-pr-delta-wiring.test.sh +13 -0
  108. package/scripts/review-prepare.sh +5 -3
  109. package/scripts/review-prepare.test.sh +18 -2
  110. package/scripts/run-shell-tests.sh +5 -0
  111. package/scripts/telemetry.test.sh +2 -0
  112. package/scripts/trusted-comments.test.sh +2 -1
  113. package/scripts/verify-phase-trail.test.sh +8 -0
  114. package/scripts/wait-ci-green.sh +61 -9
  115. package/scripts/wait-ci-green.test.sh +41 -3
  116. package/scripts/with-run-tmpdir.sh +20 -0
@@ -604,6 +604,39 @@ The block is delimited by `<!-- FORGE:BLAST_RADIUS:BEGIN -->` and `<!-- FORGE:BL
604
604
 
605
605
  ---
606
606
 
607
+ ## Phase A2.9: Append-Only Registry Check *(skip when no planned file is a hand-maintained list)* <!-- Added: forge#3969 -->
608
+
609
+ Two PRs appending at the same spot of a shared list (test paths, cron targets, workflow steps) conflict textually although every line is independent, and each conflict costs a base-sync remediation round. For each planned file that is an append-only registry (one independent entry per line, edited by many PRs):
610
+
611
+ 1. Confirm with `bash <scripts>/detect-append-registries.sh --detect` (history ranking) and `--check-union <path>`. Resolve `<scripts>` through the trusted script resolution (plugin root, `FORGE_ROOT`, `FORGEDOCK_HOME`), never `$PWD`. The helper rejects JSON, YAML, TOML, lockfiles and Markdown.
612
+ 2. Plan one of: (a) a `.gitattributes` `merge=union` entry, only when `--check-union` prints `UNION SAFE` and every line is independent (no ordering, uniqueness or structure); or (b) converting the list to glob discovery where its consumer supports it.
613
+ 3. Name the registry and the chosen option in `### Affected Paths`. Never plan union for a file with ordering, uniqueness or structure: that file is edited at the end of the list, and the pre-handoff base sync resolves its conflicts.
614
+
615
+ ---
616
+
617
+ ## Phase A2.10: Consumer Sweep Intake *(advisory: skip silently when no existing identifier changes; never blocks)* <!-- Added: forge#3970 -->
618
+
619
+ No diff exists yet, so sweep by name. List the existing identifiers the plan will change, rename or re-type (functions, classes, constants, config or context keys, CLI flags, env vars, paths, literal hashes or versions), take them from the investigation's affected files, and run the script in identifier mode. Validate (Phase V4.6) later re-runs the same sweep on the final diff and gates it.
620
+
621
+ ```bash
622
+ # FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => the sweep is skipped, never guessed)
623
+ type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
624
+ FORGE_ROOT="$(forge_root 2>/dev/null || true)"
625
+ CS_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/consumer-sweep.sh}"; [ -f "$CS_SCRIPT" ] || CS_SCRIPT=""
626
+ EXCLUDES=()
627
+ while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
628
+ < <(yq '(.build.scope.exclude_globs // []) + (.build.diff_size.exclude_globs // []) | .[]' forge.yaml 2>/dev/null || true)
629
+ if [ -z "$CS_SCRIPT" ]; then echo "CONSUMER_SWEEP: unavailable (script not found under FORGE_ROOT)"; else
630
+ bash "$CS_SCRIPT" --ident {IDENTIFIER_1} --ident {IDENTIFIER_2} ${EXCLUDES[@]+"${EXCLUDES[@]}"}; echo "CONSUMER_SWEEP_RC=$?"
631
+ fi
632
+ ```
633
+
634
+ Each identifier must match `^[A-Za-z0-9_.:/-]{4,80}$` (flags without leading dashes). Output is TSV `identifier<TAB>kind<TAB>file:line`; a `truncated` row means more sites exist than the cap shows (advisory). Exit `2` or an unavailable script is not "no sites": write "consumer sweep unavailable" in `### Risk Assessment` and continue; never block the plan.
635
+
636
+ Fold the result into Phase A2.8, with no new record grammar: each swept identifier becomes a `SYMBOL:` (`query` = the identifier) and each referencing file a `HIT:` (`disposition=change` when the plan edits it, else `verified-unaffected` with a reason after reading the site). The builder must update or justify each site (implement I6 `CONSUMERS:` block).
637
+
638
+ ---
639
+
607
640
  ## Phase A3: Consistency Rules
608
641
 
609
642
  Identify invariants that ALL affected paths must satisfy. These become the builder's consistency checklist.
@@ -636,6 +669,14 @@ Rules for ordering:
636
669
 
637
670
  ---
638
671
 
672
+ ## Phase A4.5: Edge-Case Enumeration *(skip on the skip-marker path; an empty block is valid)* <!-- Added: forge#3631 -->
673
+
674
+ List the error, edge and concurrency cases the change must survive, so validate (Phase V4.7, `scripts/check-edge-cases.sh`) can require a test for each. Cover every error class the code path can raise (not only the one in the report), boundary and empty inputs, and concurrent or repeated invocation. Skip cases that only restate a normal-path acceptance criterion.
675
+
676
+ Record one single-line record per case between `<!-- FORGE:EDGE_CASES:BEGIN -->` and `<!-- FORGE:EDGE_CASES:END -->`: `EDGE: id=e1 kind=error|edge|concurrency description=<free text>`. Ids are `e1`, `e2`, ... and are unique and stable. Only `id=` and `kind=` are parsed; the description is free text. Emit an empty block (or omit the section) when the change has no such cases. The builder maps each id to a test or an `untestable=` reason in FORGE:BUILDER (implement I6). Never use the architect marker strings as delimiters.
677
+
678
+ ---
679
+
639
680
  ## Phase A5: Risk Assessment
640
681
 
641
682
  For each non-obvious interaction or potential failure mode:
@@ -744,6 +785,16 @@ HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=change
744
785
  HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=verified-unaffected reason={one-hyphenated-token}
745
786
  <!-- FORGE:BLAST_RADIUS:END -->
746
787
 
788
+ ### Edge Cases
789
+ <!-- Phase A4.5. Human table plus the machine block. Omit when skipped; an empty block is valid. -->
790
+ | id | kind | description |
791
+ |----|------|-------------|
792
+ | e1 | {error\|edge\|concurrency} | {DESCRIPTION} |
793
+
794
+ <!-- FORGE:EDGE_CASES:BEGIN -->
795
+ EDGE: id=e1 kind={error|edge|concurrency} description={ONE_LINE_DESCRIPTION}
796
+ <!-- FORGE:EDGE_CASES:END -->
797
+
747
798
  ### Implementation Order
748
799
  1. {FIRST_CHANGE} — {WHY_FIRST}
749
800
  2. {SECOND_CHANGE} — {WHY_SECOND}
@@ -852,8 +903,10 @@ B4 → [THIS MODULE] Architecture Planning (FORGE:ARCHITECT comment)
852
903
  Phase A2.5: Pipeline Phase-Dependency Check
853
904
  Phase A2.7: Pattern Sweep Intake (review-finding issues only)
854
905
  Phase A2.8: Blast-Radius Manifest (enforced by validate V4.5, passed to review-pr)
906
+ Phase A2.10: Consumer Sweep Intake (consumer-sweep.sh --ident; gated by validate V4.6)
855
907
  Phase A3: Consistency Rules
856
908
  Phase A4: Sequence the Implementation
909
+ Phase A4.5: Edge-Case Enumeration (EDGE: ids; gated by validate V4.7)
857
910
  Phase A5: Risk Assessment
858
911
  B5 → Implement (builder reads plan, implements ALL affected paths)
859
912
  ```
@@ -42,6 +42,7 @@ Parse from $ARGUMENTS:
42
42
 
43
43
  ---
44
44
 
45
+ - `--fix-consumers "<UNADDRESSED lines>"` — optional. Set only by build B6 after validate returned `consumer_unaddressed`. Skip I1–I2 planning: for each listed `UNADDRESSED: <identifier> <file:line>` site, either update that file so it no longer depends on the old identifier (the file then joins the change set) or add a `- <path>[:<line>] unaffected: <reason>` line to the `### Consumers` block of the FORGE:BUILDER comment (the match is by path, split on the literal ` unaffected: `, so any path characters work). Never edit the checker (`scripts/consumer-sweep.sh`) or its stop-lists, and never add `unaffected:` lines without reading the site. Stage the changes, return `IMPLEMENT_RESULT: status: COMPLETE`; if a site cannot be addressed, leave it: build records it as `CONSUMERS: unresolved`. This is the single repair round; there is no second one. <!-- Added: forge#4017 -->
45
46
  - `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason. **Decomposition refusal** <!-- Added: forge#3543 -->: never implement work that the FORGE:INVESTIGATOR `### Decomposition Assessment` (`**YES**`) assigns to a separate sub-issue, even if a listed check targets it; return `status: BLOCKED` with blocker "check <id> belongs to decomposed item <N>" instead.
46
47
 
47
48
  ## Phase I1: Load Context from GitHub
@@ -173,6 +174,8 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
173
174
  - **Deliverable-type consistency check**: Before committing, compare the actual output against the CONTRACT's deliverable list. If the CONTRACT explicitly states "no code changes required", "docs only", or "configuration update only" AND the diff introduces new executable files (`.py`, `.js`, `.ts`, `.sh` — not test, config, or documentation files), STOP. Do NOT commit. Re-read the CONTRACT, the investigation recommendation, and the ARCHITECT plan. If all three agree that code is needed, update the CONTRACT comment to reflect the new deliverable type before proceeding. If only the builder decided to add code without contract support, discard the code change and implement the contracted deliverable instead. <!-- Added: forge#279 -->
174
175
  - **Pipeline check documentation — generalization rule**: When writing or updating pipeline check documentation (in `commands/*.md`), describe the **bug class**, not a specific incident. Do NOT embed: PR numbers, issue numbers, run IDs, timestamps, function names, dollar amounts, or multi-sentence incident timelines in check prose or `**Evidence**:` blocks. One brief HTML comment `<!-- Added: forge#NNN -->` is acceptable per check for traceability. `**Evidence**:` blocks must describe the vulnerability pattern (what the class of bug looks like, why it's dangerous) — not narrate a single historical occurrence. CHANGELOG entries may reference originating issues; command prompt text must not.
175
176
  - **Pattern sweep — class-wide fix and test**: When the FORGE:ARCHITECT plan or FORGE:INVESTIGATOR report contains a `### Pattern Sweep` table (`Query | Hit | Disposition`), fix EVERY hit with disposition `fix`, not only the instance the reviewer cited. Write the test against a representative set of instances from the table, not the reviewer's single repro, so it fails for any sibling still carrying the defect. Do not tune a rule, matcher, or fixture to the cited example. If a `fix` row cannot be changed, or a `not-affected` row turns out to be affected, say so in the `### Approach` of the FORGE:BUILDER comment instead of dropping it silently. Absent table = no-op. <!-- Added: forge#3449 -->
177
+ - **Consumer sweep — address every referencing site**: when the FORGE:ARCHITECT plan lists consumer-sweep sites (`HIT:` rows from `scripts/consumer-sweep.sh`, Phase A2.10), update each referencing file or justify it as unaffected. Validate (V4.6) re-runs the sweep on the final diff; in `blocking` mode it returns `consumer_unaddressed` for every referencing site outside the change set that has no justification, and build runs one repair round (`--fix-consumers`). Sites still unaddressed after that round are recorded as `CONSUMERS: unresolved` and passed to review as known risk; they never block. Record the justifications in the `CONSUMERS:` block of the FORGE:BUILDER comment (Phase I6). <!-- Added: forge#3970 -->
178
+ - **Edge cases — one test per id**: when the FORGE:ARCHITECT plan carries a `FORGE:EDGE_CASES` block (`### Edge Cases`, Phase A4.5), write one test per `EDGE:` id (error, edge and concurrency cases, not only the happy path) and keep the test in the diff's added lines. Record each id in the `### Edge Case Coverage` section of the FORGE:BUILDER comment (Phase I6). Validate (V4.7) fails with `edge-cases: <ids>` for an id with no `EDGE_TEST:` line, a mapped test name absent from the added lines of its file, or an empty `untestable=` reason. Use `untestable=<reason>` only when no test can exercise the case, and give the real reason. An empty or absent block is a no-op. <!-- Added: forge#3631 -->
176
179
  - **Endpoint response contract consumer tracing**: When changing an endpoint's response body shape or status field values (e.g., changing `"status": "healthy"` to `"status": "ok"`, renaming response keys, removing fields), grep the full repo for ALL consumers of that response body before committing. Consumers are not limited to the service being changed — they include deploy scripts, CI health checks, monitoring configs, docker-compose healthcheck definitions, Traefik probes, and any script that parses or pattern-matches on the response body. Run: `grep -rn "{old_value}\|{endpoint_path}" scripts/ infra/ .github/ docker-compose*.yml traefik/ 2>/dev/null`. All consumers whose behavior depends on the old response format MUST be updated in the same PR — a response contract change that updates only one consumer while leaving others on the old format is a deploy-time breakage. <!-- Added: forge#321 -->
177
180
  - If you discover the contract is wrong (e.g. a file doesn't exist, a function has a different signature): STOP, post a comment on the issue explaining the discrepancy, add label `needs-human`, and EXIT with `IMPLEMENT_RESULT: status: BLOCKED` and a `blocker` describing the discrepancy
178
181
 
@@ -355,6 +358,22 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
355
358
  ### Acceptance Criteria Status
356
359
  {Checklist of each criterion from the contract, marked ✅ or ❌}
357
360
 
361
+ ### Consumers
362
+ CONSUMERS:
363
+ - {path}[:{line}] unaffected: {one-line reason}
364
+ - {path} updated
365
+
366
+ CONSUMERS: unresolved
367
+ - {path}:{line} unresolved: {identifier}
368
+
369
+ > **Consumers block**: one line per referencing site of a changed or removed identifier (the architect's A2.10 sweep and the V4.6 `UNADDRESSED:` output). Only `unaffected: <reason>` lines justify a site outside the change set; the match is by file path (the line number is optional, lines shift). A file you changed needs no line. `unresolved` lines are written by validate V5 for sites left unaddressed after the one repair round (a recorded known risk for review): they never justify a site and you do not write them by hand. Omit the block when there are no sites.
370
+
371
+ ### Edge Case Coverage
372
+ EDGE_TEST: id=e1 test={path}::{test_name}
373
+ EDGE_TEST: id=e2 untestable={reason}
374
+
375
+ > **Edge case block**: one line per `EDGE:` id of the architect's `FORGE:EDGE_CASES` block, each line starting at the line start (no prose before `EDGE_TEST:`). `test=` names a file and a test name (a single token with no whitespace; name the test with underscores or hyphens) that occurs in a line this change adds to that file; a pre-existing test does not count. `untestable=` needs a non-empty reason. Omit the section when the plan has no edge cases.
376
+
358
377
  ### Testing Checklist
359
378
  - [ ] {Test scenario 1} [type:api]
360
379
  - [ ] {Test scenario 2} [type:unit]