172 lines
9.5 KiB
Python
172 lines
9.5 KiB
Python
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
HELPER = ROOT / 'scripts/lib/selection-state.py'
|
|
|
|
|
|
class SelectionTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temporary.cleanup)
|
|
self.home = Path(self.temporary.name)
|
|
self.path = self.home / '.my-ai-configuration/selection.json'
|
|
|
|
def run_state(self, action, *arguments):
|
|
return subprocess.run([sys.executable, str(HELPER), action, '--home', str(self.home),
|
|
'--manifest', str(ROOT / 'adapters/plugins.tsv'), *arguments],
|
|
capture_output=True, text=True, timeout=10)
|
|
|
|
def save(self, *arguments):
|
|
result = self.run_state('write', '--shell', 'powershell', '--flashbang', 'false', '--statusline', 'false', '--client', 'both', *arguments)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
def test_missing_selection_fails_without_creating_state(self):
|
|
result = self.run_state('read')
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn('without Quick', result.stderr)
|
|
self.assertFalse(self.path.exists())
|
|
|
|
def test_selection_round_trip_and_last_successful_write(self):
|
|
self.save('--selected', 'Superpowers', 'Anthropic Frontend Design', '--deselected', 'Context7')
|
|
original = self.path.read_bytes()
|
|
state = json.loads(self.run_state('read').stdout)
|
|
self.assertEqual(state['shell'], 'powershell')
|
|
self.assertEqual(state['client'], 'both')
|
|
self.assertEqual(state['selected'], ['Superpowers', 'Anthropic Frontend Design'])
|
|
self.assertEqual(state['deselected'], ['Context7'])
|
|
self.assertEqual(self.path.read_bytes(), original)
|
|
self.assertIn('selected\tAnthropic Frontend Design', self.run_state('read', '--format', 'tsv').stdout)
|
|
self.save('--selected', 'Context7')
|
|
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], ['Context7'])
|
|
|
|
def test_empty_selection_does_not_select_everything(self):
|
|
self.save()
|
|
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], [])
|
|
|
|
def test_legacy_state_requires_normal_update_and_can_supply_defaults(self):
|
|
self.path.parent.mkdir()
|
|
self.path.write_text(json.dumps(dict(version=1, platform='windows', client='both', selected=[], deselected=[])))
|
|
result = self.run_state('read')
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn('without Quick', result.stderr)
|
|
migrated = json.loads(self.run_state('read', '--allow-legacy').stdout)
|
|
self.assertEqual(migrated['shell'], 'powershell')
|
|
self.assertTrue(migrated['flashbang'])
|
|
self.assertTrue(migrated['statusline'])
|
|
|
|
def test_boolean_options_are_required_and_preserved(self):
|
|
self.save()
|
|
state = json.loads(self.run_state('read').stdout)
|
|
self.assertFalse(state['flashbang'])
|
|
self.assertFalse(state['statusline'])
|
|
state['flashbang'] = 'false'
|
|
self.path.write_text(json.dumps(state))
|
|
self.assertNotEqual(self.run_state('read').returncode, 0)
|
|
|
|
def test_semantic_retrieval_option_round_trips(self):
|
|
result = self.run_state('write', '--shell', 'bash', '--client', 'both',
|
|
'--flashbang', 'true', '--statusline', 'true', '--semantic-retrieval', 'true')
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
state = json.loads(self.run_state('read').stdout)
|
|
self.assertTrue(state['semantic_retrieval'])
|
|
self.assertTrue(state['statusline'])
|
|
self.assertIn('semantic_retrieval\ttrue', self.run_state('read', '--format', 'tsv').stdout)
|
|
|
|
def test_legacy_skill_profile_is_ignored(self):
|
|
self.save()
|
|
state = json.loads(self.run_state('read').stdout)
|
|
state['skill_profile'] = 'core'
|
|
self.path.write_text(json.dumps(state))
|
|
self.assertNotIn('skill_profile', json.loads(self.run_state('read').stdout))
|
|
self.assertNotIn('skill_profile', self.run_state('read', '--format', 'tsv').stdout)
|
|
|
|
def test_invalid_write_preserves_last_selection(self):
|
|
self.save('--selected', 'Superpowers')
|
|
original = self.path.read_bytes()
|
|
result = self.run_state('write', '--shell', 'bash', '--flashbang', 'true', '--client', 'codex', '--selected', 'Unknown')
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual(self.path.read_bytes(), original)
|
|
|
|
def test_corrupt_or_conflicting_state_is_rejected(self):
|
|
self.path.parent.mkdir()
|
|
for state in ('{', '[]', json.dumps(dict(version=1, platform='windows', client='both',
|
|
selected=['Context7'], deselected=['Context7']))):
|
|
self.path.write_text(state)
|
|
self.assertNotEqual(self.run_state('read').returncode, 0)
|
|
|
|
def test_removed_plugins_are_ignored_and_new_plugins_not_selected(self):
|
|
self.save('--selected', 'Superpowers')
|
|
state = json.loads(self.path.read_text())
|
|
state['selected'].append('Removed plugin')
|
|
self.path.write_text(json.dumps(state))
|
|
self.assertEqual(json.loads(self.run_state('read').stdout)['selected'], ['Superpowers'])
|
|
|
|
def test_powershell_quick_preview_uses_saved_selection_without_writes(self):
|
|
shell = shutil.which('powershell') or shutil.which('pwsh')
|
|
if not shell:
|
|
self.skipTest('PowerShell unavailable')
|
|
environment = dict(os.environ, TEST_SELECTION_HOME=str(self.home), TEST_REPOSITORY_ROOT=str(ROOT))
|
|
save = subprocess.run([shell, '-NoProfile', '-Command',
|
|
"$ErrorActionPreference = 'Stop'; . (Join-Path $env:TEST_REPOSITORY_ROOT 'scripts/lib/selection-state.ps1'); "
|
|
"Save-UpdateSelection -HomePath $env:TEST_SELECTION_HOME -RepositoryRoot $env:TEST_REPOSITORY_ROOT "
|
|
"-Shell PowerShell -Client Both -Flashbang $false -StatusLine $false -Plugins @{ Selected = @([pscustomobject]@{name='Superpowers'}); Deselected = @() }"],
|
|
env=environment, capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
|
|
original = self.path.read_bytes()
|
|
source = (ROOT / 'scripts/commands/update.ps1').read_text(encoding='utf-8-sig')
|
|
source = source.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_SELECTION_HOME')
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.tmp.ps1', dir=ROOT / 'scripts/commands',
|
|
encoding='utf-8', delete=False) as stream:
|
|
stream.write(source)
|
|
entry = Path(stream.name)
|
|
self.addCleanup(entry.unlink)
|
|
result = subprocess.run([shell, '-NoProfile', '-File', str(entry), '-Quick', '-DryRun'],
|
|
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertNotIn('Select plugins', result.stdout)
|
|
self.assertIn('DRYRUN ensure codex plugin: Superpowers', result.stdout)
|
|
self.assertNotIn('DRYRUN ensure codex plugin: Context7', result.stdout)
|
|
self.assertEqual(self.path.read_bytes(), original)
|
|
self.assertFalse((self.home / '.codex').exists())
|
|
failed = subprocess.run([shell, '-NoProfile', '-File', str(entry), '-Quick'], env=environment,
|
|
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=10)
|
|
self.assertNotEqual(failed.returncode, 0)
|
|
self.assertEqual(self.path.read_bytes(), original)
|
|
|
|
def test_bash_quick_preview_uses_saved_selection_without_writes(self):
|
|
shell = shutil.which('bash')
|
|
if os.name == 'nt':
|
|
candidate = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe'
|
|
shell = str(candidate) if candidate.exists() else None
|
|
if not shell:
|
|
self.skipTest('Bash unavailable')
|
|
save = subprocess.run([shell, '-c',
|
|
'set -euo pipefail; root=$1; home_path=$2; shell=powershell; client=both; flashbang=false; statusline=false; '
|
|
'selected_plugins=(Superpowers); deselected_plugins=(); '
|
|
'. "$root/scripts/lib/selection-state.sh"; save_update_selection',
|
|
'test', ROOT.as_posix(), self.home.as_posix()],
|
|
capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(save.returncode, 0, save.stdout + save.stderr)
|
|
original = self.path.read_bytes()
|
|
environment = dict(os.environ, HOME=self.home.as_posix())
|
|
result = subprocess.run([shell, str(ROOT / 'scripts/commands/update.sh'), '--quick', '--dry-run'],
|
|
env=environment, stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=90)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
self.assertNotIn('Select plugins', result.stdout)
|
|
self.assertIn('DRYRUN ensure codex plugin: Superpowers', result.stdout)
|
|
self.assertNotIn('DRYRUN ensure codex plugin: Context7', result.stdout)
|
|
self.assertEqual(self.path.read_bytes(), original)
|
|
self.assertFalse((self.home / '.codex').exists())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(argv=[sys.argv[0]], verbosity=1)
|