main
John Lauer Keep Codex turns running across editor reloads; fix hidden-tab startup and composer fade in the optional package 4307d94 1mo ago
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'runtime'))
from manage import Integration, KEY, JS_BEGIN
from settings import change, masked


class Settings(unittest.TestCase):
    def test_comments_trailing_commas_and_other_settings_survive(self):
        for text in ['{}', '{ // user comment\n}', '{"x":1, // comment\n}',
                     '{"x": {"list": [1,2,],}, "path":"http://example/*abc*/",}']:
            with self.subTest(text=text):
                updated = change(text, KEY, '/some/cli')
                parsed = json.loads(masked(updated))
                self.assertEqual(parsed[KEY], '/some/cli')
                restored = change(updated, KEY, remove=True)
                self.assertEqual(json.loads(masked(restored)), json.loads(masked(text)))
                self.assertEqual('user comment' in restored, 'user comment' in text)

    def test_duplicate_keys_refused(self):
        with self.assertRaises(ValueError): change('{"x":1,"x":2}', KEY, 'value')


class PackageLifecycle(unittest.TestCase):
    def extension(self, home, version='1.0.0', known=True):
        path = home / f'.local/share/code-server/extensions/openai.chatgpt-{version}-linux-x64'
        (path / 'out').mkdir(parents=True)
        (path / 'webview/assets').mkdir(parents=True)
        (path / 'package.json').write_text(json.dumps({'publisher':'openai','name':'chatgpt','version':version,'main':'out/extension.js'}))
        source = ('class Provider{async initializeWebview(a,b,c,d){a.html=await this.getWebviewContent(a),z.start()}onPanelReady(){}}'
                  if known else 'new upstream implementation')
        (path / 'out/extension.js').write_text(source)
        (path / 'webview/assets/app.css').write_text('.vertical-scroll-fade-mask{mask-image:linear-gradient(black,transparent)}')
        return path, source

    def test_opt_in_updates_and_uninstall_preserve_user_edits(self):
        with tempfile.TemporaryDirectory() as tmp:
            home = Path(tmp); manager = Integration(home)
            extension, original = self.extension(home)
            self.assertEqual(manager.reconcile(), [])
            self.assertEqual((extension / 'out/extension.js').read_text(), original)
            manager.settings.parent.mkdir(parents=True)
            manager.settings.write_text('{ // preferences\n"editor.fontSize": 17,\n}')
            manager.install(isolated=True)
            first = manager.settings.read_text()
            manager.install(isolated=True)
            self.assertEqual(first, manager.settings.read_text())
            self.assertIn(JS_BEGIN, (extension / 'out/extension.js').read_text())
            # Simulate a store update and another tool adding an unrelated patch.
            updated, updated_original = self.extension(home, '2.0.0')
            unknown, unknown_original = self.extension(home, '3.0.0', known=False)
            report = manager.reconcile()
            self.assertEqual(report[-1]['state'], 'unsupported')
            self.assertEqual((unknown / 'out/extension.js').read_text(), unknown_original)
            main = updated / 'out/extension.js'
            main.write_text(main.read_text() + '\n// another tool')
            manager.settings.write_text(change(manager.settings.read_text(), 'editor.fontSize', 22))
            manager.uninstall(isolated=True)
            self.assertFalse(manager.marker.exists())
            self.assertEqual(main.read_text(), updated_original + '\n// another tool')
            self.assertEqual(json.loads(masked(manager.settings.read_text())), {'editor.fontSize':22})
            self.assertEqual(manager.reconcile(), [])
            self.assertNotIn('adom/codex', (updated / 'webview/assets/app.css').read_text())

    def test_custom_cli_and_subsequent_user_changes_are_preserved(self):
        with tempfile.TemporaryDirectory() as tmp:
            manager = Integration(Path(tmp))
            manager.settings.parent.mkdir(parents=True)
            manager.settings.write_text(json.dumps({KEY:'/custom/codex'}))
            self.assertEqual(manager.install(isolated=True)['state'], 'custom_cli_preserved')
            self.assertFalse(manager.marker.exists())
            manager.settings.write_text('{}')
            manager.install(isolated=True)
            manager.settings.write_text(json.dumps({KEY:'/new/user/choice'}))
            manager.uninstall(isolated=True)
            self.assertEqual(json.loads(manager.settings.read_text())[KEY], '/new/user/choice')

    def test_whole_package_installs_with_no_system_dependencies(self):
        with tempfile.TemporaryDirectory() as tmp:
            subprocess.run([sys.executable, str(ROOT/'install.py'), '--home', tmp], check=True, capture_output=True)
            home = Path(tmp)
            self.assertTrue((home/'.local/state/adom/codex-integration/installed.json').exists())
            source = home/'.local/share/adom/codex/runtime/runtime.py'
            # -S excludes site packages: websockets must come from the tarball.
            result = subprocess.run([sys.executable, '-S', str(source), '--runtime-status'],
                                    env=dict(os.environ, HOME=tmp), capture_output=True, text=True)
            self.assertEqual(result.returncode, 0, result.stderr)


if __name__ == '__main__': unittest.main()