{"version":3,"file":"cross-platform-harness.test.d.ts","sourceRoot":"","sources":["../../src/core/cross-platform-harness.test.ts"],"names":[],"mappings":"","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { mkdtempSync, rmSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { join } from \"path\";\nimport { describe, expect, it } from \"vitest\";\nimport { getPowerShellConfig, resetShellConfigCache } from \"../utils/shell.js\";\nimport { executeBash, type ResolvedBashResult } from \"./bash-executor.js\";\nimport { runDoctorChecks } from \"./doctor.js\";\nimport { buildExecutionEnvironment, parseWorktreeList } from \"./footer-data-provider.js\";\nimport { buildSystemPrompt } from \"./system-prompt.js\";\nimport { getToolPrompt } from \"./tools/tools-prompt-data.js\";\n\n// ============================================================================\n// porcelain parsing tests — unit tests against known fixture data\n// ============================================================================\n\ndescribe(\"worktree porcelain parser\", () => {\n\t// Test the internal parseWorktreePorcelain via a temp git repo.\n\t// parseWorktreeList wraps spawnSync; we validate against real git.\n\n\tit(\"parses real git worktree output for current repo\", () => {\n\t\tconst result = parseWorktreeList(process.cwd());\n\t\texpect(Array.isArray(result)).toBe(true);\n\t\texpect(result.length).toBeGreaterThanOrEqual(0);\n\t\tfor (const entry of result) {\n\t\t\texpect(typeof entry.path).toBe(\"string\");\n\t\t\texpect(typeof entry.head).toBe(\"string\");\n\t\t\texpect(entry.head.length).toBe(40);\n\t\t\texpect(typeof entry.locked).toBe(\"boolean\");\n\t\t\texpect(typeof entry.prunable).toBe(\"boolean\");\n\t\t}\n\t});\n\n\tit(\"returns empty array when git is unavailable\", () => {\n\t\tconst result = parseWorktreeList(\"/dev/null/nonexistent\");\n\t\texpect(result).toEqual([]);\n\t});\n});\n\n// ============================================================================\n// execution environment tests\n// ============================================================================\n\ndescribe(\"buildExecutionEnvironment\", () => {\n\tit(\"returns host, os, loginShell on Linux\", () => {\n\t\tconst env = buildExecutionEnvironment(\"/home/user/test-repo\");\n\t\texpect(env.host).toBeTruthy();\n\t\texpect(env.host.length).toBeGreaterThan(0);\n\t\texpect(env.os).toBe(\"Linux\");\n\t\texpect(env.loginShell).toBeTruthy();\n\t\texpect(typeof env.loginShell).toBe(\"string\");\n\t});\n\n\tit(\"captures initialCwd separately from effectiveCwd\", () => {\n\t\tconst env = buildExecutionEnvironment(\"/home/user/test-repo\");\n\t\texpect(env.initialCwd).toBe(\"/home/user/test-repo\");\n\t\texpect(typeof env.effectiveCwd).toBe(\"string\");\n\t\texpect(env.effectiveCwd.length).toBeGreaterThan(0);\n\t});\n\n\tit(\"distinguishes initialCwd from effectiveCwd when they differ\", () => {\n\t\tconst env = buildExecutionEnvironment(\"/tmp/some-other-path\");\n\t\texpect(env.initialCwd).toBe(\"/tmp/some-other-path\");\n\t\texpect(env.initialCwd).not.toBe(env.effectiveCwd);\n\t\texpect(typeof env.gitRoot === \"string\" || env.gitRoot === null).toBe(true);\n\t});\n\n\tit(\"controllerGitRoot is null when initialCwd === effectiveCwd\", () => {\n\t\tconst env = buildExecutionEnvironment(process.cwd());\n\t\texpect(env.controllerGitRoot).toBeNull();\n\t});\n\n\tit(\"controllerGitRoot is populated when initialCwd is a different git repo\", () => {\n\t\t// Use /tmp as initialCwd (not a git repo) — controllerGitRoot should remain null\n\t\tconst env = buildExecutionEnvironment(\"/tmp\");\n\t\texpect(env.controllerGitRoot).toBeNull();\n\t});\n\n\tit(\"G01: reports branch name and git root when on a real branch\", () => {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"jensen-git-branch-\"));\n\t\ttry {\n\t\t\tspawnSync(\"git\", [\"init\", \"--initial-branch=main\"], { cwd: dir, stdio: \"ignore\" });\n\t\t\tspawnSync(\n\t\t\t\t\"git\",\n\t\t\t\t[\n\t\t\t\t\t\"-c\",\n\t\t\t\t\t\"user.name=Jensen Test\",\n\t\t\t\t\t\"-c\",\n\t\t\t\t\t\"user.email=jensen-test@example.invalid\",\n\t\t\t\t\t\"commit\",\n\t\t\t\t\t\"--allow-empty\",\n\t\t\t\t\t\"-m\",\n\t\t\t\t\t\"init\",\n\t\t\t\t],\n\t\t\t\t{ cwd: dir, stdio: \"ignore\" },\n\t\t\t);\n\t\t\tconst saved = process.cwd();\n\t\t\ttry {\n\t\t\t\tprocess.chdir(dir);\n\t\t\t\tconst env = buildExecutionEnvironment(saved);\n\t\t\t\texpect(env.gitRoot).toBeTruthy();\n\t\t\t\texpect(typeof env.gitBranch).toBe(\"string\");\n\t\t\t\texpect(env.gitBranch).toBe(\"main\");\n\t\t\t\texpect(env.isDetachedHead).toBe(false);\n\t\t\t} finally {\n\t\t\t\tprocess.chdir(saved);\n\t\t\t}\n\t\t} finally {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"G02: reports detached HEAD with null branch and detached flag\", () => {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"jensen-git-detached-\"));\n\t\ttry {\n\t\t\tspawnSync(\"git\", [\"init\", \"--initial-branch=main\"], { cwd: dir, stdio: \"ignore\" });\n\t\t\tspawnSync(\n\t\t\t\t\"git\",\n\t\t\t\t[\n\t\t\t\t\t\"-c\",\n\t\t\t\t\t\"user.name=Jensen Test\",\n\t\t\t\t\t\"-c\",\n\t\t\t\t\t\"user.email=jensen-test@example.invalid\",\n\t\t\t\t\t\"commit\",\n\t\t\t\t\t\"--allow-empty\",\n\t\t\t\t\t\"-m\",\n\t\t\t\t\t\"init\",\n\t\t\t\t],\n\t\t\t\t{ cwd: dir, stdio: \"ignore\" },\n\t\t\t);\n\t\t\tconst headSha = spawnSync(\"git\", [\"rev-parse\", \"HEAD\"], {\n\t\t\t\tcwd: dir,\n\t\t\t\tencoding: \"utf8\",\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t\t}).stdout.trim();\n\t\t\tspawnSync(\"git\", [\"checkout\", \"--detach\", headSha], { cwd: dir, stdio: \"ignore\" });\n\t\t\tconst saved = process.cwd();\n\t\t\ttry {\n\t\t\t\tprocess.chdir(dir);\n\t\t\t\tconst env = buildExecutionEnvironment(saved);\n\t\t\t\texpect(env.gitRoot).toBeTruthy();\n\t\t\t\texpect(env.gitBranch).toBeNull();\n\t\t\t\texpect(env.isDetachedHead).toBe(true);\n\t\t\t} finally {\n\t\t\t\tprocess.chdir(saved);\n\t\t\t}\n\t\t} finally {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"G03: reports null git info when not inside a repository\", () => {\n\t\tconst dir = mkdtempSync(join(tmpdir(), \"jensen-non-git-\"));\n\t\ttry {\n\t\t\tconst saved = process.cwd();\n\t\t\ttry {\n\t\t\t\tprocess.chdir(dir);\n\t\t\t\tconst env = buildExecutionEnvironment(saved);\n\t\t\t\texpect(env.gitRoot).toBeNull();\n\t\t\t\texpect(env.gitBranch).toBeNull();\n\t\t\t\texpect(env.isDetachedHead).toBe(false);\n\t\t\t} finally {\n\t\t\t\tprocess.chdir(saved);\n\t\t\t}\n\t\t} finally {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t}\n\t});\n\n\tit(\"never leaks sensitive data\", () => {\n\t\tconst env = buildExecutionEnvironment(\"/home/user/test-repo\");\n\t\tconst json = JSON.stringify(env);\n\t\texpect(json).not.toContain(\"API_KEY\");\n\t\texpect(json).not.toContain(\"TOKEN\");\n\t\texpect(json).not.toContain(\"SECRET\");\n\t\texpect(json).not.toContain(\"PASSWORD\");\n\t});\n\n\tit(\"detached HEAD detection produces boolean\", () => {\n\t\tconst env = buildExecutionEnvironment(process.cwd());\n\t\texpect(typeof env.isDetachedHead).toBe(\"boolean\");\n\t});\n\n\tit(\"worktreeCount is a non-negative number\", () => {\n\t\tconst env = buildExecutionEnvironment(process.cwd());\n\t\texpect(typeof env.worktreeCount).toBe(\"number\");\n\t\texpect(env.worktreeCount).toBeGreaterThanOrEqual(0);\n\t});\n\n\tit(\"handles SHELL unset gracefully\", () => {\n\t\tconst origShell = process.env.SHELL;\n\t\tdelete process.env.SHELL;\n\t\ttry {\n\t\t\tconst env = buildExecutionEnvironment(\"/tmp/test\");\n\t\t\texpect(env.loginShell).toBe(\"/bin/sh\");\n\t\t} finally {\n\t\t\tif (origShell) process.env.SHELL = origShell;\n\t\t}\n\t});\n\n\tit(\"loginShell is distinct from the bash tool shell\", () => {\n\t\tconst env = buildExecutionEnvironment(process.cwd());\n\t\t// loginShell comes from $SHELL (e.g., /usr/bin/zsh), but\n\t\t// the bash tool uses /bin/bash on Linux\n\t\texpect(typeof env.loginShell).toBe(\"string\");\n\t\t// loginShell should not be empty\n\t\texpect(env.loginShell.length).toBeGreaterThan(0);\n\t});\n});\n\n// ============================================================================\n// BashResult timestamp tests\n// ============================================================================\n\ndescribe(\"BashResult timestamps\", () => {\n\tit(\"includes startedAt and finishedAt on success\", async () => {\n\t\tconst result = (await executeBash(\"echo hello\")) as ResolvedBashResult;\n\t\texpect(typeof result.startedAt).toBe(\"string\");\n\t\texpect(typeof result.finishedAt).toBe(\"string\");\n\t\t// ISO 8601 format: starts with YYYY-MM-DD\n\t\texpect(result.startedAt).toMatch(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);\n\t\texpect(result.finishedAt).toMatch(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);\n\t\texpect(result.startedAt <= result.finishedAt).toBe(true);\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.cancelled).toBe(false);\n\t});\n\n\tit(\"includes timestamps on non-zero exit code\", async () => {\n\t\ttry {\n\t\t\tawait executeBash(\"exit 42\");\n\t\t} catch {\n\t\t\t// The tool throws on non-zero exit; we can't directly get the result\n\t\t\t// from executeBash when it throws. Tested indirectly via the tool layer.\n\t\t}\n\t});\n\n\tit(\"timestamps are ISO 8601 strings not Date objects\", async () => {\n\t\tconst result = (await executeBash(\"echo ts\")) as ResolvedBashResult;\n\t\t// Verify they're strings, not Date objects\n\t\texpect(typeof result.startedAt).toBe(\"string\");\n\t\texpect(typeof result.finishedAt).toBe(\"string\");\n\t\t// Verify they contain T separator (ISO format)\n\t\texpect(result.startedAt).toContain(\"T\");\n\t\texpect(result.finishedAt).toContain(\"T\");\n\t});\n\n\tit(\"finishedAt >= startedAt\", async () => {\n\t\tconst result = (await executeBash(\"echo timing\")) as ResolvedBashResult;\n\t\texpect(result.startedAt <= result.finishedAt).toBe(true);\n\t});\n});\n\n// ============================================================================\n// BashResult separation tests (stdout, stderr, timedOut, spawnError)\n// ============================================================================\n\ndescribe(\"BashResult stream separation\", () => {\n\tit(\"captures stdout only with exit 0\", async () => {\n\t\tconst result = (await executeBash(\"printf 'hello stdout'\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.stdout).toContain(\"hello stdout\");\n\t\texpect(result.stderr).toBe(\"\");\n\t\texpect(result.output).toContain(\"hello stdout\");\n\t\texpect(result.timedOut).toBe(false);\n\t\texpect(result.cancelled).toBe(false);\n\t\texpect(result.spawnError).toBeUndefined();\n\t});\n\n\tit(\"captures stderr only with exit 0\", async () => {\n\t\tconst result = (await executeBash(\"printf 'error message' >&2\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.stderr).toContain(\"error message\");\n\t\texpect(result.stdout).toBe(\"\");\n\t\texpect(result.timedOut).toBe(false);\n\t\texpect(result.cancelled).toBe(false);\n\t});\n\n\tit(\"captures simultaneous stdout and stderr\", async () => {\n\t\tconst result = (await executeBash(\"printf 'out'; printf 'err' >&2\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.stdout).toBe(\"out\");\n\t\texpect(result.stderr).toBe(\"err\");\n\t});\n\n\tit(\"captures stdout with non-zero exit\", async () => {\n\t\tconst result = (await executeBash(\"printf 'fail output' >&2; exit 3\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(3);\n\t\texpect(result.stderr).toContain(\"fail output\");\n\t});\n\n\tit(\"does not treat stderr as failure when exit 0\", async () => {\n\t\tconst result = (await executeBash(\"printf 'just noise' >&2; exit 0\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.stderr).toContain(\"just noise\");\n\t\texpect(result.stdout).toBe(\"\");\n\t});\n\n\tit(\"timestamps are present in all result states\", async () => {\n\t\tconst result = (await executeBash(\"true\")) as ResolvedBashResult;\n\t\texpect(result.startedAt).toMatch(/^\\d{4}-\\d{2}-\\d{2}T/);\n\t\texpect(result.finishedAt).toMatch(/^\\d{4}-\\d{2}-\\d{2}T/);\n\t\texpect(result.startedAt <= result.finishedAt).toBe(true);\n\t});\n\n\tit(\"timedOut is false on normal completion\", async () => {\n\t\tconst result = (await executeBash(\"true\")) as ResolvedBashResult;\n\t\texpect(result.timedOut).toBe(false);\n\t});\n\n\tit(\"spawnError is undefined on normal completion\", async () => {\n\t\tconst result = (await executeBash(\"true\")) as ResolvedBashResult;\n\t\texpect(result.spawnError).toBeUndefined();\n\t});\n});\n\n// ============================================================================\n// Bash evidence tests\n// ============================================================================\n\ndescribe(\"bash evidence\", () => {\n\tit(\"detects simple pipeline as non-authoritative\", async () => {\n\t\tconst result = (await executeBash(\"false | tail\")) as ResolvedBashResult;\n\t\texpect(result.evidence.pipelineSuspected).toBe(true);\n\t\t// No fd 3 control channel — stage codes are never known from untrusted channels\n\t\texpect(result.evidence.stageExitCodesKnown).toBe(false);\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(false);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_pipeline_stage_only\");\n\t\texpect(result.evidence.warning).toBeDefined();\n\t\texpect(result.evidence.warning).toContain(\"Do not use this result as authoritative validation\");\n\t});\n\n\tit(\"detects pipeline with grep as non-authoritative\", async () => {\n\t\tconst result = (await executeBash(\"printf 'ok\\n' | grep ok\")) as ResolvedBashResult;\n\t\texpect(result.evidence.pipelineSuspected).toBe(true);\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(false);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_pipeline_stage_only\");\n\t});\n\n\tit(\"non-pipeline command has explicit authority scope\", async () => {\n\t\tconst result = (await executeBash(\"echo hello\")) as ResolvedBashResult;\n\t\texpect(result.evidence.pipelineSuspected).toBe(false);\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.stdout).toContain(\"hello\");\n\t\texpect(result.evidence.exitStatusKnown).toBe(true);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(true);\n\t\texpect(result.evidence.internalCommandStatusesKnown).toBe(false);\n\t});\n\n\tit(\"non-pipeline exit 0 has explicit authority scope\", async () => {\n\t\tconst result = (await executeBash(\"true\")) as ResolvedBashResult;\n\t\texpect(result.evidence.pipelineSuspected).toBe(false);\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n\n\tit(\"pipeline exit code reflects last stage but is non-authoritative\", async () => {\n\t\tconst result = (await executeBash(\"false | grep anything\")) as ResolvedBashResult;\n\t\texpect(result.evidence.pipelineSuspected).toBe(true);\n\t\texpect(result.exitCode).toBe(1);\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(false);\n\t\texpect(result.evidence.finalShellExitCode).toBe(1);\n\t});\n\n\tit(\"compound command failure-then-success has final-shell-exit-status scope\", async () => {\n\t\tconst result = (await executeBash(\"false; true\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t\t// internal command status is not tracked\n\t\texpect(result.evidence.internalCommandStatusesKnown).toBe(false);\n\t});\n\n\tit(\"command with explicit exit 17 has authoritative evidence\", async () => {\n\t\tconst result = (await executeBash(\"bash -c 'exit 17'\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(17);\n\t\texpect(result.evidence.exitStatusAuthoritative).toBe(true);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t\texpect(result.evidence.validationEvidenceAuthoritative).toBe(true);\n\t});\n\n\tit(\"timeout has no_exit_status authority scope\", async () => {\n\t\tconst result = (await executeBash(\"sleep 5\", { timeout: 1 })) as ResolvedBashResult;\n\t\texpect(result.timedOut).toBe(true);\n\t\texpect(result.exitCode).toBeUndefined();\n\t\texpect(result.evidence.exitStatusKnown).toBe(false);\n\t\texpect(result.evidence.authorityScope).toBe(\"no_exit_status\");\n\t});\n\n\tit(\"function with internal failure has final-shell-exit-status scope\", async () => {\n\t\tconst result = (await executeBash(\"sample() { false; printf 'END\\\\n'; }; sample\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.stdout).toContain(\"END\");\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t\texpect(result.evidence.internalCommandStatusesKnown).toBe(false);\n\t});\n\n\tit(\"subshell with internal failure has final-shell-exit-status scope\", async () => {\n\t\tconst result = (await executeBash(\"(false; true)\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n\n\tit(\"recovery operator (||) has final-shell-exit-status scope\", async () => {\n\t\tconst result = (await executeBash(\"false || printf 'RECOVERED\\\\n'\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(0);\n\t\texpect(result.evidence.pipelineSuspected).toBe(false);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n\n\tit(\"success-then-failure has non-zero exit with correct scope\", async () => {\n\t\tconst result = (await executeBash(\"true; false\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(1);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t\texpect(result.evidence.exitStatusAuthoritative).toBe(true);\n\t});\n\n\tit(\"set -e failure has final-shell-exit-status scope\", async () => {\n\t\tconst result = (await executeBash(\"set -e; false; printf 'UNREACHABLE\\\\n'\")) as ResolvedBashResult;\n\t\texpect(result.exitCode).toBe(1);\n\t\texpect(result.evidence.authorityScope).toBe(\"final_shell_exit_status\");\n\t});\n});\n\n// ============================================================================\n// system prompt content tests\n// ============================================================================\n\ndescribe(\"system prompt execution environment\", () => {\n\tit(\"includes Execution environment section\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"Execution environment:\");\n\t});\n\n\tit(\"includes host field\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toMatch(/- host: \\S/);\n\t});\n\n\tit(\"includes operating system field\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toMatch(/- operating system: /);\n\t});\n\n\tit(\"includes login shell field (not plain shell)\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toMatch(/- login shell: \\//);\n\t});\n\n\tit(\"does not use ambiguous 'shell' field\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\t// Should not contain \"- shell:\" since we now use \"- login shell:\"\n\t\tconst lines = prompt.split(\"\\n\");\n\t\tconst shellLine = lines.find((l) => l.startsWith(\"- shell:\"));\n\t\texpect(shellLine).toBeUndefined();\n\t});\n\n\tit(\"includes working directory field\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toMatch(/- working directory: /);\n\t});\n\n\tit(\"includes git repository field\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toMatch(/- git repository: /);\n\t});\n});\n\ndescribe(\"system prompt evidence discipline\", () => {\n\tit(\"includes exit code discipline\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"Never declare a command succeeded without inspecting its exit code\");\n\t});\n\n\tit(\"includes evidence separation\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\n\t\t\t\"Treat stdout, stderr, exit code, timeout, cancellation, and truncation as separate pieces of evidence\",\n\t\t);\n\t});\n\n\tit(\"includes non-zero exit code is failure\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"non-zero exit code is a failure\");\n\t});\n\n\tit(\"includes stderr does not always mean failure\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"stderr alone does not mean failure\");\n\t});\n\n\tit(\"warns against conflating proposed with executed\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"Do not conflate a proposed command with an executed one\");\n\t});\n\n\tit(\"distinguishes exit code from all internal commands succeeded\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"exit code 0 does not prove every internal command succeeded\");\n\t});\n\n\tit(\"instructs not to claim all commands passed when statuses unknown\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"do not claim all internal commands passed\");\n\t});\n\n\tit(\"instructs to preserve process exit code\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain('exit \"$RC\"');\n\t});\n\n\tit(\"instructs to rerun without pipeline for validation\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"rerun the check without a pipeline\");\n\t});\n\n\tit(\"recommends single-command validation\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"Prefer direct single-command validation\");\n\t});\n});\n\ndescribe(\"system prompt command classification\", () => {\n\tit(\"includes SHORT/LONG_RUNNING/PERSISTENT\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"Classify commands before execution\");\n\t\texpect(prompt).toContain(\"SHORT\");\n\t\texpect(prompt).toContain(\"LONG_RUNNING\");\n\t\texpect(prompt).toContain(\"PERSISTENT\");\n\t});\n});\n\ndescribe(\"system prompt platform policy\", () => {\n\tit(\"includes Linux guidance on Linux\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).toContain(\"You are on Linux\");\n\t\texpect(prompt).toContain(\"Use the bash tool for all shell operations\");\n\t});\n\n\tit(\"does not include Windows guidance on Linux\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).not.toContain(\"Use the powershell tool for Windows-native workflows\");\n\t});\n});\n\ndescribe(\"system prompt legacy cwd removal\", () => {\n\tit(\"no standalone Current working directory line\", () => {\n\t\tconst prompt = buildSystemPrompt();\n\t\texpect(prompt).not.toContain(\"Current working directory:\");\n\t});\n});\n\n// ============================================================================\n// doctor worktree tests\n// ============================================================================\n\ndescribe(\"doctor worktree diagnostics\", () => {\n\tit(\"includes worktrees check in results\", async () => {\n\t\tconst result = await runDoctorChecks();\n\t\tconst worktreeCheck = result.checks.find((c) => c.name === \"worktrees\");\n\t\texpect(worktreeCheck).toBeDefined();\n\t\texpect([\"ok\", \"warn\", \"error\"]).toContain(worktreeCheck!.status);\n\t});\n\n\tit(\"worktree check has a message\", async () => {\n\t\tconst result = await runDoctorChecks();\n\t\tconst worktreeCheck = result.checks.find((c) => c.name === \"worktrees\");\n\t\texpect(worktreeCheck!.message.length).toBeGreaterThan(0);\n\t});\n\n\tit(\"skips gracefully outside a git repo\", async () => {\n\t\tconst result = await runDoctorChecks({ cwd: \"/tmp\" });\n\t\tconst worktreeCheck = result.checks.find((c) => c.name === \"worktrees\");\n\t\texpect(worktreeCheck).toBeDefined();\n\t\texpect(worktreeCheck!.status).toBe(\"ok\");\n\t\texpect(worktreeCheck!.message).toContain(\"Not a git repository\");\n\t});\n});\n\n// ============================================================================\n// tool prompt content tests\n// ============================================================================\n\ndescribe(\"bash tool prompt\", () => {\n\tit(\"includes command classification\", () => {\n\t\tconst prompt = getToolPrompt(\"bash\")!;\n\t\texpect(prompt).toContain(\"Classify commands before execution\");\n\t\texpect(prompt).toContain(\"SHORT\");\n\t\texpect(prompt).toContain(\"LONG_RUNNING\");\n\t\texpect(prompt).toContain(\"PERSISTENT\");\n\t});\n\n\tit(\"includes Linux platform guidance\", () => {\n\t\tconst prompt = getToolPrompt(\"bash\")!;\n\t\texpect(prompt).toContain(\"On Linux: use native Bash syntax\");\n\t});\n\n\tit(\"includes Windows Git Bash guidance\", () => {\n\t\tconst prompt = getToolPrompt(\"bash\")!;\n\t\texpect(prompt).toContain(\"On Windows via Git Bash\");\n\t});\n\n\tit(\"prohibits unowned background processes\", () => {\n\t\tconst prompt = getToolPrompt(\"bash\")!;\n\t\texpect(prompt).toContain(\"Do NOT use\");\n\t\texpect(prompt).toContain(\"nohup\");\n\t});\n\n\tit(\"emphasizes exit code checking\", () => {\n\t\tconst prompt = getToolPrompt(\"bash\")!;\n\t\texpect(prompt).toContain(\"non-zero exit code means the command failed\");\n\t});\n});\n\ndescribe(\"powershell tool prompt\", () => {\n\tit(\"includes SSH remote guidance\", () => {\n\t\tconst prompt = getToolPrompt(\"powershell\")!;\n\t\texpect(prompt).toContain(\"When executing PowerShell remotely via SSH\");\n\t\texpect(prompt).toContain(\"Do not send Bash syntax as the PowerShell payload\");\n\t});\n\n\tit(\"includes UTF-16LE encoding note\", () => {\n\t\tconst prompt = getToolPrompt(\"powershell\")!;\n\t\texpect(prompt).toContain(\"UTF-16LE\");\n\t\texpect(prompt).toContain(\"-EncodedCommand\");\n\t});\n\n\tit(\"includes tiered quoting guidance not simplistic rule\", () => {\n\t\tconst prompt = getToolPrompt(\"powershell\")!;\n\t\texpect(prompt).toContain(\"Avoid fragile multi-layer manual quoting\");\n\t\texpect(prompt).toContain(\"EncodedCommand\");\n\t\t// The outdated simplistic single/double quote rule should be gone\n\t\texpect(prompt).not.toContain(\"single quotes around the SSH command and double quotes inside\");\n\t});\n\n\tit(\"includes LASTEXITCODE guidance\", () => {\n\t\tconst prompt = getToolPrompt(\"powershell\")!;\n\t\texpect(prompt).toContain(\"$LASTEXITCODE\");\n\t\texpect(prompt).toContain(\"native executables\");\n\t});\n\n\tit(\"includes exit code propagation guidance\", () => {\n\t\tconst prompt = getToolPrompt(\"powershell\")!;\n\t\texpect(prompt).toContain(\"Propagate the remote exit code\");\n\t});\n});\n\n// ============================================================================\n// PowerShell discovery tests\n// ============================================================================\n\ndescribe(\"PowerShell discovery\", () => {\n\tit(\"JENSEN_PWSH_PATH env var takes priority over PATH\", () => {\n\t\t// Ensure clean cache\n\t\tresetShellConfigCache();\n\n\t\t// Without JENSEN_PWSH_PATH and without pwsh on PATH, getPowerShellConfig should throw\n\t\t// unless pwsh is actually installed\n\t\ttry {\n\t\t\tconst config = getPowerShellConfig();\n\t\t\t// If pwsh is available, just verify the config shape\n\t\t\texpect(typeof config.shell).toBe(\"string\");\n\t\t\texpect(config.shell.length).toBeGreaterThan(0);\n\t\t\texpect(Array.isArray(config.args)).toBe(true);\n\t\t\texpect([\"pwsh\", \"powershell\"]).toContain(config.flavor);\n\t\t} catch (err) {\n\t\t\t// Expected when pwsh is not available — this is fine\n\t\t\texpect(err).toBeDefined();\n\t\t}\n\n\t\tresetShellConfigCache();\n\t});\n\n\tit(\"does not scan HOME for pwsh fallback\", () => {\n\t\t// The production code must not contain HOME scan logic.\n\t\t// We verify this by checking the shell config resolution:\n\t\t// 1. It does not search $HOME/.local/powershell/pwsh\n\t\t// 2. It only uses PATH, explicit env var, or platform defaults\n\t\t// We validate by reading the source since env-based runtime tests\n\t\t// are unreliable across different machines.\n\t\tconst fs = require(\"node:fs\");\n\t\tconst shellSource = fs.readFileSync(require(\"node:path\").resolve(__dirname, \"..\", \"utils\", \"shell.ts\"), \"utf-8\");\n\t\t// The old HOME-scan code would contain join(HOME, \".local\", ...) or similar.\n\t\texpect(shellSource).not.toMatch(/\\.local.*powershell.*pwsh/);\n\t});\n});\n\n// ============================================================================\n// Doctor cache side-effects test\n// ============================================================================\n\ndescribe(\"doctor does not reset cache\", () => {\n\tit(\"doctor does not import resetShellConfigCache\", () => {\n\t\tconst fs = require(\"node:fs\");\n\t\tconst doctorSource = fs.readFileSync(require(\"node:path\").join(__dirname, \"doctor.ts\"), \"utf-8\");\n\t\t// Verify that resetShellConfigCache is NOT imported in doctor.ts\n\t\texpect(doctorSource).not.toContain(\"resetShellConfigCache\");\n\t});\n\n\tit(\"doctor pwsh check does not modify global shell cache\", async () => {\n\t\t// Set up a known cache state\n\t\tresetShellConfigCache();\n\n\t\t// Try to get the shell config first (populates cache if pwsh is available)\n\t\tlet beforeConfig: string | null = null;\n\t\ttry {\n\t\t\tbeforeConfig = getPowerShellConfig().shell;\n\t\t} catch {\n\t\t\t// pwsh not available — skip cache validation\n\t\t}\n\n\t\t// Run doctor\n\t\tawait runDoctorChecks();\n\n\t\t// After doctor, the cache should still be valid and unchanged\n\t\tif (beforeConfig) {\n\t\t\tconst afterConfig = getPowerShellConfig().shell;\n\t\t\texpect(afterConfig).toBe(beforeConfig);\n\t\t}\n\n\t\tresetShellConfigCache();\n\t});\n});\n"]}