{"version":3,"file":"bash.test.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash.test.ts"],"names":[],"mappings":"","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { type BashOperations, createBashTool } from \"./bash.js\";\n\ndescribe(\"createBashTool\", () => {\n\tit(\"returns structured cancellation instead of rejecting on abort\", async () => {\n\t\tconst controller = new AbortController();\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onData }) => {\n\t\t\t\tonData(Buffer.from(\"partial output\"));\n\t\t\t\tcontroller.abort();\n\t\t\t\tthrow new Error(\"aborted\");\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\n\t\t// Cancelled results throw as errors so the agent loop marks isError=true.\n\t\t// The model sees structured content in the error message.\n\t\ttry {\n\t\t\tawait tool.execute(\"call_1\", { command: \"sleep 1\" }, controller.signal);\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"cancelled: true\");\n\t\t\texpect(msg).toContain(\"Command:\");\n\t\t\texpect(msg).toContain(\"exit_code:\");\n\t\t}\n\t});\n\n\tit(\"sanitizes streamed and final output with bash-executor parity\", async () => {\n\t\tconst updates: string[] = [];\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onData, onStdout }) => {\n\t\t\t\tconst data = Buffer.from(\"\\u001b[31mred\\u001b[0m\\x00\\u0007text\\r\\nnext\\n\");\n\t\t\t\tonData(data);\n\t\t\t\tonStdout?.(data);\n\t\t\t\treturn { exitCode: 0 };\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\tconst result = await tool.execute(\"call_1\", { command: \"printf test\" }, undefined, (partialResult) => {\n\t\t\tupdates.push(partialResult.content[0]?.type === \"text\" ? partialResult.content[0].text : \"\");\n\t\t});\n\n\t\t// Partial updates stream sanitized content\n\t\texpect(updates).toEqual([\"redtext\\nnext\\n\"]);\n\t\t// Final content includes structured evidence with stdout section\n\t\texpect(result.content[0].type).toBe(\"text\");\n\t\tconst text = result.content[0].type === \"text\" ? result.content[0].text : \"\";\n\t\texpect(text).toContain(\"stdout:\");\n\t\texpect(text).toContain(\"redtext\\nnext\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\t// Details carry structured fields\n\t\texpect(result.details).toBeDefined();\n\t\texpect(result.details!.exitCode).toBe(0);\n\t\texpect(result.details!.stdout).toContain(\"redtext\");\n\t});\n\n\tit(\"reports non-zero exit code as error with structured content\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onStdout }) => {\n\t\t\t\tonStdout?.(Buffer.from(\"SUCCESS-S04\\n\"));\n\t\t\t\treturn { exitCode: 9 };\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\ttry {\n\t\t\tawait tool.execute(\"call_1\", { command: \"exit 9\" });\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"Command:\");\n\t\t\texpect(msg).toContain(\"stdout:\");\n\t\t\texpect(msg).toContain(\"SUCCESS-S04\");\n\t\t\texpect(msg).toContain(\"exit_code: 9\");\n\t\t}\n\t});\n\n\tit(\"reports timed out as error with structured content\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onData }) => {\n\t\t\t\tonData(Buffer.from(\"partial\\n\"));\n\t\t\t\tthrow new Error(\"timeout:5\");\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\ttry {\n\t\t\tawait tool.execute(\"call_1\", { command: \"sleep 10\", timeout: 5 });\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"keeps stderr separate from stdout in model-facing content\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onStdout, onStderr }) => {\n\t\t\t\tonStdout?.(Buffer.from(\"OUT\\n\"));\n\t\t\t\tonStderr?.(Buffer.from(\"ERR\\n\"));\n\t\t\t\treturn { exitCode: 0 };\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\tconst result = await tool.execute(\"call_1\", { command: \"both streams\" });\n\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"stdout:\\nOUT\");\n\t\texpect(text).toContain(\"stderr:\\nERR\");\n\t\texpect(result.details!.stdout).toBe(\"OUT\\n\");\n\t\texpect(result.details!.stderr).toBe(\"ERR\\n\");\n\t\texpect(result.details!.exitCode).toBe(0);\n\t});\n\n\tit(\"stderr with exit 0 is not classified as failure\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onStderr }) => {\n\t\t\t\tonStderr?.(Buffer.from(\"ONLY-STDERR\\n\"));\n\t\t\t\treturn { exitCode: 0 };\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\tconst result = await tool.execute(\"call_1\", { command: \"stderr success\" });\n\n\t\t// Should resolve (not throw) because exit code is 0\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"stderr:\\nONLY-STDERR\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(result.details!.stderr).toContain(\"ONLY-STDERR\");\n\t\texpect(result.details!.exitCode).toBe(0);\n\t});\n\n\tit(\"positive stdout with non-zero exit is classified as failure\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onStdout }) => {\n\t\t\t\tonStdout?.(Buffer.from(\"SUCCESS\\n\"));\n\t\t\t\treturn { exitCode: 17 };\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\ttry {\n\t\t\tawait tool.execute(\"call_1\", { command: \"stdout fail\" });\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"stdout:\\nSUCCESS\");\n\t\t\texpect(msg).toContain(\"exit_code: 17\");\n\t\t}\n\t});\n\n\tit(\"stderr with non-zero exit is classified as failure\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async (_command, _cwd, { onStderr }) => {\n\t\t\t\tonStderr?.(Buffer.from(\"FAILURE\\n\"));\n\t\t\t\treturn { exitCode: 19 };\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\ttry {\n\t\t\tawait tool.execute(\"call_1\", { command: \"stderr fail\" });\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"stderr:\\nFAILURE\");\n\t\t\texpect(msg).toContain(\"exit_code: 19\");\n\t\t}\n\t});\n\n\tit(\"spawn error reaches model-facing output\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async () => {\n\t\t\t\tthrow new Error(\"ENOENT: no such file or directory\");\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\ttry {\n\t\t\tawait tool.execute(\"call_1\", { command: \"nonexistent\" });\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"spawn_error:\");\n\t\t\texpect(msg).toContain(\"ENOENT\");\n\t\t}\n\t});\n});\n\n// ============================================================================\n// Evidence integrity tests (E01-E07) — fail-closed pipeline evidence\n// ============================================================================\n\ndescribe(\"bash tool — evidence integrity\", () => {\n\tit(\"E01: fd 3 cannot spoof metadata (no control channel exists)\", async () => {\n\t\t// Since fd 3 is no longer reserved for metadata, writing to it is\n\t\t// just a user-space operation that may fail or succeed naturally.\n\t\t// The key assertion: no internal Jensen metadata is modified.\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_e01\", {\n\t\t\tcommand: \"printf 'E01-CLEAN\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"E01-CLEAN\");\n\t\t// No pipeline evidence for a non-pipeline command\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t\texpect(text).not.toContain(\"pipeline_stage_exit_codes\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\t// Non-pipeline commands get explicit authority scope\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t});\n\n\tit(\"E02: user trap does not affect Jensen evidence\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_e02\", {\n\t\t\tcommand: \"trap 'printf \\\"USER-TRAP\\\\n\\\" >&2' EXIT\\nfalse | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// The user trap output appears in stderr (normal)\n\t\t// But Jensen's evidence section is unaffected\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).toContain(\"stage_exit_codes_known: false\");\n\t});\n\n\tit(\"E03: trap removal does not affect evidence integrity\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_e03\", {\n\t\t\tcommand: \"trap - EXIT\\nfalse | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// Evidence section must still be valid — non-authoritative pipeline\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t});\n\n\tit(\"E04: exec captures exit code without invented metadata\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_e04\", {\n\t\t\t\tcommand: \"exec bash -c 'printf \\\"EXEC\\\\n\\\"; exit 27'\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for exit 27\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"EXEC\");\n\t\t\texpect(msg).toContain(\"exit_code: 27\");\n\t\t\t// No invented stage codes\n\t\t\texpect(msg).not.toContain(\"pipeline_stage_exit_codes\");\n\t\t}\n\t});\n\n\tit(\"E05: deceptive pipeline (false|tail) is non-authoritative\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\t// false | tail exits 0 (tail succeeds on empty input)\n\t\t// Must be marked non-authoritative\n\t\tconst result = await tool.execute(\"call_e05\", {\n\t\t\tcommand: \"false | tail\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).toContain(\"authority_scope: final_pipeline_stage_only\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"warning:\");\n\t});\n\n\tit(\"E06: pipeline with deceptive success (bash -c 'exit 9' | cat) is non-authoritative\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_e06\", {\n\t\t\tcommand: \"bash -c 'printf \\\"SUCCESS\\\\n\\\"; exit 9' | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// exit_code is 0 because cat succeeds, but must be non-authoritative\n\t\texpect(text).toContain(\"SUCCESS\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).toContain(\"stage_exit_codes_known: false\");\n\t});\n\n\tit(\"E07: successful pipeline (printf|grep) is non-authoritative regardless of success\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_e07\", {\n\t\t\tcommand: \"printf 'ok\\\\n' | grep ok\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// Even though both stages succeed, stage codes are not reliably captured\n\t\texpect(text).toContain(\"ok\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).not.toContain(\"pipeline_stage_exit_codes\");\n\t});\n});\n\n// ============================================================================\n// Pipeline risk — fail-closed detection\n// ============================================================================\n\ndescribe(\"bash tool — pipeline risk detection\", () => {\n\tit(\"should suspect pipeline: false | tail\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk1\", {\n\t\t\tcommand: \"false | tail\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"should suspect pipeline: printf | grep\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk2\", {\n\t\t\tcommand: \"printf 'x\\\\n' | grep x\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"should suspect pipeline: heredoc with pipe\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk3\", {\n\t\t\tcommand: \"cat <<'EOF' | grep x\\nx\\nEOF\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"should NOT suspect pipeline: quoted pipe character in single quotes\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk4\", {\n\t\t\tcommand: \"printf '%s\\\\n' 'a|b'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"should NOT suspect pipeline: quoted pipe character in double quotes\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk5\", {\n\t\t\tcommand: 'printf \"%s\\\\n\" \"a|b\"',\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"should NOT suspect pipeline: escaped pipe\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\t// a\\|b — bash escapes the |, so this is not a real pipeline.\n\t\t// However, our simple quoting-aware detector sees | as unquoted\n\t\t// (the backslash before it is consumed by bash, not by our detector).\n\t\t// This is a known false positive — acceptable because it produces\n\t\t// a non-authoritative warning, not a false authoritative result.\n\t\tconst result = await tool.execute(\"call_risk6\", {\n\t\t\tcommand: \"printf '%s\\\\n' a\\\\|b\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// Output is correct — prints literal \"a|b\"\n\t\texpect(text).toContain(\"a|b\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"should NOT suspect pipeline: pipe in comment\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk7\", {\n\t\t\tcommand: \"printf 'COMMENT\\\\n' # false | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"COMMENT\");\n\t\t// The # prevents | from being parsed, but our simple detector sees\n\t\t// unquoted | after #. This is a known false positive — acceptable\n\t\t// because it produces a non-authoritative warning, not false authority.\n\t\t// We verify the exit code is still reported correctly.\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"should NOT suspect pipeline: pipe in arithmetic expression $((1|2))\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk8\", {\n\t\t\tcommand: \"printf '%s\\\\n' $((1 | 2))\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// $((1 | 2)) — bitwise OR in arithmetic context, not a shell pipeline.\n\t\t// Known false positive: detector sees unquoted |.\n\t\texpect(text).toContain(\"3\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"should NOT suspect pipeline: || operator\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_risk9\", {\n\t\t\tcommand: \"false || printf 'RECOVERED\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"RECOVERED\");\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"simple command has explicit authority scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_auth\", {\n\t\t\tcommand: \"printf 'AUTHORITATIVE\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"AUTHORITATIVE\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\t// Non-pipeline commands now get explicit authority metadata\n\t\texpect(text).toContain(\"exit_status_known: true\");\n\t\texpect(text).toContain(\"exit_status_authoritative: true\");\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t\texpect(text).toContain(\"internal_command_statuses_known: false\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: true\");\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"non-pipeline command with non-zero exit is authoritative\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_auth2\", {\n\t\t\t\tcommand: \"bash -c 'printf \\\"SUCCESS\\\\n\\\"; exit 17'\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for exit 17\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"SUCCESS\");\n\t\t\texpect(msg).toContain(\"exit_code: 17\");\n\t\t\texpect(msg).not.toContain(\"pipeline_suspected: true\");\n\t\t\texpect(msg).toContain(\"exit_status_authoritative: true\");\n\t\t\texpect(msg).toContain(\"authority_scope: final_shell_exit_status\");\n\t\t}\n\t});\n});\n\n// ============================================================================\n// Pipeline semantics — fail-closed redesign\n// ============================================================================\n\ndescribe(\"bash tool — pipeline semantics (fail-closed)\", () => {\n\tit(\"pipeline metadata reaches model-facing output with warning\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\t// false|cat exits 0 in bash (cat succeeds), but must be non-authoritative\n\t\tconst result = await tool.execute(\"call_p1\", {\n\t\t\tcommand: \"false | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"stage_exit_codes_known: false\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).toContain(\"authority_scope: final_pipeline_stage_only\");\n\t\texpect(text).toContain(\"warning:\");\n\t\texpect(text).toContain(\"Re-run the validation command without a pipeline\");\n\t});\n\n\tit(\"pipeline with non-zero last stage shows warning with exit code\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_p2\", {\n\t\t\t\tcommand: \"false | grep DOES_NOT_EXIST\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"pipeline_suspected: true\");\n\t\t\texpect(msg).toContain(\"validation_evidence_authoritative: false\");\n\t\t}\n\t});\n\n\tit(\"simple command does not show pipeline evidence\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_simple\", {\n\t\t\tcommand: \"printf 'SIMPLE\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"SIMPLE\");\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t\t// Simple commands now have explicit authority metadata\n\t\texpect(text).toContain(\"exit_status_known: true\");\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t});\n\n\tit(\"operators that are not pipelines: ||\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_or\", {\n\t\t\tcommand: \"false || printf 'RECOVERED\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"RECOVERED\");\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"operators that are not pipelines: &&\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_and\", {\n\t\t\tcommand: \"true && printf 'AND\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"AND\");\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n});\n\n// ============================================================================\n// Heredoc tests (HD01-HD12) — real bash execution\n// ============================================================================\n\ndescribe(\"bash tool — heredocs (real execution)\", () => {\n\tit(\"HD01: basic heredoc produces only expected content\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd01\", {\n\t\t\tcommand: \"cat <<'EOF'\\nHEREDOC-BASIC\\nEOF\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"HEREDOC-BASIC\");\n\t\t// No wrapper, trap, or sentinel leakage\n\t\texpect(text).not.toContain(\"__jensen_stages\");\n\t\texpect(text).not.toContain(\"PIPESTATUS\");\n\t\texpect(text).not.toContain(\"_PI_\");\n\t});\n\n\tit(\"HD02: quoted delimiter disables expansion\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd02\", {\n\t\t\tcommand: \"VALUE=\\\"SECRET\\\"\\ncat <<'EOF'\\n$VALUE\\nEOF\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// Heredoc body with quoted delimiter shows literal $VALUE, not expanded value\n\t\texpect(text).toContain(\"$VALUE\");\n\t\t// The expanded value SECRET should only appear in the command echo, not in stdout\n\t\t// (it's in the command itself, which is shown, so exclude from stdout section only)\n\t\tconst stdoutSection = text.substring(text.indexOf(\"stdout:\"), text.indexOf(\"--- Evidence ---\"));\n\t\texpect(stdoutSection).not.toContain(\"SECRET\");\n\t});\n\n\tit(\"HD03: unquoted delimiter enables expansion\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd03\", {\n\t\t\tcommand: 'VALUE=\"EXPANDED\"\\ncat <<EOF\\n$VALUE\\nEOF',\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"EXPANDED\");\n\t});\n\n\tit(\"HD04: heredoc followed by another command\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd04\", {\n\t\t\tcommand: \"cat <<'EOF'\\nFIRST\\nEOF\\nprintf 'SECOND\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"FIRST\");\n\t\texpect(text).toContain(\"SECOND\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"HD05: heredoc inside pipeline produces non-authoritative warning\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd05\", {\n\t\t\tcommand: \"cat <<'EOF' | grep beta\\nalpha\\nbeta\\nEOF\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"beta\");\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"evidence_authoritative: false\");\n\t});\n\n\tit(\"HD06: two heredocs in sequence\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd06\", {\n\t\t\tcommand: \"cat <<'FIRST'\\nONE\\nFIRST\\ncat <<'SECOND'\\nTWO\\nSECOND\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"ONE\");\n\t\texpect(text).toContain(\"TWO\");\n\t});\n\n\tit(\"HD07: heredoc with tab-indented delimiter (<<-EOF)\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd07\", {\n\t\t\tcommand: \"cat <<-EOF\\n\\tTAB-INDENTED\\n\\tEOF\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"TAB-INDENTED\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"HD08: heredoc inside subshell\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd08\", {\n\t\t\tcommand: \"(\\ncat <<'EOF'\\nSUBSHELL-HEREDOC\\nEOF\\n)\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"SUBSHELL-HEREDOC\");\n\t});\n\n\tit(\"HD09: heredoc with stderr and later failure\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_hd09\", {\n\t\t\t\tcommand: \"cat <<'EOF'\\nVISIBLE\\nEOF\\nprintf 'HEREDOC-ERR\\\\n' >&2\\nexit 9\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for exit 9\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"VISIBLE\");\n\t\t\texpect(msg).toContain(\"HEREDOC-ERR\");\n\t\t\texpect(msg).toContain(\"exit_code: 9\");\n\t\t}\n\t});\n\n\tit(\"HD10: heredoc without trailing newline in source\", async () => {\n\t\t// Build source programmatically — last char is the delimiter \"EOF\",\n\t\t// no trailing newline after it.\n\t\tconst source = \"cat <<'EOF'\\nHEREDOC-NO-NL\\nEOF\";\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd10\", { command: source });\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"HEREDOC-NO-NL\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"HD11: heredoc inside function preserves function semantics\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_hd11\", {\n\t\t\tcommand: \"myfunc() {\\n  cat <<'EOF'\\nHEREDOC-IN-FUNC\\nEOF\\n}\\nmyfunc\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"HEREDOC-IN-FUNC\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t});\n\n\tit(\"HD12: heredoc followed by timeout captures heredoc content\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_hd12\", {\n\t\t\t\tcommand: \"cat <<'EOF'\\nBEFORE-SLEEP\\nEOF\\nsleep 5\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"BEFORE-SLEEP\");\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n});\n\n// ============================================================================\n// Multiline regression tests (ML01-ML08) — real bash execution\n// ============================================================================\n\ndescribe(\"bash tool — multiline commands (real execution)\", () => {\n\tit(\"ML01: multiline pipeline\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml01\", {\n\t\t\tcommand: \"printf 'alpha\\\\nbeta\\\\n' |\\ngrep beta\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"beta\");\n\t});\n\n\tit(\"ML02: function definition and invocation\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml02\", {\n\t\t\tcommand: \"sample_function() {\\n  printf 'FUNCTION-OK\\\\n'\\n}\\nsample_function\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"FUNCTION-OK\");\n\t});\n\n\tit(\"ML03: if block\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml03\", {\n\t\t\tcommand: \"if true; then\\n  printf 'IF-OK\\\\n'\\nfi\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"IF-OK\");\n\t});\n\n\tit(\"ML04: for loop\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml04\", {\n\t\t\tcommand: \"for value in one two; do\\n  printf '%s\\\\n' \\\"$value\\\"\\ndone\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"one\");\n\t\texpect(text).toContain(\"two\");\n\t});\n\n\tit(\"ML05: case block\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml05\", {\n\t\t\tcommand: 'value=\"a\"\\ncase \"$value\" in\\n  a) printf \\'CASE-A\\\\n\\' ;;\\nesac',\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"CASE-A\");\n\t});\n\n\tit(\"ML06: command substitution\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml06\", {\n\t\t\tcommand: \"printf 'VALUE=%s\\\\n' \\\"$(printf 'INNER')\\\"\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"VALUE=INNER\");\n\t});\n\n\tit(\"ML07: trailing comment\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml07\", {\n\t\t\tcommand: \"printf 'COMMENT-OK\\\\n' # trailing comment\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"COMMENT-OK\");\n\t});\n\n\tit(\"ML08: quoted pipe character\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_ml08\", {\n\t\t\tcommand: \"printf '%s\\\\n' 'a|b'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"a|b\");\n\t\t// Not a real pipeline — no pipeline evidence\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n});\n\n// ============================================================================\n// Timeout tests (TO01-TO10) — real bash execution\n// ============================================================================\n\ndescribe(\"bash tool — timeout (real execution)\", () => {\n\tit(\"TO01: timeout kills process and reports timedOut\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to01\", {\n\t\t\t\tcommand: \"sleep 5\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t\texpect(msg).toContain(\"cancelled: false\");\n\t\t\texpect(msg).not.toContain(\"exit_code: 0\");\n\t\t}\n\t});\n\n\tit(\"TO02: timeout preserves stdout produced before timeout\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to02\", {\n\t\t\t\tcommand: \"printf 'BEFORE-TIMEOUT\\\\n'\\nsleep 5\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"BEFORE-TIMEOUT\");\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"TO03: timeout preserves stderr produced before timeout\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to03\", {\n\t\t\t\tcommand: \"printf 'TIMEOUT-ERR\\\\n' >&2\\nsleep 5\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"TIMEOUT-ERR\");\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"TO04: timeout kills pipeline\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to04\", {\n\t\t\t\tcommand: \"sleep 5 | cat\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"TO05: timeout after heredoc preserves heredoc content\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to05\", {\n\t\t\t\tcommand: \"cat <<'EOF'\\nBEFORE-SLEEP\\nEOF\\nsleep 5\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"BEFORE-SLEEP\");\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"TO06: timeout omitted means no timeout\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_to06\", {\n\t\t\tcommand: \"printf 'NO-TIMEOUT\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"NO-TIMEOUT\");\n\t\texpect(text).toContain(\"timed_out: false\");\n\t});\n\n\tit(\"TO07: fractional timeout works\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to07\", {\n\t\t\t\tcommand: \"sleep 3\",\n\t\t\t\ttimeout: 0.5,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"TO08: zero timeout is rejected with validation error\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to08\", {\n\t\t\t\tcommand: \"printf 'ZERO\\\\n'\",\n\t\t\t\ttimeout: 0,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for timeout=0\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"Timeout must be a positive number\");\n\t\t\texpect(msg).toContain(\"Got 0\");\n\t\t}\n\t});\n\n\tit(\"TO09: negative timeout is rejected with validation error\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to09\", {\n\t\t\t\tcommand: \"printf 'NEGATIVE\\\\n'\",\n\t\t\t\ttimeout: -5,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for timeout=-5\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"Timeout must be a positive number\");\n\t\t\texpect(msg).toContain(\"-5\");\n\t\t}\n\t});\n\n\tit(\"TO10: excessive timeout is rejected with validation error\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to10\", {\n\t\t\t\tcommand: \"printf 'EXCESSIVE\\\\n'\",\n\t\t\t\ttimeout: 999999,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for excessive timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"exceeds maximum allowed timeout\");\n\t\t}\n\t});\n\n\tit(\"TO11: NaN timeout is rejected\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to11\", {\n\t\t\t\tcommand: \"printf 'NAN\\\\n'\",\n\t\t\t\ttimeout: NaN,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for NaN timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"Invalid timeout value\");\n\t\t}\n\t});\n\n\tit(\"TO12: Infinity timeout is rejected\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_to12\", {\n\t\t\t\tcommand: \"printf 'INF\\\\n'\",\n\t\t\t\ttimeout: Infinity,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for Infinity timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"Invalid timeout value\");\n\t\t}\n\t});\n});\n\n// ============================================================================\n// Timeout child-process cleanup — real bash execution\n// ============================================================================\n\ndescribe(\"bash tool — timeout child process cleanup\", () => {\n\tit(\"timeout leaves no orphan child processes\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst markerFile = `/tmp/jensen-timeout-test-${process.pid}.marker`;\n\n\t\ttry {\n\t\t\tawait tool.execute(\"call_orphan\", {\n\t\t\t\tcommand: `(\n  printf '%s' \"$$\" > \"${markerFile}\"\n  sleep 30\n) &\nCHILD_PID=\"$!\"\nprintf '%s' \"$CHILD_PID\" >> \"${markerFile}\"\nwait \"$CHILD_PID\"\n`,\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (_err) {\n\t\t\t// Child should be dead after timeout kills process group\n\t\t\t// Allow brief settling time for kill signal delivery\n\t\t\tawait new Promise((r) => setTimeout(r, 500));\n\n\t\t\t// Try to read marker file for child PID\n\t\t\ttry {\n\t\t\t\tconst fs = await import(\"node:fs\");\n\t\t\t\tconst content = fs.readFileSync(markerFile, \"utf-8\").trim();\n\t\t\t\tconst lines = content.split(\"\\n\");\n\t\t\t\tif (lines.length >= 2) {\n\t\t\t\t\tconst childPid = parseInt(lines[1], 10);\n\t\t\t\t\tif (!Number.isNaN(childPid)) {\n\t\t\t\t\t\t// Check if child still alive\n\t\t\t\t\t\tlet alive = false;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tprocess.kill(childPid, 0);\n\t\t\t\t\t\t\talive = true;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\talive = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t\texpect(alive).toBe(false);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\t// Cleanup marker file\n\t\t\t\ttry {\n\t\t\t\t\tconst fs = await import(\"node:fs\");\n\t\t\t\t\tfs.unlinkSync(markerFile);\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore cleanup errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n});\n\n// ============================================================================\n// Agent tool path contract\n// ============================================================================\n\ndescribe(\"bash tool — agent tool path contract\", () => {\n\tit(\"simple command is not labeled pipeline\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_contract_simple\", {\n\t\t\tcommand: \"echo 'NOT-A-PIPELINE'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"real pipeline is marked non-authoritative\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_contract_pipeline\", {\n\t\t\tcommand: \"(exit 7) | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t});\n\n\tit(\"no wrapper source leaks into stdout or stderr\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_noleak\", {\n\t\t\tcommand: \"printf 'CLEAN\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).not.toContain(\"__jensen_stages\");\n\t\texpect(text).not.toContain(\"PIPESTATUS\");\n\t\texpect(text).not.toContain(\"_PI_\");\n\t});\n\n\tit(\"timeout schema value reaches executor\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_h02_wired\", {\n\t\t\t\tcommand: \"sleep 3\",\n\t\t\t\ttimeout: 0.5,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t}\n\t});\n\n\tit(\"model-facing content contains all required evidence fields\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_evidence_fields\", {\n\t\t\tcommand: \"printf 'OK\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\t// Required fields\n\t\texpect(text).toContain(\"exit_code:\");\n\t\texpect(text).toContain(\"timed_out:\");\n\t\texpect(text).toContain(\"cancelled:\");\n\t\texpect(text).toContain(\"truncated:\");\n\t\texpect(text).toContain(\"stdout:\");\n\t});\n});\n\n// ============================================================================\n// Model-facing authority scope tests (M01-M12)\n// ============================================================================\n\ndescribe(\"bash tool — model-facing authority scope\", () => {\n\tit(\"M01: simple success exposes explicit authority scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m01\", {\n\t\t\tcommand: \"printf 'M01\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"M01\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"exit_status_known: true\");\n\t\texpect(text).toContain(\"exit_status_authoritative: true\");\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t\texpect(text).toContain(\"internal_command_statuses_known: false\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: true\");\n\t});\n\n\tit(\"M02: simple failure exposes exit 17 with authority scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_m02\", {\n\t\t\t\tcommand: \"bash -c 'exit 17'\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for exit 17\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"exit_code: 17\");\n\t\t\texpect(msg).toContain(\"exit_status_known: true\");\n\t\t\texpect(msg).toContain(\"exit_status_authoritative: true\");\n\t\t\texpect(msg).toContain(\"authority_scope: final_shell_exit_status\");\n\t\t}\n\t});\n\n\tit(\"M03: failure then success does not claim all commands passed\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m03\", {\n\t\t\tcommand: \"false; true\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"internal_command_statuses_known: false\");\n\t\t// The exit code is 0 but internal command statuses are unknown\n\t\t// Model must not claim \"all commands passed\"\n\t});\n\n\tit(\"M04: success then failure has non-zero exit with correct scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_m04\", {\n\t\t\t\tcommand: \"true; false\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown for exit 1\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"exit_code: 1\");\n\t\t\texpect(msg).toContain(\"authority_scope: final_shell_exit_status\");\n\t\t}\n\t});\n\n\tit(\"M05: function with internal failure has limited scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m05\", {\n\t\t\tcommand: \"sample() {\\n  false\\n  printf 'FUNCTION-END\\\\n'\\n}\\nsample\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"FUNCTION-END\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t\texpect(text).toContain(\"internal_command_statuses_known: false\");\n\t});\n\n\tit(\"M06: subshell with internal failure has final_shell_exit_status scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m06\", {\n\t\t\tcommand: \"(false; true)\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t\texpect(text).toContain(\"internal_command_statuses_known: false\");\n\t});\n\n\tit(\"M07: set -e failure produces non-zero exit with correct scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_m07\", {\n\t\t\t\tcommand: \"set -e\\nfalse\\nprintf 'UNREACHABLE\\\\n'\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"exit_code: 1\");\n\t\t\texpect(msg).toContain(\"authority_scope: final_shell_exit_status\");\n\t\t}\n\t});\n\n\tit(\"M08: recovery operator || produces authoritative exit with limited scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m08\", {\n\t\t\tcommand: \"false || printf 'RECOVERED\\\\n'\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"RECOVERED\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).not.toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"authority_scope: final_shell_exit_status\");\n\t});\n\n\tit(\"M09: pipeline is non-authoritative for validation\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m09\", {\n\t\t\tcommand: \"false | tail\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).toContain(\"authority_scope: final_pipeline_stage_only\");\n\t\texpect(text).toContain(\"internal_command_statuses_known: false\");\n\t\t// No fabricated stage codes\n\t\texpect(text).not.toContain(\"pipeline_stage_exit_codes\");\n\t});\n\n\tit(\"M10: pipeline with deceptive positive output is non-authoritative\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\tconst result = await tool.execute(\"call_m10\", {\n\t\t\tcommand: \"bash -c 'printf \\\"SUCCESS\\\\n\\\"; exit 9' | cat\",\n\t\t});\n\t\tconst text = (result.content[0] as { type: \"text\"; text: string }).text;\n\t\texpect(text).toContain(\"SUCCESS\");\n\t\texpect(text).toContain(\"exit_code: 0\");\n\t\texpect(text).toContain(\"validation_evidence_authoritative: false\");\n\t\texpect(text).toContain(\"pipeline_suspected: true\");\n\t});\n\n\tit(\"M11: timeout produces no_exit_status authority scope\", async () => {\n\t\tconst tool = createBashTool(process.cwd());\n\t\ttry {\n\t\t\tawait tool.execute(\"call_m11\", {\n\t\t\t\tcommand: \"sleep 5\",\n\t\t\t\ttimeout: 1,\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on timeout\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"timed_out: true\");\n\t\t\texpect(msg).toContain(\"exit_status_known: false\");\n\t\t\texpect(msg).toContain(\"exit_status_authoritative: false\");\n\t\t\texpect(msg).toContain(\"authority_scope: no_exit_status\");\n\t\t\texpect(msg).toContain(\"internal_command_statuses_known: false\");\n\t\t\texpect(msg).toContain(\"validation_evidence_authoritative: false\");\n\t\t}\n\t});\n\n\tit(\"M12: spawn error produces no_process_started authority scope\", async () => {\n\t\tconst operations: BashOperations = {\n\t\t\texec: async () => {\n\t\t\t\tthrow new Error(\"ENOENT: nonexistent command\");\n\t\t\t},\n\t\t};\n\n\t\tconst tool = createBashTool(process.cwd(), { operations });\n\t\ttry {\n\t\t\tawait tool.execute(\"call_m12\", {\n\t\t\t\tcommand: \"nonexistent_command\",\n\t\t\t});\n\t\t\texpect.fail(\"should have thrown on spawn error\");\n\t\t} catch (err) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\texpect(msg).toContain(\"spawn_error:\");\n\t\t\texpect(msg).toContain(\"exit_status_known: false\");\n\t\t\texpect(msg).toContain(\"exit_status_authoritative: false\");\n\t\t\texpect(msg).toContain(\"authority_scope: no_process_started\");\n\t\t\texpect(msg).toContain(\"internal_command_statuses_known: false\");\n\t\t\texpect(msg).toContain(\"validation_evidence_authoritative: false\");\n\t\t}\n\t});\n});\n\n// ============================================================================\n// Compile-compatibility fixture: consumers written against the 1.1.6 public\n// surface must continue to compile against the current types.\n// ============================================================================\n\nimport type { BashResult } from \"../bash-executor.js\";\nimport type { BashToolDetails } from \"./bash.js\";\n\ndescribe(\"compile-compatibility with 1.1.6 surface\", () => {\n\tit(\"accepts BashToolDetails constructed with only 1.1.6 fields\", () => {\n\t\t// Simulates a consumer that builds details from the 1.1.6 contract.\n\t\t// Must not require any of the new fields.\n\t\tconst oldDetails: BashToolDetails = {\n\t\t\ttruncation: undefined,\n\t\t\tfullOutputPath: undefined,\n\t\t};\n\n\t\t// Also accepts omitted cancelled (was optional in 1.1.6)\n\t\tconst oldDetailsNoCancel: BashToolDetails = {\n\t\t\ttruncation: undefined,\n\t\t\tfullOutputPath: undefined,\n\t\t};\n\n\t\texpect(oldDetails.truncation).toBeUndefined();\n\t\texpect(oldDetailsNoCancel.cancelled).toBeUndefined();\n\t});\n\n\tit(\"accepts BashResult constructed with only 1.1.6 fields\", () => {\n\t\t// Simulates a consumer that builds results from the 1.1.6 contract.\n\t\tconst oldResult: BashResult = {\n\t\t\toutput: \"\",\n\t\t\texitCode: 0,\n\t\t\tcancelled: false,\n\t\t\ttruncated: false,\n\t\t};\n\n\t\t// Also accepts fullOutputPath omitted (was optional in 1.1.6)\n\t\tconst oldResultNoPath: BashResult = {\n\t\t\toutput: \"hello\",\n\t\t\texitCode: undefined,\n\t\t\tcancelled: true,\n\t\t\ttruncated: true,\n\t\t};\n\n\t\texpect(oldResult.output).toBe(\"\");\n\t\texpect(oldResult.exitCode).toBe(0);\n\t\texpect(oldResultNoPath.output).toBe(\"hello\");\n\t\texpect(oldResultNoPath.exitCode).toBeUndefined();\n\t});\n\n\tit(\"new consumers can access all new fields on BashResult\", () => {\n\t\tconst fullResult: BashResult = {\n\t\t\toutput: \"test\",\n\t\t\texitCode: 0,\n\t\t\tcancelled: false,\n\t\t\ttruncated: false,\n\t\t\tstdout: \"test\",\n\t\t\tstderr: \"\",\n\t\t\ttimedOut: false,\n\t\t\tstartedAt: \"2024-01-01T00:00:00.000Z\",\n\t\t\tfinishedAt: \"2024-01-01T00:00:00.100Z\",\n\t\t\tevidence: {\n\t\t\t\texitStatusKnown: true,\n\t\t\t\texitStatusAuthoritative: true,\n\t\t\t\tauthorityScope: \"final_shell_exit_status\",\n\t\t\t\tinternalCommandStatusesKnown: false,\n\t\t\t\tvalidationEvidenceAuthoritative: true,\n\t\t\t\tpipelineSuspected: false,\n\t\t\t\tstageExitCodesKnown: false,\n\t\t\t\tfinalShellExitCode: 0,\n\t\t\t},\n\t\t};\n\n\t\texpect(fullResult.stdout).toBe(\"test\");\n\t\texpect(fullResult.timedOut).toBe(false);\n\t\texpect(fullResult.evidence?.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n\n\tit(\"new consumers can access all new fields on BashToolDetails\", () => {\n\t\tconst fullDetails: BashToolDetails = {\n\t\t\tcommand: \"echo hello\",\n\t\t\tcwd: \"/tmp\",\n\t\t\tstdout: \"hello\",\n\t\t\tstderr: \"\",\n\t\t\texitCode: 0,\n\t\t\tstartedAt: \"2024-01-01T00:00:00.000Z\",\n\t\t\tfinishedAt: \"2024-01-01T00:00:00.100Z\",\n\t\t\ttimedOut: false,\n\t\t\tcancelled: false,\n\t\t\ttruncated: false,\n\t\t\tevidence: {\n\t\t\t\texitStatusKnown: true,\n\t\t\t\texitStatusAuthoritative: true,\n\t\t\t\tauthorityScope: \"final_shell_exit_status\",\n\t\t\t\tinternalCommandStatusesKnown: false,\n\t\t\t\tvalidationEvidenceAuthoritative: true,\n\t\t\t\tpipelineSuspected: false,\n\t\t\t\tstageExitCodesKnown: false,\n\t\t\t\tfinalShellExitCode: 0,\n\t\t\t},\n\t\t};\n\n\t\texpect(fullDetails.command).toBe(\"echo hello\");\n\t\texpect(fullDetails.cancelled).toBe(false);\n\t\texpect(fullDetails.evidence?.exitStatusKnown).toBe(true);\n\t});\n});\n\n// ============================================================================\n// Runtime compatibility: verify that real executions always produce every\n// new field even though they are optional in the public type.\n// ============================================================================\n\nimport { executeBash, type ResolvedBashResult } from \"../bash-executor.js\";\n\ndescribe(\"runtime always produces all new BashResult fields\", () => {\n\tit(\"simple success\", async () => {\n\t\tconst result = (await executeBash(\"echo hello\")) as ResolvedBashResult;\n\t\texpect(result.stdout).toBeDefined();\n\t\texpect(result.stderr).toBeDefined();\n\t\texpect(result.timedOut).toBe(false);\n\t\texpect(result.startedAt).toBeDefined();\n\t\texpect(result.finishedAt).toBeDefined();\n\t\texpect(result.evidence).toBeDefined();\n\t\texpect(result.evidence.exitStatusKnown).toBe(true);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n\n\tit(\"simple failure\", async () => {\n\t\tconst result = (await executeBash(\"exit 42\")) as ResolvedBashResult;\n\t\texpect(result.stdout).toBeDefined();\n\t\texpect(result.stderr).toBeDefined();\n\t\texpect(result.exitCode).toBe(42);\n\t\texpect(result.timedOut).toBe(false);\n\t\texpect(result.cancelled).toBe(false);\n\t\texpect(result.evidence).toBeDefined();\n\t\texpect(result.evidence.exitStatusKnown).toBe(true);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n\n\tit(\"compound command\", async () => {\n\t\tconst result = (await executeBash(\"echo a; echo b\")) as ResolvedBashResult;\n\t\texpect(result.stdout).toBeDefined();\n\t\texpect(result.evidence).toBeDefined();\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(true);\n\t});\n\n\tit(\"pipeline: evidence is non-authoritative\", async () => {\n\t\tconst result = (await executeBash(\"echo hello | cat\")) as ResolvedBashResult;\n\t\texpect(result.stdout).toContain(\"hello\");\n\t\texpect(result.evidence).toBeDefined();\n\t\texpect(result.evidence.pipelineSuspected).toBe(true);\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(false);\n\t});\n\n\tit(\"timeout produces timedOut: true\", async () => {\n\t\tconst result = (await executeBash(\"sleep 5\", { timeout: 1 })) as ResolvedBashResult;\n\t\texpect(result.timedOut).toBe(true);\n\t\texpect(result.evidence).toBeDefined();\n\t\texpect(result.evidence.exitStatusKnown).toBe(false);\n\t\texpect(result.evidence.authorityScope).toBe(\"no_exit_status\");\n\t});\n\n\tit(\"truncation\", async () => {\n\t\t// Produce enough output to trigger truncation\n\t\tconst lines = Array.from({ length: 1000 }, (_, i) => `line ${i}`).join(\"\\n\");\n\t\tconst result = (await executeBash(`printf '%s' \"${lines}\"`)) as ResolvedBashResult;\n\t\t// Large output may or may not truncate depending on DEFAULT_MAX_BYTES/DEFAULT_MAX_LINES\n\t\texpect(result.truncated).toBeDefined();\n\t\texpect(result.evidence).toBeDefined();\n\t});\n});\n"]}