refactor(coding-agent): removed head/tail params and bash-normalize module
- Deleted `bash-normalize.ts` and its tests; output truncation is handled by the streaming tail buffer and artifact spillover. - Removed `head`/`tail` schema fields from `bashSchema` and `BashToolInput`. - Updated system prompt and bash tool prompt to forbid `| head`/`| tail` pipes and other anti-patterns, directing the agent to use dedicated tools instead.
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Anthropic provider now retries generic transient connect failures (`unable to connect`, `fetch failed`, `connection error`, etc.) by falling back to the shared `isRetryableError` allowlist after the provider-specific patterns. Previously these errors bypassed the hand-curated regex in `isProviderRetryableError` and aborted the stream on the first attempt, while the OpenAI SDK and Codex `fetchWithRetry` paths already handled them.
|
||||
|
||||
## [14.9.0] - 2026-05-10
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed the `sectionSeparator` re-export from `config/prompt-templates`, so existing imports from `@oh-my-pi/pi-coding-agent/config/prompt-templates` now need to resolve `sectionSeparator` from its utility package
|
||||
@@ -17,6 +16,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the system prompt’s Bash usage guidance to explicitly forbid specific anti-patterns (`sed`/`awk` line-range reads, stderr redirects, and `| head|tail` pagination) and require using dedicated tools for those operations
|
||||
- Changed delegated subagent prompts so shared task context is now rendered only in the system-level `[context]` block, while the user-facing task message now contains only the assignment prompt text
|
||||
- Changed system prompt rendering to use block markers such as `[env]`, `[contract]`, `[role]`, `[coop]`, and `[closure]` for more explicit structural instructions
|
||||
- Changed the working-directory value in rendered prompts to use `shortenPath` before interpolation
|
||||
@@ -26,6 +26,10 @@
|
||||
- Changed `ast_edit` and `find`/`search` rendering to show resolved path values and option flags such as `limit`, `no-hidden`, and `no-reply`
|
||||
- Changed power assertion behavior to take effect only while a prompt is in flight, replacing session-level persistent assertions
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the unused `head` and `tail` parameters from the `bash` tool schema, along with the dead `normalizeBashCommand` / `applyHeadTail` post-processing module — output truncation is already handled by the harness's streaming tail buffer and artifact spillover, so the agent should rely on `read` (or the artifact link) instead of inline truncation pipes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed subagent task prompt construction so a trailing `[now]` block in the base prompts is preserved and not swallowed when rendering `subagent-system-prompt`
|
||||
|
||||
@@ -803,10 +803,8 @@ This subsystem is split into two layers:
|
||||
#### Bash tool (`src/tools/bash.ts`)
|
||||
|
||||
- Adapter class: `BashTool implements AgentTool<typeof bashSchema, BashToolDetails>`.
|
||||
- Defines tool contract (`bashSchema`: `command`, `timeout`, `cwd`, `head`, `tail`) and prompt text import (`../prompts/tools/bash.md`).
|
||||
- Defines tool contract (`bashSchema`: `command`, `timeout`, `cwd`, `pty`, optional `async`) and prompt text import (`../prompts/tools/bash.md`).
|
||||
- Pre-execution adaptation:
|
||||
- strips inline shell truncation patterns (`normalizeBashCommand`)
|
||||
- applies explicit/derived head-tail params
|
||||
- optional command interception (`checkBashInterception`) based on settings
|
||||
- expands internal URLs (`expandInternalUrls`)
|
||||
- resolves/validates working directory (`resolveToCwd`, `fs.promises.stat`).
|
||||
@@ -815,7 +813,6 @@ This subsystem is split into two layers:
|
||||
- non-PTY path: `executeBash(...)`.
|
||||
- Streaming to UI is adapter-owned: updates `onUpdate` using `createTailBuffer(...)` while backend runs.
|
||||
- Post-execution shaping is adapter-owned:
|
||||
- applies `applyHeadTail(...)`
|
||||
- converts backend cancellation/timeout/exit status into `ToolAbortError` / `ToolError`
|
||||
- returns `toolResult(...).text(...).truncationFromSummary(...)`.
|
||||
|
||||
|
||||
@@ -179,7 +179,11 @@ You **MUST NOT** blindly use coreutils through bash / general-purpose tools when
|
||||
{{#has tools "search"}}- You **MUST** use `{{toolRefs.search}}`, not shell regex search.{{/has}}
|
||||
{{#has tools "find"}}- You **MUST** use `{{toolRefs.find}}`, not shell file globbing.{{/has}}
|
||||
{{#has tools "eval"}}- Then, you **MAY** use `{{toolRefs.eval}}` for quick compute, but you **SHOULD** go step by step.{{/has}}
|
||||
{{#has tools "bash"}}- Finally, you **MAY** use `{{toolRefs.bash}}` for simple one-liners only. But this is a last resort.{{/has}}
|
||||
{{#has tools "bash"}}- Finally, you **MAY** use `{{toolRefs.bash}}` for simple one-liners only. But this is a last resort. Bash commands matching the patterns above are intercepted and blocked at runtime.
|
||||
- You **MUST NOT** read line ranges with `sed -n 'A,Bp'`, `awk 'NR≥A && NR≤B'`, or `head | tail` pipelines. Use `{{toolRefs.read}}` with `offset`/`limit`.
|
||||
- You **MUST NOT** use `2>&1` or `2>/dev/null` — stdout and stderr are already merged.
|
||||
- You **MUST NOT** suffix commands with `| head -n N` or `| tail -n N` — the harness already streams output and returns a truncated view, with the full result available via `artifact://<id>`.
|
||||
- If you catch yourself typing `cat`, `head`, `tail`, `less`, `more`, `ls`, `grep`, `rg`, `find`, `fd`, `sed -i`, `awk -i`, or a heredoc redirect inside a Bash call, stop and switch to the dedicated tool.{{/has}}
|
||||
{{#has tools "report_tool_issue"}}
|
||||
<critical>
|
||||
The `{{toolRefs.report_tool_issue}}` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `{{toolRefs.report_tool_issue}}` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.
|
||||
|
||||
@@ -27,27 +27,3 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
||||
- Truncated output is retrievable from `artifact://<id>` (linked in metadata)
|
||||
- Exit codes shown on non-zero exit
|
||||
</output>
|
||||
|
||||
<critical>
|
||||
- Use specialized tools instead of bash for any file, directory, or text-search operation. Do NOT use Bash when a dedicated tool exists — dedicated tools are faster, render diffs, respect `.gitignore`, and let the user review your work. Bash commands matching the patterns below are intercepted and blocked at runtime.
|
||||
|
||||
|Instead of (WRONG)|Use (CORRECT)|
|
||||
|---|---|
|
||||
|`cat file`, `head -n N file`|`read(path="file", limit=N)`|
|
||||
|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`|
|
||||
{{#if hasSearch}}|`grep -A 20 'pat' file`|`search(pattern="pat", path="file", post=20)`|
|
||||
|`grep -rn 'pat' dir/`|`search(pattern="pat", path="dir/")`|
|
||||
|`rg 'pattern' dir/`|`search(pattern="pattern", path="dir/")`|{{/if}}
|
||||
{{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}}
|
||||
|`ls dir/`|`read(path="dir/")`|
|
||||
|`cat <<'EOF' > file`|`write(path="file", content="…")`|
|
||||
|`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`|
|
||||
{{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}}
|
||||
- You **MUST NOT** create files with `cat <<EOF`, `echo > file`, or `printf > file`. Use `write`.
|
||||
- You **MUST NOT** read line ranges with `sed -n 'A,Bp'`, `awk 'NR≥A && NR≤B'`, or `head | tail` pipelines. Use `read` with `offset`/`limit` (or `sel` if available).
|
||||
{{#if hasAstGrep}}- You **MUST** use `ast_grep` for structural code search instead of bash `grep`/`awk`/`perl` pipelines{{/if}}
|
||||
{{#if hasAstEdit}}- You **MUST** use `ast_edit` for structural rewrites instead of bash `sed`/`awk`/`perl` pipelines{{/if}}
|
||||
- You **MUST NOT** use `2>&1` or `2>/dev/null` — stdout and stderr are already merged
|
||||
- You **MUST NOT** use `| head -n 50` or `| tail -n 100` — use `head`/`tail` parameters instead
|
||||
- If you catch yourself typing `cat`, `head`, `tail`, `less`, `more`, `ls`, `grep`, `rg`, `find`, `fd`, `sed -i`, `awk -i`, or a heredoc redirect inside a Bash call, stop and switch to the dedicated tool. There is no scenario where bash is preferable for these operations.
|
||||
</critical>
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
* Bash command normalizer - extracts patterns that are better handled natively.
|
||||
*
|
||||
* Detects and extracts:
|
||||
* - `| head -n N` / `| head -N` - extracted to headLines
|
||||
* - `| tail -n N` / `| tail -N` - extracted to tailLines
|
||||
*/
|
||||
|
||||
export interface NormalizedCommand {
|
||||
/** Cleaned command with patterns stripped */
|
||||
command: string;
|
||||
/** Extracted head line count, if any */
|
||||
headLines?: number;
|
||||
/** Extracted tail line count, if any */
|
||||
tailLines?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern to match trailing pipe to head/tail.
|
||||
* Captures: full match, command (head/tail), line count
|
||||
*
|
||||
* Matches:
|
||||
* - `| head -n 50`
|
||||
* - `| head -50`
|
||||
* - `| tail -n 100`
|
||||
* - `| tail -100`
|
||||
*
|
||||
* Does NOT match head/tail with other flags or without line count.
|
||||
*/
|
||||
const TRAILING_HEAD_TAIL_PATTERN = /\|\s*(head|tail)\s+(?:-n\s*(\d+)|(-\d+))\s*$/;
|
||||
|
||||
/**
|
||||
* Normalize a bash command by stripping patterns better handled natively.
|
||||
*
|
||||
* Extracts `| head -n N` and `| tail -n N` suffixes into separate fields
|
||||
* so they can be applied post-execution without breaking streaming.
|
||||
*
|
||||
* Strips `2>&1` since we already merge stdout/stderr.
|
||||
*/
|
||||
export function normalizeBashCommand(command: string): NormalizedCommand {
|
||||
let normalized = command;
|
||||
let headLines: number | undefined;
|
||||
let tailLines: number | undefined;
|
||||
|
||||
// Extract trailing head/tail
|
||||
const match = normalized.match(TRAILING_HEAD_TAIL_PATTERN);
|
||||
if (match) {
|
||||
const [fullMatch, cmd, nValue, dashValue] = match;
|
||||
const lineCount = nValue ? Number.parseInt(nValue, 10) : Number.parseInt(dashValue.slice(1), 10);
|
||||
|
||||
if (cmd === "head") {
|
||||
headLines = lineCount;
|
||||
} else {
|
||||
tailLines = lineCount;
|
||||
}
|
||||
|
||||
normalized = normalized.slice(0, -fullMatch.length);
|
||||
}
|
||||
|
||||
// Preserve internal whitespace (important for heredocs / indentation-sensitive scripts)
|
||||
normalized = normalized.trim();
|
||||
|
||||
return {
|
||||
command: normalized,
|
||||
headLines,
|
||||
tailLines,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply head/tail limits to output text.
|
||||
*
|
||||
* If both head and tail are specified, head is applied first (take first N lines),
|
||||
* then tail is applied (take last M lines of that).
|
||||
*/
|
||||
export function applyHeadTail(
|
||||
text: string,
|
||||
headLines?: number,
|
||||
tailLines?: number,
|
||||
): { text: string; applied: boolean; headApplied?: number; tailApplied?: number } {
|
||||
if (!headLines && !tailLines) {
|
||||
return { text, applied: false };
|
||||
}
|
||||
|
||||
let lines = text.split("\n");
|
||||
let headApplied: number | undefined;
|
||||
let tailApplied: number | undefined;
|
||||
|
||||
// Apply head first (keep first N lines)
|
||||
if (headLines !== undefined && headLines > 0 && lines.length > headLines) {
|
||||
lines = lines.slice(0, headLines);
|
||||
headApplied = headLines;
|
||||
}
|
||||
|
||||
// Then apply tail (keep last N lines)
|
||||
if (tailLines !== undefined && tailLines > 0 && lines.length > tailLines) {
|
||||
lines = lines.slice(-tailLines);
|
||||
tailApplied = tailLines;
|
||||
}
|
||||
|
||||
return {
|
||||
text: lines.join("\n"),
|
||||
applied: headApplied !== undefined || tailApplied !== undefined,
|
||||
headApplied,
|
||||
tailApplied,
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import { getSixelLineMask } from "../utils/sixel";
|
||||
import type { ToolSession } from ".";
|
||||
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
||||
import { checkBashInterception } from "./bash-interceptor";
|
||||
import { applyHeadTail } from "./bash-normalize";
|
||||
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
||||
import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
|
||||
import { resolveToCwd } from "./path-utils";
|
||||
@@ -50,8 +49,7 @@ const bashSchemaBase = Type.Object({
|
||||
),
|
||||
timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 300 })),
|
||||
cwd: Type.Optional(Type.String({ description: "working directory", examples: ["src/", "/tmp"] })),
|
||||
head: Type.Optional(Type.Number({ description: "first n lines of output" })),
|
||||
tail: Type.Optional(Type.Number({ description: "last n lines of output" })),
|
||||
|
||||
pty: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "run in pty mode",
|
||||
@@ -75,8 +73,7 @@ export interface BashToolInput {
|
||||
env?: Record<string, string>;
|
||||
timeout?: number;
|
||||
cwd?: string;
|
||||
head?: number;
|
||||
tail?: number;
|
||||
|
||||
async?: boolean;
|
||||
pty?: boolean;
|
||||
}
|
||||
@@ -266,16 +263,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
});
|
||||
}
|
||||
|
||||
#formatResultOutput(result: BashResult | BashInteractiveResult, headLines?: number, tailLines?: number): string {
|
||||
let outputText = normalizeResultOutput(result);
|
||||
const headTailResult = applyHeadTail(outputText, headLines, tailLines);
|
||||
if (headTailResult.applied) {
|
||||
outputText = headTailResult.text;
|
||||
}
|
||||
if (!outputText) {
|
||||
outputText = "(no output)";
|
||||
}
|
||||
return outputText;
|
||||
#formatResultOutput(result: BashResult | BashInteractiveResult): string {
|
||||
const outputText = normalizeResultOutput(result);
|
||||
return outputText || "(no output)";
|
||||
}
|
||||
|
||||
#buildResultText(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): string {
|
||||
@@ -297,11 +287,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
#buildCompletedResult(
|
||||
result: BashResult | BashInteractiveResult,
|
||||
timeoutSec: number,
|
||||
headLines?: number,
|
||||
tailLines?: number,
|
||||
options: { requestedTimeoutSec?: number; notices?: string[] } = {},
|
||||
): AgentToolResult<BashToolDetails> {
|
||||
const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
|
||||
const outputLines = [this.#formatResultOutput(result)];
|
||||
const notices = options.notices?.filter(Boolean) ?? [];
|
||||
if (notices.length > 0) outputLines.push("", ...notices);
|
||||
const outputText = outputLines.join("\n");
|
||||
@@ -356,8 +344,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
timeoutSec: number;
|
||||
requestedTimeoutSec?: number;
|
||||
timeoutClampNotice?: string;
|
||||
headLines?: number;
|
||||
tailLines?: number;
|
||||
|
||||
resolvedEnv?: Record<string, string>;
|
||||
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
||||
startBackgrounded: boolean;
|
||||
@@ -394,16 +381,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
},
|
||||
onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
|
||||
});
|
||||
const finalResult = this.#buildCompletedResult(
|
||||
result,
|
||||
options.timeoutSec,
|
||||
options.headLines,
|
||||
options.tailLines,
|
||||
{
|
||||
requestedTimeoutSec: options.requestedTimeoutSec,
|
||||
notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
||||
},
|
||||
);
|
||||
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
||||
requestedTimeoutSec: options.requestedTimeoutSec,
|
||||
notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
||||
});
|
||||
const finalText = this.#extractTextResult(finalResult);
|
||||
latestText = finalText;
|
||||
completion.resolve({ kind: "completed", result: finalResult });
|
||||
@@ -481,8 +462,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
env: rawEnv,
|
||||
timeout: rawTimeout = 300,
|
||||
cwd,
|
||||
head,
|
||||
tail,
|
||||
|
||||
async: asyncRequested = false,
|
||||
pty = false,
|
||||
}: BashToolInput,
|
||||
@@ -505,10 +485,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
|
||||
}
|
||||
|
||||
// Only apply explicit head/tail params from tool input.
|
||||
const headLines = head;
|
||||
const tailLines = tail;
|
||||
|
||||
// Check both the original command and the cwd-normalized command so
|
||||
// leading `cd ... &&` wrappers do not hide either shell-navigation rules
|
||||
// or the dedicated-tool command that follows the directory change.
|
||||
@@ -583,8 +559,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
timeoutSec,
|
||||
requestedTimeoutSec,
|
||||
timeoutClampNotice,
|
||||
headLines,
|
||||
tailLines,
|
||||
|
||||
resolvedEnv,
|
||||
onUpdate,
|
||||
startBackgrounded: true,
|
||||
@@ -605,8 +580,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
timeoutSec,
|
||||
requestedTimeoutSec,
|
||||
timeoutClampNotice,
|
||||
headLines,
|
||||
tailLines,
|
||||
|
||||
resolvedEnv,
|
||||
onUpdate,
|
||||
startBackgrounded,
|
||||
@@ -675,7 +649,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
if (isInteractiveResult(result) && result.timedOut) {
|
||||
throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
|
||||
}
|
||||
return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
|
||||
return this.#buildCompletedResult(result, timeoutSec, {
|
||||
requestedTimeoutSec,
|
||||
notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
|
||||
});
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { applyHeadTail, normalizeBashCommand } from "../../src/tools/bash-normalize";
|
||||
|
||||
describe("normalizeBashCommand", () => {
|
||||
describe("head/tail extraction", () => {
|
||||
it("extracts | head -n N", () => {
|
||||
const result = normalizeBashCommand("ls -la | head -n 50");
|
||||
expect(result.command).toBe("ls -la");
|
||||
expect(result.headLines).toBe(50);
|
||||
expect(result.tailLines).toBeUndefined();
|
||||
});
|
||||
|
||||
it("extracts | head -N (short form)", () => {
|
||||
const result = normalizeBashCommand("cat file.txt | head -20");
|
||||
expect(result.command).toBe("cat file.txt");
|
||||
expect(result.headLines).toBe(20);
|
||||
});
|
||||
|
||||
it("extracts | tail -n N", () => {
|
||||
const result = normalizeBashCommand("dmesg | tail -n 100");
|
||||
expect(result.command).toBe("dmesg");
|
||||
expect(result.tailLines).toBe(100);
|
||||
expect(result.headLines).toBeUndefined();
|
||||
});
|
||||
|
||||
it("extracts | tail -N (short form)", () => {
|
||||
const result = normalizeBashCommand("journalctl | tail -50");
|
||||
expect(result.command).toBe("journalctl");
|
||||
expect(result.tailLines).toBe(50);
|
||||
});
|
||||
|
||||
it("handles multiple spaces around pipe", () => {
|
||||
const result = normalizeBashCommand("git log | head -n 10");
|
||||
expect(result.command).toBe("git log");
|
||||
expect(result.headLines).toBe(10);
|
||||
});
|
||||
|
||||
it("does not extract head/tail in middle of pipeline", () => {
|
||||
const result = normalizeBashCommand("cat file | head -n 10 | grep foo");
|
||||
expect(result.command).toBe("cat file | head -n 10 | grep foo");
|
||||
expect(result.headLines).toBeUndefined();
|
||||
expect(result.tailLines).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not extract head without line count", () => {
|
||||
const result = normalizeBashCommand("cat file | head");
|
||||
expect(result.command).toBe("cat file | head");
|
||||
expect(result.headLines).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not extract head with other flags", () => {
|
||||
const result = normalizeBashCommand("cat file | head -c 100");
|
||||
expect(result.command).toBe("cat file | head -c 100");
|
||||
expect(result.headLines).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("no patterns", () => {
|
||||
it("preserves command unchanged", () => {
|
||||
const result = normalizeBashCommand("git status");
|
||||
expect(result.command).toBe("git status");
|
||||
expect(result.headLines).toBeUndefined();
|
||||
expect(result.tailLines).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves internal spacing and tabs", () => {
|
||||
const result = normalizeBashCommand("echo 'a b\t\tc'");
|
||||
expect(result.command).toBe("echo 'a b\t\tc'");
|
||||
});
|
||||
|
||||
it("preserves heredoc indentation", () => {
|
||||
const command = "python3 - <<'PY'\nfor i in [1]:\n if True:\n x = 1\nPY";
|
||||
const result = normalizeBashCommand(command);
|
||||
expect(result.command).toBe(command);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyHeadTail", () => {
|
||||
const sampleText = "line1\nline2\nline3\nline4\nline5";
|
||||
|
||||
it("returns original when no limits", () => {
|
||||
const result = applyHeadTail(sampleText);
|
||||
expect(result.text).toBe(sampleText);
|
||||
expect(result.applied).toBe(false);
|
||||
});
|
||||
|
||||
it("applies head limit", () => {
|
||||
const result = applyHeadTail(sampleText, 2);
|
||||
expect(result.text).toBe("line1\nline2");
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.headApplied).toBe(2);
|
||||
});
|
||||
|
||||
it("applies tail limit", () => {
|
||||
const result = applyHeadTail(sampleText, undefined, 2);
|
||||
expect(result.text).toBe("line4\nline5");
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.tailApplied).toBe(2);
|
||||
});
|
||||
|
||||
it("applies head then tail", () => {
|
||||
const result = applyHeadTail(sampleText, 4, 2);
|
||||
// head=4 gives: line1\nline2\nline3\nline4
|
||||
// tail=2 of that gives: line3\nline4
|
||||
expect(result.text).toBe("line3\nline4");
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.headApplied).toBe(4);
|
||||
expect(result.tailApplied).toBe(2);
|
||||
});
|
||||
|
||||
it("does not apply if text is shorter than limit", () => {
|
||||
const result = applyHeadTail(sampleText, 10);
|
||||
expect(result.text).toBe(sampleText);
|
||||
expect(result.applied).toBe(false);
|
||||
});
|
||||
|
||||
it("handles empty text", () => {
|
||||
const result = applyHeadTail("", 5);
|
||||
expect(result.text).toBe("");
|
||||
expect(result.applied).toBe(false);
|
||||
});
|
||||
|
||||
it("handles single line", () => {
|
||||
const result = applyHeadTail("single", 1);
|
||||
expect(result.text).toBe("single");
|
||||
expect(result.applied).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user