Skip to content

fix(runtime_import): resolve /plan context substitution failures for slash-command runs#31849

Merged
pelikhan merged 7 commits into
mainfrom
copilot/review-compiled-lock-file
May 13, 2026
Merged

fix(runtime_import): resolve /plan context substitution failures for slash-command runs#31849
pelikhan merged 7 commits into
mainfrom
copilot/review-compiled-lock-file

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 13, 2026

Bug Fix

What was the bug?

Audited plan.lock.yml (lock file looks correct) and the last run (ID 25774562076). The activation job flagged 6 unresolved ${{ }} expressionsgithub.event.issue.number and github.event.discussion.number appeared literally in the prompt body, and both the issue and discussion conditional blocks were always included simultaneously.

Two distinct bugs in runtime_import.cjs:

Bug 1 — expressions not resolved in body text

evaluateExpression only checks context.payload.issue.number, which is undefined for workflow_dispatch (how slash commands execute). No fallback existed for the aw_context input.

Bug 2 — {{#if github.event.issue.number}} always truthy

wrapExpressionsInTemplateConditionals wrapped conditions as {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}. isTruthy() treated the non-empty placeholder string as truthy. substitute_placeholders.cjs runs after template rendering, so the substitution was already too late.

How did you fix it?

evaluateExpression — adds an aw_context fallback: when context.payload.issue is absent, parses inputs.aw_context JSON and resolves the correct field by item_type:

// aw_context shape set by slash-command dispatcher:
// { item_type: "issue" | "discussion" | "pull_request", item_number: 123, comment_id?: 456 }
const fieldResolvers = {
  "github.event.issue.number": () =>
    awCtx.item_type === "issue" && awCtx.item_number ? String(awCtx.item_number) : undefined,
  "github.event.discussion.number": () =>
    awCtx.item_type === "discussion" && awCtx.item_number ? String(awCtx.item_number) : undefined,
  // ...
};

wrapExpressionsInTemplateConditionals — instead of wrapping in ${{ }} to produce an always-truthy placeholder, now calls evaluateExpression() inline. The template renderer receives the actual value ("123" → truthy, "" → falsy):

// Before: {{#if github.event.issue.number}} → {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} (always truthy)
// After:  {{#if github.event.issue.number}} → {{#if 123 }} or {{#if }} (correct truthy/falsy)
const evaluated = evaluateExpression(trimmed);
return evaluated.startsWith("${{") ? `{{#if }}` : `{{#if ${evaluated} }}`;

Testing

  • Updated wrapExpressionsInTemplateConditionals unit tests to assert inline-evaluated output instead of ${{ }} wrapping
  • Added 8 new evaluateExpression tests covering aw_context resolution: correct item_type, mismatched item_type, absent context, and invalid JSON

Copilot AI and others added 3 commits May 13, 2026 02:52
…ion findings

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title fix: resolve /plan workflow context substitution bugs found in lock file audit fix(runtime_import): resolve /plan context substitution failures for slash-command runs May 13, 2026
Copilot AI requested a review from pelikhan May 13, 2026 03:04
@pelikhan pelikhan marked this pull request as ready for review May 13, 2026 03:33
Copilot AI review requested due to automatic review settings May 13, 2026 03:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes runtime-import prompt rendering for slash-command (workflow_dispatch) runs by ensuring GitHub event fields (issue/discussion/PR numbers, comment id) resolve correctly and {{#if ...}} blocks don’t become erroneously truthy due to placeholder strings.

Changes:

  • Add aw_context fallback resolution in evaluateExpression() for github.event.* fields when the event payload lacks issue/discussion/PR objects.
  • Change wrapExpressionsInTemplateConditionals() to inline-evaluate conditions instead of emitting always-truthy placeholder strings.
  • Update/add unit tests to cover aw_context resolution and the new conditional-wrapping behavior.
Show a summary per file
File Description
go.mod Promotes gopkg.in/yaml.v3 from indirect to direct dependency.
actions/setup/js/runtime_import.cjs Implements aw_context fallback in expression evaluation; changes conditional wrapping to inline evaluation.
actions/setup/js/runtime_import.test.cjs Adds/updates tests for aw_context expression resolution and updated conditional wrapping output.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment thread actions/setup/js/runtime_import.cjs Outdated
Comment on lines 826 to 836
@@ -803,8 +835,21 @@ function wrapExpressionsInTemplateConditionals(content) {
return match;
}
Comment thread actions/setup/js/runtime_import.cjs Outdated
Comment on lines +838 to +852
// Evaluate the condition inline so that the template renderer (renderMarkdownTemplate /
// isTruthy) receives the actual runtime value rather than an __GH_AW__ placeholder string.
//
// If we had instead produced `{{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}`, the
// placeholder literal (a non-empty string) would always be truthy when evaluated by
// isTruthy() because substitute_placeholders.cjs runs *after* template rendering.
// Evaluating inline here ensures:
// • a known number → truthy (block kept when issue is present)
// • "" or undefined → falsy (block removed when there is no issue/discussion)
const evaluated = evaluateExpression(trimmed);
if (evaluated.startsWith("${{")) {
// Expression could not be resolved — treat as falsy (empty condition)
return `{{#if }}`;
}
return `{{#if ${evaluated} }}`;
@github-actions github-actions Bot mentioned this pull request May 13, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnose and /tdd — this is a targeted bug fix with accompanying tests, which maps squarely to those two skills.

Key Themes

  • Observability gap: When wrapExpressionsInTemplateConditionals cannot resolve an expression it now silently produces a falsy {{#if }} block. This is correct for the reported bug, but future regressions will be invisible in logs. A core.warning at that branch would make them immediately visible. ([/diagnose])
  • Test state hygiene: The new aw_context fallback describe block mutates global.context.payload.inputs without an afterEach cleanup, which can cause order-dependent test failures. ([/tdd])
  • Inconsistent test setup: The "multiple conditionals" test uses an inline try/finally while the surrounding suite uses beforeEach/afterEach. ([/tdd])
  • Static object in hot path: fieldResolvers is rebuilt on every evaluateExpression call; it should be a module-level constant. ([/diagnose])
  • Unrelated go.mod change: gopkg.in/yaml.v3 is promoted from indirect to direct without explanation; project conventions prefer go.yaml.in/yaml/v3 for direct dependencies. ([/diagnose])

Positive Highlights

  • ✅ Root cause is well-diagnosed and clearly explained in the PR description — both bugs documented with before/after reasoning
  • ✅ The aw_context fallback correctly guards against invalid JSON with a silent catch-and-continue
  • ✅ 8 new tests cover the happy path, mismatched item_type, absent context, and invalid JSON — solid coverage of the new code path
  • ✅ The inline-evaluation approach in wrapExpressionsInTemplateConditionals is architecturally cleaner than the old placeholder-based design

Verdict

No blocking issues — the fix is correct and well-tested. The comments above are improvements worth addressing before merge, with the test state leakage being the highest priority.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 6.2M

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 83/100

Excellent

Metric Value
New/modified tests analyzed 18
✅ Design tests (behavioral contracts) 18 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 14 (78%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes (114 test lines / 48 prod lines = 2.38×)
🚨 Coding-guideline violations None

Test Classification Details

View all 18 test classifications
Test File Classification Notes
should resolve github.event.issue.number from aw_context when item_type is issue runtime_import.test.cjs ✅ Design Verifies observable output for issue type
should return unresolved for github.event.issue.number when aw_context item_type is discussion runtime_import.test.cjs ✅ Design Edge case: wrong item_type produces unresolved
should resolve github.event.discussion.number from aw_context when item_type is discussion runtime_import.test.cjs ✅ Design Verifies correct field mapping for discussion
should return unresolved for github.event.discussion.number when aw_context item_type is issue runtime_import.test.cjs ✅ Design Edge case: type mismatch produces unresolved
should resolve github.event.pull_request.number from aw_context when item_type is pull_request runtime_import.test.cjs ✅ Design Verifies PR item_type handling
should resolve github.event.comment.id from aw_context when comment_id is present runtime_import.test.cjs ✅ Design Verifies optional comment_id field
should return unresolved for github.event.issue.number when aw_context is absent runtime_import.test.cjs ✅ Design Edge case: missing aw_context
should return unresolved for github.event.issue.number when aw_context is invalid JSON runtime_import.test.cjs ✅ Design Error path: malformed JSON handled gracefully
should produce {{#if }} (falsy) for github.actor when context is unavailable runtime_import.test.cjs ✅ Design Edge case: no runtime context
should produce {{#if }} (falsy) for needs.activation.outputs.text when env var is absent runtime_import.test.cjs ✅ Design Edge case: missing env var
should produce {{#if }} (falsy) for steps.foo.outputs.bar when env var is absent runtime_import.test.cjs ✅ Design Edge case: missing step output
should inline evaluate {{#if github.actor}} to the actor value runtime_import.test.cjs ✅ Design Behavioral contract: actor resolved from context
should produce {{#if }} (falsy) for github.event.issue.number when event has no issue runtime_import.test.cjs ✅ Design Edge case: event without issue payload
should inline evaluate github.event.issue.number from aw_context when item_type is issue runtime_import.test.cjs ✅ Design E2E: aw_context → wrapExpressions → output
should produce {{#if }} (falsy) for github.event.issue.number when aw_context item_type is discussion runtime_import.test.cjs ✅ Design Edge case: cross-type guard
should inline evaluate github.event.discussion.number from aw_context when item_type is discussion runtime_import.test.cjs ✅ Design E2E: discussion type resolution
should handle multiple conditionals in one string (modified) runtime_import.test.cjs ✅ Design Updated to assert evaluated value, not placeholder
should inline evaluate {{#if github.actor}} to the actor value when context is set (modified) runtime_import.test.cjs ✅ Design Replaces placeholder assertion with behavioral one

Test Inflation Note

⚠️ The test file gained 114 lines vs 48 lines in the production file (ratio: 2.38×, threshold: 2×). This triggers the inflation penalty (−10 pts). However, the additional test lines are justified: the new aw_context feature introduces 8 type-dispatch scenarios (issue / discussion / pull_request / comment / absent / invalid JSON) that inherently require more test cases than production code lines.

No action required — this is a well-motivated case of intentional scenario coverage.


Language Support

Tests analyzed:

  • 🟨 JavaScript (*.test.cjs): 18 tests (vitest)
  • 🐹 Go (*_test.go): 0 tests changed

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 18 new/modified tests verify observable behavioral contracts. The aw_context fallback for slash-command event fields is thoroughly covered across all item types, error paths (absent context, invalid JSON), and cross-type guards.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §25776603690

🧪 Test quality analysis by Test Quality Sentinel · ● 5.7M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 83/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All 18 new/modified tests verify observable behavioral contracts of the aw_context slash-command resolution feature.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

Copilot AI and others added 2 commits May 13, 2026 04:19
…apExpressionsInTemplateConditionals

- Only process expressions whose root matches a known GitHub Actions
  namespace (github., needs., steps., env., inputs.) to prevent
  non-GH dotted identifiers like experiments.foo from being collapsed
  to falsy {{#if }} (addresses review comment on lines 826-836)
- Emit {{#if true}} / {{#if }} boolean sentinels instead of inlining
  the raw resolved value to prevent template tag injection via values
  containing '}}' (addresses review comment on lines 838-852)
- Import isTruthy from is_truthy.cjs to correctly classify resolved
  values (handles edge cases like "false", "0", "null")
- Update tests to expect boolean sentinels and add experiments.foo
  regression cases

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 13, 2026

@copilot review all comments

Both review comments are addressed in the latest commits:

Comment 1 (lines 826-836) — experiments.foo broken by overly-broad match:
Removed the (/^[a-zA-Z]/.test(trimmed) && trimmed.includes(".")) clause from looksLikeGitHubExpr. The check now exclusively matches the five known GitHub Actions roots (github., needs., steps., env., inputs.). Any other dotted identifier (e.g. experiments.foo, experiments.prompt_style == "concise") is left unchanged so later stages can handle it.

Comment 2 (lines 838-852) — raw value injection via }}:
Instead of inlining the raw resolved value into {{#if ${evaluated} }}, the function now emits the safe boolean sentinels {{#if true}} (truthy) or {{#if }} (falsy). Truthiness is determined using the existing isTruthy() from is_truthy.cjs, which correctly handles edge cases like "false", "0", and "null".

Two regression tests for experiments.foo and experiments.prompt_style == "concise" were added to the non-GitHub expressions suite, and existing "with context" tests were updated to expect the boolean sentinels.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot lint js, fix wasm

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 13, 2026

@copilot lint js, fix wasm

Fixed in the latest commit:

  • JS lint: ran npm run format:cjsruntime_import.cjs and runtime_import.test.cjs are now formatted correctly (npm run lint:cjs passes clean).
  • WASM golden files: ran make update-wasm-golden to regenerate the four .golden files under pkg/workflow/testdata/TestWasmGolden_CompileFixtures/. The hash names changed because the expression strings now include github.event.client_payload.aw_context || (from a prior commit). All TestWasmGolden_* tests pass.

@pelikhan pelikhan merged commit 7398cdd into main May 13, 2026
23 of 24 checks passed
@pelikhan pelikhan deleted the copilot/review-compiled-lock-file branch May 13, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants