fix(installer): preserve healthy Python runtime and Codex connections
This commit is contained in:
1 parent
4924781e2d
commit
049c4d1338
8 files changed
+130
-5
No files matched your search
@@ -35,6 +35,8 @@ jobs:
|
||||
"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_protected_instruction_unattended.py tests/tools/test_file_write_safety.py tests/tools/test_computer_use_approval_isolation.py tests/cli/test_cli_approval_ui.py tests/cli/test_single_query_session_finalize.py -q -p no:cacheprovider -rf
|
||||
- name: Codex bridge regression tests
|
||||
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge'
|
||||
- name: Managed Python update preservation
|
||||
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/test_install_ps1_reuse_healthy_venv.py tests/test_install_ps1_python_fallback_venv.py -q -p no:cacheprovider -rf'
|
||||
- name: Project scoped and bounded search regressions
|
||||
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_search_project_scope.py tests/tools/test_search_files_cpu_windows.py tests/tools/test_search_zero_match_and_multipath.py tests/tools/test_search_giant_line_containment.py tests/tools/test_search_budget_truncation.py -q -p no:cacheprovider -rf'
|
||||
- name: Native watch and speech mux pytest regressions
|
||||
|
||||
@@ -35,7 +35,7 @@ import gecis_context
|
||||
import terminal_stream
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SURUM = "17" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
SURUM = "18" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
|
||||
|
||||
@@ -40,7 +40,7 @@ from bekci_watch import MetadataFilter
|
||||
# Installed only by owned native launcher, never an HTTP control endpoint.
|
||||
WATCH_OWNER = None
|
||||
|
||||
SURUM = "17"
|
||||
SURUM = "18"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
|
||||
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
|
||||
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
|
||||
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
|
||||
// another version is restarted so an updated IDE never keeps an old bridge alive.
|
||||
export const CODEX_BRIDGE_VERSION = '17'
|
||||
export const CODEX_BRIDGE_VERSION = '18'
|
||||
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
|
||||
|
||||
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aiturk-ide",
|
||||
"productName": "AITURK IDE",
|
||||
"private": true,
|
||||
"version": "1.0.0-beta.36",
|
||||
"version": "1.0.0-beta.37",
|
||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||
"author": "AITURK / TurkServis",
|
||||
"repository": {
|
||||
|
||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "aiturk-ide",
|
||||
"version": "1.0.0-beta.36",
|
||||
"version": "1.0.0-beta.37",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@assistant-ui/core": "0.2.23",
|
||||
|
||||
@@ -2550,6 +2550,39 @@ function Install-Repository {
|
||||
Write-Success "Repository ready"
|
||||
}
|
||||
|
||||
function Test-ReusableManagedVenv {
|
||||
param([string]$VenvPath, [string]$BasePython, [string]$Version)
|
||||
$pythonExe = Join-Path $VenvPath "Scripts\python.exe"
|
||||
if (-not (Test-Path -LiteralPath $pythonExe -PathType Leaf)) { return $false }
|
||||
$process = New-Object System.Diagnostics.Process
|
||||
try {
|
||||
$info = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$info.FileName = $pythonExe
|
||||
$info.Arguments = '-I -c "import json,sys,ssl,ctypes,venv,encodings; print(json.dumps(dict(version=str(sys.version_info.major)+''.''+str(sys.version_info.minor),prefix=sys.prefix,base=sys._base_executable)))"'
|
||||
$info.UseShellExecute = $false
|
||||
$info.CreateNoWindow = $true
|
||||
$info.RedirectStandardOutput = $true
|
||||
$info.RedirectStandardError = $true
|
||||
$process.StartInfo = $info
|
||||
if (-not $process.Start()) { return $false }
|
||||
$stdout = $process.StandardOutput.ReadToEndAsync()
|
||||
$stderr = $process.StandardError.ReadToEndAsync()
|
||||
if (-not $process.WaitForExit(15000)) {
|
||||
try { $process.Kill() } catch { }
|
||||
return $false
|
||||
}
|
||||
if ($process.ExitCode -ne 0) { return $false }
|
||||
$health = $stdout.Result | ConvertFrom-Json
|
||||
return ($health.version -eq $Version -and
|
||||
[string]::Equals([IO.Path]::GetFullPath($health.prefix), [IO.Path]::GetFullPath($VenvPath), [StringComparison]::OrdinalIgnoreCase) -and
|
||||
[string]::Equals([IO.Path]::GetFullPath($health.base), [IO.Path]::GetFullPath($BasePython), [StringComparison]::OrdinalIgnoreCase))
|
||||
} catch {
|
||||
return $false
|
||||
} finally {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Venv {
|
||||
if ($NoVenv) {
|
||||
Write-Info "Skipping virtual environment (-NoVenv)"
|
||||
@@ -2567,6 +2600,19 @@ function Install-Venv {
|
||||
throw "Hermes-managed Python is unavailable. Run install.ps1 -Stage python first."
|
||||
}
|
||||
|
||||
# Routine source updates do not require replacing a healthy interpreter.
|
||||
# Recreating it kills detached Codex bridges sharing this venv and leaves
|
||||
# every subscription model disconnected until bootstrap finishes. Verify
|
||||
# version, provenance and stdlib first; the dependency stage still performs
|
||||
# the locked sync and import checks. Broken/mismatched runtimes retain the
|
||||
# transactional repair path below.
|
||||
$existingVenvPath = Join-Path $InstallDir "venv"
|
||||
if (Test-ReusableManagedVenv -VenvPath $existingVenvPath -BasePython $resolvedPython.Path -Version $resolvedPython.Version) {
|
||||
$env:UV_PYTHON = Join-Path $existingVenvPath "Scripts\python.exe"
|
||||
Write-Success "Reusing healthy managed virtual environment; running Codex bridges preserved"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Info "Creating virtual environment with Python $($resolvedPython.Version)..."
|
||||
|
||||
Push-Location $InstallDir
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Run the actual installer functions against a real disposable Python venv."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[1] / 'scripts/install.ps1'
|
||||
PS = shutil.which('powershell') or shutil.which('pwsh')
|
||||
pytestmark = pytest.mark.skipif(os.name != 'nt' or not PS, reason='Windows installer runtime test')
|
||||
|
||||
|
||||
def function(source, name):
|
||||
start = source.index('function ' + name + ' {')
|
||||
opening = source.index('{', start)
|
||||
depth = 0
|
||||
for index in range(opening, len(source)):
|
||||
if source[index] == '{': depth += 1
|
||||
elif source[index] == '}': depth -= 1
|
||||
if depth == 0: return source[start:index + 1]
|
||||
raise AssertionError(name)
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def runtime(tmp_path_factory):
|
||||
root = tmp_path_factory.mktemp('installer-reuse')
|
||||
subprocess.run([sys.executable, '-m', 'venv', '--without-pip', str(root / 'venv')], check=True)
|
||||
return root
|
||||
|
||||
|
||||
def run(runtime, body):
|
||||
text = SOURCE.read_text(encoding='utf-8-sig')
|
||||
script = runtime / 'probe.ps1'
|
||||
prefix = "$ErrorActionPreference = 'Stop'\n"
|
||||
prefix += function(text, 'Test-ReusableManagedVenv') + '\n'
|
||||
prefix += function(text, 'Install-Venv') + '\n'
|
||||
prefix += "$InstallDir = '" + str(runtime).replace("'", "''") + "'\n"
|
||||
prefix += "$BasePython = '" + sys._base_executable.replace("'", "''") + "'\n"
|
||||
prefix += "$Version = '" + str(sys.version_info.major) + '.' + str(sys.version_info.minor) + "'\n"
|
||||
script.write_text(prefix + body, encoding='utf-8-sig')
|
||||
result = subprocess.run([PS, '-NoProfile', '-File', str(script)], capture_output=True, text=True, timeout=40)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
return result.stdout
|
||||
|
||||
|
||||
def test_healthy_update_keeps_live_python_and_never_reaches_kill_sweep(runtime):
|
||||
python = runtime / 'venv/Scripts/python.exe'
|
||||
live = subprocess.Popen([str(python), '-c', 'import time;time.sleep(90)'])
|
||||
try:
|
||||
output = run(runtime, r'''
|
||||
function Resolve-AvailablePythonVersion { return @{Path=$BasePython;Version=$Version} }
|
||||
function Write-Success { param($Message); Write-Output $Message }
|
||||
function Write-Info { throw 'Unexpected venv recreation/kill path' }
|
||||
function taskkill { throw 'Live process must not be killed' }
|
||||
Install-Venv
|
||||
''')
|
||||
assert 'Reusing healthy' in output
|
||||
assert live.poll() is None
|
||||
assert python.is_file()
|
||||
finally:
|
||||
live.terminate();live.wait(timeout=10)
|
||||
|
||||
|
||||
def test_wrong_version_or_foreign_interpreter_does_not_reuse(runtime):
|
||||
output = run(runtime, r'''
|
||||
$venvPath = Join-Path $InstallDir 'venv'
|
||||
$results = @(
|
||||
(Test-ReusableManagedVenv $venvPath $BasePython '0.0'),
|
||||
(Test-ReusableManagedVenv $venvPath (Join-Path $InstallDir 'other-python.exe') $Version),
|
||||
(Test-ReusableManagedVenv (Join-Path $InstallDir 'missing') $BasePython $Version)
|
||||
)
|
||||
ConvertTo-Json -Compress -InputObject $results
|
||||
''')
|
||||
assert json.loads(output) == [False, False, False]
|
||||
Reference in new issue
Block a user