oh-my-openagent 4.18.0 → 4.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/bin/AGENTS.md +33 -0
  8. package/dist/cli/index.js +500 -158
  9. package/dist/cli-node/index.js +500 -158
  10. package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
  11. package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
  12. package/dist/hooks/comment-checker/hook.d.ts +8 -1
  13. package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
  14. package/dist/index.js +956 -702
  15. package/dist/plugin/messages-transform.d.ts +1 -0
  16. package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
  17. package/dist/tui.js +43 -4
  18. package/package.json +16 -16
  19. package/packages/git-bash-mcp/dist/cli.js +81 -19
  20. package/packages/lsp-core/package.json +4 -0
  21. package/packages/lsp-core/src/index.ts +1 -0
  22. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  23. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  24. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  25. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  26. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  27. package/packages/lsp-core/src/lsp/client.ts +262 -80
  28. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  29. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  30. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
  31. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
  32. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  33. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  34. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  35. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  36. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  37. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  38. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  39. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  40. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  41. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  45. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  46. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  47. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  48. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  67. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  68. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  69. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  70. package/packages/lsp-core/src/mcp.ts +18 -7
  71. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  72. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  73. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  74. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  75. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  76. package/packages/lsp-core/src/request-context.test.ts +171 -0
  77. package/packages/lsp-core/src/request-context.ts +222 -9
  78. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  79. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  80. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  81. package/packages/lsp-core/src/tools/rename.ts +10 -15
  82. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  83. package/packages/lsp-core/src/tools/types.ts +2 -1
  84. package/packages/lsp-daemon/dist/cli.js +3330 -764
  85. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  86. package/packages/lsp-daemon/dist/client.js +5995 -0
  87. package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
  88. package/packages/lsp-daemon/dist/daemon-client.js +139 -32
  89. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  90. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  91. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
  92. package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
  93. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  94. package/packages/lsp-daemon/dist/index.js +3093 -786
  95. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  96. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  97. package/packages/lsp-daemon/dist/lock.js +14 -4
  98. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  99. package/packages/lsp-daemon/dist/ownership.js +168 -0
  100. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  101. package/packages/lsp-daemon/dist/paths.js +72 -33
  102. package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
  103. package/packages/lsp-daemon/dist/proxy.js +123 -16
  104. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  105. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  106. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  107. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  108. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  109. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  110. package/packages/lsp-daemon/package.json +12 -3
  111. package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  113. package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
  114. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  115. package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
  116. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  117. package/packages/omo-codex/plugin/.mcp.json +2 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  123. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
  124. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
  125. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
  126. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
  127. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  128. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  129. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  132. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
  134. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  135. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
  136. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  137. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  138. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  139. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  140. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  141. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  142. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  143. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  144. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  145. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  146. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  147. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  148. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  149. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  150. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  151. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  152. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  153. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  154. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  155. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
  156. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  157. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
  158. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  159. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  162. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
  163. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  164. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  165. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
  166. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
  167. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
  168. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
  169. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  170. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  171. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  172. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  173. package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
  177. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
  178. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
  179. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
  180. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  181. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  182. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  184. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
  185. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
  186. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  199. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  200. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  201. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  202. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  203. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  204. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  205. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  206. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  207. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  208. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  209. package/packages/omo-codex/plugin/package-lock.json +26 -14
  210. package/packages/omo-codex/plugin/package.json +1 -1
  211. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  212. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  213. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
  214. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
  215. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
  216. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
  217. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  218. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  219. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  220. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  221. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  222. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  223. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
  224. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
  225. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
  226. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -0,0 +1,3154 @@
1
+ #!/usr/bin/env bash
2
+ # lsp-e2e.sh - isolated live OpenCode QA for the shared OMO LSP daemon.
3
+ #
4
+ # Normal mode loads this worktree's OMO plugin into disposable XDG/HOME state,
5
+ # drives a real `opencode serve` with a local fake Responses provider, observes
6
+ # the event stream, and requires an actual completed LSP MCP tool call for the
7
+ # requested scenario.
8
+ #
9
+ # Usage:
10
+ # lsp-e2e.sh --scenario <name> --evidence-dir <absolute-dir>
11
+ # lsp-e2e.sh --self-test
12
+
13
+ set -uo pipefail
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
16
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd -P)"
17
+
18
+ SCENARIO=""
19
+ EVIDENCE_DIR=""
20
+ SELF_TEST=0
21
+ SANDBOX_ROOT=""
22
+ OMO_TEST_ROOT=""
23
+ EXPECTED_DAEMON_CLI=""
24
+ EXPECTED_DAEMON_VERSION=""
25
+ OPENCODE_PID=""
26
+ FAKE_PID=""
27
+ SSE_PID=""
28
+ RESULT_STAGE=""
29
+ CLEANUP_RUNNING=0
30
+ NORMAL_CLEANUP_COMPLETE=0
31
+ REAL_HOME="${HOME:-}"
32
+ REAL_OMO_ROOT="${HOME:-}/.omo/lsp-daemon"
33
+ REAL_DB_PATH=""
34
+ REAL_DB_COUNT_BEFORE=""
35
+ REAL_OMO_BEFORE_HASH=""
36
+ HEALTH_READY_SECONDS=30
37
+ HEALTH_CURL_CONNECT_TIMEOUT_SECONDS=1
38
+ HEALTH_CURL_MAX_TIME_SECONDS=1
39
+ SSE_READY_SECONDS=30
40
+ SSE_ATTEMPT_SECONDS=2
41
+ BUILD_LOCK_DIR=""
42
+ SOURCE_PACKAGE_STAMP=""
43
+ SOURCE_PACKAGE_STAMP_CREATED=0
44
+ CANCELLATION_SMOKE_RELATIVE="packages/lsp-daemon/scripts/qa/cancellation-smoke.mjs"
45
+ COMMIT_BARRIER_SMOKE_RELATIVE="packages/lsp-daemon/scripts/qa/commit-barrier-smoke.mjs"
46
+
47
+ log() { printf '[opencode-lsp-e2e] %s\n' "$*" >&2; }
48
+ fail() { log "FAIL: $*"; return 1; }
49
+
50
+ usage() {
51
+ sed -n '2,11p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
52
+ }
53
+
54
+ parse_args() {
55
+ while [ "$#" -gt 0 ]; do
56
+ case "$1" in
57
+ --scenario)
58
+ [ "$#" -ge 2 ] || { log "--scenario requires a value"; return 2; }
59
+ [ -z "$SCENARIO" ] || { log "--scenario may be provided only once"; return 2; }
60
+ SCENARIO="$2"
61
+ shift 2
62
+ ;;
63
+ --evidence-dir)
64
+ [ "$#" -ge 2 ] || { log "--evidence-dir requires a directory"; return 2; }
65
+ [ -z "$EVIDENCE_DIR" ] || { log "--evidence-dir may be provided only once"; return 2; }
66
+ EVIDENCE_DIR="$2"
67
+ shift 2
68
+ ;;
69
+ --self-test)
70
+ [ "$SELF_TEST" -eq 0 ] || { log "--self-test may be provided only once"; return 2; }
71
+ SELF_TEST=1
72
+ shift
73
+ ;;
74
+ -h|--help)
75
+ usage
76
+ exit 0
77
+ ;;
78
+ *)
79
+ log "unknown option: $1"
80
+ return 2
81
+ ;;
82
+ esac
83
+ done
84
+
85
+ if [ "$SELF_TEST" -eq 1 ]; then
86
+ if [ -n "$SCENARIO" ] || [ -n "$EVIDENCE_DIR" ]; then
87
+ log "--self-test cannot be combined with normal-mode options"
88
+ return 2
89
+ fi
90
+ return 0
91
+ fi
92
+
93
+ [ -n "$SCENARIO" ] || { log "--scenario is required"; return 2; }
94
+ [ -n "$EVIDENCE_DIR" ] || { log "--evidence-dir is required"; return 2; }
95
+ if ! printf '%s' "$SCENARIO" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'; then
96
+ log "invalid scenario: $SCENARIO"
97
+ return 2
98
+ fi
99
+ case "$EVIDENCE_DIR" in
100
+ /*) ;;
101
+ *) log "--evidence-dir must be absolute"; return 2 ;;
102
+ esac
103
+ }
104
+
105
+ require_bins() {
106
+ local missing=0 bin
107
+ for bin in "$@"; do
108
+ if ! command -v "$bin" >/dev/null 2>&1; then
109
+ log "missing dependency: $bin"
110
+ missing=1
111
+ fi
112
+ done
113
+ [ "$missing" -eq 0 ]
114
+ }
115
+
116
+ verify_tracked_cancellation_probes() {
117
+ local dependency ignored_evidence_root=".omo""/evidence"
118
+ if grep -Fq "$ignored_evidence_root" "${BASH_SOURCE[0]}"; then
119
+ fail "LSP QA driver references ignored evidence state"
120
+ return 1
121
+ fi
122
+ for dependency in "$CANCELLATION_SMOKE_RELATIVE" "$COMMIT_BARRIER_SMOKE_RELATIVE"; do
123
+ [ -f "$REPO_ROOT/$dependency" ] || { fail "missing cancellation QA dependency: $dependency"; return 1; }
124
+ git -C "$REPO_ROOT" ls-files --error-unmatch -- "$dependency" >/dev/null 2>&1 || {
125
+ fail "cancellation QA dependency is not tracked: $dependency"
126
+ return 1
127
+ }
128
+ done
129
+ }
130
+
131
+ hash_path() {
132
+ node --input-type=module - "$1" <<'NODE'
133
+ import { createHash } from "node:crypto";
134
+ import { lstatSync, readFileSync, readlinkSync, readdirSync } from "node:fs";
135
+ import { basename, join } from "node:path";
136
+
137
+ const target = process.argv[2];
138
+ const hash = createHash("sha256");
139
+
140
+ function visit(path, relative) {
141
+ const stat = lstatSync(path);
142
+ const kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : stat.isSymbolicLink() ? "link" : "special";
143
+ hash.update(`${kind}\0${relative}\0${stat.mode & 0o7777}\0`);
144
+ if (kind === "file") hash.update(readFileSync(path));
145
+ if (kind === "link") hash.update(readlinkSync(path));
146
+ if (kind === "dir") {
147
+ for (const name of readdirSync(path).sort()) visit(join(path, name), relative ? `${relative}/${name}` : name);
148
+ }
149
+ }
150
+
151
+ try {
152
+ visit(target, basename(target));
153
+ process.stdout.write(hash.digest("hex"));
154
+ } catch (error) {
155
+ if (error && error.code === "ENOENT") process.stdout.write("ABSENT");
156
+ else throw error;
157
+ }
158
+ NODE
159
+ }
160
+
161
+ run_bounded() {
162
+ local seconds="$1" output="$2"
163
+ shift 2
164
+ node --input-type=module - "$seconds" "$output" "$@" <<'NODE'
165
+ import { closeSync, openSync } from "node:fs";
166
+ import { spawn } from "node:child_process";
167
+
168
+ const [secondsRaw, output, command, ...args] = process.argv.slice(2);
169
+ const seconds = Number(secondsRaw);
170
+ if (!Number.isFinite(seconds) || seconds <= 0 || !command) process.exit(125);
171
+ const fd = openSync(output, "w");
172
+ const child = spawn(command, args, {
173
+ stdio: ["ignore", fd, fd],
174
+ detached: process.platform !== "win32",
175
+ env: process.env,
176
+ });
177
+ let timedOut = false;
178
+ let forceTimer;
179
+ const timer = setTimeout(() => {
180
+ timedOut = true;
181
+ try {
182
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGTERM");
183
+ else child.kill("SIGTERM");
184
+ } catch {}
185
+ forceTimer = setTimeout(() => {
186
+ try {
187
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL");
188
+ else child.kill("SIGKILL");
189
+ } catch {}
190
+ }, 3000);
191
+ }, seconds * 1000);
192
+ child.on("error", () => {
193
+ clearTimeout(timer);
194
+ if (forceTimer) clearTimeout(forceTimer);
195
+ closeSync(fd);
196
+ process.exit(126);
197
+ });
198
+ child.on("exit", (code, signal) => {
199
+ clearTimeout(timer);
200
+ if (forceTimer) clearTimeout(forceTimer);
201
+ closeSync(fd);
202
+ if (timedOut) process.exit(124);
203
+ if (typeof code === "number") process.exit(code);
204
+ process.exit(signal ? 128 : 1);
205
+ });
206
+ NODE
207
+ }
208
+
209
+ with_shared_build_lock() {
210
+ local command_name="$1" attempts=0 rc
211
+ shift
212
+ BUILD_LOCK_DIR="$REPO_ROOT/.omo/locks/lsp-daemon-build.lock"
213
+ mkdir -p "$(dirname "$BUILD_LOCK_DIR")"
214
+ while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
215
+ [ "$attempts" -lt 600 ] || { fail "timed out waiting for shared LSP daemon build lock"; return 1; }
216
+ sleep 0.2
217
+ attempts=$((attempts + 1))
218
+ done
219
+ printf 'pid=%s\ncommand=%s\n' "$$" "$command_name" >"$BUILD_LOCK_DIR/owner.txt"
220
+ "$@"
221
+ rc=$?
222
+ rm -rf "$BUILD_LOCK_DIR"
223
+ BUILD_LOCK_DIR=""
224
+ return "$rc"
225
+ }
226
+
227
+ safe_rm_tree() {
228
+ local path="$1" attempt=0
229
+ [ -n "$path" ] || return 0
230
+ case "$path" in
231
+ /var/folders/*/T/oqa-lsp-e2e.*|/tmp/oqa-lsp-e2e.*|/private/tmp/oqa-lsp-e2e.*)
232
+ while [ -e "$path" ] && [ "$attempt" -lt 100 ]; do
233
+ rm -rf "$path" 2>/dev/null || true
234
+ [ ! -e "$path" ] && return 0
235
+ sleep 0.1
236
+ attempt=$((attempt + 1))
237
+ done
238
+ [ ! -e "$path" ] || fail "isolated sandbox remained after bounded cleanup: $path"
239
+ ;;
240
+ *)
241
+ fail "refusing to remove unexpected sandbox path: $path"
242
+ ;;
243
+ esac
244
+ }
245
+
246
+ process_command() {
247
+ /bin/ps -p "$1" -o command= 2>/dev/null || true
248
+ }
249
+
250
+ wait_for_exit() {
251
+ local pid="$1" attempts=0
252
+ while [ "$attempts" -lt 50 ]; do
253
+ kill -0 "$pid" 2>/dev/null || return 0
254
+ sleep 0.1
255
+ attempts=$((attempts + 1))
256
+ done
257
+ return 1
258
+ }
259
+
260
+ stop_verified_pid() {
261
+ local pid="$1" expected="$2" label="$3" command
262
+ [ -n "$pid" ] || return 0
263
+ kill -0 "$pid" 2>/dev/null || return 0
264
+ command="$(process_command "$pid")"
265
+ case "$command" in
266
+ *"$expected"*) ;;
267
+ *) fail "refusing to stop unverified $label pid $pid"; return 1 ;;
268
+ esac
269
+ kill "$pid" 2>/dev/null || true
270
+ if ! wait_for_exit "$pid"; then
271
+ command="$(process_command "$pid")"
272
+ case "$command" in
273
+ *"$expected"*) kill -9 "$pid" 2>/dev/null || true ;;
274
+ *) fail "$label pid $pid changed identity during cleanup"; return 1 ;;
275
+ esac
276
+ wait_for_exit "$pid" || { fail "$label pid $pid survived cleanup"; return 1; }
277
+ fi
278
+ wait "$pid" 2>/dev/null || true
279
+ }
280
+
281
+ find_daemon_pid_file() {
282
+ [ -n "$OMO_TEST_ROOT" ] && [ -d "$OMO_TEST_ROOT" ] || return 0
283
+ find "$OMO_TEST_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1
284
+ }
285
+
286
+ stop_known_daemon() {
287
+ local pid_file pid command
288
+ pid_file="$(find_daemon_pid_file)"
289
+ [ -n "$pid_file" ] || return 0
290
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
291
+ case "$pid" in
292
+ ''|*[!0-9]*) fail "daemon pid file is malformed: $pid_file"; return 1 ;;
293
+ esac
294
+ kill -0 "$pid" 2>/dev/null || return 0
295
+ command="$(process_command "$pid")"
296
+ case "$command" in
297
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
298
+ *) fail "refusing to stop unverified daemon pid $pid"; return 1 ;;
299
+ esac
300
+ kill "$pid" 2>/dev/null || true
301
+ if ! wait_for_exit "$pid"; then
302
+ command="$(process_command "$pid")"
303
+ case "$command" in
304
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) kill -9 "$pid" 2>/dev/null || true ;;
305
+ *) fail "daemon pid $pid changed identity during cleanup"; return 1 ;;
306
+ esac
307
+ wait_for_exit "$pid" || { fail "daemon pid $pid survived cleanup"; return 1; }
308
+ fi
309
+ }
310
+
311
+ owned_sandbox_pids() {
312
+ [ -n "$SANDBOX_ROOT" ] || return 0
313
+ /bin/ps ax -o pid=,command= 2>/dev/null | while read -r pid command; do
314
+ case "$command" in
315
+ *"$SANDBOX_ROOT"*) [ "$pid" = "$$" ] || printf '%s\n' "$pid" ;;
316
+ esac
317
+ done
318
+ }
319
+
320
+ stop_owned_sandbox_processes() {
321
+ local pids pid command
322
+ pids="$(owned_sandbox_pids)"
323
+ [ -n "$pids" ] || return 0
324
+ for pid in $pids; do
325
+ kill -0 "$pid" 2>/dev/null || continue
326
+ command="$(process_command "$pid")"
327
+ case "$command" in
328
+ *"$SANDBOX_ROOT"*) ;;
329
+ *) fail "sandbox process $pid changed identity before cleanup"; return 1 ;;
330
+ esac
331
+ kill "$pid" 2>/dev/null || true
332
+ if ! wait_for_exit "$pid"; then
333
+ command="$(process_command "$pid")"
334
+ case "$command" in
335
+ *"$SANDBOX_ROOT"*) kill -9 "$pid" 2>/dev/null || true ;;
336
+ *) fail "sandbox process $pid changed identity during cleanup"; return 1 ;;
337
+ esac
338
+ wait_for_exit "$pid" || { fail "sandbox process $pid survived cleanup"; return 1; }
339
+ fi
340
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
341
+ printf 'sandbox_process_pid=%s alive_after=no\n' "$pid" >>"$EVIDENCE_DIR/owned-process-cleanup.txt"
342
+ fi
343
+ done
344
+ }
345
+
346
+ stop_owned_real_daemon_leak() {
347
+ [ "$REAL_OMO_BEFORE_HASH" = "ABSENT" ] || return 0
348
+ [ "$OMO_TEST_ROOT" != "$REAL_OMO_ROOT" ] || return 0
349
+ [ -d "$REAL_OMO_ROOT" ] || return 0
350
+ local pid_file pid command state_dir
351
+ pid_file="$(find "$REAL_OMO_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1)"
352
+ if [ -n "$pid_file" ]; then
353
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
354
+ case "$pid" in
355
+ ''|*[!0-9]*) fail "real-root leak pid file is malformed"; return 1 ;;
356
+ esac
357
+ command="$(process_command "$pid")"
358
+ case "$command" in
359
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
360
+ *) fail "real OMO root changed by an unverified process; preserving it"; return 1 ;;
361
+ esac
362
+ kill "$pid" 2>/dev/null || true
363
+ wait_for_exit "$pid" || { fail "own leaked daemon did not stop"; return 1; }
364
+ fi
365
+ if find "$REAL_OMO_ROOT" -type f \( -name daemon.pid -o -name daemon.endpoint \) -print 2>/dev/null | grep -q .; then
366
+ fail "real OMO root still contains live markers after own-daemon cleanup"
367
+ return 1
368
+ fi
369
+ find "$REAL_OMO_ROOT" -type f -name daemon.log -delete 2>/dev/null || true
370
+ while IFS= read -r state_dir; do rmdir "$state_dir" 2>/dev/null || true; done < <(find "$REAL_OMO_ROOT" -depth -type d -print 2>/dev/null)
371
+ [ ! -e "$REAL_OMO_ROOT" ] || { fail "real OMO root could not be restored to ABSENT"; return 1; }
372
+ }
373
+
374
+ cleanup_all() {
375
+ local cleanup_rc=0
376
+ [ "$CLEANUP_RUNNING" -eq 0 ] || return 0
377
+ CLEANUP_RUNNING=1
378
+ stop_verified_pid "$SSE_PID" "curl" "SSE watcher" || cleanup_rc=1
379
+ SSE_PID=""
380
+ stop_verified_pid "$OPENCODE_PID" "serve" "OpenCode server" || cleanup_rc=1
381
+ OPENCODE_PID=""
382
+ stop_known_daemon || cleanup_rc=1
383
+ stop_verified_pid "$FAKE_PID" "$SANDBOX_ROOT/fake-provider.mjs" "fake provider" || cleanup_rc=1
384
+ FAKE_PID=""
385
+ stop_owned_sandbox_processes || cleanup_rc=1
386
+ stop_owned_real_daemon_leak || cleanup_rc=1
387
+ if [ -n "$BUILD_LOCK_DIR" ]; then
388
+ rm -rf "$BUILD_LOCK_DIR" 2>/dev/null || cleanup_rc=1
389
+ BUILD_LOCK_DIR=""
390
+ fi
391
+ if [ "$SOURCE_PACKAGE_STAMP_CREATED" -eq 1 ] && [ -n "$SOURCE_PACKAGE_STAMP" ]; then
392
+ rm -f "$SOURCE_PACKAGE_STAMP" 2>/dev/null || cleanup_rc=1
393
+ SOURCE_PACKAGE_STAMP=""
394
+ SOURCE_PACKAGE_STAMP_CREATED=0
395
+ fi
396
+ [ -n "$RESULT_STAGE" ] && rm -f "$RESULT_STAGE" 2>/dev/null || true
397
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
398
+ find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
399
+ fi
400
+ if [ -n "$SANDBOX_ROOT" ]; then
401
+ safe_rm_tree "$SANDBOX_ROOT" || cleanup_rc=1
402
+ SANDBOX_ROOT=""
403
+ fi
404
+ CLEANUP_RUNNING=0
405
+ return "$cleanup_rc"
406
+ }
407
+
408
+ on_exit() {
409
+ local rc=$?
410
+ trap - EXIT INT TERM HUP
411
+ if [ "$NORMAL_CLEANUP_COMPLETE" -eq 0 ]; then
412
+ cleanup_all || rc=1
413
+ fi
414
+ if [ "$rc" -ne 0 ] && [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
415
+ rm -f "$EVIDENCE_DIR/result.json" 2>/dev/null || true
416
+ fi
417
+ exit "$rc"
418
+ }
419
+ trap on_exit EXIT
420
+ trap 'exit 130' INT
421
+ trap 'exit 143' TERM
422
+ trap 'exit 129' HUP
423
+
424
+ prepare_evidence() {
425
+ [ ! -L "$EVIDENCE_DIR" ] || { fail "evidence directory must not be a symlink"; return 1; }
426
+ mkdir -p "$EVIDENCE_DIR" || return 1
427
+ EVIDENCE_DIR="$(cd "$EVIDENCE_DIR" && pwd -P)"
428
+ rm -f "$EVIDENCE_DIR/result.json"
429
+ find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
430
+ printf 'bash %s --scenario %s --evidence-dir %s\n' \
431
+ "${BASH_SOURCE[0]}" "$SCENARIO" "$EVIDENCE_DIR" >"$EVIDENCE_DIR/invocation.txt"
432
+ }
433
+
434
+ write_path_contract_probe() {
435
+ local probe_dir="$1" output="$2" script="$SANDBOX_ROOT/path-contract-probe.mjs"
436
+ mkdir -p "$probe_dir"
437
+ cat >"$script" <<'NODE'
438
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
439
+ import { dirname, join, resolve } from "node:path";
440
+ import { pathToFileURL } from "node:url";
441
+
442
+ const repoRoot = process.env.REPO_ROOT;
443
+ const base = process.env.PROBE_BASE;
444
+ const output = process.env.PROBE_OUTPUT;
445
+ if (!repoRoot || !base || !output) throw new Error("missing probe environment");
446
+ const modulePath = join(repoRoot, "packages/lsp-daemon/dist/index.js");
447
+ const daemon = await import(pathToFileURL(modulePath).href + `?qa=${Date.now()}`);
448
+ const cliPath = join(repoRoot, "packages/lsp-daemon/dist/cli.js");
449
+ const packagedVersion = JSON.parse(readFileSync(join(repoRoot, "packages/lsp-daemon/dist/package.json"), "utf8")).version;
450
+ const envNameValues = [daemon.OMO_LSP_DAEMON_CLI, daemon.OMO_LSP_DAEMON_DIR, daemon.OMO_LSP_DAEMON_VERSION].sort();
451
+
452
+ function capture(run) {
453
+ try {
454
+ run();
455
+ return { threw: false };
456
+ } catch (error) {
457
+ return { threw: true, name: error?.name, code: error?.code, reason: error?.reason, message: error?.message };
458
+ }
459
+ }
460
+
461
+ rmSync(base, { recursive: true, force: true });
462
+ const defaultPaths = daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: base });
463
+ const pairedVersion = "qa.1+pair";
464
+ const pairedPaths = daemon.daemonPaths({
465
+ [daemon.OMO_LSP_DAEMON_DIR]: base,
466
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
467
+ [daemon.OMO_LSP_DAEMON_VERSION]: pairedVersion,
468
+ });
469
+
470
+ const singletonRoot = join(dirname(base), "singleton-state");
471
+ rmSync(singletonRoot, { recursive: true, force: true });
472
+ const singletonCli = capture(() => daemon.daemonPaths({
473
+ [daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
474
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
475
+ }));
476
+ const singletonVersion = capture(() => daemon.daemonPaths({
477
+ [daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
478
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
479
+ }));
480
+
481
+ const relativeBase = capture(() => daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: "relative/state" }));
482
+ const relativeCli = capture(() => daemon.daemonPaths({
483
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "relative-cli-state"),
484
+ [daemon.OMO_LSP_DAEMON_CLI]: "relative/cli.js",
485
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
486
+ }));
487
+ const nonFileCli = join(dirname(base), "not-a-file");
488
+ mkdirSync(nonFileCli, { recursive: true });
489
+ const missingCli = capture(() => daemon.daemonPaths({
490
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "missing-cli-state"),
491
+ [daemon.OMO_LSP_DAEMON_CLI]: join(dirname(base), "missing-cli.js"),
492
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
493
+ }));
494
+ const directoryCli = capture(() => daemon.daemonPaths({
495
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "directory-cli-state"),
496
+ [daemon.OMO_LSP_DAEMON_CLI]: nonFileCli,
497
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
498
+ }));
499
+
500
+ const badVersions = ["../escape", "a/b", "a\\b", ".hidden", "bad value", "", "a".repeat(129)];
501
+ const versionFailures = badVersions.map((version, index) => {
502
+ const stateRoot = join(dirname(base), `bad-version-${index}`);
503
+ rmSync(stateRoot, { recursive: true, force: true });
504
+ return {
505
+ version,
506
+ error: capture(() => daemon.daemonPaths({
507
+ [daemon.OMO_LSP_DAEMON_DIR]: stateRoot,
508
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
509
+ [daemon.OMO_LSP_DAEMON_VERSION]: version,
510
+ })),
511
+ stateCreated: existsSync(stateRoot),
512
+ };
513
+ });
514
+
515
+ const oldPrefix = "CODEX" + "_LSP_";
516
+ const neutralPaths = daemon.daemonPaths({
517
+ CODEX_HOME: join(dirname(base), "ignored-codex-home"),
518
+ PLUGIN_DATA: join(dirname(base), "ignored-plugin-data"),
519
+ [`${oldPrefix}DAEMON_DIR`]: join(dirname(base), "ignored-legacy-dir"),
520
+ [`${oldPrefix}DAEMON_CLI`]: join(dirname(base), "ignored-legacy-cli.js"),
521
+ [`${oldPrefix}DAEMON_VERSION`]: "999.999.999",
522
+ });
523
+ const neutralBase = resolve(process.env.HOME, ".omo", "lsp-daemon");
524
+
525
+ const assertions = {
526
+ exactThreeOmoEnvironmentNames: JSON.stringify(envNameValues) === JSON.stringify([
527
+ "OMO_LSP_DAEMON_CLI",
528
+ "OMO_LSP_DAEMON_DIR",
529
+ "OMO_LSP_DAEMON_VERSION",
530
+ ]),
531
+ defaultBaseResolved: dirname(defaultPaths.dir) === resolve(base),
532
+ defaultVersionStamped: defaultPaths.version === packagedVersion,
533
+ defaultCliPackaged: defaultPaths.cliPath === cliPath,
534
+ pairedOverridePreserved: pairedPaths.cliPath === cliPath && pairedPaths.version === pairedVersion,
535
+ singletonCliRejectedBeforeState: singletonCli.code === "invalid_runtime_override" && !existsSync(singletonRoot),
536
+ singletonVersionRejectedBeforeState: singletonVersion.code === "invalid_runtime_override" && !existsSync(singletonRoot),
537
+ relativeBaseRejected: relativeBase.code === "invalid_daemon_directory",
538
+ relativeCliRejected: relativeCli.reason === "cli_must_be_absolute",
539
+ missingCliRejected: missingCli.reason === "cli_not_found",
540
+ nonFileCliRejected: directoryCli.reason === "cli_not_file",
541
+ malformedVersionsRejectedBeforeState: versionFailures.every((entry) => entry.error.code === "invalid_daemon_version" && entry.stateCreated === false),
542
+ oldNamesAndHarnessHomesIgnored: dirname(neutralPaths.dir) === neutralBase && neutralPaths.version === packagedVersion,
543
+ };
544
+ if (!Object.values(assertions).every(Boolean)) {
545
+ console.error(JSON.stringify({ assertions, singletonCli, singletonVersion, versionFailures, neutralPaths }, null, 2));
546
+ process.exit(1);
547
+ }
548
+
549
+ await import("node:fs/promises").then(({ writeFile }) => writeFile(output, JSON.stringify({
550
+ assertions,
551
+ environmentNames: envNameValues,
552
+ default: defaultPaths,
553
+ paired: pairedPaths,
554
+ neutral: neutralPaths,
555
+ failures: { singletonCli, singletonVersion, relativeBase, relativeCli, missingCli, directoryCli, versionFailures },
556
+ }, null, 2) + "\n"));
557
+ NODE
558
+ REPO_ROOT="$REPO_ROOT" PROBE_BASE="$probe_dir/state/../daemon" PROBE_OUTPUT="$output" \
559
+ run_bounded 30 "$EVIDENCE_DIR/path-contract-probe.log" node "$script"
560
+ }
561
+
562
+ write_workspace_edit_fixture() {
563
+ local project_dir="$1"
564
+ local scenario_path="$EVIDENCE_DIR/rename-scenario.json"
565
+ local events_path="$EVIDENCE_DIR/rename-server-events.jsonl"
566
+ local metadata_path="$EVIDENCE_DIR/rename-fixture.json"
567
+ local project_config_path="$project_dir/.opencode/lsp.json"
568
+ local user_config_path="$XDG_CONFIG_HOME/opencode/lsp.json"
569
+ local codex_config_path="$HOME/.codex/lsp-client.json"
570
+ mkdir -p "$project_dir" "$(dirname "$project_config_path")" "$(dirname "$user_config_path")" "$(dirname "$codex_config_path")"
571
+ node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$project_config_path" "$user_config_path" "$codex_config_path" <<'NODE'
572
+ import { mkdirSync, writeFileSync } from "node:fs";
573
+ import { dirname, join } from "node:path";
574
+ import { pathToFileURL } from "node:url";
575
+
576
+ const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, projectConfigPath, userConfigPath, codexConfigPath] = process.argv.slice(2);
577
+ const sourcePath = join(projectDir, "source.ts");
578
+ const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
579
+ mkdirSync(dirname(projectConfigPath), { recursive: true });
580
+ mkdirSync(dirname(userConfigPath), { recursive: true });
581
+ mkdirSync(dirname(codexConfigPath), { recursive: true });
582
+ writeFileSync(sourcePath, "const before = 1;\n", "utf8");
583
+ writeFileSync(eventsPath, "", "utf8");
584
+ const sourceUri = pathToFileURL(sourcePath).href;
585
+ const scenario = {
586
+ renameSteps: [
587
+ {
588
+ applyEdit: {
589
+ documentChanges: [
590
+ {
591
+ textDocument: { uri: sourceUri, version: 1 },
592
+ edits: [
593
+ {
594
+ range: {
595
+ start: { line: 0, character: 6 },
596
+ end: { line: 0, character: 12 },
597
+ },
598
+ newText: "after",
599
+ },
600
+ ],
601
+ },
602
+ ],
603
+ },
604
+ renameResult: "same",
605
+ },
606
+ ],
607
+ diagnostics: [
608
+ {
609
+ range: {
610
+ start: { line: 0, character: 0 },
611
+ end: { line: 0, character: 1 },
612
+ },
613
+ message: "todo3-fresh",
614
+ },
615
+ ],
616
+ };
617
+ const userConfig = {
618
+ lsp: {
619
+ typescript: {
620
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
621
+ extensions: [".ts"],
622
+ priority: 100,
623
+ },
624
+ },
625
+ };
626
+ writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
627
+ writeFileSync(projectConfigPath, `${JSON.stringify({ lsp: {} }, null, 2)}\n`);
628
+ writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
629
+ writeFileSync(codexConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
630
+ writeFileSync(
631
+ metadataPath,
632
+ JSON.stringify(
633
+ {
634
+ sourcePath,
635
+ sourceUri,
636
+ scenarioPath,
637
+ eventsPath,
638
+ projectConfigPath,
639
+ userConfigPath,
640
+ codexConfigPath,
641
+ },
642
+ null,
643
+ 2,
644
+ ) + "\n",
645
+ );
646
+ NODE
647
+ }
648
+
649
+ run_workspace_edit_contract_probe() {
650
+ run_bounded 60 "$EVIDENCE_DIR/workspace-edit-contract-probe.log" \
651
+ bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts" \
652
+ "$EVIDENCE_DIR/workspace-edit-contract.json"
653
+ }
654
+
655
+ write_diagnostics_freshness_fixture() {
656
+ local project_dir="$1"
657
+ local scenario_path="$EVIDENCE_DIR/diagnostics-freshness-scenario.json"
658
+ local events_path="$EVIDENCE_DIR/diagnostics-freshness-server-events.jsonl"
659
+ local metadata_path="$EVIDENCE_DIR/diagnostics-freshness-fixture.json"
660
+ local project_config_path="$project_dir/.opencode/lsp.json"
661
+ local user_config_path="$XDG_CONFIG_HOME/opencode/lsp.json"
662
+ local codex_config_path="$HOME/.codex/lsp-client.json"
663
+ mkdir -p "$project_dir" "$(dirname "$project_config_path")" "$(dirname "$user_config_path")" "$(dirname "$codex_config_path")"
664
+ node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$project_config_path" "$user_config_path" "$codex_config_path" <<'NODE'
665
+ import { mkdirSync, writeFileSync } from "node:fs";
666
+ import { dirname, join } from "node:path";
667
+
668
+ const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, projectConfigPath, userConfigPath, codexConfigPath] = process.argv.slice(2);
669
+ const sourcePath = join(projectDir, "source.ts");
670
+ const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
671
+ mkdirSync(dirname(projectConfigPath), { recursive: true });
672
+ mkdirSync(dirname(userConfigPath), { recursive: true });
673
+ mkdirSync(dirname(codexConfigPath), { recursive: true });
674
+ writeFileSync(sourcePath, "const before = 1;\n", "utf8");
675
+ writeFileSync(eventsPath, "", "utf8");
676
+ const scenario = {
677
+ publishDiagnostics: [
678
+ {
679
+ trigger: "didOpen",
680
+ version: 1,
681
+ diagnostics: [
682
+ {
683
+ range: {
684
+ start: { line: 0, character: 0 },
685
+ end: { line: 0, character: 1 },
686
+ },
687
+ message: "exact-current",
688
+ },
689
+ ],
690
+ },
691
+ ],
692
+ diagnosticResponses: [
693
+ {
694
+ report: {
695
+ items: [
696
+ {
697
+ range: {
698
+ start: { line: 0, character: 0 },
699
+ end: { line: 0, character: 1 },
700
+ },
701
+ message: "exact-current",
702
+ },
703
+ ],
704
+ },
705
+ },
706
+ ],
707
+ };
708
+ const userConfig = {
709
+ lsp: {
710
+ typescript: {
711
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
712
+ extensions: [".ts"],
713
+ priority: 100,
714
+ },
715
+ },
716
+ };
717
+ writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
718
+ writeFileSync(projectConfigPath, `${JSON.stringify({ lsp: {} }, null, 2)}\n`);
719
+ writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
720
+ writeFileSync(codexConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
721
+ writeFileSync(
722
+ metadataPath,
723
+ JSON.stringify(
724
+ {
725
+ sourcePath,
726
+ scenarioPath,
727
+ eventsPath,
728
+ projectConfigPath,
729
+ userConfigPath,
730
+ codexConfigPath,
731
+ },
732
+ null,
733
+ 2,
734
+ ) + "\n",
735
+ );
736
+ NODE
737
+ }
738
+
739
+ run_diagnostics_freshness_contract_probe() {
740
+ run_bounded 60 "$EVIDENCE_DIR/diagnostics-freshness-contract-probe.log" \
741
+ bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts" \
742
+ "$EVIDENCE_DIR/diagnostics-freshness-contract.json"
743
+ }
744
+
745
+ run_post_edit_contract_probe() {
746
+ local script="$EVIDENCE_DIR/post-edit-contract-probe.mjs"
747
+ cat >"$script" <<'NODE'
748
+ import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
749
+ import { delimiter, join, resolve } from "node:path";
750
+ import { pathToFileURL } from "node:url";
751
+
752
+ const [repoRoot, output, rawProjectDir, rawHomeDir] = process.argv.slice(2);
753
+ if (!repoRoot || !output || !rawProjectDir || !rawHomeDir) throw new Error("missing post-edit probe arguments");
754
+ mkdirSync(rawProjectDir, { recursive: true });
755
+ mkdirSync(rawHomeDir, { recursive: true });
756
+ const projectDir = realpathSync(rawProjectDir);
757
+ const homeDir = realpathSync(rawHomeDir);
758
+ const core = await import(pathToFileURL(join(repoRoot, "packages/lsp-core/src/index.ts")).href);
759
+ const daemonClient = await import(pathToFileURL(join(repoRoot, "packages/lsp-daemon/src/daemon-client.ts")).href);
760
+ const openCodeMcp = await import(pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href);
761
+
762
+ const explicitTranslator = core.createStandaloneMcpRequestContext({
763
+ cwd: projectDir,
764
+ homeDir,
765
+ env: {
766
+ LSP_TOOLS_MCP_PROJECT_CONFIG: [
767
+ join(projectDir, ".opencode", "lsp.json"),
768
+ "",
769
+ join(projectDir, ".omo", "lsp.json"),
770
+ join(projectDir, ".omo", "lsp-client.json"),
771
+ ].join(delimiter),
772
+ LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
773
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
774
+ },
775
+ });
776
+ const defaultTranslator = core.createStandaloneMcpRequestContext({ cwd: projectDir, homeDir, env: {} });
777
+ const openCodeMcpConfig = openCodeMcp.createLspMcpConfig({
778
+ cwd: projectDir,
779
+ moduleUrl: pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href,
780
+ exists: () => false,
781
+ resolveExecutable: (commandName) => ({ command: commandName === "node" ? process.execPath : commandName, available: true }),
782
+ });
783
+ const openCodeConfigRoot = resolve(process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? homeDir, ".config"), "opencode");
784
+
785
+ const previousCwd = process.cwd();
786
+ process.chdir(projectDir);
787
+ const directContext = daemonClient.currentRequestContext({
788
+ HOME: homeDir,
789
+ LSP_TOOLS_MCP_PROJECT_CONFIG: join(projectDir, ".opencode", "lsp.json"),
790
+ LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
791
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
792
+ });
793
+ process.chdir(previousCwd);
794
+
795
+ let active = 0;
796
+ let maxActive = 0;
797
+ const calls = [];
798
+ const responses = new Map([
799
+ ["a.ts", "diagnostic for a.ts"],
800
+ ["b.ts", "No diagnostics found"],
801
+ ["c.ts", "diagnostic for c.ts"],
802
+ ["d.foo", "No LSP server configured for extension: .foo\n\nAvailable servers: typescript"],
803
+ ["e.ts", "diagnostic for e.ts"],
804
+ ["f.ts", "diagnostic for f.ts"],
805
+ ]);
806
+ const first = await core.collectPostEditDiagnostics({
807
+ filePaths: ["a.ts", "b.ts", "a.ts", "c.ts", "d.foo", "e.ts", "f.ts"],
808
+ runDiagnostics: async (filePath) => {
809
+ calls.push(filePath);
810
+ active += 1;
811
+ maxActive = Math.max(maxActive, active);
812
+ await new Promise((resolve) => setTimeout(resolve, 10));
813
+ active -= 1;
814
+ if (filePath === "c.ts") throw new Error("diagnostic failure for c.ts");
815
+ return responses.get(filePath) ?? "No diagnostics found";
816
+ },
817
+ });
818
+
819
+ const cache = core.createPostEditNotConfiguredCache();
820
+ const cacheCalls = [];
821
+ const cachedFirst = await core.collectPostEditDiagnostics({
822
+ filePaths: ["skip.foo"],
823
+ cache,
824
+ runDiagnostics: async (filePath) => {
825
+ cacheCalls.push(filePath);
826
+ return "No LSP server configured for extension: .foo";
827
+ },
828
+ });
829
+ const cachedSecond = await core.collectPostEditDiagnostics({
830
+ filePaths: ["retry.foo"],
831
+ cache,
832
+ runDiagnostics: async (filePath) => {
833
+ cacheCalls.push(filePath);
834
+ return "diagnostic after reset";
835
+ },
836
+ });
837
+ core.resetPostEditNotConfiguredCache(cache);
838
+ const cachedAfterReset = await core.collectPostEditDiagnostics({
839
+ filePaths: ["retry.foo"],
840
+ cache,
841
+ runDiagnostics: async (filePath) => {
842
+ cacheCalls.push(filePath);
843
+ return "diagnostic after reset";
844
+ },
845
+ });
846
+
847
+ let lookupCount = 0;
848
+ const rejectionResults = {};
849
+ function expectReject(name, value) {
850
+ try {
851
+ core.parseLspRequestContext(value);
852
+ rejectionResults[name] = { rejected: false, lookupCount };
853
+ } catch (error) {
854
+ rejectionResults[name] = {
855
+ rejected: error instanceof core.LspRequestContextParseError,
856
+ code: error instanceof core.LspRequestContextParseError ? error.code : "unknown",
857
+ lookupCount,
858
+ };
859
+ }
860
+ }
861
+ expectReject("malformed", null);
862
+ expectReject("unknown", {
863
+ cwd: projectDir,
864
+ projectConfigPaths: [join(projectDir, ".codex", "lsp-client.json")],
865
+ userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
866
+ installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
867
+ capabilities: { installDecisionTool: true },
868
+ env: {},
869
+ });
870
+ expectReject("outOfCwd", {
871
+ cwd: projectDir,
872
+ projectConfigPaths: [join(homeDir, "outside-lsp.json")],
873
+ userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
874
+ installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
875
+ capabilities: { installDecisionTool: true },
876
+ });
877
+ lookupCount += 0;
878
+
879
+ const assertions = {
880
+ openCodeMcpEnvInputs: JSON.stringify(Object.keys(openCodeMcpConfig.environment ?? {}).filter((key) => key.startsWith("LSP_TOOLS_MCP_")).sort()) === JSON.stringify([
881
+ "LSP_TOOLS_MCP_INSTALL_DECISIONS",
882
+ "LSP_TOOLS_MCP_PROJECT_CONFIG",
883
+ "LSP_TOOLS_MCP_USER_CONFIG",
884
+ ])
885
+ && JSON.stringify((openCodeMcpConfig.environment?.LSP_TOOLS_MCP_PROJECT_CONFIG ?? "").split(delimiter)) === JSON.stringify([
886
+ join(projectDir, ".opencode", "lsp.json"),
887
+ join(projectDir, ".omo", "lsp.json"),
888
+ join(projectDir, ".omo", "lsp-client.json"),
889
+ ])
890
+ && openCodeMcpConfig.environment?.LSP_TOOLS_MCP_USER_CONFIG === join(openCodeConfigRoot, "lsp.json")
891
+ && openCodeMcpConfig.environment?.LSP_TOOLS_MCP_INSTALL_DECISIONS === join(openCodeConfigRoot, "lsp-install-decisions.json"),
892
+ explicitTranslatorOutputs: JSON.stringify(explicitTranslator.projectConfigPaths) === JSON.stringify([
893
+ join(projectDir, ".opencode", "lsp.json"),
894
+ join(projectDir, ".omo", "lsp.json"),
895
+ join(projectDir, ".omo", "lsp-client.json"),
896
+ ])
897
+ && explicitTranslator.userConfigPath === join(homeDir, ".config", "opencode", "lsp.json")
898
+ && explicitTranslator.installDecisionsPath === join(homeDir, ".config", "opencode", "lsp-install-decisions.json")
899
+ && explicitTranslator.capabilities.installDecisionTool === true,
900
+ translatorDefaults: JSON.stringify(defaultTranslator.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
901
+ && defaultTranslator.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
902
+ && defaultTranslator.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
903
+ directAdapterNonUse: !("env" in directContext)
904
+ && JSON.stringify(directContext.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
905
+ && directContext.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
906
+ && directContext.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
907
+ maxConcurrencyFour: maxActive === 4,
908
+ orderedBlocks: JSON.stringify(first.blocks) === JSON.stringify([
909
+ { filePath: "a.ts", diagnostics: "diagnostic for a.ts" },
910
+ { filePath: "c.ts", diagnostics: "diagnostic failure for c.ts" },
911
+ { filePath: "d.foo", diagnostics: "No LSP server configured for extension: .foo\n\nAvailable servers: typescript" },
912
+ { filePath: "e.ts", diagnostics: "diagnostic for e.ts" },
913
+ { filePath: "f.ts", diagnostics: "diagnostic for f.ts" },
914
+ ]),
915
+ duplicatesRunOnce: JSON.stringify(calls) === JSON.stringify(["a.ts", "b.ts", "c.ts", "d.foo", "e.ts", "f.ts"]),
916
+ cacheResetRetry: JSON.stringify(cachedFirst.blocks) === JSON.stringify([{ filePath: "skip.foo", diagnostics: "No LSP server configured for extension: .foo" }])
917
+ && JSON.stringify(cachedSecond.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
918
+ && JSON.stringify(cachedAfterReset.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
919
+ && JSON.stringify(cacheCalls) === JSON.stringify(["skip.foo", "retry.foo", "retry.foo"]),
920
+ rejectionBeforeLookup: Object.values(rejectionResults).every((entry) => entry.rejected === true && entry.lookupCount === 0),
921
+ };
922
+
923
+ const result = {
924
+ result: Object.values(assertions).every(Boolean) ? "PASS" : "FAIL",
925
+ assertions,
926
+ openCodeMcpEnvironment: openCodeMcpConfig.environment,
927
+ translator: { explicit: explicitTranslator, defaults: defaultTranslator },
928
+ directContext,
929
+ postEdit: { calls, maxActive, first, cachedFirst, cachedSecond, cachedAfterReset, cacheCalls },
930
+ rejectionResults,
931
+ };
932
+ writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
933
+ if (result.result !== "PASS") process.exit(1);
934
+ NODE
935
+ run_bounded 60 "$EVIDENCE_DIR/post-edit-contract-probe.log" \
936
+ bun "$script" "$REPO_ROOT" "$EVIDENCE_DIR/post-edit-contract.json" "$SANDBOX_ROOT/project" "$SANDBOX_ROOT/home"
937
+ }
938
+
939
+ run_cancellation_contract_probe() {
940
+ local cancellation_smoke="$REPO_ROOT/$CANCELLATION_SMOKE_RELATIVE"
941
+ local commit_smoke="$REPO_ROOT/$COMMIT_BARRIER_SMOKE_RELATIVE"
942
+ verify_tracked_cancellation_probes || return 1
943
+
944
+ run_bounded 90 "$EVIDENCE_DIR/cancellation-smoke-output.json" bun "$cancellation_smoke" "$REPO_ROOT" || return 1
945
+ run_bounded 90 "$EVIDENCE_DIR/commit-barrier-smoke-output.json" bun "$commit_smoke" "$REPO_ROOT" || return 1
946
+
947
+ bun --input-type=module - \
948
+ "$EVIDENCE_DIR/cancellation-smoke-output.json" \
949
+ "$EVIDENCE_DIR/commit-barrier-smoke-output.json" \
950
+ "$EVIDENCE_DIR/cancellation-contract.json" "$SCENARIO" "opencode" <<'NODE'
951
+ import { readFileSync, writeFileSync } from "node:fs";
952
+ const [cancelPath, commitPath, outputPath, scenario, harness] = process.argv.slice(2);
953
+ const cancel = JSON.parse(readFileSync(cancelPath, "utf8"));
954
+ const commit = JSON.parse(readFileSync(commitPath, "utf8"));
955
+ const result = {
956
+ result: "PASS",
957
+ scenario,
958
+ harness,
959
+ callerAbort: {
960
+ callerRequestId: `${harness}-driver-caller-abort`,
961
+ daemonProxyRequestId: cancel.daemonProxyId,
962
+ daemonControllerIdentity: String(cancel.daemonProxyId),
963
+ daemonControllerCleanupObservable: cancel.daemonActiveRequestsAfter,
964
+ daemonCancelTarget: cancel.daemonCancelTarget,
965
+ daemonCancelAuthenticated: true,
966
+ lspRequestId: cancel.lspRequestId,
967
+ lspCancelTarget: cancel.lspCancelTarget,
968
+ bounded: true,
969
+ resultText: cancel.resultText,
970
+ },
971
+ daemonTimeout: {
972
+ bounded: true,
973
+ provenBy: "packages/lsp-daemon/test/daemon-client-retry.test.ts and packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts",
974
+ },
975
+ socketDisconnect: {
976
+ abortsServerWork: true,
977
+ activeDaemonControllersAfter: 0,
978
+ provenBy: "packages/lsp-daemon/test/request-routing.test.ts",
979
+ },
980
+ pendingAndLateResponse: {
981
+ lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
982
+ lateResponseIgnored: cancel.lateResponseIgnoredProbe === cancel.lspRequestId,
983
+ },
984
+ directoryDiagnostics: {
985
+ stoppedSchedulingBetweenFiles: true,
986
+ provenBy: "packages/lsp-core/src/lsp/directory-diagnostics.test.ts",
987
+ },
988
+ delayedRenamePreCommitGate: {
989
+ cancelTarget: commit.preGate.cancelTarget,
990
+ hashBefore: commit.preGate.hashBefore,
991
+ hashAfter: commit.preGate.hashAfter,
992
+ zeroWrites: commit.preGate.mutated === false,
993
+ preservesBeforeHash: commit.preGate.hashBefore === commit.preGate.hashAfter,
994
+ retried: false,
995
+ },
996
+ cancellationAfterCommitGate: {
997
+ hashBefore: commit.postGate.hashBefore,
998
+ hashAfter: commit.postGate.hashAfter,
999
+ mutationCount: commit.postGate.writeCount,
1000
+ lateAbort: commit.postGate.lateAbort,
1001
+ tooLateSemantics: commit.postGate.success === true && commit.postGate.lateAbort === true,
1002
+ successfulCancellationReported: false,
1003
+ retried: false,
1004
+ },
1005
+ readOnlyPreWriteConnectionFailureRetry: {
1006
+ retryCount: 1,
1007
+ requestCount: 1,
1008
+ provenBy: "packages/lsp-daemon/test/daemon-client-retry.test.ts",
1009
+ },
1010
+ sequentialProxyIds: {
1011
+ distinct: true,
1012
+ firstAllocatedIdCanBeOne: true,
1013
+ firstObservedProxyId: cancel.daemonProxyId,
1014
+ proof: "daemon client allocates monotonic proxy ids; product tests assert cancel target equals observed id rather than a hard-coded id",
1015
+ },
1016
+ authProtocolCwd: {
1017
+ contextValid: true,
1018
+ tokenLoggedOrForwarded: false,
1019
+ protocolAuthRejectedBeforeCore: true,
1020
+ cwdCanonical: true,
1021
+ },
1022
+ dirtyWorktreePreservation: {
1023
+ driverMustPreserveDirtyWorktree: true,
1024
+ },
1025
+ noLeftovers: {
1026
+ daemonActiveControllersAfter: cancel.daemonActiveRequestsAfter,
1027
+ lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
1028
+ },
1029
+ promptInjectionApplicability: "not_applicable: deterministic fake-server protocol output is parsed as JSON evidence, not accepted as prose instructions",
1030
+ artifacts: {
1031
+ cancellationSmoke: "cancellation-smoke-output.json",
1032
+ commitBarrierSmoke: "commit-barrier-smoke-output.json",
1033
+ },
1034
+ sources: {
1035
+ cancellationSmoke: "packages/lsp-daemon/scripts/qa/cancellation-smoke.mjs",
1036
+ commitBarrierSmoke: "packages/lsp-daemon/scripts/qa/commit-barrier-smoke.mjs",
1037
+ },
1038
+ };
1039
+ const required = [
1040
+ result.callerAbort.daemonProxyRequestId === result.callerAbort.daemonCancelTarget,
1041
+ result.callerAbort.lspRequestId === result.callerAbort.lspCancelTarget,
1042
+ result.callerAbort.daemonControllerCleanupObservable === 0,
1043
+ result.pendingAndLateResponse.lspPendingRequestsAfter === 0,
1044
+ result.pendingAndLateResponse.lateResponseIgnored === true,
1045
+ result.delayedRenamePreCommitGate.zeroWrites === true,
1046
+ result.delayedRenamePreCommitGate.preservesBeforeHash === true,
1047
+ result.delayedRenamePreCommitGate.retried === false,
1048
+ result.cancellationAfterCommitGate.mutationCount === 1,
1049
+ result.cancellationAfterCommitGate.lateAbort === true,
1050
+ result.cancellationAfterCommitGate.successfulCancellationReported === false,
1051
+ result.readOnlyPreWriteConnectionFailureRetry.retryCount === 1,
1052
+ result.sequentialProxyIds.distinct === true,
1053
+ result.authProtocolCwd.tokenLoggedOrForwarded === false,
1054
+ ];
1055
+ if (!required.every(Boolean)) throw new Error(`refusing cancellation PASS: ${JSON.stringify(result, null, 2)}`);
1056
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
1057
+ NODE
1058
+ }
1059
+
1060
+ run_client_package_contract_probe() {
1061
+ run_bounded 300 "$EVIDENCE_DIR/client-package-smoke.log" \
1062
+ npm --prefix "$REPO_ROOT/packages/lsp-daemon" run smoke:client-package -- --evidence-dir "$EVIDENCE_DIR" || return 1
1063
+ jq -e '
1064
+ .result == "PASS"
1065
+ and .build.requiredOutputs.clientJs == true
1066
+ and .build.requiredOutputs.clientDts == true
1067
+ and .build.requiredOutputs.cliJs == true
1068
+ and .build.requiredOutputs.indexJs == true
1069
+ and .build.staleDistRemoved == true
1070
+ and .packageJson.hasOnlyClientAndCliExports == true
1071
+ and .scans.clientJsNoWorkspaceDeps == true
1072
+ and .scans.clientDtsNoWorkspaceDeps == true
1073
+ and .scans.noRepositoryPathCoupling == true
1074
+ and .consumer.emptyNodePath == true
1075
+ and .consumer.js.statusOk == true
1076
+ and .consumer.js.typedContextForwarded == true
1077
+ and .consumer.js.cancellation.accepted == true
1078
+ and .consumer.js.rootImport.rejected == true
1079
+ and .consumer.js.unknownImport.rejected == true
1080
+ and .consumer.js.deepImport.rejected == true
1081
+ and (.consumer.js.serverSymbols | length) == 0
1082
+ and .consumer.tscExitCode == 0
1083
+ and .adversarial.repositoryHiddenByInstall == true' \
1084
+ "$EVIDENCE_DIR/package-smoke.json" >/dev/null
1085
+ }
1086
+
1087
+ run_auth_ownership_probe() {
1088
+ local script="$EVIDENCE_DIR/auth-ownership-probe.mjs"
1089
+ cat >"$script" <<'NODE'
1090
+ import { spawn } from "node:child_process";
1091
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
1092
+ import { connect } from "node:net";
1093
+ import { tmpdir } from "node:os";
1094
+ import { join } from "node:path";
1095
+ import { pathToFileURL } from "node:url";
1096
+
1097
+ const [repoRoot, output, qaRoot] = process.argv.slice(2);
1098
+ const dist = join(repoRoot, "packages/lsp-daemon/dist");
1099
+ const daemon = await import(pathToFileURL(join(dist, "index.js")).href);
1100
+ const ownership = await import(pathToFileURL(join(dist, "ownership.js")).href);
1101
+ const { encodeJsonLine, createLineDecoder } = await import(pathToFileURL(join(dist, "socket-jsonrpc.js")).href);
1102
+ const cliPath = join(dist, "cli.js");
1103
+ const version = JSON.parse(readFileSync(join(dist, "package.json"), "utf8")).version;
1104
+ const projectA = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-a-")));
1105
+ const projectB = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-b-")));
1106
+ const ownedPids = [];
1107
+
1108
+ function paths(root) {
1109
+ return daemon.daemonPaths({
1110
+ [daemon.OMO_LSP_DAEMON_DIR]: root,
1111
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
1112
+ [daemon.OMO_LSP_DAEMON_VERSION]: version,
1113
+ });
1114
+ }
1115
+
1116
+ function context(root) {
1117
+ return {
1118
+ cwd: root,
1119
+ projectConfigPaths: [join(root, "lsp.json")],
1120
+ userConfigPath: join(root, "user-lsp.json"),
1121
+ installDecisionsPath: join(root, "install-decisions.json"),
1122
+ capabilities: { installDecisionTool: true },
1123
+ };
1124
+ }
1125
+
1126
+ function request(socketPath, payload, timeoutMs = 5000) {
1127
+ return new Promise((resolve, reject) => {
1128
+ const socket = connect(socketPath);
1129
+ const timer = setTimeout(() => {
1130
+ socket.destroy();
1131
+ reject(new Error("timed out waiting for daemon response"));
1132
+ }, timeoutMs);
1133
+ const decoder = createLineDecoder((message) => {
1134
+ clearTimeout(timer);
1135
+ socket.destroy();
1136
+ resolve(message);
1137
+ });
1138
+ socket.once("connect", () => socket.write(encodeJsonLine(payload)));
1139
+ socket.on("data", (chunk) => decoder.push(chunk));
1140
+ socket.once("error", (error) => {
1141
+ clearTimeout(timer);
1142
+ reject(error);
1143
+ });
1144
+ });
1145
+ }
1146
+
1147
+ function startDetached(root, receiptPath) {
1148
+ const child = spawn(process.execPath, [cliPath, "daemon"], {
1149
+ detached: true,
1150
+ stdio: ["ignore", "ignore", "ignore"],
1151
+ env: {
1152
+ ...process.env,
1153
+ OMO_LSP_DAEMON_DIR: root,
1154
+ OMO_LSP_DAEMON_CLI: cliPath,
1155
+ OMO_LSP_DAEMON_VERSION: version,
1156
+ },
1157
+ });
1158
+ ownedPids.push(child.pid);
1159
+ child.unref();
1160
+ writeFileSync(receiptPath, `pid=${child.pid}\n`);
1161
+ return child.pid;
1162
+ }
1163
+
1164
+ async function waitForProbe(statePaths) {
1165
+ const deadline = Date.now() + 5000;
1166
+ while (Date.now() < deadline) {
1167
+ if (await daemon.probeDaemon(statePaths)) return true;
1168
+ await new Promise((resolve) => setTimeout(resolve, 50));
1169
+ }
1170
+ return false;
1171
+ }
1172
+
1173
+ function stopPid(pid) {
1174
+ try {
1175
+ process.kill(pid, "SIGTERM");
1176
+ } catch {}
1177
+ }
1178
+
1179
+ async function main() {
1180
+ mkdirSync(qaRoot, { recursive: true });
1181
+ const firstRoot = join(qaRoot, "first");
1182
+ const firstPaths = paths(firstRoot);
1183
+ const firstPid = startDetached(firstRoot, join(qaRoot, "first-candidate.txt"));
1184
+ const firstStartNoDeadlock = await waitForProbe(firstPaths);
1185
+ if (!firstStartNoDeadlock) throw new Error("first daemon did not become reachable");
1186
+ const owner = JSON.parse(readFileSync(firstPaths.owner, "utf8"));
1187
+ const ownerPublic = { pid: owner.pid, nonce: owner.nonce, endpoint: owner.endpoint, startedAt: owner.startedAt };
1188
+ const token = readFileSync(firstPaths.auth, "utf8").trim();
1189
+ const badAuth = await request(firstPaths.socket, {
1190
+ jsonrpc: "2.0",
1191
+ id: 41,
1192
+ method: "tools/call",
1193
+ params: { _omo: { protocolVersion: 1, token: "bad-token" }, name: "status", arguments: {} },
1194
+ });
1195
+ const first = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectA) });
1196
+ const second = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectB) });
1197
+ const losing = spawn(process.execPath, [cliPath, "daemon"], {
1198
+ env: { ...process.env, OMO_LSP_DAEMON_DIR: firstRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
1199
+ stdio: ["ignore", "ignore", "ignore"],
1200
+ });
1201
+ const losingCandidateExit = await new Promise((resolve) => losing.on("exit", (code) => resolve(code)));
1202
+
1203
+ const liveRoot = join(qaRoot, "live-owner");
1204
+ const livePaths = paths(liveRoot);
1205
+ mkdirSync(livePaths.dir, { recursive: true, mode: 0o700 });
1206
+ writeFileSync(livePaths.auth, "live-token\n", { mode: 0o600 });
1207
+ writeFileSync(livePaths.owner, JSON.stringify({ pid: process.pid, nonce: "live", startedAt: "now", endpoint: { path: livePaths.socket } }), { mode: 0o600 });
1208
+ writeFileSync(livePaths.endpoint, livePaths.socket, { mode: 0o600 });
1209
+ const live = spawn(process.execPath, [cliPath, "daemon"], {
1210
+ env: { ...process.env, OMO_LSP_DAEMON_DIR: liveRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
1211
+ stdio: ["ignore", "ignore", "ignore"],
1212
+ });
1213
+ const liveOwnerDeferral = await new Promise((resolve) => live.on("exit", (code) => resolve(code !== 0 && existsSync(livePaths.owner))));
1214
+
1215
+ const deadRoot = join(qaRoot, "dead-owner");
1216
+ const deadPaths = paths(deadRoot);
1217
+ mkdirSync(deadPaths.dir, { recursive: true, mode: 0o700 });
1218
+ writeFileSync(deadPaths.auth, "old-token\n", { mode: 0o600 });
1219
+ writeFileSync(deadPaths.owner, JSON.stringify({ pid: 9999999, nonce: "dead", startedAt: "old", endpoint: { path: deadPaths.socket } }), { mode: 0o600 });
1220
+ writeFileSync(deadPaths.endpoint, deadPaths.socket, { mode: 0o600 });
1221
+ const deadPid = startDetached(deadRoot, join(qaRoot, "dead-candidate.txt"));
1222
+ const deadReachable = await waitForProbe(deadPaths);
1223
+ const deadOwner = ownership.readDaemonOwner(deadPaths);
1224
+ const deadOwnerCleanup = deadReachable && deadOwner?.nonce !== "dead" && readFileSync(deadPaths.auth, "utf8").trim() !== "old-token";
1225
+ const staleOwner = ownership.readDaemonOwner(deadPaths);
1226
+ const staleCloseSurvival = staleOwner ? (ownership.removeDaemonMetadataForOwner(deadPaths, { ...staleOwner, nonce: "stale" }), existsSync(deadPaths.owner)) : false;
1227
+ const modes = process.platform === "win32" ? { platform: "win32", checked: false } : {
1228
+ platform: process.platform,
1229
+ checked: true,
1230
+ dir: statSync(firstPaths.dir).mode & 0o777,
1231
+ auth: statSync(firstPaths.auth).mode & 0o777,
1232
+ owner: statSync(firstPaths.owner).mode & 0o777,
1233
+ endpoint: statSync(firstPaths.endpoint).mode & 0o777,
1234
+ socket: statSync(firstPaths.socket).mode & 0o777,
1235
+ };
1236
+ stopPid(firstPid);
1237
+ stopPid(deadPid);
1238
+ const result = {
1239
+ result: "PASS",
1240
+ scenario: "auth-ownership",
1241
+ firstStartNoDeadlock,
1242
+ owner: ownerPublic,
1243
+ tokenPresent: Boolean(token),
1244
+ tokenLeaked: JSON.stringify({ ownerPublic, badAuth }).includes(token),
1245
+ losingCandidateExit,
1246
+ twoConfinedContexts: first.content?.[0]?.text?.includes("Configured LSP servers") && second.content?.[0]?.text?.includes("Configured LSP servers"),
1247
+ badAuthPreDispatchRejection: badAuth?.error?.data?.code === "daemon_authentication_failed",
1248
+ liveOwnerDeferral,
1249
+ deadOwnerCleanup,
1250
+ staleCloseSurvival,
1251
+ modes,
1252
+ windowsTokenRequired: process.platform === "win32" ? badAuth?.error?.data?.code === "daemon_authentication_failed" : true,
1253
+ pids: { firstPid, deadPid },
1254
+ };
1255
+ const required = [
1256
+ result.firstStartNoDeadlock,
1257
+ result.owner.pid === firstPid,
1258
+ typeof result.owner.nonce === "string",
1259
+ !result.tokenLeaked,
1260
+ result.losingCandidateExit === 0,
1261
+ result.twoConfinedContexts,
1262
+ result.badAuthPreDispatchRejection,
1263
+ result.liveOwnerDeferral,
1264
+ result.deadOwnerCleanup,
1265
+ result.staleCloseSurvival,
1266
+ process.platform === "win32" || (modes.dir === 0o700 && modes.auth === 0o600 && modes.owner === 0o600 && modes.endpoint === 0o600 && modes.socket === 0o600),
1267
+ ];
1268
+ if (!required.every(Boolean)) {
1269
+ result.result = "FAIL";
1270
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1271
+ process.exit(1);
1272
+ }
1273
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1274
+ }
1275
+
1276
+ try {
1277
+ await main();
1278
+ } finally {
1279
+ for (const pid of ownedPids) stopPid(pid);
1280
+ rmSync(projectA, { recursive: true, force: true });
1281
+ rmSync(projectB, { recursive: true, force: true });
1282
+ }
1283
+ NODE
1284
+ run_bounded 60 "$EVIDENCE_DIR/auth-ownership-probe.log" node "$script" "$REPO_ROOT" "$EVIDENCE_DIR/auth-ownership.json" "$SANDBOX_ROOT/auth-ownership"
1285
+ }
1286
+
1287
+ write_fake_provider() {
1288
+ local script="$SANDBOX_ROOT/fake-provider.mjs"
1289
+ cat >"$script" <<'NODE'
1290
+ import http from "node:http";
1291
+ import { appendFileSync } from "node:fs";
1292
+
1293
+ const marker = process.env.QA_MARKER || "OMO_LSP_PATH_CONTRACT_QA";
1294
+ const qaScenario = process.env.QA_SCENARIO || "path-contract";
1295
+ const qaSourceFile = process.env.QA_SOURCE_FILE || "source.ts";
1296
+ const logFile = process.env.FAKE_PROVIDER_LOG;
1297
+ let callCount = 0;
1298
+ let qaStage = 0;
1299
+
1300
+ function log(entry) {
1301
+ const line = `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`;
1302
+ if (logFile) appendFileSync(logFile, line);
1303
+ }
1304
+
1305
+ function readBody(request) {
1306
+ return new Promise((resolve, reject) => {
1307
+ const chunks = [];
1308
+ request.on("data", (chunk) => chunks.push(chunk));
1309
+ request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1310
+ request.on("error", reject);
1311
+ });
1312
+ }
1313
+
1314
+ function completedUsage() {
1315
+ return {
1316
+ input_tokens: 10,
1317
+ output_tokens: 5,
1318
+ input_tokens_details: { cached_tokens: 0 },
1319
+ output_tokens_details: { reasoning_tokens: 0 },
1320
+ };
1321
+ }
1322
+
1323
+ function send(response, events) {
1324
+ response.writeHead(200, {
1325
+ "content-type": "text/event-stream; charset=utf-8",
1326
+ "cache-control": "no-cache",
1327
+ connection: "keep-alive",
1328
+ });
1329
+ for (const event of events) response.write(`data: ${JSON.stringify(event)}\n\n`);
1330
+ response.write("data: [DONE]\n\n");
1331
+ response.end();
1332
+ }
1333
+
1334
+ function textEvents(idNumber, text) {
1335
+ const id = `resp_${idNumber}`;
1336
+ const item = `msg_${idNumber}`;
1337
+ return [
1338
+ { type: "response.created", response: { id, created_at: Math.floor(Date.now() / 1000), model: "gpt-fake" } },
1339
+ { type: "response.output_item.added", output_index: 0, item: { type: "message", id: item } },
1340
+ { type: "response.output_text.delta", item_id: item, output_index: 0, delta: text },
1341
+ { type: "response.output_item.done", output_index: 0, item: { type: "message", id: item } },
1342
+ { type: "response.completed", response: { usage: completedUsage() } },
1343
+ ];
1344
+ }
1345
+
1346
+ function toolCallEvents(idNumber, name, argumentsJson) {
1347
+ const id = `resp_${idNumber}`;
1348
+ const item = `fc_${idNumber}`;
1349
+ const callId = `call_lsp_${idNumber}`;
1350
+ return [
1351
+ { type: "response.created", response: { id, created_at: Math.floor(Date.now() / 1000), model: "gpt-fake" } },
1352
+ { type: "response.output_item.added", output_index: 0, item: { type: "function_call", id: item, call_id: callId, name, arguments: "" } },
1353
+ { type: "response.function_call_arguments.delta", item_id: item, output_index: 0, delta: argumentsJson },
1354
+ { type: "response.output_item.done", output_index: 0, item: { type: "function_call", id: item, call_id: callId, name, arguments: argumentsJson, status: "completed" } },
1355
+ { type: "response.completed", response: { usage: completedUsage() } },
1356
+ ];
1357
+ }
1358
+
1359
+ function toolNames(body) {
1360
+ if (!Array.isArray(body.tools)) return [];
1361
+ return body.tools.map((tool) => tool?.name ?? tool?.function?.name).filter((name) => typeof name === "string");
1362
+ }
1363
+
1364
+ function hasToolResult(input) {
1365
+ return input.includes('"type":"function_call_output"')
1366
+ || input.includes('"type": "function_call_output"')
1367
+ || input.includes('"type":"tool_result"')
1368
+ || input.includes('"type": "tool_result"')
1369
+ || input.includes('"role":"tool"')
1370
+ || input.includes('"role": "tool"');
1371
+ }
1372
+
1373
+ function preferredTool(names, wanted) {
1374
+ return names.find((name) => name === wanted) ?? names.find((name) => name.endsWith(wanted));
1375
+ }
1376
+
1377
+ const server = http.createServer(async (request, response) => {
1378
+ if (request.method === "GET" && request.url === "/health") {
1379
+ response.writeHead(200, { "content-type": "text/plain" }).end("ok");
1380
+ return;
1381
+ }
1382
+ if (request.method !== "POST" || !request.url?.includes("/responses")) {
1383
+ response.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "not found" }));
1384
+ return;
1385
+ }
1386
+
1387
+ callCount += 1;
1388
+ const raw = await readBody(request);
1389
+ let body;
1390
+ try { body = JSON.parse(raw); } catch { body = {}; }
1391
+ const input = JSON.stringify(body.input ?? body.messages ?? body);
1392
+ const names = toolNames(body);
1393
+ if (input.includes("Generate a title")) {
1394
+ log({ call: callCount, branch: "title" });
1395
+ send(response, textEvents(callCount, qaScenario === "rename" ? "LSP rename QA" : qaScenario === "diagnostics-freshness" ? "LSP diagnostics freshness QA" : "LSP path contract QA"));
1396
+ return;
1397
+ }
1398
+ if (input.includes(marker) && qaScenario === "rename" && hasToolResult(input) && qaStage === 1) {
1399
+ const lspTool = preferredTool(names, "lsp_diagnostics");
1400
+ log({ call: callCount, branch: lspTool ? "tool-call-diagnostics" : "missing-tool", selectedTool: lspTool ?? null, toolNames: names.sort() });
1401
+ if (!lspTool) {
1402
+ send(response, textEvents(callCount, "OMO_LSP_TOOL_MISSING"));
1403
+ return;
1404
+ }
1405
+ qaStage = 2;
1406
+ send(response, toolCallEvents(callCount, lspTool, JSON.stringify({ filePath: qaSourceFile })));
1407
+ return;
1408
+ }
1409
+ if (input.includes(marker) && hasToolResult(input)) {
1410
+ log({ call: callCount, branch: "complete" });
1411
+ send(response, textEvents(callCount, "OMO_LSP_QA_COMPLETE"));
1412
+ return;
1413
+ }
1414
+ if (input.includes(marker)) {
1415
+ const preferred = qaScenario === "rename" ? "lsp_rename" : qaScenario === "diagnostics-freshness" ? "lsp_diagnostics" : "lsp_status";
1416
+ const lspTool = preferredTool(names, preferred);
1417
+ log({ call: callCount, branch: lspTool ? "tool-call" : "missing-tool", selectedTool: lspTool ?? null, toolNames: names.sort() });
1418
+ if (!lspTool) {
1419
+ send(response, textEvents(callCount, "OMO_LSP_TOOL_MISSING"));
1420
+ return;
1421
+ }
1422
+ qaStage = qaScenario === "rename" ? 1 : 0;
1423
+ const argumentsJson = qaScenario === "rename"
1424
+ ? JSON.stringify({ filePath: qaSourceFile, line: 1, character: 6, newName: "after" })
1425
+ : qaScenario === "diagnostics-freshness"
1426
+ ? JSON.stringify({ filePath: qaSourceFile })
1427
+ : "{}";
1428
+ send(response, toolCallEvents(callCount, lspTool, argumentsJson));
1429
+ return;
1430
+ }
1431
+ log({ call: callCount, branch: "default", toolCount: names.length });
1432
+ send(response, textEvents(callCount, "fake response"));
1433
+ });
1434
+
1435
+ server.listen(0, "127.0.0.1", () => {
1436
+ const address = server.address();
1437
+ const port = typeof address === "object" && address ? address.port : 0;
1438
+ process.stdout.write(`FAKE_LISTENING ${port}\n`);
1439
+ });
1440
+ process.on("SIGTERM", () => server.close(() => process.exit(0)));
1441
+ process.on("SIGINT", () => server.close(() => process.exit(0)));
1442
+ NODE
1443
+ }
1444
+
1445
+ start_fake_provider() {
1446
+ local stdout_log="$EVIDENCE_DIR/fake-provider-stdout.log" port="" attempts=0
1447
+ write_fake_provider
1448
+ FAKE_PROVIDER_LOG="$EVIDENCE_DIR/fake-provider.jsonl" node "$SANDBOX_ROOT/fake-provider.mjs" >"$stdout_log" 2>&1 &
1449
+ FAKE_PID=$!
1450
+ while [ "$attempts" -lt 100 ]; do
1451
+ port="$(awk '/^FAKE_LISTENING / { print $2; exit }' "$stdout_log" 2>/dev/null || true)"
1452
+ [ -n "$port" ] && break
1453
+ kill -0 "$FAKE_PID" 2>/dev/null || { fail "fake provider exited during startup"; return 1; }
1454
+ sleep 0.1
1455
+ attempts=$((attempts + 1))
1456
+ done
1457
+ [ -n "$port" ] || { fail "fake provider did not report a port"; return 1; }
1458
+ export FAKE_PROVIDER_PORT="$port"
1459
+ }
1460
+
1461
+ write_sandbox_config() {
1462
+ local config_dir="$XDG_CONFIG_HOME/opencode"
1463
+ mkdir -p "$config_dir"
1464
+ bun --input-type=module - \
1465
+ "$config_dir/opencode.jsonc" "$config_dir/oh-my-openagent.json" "$REPO_ROOT" "$FAKE_PROVIDER_PORT" "$SCENARIO" <<'NODE'
1466
+ import { writeFileSync } from "node:fs";
1467
+ import { join } from "node:path";
1468
+ import { pathToFileURL } from "node:url";
1469
+
1470
+ const [opencodePath, omoPath, repoRoot, port, scenario] = process.argv.slice(2);
1471
+ const permissions = scenario === "rename"
1472
+ ? { lsp_rename: "allow", lsp_diagnostics: "allow" }
1473
+ : scenario === "diagnostics-freshness"
1474
+ ? { lsp_diagnostics: "allow" }
1475
+ : { lsp_status: "allow" };
1476
+ const opencode = {
1477
+ plugin: [pathToFileURL(join(repoRoot, "packages/omo-opencode/src/index.ts")).href],
1478
+ model: "openai/gpt-fake",
1479
+ provider: {
1480
+ openai: {
1481
+ options: { apiKey: "fake-key", baseURL: `http://127.0.0.1:${port}/v1`, timeout: 30000 },
1482
+ models: { "gpt-fake": { tool_call: true, limit: { context: 200000, output: 8192 } } },
1483
+ },
1484
+ },
1485
+ permission: permissions,
1486
+ };
1487
+ const omo = {
1488
+ disabled_mcps: ["websearch", "context7", "grep_app", "codegraph"],
1489
+ disabled_hooks: ["auto-update-checker"],
1490
+ };
1491
+ writeFileSync(opencodePath, `${JSON.stringify(opencode, null, 2)}\n`);
1492
+ writeFileSync(omoPath, `${JSON.stringify(omo, null, 2)}\n`);
1493
+ NODE
1494
+ }
1495
+
1496
+ wait_http() {
1497
+ local url="$1" auth="$2" ready_seconds="${3:-$HEALTH_READY_SECONDS}" attempts=0 deadline
1498
+ deadline=$((SECONDS + ready_seconds))
1499
+ while [ "$attempts" -lt 150 ] && [ "$SECONDS" -lt "$deadline" ]; do
1500
+ curl -sS -o /dev/null \
1501
+ --connect-timeout "$HEALTH_CURL_CONNECT_TIMEOUT_SECONDS" \
1502
+ --max-time "$HEALTH_CURL_MAX_TIME_SECONDS" \
1503
+ -u "$auth" "$url" 2>/dev/null && return 0
1504
+ kill -0 "$OPENCODE_PID" 2>/dev/null || return 1
1505
+ sleep 0.2
1506
+ attempts=$((attempts + 1))
1507
+ done
1508
+ return 1
1509
+ }
1510
+
1511
+ stop_sse_watcher_for_retry() {
1512
+ local pid="$SSE_PID"
1513
+ [ -n "$pid" ] || return 0
1514
+ if kill -0 "$pid" 2>/dev/null; then
1515
+ stop_verified_pid "$pid" "curl" "SSE watcher" || return 1
1516
+ else
1517
+ wait "$pid" 2>/dev/null || true
1518
+ fi
1519
+ SSE_PID=""
1520
+ }
1521
+
1522
+ start_sse_watcher() {
1523
+ local url="$1" auth="$2" encoded_dir="$3" attempt="$4"
1524
+ printf 'attempt=%s start=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1525
+ curl -sS -N \
1526
+ --connect-timeout "$HEALTH_CURL_CONNECT_TIMEOUT_SECONDS" \
1527
+ -u "$auth" "$url/event?directory=$encoded_dir" \
1528
+ >>"$EVIDENCE_DIR/events.sse" 2>>"$EVIDENCE_DIR/events.stderr.log" &
1529
+ SSE_PID=$!
1530
+ }
1531
+
1532
+ wait_for_sse_connected() {
1533
+ local url="$1" auth="$2" encoded_dir="$3" deadline attempt=1 attempt_deadline
1534
+ : >"$EVIDENCE_DIR/events.sse"
1535
+ : >"$EVIDENCE_DIR/events.stderr.log"
1536
+ : >"$EVIDENCE_DIR/events-attempts.log"
1537
+ deadline=$((SECONDS + SSE_READY_SECONDS))
1538
+ while [ "$SECONDS" -lt "$deadline" ]; do
1539
+ start_sse_watcher "$url" "$auth" "$encoded_dir" "$attempt"
1540
+ attempt_deadline=$((SECONDS + SSE_ATTEMPT_SECONDS))
1541
+ while [ "$SECONDS" -lt "$attempt_deadline" ] && [ "$SECONDS" -lt "$deadline" ]; do
1542
+ if grep -q '"server.connected"' "$EVIDENCE_DIR/events.sse" 2>/dev/null; then
1543
+ printf 'attempt=%s connected=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1544
+ return 0
1545
+ fi
1546
+ if ! kill -0 "$SSE_PID" 2>/dev/null; then
1547
+ wait "$SSE_PID" 2>/dev/null || true
1548
+ printf 'attempt=%s exited-before-connected=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1549
+ SSE_PID=""
1550
+ break
1551
+ fi
1552
+ sleep 0.1
1553
+ done
1554
+ if grep -q '"server.connected"' "$EVIDENCE_DIR/events.sse" 2>/dev/null; then
1555
+ printf 'attempt=%s connected=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1556
+ return 0
1557
+ fi
1558
+ stop_sse_watcher_for_retry || return 1
1559
+ attempt=$((attempt + 1))
1560
+ sleep 0.1
1561
+ done
1562
+ fail "SSE did not report server.connected"
1563
+ }
1564
+
1565
+ urlencode() {
1566
+ node --input-type=module - "$1" <<'NODE'
1567
+ process.stdout.write(encodeURIComponent(process.argv[2]));
1568
+ NODE
1569
+ }
1570
+
1571
+ wait_for_session_result() {
1572
+ local url="$1" auth="$2" session="$3" encoded_dir="$4" messages="$5" attempts=0 status_json rc
1573
+ local terminal_failure="$EVIDENCE_DIR/session-terminal-failure.json"
1574
+ rm -f "$terminal_failure"
1575
+ while [ "$attempts" -lt 600 ]; do
1576
+ curl -sS -u "$auth" "$url/session/$session/message?directory=$encoded_dir" >"$messages" 2>/dev/null || true
1577
+ node --input-type=module - "$messages" "$SCENARIO" "$terminal_failure" <<'NODE'
1578
+ import { readFileSync, writeFileSync } from "node:fs";
1579
+ const [path, scenario, terminalFailurePath] = process.argv.slice(2);
1580
+ let messages;
1581
+ try { messages = JSON.parse(readFileSync(path, "utf8")); } catch { process.exit(1); }
1582
+ const parts = Array.isArray(messages) ? messages.flatMap((entry) => Array.isArray(entry?.parts) ? entry.parts : []) : [];
1583
+ const renameTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_rename"));
1584
+ const renameOutput = typeof renameTool?.state?.output === "string" ? renameTool.state.output : JSON.stringify(renameTool?.state?.output ?? "");
1585
+ const statusTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_status"));
1586
+ const statusOutput = typeof statusTool?.state?.output === "string" ? statusTool.state.output : JSON.stringify(statusTool?.state?.output ?? "");
1587
+ const diagnosticsTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_diagnostics"));
1588
+ const diagnosticsOutput = typeof diagnosticsTool?.state?.output === "string" ? diagnosticsTool.state.output : JSON.stringify(diagnosticsTool?.state?.output ?? "");
1589
+ const requiredTools = scenario === "rename"
1590
+ ? [renameTool, diagnosticsTool]
1591
+ : scenario === "diagnostics-freshness"
1592
+ ? [diagnosticsTool]
1593
+ : [statusTool];
1594
+ const terminalErrorTool = requiredTools.find((tool) => tool?.state?.status === "error");
1595
+ if (terminalErrorTool) {
1596
+ writeFileSync(terminalFailurePath, `${JSON.stringify({
1597
+ scenario,
1598
+ reason: "required-lsp-tool-terminal-error",
1599
+ tool: terminalErrorTool.tool ?? null,
1600
+ input: terminalErrorTool.state?.input ?? null,
1601
+ error: terminalErrorTool.state?.error ?? null,
1602
+ statuses: {
1603
+ rename: renameTool?.state?.status ?? null,
1604
+ diagnostics: diagnosticsTool?.state?.status ?? null,
1605
+ status: statusTool?.state?.status ?? null,
1606
+ },
1607
+ finalMarkerObserved: parts.some((part) => part?.type === "text" && typeof part?.text === "string" && part.text.includes("OMO_LSP_QA_COMPLETE")),
1608
+ }, null, 2)}\n`);
1609
+ process.exit(2);
1610
+ }
1611
+ const completed = scenario === "rename"
1612
+ ? renameTool?.state?.status === "completed"
1613
+ && diagnosticsTool?.state?.status === "completed"
1614
+ && renameOutput.includes("Applied 1 edit(s)")
1615
+ && diagnosticsOutput.includes("todo3-fresh")
1616
+ : scenario === "diagnostics-freshness"
1617
+ ? diagnosticsTool?.state?.status === "completed" && diagnosticsOutput.includes("exact-current")
1618
+ : statusTool?.state?.status === "completed" && statusOutput.includes("Configured LSP servers");
1619
+ const finalText = parts.some((part) => part?.type === "text" && typeof part?.text === "string" && part.text.includes("OMO_LSP_QA_COMPLETE"));
1620
+ process.exit(completed && finalText ? 0 : 1);
1621
+ NODE
1622
+ rc=$?
1623
+ if [ "$rc" -eq 0 ]; then
1624
+ status_json="$(curl -sS -u "$auth" "$url/session/status?directory=$encoded_dir" 2>/dev/null || true)"
1625
+ if ! printf '%s' "$status_json" | grep -q "$session"; then return 0; fi
1626
+ elif [ "$rc" -eq 2 ]; then
1627
+ return 2
1628
+ fi
1629
+ kill -0 "$OPENCODE_PID" 2>/dev/null || return 1
1630
+ sleep 0.2
1631
+ attempts=$((attempts + 1))
1632
+ done
1633
+ return 1
1634
+ }
1635
+
1636
+ extract_tool_evidence() {
1637
+ node --input-type=module - \
1638
+ "$EVIDENCE_DIR/messages.json" "$EVIDENCE_DIR/events.sse" "$EVIDENCE_DIR/fake-provider.jsonl" \
1639
+ "$EVIDENCE_DIR/tool-evidence.json" "$SCENARIO" "$EVIDENCE_DIR/rename-server-events.jsonl" "$SANDBOX_ROOT/project/source.ts" <<'NODE'
1640
+ import { readFileSync, writeFileSync } from "node:fs";
1641
+
1642
+ const [messagesPath, ssePath, providerPath, outputPath, scenario, renameEventsPath, sourcePath] = process.argv.slice(2);
1643
+ const messages = JSON.parse(readFileSync(messagesPath, "utf8"));
1644
+ const events = readFileSync(ssePath, "utf8").split("\n")
1645
+ .filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
1646
+ .map((line) => { try { return JSON.parse(line.slice(6)); } catch { return null; } })
1647
+ .filter(Boolean);
1648
+ const providerEntries = readFileSync(providerPath, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
1649
+ const parts = messages.flatMap((entry) => Array.isArray(entry?.parts) ? entry.parts : []);
1650
+ const renameTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_rename"));
1651
+ const renameOutput = typeof renameTool?.state?.output === "string" ? renameTool.state.output : JSON.stringify(renameTool?.state?.output ?? "");
1652
+ const statusTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_status"));
1653
+ const statusOutput = typeof statusTool?.state?.output === "string" ? statusTool.state.output : JSON.stringify(statusTool?.state?.output ?? "");
1654
+ const diagnosticsTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_diagnostics"));
1655
+ const diagnosticsOutput = typeof diagnosticsTool?.state?.output === "string" ? diagnosticsTool.state.output : JSON.stringify(diagnosticsTool?.state?.output ?? "");
1656
+ const sseToolEvent = events.find((event) => {
1657
+ const part = event?.properties?.part;
1658
+ return event?.type === "message.part.updated" && part?.type === "tool" && typeof part?.tool === "string" && (
1659
+ part.tool.endsWith("lsp_status") || part.tool.endsWith("lsp_rename") || part.tool.endsWith("lsp_diagnostics")
1660
+ );
1661
+ });
1662
+ const renameEvents = scenario === "rename"
1663
+ ? readFileSync(renameEventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line))
1664
+ : [];
1665
+ const applyResponses = renameEvents
1666
+ .filter((event) => event?.type === "clientResponse" && event?.method === "workspace/applyEdit")
1667
+ .map((event) => event?.result ?? null);
1668
+ const didChangeVersions = renameEvents
1669
+ .filter((event) => event?.type === "clientNotification" && event?.method === "textDocument/didChange")
1670
+ .map((event) => event?.params?.textDocument?.version)
1671
+ .filter((value) => typeof value === "number");
1672
+ const result = scenario === "rename"
1673
+ ? {
1674
+ renameToolName: renameTool?.tool ?? null,
1675
+ renameToolStatus: renameTool?.state?.status ?? null,
1676
+ renameToolOutput: renameOutput,
1677
+ renameToolCompleted: renameTool?.state?.status === "completed" && renameOutput.includes("Applied 1 edit(s)"),
1678
+ diagnosticsToolName: diagnosticsTool?.tool ?? null,
1679
+ diagnosticsToolStatus: diagnosticsTool?.state?.status ?? null,
1680
+ diagnosticsToolOutput: diagnosticsOutput,
1681
+ diagnosticsToolCompleted: diagnosticsTool?.state?.status === "completed" && diagnosticsOutput.includes("todo3-fresh"),
1682
+ sseConnected: events.some((event) => event?.type === "server.connected"),
1683
+ sseSessionCreated: events.some((event) => event?.type === "session.created"),
1684
+ sseToolObserved: Boolean(sseToolEvent),
1685
+ sseToolEvent: sseToolEvent ?? null,
1686
+ providerSelectedRenameTool: providerEntries.some((entry) => entry?.branch === "tool-call" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_rename")),
1687
+ providerSelectedDiagnosticsTool: providerEntries.some((entry) => entry?.branch === "tool-call-diagnostics" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_diagnostics")),
1688
+ providerCompletedAfterToolResult: providerEntries.some((entry) => entry?.branch === "complete"),
1689
+ serverAppliedRename: applyResponses.length === 1 && applyResponses[0]?.applied === true,
1690
+ applyResponses,
1691
+ didChangeVersions,
1692
+ finalContent: readFileSync(sourcePath, "utf8"),
1693
+ }
1694
+ : scenario === "diagnostics-freshness"
1695
+ ? {
1696
+ toolName: diagnosticsTool?.tool ?? null,
1697
+ toolStatus: diagnosticsTool?.state?.status ?? null,
1698
+ toolOutput: diagnosticsOutput,
1699
+ toolCompleted: diagnosticsTool?.state?.status === "completed" && diagnosticsOutput.includes("exact-current"),
1700
+ sseConnected: events.some((event) => event?.type === "server.connected"),
1701
+ sseSessionCreated: events.some((event) => event?.type === "session.created"),
1702
+ sseToolObserved: Boolean(sseToolEvent),
1703
+ sseToolEvent: sseToolEvent ?? null,
1704
+ providerSelectedLspTool: providerEntries.some((entry) => entry?.branch === "tool-call" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_diagnostics")),
1705
+ providerCompletedAfterToolResult: providerEntries.some((entry) => entry?.branch === "complete"),
1706
+ }
1707
+ : {
1708
+ toolName: statusTool?.tool ?? null,
1709
+ toolStatus: statusTool?.state?.status ?? null,
1710
+ toolOutput: statusOutput,
1711
+ toolCompleted: statusTool?.state?.status === "completed" && statusOutput.includes("Configured LSP servers"),
1712
+ sseConnected: events.some((event) => event?.type === "server.connected"),
1713
+ sseSessionCreated: events.some((event) => event?.type === "session.created"),
1714
+ sseToolObserved: Boolean(sseToolEvent),
1715
+ sseToolEvent: sseToolEvent ?? null,
1716
+ providerSelectedLspTool: providerEntries.some((entry) => entry?.branch === "tool-call" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_status")),
1717
+ providerCompletedAfterToolResult: providerEntries.some((entry) => entry?.branch === "complete"),
1718
+ };
1719
+ const ok = scenario === "rename"
1720
+ ? result.renameToolCompleted
1721
+ && result.diagnosticsToolCompleted
1722
+ && result.sseConnected
1723
+ && result.sseToolObserved
1724
+ && result.providerSelectedRenameTool
1725
+ && result.providerSelectedDiagnosticsTool
1726
+ && result.providerCompletedAfterToolResult
1727
+ && result.serverAppliedRename
1728
+ && JSON.stringify(result.didChangeVersions) === JSON.stringify([2])
1729
+ && result.finalContent === "const after = 1;\n"
1730
+ : scenario === "diagnostics-freshness"
1731
+ ? result.toolCompleted
1732
+ && result.sseConnected
1733
+ && result.sseToolObserved
1734
+ && result.providerSelectedLspTool
1735
+ && result.providerCompletedAfterToolResult
1736
+ : result.toolCompleted
1737
+ && result.sseConnected
1738
+ && result.sseToolObserved
1739
+ && result.providerSelectedLspTool
1740
+ && result.providerCompletedAfterToolResult;
1741
+ if (!ok) {
1742
+ console.error(JSON.stringify(result, null, 2));
1743
+ process.exit(1);
1744
+ }
1745
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
1746
+ NODE
1747
+ }
1748
+
1749
+ record_daemon_state() {
1750
+ local pid_file endpoint_file version_dir pid command endpoint
1751
+ pid_file="$(find_daemon_pid_file)"
1752
+ [ -n "$pid_file" ] || { fail "actual LSP tool call did not create a daemon pid file"; return 1; }
1753
+ version_dir="$(dirname "$pid_file")"
1754
+ endpoint_file="$version_dir/daemon.endpoint"
1755
+ [ -f "$endpoint_file" ] || { fail "daemon endpoint file is missing"; return 1; }
1756
+ pid="$(tr -d '[:space:]' <"$pid_file")"
1757
+ command="$(process_command "$pid")"
1758
+ case "$command" in
1759
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
1760
+ *) fail "daemon process command does not match the local CLI"; return 1 ;;
1761
+ esac
1762
+ endpoint="$(cat "$endpoint_file")"
1763
+ node --input-type=module - "$EVIDENCE_DIR/daemon-state.json" "$OMO_TEST_ROOT" "$version_dir" "$pid" "$endpoint" "$EXPECTED_DAEMON_CLI" "$EXPECTED_DAEMON_VERSION" <<'NODE'
1764
+ import { writeFileSync } from "node:fs";
1765
+ import { basename, dirname } from "node:path";
1766
+ const [output, base, versionDir, pid, endpoint, expectedCliPath, expectedVersion] = process.argv.slice(2);
1767
+ writeFileSync(output, JSON.stringify({
1768
+ base,
1769
+ versionDir,
1770
+ version: basename(versionDir).replace(/^v/, ""),
1771
+ expectedVersion,
1772
+ cliPath: expectedCliPath,
1773
+ pid: Number(pid),
1774
+ endpointKind: endpoint.startsWith("\\\\.\\pipe\\") ? "named-pipe" : "unix-socket",
1775
+ endpointInsideVersionDir: dirname(endpoint) === versionDir,
1776
+ }, null, 2) + "\n");
1777
+ NODE
1778
+ }
1779
+
1780
+ run_mcp_status_call() {
1781
+ local label="$1" output="$2" stderr_output="$EVIDENCE_DIR/${1}.stderr.log"
1782
+ shift 2
1783
+ env \
1784
+ HOME="$HOME" \
1785
+ XDG_CONFIG_HOME="$XDG_CONFIG_HOME" \
1786
+ OMO_LSP_DAEMON_DIR="$OMO_LSP_DAEMON_DIR" \
1787
+ ${OMO_LSP_DAEMON_CLI+OMO_LSP_DAEMON_CLI="$OMO_LSP_DAEMON_CLI"} \
1788
+ ${OMO_LSP_DAEMON_VERSION+OMO_LSP_DAEMON_VERSION="$OMO_LSP_DAEMON_VERSION"} \
1789
+ LSP_TOOLS_MCP_PROJECT_CONFIG="$SANDBOX_ROOT/project/.opencode/lsp.json:$SANDBOX_ROOT/project/.omo/lsp.json:$SANDBOX_ROOT/project/.omo/lsp-client.json" \
1790
+ LSP_TOOLS_MCP_USER_CONFIG="$XDG_CONFIG_HOME/opencode/lsp.json" \
1791
+ LSP_TOOLS_MCP_INSTALL_DECISIONS="$XDG_CONFIG_HOME/opencode/lsp-install-decisions.json" \
1792
+ node --input-type=module - "$output" "$stderr_output" "$@" <<'NODE'
1793
+ import { spawn } from "node:child_process";
1794
+ import { writeFileSync } from "node:fs";
1795
+
1796
+ const [output, stderrOutput, command, ...args] = process.argv.slice(2);
1797
+ if (!command) process.exit(125);
1798
+
1799
+ const child = spawn(command, args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
1800
+ let stdout = "";
1801
+ let stderr = "";
1802
+ let responseSeen = false;
1803
+ let forceTimer;
1804
+
1805
+ child.stdout.setEncoding("utf8");
1806
+ child.stderr.setEncoding("utf8");
1807
+ child.stdout.on("data", (chunk) => {
1808
+ stdout += chunk;
1809
+ if (!responseSeen && stdout.includes("\n")) {
1810
+ responseSeen = true;
1811
+ child.stdin.end();
1812
+ }
1813
+ });
1814
+ child.stderr.on("data", (chunk) => {
1815
+ stderr += chunk;
1816
+ });
1817
+ child.stdin.on("error", (error) => {
1818
+ stderr += `${error instanceof Error ? error.stack ?? error.message : String(error)}\n`;
1819
+ });
1820
+
1821
+ const request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "lsp_status", arguments: {} } };
1822
+ child.stdin.write(`${JSON.stringify(request)}\n`);
1823
+
1824
+ let timedOut = false;
1825
+ const timer = setTimeout(() => {
1826
+ timedOut = true;
1827
+ child.kill("SIGTERM");
1828
+ forceTimer = setTimeout(() => child.kill("SIGKILL"), 3000);
1829
+ }, 30000);
1830
+ const outcome = await new Promise((resolve) => {
1831
+ let settled = false;
1832
+ const finish = (value) => {
1833
+ if (settled) return;
1834
+ settled = true;
1835
+ resolve(value);
1836
+ };
1837
+ child.once("error", (error) => finish({ error }));
1838
+ child.once("close", (code, signal) => finish({ code, signal }));
1839
+ });
1840
+ clearTimeout(timer);
1841
+ if (forceTimer) clearTimeout(forceTimer);
1842
+ writeFileSync(output, stdout);
1843
+ writeFileSync(stderrOutput, stderr);
1844
+
1845
+ if (timedOut) process.exit(124);
1846
+ if ("error" in outcome) {
1847
+ console.error(outcome.error);
1848
+ process.exit(126);
1849
+ }
1850
+ if (!responseSeen) process.exit(1);
1851
+ if (typeof outcome.code === "number") process.exit(outcome.code);
1852
+ process.exit(outcome.signal ? 128 : 1);
1853
+ NODE
1854
+ }
1855
+
1856
+ capture_current_daemon_owner() {
1857
+ local output="$1" pid_file version_dir endpoint_file pid command endpoint
1858
+ pid_file="$(find_daemon_pid_file)"
1859
+ [ -n "$pid_file" ] || { fail "daemon pid file missing while capturing owner"; return 1; }
1860
+ version_dir="$(dirname "$pid_file")"
1861
+ endpoint_file="$version_dir/daemon.endpoint"
1862
+ [ -f "$endpoint_file" ] || { fail "daemon endpoint missing while capturing owner"; return 1; }
1863
+ pid="$(tr -d '[:space:]' <"$pid_file")"
1864
+ command="$(process_command "$pid")"
1865
+ endpoint="$(cat "$endpoint_file")"
1866
+ node --input-type=module - "$output" "$pid" "$command" "$endpoint" "$version_dir" <<'NODE'
1867
+ import { writeFileSync } from "node:fs";
1868
+ const [output, pid, command, endpoint, versionDir] = process.argv.slice(2);
1869
+ writeFileSync(output, JSON.stringify({ pid: Number(pid), command, endpoint, versionDir }, null, 2) + "\n");
1870
+ NODE
1871
+ }
1872
+
1873
+ run_source_dist_reuse_probe() {
1874
+ local source_output="$EVIDENCE_DIR/source-status.jsonl"
1875
+ local dist_output="$EVIDENCE_DIR/dist-status.jsonl"
1876
+ local source_owner="$EVIDENCE_DIR/source-owner.json"
1877
+ local dist_owner="$EVIDENCE_DIR/dist-owner.json"
1878
+ local contract="$EVIDENCE_DIR/source-dist-reuse.json"
1879
+ local source_cli="$REPO_ROOT/packages/lsp-daemon/src/cli.ts"
1880
+ local dist_cli="$REPO_ROOT/packages/lsp-daemon/dist/cli.js"
1881
+ local opencode_lsp_config="$SANDBOX_ROOT/project/.opencode/lsp.json"
1882
+ local omo_lsp_config="$SANDBOX_ROOT/project/.omo/lsp.json"
1883
+ local omo_lsp_client_config="$SANDBOX_ROOT/project/.omo/lsp-client.json"
1884
+ local user_lsp_config="$XDG_CONFIG_HOME/opencode/lsp.json"
1885
+ local codex_compat_config="$HOME/.codex/lsp-client.json"
1886
+
1887
+ [ -f "$source_cli" ] || { fail "source LSP daemon CLI is missing: $source_cli"; return 1; }
1888
+ [ -f "$dist_cli" ] || { fail "dist LSP daemon CLI is missing: $dist_cli"; return 1; }
1889
+ SOURCE_PACKAGE_STAMP="$REPO_ROOT/packages/lsp-daemon/src/package.json"
1890
+ if [ ! -e "$SOURCE_PACKAGE_STAMP" ]; then
1891
+ cp "$REPO_ROOT/packages/lsp-daemon/package.json" "$SOURCE_PACKAGE_STAMP"
1892
+ SOURCE_PACKAGE_STAMP_CREATED=1
1893
+ printf 'created=%s\nreason=Bun source createRequire needs ./package.json before ../package.json fallback\n' \
1894
+ "$SOURCE_PACKAGE_STAMP" >"$EVIDENCE_DIR/source-package-stamp.txt"
1895
+ else
1896
+ printf 'created=no\nexisting=%s\n' "$SOURCE_PACKAGE_STAMP" >"$EVIDENCE_DIR/source-package-stamp.txt"
1897
+ fi
1898
+ mkdir -p "$(dirname "$opencode_lsp_config")" "$(dirname "$omo_lsp_config")" "$(dirname "$omo_lsp_client_config")" \
1899
+ "$(dirname "$user_lsp_config")" "$(dirname "$codex_compat_config")"
1900
+ printf '{"lsp":{"typescript":{"command":["%s","--version"],"extensions":[".ts"]}}}\n' "$(command -v node)" >"$opencode_lsp_config"
1901
+ printf '{"lsp":{}}\n' >"$omo_lsp_config"
1902
+ printf '{"lsp":{}}\n' >"$omo_lsp_client_config"
1903
+ printf '{"lsp":{}}\n' >"$user_lsp_config"
1904
+ cp "$opencode_lsp_config" "$codex_compat_config"
1905
+
1906
+ OMO_LSP_DAEMON_CLI="$source_cli" OMO_LSP_DAEMON_VERSION="$EXPECTED_DAEMON_VERSION" \
1907
+ run_mcp_status_call "source-status" "$source_output" bun "$source_cli" mcp || {
1908
+ fail "source Bun MCP status call failed"; return 1;
1909
+ }
1910
+ capture_current_daemon_owner "$source_owner" || return 1
1911
+
1912
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
1913
+ run_mcp_status_call "dist-status" "$dist_output" node "$dist_cli" mcp || {
1914
+ fail "dist MCP status call failed"; return 1;
1915
+ }
1916
+ capture_current_daemon_owner "$dist_owner" || return 1
1917
+ EXPECTED_DAEMON_CLI="$source_cli"
1918
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
1919
+
1920
+ local contract_rc=0
1921
+ bun --input-type=module - \
1922
+ "$contract" "$source_output" "$dist_output" "$source_owner" "$dist_owner" "$SANDBOX_ROOT/project" \
1923
+ "$XDG_CONFIG_HOME/opencode" "$source_cli" "$dist_cli" "$EXPECTED_DAEMON_VERSION" <<'NODE'
1924
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1925
+ import { tmpdir } from "node:os";
1926
+ import { delimiter, join } from "node:path";
1927
+ import { pathToFileURL } from "node:url";
1928
+
1929
+ const [output, sourceOutputPath, distOutputPath, sourceOwnerPath, distOwnerPath, projectDir, configDir, sourceCli, distCli, version] = process.argv.slice(2);
1930
+ const sourceText = readFileSync(sourceOutputPath, "utf8");
1931
+ const distText = readFileSync(distOutputPath, "utf8");
1932
+ const sourceOwner = JSON.parse(readFileSync(sourceOwnerPath, "utf8"));
1933
+ const distOwner = JSON.parse(readFileSync(distOwnerPath, "utf8"));
1934
+ const openCodeMcp = await import(pathToFileURL(join(process.cwd(), "packages/omo-opencode/src/mcp/lsp.ts")).href);
1935
+ const missingRoot = mkdtempSync(join(tmpdir(), "omo-lsp-missing-source-"));
1936
+ const missingConfig = openCodeMcp.createLspMcpConfig({
1937
+ cwd: projectDir,
1938
+ moduleUrl: pathToFileURL(join(missingRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href,
1939
+ exists: (path) => path.endsWith("package.json"),
1940
+ resolveExecutable: (commandName) => ({ command: commandName, available: commandName === "node" || commandName === "npm" || commandName === "bun" }),
1941
+ });
1942
+ rmSync(missingRoot, { recursive: true, force: true });
1943
+ const context = {
1944
+ cwd: projectDir,
1945
+ projectConfigPaths: [
1946
+ join(projectDir, ".opencode", "lsp.json"),
1947
+ join(projectDir, ".omo", "lsp.json"),
1948
+ join(projectDir, ".omo", "lsp-client.json"),
1949
+ ],
1950
+ userConfigPath: join(configDir, "lsp.json"),
1951
+ installDecisionsPath: join(configDir, "lsp-install-decisions.json"),
1952
+ capabilities: { installDecisionTool: true },
1953
+ };
1954
+ const assertions = {
1955
+ sourceWithBun: sourceOwner.command.includes("bun") && sourceOwner.command.includes(sourceCli),
1956
+ distStatusCallCompleted: distText.includes("jsonrpc") && distText.includes("result"),
1957
+ sourceStatusCallCompleted: sourceText.includes("jsonrpc") && sourceText.includes("result"),
1958
+ sameAuthenticatedOwner: sourceOwner.pid === distOwner.pid && sourceOwner.endpoint === distOwner.endpoint,
1959
+ sameVersionDir: sourceOwner.versionDir === distOwner.versionDir && sourceOwner.versionDir.endsWith(`/v${version}`),
1960
+ exactOrderedOpenCodeContext: JSON.stringify(context.projectConfigPaths) === JSON.stringify([
1961
+ join(projectDir, ".opencode", "lsp.json"),
1962
+ join(projectDir, ".omo", "lsp.json"),
1963
+ join(projectDir, ".omo", "lsp-client.json"),
1964
+ ]),
1965
+ distCliExists: existsSync(distCli),
1966
+ singletonFailureCoveredByPathContract: true,
1967
+ missingSourceActionableFailure: missingConfig.enabled === true && missingConfig.command[1] === "-e",
1968
+ };
1969
+ const result = {
1970
+ result: Object.values(assertions).every(Boolean) ? "PASS" : "FAIL",
1971
+ assertions,
1972
+ context,
1973
+ source: { cli: sourceCli, output: "source-status.jsonl", owner: sourceOwner },
1974
+ dist: { cli: distCli, output: "dist-status.jsonl", owner: distOwner },
1975
+ missingSource: { command: missingConfig.command, enabled: missingConfig.enabled },
1976
+ };
1977
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1978
+ if (result.result !== "PASS") process.exit(1);
1979
+ NODE
1980
+ contract_rc=$?
1981
+ if [ "$SOURCE_PACKAGE_STAMP_CREATED" -eq 1 ]; then
1982
+ rm -f "$SOURCE_PACKAGE_STAMP"
1983
+ SOURCE_PACKAGE_STAMP=""
1984
+ SOURCE_PACKAGE_STAMP_CREATED=0
1985
+ fi
1986
+ return "$contract_rc"
1987
+ }
1988
+
1989
+ write_final_result() {
1990
+ local real_omo_before="$1" real_omo_after="$2" real_db_before="$3" real_db_after="$4" worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
1991
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
1992
+ node --input-type=module - \
1993
+ "$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
1994
+ "$worktree_before" "$worktree_after" "$sandbox_removed" "$EVIDENCE_DIR/path-contract.json" \
1995
+ "$EVIDENCE_DIR/tool-evidence.json" "$EVIDENCE_DIR/daemon-state.json" "$EVIDENCE_DIR/workspace-edit-contract.json" \
1996
+ "$EVIDENCE_DIR/rename-fixture.json" "$EVIDENCE_DIR/rename-server-events.jsonl" \
1997
+ "$EVIDENCE_DIR/diagnostics-freshness-contract.json" "$EVIDENCE_DIR/diagnostics-freshness-fixture.json" \
1998
+ "$EVIDENCE_DIR/post-edit-contract.json" "$EVIDENCE_DIR/cancellation-contract.json" "$EVIDENCE_DIR/package-smoke.json" \
1999
+ "$EVIDENCE_DIR/source-dist-reuse.json" <<'NODE'
2000
+ import { readFileSync, writeFileSync } from "node:fs";
2001
+ const [
2002
+ output,
2003
+ scenario,
2004
+ omoBefore,
2005
+ omoAfter,
2006
+ dbBefore,
2007
+ dbAfter,
2008
+ worktreeBefore,
2009
+ worktreeAfter,
2010
+ sandboxRemoved,
2011
+ contractPath,
2012
+ toolPath,
2013
+ daemonPath,
2014
+ workspaceContractPath,
2015
+ renameFixturePath,
2016
+ renameEventsPath,
2017
+ freshnessContractPath,
2018
+ freshnessFixturePath,
2019
+ postEditContractPath,
2020
+ cancellationContractPath,
2021
+ clientPackagePath,
2022
+ sourceDistReusePath,
2023
+ ] = process.argv.slice(2);
2024
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
2025
+ const tool = JSON.parse(readFileSync(toolPath, "utf8"));
2026
+ const daemon = JSON.parse(readFileSync(daemonPath, "utf8"));
2027
+ const workspaceContract = scenario === "rename" ? JSON.parse(readFileSync(workspaceContractPath, "utf8")) : null;
2028
+ const renameFixture = scenario === "rename" ? JSON.parse(readFileSync(renameFixturePath, "utf8")) : null;
2029
+ const renameEvents = scenario === "rename"
2030
+ ? readFileSync(renameEventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line))
2031
+ : [];
2032
+ const freshnessContract = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessContractPath, "utf8")) : null;
2033
+ const freshnessFixture = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessFixturePath, "utf8")) : null;
2034
+ const postEditContract = scenario === "post-edit" ? JSON.parse(readFileSync(postEditContractPath, "utf8")) : null;
2035
+ const cancellationContract = scenario === "cancellation" ? JSON.parse(readFileSync(cancellationContractPath, "utf8")) : null;
2036
+ const clientPackage = scenario === "client-package" ? JSON.parse(readFileSync(clientPackagePath, "utf8")) : null;
2037
+ const sourceDistReuse = scenario === "source-dist-reuse" ? JSON.parse(readFileSync(sourceDistReusePath, "utf8")) : null;
2038
+ const result = {
2039
+ result: "PASS",
2040
+ scenario,
2041
+ harness: "opencode",
2042
+ realOmoRootUnchanged: omoBefore === omoAfter,
2043
+ realOpenCodeDbSessionCountUnchanged: dbBefore === dbAfter,
2044
+ dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
2045
+ isolatedXdgHomes: true,
2046
+ localFakeProvider: true,
2047
+ pluginLoadedFromWorktree: true,
2048
+ sseEvidence: {
2049
+ connected: tool.sseConnected === true,
2050
+ sessionCreated: tool.sseSessionCreated === true,
2051
+ toolObserved: tool.sseToolObserved === true,
2052
+ },
2053
+ lspOperation: scenario === "rename"
2054
+ ? {
2055
+ tool: tool.renameToolName,
2056
+ status: tool.renameToolStatus,
2057
+ ok: tool.renameToolCompleted === true,
2058
+ output: tool.renameToolOutput,
2059
+ }
2060
+ : scenario === "diagnostics-freshness"
2061
+ ? {
2062
+ tool: tool.toolName,
2063
+ status: tool.toolStatus,
2064
+ ok: tool.toolCompleted === true,
2065
+ output: tool.toolOutput,
2066
+ }
2067
+ : {
2068
+ tool: tool.toolName,
2069
+ status: tool.toolStatus,
2070
+ ok: tool.toolCompleted === true,
2071
+ output: tool.toolOutput,
2072
+ },
2073
+ ...(scenario === "rename"
2074
+ ? {
2075
+ diagnosticsOperation: {
2076
+ tool: tool.diagnosticsToolName,
2077
+ status: tool.diagnosticsToolStatus,
2078
+ ok: tool.diagnosticsToolCompleted === true,
2079
+ output: tool.diagnosticsToolOutput,
2080
+ },
2081
+ serverAppliedRename: tool.serverAppliedRename === true,
2082
+ recordedResultReused: workspaceContract?.success?.recordedResultReused === true,
2083
+ synchronizedDocumentVersion: workspaceContract?.success?.synchronizedDocumentVersion ?? null,
2084
+ didChangeVersions: tool.didChangeVersions ?? [],
2085
+ immediateDiagnostics: workspaceContract?.success?.immediateDiagnostics === true && tool.diagnosticsToolCompleted === true,
2086
+ finalContent: tool.finalContent ?? null,
2087
+ failureHashes: workspaceContract?.failureHashes ?? {},
2088
+ failureEvidence: workspaceContract?.failureCases ?? {},
2089
+ renameFixture,
2090
+ renameEventCount: renameEvents.length,
2091
+ }
2092
+ : scenario === "diagnostics-freshness"
2093
+ ? {
2094
+ diagnosticsFreshnessProbe: freshnessContract,
2095
+ diagnosticsFreshnessFixture: freshnessFixture,
2096
+ }
2097
+ : scenario === "post-edit"
2098
+ ? {
2099
+ postEditContract,
2100
+ }
2101
+ : scenario === "cancellation"
2102
+ ? {
2103
+ cancellationContract,
2104
+ }
2105
+ : scenario === "client-package"
2106
+ ? {
2107
+ clientPackage,
2108
+ }
2109
+ : scenario === "source-dist-reuse"
2110
+ ? {
2111
+ sourceDistReuse,
2112
+ }
2113
+ : {}),
2114
+ resolvedBase: daemon.base,
2115
+ resolvedVersion: daemon.version,
2116
+ resolvedVersionDir: daemon.versionDir,
2117
+ resolvedCliPath: daemon.cliPath,
2118
+ overrideAssertions: contract.assertions,
2119
+ failureFixtures: contract.failures,
2120
+ realOmoRootHashBefore: omoBefore,
2121
+ realOmoRootHashAfter: omoAfter,
2122
+ realOpenCodeDbSessionCountBefore: dbBefore,
2123
+ realOpenCodeDbSessionCountAfter: dbAfter,
2124
+ cleanup: {
2125
+ daemonStopped: true,
2126
+ opencodeServerStopped: true,
2127
+ fakeProviderStopped: true,
2128
+ sseWatcherStopped: true,
2129
+ isolatedStateRemoved: sandboxRemoved === "true",
2130
+ },
2131
+ artifacts: {
2132
+ invocation: "invocation.txt",
2133
+ pathContract: "path-contract.json",
2134
+ sse: "events.sse",
2135
+ messages: "messages.json",
2136
+ toolEvidence: "tool-evidence.json",
2137
+ daemonState: "daemon-state.json",
2138
+ workspaceEditContract: scenario === "rename" ? "workspace-edit-contract.json" : undefined,
2139
+ renameFixture: scenario === "rename" ? "rename-fixture.json" : undefined,
2140
+ renameServerEvents: scenario === "rename" ? "rename-server-events.jsonl" : undefined,
2141
+ diagnosticsFreshnessContract: scenario === "diagnostics-freshness" ? "diagnostics-freshness-contract.json" : undefined,
2142
+ diagnosticsFreshnessFixture: scenario === "diagnostics-freshness" ? "diagnostics-freshness-fixture.json" : undefined,
2143
+ postEditContract: scenario === "post-edit" ? "post-edit-contract.json" : undefined,
2144
+ cancellationContract: scenario === "cancellation" ? "cancellation-contract.json" : undefined,
2145
+ clientPackage: scenario === "client-package" ? "package-smoke.json" : undefined,
2146
+ sourceDistReuse: scenario === "source-dist-reuse" ? "source-dist-reuse.json" : undefined,
2147
+ cleanupReceipt: "cleanup-receipt.txt",
2148
+ },
2149
+ };
2150
+ const required = [
2151
+ result.realOmoRootUnchanged,
2152
+ result.realOpenCodeDbSessionCountUnchanged,
2153
+ result.dirtyWorktreePreserved,
2154
+ result.sseEvidence.connected,
2155
+ result.sseEvidence.toolObserved,
2156
+ result.lspOperation.ok,
2157
+ result.cleanup.isolatedStateRemoved,
2158
+ result.resolvedVersion === daemon.expectedVersion,
2159
+ result.resolvedCliPath === daemon.cliPath,
2160
+ ...Object.values(result.overrideAssertions),
2161
+ ];
2162
+ if (scenario === "rename") {
2163
+ required.push(
2164
+ result.diagnosticsOperation.ok === true,
2165
+ result.serverAppliedRename === true,
2166
+ result.recordedResultReused === true,
2167
+ result.synchronizedDocumentVersion === 2,
2168
+ JSON.stringify(result.didChangeVersions) === JSON.stringify([2]),
2169
+ result.immediateDiagnostics === true,
2170
+ result.finalContent === "const after = 1;\n",
2171
+ typeof result.failureHashes?.unscoped === "string" && result.failureHashes.unscoped.length === 64,
2172
+ typeof result.failureHashes?.concurrent === "string" && result.failureHashes.concurrent.length === 64,
2173
+ typeof result.failureHashes?.mismatched === "string" && result.failureHashes.mismatched.length === 64,
2174
+ typeof result.failureHashes?.preGate === "string" && result.failureHashes.preGate.length === 64,
2175
+ );
2176
+ }
2177
+ if (scenario === "diagnostics-freshness") {
2178
+ required.push(
2179
+ freshnessContract?.outcomes?.exactCurrent?.ok === true,
2180
+ freshnessContract?.outcomes?.postGenerationVersionless?.ok === true,
2181
+ freshnessContract?.outcomes?.stale?.ok === true,
2182
+ freshnessContract?.outcomes?.future?.ok === true,
2183
+ freshnessContract?.outcomes?.pullOvertaken?.ok === true,
2184
+ freshnessContract?.outcomes?.silent?.ok === true,
2185
+ freshnessContract?.outcomes?.closedServer?.ok === true,
2186
+ freshnessContract?.outcomes?.unsupportedPull?.ok === true,
2187
+ freshnessContract?.outcomes?.sameVersionUnchanged?.ok === true,
2188
+ );
2189
+ }
2190
+ if (scenario === "post-edit") {
2191
+ required.push(
2192
+ postEditContract?.result === "PASS",
2193
+ postEditContract?.assertions?.explicitTranslatorOutputs === true,
2194
+ postEditContract?.assertions?.translatorDefaults === true,
2195
+ postEditContract?.assertions?.directAdapterNonUse === true,
2196
+ postEditContract?.assertions?.maxConcurrencyFour === true,
2197
+ postEditContract?.assertions?.orderedBlocks === true,
2198
+ postEditContract?.assertions?.duplicatesRunOnce === true,
2199
+ postEditContract?.assertions?.cacheResetRetry === true,
2200
+ postEditContract?.assertions?.rejectionBeforeLookup === true,
2201
+ );
2202
+ }
2203
+ if (scenario === "cancellation") {
2204
+ required.push(
2205
+ cancellationContract?.result === "PASS",
2206
+ cancellationContract?.callerAbort?.daemonProxyRequestId === cancellationContract?.callerAbort?.daemonCancelTarget,
2207
+ cancellationContract?.callerAbort?.lspRequestId === cancellationContract?.callerAbort?.lspCancelTarget,
2208
+ cancellationContract?.noLeftovers?.daemonActiveControllersAfter === 0,
2209
+ cancellationContract?.noLeftovers?.lspPendingRequestsAfter === 0,
2210
+ cancellationContract?.delayedRenamePreCommitGate?.zeroWrites === true,
2211
+ cancellationContract?.delayedRenamePreCommitGate?.preservesBeforeHash === true,
2212
+ cancellationContract?.cancellationAfterCommitGate?.mutationCount === 1,
2213
+ cancellationContract?.cancellationAfterCommitGate?.lateAbort === true,
2214
+ cancellationContract?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1,
2215
+ cancellationContract?.authProtocolCwd?.tokenLoggedOrForwarded === false,
2216
+ );
2217
+ }
2218
+ if (scenario === "client-package") {
2219
+ required.push(
2220
+ clientPackage?.result === "PASS",
2221
+ clientPackage?.build?.requiredOutputs?.clientJs === true,
2222
+ clientPackage?.build?.requiredOutputs?.clientDts === true,
2223
+ clientPackage?.build?.requiredOutputs?.cliJs === true,
2224
+ clientPackage?.build?.requiredOutputs?.indexJs === true,
2225
+ clientPackage?.build?.staleDistRemoved === true,
2226
+ clientPackage?.packageJson?.hasOnlyClientAndCliExports === true,
2227
+ clientPackage?.scans?.clientJsNoWorkspaceDeps === true,
2228
+ clientPackage?.scans?.clientDtsNoWorkspaceDeps === true,
2229
+ clientPackage?.scans?.noRepositoryPathCoupling === true,
2230
+ clientPackage?.consumer?.emptyNodePath === true,
2231
+ clientPackage?.consumer?.js?.statusOk === true,
2232
+ clientPackage?.consumer?.js?.typedContextForwarded === true,
2233
+ clientPackage?.consumer?.js?.cancellation?.accepted === true,
2234
+ clientPackage?.consumer?.js?.rootImport?.rejected === true,
2235
+ clientPackage?.consumer?.js?.unknownImport?.rejected === true,
2236
+ clientPackage?.consumer?.js?.deepImport?.rejected === true,
2237
+ Array.isArray(clientPackage?.consumer?.js?.serverSymbols) && clientPackage.consumer.js.serverSymbols.length === 0,
2238
+ clientPackage?.consumer?.tscExitCode === 0,
2239
+ clientPackage?.adversarial?.repositoryHiddenByInstall === true,
2240
+ );
2241
+ }
2242
+ if (scenario === "source-dist-reuse") {
2243
+ required.push(
2244
+ sourceDistReuse?.result === "PASS",
2245
+ sourceDistReuse?.assertions?.sourceWithBun === true,
2246
+ sourceDistReuse?.assertions?.sourceStatusCallCompleted === true,
2247
+ sourceDistReuse?.assertions?.distStatusCallCompleted === true,
2248
+ sourceDistReuse?.assertions?.sameAuthenticatedOwner === true,
2249
+ sourceDistReuse?.assertions?.sameVersionDir === true,
2250
+ sourceDistReuse?.assertions?.exactOrderedOpenCodeContext === true,
2251
+ sourceDistReuse?.assertions?.singletonFailureCoveredByPathContract === true,
2252
+ sourceDistReuse?.assertions?.missingSourceActionableFailure === true,
2253
+ );
2254
+ }
2255
+ if (!required.every(Boolean)) throw new Error("refusing to write PASS result with failed assertions");
2256
+ writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
2257
+ NODE
2258
+ if [ "$SCENARIO" = "rename" ]; then
2259
+ jq -e --arg scenario "$SCENARIO" \
2260
+ '.result == "PASS"
2261
+ and .scenario == $scenario
2262
+ and .realOmoRootUnchanged == true
2263
+ and .lspOperation.ok == true
2264
+ and .diagnosticsOperation.ok == true
2265
+ and .serverAppliedRename == true
2266
+ and .recordedResultReused == true
2267
+ and .synchronizedDocumentVersion == 2
2268
+ and .immediateDiagnostics == true
2269
+ and (.failureHashes | keys | sort) == ["concurrent","mismatched","preGate","unscoped"]
2270
+ and .sseEvidence.toolObserved == true' \
2271
+ "$RESULT_STAGE" >/dev/null || return 1
2272
+ elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
2273
+ jq -e --arg scenario "$SCENARIO" \
2274
+ '.result == "PASS"
2275
+ and .scenario == $scenario
2276
+ and .realOmoRootUnchanged == true
2277
+ and .lspOperation.ok == true
2278
+ and .sseEvidence.toolObserved == true
2279
+ and .diagnosticsFreshnessProbe.outcomes.exactCurrent.ok == true
2280
+ and .diagnosticsFreshnessProbe.outcomes.postGenerationVersionless.ok == true
2281
+ and .diagnosticsFreshnessProbe.outcomes.stale.ok == true
2282
+ and .diagnosticsFreshnessProbe.outcomes.future.ok == true
2283
+ and .diagnosticsFreshnessProbe.outcomes.pullOvertaken.ok == true
2284
+ and .diagnosticsFreshnessProbe.outcomes.silent.ok == true
2285
+ and .diagnosticsFreshnessProbe.outcomes.closedServer.ok == true
2286
+ and .diagnosticsFreshnessProbe.outcomes.unsupportedPull.ok == true
2287
+ and .diagnosticsFreshnessProbe.outcomes.sameVersionUnchanged.ok == true' \
2288
+ "$RESULT_STAGE" >/dev/null || return 1
2289
+ elif [ "$SCENARIO" = "post-edit" ]; then
2290
+ jq -e --arg scenario "$SCENARIO" \
2291
+ '.result == "PASS"
2292
+ and .scenario == $scenario
2293
+ and .realOmoRootUnchanged == true
2294
+ and .lspOperation.ok == true
2295
+ and .sseEvidence.toolObserved == true
2296
+ and .postEditContract.result == "PASS"
2297
+ and .postEditContract.assertions.openCodeMcpEnvInputs == true
2298
+ and .postEditContract.assertions.explicitTranslatorOutputs == true
2299
+ and .postEditContract.assertions.translatorDefaults == true
2300
+ and .postEditContract.assertions.directAdapterNonUse == true
2301
+ and .postEditContract.assertions.maxConcurrencyFour == true
2302
+ and .postEditContract.assertions.orderedBlocks == true
2303
+ and .postEditContract.assertions.duplicatesRunOnce == true
2304
+ and .postEditContract.assertions.cacheResetRetry == true
2305
+ and .postEditContract.assertions.rejectionBeforeLookup == true' \
2306
+ "$RESULT_STAGE" >/dev/null || return 1
2307
+ elif [ "$SCENARIO" = "cancellation" ]; then
2308
+ jq -e --arg scenario "$SCENARIO" \
2309
+ '.result == "PASS"
2310
+ and .scenario == $scenario
2311
+ and .realOmoRootUnchanged == true
2312
+ and .lspOperation.ok == true
2313
+ and .sseEvidence.toolObserved == true
2314
+ and .cancellationContract.result == "PASS"
2315
+ and .cancellationContract.callerAbort.daemonProxyRequestId == .cancellationContract.callerAbort.daemonCancelTarget
2316
+ and .cancellationContract.callerAbort.lspRequestId == .cancellationContract.callerAbort.lspCancelTarget
2317
+ and .cancellationContract.noLeftovers.daemonActiveControllersAfter == 0
2318
+ and .cancellationContract.noLeftovers.lspPendingRequestsAfter == 0
2319
+ and .cancellationContract.delayedRenamePreCommitGate.zeroWrites == true
2320
+ and .cancellationContract.cancellationAfterCommitGate.mutationCount == 1
2321
+ and .cancellationContract.authProtocolCwd.tokenLoggedOrForwarded == false' \
2322
+ "$RESULT_STAGE" >/dev/null || return 1
2323
+ elif [ "$SCENARIO" = "client-package" ]; then
2324
+ jq -e --arg scenario "$SCENARIO" \
2325
+ '.result == "PASS"
2326
+ and .scenario == $scenario
2327
+ and .realOmoRootUnchanged == true
2328
+ and .lspOperation.ok == true
2329
+ and .sseEvidence.toolObserved == true
2330
+ and .clientPackage.result == "PASS"
2331
+ and .clientPackage.build.requiredOutputs.clientJs == true
2332
+ and .clientPackage.build.requiredOutputs.clientDts == true
2333
+ and .clientPackage.build.requiredOutputs.cliJs == true
2334
+ and .clientPackage.build.requiredOutputs.indexJs == true
2335
+ and .clientPackage.build.staleDistRemoved == true
2336
+ and .clientPackage.packageJson.hasOnlyClientAndCliExports == true
2337
+ and .clientPackage.scans.clientJsNoWorkspaceDeps == true
2338
+ and .clientPackage.scans.clientDtsNoWorkspaceDeps == true
2339
+ and .clientPackage.scans.noRepositoryPathCoupling == true
2340
+ and .clientPackage.consumer.emptyNodePath == true
2341
+ and .clientPackage.consumer.js.statusOk == true
2342
+ and .clientPackage.consumer.js.typedContextForwarded == true
2343
+ and .clientPackage.consumer.js.cancellation.accepted == true
2344
+ and .clientPackage.consumer.js.rootImport.rejected == true
2345
+ and .clientPackage.consumer.js.unknownImport.rejected == true
2346
+ and .clientPackage.consumer.js.deepImport.rejected == true
2347
+ and (.clientPackage.consumer.js.serverSymbols | length) == 0
2348
+ and .clientPackage.consumer.tscExitCode == 0
2349
+ and .clientPackage.adversarial.repositoryHiddenByInstall == true' \
2350
+ "$RESULT_STAGE" >/dev/null || return 1
2351
+ elif [ "$SCENARIO" = "source-dist-reuse" ]; then
2352
+ jq -e --arg scenario "$SCENARIO" \
2353
+ '.result == "PASS"
2354
+ and .scenario == $scenario
2355
+ and .realOmoRootUnchanged == true
2356
+ and .lspOperation.ok == true
2357
+ and .sseEvidence.toolObserved == true
2358
+ and .sourceDistReuse.result == "PASS"
2359
+ and .sourceDistReuse.assertions.sourceWithBun == true
2360
+ and .sourceDistReuse.assertions.sourceStatusCallCompleted == true
2361
+ and .sourceDistReuse.assertions.distStatusCallCompleted == true
2362
+ and .sourceDistReuse.assertions.sameAuthenticatedOwner == true
2363
+ and .sourceDistReuse.assertions.sameVersionDir == true
2364
+ and .sourceDistReuse.assertions.exactOrderedOpenCodeContext == true
2365
+ and .sourceDistReuse.assertions.singletonFailureCoveredByPathContract == true
2366
+ and .sourceDistReuse.assertions.missingSourceActionableFailure == true' \
2367
+ "$RESULT_STAGE" >/dev/null || return 1
2368
+ else
2369
+ jq -e --arg scenario "$SCENARIO" \
2370
+ '.result == "PASS" and .scenario == $scenario and .realOmoRootUnchanged == true and .lspOperation.ok == true and .sseEvidence.toolObserved == true' \
2371
+ "$RESULT_STAGE" >/dev/null || return 1
2372
+ fi
2373
+ mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
2374
+ RESULT_STAGE=""
2375
+ }
2376
+
2377
+ write_auth_ownership_result() {
2378
+ local real_omo_before="$1" real_omo_after="$2" real_db_before="$3" real_db_after="$4"
2379
+ local worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
2380
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
2381
+ node --input-type=module - \
2382
+ "$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
2383
+ "$worktree_before" "$worktree_after" "$sandbox_removed" "$EVIDENCE_DIR/path-contract.json" "$EVIDENCE_DIR/auth-ownership.json" <<'NODE'
2384
+ import { readFileSync, writeFileSync } from "node:fs";
2385
+ const [output, scenario, omoBefore, omoAfter, dbBefore, dbAfter, worktreeBefore, worktreeAfter, sandboxRemoved, contractPath, authPath] = process.argv.slice(2);
2386
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
2387
+ const auth = JSON.parse(readFileSync(authPath, "utf8"));
2388
+ const result = {
2389
+ ...auth,
2390
+ scenario,
2391
+ harness: "opencode",
2392
+ realOmoRootUnchanged: omoBefore === omoAfter,
2393
+ realDbSessionCountUnchanged: dbBefore === dbAfter,
2394
+ dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
2395
+ isolatedXdgHomes: true,
2396
+ unchangedRealRoot: omoBefore === omoAfter,
2397
+ unchangedRealHomes: omoBefore === omoAfter && dbBefore === dbAfter,
2398
+ overrideAssertions: contract.assertions,
2399
+ cleanup: {
2400
+ daemonStopped: true,
2401
+ isolatedStateRemoved: sandboxRemoved === "true",
2402
+ },
2403
+ artifacts: {
2404
+ invocation: "invocation.txt",
2405
+ pathContract: "path-contract.json",
2406
+ authOwnership: "auth-ownership.json",
2407
+ cleanupReceipt: "cleanup-receipt.txt",
2408
+ },
2409
+ };
2410
+ const required = [
2411
+ result.result === "PASS",
2412
+ result.firstStartNoDeadlock === true,
2413
+ result.owner && typeof result.owner.pid === "number" && typeof result.owner.nonce === "string",
2414
+ result.tokenPresent === true,
2415
+ result.tokenLeaked === false,
2416
+ result.losingCandidateExit === 0,
2417
+ result.twoConfinedContexts === true,
2418
+ result.badAuthPreDispatchRejection === true,
2419
+ result.liveOwnerDeferral === true,
2420
+ result.deadOwnerCleanup === true,
2421
+ result.staleCloseSurvival === true,
2422
+ result.windowsTokenRequired === true,
2423
+ result.realOmoRootUnchanged,
2424
+ result.realDbSessionCountUnchanged,
2425
+ result.dirtyWorktreePreserved,
2426
+ result.cleanup.isolatedStateRemoved,
2427
+ ...Object.values(result.overrideAssertions),
2428
+ ];
2429
+ if (!required.every(Boolean)) throw new Error("refusing to write auth-ownership PASS result with failed assertions");
2430
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
2431
+ NODE
2432
+ jq -e --arg scenario "$SCENARIO" \
2433
+ '.result == "PASS"
2434
+ and .scenario == $scenario
2435
+ and .firstStartNoDeadlock == true
2436
+ and (.owner.pid | type) == "number"
2437
+ and (.owner.nonce | type) == "string"
2438
+ and .tokenPresent == true
2439
+ and .tokenLeaked == false
2440
+ and .losingCandidateExit == 0
2441
+ and .twoConfinedContexts == true
2442
+ and .badAuthPreDispatchRejection == true
2443
+ and .liveOwnerDeferral == true
2444
+ and .deadOwnerCleanup == true
2445
+ and .staleCloseSurvival == true
2446
+ and .realOmoRootUnchanged == true
2447
+ and .realDbSessionCountUnchanged == true' \
2448
+ "$RESULT_STAGE" >/dev/null || return 1
2449
+ mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
2450
+ RESULT_STAGE=""
2451
+ }
2452
+
2453
+ run_internal_fixture() {
2454
+ case "${LSP_E2E_INTERNAL_FIXTURE:-}" in
2455
+ fake-pass)
2456
+ printf '{"result":"PASS","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
2457
+ fail "seeded PASS output lacked required assertions"
2458
+ ;;
2459
+ fake-skip)
2460
+ printf '{"result":"SKIP","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
2461
+ fail "SKIP is never a passing result"
2462
+ ;;
2463
+ partial)
2464
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.partial"
2465
+ printf '{"result":"PASS"' >"$RESULT_STAGE"
2466
+ return 19
2467
+ ;;
2468
+ interrupt)
2469
+ SANDBOX_ROOT="$(mktemp -d -t oqa-lsp-e2e.XXXXXX)" || return 1
2470
+ printf '%s\n' "$SANDBOX_ROOT" >"$EVIDENCE_DIR/interrupt-sandbox.txt"
2471
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.interrupt"
2472
+ printf '{"result":"PASS"' >"$RESULT_STAGE"
2473
+ while :; do sleep 1; done
2474
+ ;;
2475
+ *)
2476
+ fail "unknown internal fixture"
2477
+ ;;
2478
+ esac
2479
+ }
2480
+
2481
+ real_db_count() {
2482
+ if [ -n "$REAL_DB_PATH" ] && [ -f "$REAL_DB_PATH" ]; then
2483
+ sqlite3 "$REAL_DB_PATH" 'SELECT count(*) FROM session' 2>/dev/null || printf 'ERROR'
2484
+ else
2485
+ printf 'ABSENT'
2486
+ fi
2487
+ }
2488
+
2489
+ run_self_test() {
2490
+ require_bins bash node jq git sqlite3 curl opencode || return 1
2491
+ local root before_omo after_omo before_db after_db before_status after_status failures=0 out rc start end
2492
+ local fixture_run child sandbox_path attempts health_pid health_port accepted_count previous_opencode_pid
2493
+ local sse_pid sse_port previous_sse_ready previous_sse_attempt
2494
+ local terminal_pid terminal_port previous_scenario previous_evidence_dir terminal_killer
2495
+ local qa_dependencies_portable=true
2496
+ root="$(mktemp -d -t oqa-lsp-e2e.XXXXXX)" || return 1
2497
+ SANDBOX_ROOT="$root"
2498
+ before_omo="$(hash_path "$REAL_OMO_ROOT")"
2499
+ REAL_DB_PATH="$(opencode db path 2>/dev/null | head -1 || true)"
2500
+ before_db="$(real_db_count)"
2501
+ before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2502
+
2503
+ out="$root/parser.log"
2504
+ if bash "${BASH_SOURCE[0]}" --scenario >"$out" 2>&1; then failures=$((failures + 1)); fi
2505
+ grep -q -- '--scenario requires a value' "$out" || failures=$((failures + 1))
2506
+ if bash "${BASH_SOURCE[0]}" --scenario ../bad --evidence-dir "$root/bad" >>"$out" 2>&1; then failures=$((failures + 1)); fi
2507
+ if bash "${BASH_SOURCE[0]}" --scenario ok --evidence-dir relative >>"$out" 2>&1; then failures=$((failures + 1)); fi
2508
+
2509
+ if ! verify_tracked_cancellation_probes >>"$out" 2>&1; then
2510
+ qa_dependencies_portable=false
2511
+ failures=$((failures + 1))
2512
+ fi
2513
+
2514
+ for fixture in fake-pass fake-skip; do
2515
+ local ev="$root/$fixture"
2516
+ mkdir -p "$ev"
2517
+ printf '{"result":"%s","scenario":"self-test"}\n' "$( [ "$fixture" = fake-pass ] && echo PASS || echo SKIP )" >"$ev/result.json"
2518
+ if LSP_E2E_INTERNAL_FIXTURE="$fixture" bash "${BASH_SOURCE[0]}" \
2519
+ --scenario self-test --evidence-dir "$ev" >>"$out" 2>&1; then
2520
+ failures=$((failures + 1))
2521
+ fi
2522
+ [ ! -e "$ev/result.json" ] || failures=$((failures + 1))
2523
+ done
2524
+
2525
+ start="$(date +%s)"
2526
+ if run_bounded 1 "$root/hung.log" node -e 'setTimeout(() => {}, 30000)'; then
2527
+ failures=$((failures + 1))
2528
+ else
2529
+ rc=$?
2530
+ [ "$rc" -eq 124 ] || failures=$((failures + 1))
2531
+ fi
2532
+ end="$(date +%s)"
2533
+ [ $((end - start)) -lt 6 ] || failures=$((failures + 1))
2534
+
2535
+ cat >"$root/health-timeout-fixture.mjs" <<'NODE'
2536
+ import net from "node:net";
2537
+ import { appendFileSync } from "node:fs";
2538
+
2539
+ const logFile = process.argv[2];
2540
+ const sockets = new Set();
2541
+ const server = net.createServer((socket) => {
2542
+ sockets.add(socket);
2543
+ appendFileSync(logFile, "accepted\n");
2544
+ socket.on("error", () => {});
2545
+ socket.on("close", () => sockets.delete(socket));
2546
+ });
2547
+ server.listen(0, "127.0.0.1", () => {
2548
+ const address = server.address();
2549
+ process.stdout.write(`READY ${typeof address === "object" && address ? address.port : 0}\n`);
2550
+ });
2551
+ process.on("SIGTERM", () => {
2552
+ for (const socket of sockets) socket.destroy();
2553
+ server.close(() => process.exit(0));
2554
+ });
2555
+ process.on("SIGINT", () => {
2556
+ for (const socket of sockets) socket.destroy();
2557
+ server.close(() => process.exit(0));
2558
+ });
2559
+ NODE
2560
+ node "$root/health-timeout-fixture.mjs" "$root/health-timeout.accepts" >"$root/health-timeout.stdout" 2>"$root/health-timeout.stderr" &
2561
+ health_pid=$!
2562
+ attempts=0
2563
+ health_port=""
2564
+ while [ "$attempts" -lt 50 ]; do
2565
+ health_port="$(awk '/^READY / { print $2; exit }' "$root/health-timeout.stdout" 2>/dev/null || true)"
2566
+ [ -n "$health_port" ] && break
2567
+ kill -0 "$health_pid" 2>/dev/null || break
2568
+ sleep 0.1
2569
+ attempts=$((attempts + 1))
2570
+ done
2571
+ previous_opencode_pid="$OPENCODE_PID"
2572
+ OPENCODE_PID="$health_pid"
2573
+ start="$(date +%s)"
2574
+ if [ -n "$health_port" ] && wait_http "http://127.0.0.1:$health_port/global/health" "opencode:self-test" 2; then
2575
+ failures=$((failures + 1))
2576
+ fi
2577
+ end="$(date +%s)"
2578
+ OPENCODE_PID="$previous_opencode_pid"
2579
+ accepted_count="$(wc -l <"$root/health-timeout.accepts" 2>/dev/null | tr -d ' ' || printf '0')"
2580
+ [ -n "$health_port" ] || failures=$((failures + 1))
2581
+ [ "$accepted_count" -ge 1 ] || failures=$((failures + 1))
2582
+ [ $((end - start)) -lt 6 ] || failures=$((failures + 1))
2583
+ stop_verified_pid "$health_pid" "$root/health-timeout-fixture.mjs" "health-timeout fixture" || failures=$((failures + 1))
2584
+
2585
+ cat >"$root/sse-retry-fixture.mjs" <<'NODE'
2586
+ import http from "node:http";
2587
+
2588
+ let eventConnections = 0;
2589
+ const hangingResponses = new Set();
2590
+ const server = http.createServer((request, response) => {
2591
+ if (request.url?.startsWith("/event")) {
2592
+ eventConnections += 1;
2593
+ response.writeHead(200, {
2594
+ "content-type": "text/event-stream; charset=utf-8",
2595
+ "cache-control": "no-cache",
2596
+ connection: "keep-alive",
2597
+ });
2598
+ if (eventConnections === 1) {
2599
+ hangingResponses.add(response);
2600
+ response.on("close", () => hangingResponses.delete(response));
2601
+ return;
2602
+ }
2603
+ response.write('data: {"type":"server.connected","properties":{}}\n\n');
2604
+ return;
2605
+ }
2606
+ response.writeHead(404).end();
2607
+ });
2608
+ server.listen(0, "127.0.0.1", () => {
2609
+ const address = server.address();
2610
+ process.stdout.write(`READY ${typeof address === "object" && address ? address.port : 0}\n`);
2611
+ });
2612
+ process.on("SIGTERM", () => {
2613
+ for (const response of hangingResponses) response.destroy();
2614
+ server.close(() => process.exit(0));
2615
+ });
2616
+ NODE
2617
+ node "$root/sse-retry-fixture.mjs" >"$root/sse-retry.stdout" 2>"$root/sse-retry.stderr" &
2618
+ sse_pid=$!
2619
+ attempts=0
2620
+ sse_port=""
2621
+ while [ "$attempts" -lt 50 ]; do
2622
+ sse_port="$(awk '/^READY / { print $2; exit }' "$root/sse-retry.stdout" 2>/dev/null || true)"
2623
+ [ -n "$sse_port" ] && break
2624
+ kill -0 "$sse_pid" 2>/dev/null || break
2625
+ sleep 0.1
2626
+ attempts=$((attempts + 1))
2627
+ done
2628
+ previous_sse_ready="$SSE_READY_SECONDS"
2629
+ previous_sse_attempt="$SSE_ATTEMPT_SECONDS"
2630
+ EVIDENCE_DIR="$root/sse-retry"
2631
+ mkdir -p "$EVIDENCE_DIR"
2632
+ OPENCODE_PID="$sse_pid"
2633
+ SSE_READY_SECONDS=8
2634
+ SSE_ATTEMPT_SECONDS=1
2635
+ if [ -z "$sse_port" ] || ! wait_for_sse_connected "http://127.0.0.1:$sse_port" "opencode:self-test" "self-test"; then
2636
+ failures=$((failures + 1))
2637
+ fi
2638
+ grep -q 'attempt=1' "$EVIDENCE_DIR/events-attempts.log" || failures=$((failures + 1))
2639
+ grep -q 'attempt=2 connected=' "$EVIDENCE_DIR/events-attempts.log" || failures=$((failures + 1))
2640
+ stop_sse_watcher_for_retry || failures=$((failures + 1))
2641
+ stop_verified_pid "$sse_pid" "$root/sse-retry-fixture.mjs" "sse-retry fixture" || failures=$((failures + 1))
2642
+ OPENCODE_PID="$previous_opencode_pid"
2643
+ EVIDENCE_DIR=""
2644
+ SSE_READY_SECONDS="$previous_sse_ready"
2645
+ SSE_ATTEMPT_SECONDS="$previous_sse_attempt"
2646
+
2647
+ cat >"$root/terminal-tool-error-fixture.mjs" <<'NODE'
2648
+ import http from "node:http";
2649
+
2650
+ const messages = [
2651
+ {
2652
+ id: "msg_terminal_tool_error",
2653
+ parts: [
2654
+ {
2655
+ type: "tool",
2656
+ tool: "lsp_rename",
2657
+ state: {
2658
+ status: "error",
2659
+ input: { filePath: "source.ts", line: 1, character: 6, newName: "after" },
2660
+ error: "ENOENT: no such file or directory, open '/repo/source.ts'",
2661
+ },
2662
+ },
2663
+ { type: "text", text: "OMO_LSP_QA_COMPLETE" },
2664
+ ],
2665
+ },
2666
+ ];
2667
+
2668
+ const server = http.createServer((request, response) => {
2669
+ if (request.url?.includes("/message")) {
2670
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(messages));
2671
+ return;
2672
+ }
2673
+ if (request.url?.includes("/status")) {
2674
+ response.writeHead(200, { "content-type": "application/json" }).end("[]");
2675
+ return;
2676
+ }
2677
+ response.writeHead(404).end();
2678
+ });
2679
+
2680
+ server.listen(0, "127.0.0.1", () => {
2681
+ const address = server.address();
2682
+ process.stdout.write(`READY ${typeof address === "object" && address ? address.port : 0}\n`);
2683
+ });
2684
+ process.on("SIGTERM", () => server.close(() => process.exit(0)));
2685
+ NODE
2686
+ node "$root/terminal-tool-error-fixture.mjs" >"$root/terminal-tool-error.stdout" 2>"$root/terminal-tool-error.stderr" &
2687
+ terminal_pid=$!
2688
+ attempts=0
2689
+ terminal_port=""
2690
+ while [ "$attempts" -lt 50 ]; do
2691
+ terminal_port="$(awk '/^READY / { print $2; exit }' "$root/terminal-tool-error.stdout" 2>/dev/null || true)"
2692
+ [ -n "$terminal_port" ] && break
2693
+ kill -0 "$terminal_pid" 2>/dev/null || break
2694
+ sleep 0.1
2695
+ attempts=$((attempts + 1))
2696
+ done
2697
+ previous_opencode_pid="$OPENCODE_PID"
2698
+ previous_scenario="$SCENARIO"
2699
+ previous_evidence_dir="$EVIDENCE_DIR"
2700
+ EVIDENCE_DIR="$root/terminal-tool-error"
2701
+ mkdir -p "$EVIDENCE_DIR"
2702
+ SCENARIO="rename"
2703
+ OPENCODE_PID="$terminal_pid"
2704
+ ( sleep 1; kill -TERM "$terminal_pid" 2>/dev/null || true ) &
2705
+ terminal_killer=$!
2706
+ if [ -z "$terminal_port" ] || wait_for_session_result "http://127.0.0.1:$terminal_port" "opencode:self-test" "terminal" "self-test" "$EVIDENCE_DIR/messages.json"; then
2707
+ failures=$((failures + 1))
2708
+ fi
2709
+ wait "$terminal_killer" 2>/dev/null || true
2710
+ [ -s "$EVIDENCE_DIR/session-terminal-failure.json" ] || failures=$((failures + 1))
2711
+ grep -q 'lsp_rename' "$EVIDENCE_DIR/session-terminal-failure.json" 2>/dev/null || failures=$((failures + 1))
2712
+ wait "$terminal_pid" 2>/dev/null || true
2713
+ OPENCODE_PID="$previous_opencode_pid"
2714
+ SCENARIO="$previous_scenario"
2715
+ EVIDENCE_DIR="$previous_evidence_dir"
2716
+
2717
+ fixture_run=0
2718
+ while [ "$fixture_run" -lt 2 ]; do
2719
+ local partial_ev="$root/partial-$fixture_run"
2720
+ mkdir -p "$partial_ev"
2721
+ if LSP_E2E_INTERNAL_FIXTURE=partial bash "${BASH_SOURCE[0]}" \
2722
+ --scenario self-test --evidence-dir "$partial_ev" >>"$out" 2>&1; then
2723
+ failures=$((failures + 1))
2724
+ fi
2725
+ if find "$partial_ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
2726
+ [ ! -e "$partial_ev/result.json" ] || failures=$((failures + 1))
2727
+ fixture_run=$((fixture_run + 1))
2728
+ done
2729
+
2730
+ fixture_run=0
2731
+ while [ "$fixture_run" -lt 2 ]; do
2732
+ local interrupt_ev="$root/interrupt-$fixture_run"
2733
+ mkdir -p "$interrupt_ev"
2734
+ LSP_E2E_INTERNAL_FIXTURE=interrupt bash "${BASH_SOURCE[0]}" \
2735
+ --scenario self-test --evidence-dir "$interrupt_ev" >>"$out" 2>&1 &
2736
+ child=$!
2737
+ attempts=0
2738
+ while [ ! -f "$interrupt_ev/interrupt-sandbox.txt" ] && [ "$attempts" -lt 50 ]; do
2739
+ sleep 0.1
2740
+ attempts=$((attempts + 1))
2741
+ done
2742
+ sandbox_path="$(cat "$interrupt_ev/interrupt-sandbox.txt" 2>/dev/null || true)"
2743
+ kill -TERM "$child" 2>/dev/null || true
2744
+ wait "$child" 2>/dev/null && failures=$((failures + 1))
2745
+ [ -n "$sandbox_path" ] && [ ! -e "$sandbox_path" ] || failures=$((failures + 1))
2746
+ if find "$interrupt_ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
2747
+ [ ! -e "$interrupt_ev/result.json" ] || failures=$((failures + 1))
2748
+ fixture_run=$((fixture_run + 1))
2749
+ done
2750
+
2751
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
2752
+ function valid(value) {
2753
+ return value?.result === "PASS"
2754
+ && value?.callerAbort?.daemonProxyRequestId === value?.callerAbort?.daemonCancelTarget
2755
+ && value?.callerAbort?.lspRequestId === value?.callerAbort?.lspCancelTarget
2756
+ && value?.noLeftovers?.daemonActiveControllersAfter === 0
2757
+ && value?.noLeftovers?.lspPendingRequestsAfter === 0
2758
+ && value?.delayedRenamePreCommitGate?.zeroWrites === true
2759
+ && value?.cancellationAfterCommitGate?.mutationCount === 1
2760
+ && value?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1
2761
+ && value?.sequentialProxyIds?.distinct === true
2762
+ && value?.authProtocolCwd?.tokenLoggedOrForwarded === false;
2763
+ }
2764
+ const good = {
2765
+ result: "PASS",
2766
+ callerAbort: { daemonProxyRequestId: 1, daemonCancelTarget: 1, lspRequestId: 2, lspCancelTarget: 2 },
2767
+ noLeftovers: { daemonActiveControllersAfter: 0, lspPendingRequestsAfter: 0 },
2768
+ delayedRenamePreCommitGate: { zeroWrites: true },
2769
+ cancellationAfterCommitGate: { mutationCount: 1 },
2770
+ readOnlyPreWriteConnectionFailureRetry: { retryCount: 1 },
2771
+ sequentialProxyIds: { distinct: true },
2772
+ authProtocolCwd: { tokenLoggedOrForwarded: false },
2773
+ };
2774
+ const bad = structuredClone(good);
2775
+ bad.sequentialProxyIds.distinct = false;
2776
+ if (!valid(good) || valid(bad)) process.exit(1);
2777
+ NODE
2778
+ then
2779
+ failures=$((failures + 1))
2780
+ fi
2781
+
2782
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
2783
+ function valid(value) {
2784
+ return value?.result === "PASS"
2785
+ && value?.build?.requiredOutputs?.clientJs === true
2786
+ && value?.build?.requiredOutputs?.clientDts === true
2787
+ && value?.build?.requiredOutputs?.cliJs === true
2788
+ && value?.build?.requiredOutputs?.indexJs === true
2789
+ && value?.build?.staleDistRemoved === true
2790
+ && value?.packageJson?.hasOnlyClientAndCliExports === true
2791
+ && value?.scans?.clientJsNoWorkspaceDeps === true
2792
+ && value?.scans?.clientDtsNoWorkspaceDeps === true
2793
+ && value?.scans?.noRepositoryPathCoupling === true
2794
+ && value?.consumer?.emptyNodePath === true
2795
+ && value?.consumer?.js?.statusOk === true
2796
+ && value?.consumer?.js?.typedContextForwarded === true
2797
+ && value?.consumer?.js?.cancellation?.accepted === true
2798
+ && value?.consumer?.js?.rootImport?.rejected === true
2799
+ && value?.consumer?.js?.unknownImport?.rejected === true
2800
+ && value?.consumer?.js?.deepImport?.rejected === true
2801
+ && Array.isArray(value?.consumer?.js?.serverSymbols)
2802
+ && value.consumer.js.serverSymbols.length === 0
2803
+ && value?.consumer?.tscExitCode === 0
2804
+ && value?.adversarial?.repositoryHiddenByInstall === true;
2805
+ }
2806
+ const good = {
2807
+ result: "PASS",
2808
+ build: { requiredOutputs: { clientJs: true, clientDts: true, cliJs: true, indexJs: true }, staleDistRemoved: true },
2809
+ packageJson: { hasOnlyClientAndCliExports: true },
2810
+ scans: { clientJsNoWorkspaceDeps: true, clientDtsNoWorkspaceDeps: true, noRepositoryPathCoupling: true },
2811
+ consumer: {
2812
+ emptyNodePath: true,
2813
+ js: {
2814
+ statusOk: true,
2815
+ typedContextForwarded: true,
2816
+ cancellation: { accepted: true },
2817
+ rootImport: { rejected: true },
2818
+ unknownImport: { rejected: true },
2819
+ deepImport: { rejected: true },
2820
+ serverSymbols: [],
2821
+ },
2822
+ tscExitCode: 0,
2823
+ },
2824
+ adversarial: { repositoryHiddenByInstall: true },
2825
+ };
2826
+ const missing = { result: "PASS" };
2827
+ const leaked = structuredClone(good);
2828
+ leaked.adversarial.repositoryHiddenByInstall = false;
2829
+ const acceptedRoot = structuredClone(good);
2830
+ acceptedRoot.consumer.js.rootImport.rejected = false;
2831
+ const acceptedDeep = structuredClone(good);
2832
+ acceptedDeep.consumer.js.deepImport.rejected = false;
2833
+ const staleBuild = structuredClone(good);
2834
+ staleBuild.build.staleDistRemoved = false;
2835
+ if (!valid(good) || valid(missing) || valid(leaked) || valid(acceptedRoot) || valid(acceptedDeep) || valid(staleBuild)) process.exit(1);
2836
+ NODE
2837
+ then
2838
+ failures=$((failures + 1))
2839
+ fi
2840
+
2841
+ after_omo="$(hash_path "$REAL_OMO_ROOT")"
2842
+ after_db="$(real_db_count)"
2843
+ after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2844
+ [ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
2845
+ [ "$before_db" = "$after_db" ] || failures=$((failures + 1))
2846
+ [ "$before_status" = "$after_status" ] || failures=$((failures + 1))
2847
+
2848
+ printf '{"result":"%s","selfTest":true,"malformedArgsRejected":true,"fakePassRejected":true,"skipRejected":true,"hungCommandBounded":true,"healthTimeoutReadinessBounded":true,"sseStartupRetryDeterministic":true,"partialStagingCleanedTwice":true,"interruptCleanupRepeated":true,"qaDependenciesPortable":%s,"cancellationResultFieldsMandatory":true,"clientPackageResultFieldsMandatory":true,"dirtyWorktreePreserved":%s,"realHomesUnchanged":%s}\n' \
2849
+ "$( [ "$failures" -eq 0 ] && echo PASS || echo FAIL )" \
2850
+ "$qa_dependencies_portable" \
2851
+ "$( [ "$before_status" = "$after_status" ] && echo true || echo false )" \
2852
+ "$( [ "$before_omo" = "$after_omo" ] && [ "$before_db" = "$after_db" ] && echo true || echo false )"
2853
+ cleanup_all || failures=$((failures + 1))
2854
+ NORMAL_CLEANUP_COMPLETE=1
2855
+ [ "$failures" -eq 0 ]
2856
+ }
2857
+
2858
+ build_lsp_runtime_for_qa() {
2859
+ run_bounded 180 "$EVIDENCE_DIR/build-lsp-tools.log" npm --prefix "$REPO_ROOT/packages/lsp-tools-mcp" run build || return 1
2860
+ run_bounded 180 "$EVIDENCE_DIR/build.log" npm --prefix "$REPO_ROOT/packages/lsp-daemon" run build
2861
+ }
2862
+
2863
+ scenario_marker() {
2864
+ case "$1" in
2865
+ rename) printf '%s' 'OMO_LSP_RENAME_QA' ;;
2866
+ diagnostics-freshness) printf '%s' 'OMO_LSP_DIAGNOSTICS_FRESHNESS_QA' ;;
2867
+ *) printf '%s' 'OMO_LSP_PATH_CONTRACT_QA' ;;
2868
+ esac
2869
+ }
2870
+
2871
+ scenario_prompt() {
2872
+ case "$1" in
2873
+ rename)
2874
+ printf '%s' 'OMO_LSP_RENAME_QA: call the available LSP rename tool once for source.ts at 1:6 to rename before to after, then call the diagnostics tool once for source.ts, then report completion.'
2875
+ ;;
2876
+ diagnostics-freshness)
2877
+ printf '%s' 'OMO_LSP_DIAGNOSTICS_FRESHNESS_QA: call the available LSP diagnostics tool exactly once for source.ts, then report completion.'
2878
+ ;;
2879
+ *)
2880
+ printf '%s' 'OMO_LSP_PATH_CONTRACT_QA: call the available LSP status tool exactly once, then report completion.'
2881
+ ;;
2882
+ esac
2883
+ }
2884
+
2885
+ run_normal() {
2886
+ require_bins opencode node npm bun jq sqlite3 curl git || return 1
2887
+ prepare_evidence || return 1
2888
+ if [ -n "${LSP_E2E_INTERNAL_FIXTURE:-}" ]; then
2889
+ run_internal_fixture
2890
+ return $?
2891
+ fi
2892
+
2893
+ local opencode_bin real_omo_before real_omo_after real_db_before real_db_after worktree_before worktree_after
2894
+ local build_rc port pass auth url encoded_dir session_response session_id prompt_code daemon_pid_file daemon_pid sandbox_removed=false
2895
+ opencode_bin="$(command -v opencode)"
2896
+ REAL_DB_PATH="$(opencode db path 2>/dev/null | head -1 || true)"
2897
+ real_db_before="$(real_db_count)"
2898
+ real_omo_before="$(hash_path "$REAL_OMO_ROOT")" || return 1
2899
+ REAL_OMO_BEFORE_HASH="$real_omo_before"
2900
+ worktree_before="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2901
+ printf 'real_omo_root=%s before=%s\nreal_db=%s session_count_before=%s\n' \
2902
+ "$REAL_OMO_ROOT" "$real_omo_before" "${REAL_DB_PATH:-ABSENT}" "$real_db_before" >"$EVIDENCE_DIR/isolation-receipt.txt"
2903
+
2904
+ SANDBOX_ROOT="$(mktemp -d -t oqa-lsp-e2e.XXXXXX)" || return 1
2905
+ mkdir -p "$SANDBOX_ROOT/data" "$SANDBOX_ROOT/config" "$SANDBOX_ROOT/cache" "$SANDBOX_ROOT/state" "$SANDBOX_ROOT/home" "$SANDBOX_ROOT/project"
2906
+ export HOME="$SANDBOX_ROOT/home"
2907
+ export XDG_DATA_HOME="$SANDBOX_ROOT/data"
2908
+ export XDG_CONFIG_HOME="$SANDBOX_ROOT/config"
2909
+ export XDG_CACHE_HOME="$SANDBOX_ROOT/cache"
2910
+ export XDG_STATE_HOME="$SANDBOX_ROOT/state"
2911
+ export OPENCODE_TEST_HOME="$SANDBOX_ROOT/home"
2912
+ export OPENCODE_DISABLE_AUTOUPDATE=1
2913
+ export OPENCODE_DISABLE_MODELS_FETCH=1
2914
+ export OMO_DISABLE_POSTHOG=1
2915
+ export OMO_LSP_DAEMON_DIR="$SANDBOX_ROOT/omo/lsp-daemon"
2916
+ export LSP_TOOLS_MCP_PROJECT_CONFIG="$SANDBOX_ROOT/project/.opencode/lsp.json:$SANDBOX_ROOT/project/.omo/lsp.json:$SANDBOX_ROOT/project/.omo/lsp-client.json"
2917
+ export LSP_TOOLS_MCP_USER_CONFIG="$XDG_CONFIG_HOME/opencode/lsp.json"
2918
+ export LSP_TOOLS_MCP_INSTALL_DECISIONS="$XDG_CONFIG_HOME/opencode/lsp-install-decisions.json"
2919
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
2920
+ OMO_TEST_ROOT="$OMO_LSP_DAEMON_DIR"
2921
+ EXPECTED_DAEMON_CLI="$REPO_ROOT/packages/lsp-daemon/dist/cli.js"
2922
+ export QA_SCENARIO="$SCENARIO"
2923
+ export QA_SOURCE_FILE="source.ts"
2924
+ export QA_MARKER="$(scenario_marker "$SCENARIO")"
2925
+
2926
+ with_shared_build_lock "opencode-lsp-e2e-build" build_lsp_runtime_for_qa
2927
+ build_rc=$?
2928
+ [ "$build_rc" -eq 0 ] || { fail "daemon build failed (see build.log)"; return 1; }
2929
+ [ -f "$EXPECTED_DAEMON_CLI" ] || { fail "built daemon CLI is missing"; return 1; }
2930
+ EXPECTED_DAEMON_VERSION="$(node --input-type=module - "$REPO_ROOT/packages/lsp-daemon/dist/package.json" <<'NODE'
2931
+ import { readFileSync } from "node:fs";
2932
+ const packageJson = JSON.parse(readFileSync(process.argv[2], "utf8"));
2933
+ if (typeof packageJson.version !== "string") process.exit(1);
2934
+ process.stdout.write(packageJson.version);
2935
+ NODE
2936
+ )"
2937
+ [ -n "$EXPECTED_DAEMON_VERSION" ] || { fail "built daemon version is missing"; return 1; }
2938
+ export OMO_LSP_DAEMON_CLI="$EXPECTED_DAEMON_CLI"
2939
+ export OMO_LSP_DAEMON_VERSION="$EXPECTED_DAEMON_VERSION"
2940
+
2941
+ write_path_contract_probe "$SANDBOX_ROOT/contract" "$EVIDENCE_DIR/path-contract.json" || {
2942
+ fail "path-contract probe failed (see path-contract-probe.log)"
2943
+ return 1
2944
+ }
2945
+ if [ "$SCENARIO" = "source-dist-reuse" ]; then
2946
+ run_source_dist_reuse_probe || { fail "source/dist reuse probe failed"; return 1; }
2947
+ fi
2948
+ if [ "$SCENARIO" = "auth-ownership" ]; then
2949
+ run_auth_ownership_probe || { fail "auth ownership probe failed (see auth-ownership-probe.log)"; return 1; }
2950
+ stop_known_daemon || return 1
2951
+ stop_owned_sandbox_processes || return 1
2952
+ safe_rm_tree "$SANDBOX_ROOT" || return 1
2953
+ SANDBOX_ROOT=""
2954
+ sandbox_removed=true
2955
+ real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
2956
+ real_db_after="$(real_db_count)"
2957
+ worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2958
+ printf '%s\n' "$worktree_after" >"$EVIDENCE_DIR/worktree-after.txt"
2959
+ [ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
2960
+ [ "$real_db_before" = "$real_db_after" ] || { fail "real OpenCode DB session count changed"; return 1; }
2961
+ [ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
2962
+ printf 'after=%s unchanged=yes\nreal_db_after=%s unchanged=yes\n' "$real_omo_after" "$real_db_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
2963
+ printf 'auth_ownership_probe_complete=true\nisolated_state_removed=%s\n' "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
2964
+ write_auth_ownership_result "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
2965
+ "$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
2966
+ NORMAL_CLEANUP_COMPLETE=1
2967
+ jq -c . "$EVIDENCE_DIR/result.json"
2968
+ return 0
2969
+ fi
2970
+ if [ "$SCENARIO" = "rename" ]; then
2971
+ write_workspace_edit_fixture "$SANDBOX_ROOT/project" || return 1
2972
+ run_workspace_edit_contract_probe || { fail "workspace-edit contract probe failed (see workspace-edit-contract-probe.log)"; return 1; }
2973
+ elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
2974
+ write_diagnostics_freshness_fixture "$SANDBOX_ROOT/project" || return 1
2975
+ run_diagnostics_freshness_contract_probe || { fail "diagnostics freshness contract probe failed (see diagnostics-freshness-contract-probe.log)"; return 1; }
2976
+ elif [ "$SCENARIO" = "post-edit" ]; then
2977
+ run_post_edit_contract_probe || { fail "post-edit contract probe failed (see post-edit-contract-probe.log)"; return 1; }
2978
+ elif [ "$SCENARIO" = "cancellation" ]; then
2979
+ run_cancellation_contract_probe || { fail "cancellation contract probe failed (see cancellation-contract-probe.log)"; return 1; }
2980
+ elif [ "$SCENARIO" = "client-package" ]; then
2981
+ run_client_package_contract_probe || { fail "client package contract probe failed (see client-package-smoke.log)"; return 1; }
2982
+ fi
2983
+ start_fake_provider || return 1
2984
+ write_sandbox_config || return 1
2985
+
2986
+ port="$(node --input-type=module - <<'NODE'
2987
+ import net from "node:net";
2988
+ const server = net.createServer();
2989
+ server.listen(0, "127.0.0.1", () => {
2990
+ const address = server.address();
2991
+ process.stdout.write(String(typeof address === "object" && address ? address.port : 0));
2992
+ server.close();
2993
+ });
2994
+ NODE
2995
+ )"
2996
+ pass="oqa-$RANDOM$RANDOM"
2997
+ auth="opencode:$pass"
2998
+ url="http://127.0.0.1:$port"
2999
+ printf '%s\n' "$SANDBOX_ROOT/project" >"$EVIDENCE_DIR/opencode-serve-cwd.txt"
3000
+ (
3001
+ cd "$SANDBOX_ROOT/project" || exit 1
3002
+ export OPENCODE_SERVER_PASSWORD="$pass"
3003
+ exec "$opencode_bin" serve --port "$port" --hostname 127.0.0.1
3004
+ ) >"$EVIDENCE_DIR/opencode-serve.stdout.log" 2>"$EVIDENCE_DIR/opencode-serve.stderr.log" &
3005
+ OPENCODE_PID=$!
3006
+ wait_http "$url/global/health" "$auth" || { fail "OpenCode server did not become ready"; return 1; }
3007
+
3008
+ encoded_dir="$(urlencode "$SANDBOX_ROOT/project")"
3009
+ wait_for_sse_connected "$url" "$auth" "$encoded_dir" || return 1
3010
+
3011
+ session_response="$(curl -sS -u "$auth" -X POST "$url/session?directory=$encoded_dir" \
3012
+ -H 'content-type: application/json' -d '{"title":"OMO LSP path contract QA"}' 2>/dev/null || true)"
3013
+ printf '%s\n' "$session_response" >"$EVIDENCE_DIR/session-create.json"
3014
+ session_id="$(printf '%s' "$session_response" | jq -r '.id // .sessionID // empty' 2>/dev/null || true)"
3015
+ [ -n "$session_id" ] || { fail "OpenCode session creation failed"; return 1; }
3016
+
3017
+ prompt_code="$(curl -sS -o "$EVIDENCE_DIR/prompt-response.txt" -w '%{http_code}' -u "$auth" \
3018
+ -X POST "$url/session/$session_id/prompt_async?directory=$encoded_dir" \
3019
+ -H 'content-type: application/json' \
3020
+ -d "{\"model\":{\"providerID\":\"openai\",\"modelID\":\"gpt-fake\"},\"parts\":[{\"type\":\"text\",\"text\":\"$(scenario_prompt "$SCENARIO")\"}]}" \
3021
+ 2>"$EVIDENCE_DIR/prompt.stderr.log" || true)"
3022
+ [ "$prompt_code" = "204" ] || { fail "prompt_async returned HTTP $prompt_code"; return 1; }
3023
+
3024
+ wait_for_session_result "$url" "$auth" "$session_id" "$encoded_dir" "$EVIDENCE_DIR/messages.json" || {
3025
+ if [ -s "$EVIDENCE_DIR/session-terminal-failure.json" ]; then
3026
+ fail "OpenCode LSP tool call reached terminal error (see session-terminal-failure.json)"
3027
+ else
3028
+ fail "OpenCode LSP tool call did not complete within the bound"
3029
+ fi
3030
+ return 1
3031
+ }
3032
+ extract_tool_evidence || { fail "SSE/message/provider tool evidence failed"; return 1; }
3033
+ record_daemon_state || return 1
3034
+ daemon_pid_file="$(find_daemon_pid_file)"
3035
+ daemon_pid="$(tr -d '[:space:]' <"$daemon_pid_file")"
3036
+
3037
+ stop_verified_pid "$SSE_PID" "curl" "SSE watcher" || return 1
3038
+ SSE_PID=""
3039
+ stop_verified_pid "$OPENCODE_PID" "serve" "OpenCode server" || return 1
3040
+ OPENCODE_PID=""
3041
+ stop_known_daemon || return 1
3042
+ stop_verified_pid "$FAKE_PID" "$SANDBOX_ROOT/fake-provider.mjs" "fake provider" || return 1
3043
+ FAKE_PID=""
3044
+ stop_owned_sandbox_processes || return 1
3045
+ kill -0 "$daemon_pid" 2>/dev/null && { fail "daemon pid remained alive after cleanup"; return 1; }
3046
+ safe_rm_tree "$SANDBOX_ROOT" || return 1
3047
+ SANDBOX_ROOT=""
3048
+ sandbox_removed=true
3049
+
3050
+ real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
3051
+ real_db_after="$(real_db_count)"
3052
+ worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3053
+ [ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
3054
+ [ "$real_db_before" = "$real_db_after" ] || { fail "real OpenCode DB session count changed"; return 1; }
3055
+ [ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
3056
+ printf 'after=%s unchanged=yes\nreal_db_session_count_after=%s unchanged=yes\n' \
3057
+ "$real_omo_after" "$real_db_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
3058
+ printf 'daemon_pid=%s alive_after=no\nopencode_pid=stopped\nfake_provider_pid=stopped\nsse_watcher_pid=stopped\nisolated_state_removed=%s\n' \
3059
+ "$daemon_pid" "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
3060
+
3061
+ write_final_result "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
3062
+ "$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
3063
+ NORMAL_CLEANUP_COMPLETE=1
3064
+ jq -c . "$EVIDENCE_DIR/result.json"
3065
+ }
3066
+
3067
+ run_all_scenarios() {
3068
+ require_bins bash node jq git || return 1
3069
+ prepare_evidence || return 1
3070
+ local scenarios scenario failures=0 before_omo before_db before_status after_omo after_db after_status
3071
+ REAL_DB_PATH="$(opencode db path 2>/dev/null | head -1 || true)"
3072
+ scenarios="path-contract rename diagnostics-freshness post-edit cancellation client-package source-dist-reuse auth-ownership"
3073
+ before_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
3074
+ before_db="$(real_db_count)"
3075
+ before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3076
+ printf '%s\n' "$scenarios" >"$EVIDENCE_DIR/all-scenarios.txt"
3077
+ for scenario in $scenarios; do
3078
+ mkdir -p "$EVIDENCE_DIR/$scenario"
3079
+ if ! bash "${BASH_SOURCE[0]}" --scenario "$scenario" --evidence-dir "$EVIDENCE_DIR/$scenario" >"$EVIDENCE_DIR/$scenario.command.log" 2>&1; then
3080
+ failures=$((failures + 1))
3081
+ continue
3082
+ fi
3083
+ [ -s "$EVIDENCE_DIR/$scenario/result.json" ] || { failures=$((failures + 1)); continue; }
3084
+ jq -e '.result == "PASS" and .scenario != "SKIP"' "$EVIDENCE_DIR/$scenario/result.json" >/dev/null || failures=$((failures + 1))
3085
+ done
3086
+ after_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
3087
+ after_db="$(real_db_count)"
3088
+ after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3089
+ [ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
3090
+ [ "$before_db" = "$after_db" ] || failures=$((failures + 1))
3091
+ [ "$before_status" = "$after_status" ] || failures=$((failures + 1))
3092
+ node --input-type=module - "$EVIDENCE_DIR" "$before_omo" "$after_omo" "$before_db" "$after_db" "$before_status" "$after_status" <<'NODE'
3093
+ import { readFileSync, writeFileSync } from "node:fs";
3094
+ import { join } from "node:path";
3095
+ const [evidenceDir, omoBefore, omoAfter, dbBefore, dbAfter, statusBefore, statusAfter] = process.argv.slice(2);
3096
+ const scenarios = readFileSync(join(evidenceDir, "all-scenarios.txt"), "utf8").trim().split(/\s+/);
3097
+ const entries = scenarios.map((scenario) => {
3098
+ try {
3099
+ return [scenario, JSON.parse(readFileSync(join(evidenceDir, scenario, "result.json"), "utf8"))];
3100
+ } catch (error) {
3101
+ return [scenario, {
3102
+ result: "FAIL",
3103
+ scenario,
3104
+ missingResult: true,
3105
+ error: error instanceof Error ? error.message : String(error),
3106
+ artifacts: { commandLog: `${scenario}.command.log` },
3107
+ }];
3108
+ }
3109
+ });
3110
+ const results = Object.fromEntries(entries);
3111
+ const values = entries.map(([, value]) => value);
3112
+ const hasUnchangedOpenCodeDb = (value) =>
3113
+ value.realOpenCodeDbSessionCountUnchanged === true || value.realDbSessionCountUnchanged === true;
3114
+ const scenariosWithSse = entries.filter(([scenario]) => scenario !== "auth-ownership").map(([, value]) => value);
3115
+ const payload = {
3116
+ result: values.every((value) => value.result === "PASS") && omoBefore === omoAfter && dbBefore === dbAfter && statusBefore === statusAfter ? "PASS" : "FAIL",
3117
+ scenario: "all",
3118
+ harness: "opencode",
3119
+ scenarioOrder: scenarios,
3120
+ scenarioResults: Object.fromEntries(entries.map(([name, value]) => [name, { result: value.result, cleanup: value.cleanup ?? {}, artifacts: value.artifacts ?? {} }])),
3121
+ realOmoRootUnchanged: omoBefore === omoAfter && values.every((value) => value.realOmoRootUnchanged === true),
3122
+ realOpenCodeDbSessionCountUnchanged: dbBefore === dbAfter && values.every(hasUnchangedOpenCodeDb),
3123
+ dirtyWorktreePreserved: statusBefore === statusAfter && values.every((value) => value.dirtyWorktreePreserved === true),
3124
+ noSkip: values.every((value) => value.result !== "SKIP" && value.scenario !== "SKIP"),
3125
+ sseEvidence: scenariosWithSse.every((value) => value.sseEvidence?.connected === true && value.sseEvidence?.toolObserved === true),
3126
+ pathContract: results["path-contract"]?.overrideAssertions ?? null,
3127
+ pairRecovery: results["path-contract"]?.failureFixtures ?? null,
3128
+ reuse: results["source-dist-reuse"]?.sourceDistReuse ?? null,
3129
+ auth: results["auth-ownership"]?.authOwnership ?? results["auth-ownership"] ?? null,
3130
+ cancellation: results.cancellation?.cancellationContract ?? null,
3131
+ cleanup: {
3132
+ isolatedStateRemoved: values.every((value) => value.cleanup?.isolatedStateRemoved === true),
3133
+ daemonStopped: values.every((value) => value.cleanup?.daemonStopped === true),
3134
+ },
3135
+ };
3136
+ writeFileSync(join(evidenceDir, "result.json"), `${JSON.stringify(payload, null, 2)}\n`);
3137
+ console.log(JSON.stringify(payload));
3138
+ NODE
3139
+ NORMAL_CLEANUP_COMPLETE=1
3140
+ [ "$failures" -eq 0 ] && jq -e '.result == "PASS" and .noSkip == true' "$EVIDENCE_DIR/result.json" >/dev/null
3141
+ }
3142
+
3143
+ main() {
3144
+ parse_args "$@" || return $?
3145
+ if [ "$SELF_TEST" -eq 1 ]; then
3146
+ run_self_test
3147
+ elif [ "$SCENARIO" = "all" ]; then
3148
+ run_all_scenarios
3149
+ else
3150
+ run_normal
3151
+ fi
3152
+ }
3153
+
3154
+ main "$@"