From c3f543ecbe04da6c9fcd2d9d17a8497e0b9ab8c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:08:14 +0000 Subject: [PATCH 1/3] Trim trailing whitespace and cap blank runs in inlined prompt content The unified prompt creation step embeds markdown prompt content inside a `run: |` heredoc, which is a YAML block scalar. normalizeBlankLines deliberately preserves block-scalar payload verbatim (arbitrary scalars can carry semantically significant trailing whitespace and blank runs, e.g. shell line continuations), so trailing whitespace on prompt content lines and long blank runs survived into inlined lock files as yamllint trailing-spaces / empty-lines noise. Prompt content is markdown text the compiler owns, where trailing whitespace is never meaningful and long blank runs are just noise, so it is now cleaned at emission: trailing whitespace is trimmed per line and consecutive blank lines are capped at maxConsecutiveBlankLines, tracked across chunk boundaries. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../smoke-copilot.golden | 4 +-- pkg/workflow/unified_prompt_step.go | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden index 3adee4f9cee..dbde9edcdd4 100644 --- a/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden +++ b/pkg/workflow/testdata/TestWasmGolden_CompileFixtures/smoke-copilot.golden @@ -313,13 +313,13 @@ jobs: 1. **GitHub MCP Testing**: Review the last 2 merged pull requests in __GH_AW_GITHUB_REPOSITORY__ 2. **MCP Scripts GH CLI Testing**: Use the `mcpscripts-gh` tool to query 2 pull requests from __GH_AW_GITHUB_REPOSITORY__ (use args: "pr list --repo __GH_AW_GITHUB_REPOSITORY__ --limit 2 --json number,title,author") - 3. **Serena MCP Testing**: + 3. **Serena MCP Testing**: - Use the Serena MCP server tool `activate_project` to initialize the workspace at `__GH_AW_GITHUB_WORKSPACE__` and verify it succeeds (do NOT use bash to run go commands - use Serena's MCP tools) - After initialization, use the `find_symbol` tool to search for symbols (find which tool to call) and verify that at least 3 symbols are found in the results 4. **Playwright Testing**: Use the playwright tools to navigate to and verify the page title contains "GitHub" (do NOT try to install playwright - use the provided MCP tools) 5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) - 7. **Discussion Interaction Testing**: + 7. **Discussion Interaction Testing**: - Use the `github-discussion-query` mcp-script tool with params: `limit=1, jq=".[0]"` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) - Use the `add_comment` tool with `discussion_number: ` to add a fun, playful comment stating that the smoke test agent was here diff --git a/pkg/workflow/unified_prompt_step.go b/pkg/workflow/unified_prompt_step.go index 0e9b79cf695..d33d128d10b 100644 --- a/pkg/workflow/unified_prompt_step.go +++ b/pkg/workflow/unified_prompt_step.go @@ -457,6 +457,16 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil // 2. Write user prompt chunks (appended after built-in sections). // All chunks are written into the same heredoc block (opened above or here) // to minimise the number of delimiter lines in the compiled lock file. + // + // The heredoc payload is a YAML block scalar, so normalizeBlankLines preserves + // it verbatim (it must, since arbitrary block scalars can carry semantically + // significant trailing whitespace and blank runs). Prompt content is markdown + // text the compiler owns, where trailing whitespace is never meaningful and + // long blank runs are noise, so it is cleaned here instead: trailing whitespace + // is trimmed from every line and consecutive blank lines are capped at + // maxConsecutiveBlankLines. userBlankRun is tracked across chunks so a run that + // straddles a chunk boundary is still collapsed. + userBlankRun := 0 for chunkIdx, chunk := range userPromptChunks { unifiedPromptLog.Printf("Writing user prompt chunk %d/%d", chunkIdx+1, len(userPromptChunks)) @@ -472,6 +482,7 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil inHeredoc = true } yaml.WriteString(" " + chunk + "\n") + userBlankRun = 0 continue } @@ -483,8 +494,20 @@ func (c *Compiler) generateUnifiedPromptCreationStep(yaml *strings.Builder, buil lines := strings.SplitSeq(chunk, "\n") for line := range lines { + trimmed := strings.TrimRight(line, " \t") + if trimmed == "" { + // Collapse over-long blank runs; emit truly empty lines (no + // indentation) so they carry no trailing whitespace. + if userBlankRun >= maxConsecutiveBlankLines { + continue + } + userBlankRun++ + yaml.WriteByte('\n') + continue + } + userBlankRun = 0 yaml.WriteString(" ") - yaml.WriteString(line) + yaml.WriteString(trimmed) yaml.WriteByte('\n') } } From 33f19cfd43448e063c9115c52f56b90574e9882f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:27:32 +0000 Subject: [PATCH 2/3] Plan: add unit tests for blank-run cap and trailing whitespace trimming Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/release.lock.yml | 3 ++- .github/workflows/smoke-claude.lock.yml | 11 ++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 9840a38941a..b670dfbb32e 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,6 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d6e82aec6fea9061e1ef1cbb085324e6ed6c079230d70bbbb96611bb18cf7cf","body_hash":"646353d7bb4e5523bc85349c2cce38188190095a303f83cc95961ff145a47043","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index bbd7f950502..5d3e121099f 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -436,9 +436,6 @@ jobs: ``` - - - # Smoke Test: Claude Engine Validation. **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** @@ -454,11 +451,11 @@ jobs: 5. **Tavily Web Search Testing**: Use the Tavily MCP server to perform a web search for "GitHub Agentic Workflows" and verify that results are returned with at least one item 6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) 7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) - 8. **Discussion Interaction Testing**: + 8. **Discussion Interaction Testing**: - Use `gh api repos/__GH_AW_GITHUB_REPOSITORY__/discussions?per_page=1` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) - Use the `add_comment` tool with `discussion_number: ` to add a fun, comic-book style comment stating that the smoke test agent was here - 9. **Agentic Workflows MCP Testing**: + 9. **Agentic Workflows MCP Testing**: - Call the `agentic-workflows` MCP tool using the `status` method with workflow name `smoke-claude` to query workflow status - If the tool returns an error or no results, mark this test as ❌ and note "Tool unavailable or workflow not found" but continue to the Output section - If the tool succeeds, extract key information from the response: total runs, success/failure counts, last run timestamp @@ -496,7 +493,7 @@ jobs: - Note: This will bundle all review comments from test #14 - After submitting, use the GitHub MCP tool to list review threads on the PR and note the thread IDs from review comments you created in test #14 — these will be used in test #16 - 16. **Resolve Review Thread Testing**: + 16. **Resolve Review Thread Testing**: - Use the GitHub MCP tool to list review threads on the PR and filter for threads that are **not yet resolved** (`isResolved: false`) - Prefer resolving a thread created by your own review comments from test #14 in this run - Use `thread_id: ""` from one of those unresolved threads @@ -508,7 +505,7 @@ jobs: - Verify the tool call succeeds - Note: May fail if reviewer is already assigned or doesn't have access - 18. **Push to PR Branch Testing**: + 18. **Push to PR Branch Testing**: - Create a test file at `smoke-test-files/smoke-claude-push-test.md` in the repository workspace with content "Smoke test push — Run __GH_AW_GITHUB_RUN_ID__" - Use the `push_to_pull_request_branch` tool to push this change - Use `pr_number: ` and `commit_message: "test: Add smoke test file"` From 75c338ecc1a89962599f6e5a8daee505c1c4c577 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:33:12 +0000 Subject: [PATCH 3/3] Add unit tests for blank-run cap and trailing whitespace trimming in user prompt chunks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/unified_prompt_step_test.go | 112 +++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/pkg/workflow/unified_prompt_step_test.go b/pkg/workflow/unified_prompt_step_test.go index a85c7228cf1..d950ba4b674 100644 --- a/pkg/workflow/unified_prompt_step_test.go +++ b/pkg/workflow/unified_prompt_step_test.go @@ -606,3 +606,115 @@ func TestCollectPromptSections_PRCommentPushToPRBranchGuidance(t *testing.T) { } }) } + +// TestGenerateUnifiedPromptCreationStep_TrailingWhitespace verifies that trailing +// whitespace is stripped from every user-prompt content line. +func TestGenerateUnifiedPromptCreationStep_TrailingWhitespace(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // Chunk lines with assorted trailing whitespace + userPromptChunks := []string{"line one \nline two\t\nline three \t "} + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + assert.NotContains(t, output, "line one ", "trailing spaces should be stripped") + assert.NotContains(t, output, "line two\t", "trailing tab should be stripped") + assert.NotContains(t, output, "line three \t ", "mixed trailing whitespace should be stripped") + assert.Contains(t, output, " line one\n", "content should be present with indentation") + assert.Contains(t, output, " line two\n", "content should be present with indentation") + assert.Contains(t, output, " line three\n", "content should be present with indentation") +} + +// TestGenerateUnifiedPromptCreationStep_BlankRunCapWithinChunk verifies that a run of +// more than maxConsecutiveBlankLines blank lines inside a single chunk is capped. +func TestGenerateUnifiedPromptCreationStep_BlankRunCapWithinChunk(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // Four consecutive blank lines between content lines – should be collapsed to two. + userPromptChunks := []string{"# Start\n\n\n\n\n# End"} + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + assert.Contains(t, output, "# Start", "start marker should be present") + assert.Contains(t, output, "# End", "end marker should be present") + + // At most maxConsecutiveBlankLines (2) blank lines should appear between them. + startIdx := strings.Index(output, "# Start") + endIdx := strings.Index(output, "# End") + require.Less(t, startIdx, endIdx) + between := output[startIdx:endIdx] + // Three or more consecutive newlines would indicate a blank run of >2. + assert.NotContains(t, between, "\n\n\n\n", "blank run should be capped at 2") +} + +// TestGenerateUnifiedPromptCreationStep_BlankRunCapAcrossChunks verifies that a blank +// run straddling two adjacent chunks is still capped correctly. +func TestGenerateUnifiedPromptCreationStep_BlankRunCapAcrossChunks(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // chunk1 ends with two blank lines; chunk2 starts with two blank lines. + // Across the boundary that would be four consecutive blanks; they should be collapsed. + userPromptChunks := []string{ + "# Chunk1\n\n\n", + "\n\n# Chunk2", + } + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + startIdx := strings.Index(output, "# Chunk1") + endIdx := strings.Index(output, "# Chunk2") + require.Less(t, startIdx, endIdx) + between := output[startIdx:endIdx] + assert.NotContains(t, between, "\n\n\n\n", "blank run across chunk boundary should be capped at 2") +} + +// TestGenerateUnifiedPromptCreationStep_BlankRunCapAdjacentToRuntimeImport verifies +// that blank-run tracking resets correctly around a runtime-import macro so blank lines +// on both sides of the macro are handled independently. +func TestGenerateUnifiedPromptCreationStep_BlankRunCapAdjacentToRuntimeImport(t *testing.T) { + compiler := &Compiler{} + data := &WorkflowData{ + ParsedTools: NewTools(map[string]any{}), + } + builtinSections := compiler.collectPromptSections(data) + + // chunk[0] ends with a long blank run, chunk[1] is a runtime-import macro, + // chunk[2] starts with a long blank run. + userPromptChunks := []string{ + "# Before\n\n\n\n", + "{{#runtime-import docs/task.md}}", + "\n\n\n\n# After", + } + + var sb strings.Builder + compiler.generateUnifiedPromptCreationStep(&sb, builtinSections, userPromptChunks, nil, data) + output := sb.String() + + // Runtime-import macro must be present verbatim. + assert.Contains(t, output, "{{#runtime-import docs/task.md}}", "runtime-import macro must be emitted verbatim") + + // Content markers must appear. + assert.Contains(t, output, "# Before", "before-marker should be present") + assert.Contains(t, output, "# After", "after-marker should be present") + + // No run of four or more newlines (i.e., 3+ consecutive blank lines) anywhere. + assert.NotContains(t, output, "\n\n\n\n", "blank run should be capped throughout the output") +}