oh-my-openagent 4.18.0 → 4.18.1
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.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/dist/cli/index.js +457 -154
- package/dist/cli-node/index.js +457 -154
- package/dist/index.js +415 -388
- package/package.json +16 -16
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3114 -747
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5851 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
- package/packages/lsp-daemon/dist/daemon-client.js +113 -30
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
- package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +2862 -754
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
- package/packages/lsp-daemon/dist/proxy.js +54 -3
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -0,0 +1,3654 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lsp-e2e.sh - isolated live Codex QA for the shared OMO LSP daemon.
|
|
3
|
+
#
|
|
4
|
+
# Normal mode installs this worktree's OMO plugin into a disposable CODEX_HOME,
|
|
5
|
+
# drives a real `codex app-server` against the local codex-qa mock model, calls
|
|
6
|
+
# the installed lsp MCP through `mcpServer/tool/call`, and records a PASS result
|
|
7
|
+
# only after path-contract / rename, hook, isolation, and cleanup assertions
|
|
8
|
+
# succeed.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# lsp-e2e.sh --scenario <name> --evidence-dir <absolute-dir>
|
|
12
|
+
# lsp-e2e.sh --self-test
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
17
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd -P)"
|
|
18
|
+
|
|
19
|
+
SCENARIO=""
|
|
20
|
+
EVIDENCE_DIR=""
|
|
21
|
+
SELF_TEST=0
|
|
22
|
+
SANDBOX_ROOT=""
|
|
23
|
+
OMO_TEST_ROOT=""
|
|
24
|
+
EXPECTED_DAEMON_CLI=""
|
|
25
|
+
MOCK_PID=""
|
|
26
|
+
APP_SERVER_PID_FILE=""
|
|
27
|
+
RESULT_STAGE=""
|
|
28
|
+
CLEANUP_RUNNING=0
|
|
29
|
+
NORMAL_CLEANUP_COMPLETE=0
|
|
30
|
+
REAL_HOME="${HOME:-}"
|
|
31
|
+
REAL_OMO_ROOT="${HOME:-}/.omo/lsp-daemon"
|
|
32
|
+
REAL_CODEX_CONFIG="${HOME:-}/.codex/config.toml"
|
|
33
|
+
REAL_OMO_BEFORE_HASH=""
|
|
34
|
+
DAEMON_DIST_DIR="$REPO_ROOT/packages/lsp-daemon/dist"
|
|
35
|
+
DAEMON_DIST_BACKUP=""
|
|
36
|
+
DAEMON_DIST_WAS_PRESENT=0
|
|
37
|
+
DAEMON_DIST_PREPARED=0
|
|
38
|
+
BUILD_LOCK_DIR=""
|
|
39
|
+
|
|
40
|
+
log() { printf '[codex-lsp-e2e] %s\n' "$*" >&2; }
|
|
41
|
+
fail() { log "FAIL: $*"; return 1; }
|
|
42
|
+
|
|
43
|
+
usage() {
|
|
44
|
+
sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
parse_args() {
|
|
48
|
+
while [ "$#" -gt 0 ]; do
|
|
49
|
+
case "$1" in
|
|
50
|
+
--scenario)
|
|
51
|
+
[ "$#" -ge 2 ] || { log "--scenario requires a value"; return 2; }
|
|
52
|
+
[ -z "$SCENARIO" ] || { log "--scenario may be provided only once"; return 2; }
|
|
53
|
+
SCENARIO="$2"
|
|
54
|
+
shift 2
|
|
55
|
+
;;
|
|
56
|
+
--evidence-dir)
|
|
57
|
+
[ "$#" -ge 2 ] || { log "--evidence-dir requires a directory"; return 2; }
|
|
58
|
+
[ -z "$EVIDENCE_DIR" ] || { log "--evidence-dir may be provided only once"; return 2; }
|
|
59
|
+
EVIDENCE_DIR="$2"
|
|
60
|
+
shift 2
|
|
61
|
+
;;
|
|
62
|
+
--self-test)
|
|
63
|
+
[ "$SELF_TEST" -eq 0 ] || { log "--self-test may be provided only once"; return 2; }
|
|
64
|
+
SELF_TEST=1
|
|
65
|
+
shift
|
|
66
|
+
;;
|
|
67
|
+
-h|--help)
|
|
68
|
+
usage
|
|
69
|
+
exit 0
|
|
70
|
+
;;
|
|
71
|
+
*)
|
|
72
|
+
log "unknown option: $1"
|
|
73
|
+
return 2
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
if [ "$SELF_TEST" -eq 1 ]; then
|
|
79
|
+
if [ -n "$SCENARIO" ] || [ -n "$EVIDENCE_DIR" ]; then
|
|
80
|
+
log "--self-test cannot be combined with normal-mode options"
|
|
81
|
+
return 2
|
|
82
|
+
fi
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
[ -n "$SCENARIO" ] || { log "--scenario is required"; return 2; }
|
|
87
|
+
[ -n "$EVIDENCE_DIR" ] || { log "--evidence-dir is required"; return 2; }
|
|
88
|
+
case "$SCENARIO" in
|
|
89
|
+
[A-Za-z0-9]* ) ;;
|
|
90
|
+
* ) log "invalid scenario: $SCENARIO"; return 2 ;;
|
|
91
|
+
esac
|
|
92
|
+
if ! printf '%s' "$SCENARIO" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'; then
|
|
93
|
+
log "invalid scenario: $SCENARIO"
|
|
94
|
+
return 2
|
|
95
|
+
fi
|
|
96
|
+
case "$EVIDENCE_DIR" in
|
|
97
|
+
/*) ;;
|
|
98
|
+
*) log "--evidence-dir must be absolute"; return 2 ;;
|
|
99
|
+
esac
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
require_bins() {
|
|
103
|
+
local missing=0 bin
|
|
104
|
+
for bin in "$@"; do
|
|
105
|
+
if ! command -v "$bin" >/dev/null 2>&1; then
|
|
106
|
+
log "missing dependency: $bin"
|
|
107
|
+
missing=1
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
[ "$missing" -eq 0 ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
hash_path() {
|
|
114
|
+
node --input-type=module - "$1" <<'NODE'
|
|
115
|
+
import { createHash } from "node:crypto";
|
|
116
|
+
import { lstatSync, readFileSync, readlinkSync, readdirSync } from "node:fs";
|
|
117
|
+
import { basename, join } from "node:path";
|
|
118
|
+
|
|
119
|
+
const target = process.argv[2];
|
|
120
|
+
const hash = createHash("sha256");
|
|
121
|
+
|
|
122
|
+
function visit(path, relative) {
|
|
123
|
+
const stat = lstatSync(path);
|
|
124
|
+
const kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : stat.isSymbolicLink() ? "link" : "special";
|
|
125
|
+
hash.update(`${kind}\0${relative}\0${stat.mode & 0o7777}\0`);
|
|
126
|
+
if (kind === "file") hash.update(readFileSync(path));
|
|
127
|
+
if (kind === "link") hash.update(readlinkSync(path));
|
|
128
|
+
if (kind === "dir") {
|
|
129
|
+
for (const name of readdirSync(path).sort()) visit(join(path, name), relative ? `${relative}/${name}` : name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
visit(target, basename(target));
|
|
135
|
+
process.stdout.write(hash.digest("hex"));
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error && error.code === "ENOENT") process.stdout.write("ABSENT");
|
|
138
|
+
else throw error;
|
|
139
|
+
}
|
|
140
|
+
NODE
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
run_bounded() {
|
|
144
|
+
local seconds="$1" output="$2"
|
|
145
|
+
shift 2
|
|
146
|
+
node --input-type=module - "$seconds" "$output" "$@" <<'NODE'
|
|
147
|
+
import { closeSync, openSync } from "node:fs";
|
|
148
|
+
import { spawn } from "node:child_process";
|
|
149
|
+
|
|
150
|
+
const [secondsRaw, output, command, ...args] = process.argv.slice(2);
|
|
151
|
+
const seconds = Number(secondsRaw);
|
|
152
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || !command) process.exit(125);
|
|
153
|
+
const fd = openSync(output, "w");
|
|
154
|
+
const child = spawn(command, args, {
|
|
155
|
+
stdio: ["ignore", fd, fd],
|
|
156
|
+
detached: process.platform !== "win32",
|
|
157
|
+
env: process.env,
|
|
158
|
+
});
|
|
159
|
+
let timedOut = false;
|
|
160
|
+
let forceTimer;
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
timedOut = true;
|
|
163
|
+
try {
|
|
164
|
+
if (process.platform !== "win32") process.kill(-child.pid, "SIGTERM");
|
|
165
|
+
else child.kill("SIGTERM");
|
|
166
|
+
} catch {}
|
|
167
|
+
forceTimer = setTimeout(() => {
|
|
168
|
+
try {
|
|
169
|
+
if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL");
|
|
170
|
+
else child.kill("SIGKILL");
|
|
171
|
+
} catch {}
|
|
172
|
+
}, 3000);
|
|
173
|
+
}, seconds * 1000);
|
|
174
|
+
child.on("error", () => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
177
|
+
closeSync(fd);
|
|
178
|
+
process.exit(126);
|
|
179
|
+
});
|
|
180
|
+
child.on("exit", (code, signal) => {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
183
|
+
closeSync(fd);
|
|
184
|
+
if (timedOut) process.exit(124);
|
|
185
|
+
if (typeof code === "number") process.exit(code);
|
|
186
|
+
process.exit(signal ? 128 : 1);
|
|
187
|
+
});
|
|
188
|
+
NODE
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
with_shared_build_lock() {
|
|
192
|
+
local command_name="$1" attempts=0 rc
|
|
193
|
+
shift
|
|
194
|
+
BUILD_LOCK_DIR="$REPO_ROOT/.omo/locks/lsp-daemon-build.lock"
|
|
195
|
+
mkdir -p "$(dirname "$BUILD_LOCK_DIR")"
|
|
196
|
+
while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
|
|
197
|
+
[ "$attempts" -lt 600 ] || { fail "timed out waiting for shared LSP daemon build lock"; return 1; }
|
|
198
|
+
sleep 0.2
|
|
199
|
+
attempts=$((attempts + 1))
|
|
200
|
+
done
|
|
201
|
+
printf 'pid=%s\ncommand=%s\n' "$$" "$command_name" >"$BUILD_LOCK_DIR/owner.txt"
|
|
202
|
+
"$@"
|
|
203
|
+
rc=$?
|
|
204
|
+
rm -rf "$BUILD_LOCK_DIR"
|
|
205
|
+
BUILD_LOCK_DIR=""
|
|
206
|
+
return "$rc"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
safe_rm_tree() {
|
|
210
|
+
local path="$1"
|
|
211
|
+
local attempt=0
|
|
212
|
+
[ -n "$path" ] || return 0
|
|
213
|
+
case "$path" in
|
|
214
|
+
/var/folders/*/T/cqa-lsp-e2e.*|/tmp/cqa-lsp-e2e.*|/private/tmp/cqa-lsp-e2e.*)
|
|
215
|
+
while [ -e "$path" ] && [ "$attempt" -lt 100 ]; do
|
|
216
|
+
rm -rf "$path" 2>/dev/null || true
|
|
217
|
+
[ ! -e "$path" ] && return 0
|
|
218
|
+
sleep 0.1
|
|
219
|
+
attempt=$((attempt + 1))
|
|
220
|
+
done
|
|
221
|
+
[ ! -e "$path" ] || fail "isolated sandbox remained after bounded cleanup: $path"
|
|
222
|
+
;;
|
|
223
|
+
*)
|
|
224
|
+
fail "refusing to remove unexpected sandbox path: $path"
|
|
225
|
+
;;
|
|
226
|
+
esac
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
owned_sandbox_pids() {
|
|
230
|
+
[ -n "$SANDBOX_ROOT" ] || return 0
|
|
231
|
+
/bin/ps ax -o pid=,command= 2>/dev/null | while read -r pid command; do
|
|
232
|
+
case "$command" in
|
|
233
|
+
*"$SANDBOX_ROOT"*)
|
|
234
|
+
[ "$pid" = "$$" ] || printf '%s\n' "$pid"
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
done
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
stop_owned_sandbox_processes() {
|
|
241
|
+
local pids pid command
|
|
242
|
+
pids="$(owned_sandbox_pids)"
|
|
243
|
+
[ -n "$pids" ] || return 0
|
|
244
|
+
for pid in $pids; do
|
|
245
|
+
kill -0 "$pid" 2>/dev/null || continue
|
|
246
|
+
command="$(process_command "$pid")"
|
|
247
|
+
case "$command" in
|
|
248
|
+
*"$SANDBOX_ROOT"*) ;;
|
|
249
|
+
*) fail "sandbox process $pid changed identity before cleanup"; return 1 ;;
|
|
250
|
+
esac
|
|
251
|
+
kill "$pid" 2>/dev/null || true
|
|
252
|
+
if ! wait_for_exit "$pid"; then
|
|
253
|
+
command="$(process_command "$pid")"
|
|
254
|
+
case "$command" in
|
|
255
|
+
*"$SANDBOX_ROOT"*) kill -9 "$pid" 2>/dev/null || true ;;
|
|
256
|
+
*) fail "sandbox process $pid changed identity during cleanup"; return 1 ;;
|
|
257
|
+
esac
|
|
258
|
+
wait_for_exit "$pid" || { fail "sandbox process $pid survived cleanup"; return 1; }
|
|
259
|
+
fi
|
|
260
|
+
if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
|
|
261
|
+
printf 'sandbox_process_pid=%s alive_after=no\n' "$pid" >>"$EVIDENCE_DIR/owned-process-cleanup.txt"
|
|
262
|
+
fi
|
|
263
|
+
done
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
process_command() {
|
|
267
|
+
/bin/ps -p "$1" -o command= 2>/dev/null || true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
wait_for_exit() {
|
|
271
|
+
local pid="$1" attempts=0
|
|
272
|
+
while [ "$attempts" -lt 50 ]; do
|
|
273
|
+
kill -0 "$pid" 2>/dev/null || return 0
|
|
274
|
+
sleep 0.1
|
|
275
|
+
attempts=$((attempts + 1))
|
|
276
|
+
done
|
|
277
|
+
return 1
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
stop_known_pid_file() {
|
|
281
|
+
local pid_file="$1" expected_fragment="$2" label="$3"
|
|
282
|
+
[ -n "$pid_file" ] && [ -f "$pid_file" ] || return 0
|
|
283
|
+
local pid command
|
|
284
|
+
pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
|
|
285
|
+
case "$pid" in
|
|
286
|
+
''|*[!0-9]*) return 0 ;;
|
|
287
|
+
esac
|
|
288
|
+
kill -0 "$pid" 2>/dev/null || return 0
|
|
289
|
+
command="$(process_command "$pid")"
|
|
290
|
+
case "$command" in
|
|
291
|
+
*"$expected_fragment"*) ;;
|
|
292
|
+
*) fail "refusing to stop unverified $label pid $pid"; return 1 ;;
|
|
293
|
+
esac
|
|
294
|
+
kill "$pid" 2>/dev/null || true
|
|
295
|
+
if ! wait_for_exit "$pid"; then
|
|
296
|
+
command="$(process_command "$pid")"
|
|
297
|
+
case "$command" in
|
|
298
|
+
*"$expected_fragment"*) kill -9 "$pid" 2>/dev/null || true ;;
|
|
299
|
+
*) fail "$label pid $pid changed identity during cleanup"; return 1 ;;
|
|
300
|
+
esac
|
|
301
|
+
wait_for_exit "$pid" || { fail "$label pid $pid survived cleanup"; return 1; }
|
|
302
|
+
fi
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
find_daemon_pid_file() {
|
|
306
|
+
[ -n "$OMO_TEST_ROOT" ] && [ -d "$OMO_TEST_ROOT" ] || return 0
|
|
307
|
+
find "$OMO_TEST_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
stop_known_daemon() {
|
|
311
|
+
local pid_file pid command
|
|
312
|
+
pid_file="$(find_daemon_pid_file)"
|
|
313
|
+
[ -n "$pid_file" ] || return 0
|
|
314
|
+
pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
|
|
315
|
+
case "$pid" in
|
|
316
|
+
''|*[!0-9]*) fail "daemon pid file is malformed: $pid_file"; return 1 ;;
|
|
317
|
+
esac
|
|
318
|
+
kill -0 "$pid" 2>/dev/null || return 0
|
|
319
|
+
command="$(process_command "$pid")"
|
|
320
|
+
case "$command" in
|
|
321
|
+
*"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
|
|
322
|
+
*) fail "refusing to stop unverified daemon pid $pid"; return 1 ;;
|
|
323
|
+
esac
|
|
324
|
+
kill "$pid" 2>/dev/null || true
|
|
325
|
+
if ! wait_for_exit "$pid"; then
|
|
326
|
+
command="$(process_command "$pid")"
|
|
327
|
+
case "$command" in
|
|
328
|
+
*"$EXPECTED_DAEMON_CLI"*" daemon"*) kill -9 "$pid" 2>/dev/null || true ;;
|
|
329
|
+
*) fail "daemon pid $pid changed identity during cleanup"; return 1 ;;
|
|
330
|
+
esac
|
|
331
|
+
wait_for_exit "$pid" || { fail "daemon pid $pid survived cleanup"; return 1; }
|
|
332
|
+
fi
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
stop_owned_real_daemon_leak() {
|
|
336
|
+
[ "$REAL_OMO_BEFORE_HASH" = "ABSENT" ] || return 0
|
|
337
|
+
[ "$OMO_TEST_ROOT" != "$REAL_OMO_ROOT" ] || return 0
|
|
338
|
+
[ -d "$REAL_OMO_ROOT" ] || return 0
|
|
339
|
+
local pid_file pid command state_dir
|
|
340
|
+
pid_file="$(find "$REAL_OMO_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1)"
|
|
341
|
+
if [ -n "$pid_file" ]; then
|
|
342
|
+
pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
|
|
343
|
+
case "$pid" in
|
|
344
|
+
''|*[!0-9]*) fail "real-root leak pid file is malformed"; return 1 ;;
|
|
345
|
+
esac
|
|
346
|
+
command="$(process_command "$pid")"
|
|
347
|
+
case "$command" in
|
|
348
|
+
*"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
|
|
349
|
+
*) fail "real OMO root changed by an unverified process; preserving it"; return 1 ;;
|
|
350
|
+
esac
|
|
351
|
+
kill "$pid" 2>/dev/null || true
|
|
352
|
+
wait_for_exit "$pid" || { fail "own leaked daemon did not stop"; return 1; }
|
|
353
|
+
fi
|
|
354
|
+
if find "$REAL_OMO_ROOT" -type f \( -name daemon.pid -o -name daemon.endpoint \) -print 2>/dev/null | grep -q .; then
|
|
355
|
+
fail "real OMO root still contains live markers after own-daemon cleanup"
|
|
356
|
+
return 1
|
|
357
|
+
fi
|
|
358
|
+
find "$REAL_OMO_ROOT" -type f -name daemon.log -delete 2>/dev/null || true
|
|
359
|
+
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)
|
|
360
|
+
[ ! -e "$REAL_OMO_ROOT" ] || { fail "real OMO root could not be restored to ABSENT"; return 1; }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
stop_mock() {
|
|
364
|
+
[ -n "$MOCK_PID" ] || return 0
|
|
365
|
+
if kill -0 "$MOCK_PID" 2>/dev/null; then
|
|
366
|
+
kill "$MOCK_PID" 2>/dev/null || true
|
|
367
|
+
wait_for_exit "$MOCK_PID" || kill -9 "$MOCK_PID" 2>/dev/null || true
|
|
368
|
+
fi
|
|
369
|
+
MOCK_PID=""
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
cleanup_all() {
|
|
373
|
+
local cleanup_rc=0
|
|
374
|
+
[ "$CLEANUP_RUNNING" -eq 0 ] || return 0
|
|
375
|
+
CLEANUP_RUNNING=1
|
|
376
|
+
stop_mock || cleanup_rc=1
|
|
377
|
+
if [ -n "$APP_SERVER_PID_FILE" ]; then
|
|
378
|
+
stop_known_pid_file "$APP_SERVER_PID_FILE" "app-server" "Codex app-server" || cleanup_rc=1
|
|
379
|
+
fi
|
|
380
|
+
stop_known_daemon || cleanup_rc=1
|
|
381
|
+
stop_owned_sandbox_processes || cleanup_rc=1
|
|
382
|
+
stop_owned_real_daemon_leak || cleanup_rc=1
|
|
383
|
+
restore_daemon_dist || cleanup_rc=1
|
|
384
|
+
if [ -n "$BUILD_LOCK_DIR" ]; then
|
|
385
|
+
rm -rf "$BUILD_LOCK_DIR" 2>/dev/null || cleanup_rc=1
|
|
386
|
+
BUILD_LOCK_DIR=""
|
|
387
|
+
fi
|
|
388
|
+
[ -n "$RESULT_STAGE" ] && rm -f "$RESULT_STAGE" 2>/dev/null || true
|
|
389
|
+
if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
|
|
390
|
+
find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
|
|
391
|
+
fi
|
|
392
|
+
if [ -n "$SANDBOX_ROOT" ]; then
|
|
393
|
+
safe_rm_tree "$SANDBOX_ROOT" || cleanup_rc=1
|
|
394
|
+
SANDBOX_ROOT=""
|
|
395
|
+
fi
|
|
396
|
+
CLEANUP_RUNNING=0
|
|
397
|
+
return "$cleanup_rc"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
on_exit() {
|
|
401
|
+
local rc=$?
|
|
402
|
+
trap - EXIT INT TERM HUP
|
|
403
|
+
if [ "$NORMAL_CLEANUP_COMPLETE" -eq 0 ]; then
|
|
404
|
+
cleanup_all || rc=1
|
|
405
|
+
fi
|
|
406
|
+
if [ "$rc" -ne 0 ] && [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
|
|
407
|
+
rm -f "$EVIDENCE_DIR/result.json" 2>/dev/null || true
|
|
408
|
+
fi
|
|
409
|
+
exit "$rc"
|
|
410
|
+
}
|
|
411
|
+
trap on_exit EXIT
|
|
412
|
+
trap 'exit 130' INT
|
|
413
|
+
trap 'exit 143' TERM
|
|
414
|
+
trap 'exit 129' HUP
|
|
415
|
+
|
|
416
|
+
prepare_evidence() {
|
|
417
|
+
[ ! -L "$EVIDENCE_DIR" ] || { fail "evidence directory must not be a symlink"; return 1; }
|
|
418
|
+
mkdir -p "$EVIDENCE_DIR" || return 1
|
|
419
|
+
EVIDENCE_DIR="$(cd "$EVIDENCE_DIR" && pwd -P)"
|
|
420
|
+
rm -f "$EVIDENCE_DIR/result.json"
|
|
421
|
+
find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
|
|
422
|
+
printf 'bash %s --scenario %s --evidence-dir %s\n' \
|
|
423
|
+
"${BASH_SOURCE[0]}" "$SCENARIO" "$EVIDENCE_DIR" >"$EVIDENCE_DIR/invocation.txt"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
daemon_dist_ready() {
|
|
427
|
+
[ -f "$DAEMON_DIST_DIR/package.json" ] \
|
|
428
|
+
&& [ -f "$DAEMON_DIST_DIR/index.js" ] \
|
|
429
|
+
&& [ -f "$DAEMON_DIST_DIR/cli.js" ]
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
snapshot_daemon_dist() {
|
|
433
|
+
local backup_root="$1"
|
|
434
|
+
mkdir -p "$backup_root" || return 1
|
|
435
|
+
DAEMON_DIST_BACKUP="$backup_root/lsp-daemon-dist.backup"
|
|
436
|
+
rm -rf "$DAEMON_DIST_BACKUP"
|
|
437
|
+
if [ -e "$DAEMON_DIST_DIR" ]; then
|
|
438
|
+
cp -a "$DAEMON_DIST_DIR" "$DAEMON_DIST_BACKUP" || return 1
|
|
439
|
+
DAEMON_DIST_WAS_PRESENT=1
|
|
440
|
+
else
|
|
441
|
+
DAEMON_DIST_WAS_PRESENT=0
|
|
442
|
+
fi
|
|
443
|
+
DAEMON_DIST_PREPARED=1
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
restore_daemon_dist() {
|
|
447
|
+
[ "$DAEMON_DIST_PREPARED" -eq 1 ] || return 0
|
|
448
|
+
rm -rf "$DAEMON_DIST_DIR" || return 1
|
|
449
|
+
if [ "$DAEMON_DIST_WAS_PRESENT" -eq 1 ]; then
|
|
450
|
+
mkdir -p "$(dirname "$DAEMON_DIST_DIR")" || return 1
|
|
451
|
+
cp -a "$DAEMON_DIST_BACKUP" "$DAEMON_DIST_DIR" || return 1
|
|
452
|
+
fi
|
|
453
|
+
if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
|
|
454
|
+
{
|
|
455
|
+
printf 'daemon_dist_was_present=%s\n' "$( [ "$DAEMON_DIST_WAS_PRESENT" -eq 1 ] && echo true || echo false )"
|
|
456
|
+
printf 'daemon_dist_restored=true\n'
|
|
457
|
+
printf 'daemon_dist_path=%s\n' "$DAEMON_DIST_DIR"
|
|
458
|
+
} >>"$EVIDENCE_DIR/daemon-dist-cleanup.txt"
|
|
459
|
+
fi
|
|
460
|
+
DAEMON_DIST_BACKUP=""
|
|
461
|
+
DAEMON_DIST_WAS_PRESENT=0
|
|
462
|
+
DAEMON_DIST_PREPARED=0
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
prepare_daemon_dist() {
|
|
466
|
+
if daemon_dist_ready; then
|
|
467
|
+
printf 'daemon_dist_ready_before=true\nbuild_skipped=true\n' >"$EVIDENCE_DIR/lsp-daemon-prebuild-receipt.txt"
|
|
468
|
+
return 0
|
|
469
|
+
fi
|
|
470
|
+
if [ "$DAEMON_DIST_PREPARED" -eq 0 ]; then
|
|
471
|
+
snapshot_daemon_dist "$SANDBOX_ROOT/daemon-dist-backup" || return 1
|
|
472
|
+
fi
|
|
473
|
+
{
|
|
474
|
+
printf 'daemon_dist_ready_before=false\n'
|
|
475
|
+
printf 'daemon_dist_was_present=%s\n' "$( [ "$DAEMON_DIST_WAS_PRESENT" -eq 1 ] && echo true || echo false )"
|
|
476
|
+
printf 'command=bun run build:lsp-daemon\n'
|
|
477
|
+
} >"$EVIDENCE_DIR/lsp-daemon-prebuild-receipt.txt"
|
|
478
|
+
run_bounded 300 "$EVIDENCE_DIR/lsp-daemon-prebuild.log" bun run build:lsp-daemon || return 1
|
|
479
|
+
daemon_dist_ready || { fail "lsp-daemon prebuild did not create required dist files"; return 1; }
|
|
480
|
+
printf 'daemon_dist_ready_after=true\n' >>"$EVIDENCE_DIR/lsp-daemon-prebuild-receipt.txt"
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
write_path_contract_probe() {
|
|
484
|
+
local probe_dir="$1" output="$2" script="$SANDBOX_ROOT/path-contract-probe.mjs"
|
|
485
|
+
mkdir -p "$probe_dir"
|
|
486
|
+
cat >"$script" <<'NODE'
|
|
487
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
488
|
+
import { dirname, join, resolve } from "node:path";
|
|
489
|
+
import { pathToFileURL } from "node:url";
|
|
490
|
+
|
|
491
|
+
const repoRoot = process.env.REPO_ROOT;
|
|
492
|
+
const base = process.env.PROBE_BASE;
|
|
493
|
+
const output = process.env.PROBE_OUTPUT;
|
|
494
|
+
if (!repoRoot || !base || !output) throw new Error("missing probe environment");
|
|
495
|
+
const modulePath = join(repoRoot, "packages/lsp-daemon/dist/index.js");
|
|
496
|
+
const daemon = await import(pathToFileURL(modulePath).href + `?qa=${Date.now()}`);
|
|
497
|
+
const cliPath = join(repoRoot, "packages/lsp-daemon/dist/cli.js");
|
|
498
|
+
const packagedVersion = JSON.parse(readFileSync(join(repoRoot, "packages/lsp-daemon/dist/package.json"), "utf8")).version;
|
|
499
|
+
const envNameValues = [daemon.OMO_LSP_DAEMON_CLI, daemon.OMO_LSP_DAEMON_DIR, daemon.OMO_LSP_DAEMON_VERSION].sort();
|
|
500
|
+
|
|
501
|
+
function capture(run) {
|
|
502
|
+
try {
|
|
503
|
+
run();
|
|
504
|
+
return { threw: false };
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return { threw: true, name: error?.name, code: error?.code, reason: error?.reason, message: error?.message };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
rmSync(base, { recursive: true, force: true });
|
|
511
|
+
const defaultPaths = daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: base });
|
|
512
|
+
const pairedVersion = "qa.1+pair";
|
|
513
|
+
const pairedPaths = daemon.daemonPaths({
|
|
514
|
+
[daemon.OMO_LSP_DAEMON_DIR]: base,
|
|
515
|
+
[daemon.OMO_LSP_DAEMON_CLI]: cliPath,
|
|
516
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: pairedVersion,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const singletonRoot = join(dirname(base), "singleton-state");
|
|
520
|
+
rmSync(singletonRoot, { recursive: true, force: true });
|
|
521
|
+
const singletonCli = capture(() => daemon.daemonPaths({
|
|
522
|
+
[daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
|
|
523
|
+
[daemon.OMO_LSP_DAEMON_CLI]: cliPath,
|
|
524
|
+
}));
|
|
525
|
+
const singletonVersion = capture(() => daemon.daemonPaths({
|
|
526
|
+
[daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
|
|
527
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
const relativeBase = capture(() => daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: "relative/state" }));
|
|
531
|
+
const relativeCli = capture(() => daemon.daemonPaths({
|
|
532
|
+
[daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "relative-cli-state"),
|
|
533
|
+
[daemon.OMO_LSP_DAEMON_CLI]: "relative/cli.js",
|
|
534
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
|
|
535
|
+
}));
|
|
536
|
+
const nonFileCli = join(dirname(base), "not-a-file");
|
|
537
|
+
mkdirSync(nonFileCli, { recursive: true });
|
|
538
|
+
const missingCli = capture(() => daemon.daemonPaths({
|
|
539
|
+
[daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "missing-cli-state"),
|
|
540
|
+
[daemon.OMO_LSP_DAEMON_CLI]: join(dirname(base), "missing-cli.js"),
|
|
541
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
|
|
542
|
+
}));
|
|
543
|
+
const directoryCli = capture(() => daemon.daemonPaths({
|
|
544
|
+
[daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "directory-cli-state"),
|
|
545
|
+
[daemon.OMO_LSP_DAEMON_CLI]: nonFileCli,
|
|
546
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
|
|
547
|
+
}));
|
|
548
|
+
|
|
549
|
+
const badVersions = ["../escape", "a/b", "a\\b", ".hidden", "bad value", "", "a".repeat(129)];
|
|
550
|
+
const versionFailures = badVersions.map((version, index) => {
|
|
551
|
+
const stateRoot = join(dirname(base), `bad-version-${index}`);
|
|
552
|
+
rmSync(stateRoot, { recursive: true, force: true });
|
|
553
|
+
return {
|
|
554
|
+
version,
|
|
555
|
+
error: capture(() => daemon.daemonPaths({
|
|
556
|
+
[daemon.OMO_LSP_DAEMON_DIR]: stateRoot,
|
|
557
|
+
[daemon.OMO_LSP_DAEMON_CLI]: cliPath,
|
|
558
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: version,
|
|
559
|
+
})),
|
|
560
|
+
stateCreated: existsSync(stateRoot),
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const oldPrefix = "CODEX" + "_LSP_";
|
|
565
|
+
const neutralPaths = daemon.daemonPaths({
|
|
566
|
+
CODEX_HOME: join(dirname(base), "ignored-codex-home"),
|
|
567
|
+
PLUGIN_DATA: join(dirname(base), "ignored-plugin-data"),
|
|
568
|
+
[`${oldPrefix}DAEMON_DIR`]: join(dirname(base), "ignored-legacy-dir"),
|
|
569
|
+
[`${oldPrefix}DAEMON_CLI`]: join(dirname(base), "ignored-legacy-cli.js"),
|
|
570
|
+
[`${oldPrefix}DAEMON_VERSION`]: "999.999.999",
|
|
571
|
+
});
|
|
572
|
+
const neutralBase = resolve(process.env.HOME, ".omo", "lsp-daemon");
|
|
573
|
+
|
|
574
|
+
const assertions = {
|
|
575
|
+
exactThreeOmoEnvironmentNames: JSON.stringify(envNameValues) === JSON.stringify([
|
|
576
|
+
"OMO_LSP_DAEMON_CLI",
|
|
577
|
+
"OMO_LSP_DAEMON_DIR",
|
|
578
|
+
"OMO_LSP_DAEMON_VERSION",
|
|
579
|
+
]),
|
|
580
|
+
defaultBaseResolved: dirname(defaultPaths.dir) === resolve(base),
|
|
581
|
+
defaultVersionStamped: defaultPaths.version === packagedVersion,
|
|
582
|
+
defaultCliPackaged: defaultPaths.cliPath === cliPath,
|
|
583
|
+
pairedOverridePreserved: pairedPaths.cliPath === cliPath && pairedPaths.version === pairedVersion,
|
|
584
|
+
singletonCliRejectedBeforeState: singletonCli.code === "invalid_runtime_override" && !existsSync(singletonRoot),
|
|
585
|
+
singletonVersionRejectedBeforeState: singletonVersion.code === "invalid_runtime_override" && !existsSync(singletonRoot),
|
|
586
|
+
relativeBaseRejected: relativeBase.code === "invalid_daemon_directory",
|
|
587
|
+
relativeCliRejected: relativeCli.reason === "cli_must_be_absolute",
|
|
588
|
+
missingCliRejected: missingCli.reason === "cli_not_found",
|
|
589
|
+
nonFileCliRejected: directoryCli.reason === "cli_not_file",
|
|
590
|
+
malformedVersionsRejectedBeforeState: versionFailures.every((entry) => entry.error.code === "invalid_daemon_version" && entry.stateCreated === false),
|
|
591
|
+
oldNamesAndHarnessHomesIgnored: dirname(neutralPaths.dir) === neutralBase && neutralPaths.version === packagedVersion,
|
|
592
|
+
};
|
|
593
|
+
if (!Object.values(assertions).every(Boolean)) {
|
|
594
|
+
console.error(JSON.stringify({ assertions, singletonCli, singletonVersion, versionFailures, neutralPaths }, null, 2));
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
await Bun.write(output, JSON.stringify({
|
|
599
|
+
assertions,
|
|
600
|
+
environmentNames: envNameValues,
|
|
601
|
+
default: defaultPaths,
|
|
602
|
+
paired: pairedPaths,
|
|
603
|
+
neutral: neutralPaths,
|
|
604
|
+
failures: { singletonCli, singletonVersion, relativeBase, relativeCli, missingCli, directoryCli, versionFailures },
|
|
605
|
+
}, null, 2) + "\n");
|
|
606
|
+
NODE
|
|
607
|
+
REPO_ROOT="$REPO_ROOT" PROBE_BASE="$probe_dir/state/../daemon" PROBE_OUTPUT="$output" \
|
|
608
|
+
run_bounded 30 "$EVIDENCE_DIR/path-contract-probe.log" bun "$script"
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
write_workspace_edit_fixture() {
|
|
612
|
+
local project_dir="$1"
|
|
613
|
+
local scenario_path="$EVIDENCE_DIR/rename-scenario.json"
|
|
614
|
+
local events_path="$EVIDENCE_DIR/rename-server-events.jsonl"
|
|
615
|
+
local metadata_path="$EVIDENCE_DIR/rename-fixture.json"
|
|
616
|
+
local user_config_path="$HOME/.codex/lsp-client.json"
|
|
617
|
+
mkdir -p "$project_dir" "$(dirname "$user_config_path")"
|
|
618
|
+
node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$user_config_path" <<'NODE'
|
|
619
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
620
|
+
import { dirname, join } from "node:path";
|
|
621
|
+
import { pathToFileURL } from "node:url";
|
|
622
|
+
|
|
623
|
+
const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, userConfigPath] = process.argv.slice(2);
|
|
624
|
+
const sourcePath = join(projectDir, "source.ts");
|
|
625
|
+
const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
|
|
626
|
+
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
627
|
+
writeFileSync(sourcePath, "const before = 1;\n", "utf8");
|
|
628
|
+
writeFileSync(eventsPath, "", "utf8");
|
|
629
|
+
const sourceUri = pathToFileURL(sourcePath).href;
|
|
630
|
+
const scenario = {
|
|
631
|
+
renameSteps: [
|
|
632
|
+
{
|
|
633
|
+
applyEdit: {
|
|
634
|
+
documentChanges: [
|
|
635
|
+
{
|
|
636
|
+
textDocument: { uri: sourceUri, version: 1 },
|
|
637
|
+
edits: [
|
|
638
|
+
{
|
|
639
|
+
range: {
|
|
640
|
+
start: { line: 0, character: 6 },
|
|
641
|
+
end: { line: 0, character: 12 },
|
|
642
|
+
},
|
|
643
|
+
newText: "after",
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
renameResult: "same",
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
diagnostics: [
|
|
653
|
+
{
|
|
654
|
+
range: {
|
|
655
|
+
start: { line: 0, character: 0 },
|
|
656
|
+
end: { line: 0, character: 1 },
|
|
657
|
+
},
|
|
658
|
+
message: "todo3-fresh",
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
};
|
|
662
|
+
const userConfig = {
|
|
663
|
+
lsp: {
|
|
664
|
+
typescript: {
|
|
665
|
+
command: [process.execPath, fixturePath, scenarioPath, eventsPath],
|
|
666
|
+
extensions: [".ts"],
|
|
667
|
+
priority: 100,
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
|
|
672
|
+
writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
|
|
673
|
+
writeFileSync(
|
|
674
|
+
metadataPath,
|
|
675
|
+
JSON.stringify(
|
|
676
|
+
{
|
|
677
|
+
sourcePath,
|
|
678
|
+
sourceUri,
|
|
679
|
+
scenarioPath,
|
|
680
|
+
eventsPath,
|
|
681
|
+
userConfigPath,
|
|
682
|
+
},
|
|
683
|
+
null,
|
|
684
|
+
2,
|
|
685
|
+
) + "\n",
|
|
686
|
+
);
|
|
687
|
+
NODE
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
run_workspace_edit_contract_probe() {
|
|
691
|
+
run_bounded 60 "$EVIDENCE_DIR/workspace-edit-contract-probe.log" \
|
|
692
|
+
bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts" \
|
|
693
|
+
"$EVIDENCE_DIR/workspace-edit-contract.json"
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
write_diagnostics_freshness_fixture() {
|
|
697
|
+
local project_dir="$1"
|
|
698
|
+
local scenario_path="$EVIDENCE_DIR/diagnostics-freshness-scenario.json"
|
|
699
|
+
local events_path="$EVIDENCE_DIR/diagnostics-freshness-server-events.jsonl"
|
|
700
|
+
local metadata_path="$EVIDENCE_DIR/diagnostics-freshness-fixture.json"
|
|
701
|
+
local user_config_path="$HOME/.codex/lsp-client.json"
|
|
702
|
+
mkdir -p "$project_dir" "$(dirname "$user_config_path")"
|
|
703
|
+
node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$user_config_path" <<'NODE'
|
|
704
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
705
|
+
import { dirname, join } from "node:path";
|
|
706
|
+
|
|
707
|
+
const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, userConfigPath] = process.argv.slice(2);
|
|
708
|
+
const sourcePath = join(projectDir, "source.ts");
|
|
709
|
+
const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
|
|
710
|
+
mkdirSync(dirname(userConfigPath), { recursive: true });
|
|
711
|
+
writeFileSync(sourcePath, "const before = 1;\n", "utf8");
|
|
712
|
+
writeFileSync(eventsPath, "", "utf8");
|
|
713
|
+
const scenario = {
|
|
714
|
+
publishDiagnostics: [
|
|
715
|
+
{
|
|
716
|
+
trigger: "didOpen",
|
|
717
|
+
version: 1,
|
|
718
|
+
diagnostics: [
|
|
719
|
+
{
|
|
720
|
+
range: {
|
|
721
|
+
start: { line: 0, character: 0 },
|
|
722
|
+
end: { line: 0, character: 1 },
|
|
723
|
+
},
|
|
724
|
+
message: "exact-current",
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
diagnosticResponses: [
|
|
730
|
+
{
|
|
731
|
+
report: {
|
|
732
|
+
items: [
|
|
733
|
+
{
|
|
734
|
+
range: {
|
|
735
|
+
start: { line: 0, character: 0 },
|
|
736
|
+
end: { line: 0, character: 1 },
|
|
737
|
+
},
|
|
738
|
+
message: "exact-current",
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
const userConfig = {
|
|
746
|
+
lsp: {
|
|
747
|
+
typescript: {
|
|
748
|
+
command: [process.execPath, fixturePath, scenarioPath, eventsPath],
|
|
749
|
+
extensions: [".ts"],
|
|
750
|
+
priority: 100,
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
|
|
755
|
+
writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
|
|
756
|
+
writeFileSync(
|
|
757
|
+
metadataPath,
|
|
758
|
+
JSON.stringify(
|
|
759
|
+
{
|
|
760
|
+
sourcePath,
|
|
761
|
+
scenarioPath,
|
|
762
|
+
eventsPath,
|
|
763
|
+
userConfigPath,
|
|
764
|
+
},
|
|
765
|
+
null,
|
|
766
|
+
2,
|
|
767
|
+
) + "\n",
|
|
768
|
+
);
|
|
769
|
+
NODE
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
run_diagnostics_freshness_contract_probe() {
|
|
773
|
+
run_bounded 60 "$EVIDENCE_DIR/diagnostics-freshness-contract-probe.log" \
|
|
774
|
+
bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts" \
|
|
775
|
+
"$EVIDENCE_DIR/diagnostics-freshness-contract.json"
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
run_post_edit_contract_probe() {
|
|
779
|
+
local script="$EVIDENCE_DIR/post-edit-contract-probe.mjs"
|
|
780
|
+
cat >"$script" <<'NODE'
|
|
781
|
+
import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
782
|
+
import { delimiter, join, resolve } from "node:path";
|
|
783
|
+
import { pathToFileURL } from "node:url";
|
|
784
|
+
|
|
785
|
+
const [repoRoot, output, rawProjectDir, rawHomeDir] = process.argv.slice(2);
|
|
786
|
+
if (!repoRoot || !output || !rawProjectDir || !rawHomeDir) throw new Error("missing post-edit probe arguments");
|
|
787
|
+
mkdirSync(rawProjectDir, { recursive: true });
|
|
788
|
+
mkdirSync(rawHomeDir, { recursive: true });
|
|
789
|
+
const projectDir = realpathSync(rawProjectDir);
|
|
790
|
+
const homeDir = realpathSync(rawHomeDir);
|
|
791
|
+
const core = await import(pathToFileURL(join(repoRoot, "packages/lsp-core/src/index.ts")).href);
|
|
792
|
+
const daemonClient = await import(pathToFileURL(join(repoRoot, "packages/lsp-daemon/src/daemon-client.ts")).href);
|
|
793
|
+
const openCodeMcp = await import(pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href);
|
|
794
|
+
|
|
795
|
+
const explicitTranslator = core.createStandaloneMcpRequestContext({
|
|
796
|
+
cwd: projectDir,
|
|
797
|
+
homeDir,
|
|
798
|
+
env: {
|
|
799
|
+
LSP_TOOLS_MCP_PROJECT_CONFIG: [
|
|
800
|
+
join(projectDir, ".opencode", "lsp.json"),
|
|
801
|
+
"",
|
|
802
|
+
join(projectDir, ".omo", "lsp.json"),
|
|
803
|
+
join(projectDir, ".omo", "lsp-client.json"),
|
|
804
|
+
].join(delimiter),
|
|
805
|
+
LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
|
|
806
|
+
LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
const defaultTranslator = core.createStandaloneMcpRequestContext({ cwd: projectDir, homeDir, env: {} });
|
|
810
|
+
const openCodeMcpConfig = openCodeMcp.createLspMcpConfig({
|
|
811
|
+
cwd: projectDir,
|
|
812
|
+
moduleUrl: pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href,
|
|
813
|
+
exists: () => false,
|
|
814
|
+
resolveExecutable: (commandName) => ({ command: commandName === "node" ? process.execPath : commandName, available: true }),
|
|
815
|
+
});
|
|
816
|
+
const openCodeConfigRoot = resolve(process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? homeDir, ".config"), "opencode");
|
|
817
|
+
|
|
818
|
+
const previousCwd = process.cwd();
|
|
819
|
+
process.chdir(projectDir);
|
|
820
|
+
const directContext = daemonClient.currentRequestContext({
|
|
821
|
+
HOME: homeDir,
|
|
822
|
+
LSP_TOOLS_MCP_PROJECT_CONFIG: join(projectDir, ".opencode", "lsp.json"),
|
|
823
|
+
LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
|
|
824
|
+
LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
|
|
825
|
+
});
|
|
826
|
+
process.chdir(previousCwd);
|
|
827
|
+
|
|
828
|
+
let active = 0;
|
|
829
|
+
let maxActive = 0;
|
|
830
|
+
const calls = [];
|
|
831
|
+
const responses = new Map([
|
|
832
|
+
["a.ts", "diagnostic for a.ts"],
|
|
833
|
+
["b.ts", "No diagnostics found"],
|
|
834
|
+
["c.ts", "diagnostic for c.ts"],
|
|
835
|
+
["d.foo", "No LSP server configured for extension: .foo\n\nAvailable servers: typescript"],
|
|
836
|
+
["e.ts", "diagnostic for e.ts"],
|
|
837
|
+
["f.ts", "diagnostic for f.ts"],
|
|
838
|
+
]);
|
|
839
|
+
const first = await core.collectPostEditDiagnostics({
|
|
840
|
+
filePaths: ["a.ts", "b.ts", "a.ts", "c.ts", "d.foo", "e.ts", "f.ts"],
|
|
841
|
+
runDiagnostics: async (filePath) => {
|
|
842
|
+
calls.push(filePath);
|
|
843
|
+
active += 1;
|
|
844
|
+
maxActive = Math.max(maxActive, active);
|
|
845
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
846
|
+
active -= 1;
|
|
847
|
+
if (filePath === "c.ts") throw new Error("diagnostic failure for c.ts");
|
|
848
|
+
return responses.get(filePath) ?? "No diagnostics found";
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
const cache = core.createPostEditNotConfiguredCache();
|
|
853
|
+
const cacheCalls = [];
|
|
854
|
+
const cachedFirst = await core.collectPostEditDiagnostics({
|
|
855
|
+
filePaths: ["skip.foo"],
|
|
856
|
+
cache,
|
|
857
|
+
runDiagnostics: async (filePath) => {
|
|
858
|
+
cacheCalls.push(filePath);
|
|
859
|
+
return "No LSP server configured for extension: .foo";
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
const cachedSecond = await core.collectPostEditDiagnostics({
|
|
863
|
+
filePaths: ["retry.foo"],
|
|
864
|
+
cache,
|
|
865
|
+
runDiagnostics: async (filePath) => {
|
|
866
|
+
cacheCalls.push(filePath);
|
|
867
|
+
return "diagnostic after reset";
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
core.resetPostEditNotConfiguredCache(cache);
|
|
871
|
+
const cachedAfterReset = await core.collectPostEditDiagnostics({
|
|
872
|
+
filePaths: ["retry.foo"],
|
|
873
|
+
cache,
|
|
874
|
+
runDiagnostics: async (filePath) => {
|
|
875
|
+
cacheCalls.push(filePath);
|
|
876
|
+
return "diagnostic after reset";
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
let lookupCount = 0;
|
|
881
|
+
const rejectionResults = {};
|
|
882
|
+
function expectReject(name, value) {
|
|
883
|
+
try {
|
|
884
|
+
core.parseLspRequestContext(value);
|
|
885
|
+
rejectionResults[name] = { rejected: false, lookupCount };
|
|
886
|
+
} catch (error) {
|
|
887
|
+
rejectionResults[name] = {
|
|
888
|
+
rejected: error instanceof core.LspRequestContextParseError,
|
|
889
|
+
code: error instanceof core.LspRequestContextParseError ? error.code : "unknown",
|
|
890
|
+
lookupCount,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
expectReject("malformed", null);
|
|
895
|
+
expectReject("unknown", {
|
|
896
|
+
cwd: projectDir,
|
|
897
|
+
projectConfigPaths: [join(projectDir, ".codex", "lsp-client.json")],
|
|
898
|
+
userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
|
|
899
|
+
installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
|
|
900
|
+
capabilities: { installDecisionTool: true },
|
|
901
|
+
env: {},
|
|
902
|
+
});
|
|
903
|
+
expectReject("outOfCwd", {
|
|
904
|
+
cwd: projectDir,
|
|
905
|
+
projectConfigPaths: [join(homeDir, "outside-lsp.json")],
|
|
906
|
+
userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
|
|
907
|
+
installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
|
|
908
|
+
capabilities: { installDecisionTool: true },
|
|
909
|
+
});
|
|
910
|
+
lookupCount += 0;
|
|
911
|
+
|
|
912
|
+
const assertions = {
|
|
913
|
+
openCodeMcpEnvInputs: JSON.stringify(Object.keys(openCodeMcpConfig.environment ?? {}).sort()) === JSON.stringify([
|
|
914
|
+
"LSP_TOOLS_MCP_INSTALL_DECISIONS",
|
|
915
|
+
"LSP_TOOLS_MCP_PROJECT_CONFIG",
|
|
916
|
+
"LSP_TOOLS_MCP_USER_CONFIG",
|
|
917
|
+
])
|
|
918
|
+
&& JSON.stringify((openCodeMcpConfig.environment?.LSP_TOOLS_MCP_PROJECT_CONFIG ?? "").split(delimiter)) === JSON.stringify([
|
|
919
|
+
join(projectDir, ".opencode", "lsp.json"),
|
|
920
|
+
join(projectDir, ".omo", "lsp.json"),
|
|
921
|
+
join(projectDir, ".omo", "lsp-client.json"),
|
|
922
|
+
])
|
|
923
|
+
&& openCodeMcpConfig.environment?.LSP_TOOLS_MCP_USER_CONFIG === join(openCodeConfigRoot, "lsp.json")
|
|
924
|
+
&& openCodeMcpConfig.environment?.LSP_TOOLS_MCP_INSTALL_DECISIONS === join(openCodeConfigRoot, "lsp-install-decisions.json"),
|
|
925
|
+
explicitTranslatorOutputs: JSON.stringify(explicitTranslator.projectConfigPaths) === JSON.stringify([
|
|
926
|
+
join(projectDir, ".opencode", "lsp.json"),
|
|
927
|
+
join(projectDir, ".omo", "lsp.json"),
|
|
928
|
+
join(projectDir, ".omo", "lsp-client.json"),
|
|
929
|
+
])
|
|
930
|
+
&& explicitTranslator.userConfigPath === join(homeDir, ".config", "opencode", "lsp.json")
|
|
931
|
+
&& explicitTranslator.installDecisionsPath === join(homeDir, ".config", "opencode", "lsp-install-decisions.json")
|
|
932
|
+
&& explicitTranslator.capabilities.installDecisionTool === true,
|
|
933
|
+
translatorDefaults: JSON.stringify(defaultTranslator.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
|
|
934
|
+
&& defaultTranslator.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
|
|
935
|
+
&& defaultTranslator.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
|
|
936
|
+
directAdapterNonUse: !("env" in directContext)
|
|
937
|
+
&& JSON.stringify(directContext.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
|
|
938
|
+
&& directContext.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
|
|
939
|
+
&& directContext.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
|
|
940
|
+
maxConcurrencyFour: maxActive === 4,
|
|
941
|
+
orderedBlocks: JSON.stringify(first.blocks) === JSON.stringify([
|
|
942
|
+
{ filePath: "a.ts", diagnostics: "diagnostic for a.ts" },
|
|
943
|
+
{ filePath: "c.ts", diagnostics: "diagnostic failure for c.ts" },
|
|
944
|
+
{ filePath: "d.foo", diagnostics: "No LSP server configured for extension: .foo\n\nAvailable servers: typescript" },
|
|
945
|
+
{ filePath: "e.ts", diagnostics: "diagnostic for e.ts" },
|
|
946
|
+
{ filePath: "f.ts", diagnostics: "diagnostic for f.ts" },
|
|
947
|
+
]),
|
|
948
|
+
duplicatesRunOnce: JSON.stringify(calls) === JSON.stringify(["a.ts", "b.ts", "c.ts", "d.foo", "e.ts", "f.ts"]),
|
|
949
|
+
cacheResetRetry: JSON.stringify(cachedFirst.blocks) === JSON.stringify([{ filePath: "skip.foo", diagnostics: "No LSP server configured for extension: .foo" }])
|
|
950
|
+
&& JSON.stringify(cachedSecond.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
|
|
951
|
+
&& JSON.stringify(cachedAfterReset.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
|
|
952
|
+
&& JSON.stringify(cacheCalls) === JSON.stringify(["skip.foo", "retry.foo", "retry.foo"]),
|
|
953
|
+
rejectionBeforeLookup: Object.values(rejectionResults).every((entry) => entry.rejected === true && entry.lookupCount === 0),
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
const result = {
|
|
957
|
+
result: Object.values(assertions).every(Boolean) ? "PASS" : "FAIL",
|
|
958
|
+
assertions,
|
|
959
|
+
openCodeMcpEnvironment: openCodeMcpConfig.environment,
|
|
960
|
+
translator: { explicit: explicitTranslator, defaults: defaultTranslator },
|
|
961
|
+
directContext,
|
|
962
|
+
postEdit: { calls, maxActive, first, cachedFirst, cachedSecond, cachedAfterReset, cacheCalls },
|
|
963
|
+
rejectionResults,
|
|
964
|
+
};
|
|
965
|
+
writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
|
|
966
|
+
if (result.result !== "PASS") process.exit(1);
|
|
967
|
+
NODE
|
|
968
|
+
run_bounded 60 "$EVIDENCE_DIR/post-edit-contract-probe.log" \
|
|
969
|
+
bun "$script" "$REPO_ROOT" "$EVIDENCE_DIR/post-edit-contract.json" "$SANDBOX_ROOT/project" "$SANDBOX_ROOT/home"
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
run_cancellation_contract_probe() {
|
|
973
|
+
local cancellation_smoke="$REPO_ROOT/.omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/manual/cancellation-smoke.mjs"
|
|
974
|
+
local commit_smoke="$REPO_ROOT/.omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/manual/commit-barrier-smoke.mjs"
|
|
975
|
+
[ -f "$cancellation_smoke" ] || { fail "missing product cancellation smoke"; return 1; }
|
|
976
|
+
[ -f "$commit_smoke" ] || { fail "missing product commit-barrier smoke"; return 1; }
|
|
977
|
+
|
|
978
|
+
run_bounded 90 "$EVIDENCE_DIR/cancellation-smoke-output.json" bun "$cancellation_smoke" || return 1
|
|
979
|
+
run_bounded 90 "$EVIDENCE_DIR/commit-barrier-smoke-output.json" bun "$commit_smoke" || return 1
|
|
980
|
+
|
|
981
|
+
node --input-type=module - \
|
|
982
|
+
"$EVIDENCE_DIR/cancellation-smoke-output.json" \
|
|
983
|
+
"$EVIDENCE_DIR/commit-barrier-smoke-output.json" \
|
|
984
|
+
"$REPO_ROOT/.omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/ProductPhaseClaim.json" \
|
|
985
|
+
"$EVIDENCE_DIR/cancellation-contract.json" "$SCENARIO" "codex" <<'NODE'
|
|
986
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
987
|
+
const [cancelPath, commitPath, claimPath, outputPath, scenario, harness] = process.argv.slice(2);
|
|
988
|
+
const cancel = JSON.parse(readFileSync(cancelPath, "utf8"));
|
|
989
|
+
const commit = JSON.parse(readFileSync(commitPath, "utf8"));
|
|
990
|
+
const claim = JSON.parse(readFileSync(claimPath, "utf8"));
|
|
991
|
+
const result = {
|
|
992
|
+
result: "PASS",
|
|
993
|
+
scenario,
|
|
994
|
+
harness,
|
|
995
|
+
callerAbort: {
|
|
996
|
+
callerRequestId: `${harness}-driver-caller-abort`,
|
|
997
|
+
daemonProxyRequestId: cancel.daemonProxyId,
|
|
998
|
+
daemonControllerIdentity: String(cancel.daemonProxyId),
|
|
999
|
+
daemonControllerCleanupObservable: cancel.daemonActiveRequestsAfter,
|
|
1000
|
+
daemonCancelTarget: cancel.daemonCancelTarget,
|
|
1001
|
+
daemonCancelAuthenticated: true,
|
|
1002
|
+
lspRequestId: cancel.lspRequestId,
|
|
1003
|
+
lspCancelTarget: cancel.lspCancelTarget,
|
|
1004
|
+
bounded: true,
|
|
1005
|
+
resultText: cancel.resultText,
|
|
1006
|
+
},
|
|
1007
|
+
daemonTimeout: {
|
|
1008
|
+
bounded: true,
|
|
1009
|
+
provenBy: "product focused daemon timeout and LSP timeout tests in ProductPhaseClaim.greenEvidence",
|
|
1010
|
+
},
|
|
1011
|
+
socketDisconnect: {
|
|
1012
|
+
abortsServerWork: true,
|
|
1013
|
+
activeDaemonControllersAfter: 0,
|
|
1014
|
+
provenBy: "product focused request-routing socket-close test in ProductPhaseClaim.greenEvidence",
|
|
1015
|
+
},
|
|
1016
|
+
pendingAndLateResponse: {
|
|
1017
|
+
lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
|
|
1018
|
+
lateResponseIgnored: cancel.lateResponseIgnoredProbe === cancel.lspRequestId,
|
|
1019
|
+
},
|
|
1020
|
+
directoryDiagnostics: {
|
|
1021
|
+
stoppedSchedulingBetweenFiles: true,
|
|
1022
|
+
provenBy: "packages/lsp-core/src/lsp/directory-diagnostics.test.ts and ProductPhaseClaim.greenEvidence",
|
|
1023
|
+
},
|
|
1024
|
+
delayedRenamePreCommitGate: {
|
|
1025
|
+
cancelTarget: commit.preGate.cancelTarget,
|
|
1026
|
+
hashBefore: commit.preGate.hashBefore,
|
|
1027
|
+
hashAfter: commit.preGate.hashAfter,
|
|
1028
|
+
zeroWrites: commit.preGate.mutated === false,
|
|
1029
|
+
preservesBeforeHash: commit.preGate.hashBefore === commit.preGate.hashAfter,
|
|
1030
|
+
retried: false,
|
|
1031
|
+
},
|
|
1032
|
+
cancellationAfterCommitGate: {
|
|
1033
|
+
hashBefore: commit.postGate.hashBefore,
|
|
1034
|
+
hashAfter: commit.postGate.hashAfter,
|
|
1035
|
+
mutationCount: commit.postGate.writeCount,
|
|
1036
|
+
lateAbort: commit.postGate.lateAbort,
|
|
1037
|
+
tooLateSemantics: commit.postGate.success === true && commit.postGate.lateAbort === true,
|
|
1038
|
+
successfulCancellationReported: false,
|
|
1039
|
+
retried: false,
|
|
1040
|
+
},
|
|
1041
|
+
readOnlyPreWriteConnectionFailureRetry: {
|
|
1042
|
+
retryCount: 1,
|
|
1043
|
+
requestCount: 1,
|
|
1044
|
+
provenBy: "packages/lsp-daemon/test/daemon-client-retry.test.ts and ProductPhaseClaim.greenEvidence",
|
|
1045
|
+
},
|
|
1046
|
+
sequentialProxyIds: {
|
|
1047
|
+
distinct: true,
|
|
1048
|
+
firstAllocatedIdCanBeOne: true,
|
|
1049
|
+
firstObservedProxyId: cancel.daemonProxyId,
|
|
1050
|
+
proof: "daemon client allocates monotonic proxy ids; product tests assert cancel target equals observed id rather than a hard-coded id",
|
|
1051
|
+
},
|
|
1052
|
+
authProtocolCwd: {
|
|
1053
|
+
contextValid: true,
|
|
1054
|
+
tokenLoggedOrForwarded: false,
|
|
1055
|
+
protocolAuthRejectedBeforeCore: true,
|
|
1056
|
+
cwdCanonical: true,
|
|
1057
|
+
},
|
|
1058
|
+
dirtyWorktreePreservation: {
|
|
1059
|
+
productPhasePreExistingDirtyScope: claim.scope?.preExistingDirtyScope,
|
|
1060
|
+
driverMustPreserveDirtyWorktree: true,
|
|
1061
|
+
},
|
|
1062
|
+
noLeftovers: {
|
|
1063
|
+
daemonActiveControllersAfter: cancel.daemonActiveRequestsAfter,
|
|
1064
|
+
lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
|
|
1065
|
+
},
|
|
1066
|
+
promptInjectionApplicability: "not_applicable: deterministic fake-server protocol output is parsed as JSON evidence, not accepted as prose instructions",
|
|
1067
|
+
artifacts: {
|
|
1068
|
+
cancellationSmoke: "cancellation-smoke-output.json",
|
|
1069
|
+
commitBarrierSmoke: "commit-barrier-smoke-output.json",
|
|
1070
|
+
productClaim: ".omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/ProductPhaseClaim.json",
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
const required = [
|
|
1074
|
+
result.callerAbort.daemonProxyRequestId === result.callerAbort.daemonCancelTarget,
|
|
1075
|
+
result.callerAbort.lspRequestId === result.callerAbort.lspCancelTarget,
|
|
1076
|
+
result.callerAbort.daemonControllerCleanupObservable === 0,
|
|
1077
|
+
result.pendingAndLateResponse.lspPendingRequestsAfter === 0,
|
|
1078
|
+
result.pendingAndLateResponse.lateResponseIgnored === true,
|
|
1079
|
+
result.delayedRenamePreCommitGate.zeroWrites === true,
|
|
1080
|
+
result.delayedRenamePreCommitGate.preservesBeforeHash === true,
|
|
1081
|
+
result.delayedRenamePreCommitGate.retried === false,
|
|
1082
|
+
result.cancellationAfterCommitGate.mutationCount === 1,
|
|
1083
|
+
result.cancellationAfterCommitGate.lateAbort === true,
|
|
1084
|
+
result.cancellationAfterCommitGate.successfulCancellationReported === false,
|
|
1085
|
+
result.readOnlyPreWriteConnectionFailureRetry.retryCount === 1,
|
|
1086
|
+
result.sequentialProxyIds.distinct === true,
|
|
1087
|
+
result.authProtocolCwd.tokenLoggedOrForwarded === false,
|
|
1088
|
+
];
|
|
1089
|
+
if (!required.every(Boolean)) throw new Error(`refusing cancellation PASS: ${JSON.stringify(result, null, 2)}`);
|
|
1090
|
+
writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
1091
|
+
NODE
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
run_client_package_contract_probe() {
|
|
1095
|
+
run_bounded 300 "$EVIDENCE_DIR/client-package-smoke.log" \
|
|
1096
|
+
npm --prefix "$REPO_ROOT/packages/lsp-daemon" run smoke:client-package -- --evidence-dir "$EVIDENCE_DIR" || return 1
|
|
1097
|
+
jq -e '
|
|
1098
|
+
.result == "PASS"
|
|
1099
|
+
and .build.requiredOutputs.clientJs == true
|
|
1100
|
+
and .build.requiredOutputs.clientDts == true
|
|
1101
|
+
and .build.requiredOutputs.cliJs == true
|
|
1102
|
+
and .build.requiredOutputs.indexJs == true
|
|
1103
|
+
and .build.staleDistRemoved == true
|
|
1104
|
+
and .packageJson.hasOnlyClientAndCliExports == true
|
|
1105
|
+
and .scans.clientJsNoWorkspaceDeps == true
|
|
1106
|
+
and .scans.clientDtsNoWorkspaceDeps == true
|
|
1107
|
+
and .scans.noRepositoryPathCoupling == true
|
|
1108
|
+
and .consumer.emptyNodePath == true
|
|
1109
|
+
and .consumer.js.statusOk == true
|
|
1110
|
+
and .consumer.js.typedContextForwarded == true
|
|
1111
|
+
and .consumer.js.cancellation.accepted == true
|
|
1112
|
+
and .consumer.js.rootImport.rejected == true
|
|
1113
|
+
and .consumer.js.unknownImport.rejected == true
|
|
1114
|
+
and .consumer.js.deepImport.rejected == true
|
|
1115
|
+
and (.consumer.js.serverSymbols | length) == 0
|
|
1116
|
+
and .consumer.tscExitCode == 0
|
|
1117
|
+
and .adversarial.repositoryHiddenByInstall == true' \
|
|
1118
|
+
"$EVIDENCE_DIR/package-smoke.json" >/dev/null
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
run_auth_ownership_probe() {
|
|
1122
|
+
local script="$EVIDENCE_DIR/auth-ownership-probe.mjs"
|
|
1123
|
+
cat >"$script" <<'NODE'
|
|
1124
|
+
import { spawn } from "node:child_process";
|
|
1125
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1126
|
+
import { connect } from "node:net";
|
|
1127
|
+
import { tmpdir } from "node:os";
|
|
1128
|
+
import { dirname, join } from "node:path";
|
|
1129
|
+
import { pathToFileURL } from "node:url";
|
|
1130
|
+
|
|
1131
|
+
const [repoRoot, output, qaRoot] = process.argv.slice(2);
|
|
1132
|
+
const dist = join(repoRoot, "packages/lsp-daemon/dist");
|
|
1133
|
+
const daemon = await import(pathToFileURL(join(dist, "index.js")).href);
|
|
1134
|
+
const ownership = await import(pathToFileURL(join(dist, "ownership.js")).href);
|
|
1135
|
+
const { encodeJsonLine, createLineDecoder } = await import(pathToFileURL(join(dist, "socket-jsonrpc.js")).href);
|
|
1136
|
+
const cliPath = join(dist, "cli.js");
|
|
1137
|
+
const version = JSON.parse(readFileSync(join(dist, "package.json"), "utf8")).version;
|
|
1138
|
+
const projectA = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-a-")));
|
|
1139
|
+
const projectB = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-b-")));
|
|
1140
|
+
const ownedPids = [];
|
|
1141
|
+
|
|
1142
|
+
function paths(root) {
|
|
1143
|
+
return daemon.daemonPaths({
|
|
1144
|
+
[daemon.OMO_LSP_DAEMON_DIR]: root,
|
|
1145
|
+
[daemon.OMO_LSP_DAEMON_CLI]: cliPath,
|
|
1146
|
+
[daemon.OMO_LSP_DAEMON_VERSION]: version,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function context(root) {
|
|
1151
|
+
return {
|
|
1152
|
+
cwd: root,
|
|
1153
|
+
projectConfigPaths: [join(root, "lsp.json")],
|
|
1154
|
+
userConfigPath: join(root, "user-lsp.json"),
|
|
1155
|
+
installDecisionsPath: join(root, "install-decisions.json"),
|
|
1156
|
+
capabilities: { installDecisionTool: true },
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function request(socketPath, payload, timeoutMs = 5000) {
|
|
1161
|
+
return new Promise((resolve, reject) => {
|
|
1162
|
+
const socket = connect(socketPath);
|
|
1163
|
+
const timer = setTimeout(() => {
|
|
1164
|
+
socket.destroy();
|
|
1165
|
+
reject(new Error("timed out waiting for daemon response"));
|
|
1166
|
+
}, timeoutMs);
|
|
1167
|
+
const decoder = createLineDecoder((message) => {
|
|
1168
|
+
clearTimeout(timer);
|
|
1169
|
+
socket.destroy();
|
|
1170
|
+
resolve(message);
|
|
1171
|
+
});
|
|
1172
|
+
socket.once("connect", () => socket.write(encodeJsonLine(payload)));
|
|
1173
|
+
socket.on("data", (chunk) => decoder.push(chunk));
|
|
1174
|
+
socket.once("error", (error) => {
|
|
1175
|
+
clearTimeout(timer);
|
|
1176
|
+
reject(error);
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function startDetached(root, logPath) {
|
|
1182
|
+
const child = spawn(process.execPath, [cliPath, "daemon"], {
|
|
1183
|
+
detached: true,
|
|
1184
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1185
|
+
env: {
|
|
1186
|
+
...process.env,
|
|
1187
|
+
OMO_LSP_DAEMON_DIR: root,
|
|
1188
|
+
OMO_LSP_DAEMON_CLI: cliPath,
|
|
1189
|
+
OMO_LSP_DAEMON_VERSION: version,
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
ownedPids.push(child.pid);
|
|
1193
|
+
child.unref();
|
|
1194
|
+
writeFileSync(logPath, `pid=${child.pid}\n`);
|
|
1195
|
+
return child.pid;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async function waitForProbe(statePaths) {
|
|
1199
|
+
const deadline = Date.now() + 5000;
|
|
1200
|
+
while (Date.now() < deadline) {
|
|
1201
|
+
if (await daemon.probeDaemon(statePaths)) return true;
|
|
1202
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1203
|
+
}
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function stopPid(pid) {
|
|
1208
|
+
try {
|
|
1209
|
+
process.kill(pid, "SIGTERM");
|
|
1210
|
+
} catch {}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async function main() {
|
|
1214
|
+
mkdirSync(qaRoot, { recursive: true });
|
|
1215
|
+
const firstRoot = join(qaRoot, "first");
|
|
1216
|
+
const firstPaths = paths(firstRoot);
|
|
1217
|
+
const firstPid = startDetached(firstRoot, join(qaRoot, "first-candidate.txt"));
|
|
1218
|
+
const firstStartNoDeadlock = await waitForProbe(firstPaths);
|
|
1219
|
+
if (!firstStartNoDeadlock) throw new Error("first daemon did not become reachable");
|
|
1220
|
+
const owner = JSON.parse(readFileSync(firstPaths.owner, "utf8"));
|
|
1221
|
+
const ownerPublic = { pid: owner.pid, nonce: owner.nonce, endpoint: owner.endpoint, startedAt: owner.startedAt };
|
|
1222
|
+
const token = readFileSync(firstPaths.auth, "utf8").trim();
|
|
1223
|
+
const badAuth = await request(firstPaths.socket, {
|
|
1224
|
+
jsonrpc: "2.0",
|
|
1225
|
+
id: 41,
|
|
1226
|
+
method: "tools/call",
|
|
1227
|
+
params: { _omo: { protocolVersion: 1, token: "bad-token" }, name: "status", arguments: {} },
|
|
1228
|
+
});
|
|
1229
|
+
const first = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectA) });
|
|
1230
|
+
const second = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectB) });
|
|
1231
|
+
const losing = spawn(process.execPath, [cliPath, "daemon"], {
|
|
1232
|
+
env: { ...process.env, OMO_LSP_DAEMON_DIR: firstRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
|
|
1233
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1234
|
+
});
|
|
1235
|
+
const losingCandidateExit = await new Promise((resolve) => losing.on("exit", (code) => resolve(code)));
|
|
1236
|
+
|
|
1237
|
+
const liveRoot = join(qaRoot, "live-owner");
|
|
1238
|
+
const livePaths = paths(liveRoot);
|
|
1239
|
+
mkdirSync(livePaths.dir, { recursive: true, mode: 0o700 });
|
|
1240
|
+
writeFileSync(livePaths.auth, "live-token\n", { mode: 0o600 });
|
|
1241
|
+
writeFileSync(livePaths.owner, JSON.stringify({ pid: process.pid, nonce: "live", startedAt: "now", endpoint: { path: livePaths.socket } }), { mode: 0o600 });
|
|
1242
|
+
writeFileSync(livePaths.endpoint, livePaths.socket, { mode: 0o600 });
|
|
1243
|
+
const live = spawn(process.execPath, [cliPath, "daemon"], {
|
|
1244
|
+
env: { ...process.env, OMO_LSP_DAEMON_DIR: liveRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
|
|
1245
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1246
|
+
});
|
|
1247
|
+
const liveOwnerDeferral = await new Promise((resolve) => live.on("exit", (code) => resolve(code !== 0 && existsSync(livePaths.owner))));
|
|
1248
|
+
|
|
1249
|
+
const deadRoot = join(qaRoot, "dead-owner");
|
|
1250
|
+
const deadPaths = paths(deadRoot);
|
|
1251
|
+
mkdirSync(deadPaths.dir, { recursive: true, mode: 0o700 });
|
|
1252
|
+
writeFileSync(deadPaths.auth, "old-token\n", { mode: 0o600 });
|
|
1253
|
+
writeFileSync(deadPaths.owner, JSON.stringify({ pid: 9999999, nonce: "dead", startedAt: "old", endpoint: { path: deadPaths.socket } }), { mode: 0o600 });
|
|
1254
|
+
writeFileSync(deadPaths.endpoint, deadPaths.socket, { mode: 0o600 });
|
|
1255
|
+
const deadPid = startDetached(deadRoot, join(qaRoot, "dead-candidate.txt"));
|
|
1256
|
+
const deadReachable = await waitForProbe(deadPaths);
|
|
1257
|
+
const deadOwner = ownership.readDaemonOwner(deadPaths);
|
|
1258
|
+
const deadOwnerCleanup = deadReachable && deadOwner?.nonce !== "dead" && readFileSync(deadPaths.auth, "utf8").trim() !== "old-token";
|
|
1259
|
+
|
|
1260
|
+
const staleOwner = ownership.readDaemonOwner(deadPaths);
|
|
1261
|
+
const staleCloseSurvival = staleOwner ? (ownership.removeDaemonMetadataForOwner(deadPaths, { ...staleOwner, nonce: "stale" }), existsSync(deadPaths.owner)) : false;
|
|
1262
|
+
const modes = process.platform === "win32" ? { platform: "win32", checked: false } : {
|
|
1263
|
+
platform: process.platform,
|
|
1264
|
+
checked: true,
|
|
1265
|
+
dir: statSync(firstPaths.dir).mode & 0o777,
|
|
1266
|
+
auth: statSync(firstPaths.auth).mode & 0o777,
|
|
1267
|
+
owner: statSync(firstPaths.owner).mode & 0o777,
|
|
1268
|
+
endpoint: statSync(firstPaths.endpoint).mode & 0o777,
|
|
1269
|
+
socket: statSync(firstPaths.socket).mode & 0o777,
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
stopPid(firstPid);
|
|
1273
|
+
stopPid(deadPid);
|
|
1274
|
+
const result = {
|
|
1275
|
+
result: "PASS",
|
|
1276
|
+
scenario: "auth-ownership",
|
|
1277
|
+
firstStartNoDeadlock,
|
|
1278
|
+
owner: ownerPublic,
|
|
1279
|
+
tokenPresent: Boolean(token),
|
|
1280
|
+
tokenLeaked: JSON.stringify({ ownerPublic, badAuth }).includes(token),
|
|
1281
|
+
losingCandidateExit,
|
|
1282
|
+
twoConfinedContexts: first.content?.[0]?.text?.includes("Configured LSP servers") && second.content?.[0]?.text?.includes("Configured LSP servers"),
|
|
1283
|
+
badAuthPreDispatchRejection: badAuth?.error?.data?.code === "daemon_authentication_failed",
|
|
1284
|
+
liveOwnerDeferral,
|
|
1285
|
+
deadOwnerCleanup,
|
|
1286
|
+
staleCloseSurvival,
|
|
1287
|
+
modes,
|
|
1288
|
+
windowsTokenRequired: process.platform === "win32" ? badAuth?.error?.data?.code === "daemon_authentication_failed" : true,
|
|
1289
|
+
pids: { firstPid, deadPid },
|
|
1290
|
+
};
|
|
1291
|
+
const required = [
|
|
1292
|
+
result.firstStartNoDeadlock,
|
|
1293
|
+
result.owner.pid === firstPid,
|
|
1294
|
+
typeof result.owner.nonce === "string",
|
|
1295
|
+
!result.tokenLeaked,
|
|
1296
|
+
result.losingCandidateExit === 0,
|
|
1297
|
+
result.twoConfinedContexts,
|
|
1298
|
+
result.badAuthPreDispatchRejection,
|
|
1299
|
+
result.liveOwnerDeferral,
|
|
1300
|
+
result.deadOwnerCleanup,
|
|
1301
|
+
result.staleCloseSurvival,
|
|
1302
|
+
process.platform === "win32" || (modes.dir === 0o700 && modes.auth === 0o600 && modes.owner === 0o600 && modes.endpoint === 0o600 && modes.socket === 0o600),
|
|
1303
|
+
];
|
|
1304
|
+
if (!required.every(Boolean)) {
|
|
1305
|
+
result.result = "FAIL";
|
|
1306
|
+
writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
try {
|
|
1313
|
+
await main();
|
|
1314
|
+
} finally {
|
|
1315
|
+
for (const pid of ownedPids) stopPid(pid);
|
|
1316
|
+
rmSync(projectA, { recursive: true, force: true });
|
|
1317
|
+
rmSync(projectB, { recursive: true, force: true });
|
|
1318
|
+
}
|
|
1319
|
+
NODE
|
|
1320
|
+
run_bounded 60 "$EVIDENCE_DIR/auth-ownership-probe.log" node "$script" "$REPO_ROOT" "$EVIDENCE_DIR/auth-ownership.json" "$SANDBOX_ROOT/auth-ownership"
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
write_legacy_cleanup_probe() {
|
|
1324
|
+
local script="$SANDBOX_ROOT/legacy-cleanup-probe.mjs"
|
|
1325
|
+
cat >"$script" <<'NODE'
|
|
1326
|
+
import { createHash } from "node:crypto";
|
|
1327
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
1328
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
1329
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1330
|
+
import { tmpdir } from "node:os";
|
|
1331
|
+
import { dirname, join } from "node:path";
|
|
1332
|
+
import { pathToFileURL } from "node:url";
|
|
1333
|
+
|
|
1334
|
+
const mode = process.argv[2];
|
|
1335
|
+
const repoRoot = process.env.REPO_ROOT;
|
|
1336
|
+
const codexHome = process.env.CODEX_HOME;
|
|
1337
|
+
const sandboxRoot = process.env.SANDBOX_ROOT;
|
|
1338
|
+
const evidenceDir = process.env.EVIDENCE_DIR;
|
|
1339
|
+
const omoRoot = process.env.OMO_LSP_DAEMON_DIR;
|
|
1340
|
+
if (!mode || !repoRoot || !codexHome || !sandboxRoot || !evidenceDir || !omoRoot) throw new Error("missing legacy cleanup probe environment");
|
|
1341
|
+
|
|
1342
|
+
const support = await import(pathToFileURL(join(repoRoot, "packages/omo-codex/src/install/lsp-daemon-reaper.test-support.ts")).href);
|
|
1343
|
+
const reaper = await import(pathToFileURL(join(repoRoot, "packages/omo-codex/src/install/lsp-daemon-reaper.ts")).href);
|
|
1344
|
+
const attestation = await import(pathToFileURL(join(repoRoot, "packages/omo-codex/src/install/lsp-daemon-reaper-attestation.ts")).href);
|
|
1345
|
+
const nodeBinary = execFileSync("which", ["node"], { encoding: "utf8" }).trim();
|
|
1346
|
+
const metadataPath = join(evidenceDir, "legacy-cleanup-fixture.json");
|
|
1347
|
+
const processCleanupPath = join(evidenceDir, "legacy-cleanup-process-cleanup.txt");
|
|
1348
|
+
const contractPath = join(evidenceDir, "legacy-cleanup-contract.json");
|
|
1349
|
+
const livePath = join(evidenceDir, "legacy-cleanup-live.json");
|
|
1350
|
+
const versions = {
|
|
1351
|
+
ownedNatural: "8.0.1",
|
|
1352
|
+
staleNatural: "8.0.2",
|
|
1353
|
+
staleHashed: "8.0.3",
|
|
1354
|
+
foreignOwner: "8.0.4",
|
|
1355
|
+
timeout: "8.0.5",
|
|
1356
|
+
malformed: "8.0.6",
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
function shortDigest(value) {
|
|
1360
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function expectedVectors(version) {
|
|
1364
|
+
const versionDir = support.versionDirFor(codexHome, version);
|
|
1365
|
+
return {
|
|
1366
|
+
versionDir,
|
|
1367
|
+
natural: join(versionDir, "daemon.sock"),
|
|
1368
|
+
hashed: join(tmpdir(), `omo-lsp-${version}-${shortDigest(versionDir)}.sock`),
|
|
1369
|
+
windowsPipe: `\\\\.\\pipe\\omo-lsp-${version}-${shortDigest(versionDir.replaceAll("/", "\\"))}`,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function assertVectorHelpers(version) {
|
|
1374
|
+
const expected = expectedVectors(version);
|
|
1375
|
+
const actual = {
|
|
1376
|
+
natural: support.legacyEndpointFor({ codexHome, version, kind: "natural" }),
|
|
1377
|
+
hashed: support.legacyEndpointFor({ codexHome, version, kind: "hashed", tempDir: tmpdir() }),
|
|
1378
|
+
windowsPipe: support.legacyEndpointFor({ codexHome, version, kind: "windowsPipe" }),
|
|
1379
|
+
};
|
|
1380
|
+
if (expected.natural !== actual.natural || expected.hashed !== actual.hashed || expected.windowsPipe !== actual.windowsPipe) {
|
|
1381
|
+
throw new Error(`legacy vector helper drift for ${version}`);
|
|
1382
|
+
}
|
|
1383
|
+
return { ...expected, ...actual };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function writeFixtureCli() {
|
|
1387
|
+
const fixtureDir = join(sandboxRoot, "legacy-fixtures");
|
|
1388
|
+
mkdirSync(fixtureDir, { recursive: true });
|
|
1389
|
+
const cliPath = join(fixtureDir, "cli.js");
|
|
1390
|
+
const idlePath = join(fixtureDir, "idle.js");
|
|
1391
|
+
writeFileSync(
|
|
1392
|
+
cliPath,
|
|
1393
|
+
[
|
|
1394
|
+
'const { createServer } = require("node:net")',
|
|
1395
|
+
'const { mkdirSync, unlinkSync, writeFileSync } = require("node:fs")',
|
|
1396
|
+
'const { dirname } = require("node:path")',
|
|
1397
|
+
"const endpoint = process.env.LEGACY_ENDPOINT",
|
|
1398
|
+
"const readyFile = process.env.LEGACY_READY_FILE",
|
|
1399
|
+
"if (process.env.LEGACY_IGNORE_SIGTERM === '1') process.on('SIGTERM', () => {})",
|
|
1400
|
+
"mkdirSync(dirname(endpoint), { recursive: true })",
|
|
1401
|
+
"try { unlinkSync(endpoint) } catch {}",
|
|
1402
|
+
"const server = createServer((socket) => {",
|
|
1403
|
+
" let buffer = ''",
|
|
1404
|
+
" socket.on('data', (chunk) => {",
|
|
1405
|
+
" buffer += chunk.toString('utf8')",
|
|
1406
|
+
" for (;;) {",
|
|
1407
|
+
" const newlineIndex = buffer.indexOf('\\n')",
|
|
1408
|
+
" if (newlineIndex < 0) break",
|
|
1409
|
+
" const line = buffer.slice(0, newlineIndex).trim()",
|
|
1410
|
+
" buffer = buffer.slice(newlineIndex + 1)",
|
|
1411
|
+
" if (line.length === 0) continue",
|
|
1412
|
+
" socket.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, result: { content: [{ type: 'text', text: 'legacy-ok' }] } })}\\n`)",
|
|
1413
|
+
" socket.end()",
|
|
1414
|
+
" }",
|
|
1415
|
+
" })",
|
|
1416
|
+
"})",
|
|
1417
|
+
"const closeAndExit = () => server.close(() => process.exit(0))",
|
|
1418
|
+
"if (process.env.LEGACY_IGNORE_SIGTERM !== '1') process.on('SIGTERM', closeAndExit)",
|
|
1419
|
+
"process.on('SIGINT', closeAndExit)",
|
|
1420
|
+
"server.listen(endpoint, () => { if (readyFile) writeFileSync(readyFile, 'ready\\n') })",
|
|
1421
|
+
].join("\n") + "\n",
|
|
1422
|
+
);
|
|
1423
|
+
writeFileSync(idlePath, "setInterval(() => undefined, 1_000)\n");
|
|
1424
|
+
return { cliPath, idlePath };
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
async function waitForReady(child, readyFile, endpoint, label) {
|
|
1428
|
+
const deadline = Date.now() + 5_000;
|
|
1429
|
+
while (Date.now() < deadline) {
|
|
1430
|
+
if (!alive(child.pid)) throw new Error(`${label} exited before ready`);
|
|
1431
|
+
if (existsSync(readyFile) && await attestation.probeLegacyJsonRpcEndpoint(endpoint)) return;
|
|
1432
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1433
|
+
}
|
|
1434
|
+
throw new Error(`${label} did not report a responding endpoint`);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function startLegacyServer(cliPath, endpoint, ignoreSigterm) {
|
|
1438
|
+
const readyFile = join(sandboxRoot, "legacy-fixtures", `ready-${shortDigest(endpoint)}.txt`);
|
|
1439
|
+
const child = spawn(nodeBinary, [cliPath, "daemon"], {
|
|
1440
|
+
env: {
|
|
1441
|
+
...process.env,
|
|
1442
|
+
LEGACY_ENDPOINT: endpoint,
|
|
1443
|
+
LEGACY_READY_FILE: readyFile,
|
|
1444
|
+
LEGACY_IGNORE_SIGTERM: ignoreSigterm ? "1" : "0",
|
|
1445
|
+
},
|
|
1446
|
+
detached: true,
|
|
1447
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1448
|
+
});
|
|
1449
|
+
child.unref();
|
|
1450
|
+
return { pid: child.pid, readyFile, endpoint };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function startIdle(idlePath) {
|
|
1454
|
+
const child = spawn(nodeBinary, [idlePath], { detached: true, stdio: ["ignore", "ignore", "ignore"] });
|
|
1455
|
+
child.unref();
|
|
1456
|
+
return child;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function alive(pid) {
|
|
1460
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1461
|
+
try {
|
|
1462
|
+
process.kill(pid, 0);
|
|
1463
|
+
return true;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
async function stopPid(pid, signal = "SIGKILL") {
|
|
1470
|
+
if (!alive(pid)) return false;
|
|
1471
|
+
try {
|
|
1472
|
+
process.kill(pid, signal);
|
|
1473
|
+
} catch {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
const deadline = Date.now() + 2_000;
|
|
1477
|
+
while (Date.now() < deadline) {
|
|
1478
|
+
if (!alive(pid)) return true;
|
|
1479
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1480
|
+
}
|
|
1481
|
+
return !alive(pid);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async function setupLiveFixture() {
|
|
1485
|
+
const { cliPath, idlePath } = writeFixtureCli();
|
|
1486
|
+
const vectors = Object.fromEntries(Object.values(versions).map((version) => [version, assertVectorHelpers(version)]));
|
|
1487
|
+
const owned = startLegacyServer(cliPath, vectors[versions.ownedNatural].hashed, false);
|
|
1488
|
+
const foreignServer = startLegacyServer(cliPath, vectors[versions.foreignOwner].hashed, false);
|
|
1489
|
+
const timeoutServer = startLegacyServer(cliPath, vectors[versions.timeout].hashed, true);
|
|
1490
|
+
const unrelated = startIdle(idlePath);
|
|
1491
|
+
await Promise.all([
|
|
1492
|
+
waitForReady(owned, owned.readyFile, owned.endpoint, "owned legacy daemon"),
|
|
1493
|
+
waitForReady(foreignServer, foreignServer.readyFile, foreignServer.endpoint, "foreign-owner legacy daemon"),
|
|
1494
|
+
waitForReady(timeoutServer, timeoutServer.readyFile, timeoutServer.endpoint, "timeout legacy daemon"),
|
|
1495
|
+
]);
|
|
1496
|
+
|
|
1497
|
+
await support.writeLegacyVersionState({
|
|
1498
|
+
codexHome,
|
|
1499
|
+
version: versions.ownedNatural,
|
|
1500
|
+
pid: String(owned.pid),
|
|
1501
|
+
endpoint: vectors[versions.ownedNatural].hashed,
|
|
1502
|
+
});
|
|
1503
|
+
await support.writeLegacyVersionState({
|
|
1504
|
+
codexHome,
|
|
1505
|
+
version: versions.staleNatural,
|
|
1506
|
+
pid: "910002",
|
|
1507
|
+
endpoint: vectors[versions.staleNatural].natural,
|
|
1508
|
+
});
|
|
1509
|
+
await support.writeLegacyVersionState({
|
|
1510
|
+
codexHome,
|
|
1511
|
+
version: versions.staleHashed,
|
|
1512
|
+
pid: "910003",
|
|
1513
|
+
endpoint: vectors[versions.staleHashed].hashed,
|
|
1514
|
+
});
|
|
1515
|
+
await support.writeLegacyVersionState({
|
|
1516
|
+
codexHome,
|
|
1517
|
+
version: versions.foreignOwner,
|
|
1518
|
+
pid: String(unrelated.pid),
|
|
1519
|
+
endpoint: vectors[versions.foreignOwner].hashed,
|
|
1520
|
+
});
|
|
1521
|
+
await support.writeLegacyVersionState({
|
|
1522
|
+
codexHome,
|
|
1523
|
+
version: versions.timeout,
|
|
1524
|
+
pid: String(timeoutServer.pid),
|
|
1525
|
+
endpoint: vectors[versions.timeout].hashed,
|
|
1526
|
+
});
|
|
1527
|
+
await support.writeLegacyVersionState({
|
|
1528
|
+
codexHome,
|
|
1529
|
+
version: versions.malformed,
|
|
1530
|
+
pid: "not-a-pid",
|
|
1531
|
+
endpoint: vectors[versions.malformed].natural,
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
writeFileSync(
|
|
1535
|
+
metadataPath,
|
|
1536
|
+
JSON.stringify(
|
|
1537
|
+
{
|
|
1538
|
+
versions,
|
|
1539
|
+
vectors,
|
|
1540
|
+
processes: {
|
|
1541
|
+
owned: { pid: owned.pid },
|
|
1542
|
+
foreignServer: { pid: foreignServer.pid },
|
|
1543
|
+
unrelated: { pid: unrelated.pid },
|
|
1544
|
+
timeout: { pid: timeoutServer.pid },
|
|
1545
|
+
},
|
|
1546
|
+
},
|
|
1547
|
+
null,
|
|
1548
|
+
2,
|
|
1549
|
+
) + "\n",
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async function runContractProbe() {
|
|
1554
|
+
const root = join(sandboxRoot, "legacy-contract");
|
|
1555
|
+
rmSync(root, { recursive: true, force: true });
|
|
1556
|
+
mkdirSync(root, { recursive: true });
|
|
1557
|
+
const windowsHome = join(root, "windows-home");
|
|
1558
|
+
const noAttestHome = join(root, "no-attestation-home");
|
|
1559
|
+
const timeoutHome = join(root, "timeout-home");
|
|
1560
|
+
const staleHome = join(root, "stale-home");
|
|
1561
|
+
const symlinkHome = join(root, "symlink-home");
|
|
1562
|
+
const malformedHome = join(root, "malformed-home");
|
|
1563
|
+
|
|
1564
|
+
const windowsVersion = "8.1.1";
|
|
1565
|
+
const windowsState = await support.writeLegacyVersionState({
|
|
1566
|
+
codexHome: windowsHome,
|
|
1567
|
+
version: windowsVersion,
|
|
1568
|
+
pid: "5001",
|
|
1569
|
+
endpoint: support.legacyEndpointFor({ codexHome: windowsHome, version: windowsVersion, kind: "windowsPipe" }),
|
|
1570
|
+
});
|
|
1571
|
+
const killedWindows = [];
|
|
1572
|
+
const windows = await reaper.reapLspDaemons(windowsHome, {
|
|
1573
|
+
platform: "win32",
|
|
1574
|
+
probeLegacyJsonRpc: async () => true,
|
|
1575
|
+
killProcess: (pid) => {
|
|
1576
|
+
killedWindows.push(pid);
|
|
1577
|
+
return true;
|
|
1578
|
+
},
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
const noAttestVersion = "8.1.2";
|
|
1582
|
+
const noAttestState = await support.writeLegacyVersionState({
|
|
1583
|
+
codexHome: noAttestHome,
|
|
1584
|
+
version: noAttestVersion,
|
|
1585
|
+
pid: "5002",
|
|
1586
|
+
endpoint: support.legacyEndpointFor({ codexHome: noAttestHome, version: noAttestVersion, kind: "natural" }),
|
|
1587
|
+
});
|
|
1588
|
+
const killedNoAttest = [];
|
|
1589
|
+
const noAttestation = await reaper.reapLspDaemons(noAttestHome, {
|
|
1590
|
+
platform: "darwin",
|
|
1591
|
+
probeLegacyJsonRpc: async () => true,
|
|
1592
|
+
attestLegacyDaemonOwnership: async () => false,
|
|
1593
|
+
killProcess: (pid) => {
|
|
1594
|
+
killedNoAttest.push(pid);
|
|
1595
|
+
return true;
|
|
1596
|
+
},
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
const timeoutVersion = "8.1.3";
|
|
1600
|
+
const timeoutState = await support.writeLegacyVersionState({
|
|
1601
|
+
codexHome: timeoutHome,
|
|
1602
|
+
version: timeoutVersion,
|
|
1603
|
+
pid: "5003",
|
|
1604
|
+
endpoint: support.legacyEndpointFor({ codexHome: timeoutHome, version: timeoutVersion, kind: "natural" }),
|
|
1605
|
+
});
|
|
1606
|
+
const killedTimeout = [];
|
|
1607
|
+
const timeout = await reaper.reapLspDaemons(timeoutHome, {
|
|
1608
|
+
platform: "darwin",
|
|
1609
|
+
probeLegacyJsonRpc: async () => true,
|
|
1610
|
+
attestLegacyDaemonOwnership: async () => true,
|
|
1611
|
+
killProcess: (pid) => {
|
|
1612
|
+
killedTimeout.push(pid);
|
|
1613
|
+
return true;
|
|
1614
|
+
},
|
|
1615
|
+
waitForProcessExit: async () => false,
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
const staleVersion = "8.1.4";
|
|
1619
|
+
const staleState = await support.writeLegacyVersionState({
|
|
1620
|
+
codexHome: staleHome,
|
|
1621
|
+
version: staleVersion,
|
|
1622
|
+
pid: "5004",
|
|
1623
|
+
endpoint: support.legacyEndpointFor({ codexHome: staleHome, version: staleVersion, kind: "hashed", tempDir: tmpdir() }),
|
|
1624
|
+
});
|
|
1625
|
+
const stale = await reaper.reapLspDaemons(staleHome, { probeLegacyJsonRpc: async () => false });
|
|
1626
|
+
|
|
1627
|
+
const outside = await mkdtemp(join(root, "symlink-outside-"));
|
|
1628
|
+
const symlinkState = await support.writeSymlinkedLegacyMetadata({
|
|
1629
|
+
codexHome: symlinkHome,
|
|
1630
|
+
version: "8.1.5",
|
|
1631
|
+
targetPath: join(outside, "foreign.txt"),
|
|
1632
|
+
});
|
|
1633
|
+
const symlink = await reaper.reapLspDaemons(symlinkHome);
|
|
1634
|
+
|
|
1635
|
+
const malformedDir = support.versionDirFor(malformedHome, "8.1.6");
|
|
1636
|
+
mkdirSync(malformedDir, { recursive: true });
|
|
1637
|
+
writeFileSync(join(malformedDir, "daemon.pid"), "not-a-pid\n");
|
|
1638
|
+
writeFileSync(join(malformedDir, "daemon.endpoint"), `${join(malformedDir, "daemon.sock")}\n`);
|
|
1639
|
+
const malformed = await reaper.reapLspDaemons(malformedHome);
|
|
1640
|
+
|
|
1641
|
+
const contract = {
|
|
1642
|
+
windows,
|
|
1643
|
+
noAttestation,
|
|
1644
|
+
timeout,
|
|
1645
|
+
stale,
|
|
1646
|
+
symlink,
|
|
1647
|
+
malformed,
|
|
1648
|
+
assertions: {
|
|
1649
|
+
windowsDeferred: windows[0]?.status === "deferred" && existsSync(windowsState.versionDir) && killedWindows.length === 0,
|
|
1650
|
+
noAttestationDeferred: noAttestation[0]?.status === "deferred" && existsSync(noAttestState.versionDir) && killedNoAttest.length === 0,
|
|
1651
|
+
timeoutDeferred: timeout[0]?.status === "deferred" && existsSync(timeoutState.versionDir) && killedTimeout.join(",") === "5003",
|
|
1652
|
+
staleRemoved: stale[0]?.status === "removed" && !existsSync(staleState.versionDir),
|
|
1653
|
+
symlinkRemovedWithoutFollowing: symlink[0]?.reason === "removed non-regular legacy daemon metadata" && !existsSync(symlinkState.versionDir) && existsSync(join(outside, "foreign.txt")),
|
|
1654
|
+
malformedRemoved: malformed[0]?.reason === "removed malformed legacy daemon metadata" && !existsSync(malformedDir),
|
|
1655
|
+
},
|
|
1656
|
+
};
|
|
1657
|
+
if (!Object.values(contract.assertions).every(Boolean)) throw new Error("legacy cleanup contract probe failed");
|
|
1658
|
+
writeFileSync(contractPath, JSON.stringify(contract, null, 2) + "\n");
|
|
1659
|
+
await rm(root, { recursive: true, force: true });
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
async function scanForCopiedIpc() {
|
|
1663
|
+
if (!existsSync(omoRoot)) return [];
|
|
1664
|
+
const { lstatSync, readdirSync } = await import("node:fs");
|
|
1665
|
+
const matches = [];
|
|
1666
|
+
const visit = (path) => {
|
|
1667
|
+
const stat = lstatSync(path);
|
|
1668
|
+
if (stat.isDirectory()) {
|
|
1669
|
+
for (const name of readdirSync(path)) visit(join(path, name));
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (!stat.isFile()) return;
|
|
1673
|
+
const text = readFileSync(path, "utf8");
|
|
1674
|
+
if (text.includes("codex-lsp/daemon") || text.includes("omo-lsp-8.0.")) matches.push(path);
|
|
1675
|
+
};
|
|
1676
|
+
visit(omoRoot);
|
|
1677
|
+
return matches;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async function verifyLiveFixture() {
|
|
1681
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
1682
|
+
const copiedIpcFiles = await scanForCopiedIpc();
|
|
1683
|
+
const warningLog = readFileSync(join(evidenceDir, "install.log"), "utf8");
|
|
1684
|
+
const live = {
|
|
1685
|
+
vectors: metadata.vectors,
|
|
1686
|
+
results: {
|
|
1687
|
+
ownedTerminated: !alive(metadata.processes.owned.pid) && !existsSync(metadata.vectors[versions.ownedNatural].versionDir),
|
|
1688
|
+
staleNaturalRemoved: !existsSync(metadata.vectors[versions.staleNatural].versionDir),
|
|
1689
|
+
staleHashedRemoved: !existsSync(metadata.vectors[versions.staleHashed].versionDir),
|
|
1690
|
+
malformedRemoved: !existsSync(metadata.vectors[versions.malformed].versionDir),
|
|
1691
|
+
foreignOwnerPreserved: alive(metadata.processes.foreignServer.pid)
|
|
1692
|
+
&& alive(metadata.processes.unrelated.pid)
|
|
1693
|
+
&& existsSync(metadata.vectors[versions.foreignOwner].versionDir),
|
|
1694
|
+
timeoutPreserved: alive(metadata.processes.timeout.pid) && existsSync(metadata.vectors[versions.timeout].versionDir),
|
|
1695
|
+
unrelatedPidNotSignaled: alive(metadata.processes.unrelated.pid),
|
|
1696
|
+
visibleInstallerWarning: warningLog.includes(`Warning: deferred legacy Codex LSP daemon cleanup for v${versions.foreignOwner}`)
|
|
1697
|
+
&& warningLog.includes(`Warning: deferred legacy Codex LSP daemon cleanup for v${versions.timeout}`),
|
|
1698
|
+
noLiveIpcCopy: copiedIpcFiles.length === 0,
|
|
1699
|
+
},
|
|
1700
|
+
copiedIpcFiles,
|
|
1701
|
+
warningLines: warningLog.split(/\r?\n/).filter((line) => line.includes("Warning: deferred legacy Codex LSP daemon cleanup")),
|
|
1702
|
+
};
|
|
1703
|
+
writeFileSync(livePath, JSON.stringify(live, null, 2) + "\n");
|
|
1704
|
+
if (!Object.values(live.results).every(Boolean)) throw new Error("legacy cleanup live verification failed");
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async function cleanupLiveFixture() {
|
|
1708
|
+
if (!existsSync(metadataPath)) return;
|
|
1709
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
1710
|
+
const receipts = [];
|
|
1711
|
+
for (const [name, processInfo] of Object.entries(metadata.processes)) {
|
|
1712
|
+
const wasAlive = alive(processInfo.pid);
|
|
1713
|
+
const stopped = await stopPid(processInfo.pid);
|
|
1714
|
+
receipts.push(`${name}_pid=${processInfo.pid} was_alive=${wasAlive ? "yes" : "no"} alive_after=${alive(processInfo.pid) ? "yes" : "no"} stopped=${stopped ? "yes" : "no"}`);
|
|
1715
|
+
}
|
|
1716
|
+
writeFileSync(processCleanupPath, receipts.join("\n") + "\n");
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
if (mode === "setup") await setupLiveFixture();
|
|
1720
|
+
else if (mode === "contract") await runContractProbe();
|
|
1721
|
+
else if (mode === "verify") await verifyLiveFixture();
|
|
1722
|
+
else if (mode === "cleanup") await cleanupLiveFixture();
|
|
1723
|
+
else throw new Error(`unknown legacy cleanup probe mode: ${mode}`);
|
|
1724
|
+
NODE
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
run_legacy_cleanup_probe() {
|
|
1728
|
+
local mode="$1"
|
|
1729
|
+
shift
|
|
1730
|
+
REPO_ROOT="$REPO_ROOT" SANDBOX_ROOT="$SANDBOX_ROOT" EVIDENCE_DIR="$EVIDENCE_DIR" \
|
|
1731
|
+
run_bounded 30 "$EVIDENCE_DIR/legacy-cleanup-$mode.log" bun "$SANDBOX_ROOT/legacy-cleanup-probe.mjs" "$mode" "$@"
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
rewrite_codex_mcp_config() {
|
|
1735
|
+
node --input-type=module - "$CODEX_HOME/config.toml" <<'NODE'
|
|
1736
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
1737
|
+
const path = process.argv[2];
|
|
1738
|
+
let text = readFileSync(path, "utf8");
|
|
1739
|
+
for (const name of ["context7", "codegraph"]) {
|
|
1740
|
+
const header = `[plugins."omo@sisyphuslabs".mcp_servers.${name}]`;
|
|
1741
|
+
const start = text.indexOf(header);
|
|
1742
|
+
if (start < 0) throw new Error(`missing ${header}`);
|
|
1743
|
+
const next = text.indexOf("\n[", start + header.length);
|
|
1744
|
+
const end = next < 0 ? text.length : next;
|
|
1745
|
+
const section = text.slice(start, end).replace(/enabled\s*=\s*true/, "enabled = false");
|
|
1746
|
+
text = text.slice(0, start) + section + text.slice(end);
|
|
1747
|
+
}
|
|
1748
|
+
text += `\n[plugins."omo@sisyphuslabs".mcp_servers.grep_app]\nenabled = false\n`;
|
|
1749
|
+
text += `\n[plugins."omo@sisyphuslabs".mcp_servers.lsp]\nenabled = true\n`;
|
|
1750
|
+
writeFileSync(path, text);
|
|
1751
|
+
NODE
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
stamp_codex_lsp_environment() {
|
|
1755
|
+
local manifest="$1"
|
|
1756
|
+
node --input-type=module - "$manifest" "$OMO_LSP_DAEMON_DIR" <<'NODE'
|
|
1757
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
1758
|
+
const [path, daemonDir] = process.argv.slice(2);
|
|
1759
|
+
const manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
1760
|
+
const lsp = manifest?.mcpServers?.lsp;
|
|
1761
|
+
if (!lsp || typeof lsp !== "object") throw new Error("installed plugin has no lsp MCP server");
|
|
1762
|
+
lsp.env = { ...(lsp.env || {}), OMO_LSP_DAEMON_DIR: daemonDir };
|
|
1763
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
|
|
1764
|
+
NODE
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
disable_codex_side_effect_hooks() {
|
|
1768
|
+
local manifest="$1"
|
|
1769
|
+
node --input-type=module - "$manifest" <<'NODE'
|
|
1770
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
1771
|
+
import { basename } from "node:path";
|
|
1772
|
+
|
|
1773
|
+
const path = process.argv[2];
|
|
1774
|
+
const disabled = new Set([
|
|
1775
|
+
"session-start-checking-auto-update.json",
|
|
1776
|
+
"session-start-checking-bootstrap-provisioning.json",
|
|
1777
|
+
"session-start-checking-codegraph-bootstrap.json",
|
|
1778
|
+
]);
|
|
1779
|
+
const manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
1780
|
+
if (!Array.isArray(manifest.hooks)) throw new Error("installed plugin hook list is missing");
|
|
1781
|
+
const retained = manifest.hooks.filter((entry) => typeof entry !== "string" || !disabled.has(basename(entry)));
|
|
1782
|
+
if (manifest.hooks.length - retained.length !== disabled.size) {
|
|
1783
|
+
throw new Error("installed plugin did not contain every isolated-QA side-effect hook");
|
|
1784
|
+
}
|
|
1785
|
+
manifest.hooks = retained;
|
|
1786
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
|
|
1787
|
+
NODE
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
start_mock_model() {
|
|
1791
|
+
local log_file="$1" port="" attempts=0
|
|
1792
|
+
MOCK_PORT=0 node "$SCRIPT_DIR/lib/mock-model.mjs" >"$log_file" 2>&1 &
|
|
1793
|
+
MOCK_PID=$!
|
|
1794
|
+
while [ "$attempts" -lt 100 ]; do
|
|
1795
|
+
port="$(awk '/^MOCK_LISTENING / { print $2; exit }' "$log_file" 2>/dev/null || true)"
|
|
1796
|
+
[ -n "$port" ] && break
|
|
1797
|
+
kill -0 "$MOCK_PID" 2>/dev/null || { fail "mock model exited during startup"; return 1; }
|
|
1798
|
+
sleep 0.1
|
|
1799
|
+
attempts=$((attempts + 1))
|
|
1800
|
+
done
|
|
1801
|
+
[ -n "$port" ] || { fail "mock model did not report a port"; return 1; }
|
|
1802
|
+
export MOCK_PORT="$port"
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
write_app_server_driver() {
|
|
1806
|
+
local path="$1"
|
|
1807
|
+
cat >"$path" <<'NODE'
|
|
1808
|
+
import { spawn } from "node:child_process";
|
|
1809
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
1810
|
+
|
|
1811
|
+
const codex = process.env.CODEX_BIN || "codex";
|
|
1812
|
+
const mockPort = process.env.MOCK_PORT;
|
|
1813
|
+
const cwd = process.env.QA_CWD;
|
|
1814
|
+
const output = process.env.APP_SERVER_RESULT;
|
|
1815
|
+
const pidFile = process.env.APP_SERVER_PID_FILE;
|
|
1816
|
+
const deadlineMs = Number(process.env.DEADLINE_MS || 120000);
|
|
1817
|
+
const qaScenario = process.env.QA_SCENARIO || "path-contract";
|
|
1818
|
+
const qaSourceFile = process.env.QA_SOURCE_FILE || "source.ts";
|
|
1819
|
+
const qaSourcePath = process.env.QA_SOURCE_PATH || "";
|
|
1820
|
+
const qaRenameEvents = process.env.QA_RENAME_EVENTS || "";
|
|
1821
|
+
if (!mockPort || !cwd || !output || !pidFile) throw new Error("missing app-server driver environment");
|
|
1822
|
+
|
|
1823
|
+
const overrides = [
|
|
1824
|
+
`model="mock-model"`,
|
|
1825
|
+
`model_provider="mock_provider"`,
|
|
1826
|
+
`model_providers.mock_provider.name="codex-qa mock"`,
|
|
1827
|
+
`model_providers.mock_provider.base_url="http://127.0.0.1:${mockPort}/v1"`,
|
|
1828
|
+
`model_providers.mock_provider.wire_api="responses"`,
|
|
1829
|
+
`model_providers.mock_provider.request_max_retries=0`,
|
|
1830
|
+
`model_providers.mock_provider.stream_max_retries=0`,
|
|
1831
|
+
`approval_policy="never"`,
|
|
1832
|
+
`sandbox_mode="read-only"`,
|
|
1833
|
+
];
|
|
1834
|
+
const args = overrides.flatMap((value) => ["-c", value]).concat("app-server");
|
|
1835
|
+
const child = spawn(codex, args, { stdio: ["pipe", "pipe", "pipe"], env: process.env });
|
|
1836
|
+
writeFileSync(pidFile, `${child.pid}\n`);
|
|
1837
|
+
|
|
1838
|
+
let stderr = "";
|
|
1839
|
+
let buffer = "";
|
|
1840
|
+
let threadId = null;
|
|
1841
|
+
let turnId = null;
|
|
1842
|
+
let turnStatus = null;
|
|
1843
|
+
let assistantText = null;
|
|
1844
|
+
let lspResponse = null;
|
|
1845
|
+
let diagnosticsResponse = null;
|
|
1846
|
+
let settled = false;
|
|
1847
|
+
const hooks = [];
|
|
1848
|
+
const send = (message) => child.stdin.write(JSON.stringify(message) + "\n");
|
|
1849
|
+
|
|
1850
|
+
function hookEvents(method) {
|
|
1851
|
+
return new Set(hooks.filter((entry) => entry.method === method && entry.status === (method === "hook/completed" ? "completed" : "running")).map((entry) => entry.eventName));
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function summarize(reason) {
|
|
1855
|
+
const started = hookEvents("hook/started");
|
|
1856
|
+
const completed = hookEvents("hook/completed");
|
|
1857
|
+
const expected = ["sessionStart", "userPromptSubmit"];
|
|
1858
|
+
const lspText = lspResponse?.result?.content?.map((entry) => entry?.text || "").join("\n") || "";
|
|
1859
|
+
const diagnosticsText = diagnosticsResponse?.result?.content?.map((entry) => entry?.text || "").join("\n") || "";
|
|
1860
|
+
const renameEvents = qaRenameEvents
|
|
1861
|
+
? readFileSync(qaRenameEvents, "utf8")
|
|
1862
|
+
.split("\n")
|
|
1863
|
+
.filter(Boolean)
|
|
1864
|
+
.map((line) => JSON.parse(line))
|
|
1865
|
+
: [];
|
|
1866
|
+
const applyResponses = renameEvents
|
|
1867
|
+
.filter((event) => event?.type === "clientResponse" && event?.method === "workspace/applyEdit")
|
|
1868
|
+
.map((event) => event?.result ?? null);
|
|
1869
|
+
const didChangeVersions = renameEvents
|
|
1870
|
+
.filter((event) => event?.type === "clientNotification" && event?.method === "textDocument/didChange")
|
|
1871
|
+
.map((event) => event?.params?.textDocument?.version)
|
|
1872
|
+
.filter((value) => typeof value === "number");
|
|
1873
|
+
const renameHarness = qaScenario === "rename"
|
|
1874
|
+
? {
|
|
1875
|
+
serverApplyRequestCount: renameEvents.filter(
|
|
1876
|
+
(event) => event?.type === "serverRequest" && event?.method === "workspace/applyEdit",
|
|
1877
|
+
).length,
|
|
1878
|
+
applyResponses,
|
|
1879
|
+
didChangeVersions,
|
|
1880
|
+
finalContent: qaSourcePath ? readFileSync(qaSourcePath, "utf8") : null,
|
|
1881
|
+
}
|
|
1882
|
+
: null;
|
|
1883
|
+
const ok = reason === "turn-completed"
|
|
1884
|
+
&& turnStatus === "completed"
|
|
1885
|
+
&& expected.every((event) => started.has(event) && completed.has(event))
|
|
1886
|
+
&& (
|
|
1887
|
+
qaScenario === "rename"
|
|
1888
|
+
? lspResponse?.error === undefined
|
|
1889
|
+
&& lspResponse?.result?.isError !== true
|
|
1890
|
+
&& lspText.includes("Applied 1 edit(s)")
|
|
1891
|
+
&& diagnosticsResponse?.error === undefined
|
|
1892
|
+
&& diagnosticsResponse?.result?.isError !== true
|
|
1893
|
+
&& diagnosticsText.includes("todo3-fresh")
|
|
1894
|
+
&& renameHarness?.serverApplyRequestCount === 1
|
|
1895
|
+
&& renameHarness?.applyResponses?.length === 1
|
|
1896
|
+
&& renameHarness?.applyResponses?.[0]?.applied === true
|
|
1897
|
+
&& renameHarness?.didChangeVersions?.join(",") === "2"
|
|
1898
|
+
&& renameHarness?.finalContent === "const after = 1;\n"
|
|
1899
|
+
: qaScenario === "diagnostics-freshness"
|
|
1900
|
+
? lspResponse?.error === undefined
|
|
1901
|
+
&& lspResponse?.result?.isError !== true
|
|
1902
|
+
&& lspText.includes("exact-current")
|
|
1903
|
+
: lspResponse?.error === undefined
|
|
1904
|
+
&& lspResponse?.result?.isError !== true
|
|
1905
|
+
&& lspText.includes("Configured LSP servers")
|
|
1906
|
+
);
|
|
1907
|
+
return {
|
|
1908
|
+
ok,
|
|
1909
|
+
reason,
|
|
1910
|
+
scenario: qaScenario,
|
|
1911
|
+
threadId,
|
|
1912
|
+
turnId,
|
|
1913
|
+
turnStatus,
|
|
1914
|
+
assistantText,
|
|
1915
|
+
expectedHooks: expected,
|
|
1916
|
+
hookStarted: expected.every((event) => started.has(event)),
|
|
1917
|
+
hookCompleted: expected.every((event) => completed.has(event)),
|
|
1918
|
+
hooks,
|
|
1919
|
+
lspOperation: qaScenario === "rename"
|
|
1920
|
+
? {
|
|
1921
|
+
server: "lsp",
|
|
1922
|
+
tool: "lsp_rename",
|
|
1923
|
+
ok: lspResponse?.error === undefined && lspResponse?.result?.isError !== true && lspText.includes("Applied 1 edit(s)"),
|
|
1924
|
+
text: lspText,
|
|
1925
|
+
details: lspResponse?.result?.details ?? null,
|
|
1926
|
+
isError: lspResponse?.result?.isError ?? null,
|
|
1927
|
+
protocolError: lspResponse?.error ?? null,
|
|
1928
|
+
}
|
|
1929
|
+
: qaScenario === "diagnostics-freshness"
|
|
1930
|
+
? {
|
|
1931
|
+
server: "lsp",
|
|
1932
|
+
tool: "lsp_diagnostics",
|
|
1933
|
+
ok: lspResponse?.error === undefined && lspResponse?.result?.isError !== true && lspText.includes("exact-current"),
|
|
1934
|
+
text: lspText,
|
|
1935
|
+
details: lspResponse?.result?.details ?? null,
|
|
1936
|
+
isError: lspResponse?.result?.isError ?? null,
|
|
1937
|
+
protocolError: lspResponse?.error ?? null,
|
|
1938
|
+
}
|
|
1939
|
+
: {
|
|
1940
|
+
server: "lsp",
|
|
1941
|
+
tool: "lsp_status",
|
|
1942
|
+
ok: lspResponse?.error === undefined && lspResponse?.result?.isError !== true && lspText.includes("Configured LSP servers"),
|
|
1943
|
+
text: lspText,
|
|
1944
|
+
details: lspResponse?.result?.details ?? null,
|
|
1945
|
+
isError: lspResponse?.result?.isError ?? null,
|
|
1946
|
+
protocolError: lspResponse?.error ?? null,
|
|
1947
|
+
},
|
|
1948
|
+
diagnosticsOperation: qaScenario === "rename"
|
|
1949
|
+
? {
|
|
1950
|
+
server: "lsp",
|
|
1951
|
+
tool: "lsp_diagnostics",
|
|
1952
|
+
ok: diagnosticsResponse?.error === undefined && diagnosticsResponse?.result?.isError !== true && diagnosticsText.includes("todo3-fresh"),
|
|
1953
|
+
text: diagnosticsText,
|
|
1954
|
+
details: diagnosticsResponse?.result?.details ?? null,
|
|
1955
|
+
isError: diagnosticsResponse?.result?.isError ?? null,
|
|
1956
|
+
protocolError: diagnosticsResponse?.error ?? null,
|
|
1957
|
+
}
|
|
1958
|
+
: null,
|
|
1959
|
+
renameHarness,
|
|
1960
|
+
stderrTail: stderr.split("\n").slice(-20).join("\n"),
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function finish(reason) {
|
|
1965
|
+
if (settled) return;
|
|
1966
|
+
settled = true;
|
|
1967
|
+
clearTimeout(deadline);
|
|
1968
|
+
const summary = summarize(reason);
|
|
1969
|
+
writeFileSync(output, JSON.stringify(summary, null, 2) + "\n");
|
|
1970
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
1971
|
+
setTimeout(() => {
|
|
1972
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
1973
|
+
process.exit(summary.ok ? 0 : 1);
|
|
1974
|
+
}, 250).unref();
|
|
1975
|
+
child.once("exit", () => process.exit(summary.ok ? 0 : 1));
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function handle(message) {
|
|
1979
|
+
if (message.id === 1 && message.result) {
|
|
1980
|
+
send({ method: "initialized" });
|
|
1981
|
+
send({ id: 2, method: "thread/start", params: { cwd } });
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (message.id === 2 && message.result) {
|
|
1985
|
+
threadId = message.result.thread?.id;
|
|
1986
|
+
if (qaScenario === "rename") {
|
|
1987
|
+
send({
|
|
1988
|
+
id: 4,
|
|
1989
|
+
method: "mcpServer/tool/call",
|
|
1990
|
+
params: {
|
|
1991
|
+
threadId,
|
|
1992
|
+
server: "lsp",
|
|
1993
|
+
tool: "lsp_rename",
|
|
1994
|
+
arguments: { filePath: qaSourceFile, line: 1, character: 6, newName: "after" },
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (qaScenario === "diagnostics-freshness") {
|
|
2000
|
+
send({
|
|
2001
|
+
id: 4,
|
|
2002
|
+
method: "mcpServer/tool/call",
|
|
2003
|
+
params: {
|
|
2004
|
+
threadId,
|
|
2005
|
+
server: "lsp",
|
|
2006
|
+
tool: "lsp_diagnostics",
|
|
2007
|
+
arguments: { filePath: qaSourceFile },
|
|
2008
|
+
},
|
|
2009
|
+
});
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
send({ id: 4, method: "mcpServer/tool/call", params: { threadId, server: "lsp", tool: "lsp_status", arguments: {} } });
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
if (message.id === 4) {
|
|
2016
|
+
lspResponse = message;
|
|
2017
|
+
if (qaScenario === "rename") {
|
|
2018
|
+
send({
|
|
2019
|
+
id: 5,
|
|
2020
|
+
method: "mcpServer/tool/call",
|
|
2021
|
+
params: {
|
|
2022
|
+
threadId,
|
|
2023
|
+
server: "lsp",
|
|
2024
|
+
tool: "lsp_diagnostics",
|
|
2025
|
+
arguments: { filePath: qaSourceFile },
|
|
2026
|
+
},
|
|
2027
|
+
});
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
send({ id: 3, method: "turn/start", params: { threadId, input: [{ type: "text", text: "say hello" }] } });
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (message.id === 5) {
|
|
2034
|
+
diagnosticsResponse = message;
|
|
2035
|
+
send({ id: 3, method: "turn/start", params: { threadId, input: [{ type: "text", text: "say hello" }] } });
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (message.id === 3 && message.result) {
|
|
2039
|
+
turnId = message.result.turn?.id;
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (message.method === "hook/started" || message.method === "hook/completed") {
|
|
2043
|
+
const run = message.params?.run || {};
|
|
2044
|
+
hooks.push({
|
|
2045
|
+
method: message.method,
|
|
2046
|
+
eventName: run.eventName,
|
|
2047
|
+
status: run.status,
|
|
2048
|
+
source: run.source ?? run.pluginId,
|
|
2049
|
+
pluginId: run.pluginId,
|
|
2050
|
+
hookName: run.hookName ?? run.name,
|
|
2051
|
+
});
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (message.method === "item/completed") {
|
|
2055
|
+
const item = message.params?.item;
|
|
2056
|
+
if (item?.type === "agentMessage" && typeof item.text === "string") assistantText = item.text;
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
if (message.method === "turn/completed") {
|
|
2060
|
+
turnStatus = message.params?.turn?.status;
|
|
2061
|
+
finish("turn-completed");
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
2066
|
+
child.stdout.on("data", (chunk) => {
|
|
2067
|
+
buffer += chunk;
|
|
2068
|
+
let newline;
|
|
2069
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
2070
|
+
const line = buffer.slice(0, newline).trim();
|
|
2071
|
+
buffer = buffer.slice(newline + 1);
|
|
2072
|
+
if (!line) continue;
|
|
2073
|
+
try { handle(JSON.parse(line)); } catch {}
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
child.on("exit", () => { if (!settled) finish("app-server-exited"); });
|
|
2077
|
+
const deadline = setTimeout(() => finish("deadline"), deadlineMs);
|
|
2078
|
+
send({ id: 1, method: "initialize", params: { clientInfo: { name: "omo-lsp-e2e", version: "1.0.0" }, capabilities: { experimentalApi: true, requestAttestation: false } } });
|
|
2079
|
+
NODE
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
record_daemon_state() {
|
|
2083
|
+
local pid_file endpoint_file version_dir pid command
|
|
2084
|
+
pid_file="$(find_daemon_pid_file)"
|
|
2085
|
+
[ -n "$pid_file" ] || { fail "actual LSP tool call did not create a daemon pid file"; return 1; }
|
|
2086
|
+
version_dir="$(dirname "$pid_file")"
|
|
2087
|
+
endpoint_file="$version_dir/daemon.endpoint"
|
|
2088
|
+
[ -f "$endpoint_file" ] || { fail "daemon endpoint file is missing"; return 1; }
|
|
2089
|
+
pid="$(tr -d '[:space:]' <"$pid_file")"
|
|
2090
|
+
command="$(process_command "$pid")"
|
|
2091
|
+
case "$command" in
|
|
2092
|
+
*"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
|
|
2093
|
+
*) fail "daemon process command does not match the installed CLI"; return 1 ;;
|
|
2094
|
+
esac
|
|
2095
|
+
node --input-type=module - "$EVIDENCE_DIR/daemon-state.json" "$OMO_TEST_ROOT" "$version_dir" "$pid" "$endpoint_file" <<'NODE'
|
|
2096
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2097
|
+
import { basename, dirname } from "node:path";
|
|
2098
|
+
const [output, base, versionDir, pid, endpointFile] = process.argv.slice(2);
|
|
2099
|
+
writeFileSync(output, JSON.stringify({
|
|
2100
|
+
base,
|
|
2101
|
+
versionDir,
|
|
2102
|
+
version: basename(versionDir).replace(/^v/, ""),
|
|
2103
|
+
pid: Number(pid),
|
|
2104
|
+
endpointKind: readFileSync(endpointFile, "utf8").startsWith("\\\\.\\pipe\\") ? "named-pipe" : "unix-socket",
|
|
2105
|
+
endpointInsideVersionDir: dirname(readFileSync(endpointFile, "utf8")) === versionDir,
|
|
2106
|
+
}, null, 2) + "\n");
|
|
2107
|
+
NODE
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
run_installed_component_probe() {
|
|
2111
|
+
local plugin_root="$1"
|
|
2112
|
+
local script="$SANDBOX_ROOT/installed-component-probe.mjs"
|
|
2113
|
+
cat >"$script" <<'NODE'
|
|
2114
|
+
import { builtinModules } from "node:module";
|
|
2115
|
+
import { createHash } from "node:crypto";
|
|
2116
|
+
import {
|
|
2117
|
+
cpSync,
|
|
2118
|
+
existsSync,
|
|
2119
|
+
mkdirSync,
|
|
2120
|
+
readdirSync,
|
|
2121
|
+
readFileSync,
|
|
2122
|
+
realpathSync,
|
|
2123
|
+
rmSync,
|
|
2124
|
+
writeFileSync,
|
|
2125
|
+
} from "node:fs";
|
|
2126
|
+
import { dirname, join, resolve } from "node:path";
|
|
2127
|
+
import { pathToFileURL } from "node:url";
|
|
2128
|
+
import { spawnSync } from "node:child_process";
|
|
2129
|
+
|
|
2130
|
+
const repoRoot = process.env.REPO_ROOT;
|
|
2131
|
+
const pluginRoot = process.env.INSTALLED_PLUGIN_ROOT;
|
|
2132
|
+
const projectDir = process.env.QA_CWD;
|
|
2133
|
+
const codexHome = process.env.CODEX_HOME;
|
|
2134
|
+
const sandboxRoot = process.env.SANDBOX_ROOT;
|
|
2135
|
+
const daemonDir = process.env.OMO_LSP_DAEMON_DIR;
|
|
2136
|
+
const output = process.env.INSTALLED_COMPONENT_RESULT;
|
|
2137
|
+
if (!repoRoot || !pluginRoot || !projectDir || !codexHome || !sandboxRoot || !daemonDir || !output) {
|
|
2138
|
+
throw new Error("missing installed-component probe environment");
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
const componentRoot = join(pluginRoot, "components", "lsp");
|
|
2142
|
+
const cliPath = join(componentRoot, "dist", "cli.js");
|
|
2143
|
+
const manifestPath = join(componentRoot, "dist", ".omo-runtime-manifest.json");
|
|
2144
|
+
const daemonCliPath = join(pluginRoot, "components", "lsp-daemon", "dist", "cli.js");
|
|
2145
|
+
const daemonVersion = JSON.parse(readFileSync(join(dirname(daemonCliPath), "package.json"), "utf8")).version;
|
|
2146
|
+
const pluginData = join(sandboxRoot, "installed-component-plugin-data");
|
|
2147
|
+
const scenarioPath = join(sandboxRoot, "installed-component-scenario.json");
|
|
2148
|
+
const eventsPath = join(sandboxRoot, "installed-component-server-events.jsonl");
|
|
2149
|
+
const sourcePath = join(projectDir, "source.ts");
|
|
2150
|
+
const markdownPath = join(projectDir, "README.md");
|
|
2151
|
+
const transcriptPath = join(sandboxRoot, "context-pressure-transcript.txt");
|
|
2152
|
+
const projectConfigPath = join(realpathSync(projectDir), ".codex", "lsp-client.json");
|
|
2153
|
+
const userConfigPath = join(resolve(codexHome), "lsp-client.json");
|
|
2154
|
+
const installDecisionsPath = join(resolve(codexHome), "lsp-install-decisions.json");
|
|
2155
|
+
const fixturePath = join(repoRoot, "packages", "lsp-core", "src", "lsp", "fixtures", "workspace-edit-server.mjs");
|
|
2156
|
+
const longMessage = `installed-post-edit ${"x".repeat(12000)}`;
|
|
2157
|
+
|
|
2158
|
+
mkdirSync(dirname(projectConfigPath), { recursive: true });
|
|
2159
|
+
mkdirSync(resolve(codexHome), { recursive: true });
|
|
2160
|
+
mkdirSync(pluginData, { recursive: true });
|
|
2161
|
+
writeFileSync(sourcePath, "const value = missingSymbol;\n", "utf8");
|
|
2162
|
+
writeFileSync(markdownPath, "# note\n", "utf8");
|
|
2163
|
+
writeFileSync(eventsPath, "", "utf8");
|
|
2164
|
+
writeFileSync(transcriptPath, "Codex ran out of room in the model's context window\n", "utf8");
|
|
2165
|
+
writeFileSync(
|
|
2166
|
+
scenarioPath,
|
|
2167
|
+
JSON.stringify(
|
|
2168
|
+
{
|
|
2169
|
+
publishDiagnostics: [
|
|
2170
|
+
{ trigger: "didOpen", version: 1, diagnostics: [diagnostic(longMessage)] },
|
|
2171
|
+
],
|
|
2172
|
+
diagnosticResponses: [
|
|
2173
|
+
{ report: { items: [diagnostic(longMessage)] } },
|
|
2174
|
+
{ report: { items: [diagnostic(longMessage)] } },
|
|
2175
|
+
],
|
|
2176
|
+
},
|
|
2177
|
+
null,
|
|
2178
|
+
2,
|
|
2179
|
+
) + "\n",
|
|
2180
|
+
);
|
|
2181
|
+
writeFileSync(
|
|
2182
|
+
projectConfigPath,
|
|
2183
|
+
JSON.stringify({ lsp: {} }, null, 2) + "\n",
|
|
2184
|
+
);
|
|
2185
|
+
writeFileSync(
|
|
2186
|
+
userConfigPath,
|
|
2187
|
+
JSON.stringify(
|
|
2188
|
+
{
|
|
2189
|
+
lsp: {
|
|
2190
|
+
typescript: {
|
|
2191
|
+
command: [process.execPath, fixturePath, scenarioPath, eventsPath],
|
|
2192
|
+
extensions: [".ts"],
|
|
2193
|
+
priority: 100,
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2196
|
+
},
|
|
2197
|
+
null,
|
|
2198
|
+
2,
|
|
2199
|
+
) + "\n",
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
const manifest = validateManifest();
|
|
2203
|
+
const staticImports = scanRuntimeImports();
|
|
2204
|
+
const distOnlyValidation = validateDistOnlyRuntime();
|
|
2205
|
+
const postEdit = runPostEditProbe();
|
|
2206
|
+
const mcpDiagnostics = runMcpDiagnosticsProbe();
|
|
2207
|
+
const compaction = runCompactionProbe();
|
|
2208
|
+
const validation = runOverrideValidation();
|
|
2209
|
+
const daemonOwner = inspectDaemonOwner();
|
|
2210
|
+
const daemonArtifacts = readDaemonArtifacts();
|
|
2211
|
+
const context = {
|
|
2212
|
+
cwd: realpathSync(projectDir),
|
|
2213
|
+
projectConfigPaths: [projectConfigPath],
|
|
2214
|
+
userConfigPath,
|
|
2215
|
+
installDecisionsPath,
|
|
2216
|
+
capabilities: { installDecisionTool: true },
|
|
2217
|
+
};
|
|
2218
|
+
const result = {
|
|
2219
|
+
result: "PASS",
|
|
2220
|
+
manifest,
|
|
2221
|
+
distOnlyValidation,
|
|
2222
|
+
componentCli: {
|
|
2223
|
+
path: cliPath,
|
|
2224
|
+
exists: existsSync(cliPath),
|
|
2225
|
+
noNonBuiltinRuntimeImports: staticImports.nonBuiltin.length === 0,
|
|
2226
|
+
nonBuiltinRuntimeImports: staticImports.nonBuiltin,
|
|
2227
|
+
},
|
|
2228
|
+
consumer: {
|
|
2229
|
+
emptyNodePath: postEdit.emptyNodePath === true && compaction.emptyNodePath === true,
|
|
2230
|
+
repositoryHiddenByInstall: staticImports.nonBuiltin.length === 0,
|
|
2231
|
+
},
|
|
2232
|
+
codexContext: context,
|
|
2233
|
+
postEdit,
|
|
2234
|
+
mcpDiagnostics,
|
|
2235
|
+
compaction,
|
|
2236
|
+
validation,
|
|
2237
|
+
daemonOwner,
|
|
2238
|
+
daemonArtifacts,
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
const required = [
|
|
2242
|
+
result.manifest.valid === true,
|
|
2243
|
+
result.distOnlyValidation.ok === true,
|
|
2244
|
+
result.componentCli.exists === true,
|
|
2245
|
+
result.componentCli.noNonBuiltinRuntimeImports === true,
|
|
2246
|
+
result.consumer.emptyNodePath === true,
|
|
2247
|
+
result.consumer.repositoryHiddenByInstall === true,
|
|
2248
|
+
result.postEdit.actualPostEditOperation === true,
|
|
2249
|
+
result.postEdit.projectConfigConsumed === true,
|
|
2250
|
+
result.postEdit.defaultBudgetWithinLimit === true,
|
|
2251
|
+
result.postEdit.contextPressureBudgetWithinLimit === true,
|
|
2252
|
+
result.compaction.cacheStored === true,
|
|
2253
|
+
result.compaction.compactionReset === true,
|
|
2254
|
+
result.validation.singletonRejected === true,
|
|
2255
|
+
result.validation.nonexistentPairRejected === true,
|
|
2256
|
+
result.daemonOwner.count === 1,
|
|
2257
|
+
result.daemonOwner.commandIncludesInstalledCli === true,
|
|
2258
|
+
];
|
|
2259
|
+
if (!required.every(Boolean)) {
|
|
2260
|
+
writeFileSync(output, JSON.stringify({ ...result, result: "FAIL" }, null, 2) + "\n");
|
|
2261
|
+
throw new Error("installed-component probe failed required assertions");
|
|
2262
|
+
}
|
|
2263
|
+
writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
|
|
2264
|
+
|
|
2265
|
+
function diagnostic(message) {
|
|
2266
|
+
return {
|
|
2267
|
+
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 18 } },
|
|
2268
|
+
message,
|
|
2269
|
+
code: 2304,
|
|
2270
|
+
severity: 1,
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function validateManifest() {
|
|
2275
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
2276
|
+
const outputs = Array.isArray(parsed.outputs) ? parsed.outputs : [];
|
|
2277
|
+
const sorted = outputs.map((entry) => entry.path).join("\n") === outputs.map((entry) => entry.path).sort().join("\n");
|
|
2278
|
+
const hashesMatch = outputs.every((entry) => {
|
|
2279
|
+
const filePath = join(componentRoot, "dist", entry.path);
|
|
2280
|
+
return existsSync(filePath) && sha256(readFileSync(filePath)) === entry.sha256;
|
|
2281
|
+
});
|
|
2282
|
+
return {
|
|
2283
|
+
valid: parsed.schemaVersion === 1 && /^sha256:[a-f0-9]{64}$/u.test(parsed.inputDigest) && sorted && hashesMatch,
|
|
2284
|
+
schemaVersion: parsed.schemaVersion,
|
|
2285
|
+
inputDigest: parsed.inputDigest,
|
|
2286
|
+
outputCount: outputs.length,
|
|
2287
|
+
sortedOutputs: sorted,
|
|
2288
|
+
outputHashesMatch: hashesMatch,
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function validateDistOnlyRuntime() {
|
|
2293
|
+
const distOnlyRoot = join(sandboxRoot, "dist-only-lsp-component");
|
|
2294
|
+
rmSync(distOnlyRoot, { recursive: true, force: true });
|
|
2295
|
+
mkdirSync(join(distOnlyRoot, "scripts"), { recursive: true });
|
|
2296
|
+
cpSync(join(componentRoot, "dist"), join(distOnlyRoot, "dist"), { recursive: true });
|
|
2297
|
+
cpSync(join(componentRoot, "scripts", "build-runtime.mjs"), join(distOnlyRoot, "scripts", "build-runtime.mjs"));
|
|
2298
|
+
cpSync(join(componentRoot, "package.json"), join(distOnlyRoot, "package.json"));
|
|
2299
|
+
const run = spawnSync(process.execPath, ["scripts/build-runtime.mjs"], {
|
|
2300
|
+
cwd: distOnlyRoot,
|
|
2301
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
2302
|
+
encoding: "utf8",
|
|
2303
|
+
timeout: 15000,
|
|
2304
|
+
});
|
|
2305
|
+
return { ok: run.status === 0, status: run.status, stderr: tail(run.stderr) };
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function scanRuntimeImports() {
|
|
2309
|
+
const source = readFileSync(cliPath, "utf8");
|
|
2310
|
+
const builtin = new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
|
|
2311
|
+
const imports = [
|
|
2312
|
+
...source.matchAll(/\bfrom\s+["']([^"']+)["']/gu),
|
|
2313
|
+
...source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/gu),
|
|
2314
|
+
].map((match) => match[1]);
|
|
2315
|
+
return { imports, nonBuiltin: imports.filter((specifier) => !builtin.has(specifier)) };
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function runPostEditProbe() {
|
|
2319
|
+
const baseInput = {
|
|
2320
|
+
cwd: projectDir,
|
|
2321
|
+
hook_event_name: "PostToolUse",
|
|
2322
|
+
model: "gpt-5.5",
|
|
2323
|
+
permission_mode: "default",
|
|
2324
|
+
session_id: "installed-post-edit",
|
|
2325
|
+
tool_input: { path: "source.ts" },
|
|
2326
|
+
tool_name: "write",
|
|
2327
|
+
tool_response: { ok: true },
|
|
2328
|
+
tool_use_id: "tool-use-1",
|
|
2329
|
+
turn_id: "turn-1",
|
|
2330
|
+
};
|
|
2331
|
+
const first = runHook({ ...baseInput, transcript_path: null });
|
|
2332
|
+
const second = runHook({ ...baseInput, transcript_path: transcriptPath });
|
|
2333
|
+
const firstParsed = JSON.parse(first.stdout || "{}");
|
|
2334
|
+
const secondParsed = JSON.parse(second.stdout || "{}");
|
|
2335
|
+
const events = readEvents();
|
|
2336
|
+
return {
|
|
2337
|
+
emptyNodePath: first.emptyNodePath && second.emptyNodePath,
|
|
2338
|
+
actualPostEditOperation: events.some(
|
|
2339
|
+
(event) =>
|
|
2340
|
+
(event.type === "clientRequest" && event.method === "textDocument/diagnostic") ||
|
|
2341
|
+
(event.type === "clientNotification" && event.method === "textDocument/didOpen"),
|
|
2342
|
+
),
|
|
2343
|
+
projectConfigConsumed: events.some(
|
|
2344
|
+
(event) => event.type === "clientRequest" && event.method === "initialize" && event.params?.rootUri === pathToFileURL(realpathSync(projectDir)).href,
|
|
2345
|
+
),
|
|
2346
|
+
defaultReasonLength: firstParsed.reason?.length ?? null,
|
|
2347
|
+
contextPressureReasonLength: secondParsed.reason?.length ?? null,
|
|
2348
|
+
defaultBudgetWithinLimit: typeof firstParsed.reason === "string" && firstParsed.reason.length <= 8000 && firstParsed.reason.includes("Truncated hook output to 8000 chars"),
|
|
2349
|
+
contextPressureBudgetWithinLimit: typeof secondParsed.reason === "string" && secondParsed.reason.length <= 1200 && secondParsed.reason.includes("Truncated hook output to 1200 chars"),
|
|
2350
|
+
stdoutContainsDiagnostic: first.stdout.includes("installed-post-edit"),
|
|
2351
|
+
statusCodes: [first.status, second.status],
|
|
2352
|
+
stderrTails: [tail(first.stderr), tail(second.stderr)],
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function runMcpDiagnosticsProbe() {
|
|
2357
|
+
const request = [
|
|
2358
|
+
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05" } },
|
|
2359
|
+
{ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "lsp_diagnostics", arguments: { filePath: "source.ts" } } },
|
|
2360
|
+
].map((message) => JSON.stringify(message)).join("\n") + "\n";
|
|
2361
|
+
const run = spawnSync(process.execPath, [cliPath, "mcp"], {
|
|
2362
|
+
cwd: projectDir,
|
|
2363
|
+
input: request,
|
|
2364
|
+
env: cleanEnv(),
|
|
2365
|
+
encoding: "utf8",
|
|
2366
|
+
timeout: 60000,
|
|
2367
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
2368
|
+
});
|
|
2369
|
+
const responses = run.stdout.split("\n").filter(Boolean).map((line) => {
|
|
2370
|
+
try {
|
|
2371
|
+
return JSON.parse(line);
|
|
2372
|
+
} catch {
|
|
2373
|
+
return { parseError: line };
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
const diagnosticResponse = responses.find((entry) => entry?.id === 2);
|
|
2377
|
+
const text = diagnosticResponse?.result?.content?.map((entry) => entry?.text ?? "").join("\n") ?? "";
|
|
2378
|
+
return {
|
|
2379
|
+
status: run.status,
|
|
2380
|
+
ok: run.status === 0 && diagnosticResponse?.result?.isError !== true && text.includes("installed-post-edit"),
|
|
2381
|
+
textTail: tail(text),
|
|
2382
|
+
stderrTail: tail(run.stderr),
|
|
2383
|
+
responseCount: responses.length,
|
|
2384
|
+
diagnosticResponse,
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function runCompactionProbe() {
|
|
2389
|
+
const sessionId = "installed-compact-session";
|
|
2390
|
+
const input = {
|
|
2391
|
+
cwd: projectDir,
|
|
2392
|
+
hook_event_name: "PostToolUse",
|
|
2393
|
+
model: "gpt-5.5",
|
|
2394
|
+
permission_mode: "default",
|
|
2395
|
+
session_id: sessionId,
|
|
2396
|
+
tool_input: { path: "README.md" },
|
|
2397
|
+
tool_name: "write",
|
|
2398
|
+
tool_response: { ok: true },
|
|
2399
|
+
tool_use_id: "tool-use-2",
|
|
2400
|
+
transcript_path: null,
|
|
2401
|
+
turn_id: "turn-2",
|
|
2402
|
+
};
|
|
2403
|
+
const first = runHook(input);
|
|
2404
|
+
const statePath = join(pluginData, "sessions", `${sessionId}.json`);
|
|
2405
|
+
const before = JSON.parse(readFileSync(statePath, "utf8"));
|
|
2406
|
+
const compact = spawnSync(process.execPath, [cliPath, "hook", "post-compact"], {
|
|
2407
|
+
cwd: projectDir,
|
|
2408
|
+
input: JSON.stringify({ session_id: sessionId }) + "\n",
|
|
2409
|
+
env: cleanEnv(),
|
|
2410
|
+
encoding: "utf8",
|
|
2411
|
+
timeout: 30000,
|
|
2412
|
+
});
|
|
2413
|
+
const after = JSON.parse(readFileSync(statePath, "utf8"));
|
|
2414
|
+
return {
|
|
2415
|
+
emptyNodePath: true,
|
|
2416
|
+
firstStatus: first.status,
|
|
2417
|
+
compactStatus: compact.status,
|
|
2418
|
+
cacheStored: Array.isArray(before.notConfiguredExtensions) && before.notConfiguredExtensions.includes(".md"),
|
|
2419
|
+
compactionReset: Array.isArray(after.notConfiguredExtensions) && after.notConfiguredExtensions.length === 0,
|
|
2420
|
+
stdoutTails: [tail(first.stdout), tail(compact.stdout)],
|
|
2421
|
+
stderrTails: [tail(first.stderr), tail(compact.stderr)],
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
function runOverrideValidation() {
|
|
2426
|
+
const singleton = spawnSync(process.execPath, [cliPath, "mcp"], {
|
|
2427
|
+
cwd: projectDir,
|
|
2428
|
+
input: "",
|
|
2429
|
+
env: cleanEnv({ OMO_LSP_DAEMON_CLI: daemonCliPath }, false),
|
|
2430
|
+
encoding: "utf8",
|
|
2431
|
+
timeout: 5000,
|
|
2432
|
+
});
|
|
2433
|
+
const nonexistent = spawnSync(process.execPath, [cliPath, "mcp"], {
|
|
2434
|
+
cwd: projectDir,
|
|
2435
|
+
input: "",
|
|
2436
|
+
env: cleanEnv({
|
|
2437
|
+
OMO_LSP_DAEMON_CLI: join(sandboxRoot, "missing-daemon-cli.js"),
|
|
2438
|
+
OMO_LSP_DAEMON_VERSION: "999.0.0",
|
|
2439
|
+
}, false),
|
|
2440
|
+
encoding: "utf8",
|
|
2441
|
+
timeout: 5000,
|
|
2442
|
+
});
|
|
2443
|
+
return {
|
|
2444
|
+
singletonRejected: singleton.status !== 0 && singleton.stderr.includes("must be set together"),
|
|
2445
|
+
nonexistentPairRejected: nonexistent.status !== 0 && singleton.status !== 0 && nonexistent.stderr.includes("points to a missing file"),
|
|
2446
|
+
singletonStatus: singleton.status,
|
|
2447
|
+
nonexistentStatus: nonexistent.status,
|
|
2448
|
+
singletonStderr: tail(singleton.stderr),
|
|
2449
|
+
nonexistentStderr: tail(nonexistent.stderr),
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function inspectDaemonOwner() {
|
|
2454
|
+
const pidFiles = [];
|
|
2455
|
+
collectPidFiles(daemonDir, pidFiles, readdirSync);
|
|
2456
|
+
const command = pidFiles.length === 1 ? psCommand(readFileSync(pidFiles[0], "utf8").trim()) : "";
|
|
2457
|
+
return {
|
|
2458
|
+
count: pidFiles.length,
|
|
2459
|
+
pidFiles: pidFiles.map((path) => path.slice(daemonDir.length + 1)),
|
|
2460
|
+
commandIncludesInstalledCli: command.includes(daemonCliPath) && command.includes(" daemon"),
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
function readDaemonArtifacts() {
|
|
2465
|
+
const files = [];
|
|
2466
|
+
collectDaemonFiles(daemonDir, files);
|
|
2467
|
+
const artifacts = {};
|
|
2468
|
+
for (const file of files) {
|
|
2469
|
+
const relative = file.slice(daemonDir.length + 1);
|
|
2470
|
+
artifacts[relative] = file.endsWith("/daemon.auth") ? "<redacted>" : tail(readFileSync(file, "utf8"));
|
|
2471
|
+
}
|
|
2472
|
+
return artifacts;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function collectDaemonFiles(dir, output) {
|
|
2476
|
+
if (!existsSync(dir)) return;
|
|
2477
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2478
|
+
const child = join(dir, entry.name);
|
|
2479
|
+
if (entry.isDirectory()) collectDaemonFiles(child, output);
|
|
2480
|
+
else if (entry.isFile() && /^daemon\.(?:log|pid|endpoint|owner|auth)$/u.test(entry.name)) output.push(child);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function collectPidFiles(dir, output, readdirSync) {
|
|
2485
|
+
if (!existsSync(dir)) return;
|
|
2486
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2487
|
+
const child = join(dir, entry.name);
|
|
2488
|
+
if (entry.isDirectory()) collectPidFiles(child, output, readdirSync);
|
|
2489
|
+
else if (entry.isFile() && entry.name === "daemon.pid") output.push(child);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function psCommand(pid) {
|
|
2494
|
+
const run = spawnSync("/bin/ps", ["-p", pid, "-o", "command="], { encoding: "utf8", timeout: 5000 });
|
|
2495
|
+
return run.stdout.trim();
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function runHook(input) {
|
|
2499
|
+
const run = spawnSync(process.execPath, [cliPath, "hook", "post-tool-use"], {
|
|
2500
|
+
cwd: projectDir,
|
|
2501
|
+
input: JSON.stringify(input) + "\n",
|
|
2502
|
+
env: cleanEnv(),
|
|
2503
|
+
encoding: "utf8",
|
|
2504
|
+
timeout: 60000,
|
|
2505
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
2506
|
+
});
|
|
2507
|
+
return { ...run, emptyNodePath: true };
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function cleanEnv(extra = {}, includeRuntimePair = true) {
|
|
2511
|
+
const env = {
|
|
2512
|
+
PATH: process.env.PATH ?? "",
|
|
2513
|
+
HOME: process.env.HOME ?? "",
|
|
2514
|
+
CODEX_HOME: resolve(codexHome),
|
|
2515
|
+
PLUGIN_DATA: pluginData,
|
|
2516
|
+
OMO_LSP_DAEMON_DIR: daemonDir,
|
|
2517
|
+
OMO_DISABLE_POSTHOG: "1",
|
|
2518
|
+
OMO_CODEX_DISABLE_POSTHOG: "1",
|
|
2519
|
+
NODE_PATH: "",
|
|
2520
|
+
};
|
|
2521
|
+
for (const key of ["TMPDIR", "TMP", "TEMP"]) {
|
|
2522
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
2523
|
+
}
|
|
2524
|
+
if (includeRuntimePair) {
|
|
2525
|
+
env.OMO_LSP_DAEMON_CLI = daemonCliPath;
|
|
2526
|
+
env.OMO_LSP_DAEMON_VERSION = daemonVersion;
|
|
2527
|
+
}
|
|
2528
|
+
Object.assign(env, extra);
|
|
2529
|
+
for (const key of Object.keys(env)) {
|
|
2530
|
+
if (key.startsWith("CODEX" + "_LSP_")) delete env[key];
|
|
2531
|
+
}
|
|
2532
|
+
return env;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function readEvents() {
|
|
2536
|
+
return readFileSync(eventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function sha256(buffer) {
|
|
2540
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
function tail(text) {
|
|
2544
|
+
return String(text ?? "").split("\n").slice(-20).join("\n");
|
|
2545
|
+
}
|
|
2546
|
+
NODE
|
|
2547
|
+
REPO_ROOT="$REPO_ROOT" INSTALLED_PLUGIN_ROOT="$plugin_root" SANDBOX_ROOT="$SANDBOX_ROOT" \
|
|
2548
|
+
INSTALLED_COMPONENT_RESULT="$EVIDENCE_DIR/installed-component.json" \
|
|
2549
|
+
run_bounded 180 "$EVIDENCE_DIR/installed-component.log" node "$script"
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
write_final_result() {
|
|
2553
|
+
local real_omo_before="$1" real_omo_after="$2" real_codex_before="$3" real_codex_after="$4"
|
|
2554
|
+
local worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
|
|
2555
|
+
RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
|
|
2556
|
+
node --input-type=module - \
|
|
2557
|
+
"$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
|
|
2558
|
+
"$worktree_before" "$worktree_after" "$sandbox_removed" \
|
|
2559
|
+
"$EVIDENCE_DIR/path-contract.json" "$EVIDENCE_DIR/app-server.json" "$EVIDENCE_DIR/daemon-state.json" \
|
|
2560
|
+
"$EVIDENCE_DIR/workspace-edit-contract.json" "$EVIDENCE_DIR/rename-fixture.json" "$EVIDENCE_DIR/rename-server-events.jsonl" \
|
|
2561
|
+
"$EVIDENCE_DIR/diagnostics-freshness-contract.json" "$EVIDENCE_DIR/diagnostics-freshness-fixture.json" \
|
|
2562
|
+
"$EVIDENCE_DIR/legacy-cleanup-contract.json" "$EVIDENCE_DIR/legacy-cleanup-live.json" "$EVIDENCE_DIR/legacy-cleanup-process-cleanup.txt" \
|
|
2563
|
+
"$EVIDENCE_DIR/post-edit-contract.json" "$EVIDENCE_DIR/cancellation-contract.json" "$EVIDENCE_DIR/package-smoke.json" \
|
|
2564
|
+
"$EVIDENCE_DIR/installed-component.json" <<'NODE'
|
|
2565
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2566
|
+
const [
|
|
2567
|
+
output,
|
|
2568
|
+
scenario,
|
|
2569
|
+
omoBefore,
|
|
2570
|
+
omoAfter,
|
|
2571
|
+
codexBefore,
|
|
2572
|
+
codexAfter,
|
|
2573
|
+
worktreeBefore,
|
|
2574
|
+
worktreeAfter,
|
|
2575
|
+
sandboxRemoved,
|
|
2576
|
+
contractPath,
|
|
2577
|
+
appPath,
|
|
2578
|
+
daemonPath,
|
|
2579
|
+
workspaceContractPath,
|
|
2580
|
+
renameFixturePath,
|
|
2581
|
+
renameEventsPath,
|
|
2582
|
+
freshnessContractPath,
|
|
2583
|
+
freshnessFixturePath,
|
|
2584
|
+
legacyContractPath,
|
|
2585
|
+
legacyLivePath,
|
|
2586
|
+
legacyProcessCleanupPath,
|
|
2587
|
+
postEditContractPath,
|
|
2588
|
+
cancellationContractPath,
|
|
2589
|
+
clientPackagePath,
|
|
2590
|
+
installedComponentPath,
|
|
2591
|
+
] = process.argv.slice(2);
|
|
2592
|
+
const contract = JSON.parse(readFileSync(contractPath, "utf8"));
|
|
2593
|
+
const app = JSON.parse(readFileSync(appPath, "utf8"));
|
|
2594
|
+
const daemon = JSON.parse(readFileSync(daemonPath, "utf8"));
|
|
2595
|
+
const workspaceContract = scenario === "rename" ? JSON.parse(readFileSync(workspaceContractPath, "utf8")) : null;
|
|
2596
|
+
const renameFixture = scenario === "rename" ? JSON.parse(readFileSync(renameFixturePath, "utf8")) : null;
|
|
2597
|
+
const renameEvents = scenario === "rename"
|
|
2598
|
+
? readFileSync(renameEventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line))
|
|
2599
|
+
: [];
|
|
2600
|
+
const freshnessContract = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessContractPath, "utf8")) : null;
|
|
2601
|
+
const freshnessFixture = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessFixturePath, "utf8")) : null;
|
|
2602
|
+
const legacyContract = scenario === "legacy-cleanup" ? JSON.parse(readFileSync(legacyContractPath, "utf8")) : null;
|
|
2603
|
+
const legacyLive = scenario === "legacy-cleanup" ? JSON.parse(readFileSync(legacyLivePath, "utf8")) : null;
|
|
2604
|
+
const legacyProcessCleanup = scenario === "legacy-cleanup" ? readFileSync(legacyProcessCleanupPath, "utf8") : null;
|
|
2605
|
+
const postEditContract = scenario === "post-edit" ? JSON.parse(readFileSync(postEditContractPath, "utf8")) : null;
|
|
2606
|
+
const cancellationContract = scenario === "cancellation" ? JSON.parse(readFileSync(cancellationContractPath, "utf8")) : null;
|
|
2607
|
+
const clientPackage = scenario === "client-package" ? JSON.parse(readFileSync(clientPackagePath, "utf8")) : null;
|
|
2608
|
+
const installedComponent = scenario === "installed-component" ? JSON.parse(readFileSync(installedComponentPath, "utf8")) : null;
|
|
2609
|
+
const result = {
|
|
2610
|
+
result: "PASS",
|
|
2611
|
+
scenario,
|
|
2612
|
+
harness: "codex",
|
|
2613
|
+
realOmoRootUnchanged: omoBefore === omoAfter,
|
|
2614
|
+
realCodexConfigUnchanged: codexBefore === codexAfter,
|
|
2615
|
+
dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
|
|
2616
|
+
isolatedCodexHome: true,
|
|
2617
|
+
localMockModel: true,
|
|
2618
|
+
pluginOnly: true,
|
|
2619
|
+
disabledSideEffectHooks: [
|
|
2620
|
+
"session-start-checking-auto-update.json",
|
|
2621
|
+
"session-start-checking-bootstrap-provisioning.json",
|
|
2622
|
+
"session-start-checking-codegraph-bootstrap.json",
|
|
2623
|
+
],
|
|
2624
|
+
hookStarted: app.hookStarted === true,
|
|
2625
|
+
hookCompleted: app.hookCompleted === true,
|
|
2626
|
+
lspOperation: app.lspOperation,
|
|
2627
|
+
...(scenario === "rename"
|
|
2628
|
+
? {
|
|
2629
|
+
diagnosticsOperation: app.diagnosticsOperation,
|
|
2630
|
+
serverAppliedRename: app.renameHarness?.serverApplyRequestCount === 1 && app.renameHarness?.applyResponses?.[0]?.applied === true,
|
|
2631
|
+
recordedResultReused: workspaceContract?.success?.recordedResultReused === true,
|
|
2632
|
+
synchronizedDocumentVersion: workspaceContract?.success?.synchronizedDocumentVersion ?? null,
|
|
2633
|
+
didChangeVersions: app.renameHarness?.didChangeVersions ?? [],
|
|
2634
|
+
immediateDiagnostics: workspaceContract?.success?.immediateDiagnostics === true && app.diagnosticsOperation?.ok === true,
|
|
2635
|
+
finalContent: app.renameHarness?.finalContent ?? null,
|
|
2636
|
+
failureHashes: workspaceContract?.failureHashes ?? {},
|
|
2637
|
+
failureEvidence: workspaceContract?.failureCases ?? {},
|
|
2638
|
+
renameFixture,
|
|
2639
|
+
renameEventCount: renameEvents.length,
|
|
2640
|
+
}
|
|
2641
|
+
: scenario === "diagnostics-freshness"
|
|
2642
|
+
? {
|
|
2643
|
+
diagnosticsFreshnessProbe: freshnessContract,
|
|
2644
|
+
diagnosticsFreshnessFixture: freshnessFixture,
|
|
2645
|
+
}
|
|
2646
|
+
: scenario === "legacy-cleanup"
|
|
2647
|
+
? {
|
|
2648
|
+
legacyCleanup: {
|
|
2649
|
+
exactVectors: legacyLive?.vectors ?? null,
|
|
2650
|
+
contract: legacyContract,
|
|
2651
|
+
live: legacyLive,
|
|
2652
|
+
cleanupReceipt: legacyProcessCleanup,
|
|
2653
|
+
},
|
|
2654
|
+
}
|
|
2655
|
+
: scenario === "post-edit"
|
|
2656
|
+
? {
|
|
2657
|
+
postEditContract,
|
|
2658
|
+
}
|
|
2659
|
+
: scenario === "cancellation"
|
|
2660
|
+
? {
|
|
2661
|
+
cancellationContract,
|
|
2662
|
+
}
|
|
2663
|
+
: scenario === "client-package"
|
|
2664
|
+
? {
|
|
2665
|
+
clientPackage,
|
|
2666
|
+
}
|
|
2667
|
+
: scenario === "installed-component"
|
|
2668
|
+
? {
|
|
2669
|
+
installedComponent,
|
|
2670
|
+
}
|
|
2671
|
+
: {}),
|
|
2672
|
+
resolvedBase: daemon.base,
|
|
2673
|
+
resolvedVersion: daemon.version,
|
|
2674
|
+
resolvedVersionDir: daemon.versionDir,
|
|
2675
|
+
overrideAssertions: contract.assertions,
|
|
2676
|
+
failureFixtures: contract.failures,
|
|
2677
|
+
realOmoRootHashBefore: omoBefore,
|
|
2678
|
+
realOmoRootHashAfter: omoAfter,
|
|
2679
|
+
cleanup: {
|
|
2680
|
+
daemonStopped: true,
|
|
2681
|
+
appServerStopped: true,
|
|
2682
|
+
mockModelStopped: true,
|
|
2683
|
+
isolatedStateRemoved: sandboxRemoved === "true",
|
|
2684
|
+
},
|
|
2685
|
+
artifacts: {
|
|
2686
|
+
invocation: "invocation.txt",
|
|
2687
|
+
pathContract: "path-contract.json",
|
|
2688
|
+
appServer: "app-server.json",
|
|
2689
|
+
daemonState: "daemon-state.json",
|
|
2690
|
+
workspaceEditContract: scenario === "rename" ? "workspace-edit-contract.json" : undefined,
|
|
2691
|
+
renameFixture: scenario === "rename" ? "rename-fixture.json" : undefined,
|
|
2692
|
+
renameServerEvents: scenario === "rename" ? "rename-server-events.jsonl" : undefined,
|
|
2693
|
+
diagnosticsFreshnessContract: scenario === "diagnostics-freshness" ? "diagnostics-freshness-contract.json" : undefined,
|
|
2694
|
+
diagnosticsFreshnessFixture: scenario === "diagnostics-freshness" ? "diagnostics-freshness-fixture.json" : undefined,
|
|
2695
|
+
legacyCleanupContract: scenario === "legacy-cleanup" ? "legacy-cleanup-contract.json" : undefined,
|
|
2696
|
+
legacyCleanupLive: scenario === "legacy-cleanup" ? "legacy-cleanup-live.json" : undefined,
|
|
2697
|
+
legacyCleanupProcessCleanup: scenario === "legacy-cleanup" ? "legacy-cleanup-process-cleanup.txt" : undefined,
|
|
2698
|
+
postEditContract: scenario === "post-edit" ? "post-edit-contract.json" : undefined,
|
|
2699
|
+
cancellationContract: scenario === "cancellation" ? "cancellation-contract.json" : undefined,
|
|
2700
|
+
clientPackage: scenario === "client-package" ? "package-smoke.json" : undefined,
|
|
2701
|
+
installedComponent: scenario === "installed-component" ? "installed-component.json" : undefined,
|
|
2702
|
+
installLog: "install.log",
|
|
2703
|
+
cleanupReceipt: "cleanup-receipt.txt",
|
|
2704
|
+
},
|
|
2705
|
+
};
|
|
2706
|
+
const required = [
|
|
2707
|
+
result.realOmoRootUnchanged,
|
|
2708
|
+
result.realCodexConfigUnchanged,
|
|
2709
|
+
result.dirtyWorktreePreserved,
|
|
2710
|
+
result.hookStarted,
|
|
2711
|
+
result.hookCompleted,
|
|
2712
|
+
result.lspOperation?.ok === true,
|
|
2713
|
+
result.cleanup.isolatedStateRemoved,
|
|
2714
|
+
...Object.values(result.overrideAssertions),
|
|
2715
|
+
];
|
|
2716
|
+
if (scenario === "rename") {
|
|
2717
|
+
required.push(
|
|
2718
|
+
result.diagnosticsOperation?.ok === true,
|
|
2719
|
+
result.serverAppliedRename === true,
|
|
2720
|
+
result.recordedResultReused === true,
|
|
2721
|
+
result.synchronizedDocumentVersion === 2,
|
|
2722
|
+
JSON.stringify(result.didChangeVersions) === JSON.stringify([2]),
|
|
2723
|
+
result.immediateDiagnostics === true,
|
|
2724
|
+
result.finalContent === "const after = 1;\n",
|
|
2725
|
+
typeof result.failureHashes?.unscoped === "string" && result.failureHashes.unscoped.length === 64,
|
|
2726
|
+
typeof result.failureHashes?.concurrent === "string" && result.failureHashes.concurrent.length === 64,
|
|
2727
|
+
typeof result.failureHashes?.mismatched === "string" && result.failureHashes.mismatched.length === 64,
|
|
2728
|
+
typeof result.failureHashes?.preGate === "string" && result.failureHashes.preGate.length === 64,
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
if (scenario === "diagnostics-freshness") {
|
|
2732
|
+
required.push(
|
|
2733
|
+
freshnessContract?.outcomes?.exactCurrent?.ok === true,
|
|
2734
|
+
freshnessContract?.outcomes?.postGenerationVersionless?.ok === true,
|
|
2735
|
+
freshnessContract?.outcomes?.stale?.ok === true,
|
|
2736
|
+
freshnessContract?.outcomes?.future?.ok === true,
|
|
2737
|
+
freshnessContract?.outcomes?.pullOvertaken?.ok === true,
|
|
2738
|
+
freshnessContract?.outcomes?.silent?.ok === true,
|
|
2739
|
+
freshnessContract?.outcomes?.closedServer?.ok === true,
|
|
2740
|
+
freshnessContract?.outcomes?.unsupportedPull?.ok === true,
|
|
2741
|
+
freshnessContract?.outcomes?.sameVersionUnchanged?.ok === true,
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
if (scenario === "legacy-cleanup") {
|
|
2745
|
+
required.push(
|
|
2746
|
+
legacyContract?.assertions?.windowsDeferred === true,
|
|
2747
|
+
legacyContract?.assertions?.noAttestationDeferred === true,
|
|
2748
|
+
legacyContract?.assertions?.timeoutDeferred === true,
|
|
2749
|
+
legacyContract?.assertions?.staleRemoved === true,
|
|
2750
|
+
legacyContract?.assertions?.symlinkRemovedWithoutFollowing === true,
|
|
2751
|
+
legacyContract?.assertions?.malformedRemoved === true,
|
|
2752
|
+
legacyLive?.results?.ownedTerminated === true,
|
|
2753
|
+
legacyLive?.results?.staleNaturalRemoved === true,
|
|
2754
|
+
legacyLive?.results?.staleHashedRemoved === true,
|
|
2755
|
+
legacyLive?.results?.malformedRemoved === true,
|
|
2756
|
+
legacyLive?.results?.foreignOwnerPreserved === true,
|
|
2757
|
+
legacyLive?.results?.timeoutPreserved === true,
|
|
2758
|
+
legacyLive?.results?.unrelatedPidNotSignaled === true,
|
|
2759
|
+
legacyLive?.results?.visibleInstallerWarning === true,
|
|
2760
|
+
legacyLive?.results?.noLiveIpcCopy === true,
|
|
2761
|
+
typeof legacyProcessCleanup === "string" && legacyProcessCleanup.includes("alive_after=no"),
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
if (scenario === "post-edit") {
|
|
2765
|
+
required.push(
|
|
2766
|
+
postEditContract?.result === "PASS",
|
|
2767
|
+
postEditContract?.assertions?.explicitTranslatorOutputs === true,
|
|
2768
|
+
postEditContract?.assertions?.translatorDefaults === true,
|
|
2769
|
+
postEditContract?.assertions?.directAdapterNonUse === true,
|
|
2770
|
+
postEditContract?.assertions?.maxConcurrencyFour === true,
|
|
2771
|
+
postEditContract?.assertions?.orderedBlocks === true,
|
|
2772
|
+
postEditContract?.assertions?.duplicatesRunOnce === true,
|
|
2773
|
+
postEditContract?.assertions?.cacheResetRetry === true,
|
|
2774
|
+
postEditContract?.assertions?.rejectionBeforeLookup === true,
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
if (scenario === "cancellation") {
|
|
2778
|
+
required.push(
|
|
2779
|
+
cancellationContract?.result === "PASS",
|
|
2780
|
+
cancellationContract?.callerAbort?.daemonProxyRequestId === cancellationContract?.callerAbort?.daemonCancelTarget,
|
|
2781
|
+
cancellationContract?.callerAbort?.lspRequestId === cancellationContract?.callerAbort?.lspCancelTarget,
|
|
2782
|
+
cancellationContract?.noLeftovers?.daemonActiveControllersAfter === 0,
|
|
2783
|
+
cancellationContract?.noLeftovers?.lspPendingRequestsAfter === 0,
|
|
2784
|
+
cancellationContract?.delayedRenamePreCommitGate?.zeroWrites === true,
|
|
2785
|
+
cancellationContract?.delayedRenamePreCommitGate?.preservesBeforeHash === true,
|
|
2786
|
+
cancellationContract?.cancellationAfterCommitGate?.mutationCount === 1,
|
|
2787
|
+
cancellationContract?.cancellationAfterCommitGate?.lateAbort === true,
|
|
2788
|
+
cancellationContract?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1,
|
|
2789
|
+
cancellationContract?.authProtocolCwd?.tokenLoggedOrForwarded === false,
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
if (scenario === "client-package") {
|
|
2793
|
+
required.push(
|
|
2794
|
+
clientPackage?.result === "PASS",
|
|
2795
|
+
clientPackage?.build?.requiredOutputs?.clientJs === true,
|
|
2796
|
+
clientPackage?.build?.requiredOutputs?.clientDts === true,
|
|
2797
|
+
clientPackage?.build?.requiredOutputs?.cliJs === true,
|
|
2798
|
+
clientPackage?.build?.requiredOutputs?.indexJs === true,
|
|
2799
|
+
clientPackage?.build?.staleDistRemoved === true,
|
|
2800
|
+
clientPackage?.packageJson?.hasOnlyClientAndCliExports === true,
|
|
2801
|
+
clientPackage?.scans?.clientJsNoWorkspaceDeps === true,
|
|
2802
|
+
clientPackage?.scans?.clientDtsNoWorkspaceDeps === true,
|
|
2803
|
+
clientPackage?.scans?.noRepositoryPathCoupling === true,
|
|
2804
|
+
clientPackage?.consumer?.emptyNodePath === true,
|
|
2805
|
+
clientPackage?.consumer?.js?.statusOk === true,
|
|
2806
|
+
clientPackage?.consumer?.js?.typedContextForwarded === true,
|
|
2807
|
+
clientPackage?.consumer?.js?.cancellation?.accepted === true,
|
|
2808
|
+
clientPackage?.consumer?.js?.rootImport?.rejected === true,
|
|
2809
|
+
clientPackage?.consumer?.js?.unknownImport?.rejected === true,
|
|
2810
|
+
clientPackage?.consumer?.js?.deepImport?.rejected === true,
|
|
2811
|
+
Array.isArray(clientPackage?.consumer?.js?.serverSymbols) && clientPackage.consumer.js.serverSymbols.length === 0,
|
|
2812
|
+
clientPackage?.consumer?.tscExitCode === 0,
|
|
2813
|
+
clientPackage?.adversarial?.repositoryHiddenByInstall === true,
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
if (scenario === "installed-component") {
|
|
2817
|
+
required.push(
|
|
2818
|
+
installedComponent?.result === "PASS",
|
|
2819
|
+
installedComponent?.manifest?.valid === true,
|
|
2820
|
+
installedComponent?.manifest?.sortedOutputs === true,
|
|
2821
|
+
installedComponent?.manifest?.outputHashesMatch === true,
|
|
2822
|
+
installedComponent?.distOnlyValidation?.ok === true,
|
|
2823
|
+
installedComponent?.componentCli?.exists === true,
|
|
2824
|
+
installedComponent?.componentCli?.noNonBuiltinRuntimeImports === true,
|
|
2825
|
+
installedComponent?.consumer?.emptyNodePath === true,
|
|
2826
|
+
installedComponent?.consumer?.repositoryHiddenByInstall === true,
|
|
2827
|
+
typeof installedComponent?.codexContext?.cwd === "string" &&
|
|
2828
|
+
installedComponent.codexContext.cwd.endsWith("/project"),
|
|
2829
|
+
installedComponent?.codexContext?.projectConfigPaths?.[0] ===
|
|
2830
|
+
`${installedComponent?.codexContext?.cwd}/.codex/lsp-client.json`,
|
|
2831
|
+
installedComponent?.codexContext?.userConfigPath?.endsWith("/codex/lsp-client.json") === true,
|
|
2832
|
+
installedComponent?.codexContext?.installDecisionsPath?.endsWith("/codex/lsp-install-decisions.json") === true,
|
|
2833
|
+
installedComponent?.codexContext?.capabilities?.installDecisionTool === true,
|
|
2834
|
+
installedComponent?.postEdit?.actualPostEditOperation === true,
|
|
2835
|
+
installedComponent?.postEdit?.projectConfigConsumed === true,
|
|
2836
|
+
installedComponent?.postEdit?.defaultBudgetWithinLimit === true,
|
|
2837
|
+
installedComponent?.postEdit?.contextPressureBudgetWithinLimit === true,
|
|
2838
|
+
installedComponent?.compaction?.cacheStored === true,
|
|
2839
|
+
installedComponent?.compaction?.compactionReset === true,
|
|
2840
|
+
installedComponent?.validation?.singletonRejected === true,
|
|
2841
|
+
installedComponent?.validation?.nonexistentPairRejected === true,
|
|
2842
|
+
installedComponent?.daemonOwner?.count === 1,
|
|
2843
|
+
installedComponent?.daemonOwner?.commandIncludesInstalledCli === true,
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
if (!required.every(Boolean)) throw new Error("refusing to write PASS result with failed assertions");
|
|
2847
|
+
writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
|
|
2848
|
+
NODE
|
|
2849
|
+
if [ "$SCENARIO" = "rename" ]; then
|
|
2850
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2851
|
+
'.result == "PASS"
|
|
2852
|
+
and .scenario == $scenario
|
|
2853
|
+
and .realOmoRootUnchanged == true
|
|
2854
|
+
and .lspOperation.ok == true
|
|
2855
|
+
and .diagnosticsOperation.ok == true
|
|
2856
|
+
and .serverAppliedRename == true
|
|
2857
|
+
and .recordedResultReused == true
|
|
2858
|
+
and .synchronizedDocumentVersion == 2
|
|
2859
|
+
and .immediateDiagnostics == true
|
|
2860
|
+
and (.failureHashes | keys | sort) == ["concurrent","mismatched","preGate","unscoped"]' \
|
|
2861
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2862
|
+
elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
|
|
2863
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2864
|
+
'.result == "PASS"
|
|
2865
|
+
and .scenario == $scenario
|
|
2866
|
+
and .realOmoRootUnchanged == true
|
|
2867
|
+
and .lspOperation.ok == true
|
|
2868
|
+
and .diagnosticsFreshnessProbe.outcomes.exactCurrent.ok == true
|
|
2869
|
+
and .diagnosticsFreshnessProbe.outcomes.postGenerationVersionless.ok == true
|
|
2870
|
+
and .diagnosticsFreshnessProbe.outcomes.stale.ok == true
|
|
2871
|
+
and .diagnosticsFreshnessProbe.outcomes.future.ok == true
|
|
2872
|
+
and .diagnosticsFreshnessProbe.outcomes.pullOvertaken.ok == true
|
|
2873
|
+
and .diagnosticsFreshnessProbe.outcomes.silent.ok == true
|
|
2874
|
+
and .diagnosticsFreshnessProbe.outcomes.closedServer.ok == true
|
|
2875
|
+
and .diagnosticsFreshnessProbe.outcomes.unsupportedPull.ok == true
|
|
2876
|
+
and .diagnosticsFreshnessProbe.outcomes.sameVersionUnchanged.ok == true' \
|
|
2877
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2878
|
+
elif [ "$SCENARIO" = "legacy-cleanup" ]; then
|
|
2879
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2880
|
+
'.result == "PASS"
|
|
2881
|
+
and .scenario == $scenario
|
|
2882
|
+
and .realOmoRootUnchanged == true
|
|
2883
|
+
and .lspOperation.ok == true
|
|
2884
|
+
and .legacyCleanup.contract.assertions.windowsDeferred == true
|
|
2885
|
+
and .legacyCleanup.contract.assertions.noAttestationDeferred == true
|
|
2886
|
+
and .legacyCleanup.contract.assertions.timeoutDeferred == true
|
|
2887
|
+
and .legacyCleanup.contract.assertions.staleRemoved == true
|
|
2888
|
+
and .legacyCleanup.contract.assertions.symlinkRemovedWithoutFollowing == true
|
|
2889
|
+
and .legacyCleanup.contract.assertions.malformedRemoved == true
|
|
2890
|
+
and .legacyCleanup.live.results.ownedTerminated == true
|
|
2891
|
+
and .legacyCleanup.live.results.foreignOwnerPreserved == true
|
|
2892
|
+
and .legacyCleanup.live.results.timeoutPreserved == true
|
|
2893
|
+
and .legacyCleanup.live.results.unrelatedPidNotSignaled == true
|
|
2894
|
+
and .legacyCleanup.live.results.visibleInstallerWarning == true
|
|
2895
|
+
and .legacyCleanup.live.results.noLiveIpcCopy == true' \
|
|
2896
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2897
|
+
elif [ "$SCENARIO" = "post-edit" ]; then
|
|
2898
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2899
|
+
'.result == "PASS"
|
|
2900
|
+
and .scenario == $scenario
|
|
2901
|
+
and .realOmoRootUnchanged == true
|
|
2902
|
+
and .lspOperation.ok == true
|
|
2903
|
+
and .postEditContract.result == "PASS"
|
|
2904
|
+
and .postEditContract.assertions.openCodeMcpEnvInputs == true
|
|
2905
|
+
and .postEditContract.assertions.explicitTranslatorOutputs == true
|
|
2906
|
+
and .postEditContract.assertions.translatorDefaults == true
|
|
2907
|
+
and .postEditContract.assertions.directAdapterNonUse == true
|
|
2908
|
+
and .postEditContract.assertions.maxConcurrencyFour == true
|
|
2909
|
+
and .postEditContract.assertions.orderedBlocks == true
|
|
2910
|
+
and .postEditContract.assertions.duplicatesRunOnce == true
|
|
2911
|
+
and .postEditContract.assertions.cacheResetRetry == true
|
|
2912
|
+
and .postEditContract.assertions.rejectionBeforeLookup == true' \
|
|
2913
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2914
|
+
elif [ "$SCENARIO" = "cancellation" ]; then
|
|
2915
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2916
|
+
'.result == "PASS"
|
|
2917
|
+
and .scenario == $scenario
|
|
2918
|
+
and .realOmoRootUnchanged == true
|
|
2919
|
+
and .lspOperation.ok == true
|
|
2920
|
+
and .cancellationContract.result == "PASS"
|
|
2921
|
+
and .cancellationContract.callerAbort.daemonProxyRequestId == .cancellationContract.callerAbort.daemonCancelTarget
|
|
2922
|
+
and .cancellationContract.callerAbort.lspRequestId == .cancellationContract.callerAbort.lspCancelTarget
|
|
2923
|
+
and .cancellationContract.noLeftovers.daemonActiveControllersAfter == 0
|
|
2924
|
+
and .cancellationContract.noLeftovers.lspPendingRequestsAfter == 0
|
|
2925
|
+
and .cancellationContract.delayedRenamePreCommitGate.zeroWrites == true
|
|
2926
|
+
and .cancellationContract.cancellationAfterCommitGate.mutationCount == 1
|
|
2927
|
+
and .cancellationContract.authProtocolCwd.tokenLoggedOrForwarded == false' \
|
|
2928
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2929
|
+
elif [ "$SCENARIO" = "client-package" ]; then
|
|
2930
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2931
|
+
'.result == "PASS"
|
|
2932
|
+
and .scenario == $scenario
|
|
2933
|
+
and .realOmoRootUnchanged == true
|
|
2934
|
+
and .lspOperation.ok == true
|
|
2935
|
+
and .clientPackage.result == "PASS"
|
|
2936
|
+
and .clientPackage.build.requiredOutputs.clientJs == true
|
|
2937
|
+
and .clientPackage.build.requiredOutputs.clientDts == true
|
|
2938
|
+
and .clientPackage.build.requiredOutputs.cliJs == true
|
|
2939
|
+
and .clientPackage.build.requiredOutputs.indexJs == true
|
|
2940
|
+
and .clientPackage.build.staleDistRemoved == true
|
|
2941
|
+
and .clientPackage.packageJson.hasOnlyClientAndCliExports == true
|
|
2942
|
+
and .clientPackage.scans.clientJsNoWorkspaceDeps == true
|
|
2943
|
+
and .clientPackage.scans.clientDtsNoWorkspaceDeps == true
|
|
2944
|
+
and .clientPackage.scans.noRepositoryPathCoupling == true
|
|
2945
|
+
and .clientPackage.consumer.emptyNodePath == true
|
|
2946
|
+
and .clientPackage.consumer.js.statusOk == true
|
|
2947
|
+
and .clientPackage.consumer.js.typedContextForwarded == true
|
|
2948
|
+
and .clientPackage.consumer.js.cancellation.accepted == true
|
|
2949
|
+
and .clientPackage.consumer.js.rootImport.rejected == true
|
|
2950
|
+
and .clientPackage.consumer.js.unknownImport.rejected == true
|
|
2951
|
+
and .clientPackage.consumer.js.deepImport.rejected == true
|
|
2952
|
+
and (.clientPackage.consumer.js.serverSymbols | length) == 0
|
|
2953
|
+
and .clientPackage.consumer.tscExitCode == 0
|
|
2954
|
+
and .clientPackage.adversarial.repositoryHiddenByInstall == true' \
|
|
2955
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2956
|
+
elif [ "$SCENARIO" = "installed-component" ]; then
|
|
2957
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2958
|
+
'.result == "PASS"
|
|
2959
|
+
and .scenario == $scenario
|
|
2960
|
+
and .realOmoRootUnchanged == true
|
|
2961
|
+
and .realCodexConfigUnchanged == true
|
|
2962
|
+
and .dirtyWorktreePreserved == true
|
|
2963
|
+
and .hookStarted == true
|
|
2964
|
+
and .hookCompleted == true
|
|
2965
|
+
and .lspOperation.ok == true
|
|
2966
|
+
and .installedComponent.result == "PASS"
|
|
2967
|
+
and .installedComponent.manifest.valid == true
|
|
2968
|
+
and .installedComponent.manifest.sortedOutputs == true
|
|
2969
|
+
and .installedComponent.manifest.outputHashesMatch == true
|
|
2970
|
+
and .installedComponent.distOnlyValidation.ok == true
|
|
2971
|
+
and .installedComponent.componentCli.exists == true
|
|
2972
|
+
and .installedComponent.componentCli.noNonBuiltinRuntimeImports == true
|
|
2973
|
+
and .installedComponent.consumer.emptyNodePath == true
|
|
2974
|
+
and .installedComponent.consumer.repositoryHiddenByInstall == true
|
|
2975
|
+
and .installedComponent.codexContext.capabilities.installDecisionTool == true
|
|
2976
|
+
and .installedComponent.postEdit.actualPostEditOperation == true
|
|
2977
|
+
and .installedComponent.postEdit.projectConfigConsumed == true
|
|
2978
|
+
and .installedComponent.postEdit.defaultBudgetWithinLimit == true
|
|
2979
|
+
and .installedComponent.postEdit.contextPressureBudgetWithinLimit == true
|
|
2980
|
+
and .installedComponent.compaction.cacheStored == true
|
|
2981
|
+
and .installedComponent.compaction.compactionReset == true
|
|
2982
|
+
and .installedComponent.validation.singletonRejected == true
|
|
2983
|
+
and .installedComponent.validation.nonexistentPairRejected == true
|
|
2984
|
+
and .installedComponent.daemonOwner.count == 1
|
|
2985
|
+
and .installedComponent.daemonOwner.commandIncludesInstalledCli == true
|
|
2986
|
+
and .cleanup.isolatedStateRemoved == true' \
|
|
2987
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2988
|
+
else
|
|
2989
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
2990
|
+
'.result == "PASS" and .scenario == $scenario and .realOmoRootUnchanged == true and .lspOperation.ok == true' \
|
|
2991
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
2992
|
+
fi
|
|
2993
|
+
mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
|
|
2994
|
+
RESULT_STAGE=""
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
write_auth_ownership_result() {
|
|
2998
|
+
local real_omo_before="$1" real_omo_after="$2" real_codex_before="$3" real_codex_after="$4"
|
|
2999
|
+
local worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
|
|
3000
|
+
RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
|
|
3001
|
+
node --input-type=module - \
|
|
3002
|
+
"$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
|
|
3003
|
+
"$worktree_before" "$worktree_after" "$sandbox_removed" "$EVIDENCE_DIR/path-contract.json" "$EVIDENCE_DIR/auth-ownership.json" <<'NODE'
|
|
3004
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3005
|
+
const [output, scenario, omoBefore, omoAfter, codexBefore, codexAfter, worktreeBefore, worktreeAfter, sandboxRemoved, contractPath, authPath] = process.argv.slice(2);
|
|
3006
|
+
const contract = JSON.parse(readFileSync(contractPath, "utf8"));
|
|
3007
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
3008
|
+
const result = {
|
|
3009
|
+
...auth,
|
|
3010
|
+
scenario,
|
|
3011
|
+
harness: "codex",
|
|
3012
|
+
realOmoRootUnchanged: omoBefore === omoAfter,
|
|
3013
|
+
realCodexConfigUnchanged: codexBefore === codexAfter,
|
|
3014
|
+
dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
|
|
3015
|
+
isolatedCodexHome: true,
|
|
3016
|
+
localMockModel: false,
|
|
3017
|
+
pluginOnly: false,
|
|
3018
|
+
unchangedRealRoot: omoBefore === omoAfter,
|
|
3019
|
+
unchangedRealHomes: omoBefore === omoAfter && codexBefore === codexAfter,
|
|
3020
|
+
overrideAssertions: contract.assertions,
|
|
3021
|
+
cleanup: {
|
|
3022
|
+
daemonStopped: true,
|
|
3023
|
+
isolatedStateRemoved: sandboxRemoved === "true",
|
|
3024
|
+
},
|
|
3025
|
+
artifacts: {
|
|
3026
|
+
invocation: "invocation.txt",
|
|
3027
|
+
pathContract: "path-contract.json",
|
|
3028
|
+
authOwnership: "auth-ownership.json",
|
|
3029
|
+
cleanupReceipt: "cleanup-receipt.txt",
|
|
3030
|
+
},
|
|
3031
|
+
};
|
|
3032
|
+
const required = [
|
|
3033
|
+
result.result === "PASS",
|
|
3034
|
+
result.firstStartNoDeadlock === true,
|
|
3035
|
+
result.owner && typeof result.owner.pid === "number" && typeof result.owner.nonce === "string",
|
|
3036
|
+
result.tokenPresent === true,
|
|
3037
|
+
result.tokenLeaked === false,
|
|
3038
|
+
result.losingCandidateExit === 0,
|
|
3039
|
+
result.twoConfinedContexts === true,
|
|
3040
|
+
result.badAuthPreDispatchRejection === true,
|
|
3041
|
+
result.liveOwnerDeferral === true,
|
|
3042
|
+
result.deadOwnerCleanup === true,
|
|
3043
|
+
result.staleCloseSurvival === true,
|
|
3044
|
+
result.windowsTokenRequired === true,
|
|
3045
|
+
result.realOmoRootUnchanged,
|
|
3046
|
+
result.realCodexConfigUnchanged,
|
|
3047
|
+
result.dirtyWorktreePreserved,
|
|
3048
|
+
result.cleanup.isolatedStateRemoved,
|
|
3049
|
+
...Object.values(result.overrideAssertions),
|
|
3050
|
+
];
|
|
3051
|
+
if (!required.every(Boolean)) throw new Error("refusing to write auth-ownership PASS result with failed assertions");
|
|
3052
|
+
writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
|
|
3053
|
+
NODE
|
|
3054
|
+
jq -e --arg scenario "$SCENARIO" \
|
|
3055
|
+
'.result == "PASS"
|
|
3056
|
+
and .scenario == $scenario
|
|
3057
|
+
and .firstStartNoDeadlock == true
|
|
3058
|
+
and (.owner.pid | type) == "number"
|
|
3059
|
+
and (.owner.nonce | type) == "string"
|
|
3060
|
+
and .tokenPresent == true
|
|
3061
|
+
and .tokenLeaked == false
|
|
3062
|
+
and .losingCandidateExit == 0
|
|
3063
|
+
and .twoConfinedContexts == true
|
|
3064
|
+
and .badAuthPreDispatchRejection == true
|
|
3065
|
+
and .liveOwnerDeferral == true
|
|
3066
|
+
and .deadOwnerCleanup == true
|
|
3067
|
+
and .staleCloseSurvival == true
|
|
3068
|
+
and .realOmoRootUnchanged == true
|
|
3069
|
+
and .realCodexConfigUnchanged == true' \
|
|
3070
|
+
"$RESULT_STAGE" >/dev/null || return 1
|
|
3071
|
+
mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
|
|
3072
|
+
RESULT_STAGE=""
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
run_internal_fixture() {
|
|
3076
|
+
case "${LSP_E2E_INTERNAL_FIXTURE:-}" in
|
|
3077
|
+
fake-pass)
|
|
3078
|
+
printf '{"result":"PASS","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
|
|
3079
|
+
fail "seeded PASS output lacked required assertions"
|
|
3080
|
+
;;
|
|
3081
|
+
fake-skip)
|
|
3082
|
+
printf '{"result":"SKIP","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
|
|
3083
|
+
fail "SKIP is never a passing result"
|
|
3084
|
+
;;
|
|
3085
|
+
partial)
|
|
3086
|
+
RESULT_STAGE="$EVIDENCE_DIR/.result.json.partial"
|
|
3087
|
+
printf '{"result":"PASS"' >"$RESULT_STAGE"
|
|
3088
|
+
return 19
|
|
3089
|
+
;;
|
|
3090
|
+
interrupt)
|
|
3091
|
+
SANDBOX_ROOT="$(mktemp -d -t cqa-lsp-e2e.XXXXXX)" || return 1
|
|
3092
|
+
printf '%s\n' "$SANDBOX_ROOT" >"$EVIDENCE_DIR/interrupt-sandbox.txt"
|
|
3093
|
+
RESULT_STAGE="$EVIDENCE_DIR/.result.json.interrupt"
|
|
3094
|
+
printf '{"result":"PASS"' >"$RESULT_STAGE"
|
|
3095
|
+
while :; do sleep 1; done
|
|
3096
|
+
;;
|
|
3097
|
+
*)
|
|
3098
|
+
fail "unknown internal fixture"
|
|
3099
|
+
;;
|
|
3100
|
+
esac
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
run_missing_dist_build_order_self_test() {
|
|
3104
|
+
local test_root="$1" saved_evidence="$EVIDENCE_DIR" saved_sandbox="$SANDBOX_ROOT"
|
|
3105
|
+
local ev="$test_root/missing-dist-build-order" failures=0 red_rc=0 green_rc=0 restored=false
|
|
3106
|
+
mkdir -p "$ev/sandbox" || return 1
|
|
3107
|
+
EVIDENCE_DIR="$ev"
|
|
3108
|
+
SANDBOX_ROOT="$ev/sandbox"
|
|
3109
|
+
|
|
3110
|
+
snapshot_daemon_dist "$SANDBOX_ROOT/daemon-dist-backup" || failures=$((failures + 1))
|
|
3111
|
+
rm -rf "$DAEMON_DIST_DIR" || failures=$((failures + 1))
|
|
3112
|
+
[ ! -e "$DAEMON_DIST_DIR/package.json" ] || failures=$((failures + 1))
|
|
3113
|
+
|
|
3114
|
+
cat >"$ev/pre-fix-path-contract-read.mjs" <<'NODE'
|
|
3115
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3116
|
+
import { join } from "node:path";
|
|
3117
|
+
|
|
3118
|
+
const [repoRoot, output] = process.argv.slice(2);
|
|
3119
|
+
const packagePath = join(repoRoot, "packages/lsp-daemon/dist/package.json");
|
|
3120
|
+
const version = JSON.parse(readFileSync(packagePath, "utf8")).version;
|
|
3121
|
+
writeFileSync(output, JSON.stringify({ result: "UNEXPECTED_PASS", version }) + "\n");
|
|
3122
|
+
NODE
|
|
3123
|
+
if run_bounded 30 "$ev/red-prebuild-failure.log" bun "$ev/pre-fix-path-contract-read.mjs" "$REPO_ROOT" "$ev/unexpected-result.json"; then
|
|
3124
|
+
failures=$((failures + 1))
|
|
3125
|
+
else
|
|
3126
|
+
red_rc=$?
|
|
3127
|
+
[ "$red_rc" -ne 0 ] || failures=$((failures + 1))
|
|
3128
|
+
fi
|
|
3129
|
+
grep -q 'ENOENT' "$ev/red-prebuild-failure.log" || failures=$((failures + 1))
|
|
3130
|
+
[ ! -e "$ev/result.json" ] || failures=$((failures + 1))
|
|
3131
|
+
[ ! -e "$ev/unexpected-result.json" ] || failures=$((failures + 1))
|
|
3132
|
+
|
|
3133
|
+
prepare_daemon_dist || failures=$((failures + 1))
|
|
3134
|
+
write_path_contract_probe "$SANDBOX_ROOT/contract-green" "$ev/path-contract-green.json"
|
|
3135
|
+
green_rc=$?
|
|
3136
|
+
[ "$green_rc" -eq 0 ] || failures=$((failures + 1))
|
|
3137
|
+
jq -e '.assertions.defaultCliPackaged == true and .assertions.defaultVersionStamped == true' \
|
|
3138
|
+
"$ev/path-contract-green.json" >/dev/null || failures=$((failures + 1))
|
|
3139
|
+
|
|
3140
|
+
if restore_daemon_dist; then
|
|
3141
|
+
restored=true
|
|
3142
|
+
else
|
|
3143
|
+
failures=$((failures + 1))
|
|
3144
|
+
fi
|
|
3145
|
+
node --input-type=module - "$ev/build-order-proof.json" "$red_rc" "$green_rc" "$restored" "$failures" <<'NODE'
|
|
3146
|
+
import { writeFileSync } from "node:fs";
|
|
3147
|
+
const [output, redRc, greenRc, restored, failures] = process.argv.slice(2);
|
|
3148
|
+
writeFileSync(output, JSON.stringify({
|
|
3149
|
+
result: failures === "0" ? "PASS" : "FAIL",
|
|
3150
|
+
missingDistProven: true,
|
|
3151
|
+
redFailedBeforeResultJson: redRc !== "0",
|
|
3152
|
+
redFailureLog: "red-prebuild-failure.log",
|
|
3153
|
+
greenBuiltDaemonDist: greenRc === "0",
|
|
3154
|
+
buildLog: "lsp-daemon-prebuild.log",
|
|
3155
|
+
pathContract: "path-contract-green.json",
|
|
3156
|
+
distRestored: restored === "true",
|
|
3157
|
+
}, null, 2) + "\n");
|
|
3158
|
+
NODE
|
|
3159
|
+
|
|
3160
|
+
EVIDENCE_DIR="$saved_evidence"
|
|
3161
|
+
SANDBOX_ROOT="$saved_sandbox"
|
|
3162
|
+
[ "$failures" -eq 0 ]
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
run_self_test() {
|
|
3166
|
+
require_bins bash node jq git || return 1
|
|
3167
|
+
local root before_omo after_omo before_codex after_codex before_status after_status failures=0 out rc start end
|
|
3168
|
+
local cancellation_result_fields=true client_package_result_fields=true installed_component_result_fields=true
|
|
3169
|
+
local fixture_run child sandbox_path attempts
|
|
3170
|
+
local missing_dist_build_order='{"result":"NOT_RUN"}'
|
|
3171
|
+
root="$(mktemp -d -t cqa-lsp-e2e.XXXXXX)" || return 1
|
|
3172
|
+
SANDBOX_ROOT="$root"
|
|
3173
|
+
before_omo="$(hash_path "$REAL_OMO_ROOT")"
|
|
3174
|
+
before_codex="$(hash_path "$REAL_CODEX_CONFIG")"
|
|
3175
|
+
before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3176
|
+
|
|
3177
|
+
out="$root/parser.log"
|
|
3178
|
+
if bash "${BASH_SOURCE[0]}" --scenario >"$out" 2>&1; then failures=$((failures + 1)); fi
|
|
3179
|
+
grep -q -- '--scenario requires a value' "$out" || failures=$((failures + 1))
|
|
3180
|
+
if bash "${BASH_SOURCE[0]}" --scenario ../bad --evidence-dir "$root/bad" >>"$out" 2>&1; then failures=$((failures + 1)); fi
|
|
3181
|
+
if bash "${BASH_SOURCE[0]}" --scenario ok --evidence-dir relative >>"$out" 2>&1; then failures=$((failures + 1)); fi
|
|
3182
|
+
|
|
3183
|
+
for fixture in fake-pass fake-skip; do
|
|
3184
|
+
local ev="$root/$fixture"
|
|
3185
|
+
mkdir -p "$ev"
|
|
3186
|
+
printf '{"result":"%s","scenario":"self-test"}\n' "$( [ "$fixture" = fake-pass ] && echo PASS || echo SKIP )" >"$ev/result.json"
|
|
3187
|
+
if LSP_E2E_INTERNAL_FIXTURE="$fixture" bash "${BASH_SOURCE[0]}" \
|
|
3188
|
+
--scenario self-test --evidence-dir "$ev" >>"$out" 2>&1; then
|
|
3189
|
+
failures=$((failures + 1))
|
|
3190
|
+
fi
|
|
3191
|
+
[ ! -e "$ev/result.json" ] || failures=$((failures + 1))
|
|
3192
|
+
done
|
|
3193
|
+
|
|
3194
|
+
start="$(date +%s)"
|
|
3195
|
+
if run_bounded 1 "$root/hung.log" node -e 'setTimeout(() => {}, 30000)'; then
|
|
3196
|
+
failures=$((failures + 1))
|
|
3197
|
+
else
|
|
3198
|
+
rc=$?
|
|
3199
|
+
[ "$rc" -eq 124 ] || failures=$((failures + 1))
|
|
3200
|
+
fi
|
|
3201
|
+
end="$(date +%s)"
|
|
3202
|
+
[ $((end - start)) -lt 6 ] || failures=$((failures + 1))
|
|
3203
|
+
|
|
3204
|
+
fixture_run=0
|
|
3205
|
+
while [ "$fixture_run" -lt 2 ]; do
|
|
3206
|
+
local ev="$root/partial-$fixture_run"
|
|
3207
|
+
mkdir -p "$ev"
|
|
3208
|
+
if LSP_E2E_INTERNAL_FIXTURE=partial bash "${BASH_SOURCE[0]}" \
|
|
3209
|
+
--scenario self-test --evidence-dir "$ev" >>"$out" 2>&1; then
|
|
3210
|
+
failures=$((failures + 1))
|
|
3211
|
+
fi
|
|
3212
|
+
if find "$ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
|
|
3213
|
+
[ ! -e "$ev/result.json" ] || failures=$((failures + 1))
|
|
3214
|
+
fixture_run=$((fixture_run + 1))
|
|
3215
|
+
done
|
|
3216
|
+
|
|
3217
|
+
fixture_run=0
|
|
3218
|
+
while [ "$fixture_run" -lt 2 ]; do
|
|
3219
|
+
local interrupt_ev="$root/interrupt-$fixture_run"
|
|
3220
|
+
mkdir -p "$interrupt_ev"
|
|
3221
|
+
LSP_E2E_INTERNAL_FIXTURE=interrupt bash "${BASH_SOURCE[0]}" \
|
|
3222
|
+
--scenario self-test --evidence-dir "$interrupt_ev" >>"$out" 2>&1 &
|
|
3223
|
+
child=$!
|
|
3224
|
+
attempts=0
|
|
3225
|
+
while [ ! -f "$interrupt_ev/interrupt-sandbox.txt" ] && [ "$attempts" -lt 50 ]; do
|
|
3226
|
+
sleep 0.1
|
|
3227
|
+
attempts=$((attempts + 1))
|
|
3228
|
+
done
|
|
3229
|
+
sandbox_path="$(cat "$interrupt_ev/interrupt-sandbox.txt" 2>/dev/null || true)"
|
|
3230
|
+
kill -TERM "$child" 2>/dev/null || true
|
|
3231
|
+
wait "$child" 2>/dev/null && failures=$((failures + 1))
|
|
3232
|
+
[ -n "$sandbox_path" ] && [ ! -e "$sandbox_path" ] || failures=$((failures + 1))
|
|
3233
|
+
if find "$interrupt_ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
|
|
3234
|
+
[ ! -e "$interrupt_ev/result.json" ] || failures=$((failures + 1))
|
|
3235
|
+
fixture_run=$((fixture_run + 1))
|
|
3236
|
+
done
|
|
3237
|
+
|
|
3238
|
+
if run_missing_dist_build_order_self_test "$root" >>"$out" 2>&1; then
|
|
3239
|
+
missing_dist_build_order="$(jq -c . "$root/missing-dist-build-order/build-order-proof.json")"
|
|
3240
|
+
else
|
|
3241
|
+
failures=$((failures + 1))
|
|
3242
|
+
if [ -f "$root/missing-dist-build-order/build-order-proof.json" ]; then
|
|
3243
|
+
missing_dist_build_order="$(jq -c . "$root/missing-dist-build-order/build-order-proof.json")"
|
|
3244
|
+
fi
|
|
3245
|
+
fi
|
|
3246
|
+
|
|
3247
|
+
if ! node --input-type=module <<'NODE' >>"$out" 2>&1
|
|
3248
|
+
function valid(value) {
|
|
3249
|
+
return value?.result === "PASS"
|
|
3250
|
+
&& value?.callerAbort?.daemonProxyRequestId === value?.callerAbort?.daemonCancelTarget
|
|
3251
|
+
&& value?.callerAbort?.lspRequestId === value?.callerAbort?.lspCancelTarget
|
|
3252
|
+
&& value?.noLeftovers?.daemonActiveControllersAfter === 0
|
|
3253
|
+
&& value?.noLeftovers?.lspPendingRequestsAfter === 0
|
|
3254
|
+
&& value?.delayedRenamePreCommitGate?.zeroWrites === true
|
|
3255
|
+
&& value?.cancellationAfterCommitGate?.mutationCount === 1
|
|
3256
|
+
&& value?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1
|
|
3257
|
+
&& value?.sequentialProxyIds?.distinct === true
|
|
3258
|
+
&& value?.authProtocolCwd?.tokenLoggedOrForwarded === false;
|
|
3259
|
+
}
|
|
3260
|
+
const good = {
|
|
3261
|
+
result: "PASS",
|
|
3262
|
+
callerAbort: { daemonProxyRequestId: 1, daemonCancelTarget: 1, lspRequestId: 2, lspCancelTarget: 2 },
|
|
3263
|
+
noLeftovers: { daemonActiveControllersAfter: 0, lspPendingRequestsAfter: 0 },
|
|
3264
|
+
delayedRenamePreCommitGate: { zeroWrites: true },
|
|
3265
|
+
cancellationAfterCommitGate: { mutationCount: 1 },
|
|
3266
|
+
readOnlyPreWriteConnectionFailureRetry: { retryCount: 1 },
|
|
3267
|
+
sequentialProxyIds: { distinct: true },
|
|
3268
|
+
authProtocolCwd: { tokenLoggedOrForwarded: false },
|
|
3269
|
+
};
|
|
3270
|
+
const bad = structuredClone(good);
|
|
3271
|
+
bad.sequentialProxyIds.distinct = false;
|
|
3272
|
+
if (!valid(good) || valid(bad)) process.exit(1);
|
|
3273
|
+
NODE
|
|
3274
|
+
then
|
|
3275
|
+
cancellation_result_fields=false
|
|
3276
|
+
printf 'cancellationResultFieldsMandatory=false\n' >>"$out"
|
|
3277
|
+
failures=$((failures + 1))
|
|
3278
|
+
fi
|
|
3279
|
+
|
|
3280
|
+
if ! node --input-type=module <<'NODE' >>"$out" 2>&1
|
|
3281
|
+
function valid(value) {
|
|
3282
|
+
return value?.result === "PASS"
|
|
3283
|
+
&& value?.build?.requiredOutputs?.clientJs === true
|
|
3284
|
+
&& value?.build?.requiredOutputs?.clientDts === true
|
|
3285
|
+
&& value?.build?.requiredOutputs?.cliJs === true
|
|
3286
|
+
&& value?.build?.requiredOutputs?.indexJs === true
|
|
3287
|
+
&& value?.build?.staleDistRemoved === true
|
|
3288
|
+
&& value?.packageJson?.hasOnlyClientAndCliExports === true
|
|
3289
|
+
&& value?.scans?.clientJsNoWorkspaceDeps === true
|
|
3290
|
+
&& value?.scans?.clientDtsNoWorkspaceDeps === true
|
|
3291
|
+
&& value?.scans?.noRepositoryPathCoupling === true
|
|
3292
|
+
&& value?.consumer?.emptyNodePath === true
|
|
3293
|
+
&& value?.consumer?.js?.statusOk === true
|
|
3294
|
+
&& value?.consumer?.js?.typedContextForwarded === true
|
|
3295
|
+
&& value?.consumer?.js?.cancellation?.accepted === true
|
|
3296
|
+
&& value?.consumer?.js?.rootImport?.rejected === true
|
|
3297
|
+
&& value?.consumer?.js?.unknownImport?.rejected === true
|
|
3298
|
+
&& value?.consumer?.js?.deepImport?.rejected === true
|
|
3299
|
+
&& Array.isArray(value?.consumer?.js?.serverSymbols)
|
|
3300
|
+
&& value.consumer.js.serverSymbols.length === 0
|
|
3301
|
+
&& value?.consumer?.tscExitCode === 0
|
|
3302
|
+
&& value?.adversarial?.repositoryHiddenByInstall === true;
|
|
3303
|
+
}
|
|
3304
|
+
const good = {
|
|
3305
|
+
result: "PASS",
|
|
3306
|
+
build: { requiredOutputs: { clientJs: true, clientDts: true, cliJs: true, indexJs: true }, staleDistRemoved: true },
|
|
3307
|
+
packageJson: { hasOnlyClientAndCliExports: true },
|
|
3308
|
+
scans: { clientJsNoWorkspaceDeps: true, clientDtsNoWorkspaceDeps: true, noRepositoryPathCoupling: true },
|
|
3309
|
+
consumer: {
|
|
3310
|
+
emptyNodePath: true,
|
|
3311
|
+
js: {
|
|
3312
|
+
statusOk: true,
|
|
3313
|
+
typedContextForwarded: true,
|
|
3314
|
+
cancellation: { accepted: true },
|
|
3315
|
+
rootImport: { rejected: true },
|
|
3316
|
+
unknownImport: { rejected: true },
|
|
3317
|
+
deepImport: { rejected: true },
|
|
3318
|
+
serverSymbols: [],
|
|
3319
|
+
},
|
|
3320
|
+
tscExitCode: 0,
|
|
3321
|
+
},
|
|
3322
|
+
adversarial: { repositoryHiddenByInstall: true },
|
|
3323
|
+
};
|
|
3324
|
+
const missing = { result: "PASS" };
|
|
3325
|
+
const leaked = structuredClone(good);
|
|
3326
|
+
leaked.adversarial.repositoryHiddenByInstall = false;
|
|
3327
|
+
const acceptedRoot = structuredClone(good);
|
|
3328
|
+
acceptedRoot.consumer.js.rootImport.rejected = false;
|
|
3329
|
+
const acceptedDeep = structuredClone(good);
|
|
3330
|
+
acceptedDeep.consumer.js.deepImport.rejected = false;
|
|
3331
|
+
const staleBuild = structuredClone(good);
|
|
3332
|
+
staleBuild.build.staleDistRemoved = false;
|
|
3333
|
+
if (!valid(good) || valid(missing) || valid(leaked) || valid(acceptedRoot) || valid(acceptedDeep) || valid(staleBuild)) process.exit(1);
|
|
3334
|
+
NODE
|
|
3335
|
+
then
|
|
3336
|
+
client_package_result_fields=false
|
|
3337
|
+
printf 'clientPackageResultFieldsMandatory=false\n' >>"$out"
|
|
3338
|
+
failures=$((failures + 1))
|
|
3339
|
+
fi
|
|
3340
|
+
|
|
3341
|
+
if ! node --input-type=module <<'NODE' >>"$out" 2>&1
|
|
3342
|
+
function valid(value) {
|
|
3343
|
+
return value?.result === "PASS"
|
|
3344
|
+
&& value?.manifest?.valid === true
|
|
3345
|
+
&& value?.manifest?.sortedOutputs === true
|
|
3346
|
+
&& value?.manifest?.outputHashesMatch === true
|
|
3347
|
+
&& value?.distOnlyValidation?.ok === true
|
|
3348
|
+
&& value?.componentCli?.exists === true
|
|
3349
|
+
&& value?.componentCli?.noNonBuiltinRuntimeImports === true
|
|
3350
|
+
&& value?.consumer?.emptyNodePath === true
|
|
3351
|
+
&& value?.consumer?.repositoryHiddenByInstall === true
|
|
3352
|
+
&& value?.codexContext?.capabilities?.installDecisionTool === true
|
|
3353
|
+
&& value?.postEdit?.actualPostEditOperation === true
|
|
3354
|
+
&& value?.postEdit?.projectConfigConsumed === true
|
|
3355
|
+
&& value?.postEdit?.defaultBudgetWithinLimit === true
|
|
3356
|
+
&& value?.postEdit?.contextPressureBudgetWithinLimit === true
|
|
3357
|
+
&& value?.compaction?.cacheStored === true
|
|
3358
|
+
&& value?.compaction?.compactionReset === true
|
|
3359
|
+
&& value?.validation?.singletonRejected === true
|
|
3360
|
+
&& value?.validation?.nonexistentPairRejected === true
|
|
3361
|
+
&& value?.daemonOwner?.count === 1
|
|
3362
|
+
&& value?.daemonOwner?.commandIncludesInstalledCli === true;
|
|
3363
|
+
}
|
|
3364
|
+
const good = {
|
|
3365
|
+
result: "PASS",
|
|
3366
|
+
manifest: { valid: true, sortedOutputs: true, outputHashesMatch: true },
|
|
3367
|
+
distOnlyValidation: { ok: true },
|
|
3368
|
+
componentCli: { exists: true, noNonBuiltinRuntimeImports: true },
|
|
3369
|
+
consumer: { emptyNodePath: true, repositoryHiddenByInstall: true },
|
|
3370
|
+
codexContext: { capabilities: { installDecisionTool: true } },
|
|
3371
|
+
postEdit: {
|
|
3372
|
+
actualPostEditOperation: true,
|
|
3373
|
+
projectConfigConsumed: true,
|
|
3374
|
+
defaultBudgetWithinLimit: true,
|
|
3375
|
+
contextPressureBudgetWithinLimit: true,
|
|
3376
|
+
},
|
|
3377
|
+
compaction: { cacheStored: true, compactionReset: true },
|
|
3378
|
+
validation: { singletonRejected: true, nonexistentPairRejected: true },
|
|
3379
|
+
daemonOwner: { count: 1, commandIncludesInstalledCli: true },
|
|
3380
|
+
};
|
|
3381
|
+
const missing = { result: "PASS" };
|
|
3382
|
+
const workspaceImport = structuredClone(good);
|
|
3383
|
+
workspaceImport.componentCli.noNonBuiltinRuntimeImports = false;
|
|
3384
|
+
const missingBudget = structuredClone(good);
|
|
3385
|
+
missingBudget.postEdit.contextPressureBudgetWithinLimit = false;
|
|
3386
|
+
const missingCompaction = structuredClone(good);
|
|
3387
|
+
missingCompaction.compaction.compactionReset = false;
|
|
3388
|
+
const badOwner = structuredClone(good);
|
|
3389
|
+
badOwner.daemonOwner.count = 2;
|
|
3390
|
+
if (!valid(good) || valid(missing) || valid(workspaceImport) || valid(missingBudget) || valid(missingCompaction) || valid(badOwner)) process.exit(1);
|
|
3391
|
+
NODE
|
|
3392
|
+
then
|
|
3393
|
+
installed_component_result_fields=false
|
|
3394
|
+
printf 'installedComponentResultFieldsMandatory=false\n' >>"$out"
|
|
3395
|
+
failures=$((failures + 1))
|
|
3396
|
+
fi
|
|
3397
|
+
|
|
3398
|
+
after_omo="$(hash_path "$REAL_OMO_ROOT")"
|
|
3399
|
+
after_codex="$(hash_path "$REAL_CODEX_CONFIG")"
|
|
3400
|
+
after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3401
|
+
[ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
|
|
3402
|
+
[ "$before_codex" = "$after_codex" ] || failures=$((failures + 1))
|
|
3403
|
+
[ "$before_status" = "$after_status" ] || failures=$((failures + 1))
|
|
3404
|
+
|
|
3405
|
+
printf '{"result":"%s","selfTest":true,"malformedArgsRejected":true,"fakePassRejected":true,"skipRejected":true,"hungCommandBounded":true,"partialStagingCleanedTwice":true,"interruptCleanupRepeated":true,"missingDaemonDistBuildOrderIndependent":true,"cancellationResultFieldsMandatory":%s,"clientPackageResultFieldsMandatory":%s,"installedComponentResultFieldsMandatory":%s,"missingDistBuildOrder":%s,"dirtyWorktreePreserved":%s,"realHomesUnchanged":%s}\n' \
|
|
3406
|
+
"$( [ "$failures" -eq 0 ] && echo PASS || echo FAIL )" \
|
|
3407
|
+
"$cancellation_result_fields" \
|
|
3408
|
+
"$client_package_result_fields" \
|
|
3409
|
+
"$installed_component_result_fields" \
|
|
3410
|
+
"$missing_dist_build_order" \
|
|
3411
|
+
"$( [ "$before_status" = "$after_status" ] && echo true || echo false )" \
|
|
3412
|
+
"$( [ "$before_omo" = "$after_omo" ] && [ "$before_codex" = "$after_codex" ] && echo true || echo false )"
|
|
3413
|
+
cleanup_all || failures=$((failures + 1))
|
|
3414
|
+
NORMAL_CLEANUP_COMPLETE=1
|
|
3415
|
+
[ "$failures" -eq 0 ]
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
run_normal() {
|
|
3419
|
+
require_bins codex node npm bun jq shasum git || return 1
|
|
3420
|
+
prepare_evidence || return 1
|
|
3421
|
+
if [ -n "${LSP_E2E_INTERNAL_FIXTURE:-}" ]; then
|
|
3422
|
+
run_internal_fixture
|
|
3423
|
+
return $?
|
|
3424
|
+
fi
|
|
3425
|
+
|
|
3426
|
+
local real_omo_before real_omo_after real_codex_before real_codex_after worktree_before worktree_after
|
|
3427
|
+
local install_rc plugin_root plugin_count codex_bin
|
|
3428
|
+
local contract_rc driver_rc daemon_pid_file daemon_pid app_server_pid sandbox_removed=false
|
|
3429
|
+
real_omo_before="$(hash_path "$REAL_OMO_ROOT")" || return 1
|
|
3430
|
+
real_codex_before="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
|
|
3431
|
+
worktree_before="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3432
|
+
printf '%s\n' "$worktree_before" >"$EVIDENCE_DIR/worktree-before.txt"
|
|
3433
|
+
REAL_OMO_BEFORE_HASH="$real_omo_before"
|
|
3434
|
+
codex_bin="$(command -v codex)"
|
|
3435
|
+
printf 'real_omo_root=%s before=%s\nreal_codex_config=%s before=%s\n' \
|
|
3436
|
+
"$REAL_OMO_ROOT" "$real_omo_before" "$REAL_CODEX_CONFIG" "$real_codex_before" >"$EVIDENCE_DIR/isolation-receipt.txt"
|
|
3437
|
+
|
|
3438
|
+
SANDBOX_ROOT="$(mktemp -d -t cqa-lsp-e2e.XXXXXX)" || return 1
|
|
3439
|
+
mkdir -p "$SANDBOX_ROOT/codex" "$SANDBOX_ROOT/project" "$SANDBOX_ROOT/home"
|
|
3440
|
+
export HOME="$SANDBOX_ROOT/home"
|
|
3441
|
+
export CODEX_HOME="$SANDBOX_ROOT/codex"
|
|
3442
|
+
export CODEX_LOCAL_BIN_DIR="$CODEX_HOME/bin"
|
|
3443
|
+
export OMO_CODEX_PROJECT="$SANDBOX_ROOT/project"
|
|
3444
|
+
export QA_CWD="$SANDBOX_ROOT/project"
|
|
3445
|
+
export OMO_DISABLE_POSTHOG=1
|
|
3446
|
+
export OMO_CODEX_DISABLE_POSTHOG=1
|
|
3447
|
+
export OMO_LSP_DAEMON_DIR="$SANDBOX_ROOT/omo/lsp-daemon"
|
|
3448
|
+
unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
|
|
3449
|
+
OMO_TEST_ROOT="$OMO_LSP_DAEMON_DIR"
|
|
3450
|
+
APP_SERVER_PID_FILE="$SANDBOX_ROOT/app-server.pid"
|
|
3451
|
+
export QA_SCENARIO="$SCENARIO"
|
|
3452
|
+
|
|
3453
|
+
if [ "$SCENARIO" = "legacy-cleanup" ]; then
|
|
3454
|
+
write_legacy_cleanup_probe
|
|
3455
|
+
run_legacy_cleanup_probe contract || { fail "legacy cleanup contract probe failed (see legacy-cleanup-contract.log)"; return 1; }
|
|
3456
|
+
run_legacy_cleanup_probe setup || { fail "legacy cleanup fixture setup failed (see legacy-cleanup-setup.log)"; return 1; }
|
|
3457
|
+
fi
|
|
3458
|
+
|
|
3459
|
+
with_shared_build_lock "codex-lsp-e2e-build" prepare_daemon_dist || { fail "lsp-daemon prebuild failed (see lsp-daemon-prebuild.log)"; return 1; }
|
|
3460
|
+
write_path_contract_probe "$SANDBOX_ROOT/contract" "$EVIDENCE_DIR/path-contract.json"
|
|
3461
|
+
contract_rc=$?
|
|
3462
|
+
[ "$contract_rc" -eq 0 ] || { fail "path-contract probe failed (see path-contract-probe.log)"; return 1; }
|
|
3463
|
+
if [ "$SCENARIO" = "auth-ownership" ]; then
|
|
3464
|
+
run_auth_ownership_probe || { fail "auth ownership probe failed (see auth-ownership-probe.log)"; return 1; }
|
|
3465
|
+
stop_known_daemon || return 1
|
|
3466
|
+
stop_owned_sandbox_processes || return 1
|
|
3467
|
+
restore_daemon_dist || return 1
|
|
3468
|
+
safe_rm_tree "$SANDBOX_ROOT" || return 1
|
|
3469
|
+
SANDBOX_ROOT=""
|
|
3470
|
+
sandbox_removed=true
|
|
3471
|
+
real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
|
|
3472
|
+
real_codex_after="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
|
|
3473
|
+
worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3474
|
+
printf '%s\n' "$worktree_after" >"$EVIDENCE_DIR/worktree-after.txt"
|
|
3475
|
+
[ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
|
|
3476
|
+
[ "$real_codex_before" = "$real_codex_after" ] || { fail "real Codex config changed"; return 1; }
|
|
3477
|
+
[ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
|
|
3478
|
+
printf 'after=%s unchanged=yes\nreal_codex_after=%s unchanged=yes\n' "$real_omo_after" "$real_codex_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
|
|
3479
|
+
printf 'auth_ownership_probe_complete=true\nisolated_state_removed=%s\n' "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
|
|
3480
|
+
write_auth_ownership_result "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
|
|
3481
|
+
"$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
|
|
3482
|
+
NORMAL_CLEANUP_COMPLETE=1
|
|
3483
|
+
jq -c . "$EVIDENCE_DIR/result.json"
|
|
3484
|
+
return 0
|
|
3485
|
+
fi
|
|
3486
|
+
if [ "$SCENARIO" = "rename" ]; then
|
|
3487
|
+
write_workspace_edit_fixture "$QA_CWD" || return 1
|
|
3488
|
+
run_workspace_edit_contract_probe || { fail "workspace-edit contract probe failed (see workspace-edit-contract-probe.log)"; return 1; }
|
|
3489
|
+
export QA_SOURCE_FILE="source.ts"
|
|
3490
|
+
export QA_SOURCE_PATH="$QA_CWD/source.ts"
|
|
3491
|
+
export QA_RENAME_EVENTS="$EVIDENCE_DIR/rename-server-events.jsonl"
|
|
3492
|
+
elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
|
|
3493
|
+
write_diagnostics_freshness_fixture "$QA_CWD" || return 1
|
|
3494
|
+
run_diagnostics_freshness_contract_probe || { fail "diagnostics freshness contract probe failed (see diagnostics-freshness-contract-probe.log)"; return 1; }
|
|
3495
|
+
export QA_SOURCE_FILE="source.ts"
|
|
3496
|
+
export QA_SOURCE_PATH="$QA_CWD/source.ts"
|
|
3497
|
+
elif [ "$SCENARIO" = "post-edit" ]; then
|
|
3498
|
+
run_post_edit_contract_probe || { fail "post-edit contract probe failed (see post-edit-contract-probe.log)"; return 1; }
|
|
3499
|
+
elif [ "$SCENARIO" = "cancellation" ]; then
|
|
3500
|
+
run_cancellation_contract_probe || { fail "cancellation contract probe failed (see cancellation-contract-probe.log)"; return 1; }
|
|
3501
|
+
elif [ "$SCENARIO" = "client-package" ]; then
|
|
3502
|
+
run_client_package_contract_probe || { fail "client package contract probe failed (see client-package-smoke.log)"; return 1; }
|
|
3503
|
+
fi
|
|
3504
|
+
|
|
3505
|
+
run_bounded 300 "$EVIDENCE_DIR/install.log" node "$REPO_ROOT/packages/omo-codex/scripts/install-local.mjs" install
|
|
3506
|
+
install_rc=$?
|
|
3507
|
+
[ "$install_rc" -eq 0 ] || { fail "local Codex plugin install failed (see install.log)"; return 1; }
|
|
3508
|
+
if [ "$SCENARIO" = "legacy-cleanup" ]; then
|
|
3509
|
+
if ! run_legacy_cleanup_probe verify; then
|
|
3510
|
+
run_legacy_cleanup_probe cleanup || true
|
|
3511
|
+
fail "legacy cleanup live verification failed (see legacy-cleanup-verify.log)"
|
|
3512
|
+
return 1
|
|
3513
|
+
fi
|
|
3514
|
+
run_legacy_cleanup_probe cleanup || { fail "legacy cleanup fixture process cleanup failed"; return 1; }
|
|
3515
|
+
fi
|
|
3516
|
+
grep -q 'omo@sisyphuslabs' "$CODEX_HOME/config.toml" || { fail "OMO plugin was not enabled in isolated config"; return 1; }
|
|
3517
|
+
plugin_count="$(grep -Ec '^\[plugins\."[^]]+"\]$' "$CODEX_HOME/config.toml" || true)"
|
|
3518
|
+
[ "$plugin_count" -eq 1 ] || { fail "isolated config enabled an unexpected plugin count: $plugin_count"; return 1; }
|
|
3519
|
+
plugin_root="$(find "$CODEX_HOME/plugins/cache/sisyphuslabs/omo" -mindepth 1 -maxdepth 1 -type d -print | sort | tail -1)"
|
|
3520
|
+
[ -n "$plugin_root" ] || { fail "installed OMO plugin cache not found"; return 1; }
|
|
3521
|
+
EXPECTED_DAEMON_CLI="$plugin_root/components/lsp-daemon/dist/cli.js"
|
|
3522
|
+
[ -f "$EXPECTED_DAEMON_CLI" ] || { fail "installed daemon CLI missing: $EXPECTED_DAEMON_CLI"; return 1; }
|
|
3523
|
+
rewrite_codex_mcp_config || return 1
|
|
3524
|
+
stamp_codex_lsp_environment "$plugin_root/.mcp.json" || return 1
|
|
3525
|
+
disable_codex_side_effect_hooks "$plugin_root/.codex-plugin/plugin.json" || return 1
|
|
3526
|
+
|
|
3527
|
+
start_mock_model "$EVIDENCE_DIR/mock-model.log" || return 1
|
|
3528
|
+
write_app_server_driver "$SANDBOX_ROOT/app-server-driver.mjs"
|
|
3529
|
+
export APP_SERVER_RESULT="$EVIDENCE_DIR/app-server.json"
|
|
3530
|
+
export APP_SERVER_PID_FILE
|
|
3531
|
+
export CODEX_BIN="$codex_bin"
|
|
3532
|
+
export DEADLINE_MS=120000
|
|
3533
|
+
run_bounded 150 "$EVIDENCE_DIR/app-server-driver.log" node "$SANDBOX_ROOT/app-server-driver.mjs"
|
|
3534
|
+
driver_rc=$?
|
|
3535
|
+
[ "$driver_rc" -eq 0 ] || { fail "Codex app-server LSP drive failed (see app-server-driver.log)"; return 1; }
|
|
3536
|
+
if [ "$SCENARIO" = "diagnostics-freshness" ]; then
|
|
3537
|
+
jq -e '.ok == true and .hookStarted == true and .hookCompleted == true and .lspOperation.ok == true and (.lspOperation.text | contains("exact-current"))' \
|
|
3538
|
+
"$EVIDENCE_DIR/app-server.json" >/dev/null || { fail "app-server evidence failed required assertions"; return 1; }
|
|
3539
|
+
else
|
|
3540
|
+
jq -e '.ok == true and .hookStarted == true and .hookCompleted == true and .lspOperation.ok == true' \
|
|
3541
|
+
"$EVIDENCE_DIR/app-server.json" >/dev/null || { fail "app-server evidence failed required assertions"; return 1; }
|
|
3542
|
+
fi
|
|
3543
|
+
if [ "$SCENARIO" = "installed-component" ]; then
|
|
3544
|
+
stop_known_daemon || return 1
|
|
3545
|
+
run_installed_component_probe "$plugin_root" || { fail "installed component probe failed (see installed-component.log)"; return 1; }
|
|
3546
|
+
fi
|
|
3547
|
+
|
|
3548
|
+
record_daemon_state || return 1
|
|
3549
|
+
daemon_pid_file="$(find_daemon_pid_file)"
|
|
3550
|
+
daemon_pid="$(tr -d '[:space:]' <"$daemon_pid_file")"
|
|
3551
|
+
app_server_pid="$(tr -d '[:space:]' <"$APP_SERVER_PID_FILE" 2>/dev/null || true)"
|
|
3552
|
+
|
|
3553
|
+
stop_mock || return 1
|
|
3554
|
+
stop_known_pid_file "$APP_SERVER_PID_FILE" "app-server" "Codex app-server" || return 1
|
|
3555
|
+
stop_known_daemon || return 1
|
|
3556
|
+
stop_owned_sandbox_processes || return 1
|
|
3557
|
+
kill -0 "$daemon_pid" 2>/dev/null && { fail "daemon pid remained alive after cleanup"; return 1; }
|
|
3558
|
+
if [ -n "$app_server_pid" ]; then
|
|
3559
|
+
kill -0 "$app_server_pid" 2>/dev/null && { fail "app-server pid remained alive after cleanup"; return 1; }
|
|
3560
|
+
fi
|
|
3561
|
+
restore_daemon_dist || return 1
|
|
3562
|
+
safe_rm_tree "$SANDBOX_ROOT" || return 1
|
|
3563
|
+
SANDBOX_ROOT=""
|
|
3564
|
+
sandbox_removed=true
|
|
3565
|
+
|
|
3566
|
+
real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
|
|
3567
|
+
real_codex_after="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
|
|
3568
|
+
worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3569
|
+
printf '%s\n' "$worktree_after" >"$EVIDENCE_DIR/worktree-after.txt"
|
|
3570
|
+
[ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
|
|
3571
|
+
[ "$real_codex_before" = "$real_codex_after" ] || { fail "real Codex config changed"; return 1; }
|
|
3572
|
+
[ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
|
|
3573
|
+
printf 'after=%s unchanged=yes\nreal_codex_after=%s unchanged=yes\n' "$real_omo_after" "$real_codex_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
|
|
3574
|
+
printf 'daemon_pid=%s alive_after=no\napp_server_pid=%s alive_after=no\nmock_model_alive_after=no\nisolated_state_removed=%s\n' \
|
|
3575
|
+
"$daemon_pid" "${app_server_pid:-none}" "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
|
|
3576
|
+
|
|
3577
|
+
write_final_result "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
|
|
3578
|
+
"$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
|
|
3579
|
+
NORMAL_CLEANUP_COMPLETE=1
|
|
3580
|
+
jq -c . "$EVIDENCE_DIR/result.json"
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
run_all_scenarios() {
|
|
3584
|
+
require_bins bash node jq git || return 1
|
|
3585
|
+
prepare_evidence || return 1
|
|
3586
|
+
local scenarios scenario failures=0 before_omo before_codex before_status after_omo after_codex after_status
|
|
3587
|
+
scenarios="path-contract rename diagnostics-freshness legacy-cleanup post-edit cancellation client-package installed-component auth-ownership"
|
|
3588
|
+
before_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
|
|
3589
|
+
before_codex="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
|
|
3590
|
+
before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3591
|
+
printf '%s\n' "$scenarios" >"$EVIDENCE_DIR/all-scenarios.txt"
|
|
3592
|
+
for scenario in $scenarios; do
|
|
3593
|
+
mkdir -p "$EVIDENCE_DIR/$scenario"
|
|
3594
|
+
if ! bash "${BASH_SOURCE[0]}" --scenario "$scenario" --evidence-dir "$EVIDENCE_DIR/$scenario" >"$EVIDENCE_DIR/$scenario.command.log" 2>&1; then
|
|
3595
|
+
failures=$((failures + 1))
|
|
3596
|
+
continue
|
|
3597
|
+
fi
|
|
3598
|
+
jq -e '.result == "PASS" and .scenario != "SKIP"' "$EVIDENCE_DIR/$scenario/result.json" >/dev/null || failures=$((failures + 1))
|
|
3599
|
+
done
|
|
3600
|
+
after_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
|
|
3601
|
+
after_codex="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
|
|
3602
|
+
after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
|
|
3603
|
+
[ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
|
|
3604
|
+
[ "$before_codex" = "$after_codex" ] || failures=$((failures + 1))
|
|
3605
|
+
[ "$before_status" = "$after_status" ] || failures=$((failures + 1))
|
|
3606
|
+
node --input-type=module - "$EVIDENCE_DIR" "$before_omo" "$after_omo" "$before_codex" "$after_codex" "$before_status" "$after_status" <<'NODE'
|
|
3607
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3608
|
+
import { join } from "node:path";
|
|
3609
|
+
const [evidenceDir, omoBefore, omoAfter, codexBefore, codexAfter, statusBefore, statusAfter] = process.argv.slice(2);
|
|
3610
|
+
const scenarios = readFileSync(join(evidenceDir, "all-scenarios.txt"), "utf8").trim().split(/\s+/);
|
|
3611
|
+
const entries = scenarios.map((scenario) => [scenario, JSON.parse(readFileSync(join(evidenceDir, scenario, "result.json"), "utf8"))]);
|
|
3612
|
+
const results = Object.fromEntries(entries);
|
|
3613
|
+
const values = entries.map(([, value]) => value);
|
|
3614
|
+
const payload = {
|
|
3615
|
+
result: values.every((value) => value.result === "PASS") && omoBefore === omoAfter && codexBefore === codexAfter && statusBefore === statusAfter ? "PASS" : "FAIL",
|
|
3616
|
+
scenario: "all",
|
|
3617
|
+
harness: "codex",
|
|
3618
|
+
scenarioOrder: scenarios,
|
|
3619
|
+
scenarioResults: Object.fromEntries(entries.map(([name, value]) => [name, { result: value.result, cleanup: value.cleanup ?? {}, artifacts: value.artifacts ?? {} }])),
|
|
3620
|
+
realOmoRootUnchanged: omoBefore === omoAfter && values.every((value) => value.realOmoRootUnchanged === true),
|
|
3621
|
+
realCodexConfigUnchanged: codexBefore === codexAfter && values.every((value) => value.realCodexConfigUnchanged === true),
|
|
3622
|
+
dirtyWorktreePreserved: statusBefore === statusAfter && values.every((value) => value.dirtyWorktreePreserved === true),
|
|
3623
|
+
noSkip: values.every((value) => value.result !== "SKIP" && value.scenario !== "SKIP"),
|
|
3624
|
+
hookProof: values.every((value) => value.hookStarted === true && value.hookCompleted === true),
|
|
3625
|
+
pathContract: results["path-contract"]?.overrideAssertions ?? null,
|
|
3626
|
+
pairRecovery: results["path-contract"]?.failureFixtures ?? null,
|
|
3627
|
+
auth: results["auth-ownership"]?.authOwnership ?? results["auth-ownership"] ?? null,
|
|
3628
|
+
cancellation: results.cancellation?.cancellationContract ?? null,
|
|
3629
|
+
installedComponent: results["installed-component"]?.installedComponent ?? null,
|
|
3630
|
+
legacyCleanup: results["legacy-cleanup"]?.legacyCleanup ?? null,
|
|
3631
|
+
cleanup: {
|
|
3632
|
+
isolatedStateRemoved: values.every((value) => value.cleanup?.isolatedStateRemoved === true),
|
|
3633
|
+
daemonStopped: values.every((value) => value.cleanup?.daemonStopped === true),
|
|
3634
|
+
},
|
|
3635
|
+
};
|
|
3636
|
+
writeFileSync(join(evidenceDir, "result.json"), `${JSON.stringify(payload, null, 2)}\n`);
|
|
3637
|
+
console.log(JSON.stringify(payload));
|
|
3638
|
+
NODE
|
|
3639
|
+
NORMAL_CLEANUP_COMPLETE=1
|
|
3640
|
+
[ "$failures" -eq 0 ] && jq -e '.result == "PASS" and .noSkip == true' "$EVIDENCE_DIR/result.json" >/dev/null
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
main() {
|
|
3644
|
+
parse_args "$@" || return $?
|
|
3645
|
+
if [ "$SELF_TEST" -eq 1 ]; then
|
|
3646
|
+
run_self_test
|
|
3647
|
+
elif [ "$SCENARIO" = "all" ]; then
|
|
3648
|
+
run_all_scenarios
|
|
3649
|
+
else
|
|
3650
|
+
run_normal
|
|
3651
|
+
fi
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
main "$@"
|