refactor(scripts): organize script entry points
This commit is contained in:
1 parent
793eee5b61
commit
11c2b6dfcb
41 files changed
+102
-101
No files matched your search
@@ -0,0 +1,68 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-build-validation-" + [Guid]::NewGuid())
|
||||
New-Item $work -ItemType Directory -Force | Out-Null
|
||||
|
||||
function New-RepoCopy {
|
||||
$copy = Join-Path $work ("case-" + [Guid]::NewGuid())
|
||||
New-Item $copy -ItemType Directory -Force | Out-Null
|
||||
Get-ChildItem $root -Force | Where-Object { $_.Name -notin @('.git','generated') } | ForEach-Object {
|
||||
Copy-Item $_.FullName (Join-Path $copy $_.Name) -Recurse -Force
|
||||
}
|
||||
return $copy
|
||||
}
|
||||
|
||||
function Test-BuildFails([string]$Label, [scriptblock]$Mutate) {
|
||||
$copy = New-RepoCopy
|
||||
& $Mutate $copy
|
||||
$failed = $false
|
||||
try { & (Join-Path $copy 'scripts/commands/build.ps1') 2>&1 | Out-Null }
|
||||
catch { $failed = $true }
|
||||
Remove-Item $copy -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (-not $failed) { throw "Expected build to fail for case '$Label' but it succeeded." }
|
||||
}
|
||||
|
||||
$baseline = New-RepoCopy
|
||||
& (Join-Path $baseline 'scripts/commands/build.ps1') -Summary | Out-Null
|
||||
|
||||
Test-BuildFails 'invalid Claude settings.json' {
|
||||
param($copy)
|
||||
Add-Content (Join-Path $copy 'adapters/claude/config/settings.json') '} this is not json {'
|
||||
}
|
||||
|
||||
Test-BuildFails 'duplicate agent name' {
|
||||
param($copy)
|
||||
$architect = Join-Path $copy 'shared/agents/architect/agent.yml'
|
||||
(Get-Content $architect -Raw) -replace 'name:\s*\S+', 'name: implementer' | Set-Content $architect -NoNewline
|
||||
}
|
||||
|
||||
Test-BuildFails 'unknown Claude tool name in capability manifest' {
|
||||
param($copy)
|
||||
$manifest = Join-Path $copy 'adapters/claude/capabilities.tsv'
|
||||
$lines = Get-Content $manifest | ForEach-Object { if ($_ -match '^architect\t') { "architect`tShell" } else { $_ } }
|
||||
Set-Content $manifest $lines
|
||||
}
|
||||
|
||||
Test-BuildFails 'duplicate plugin name' {
|
||||
param($copy)
|
||||
$manifest = Join-Path $copy 'adapters/plugins.tsv'
|
||||
$lines = Get-Content $manifest
|
||||
Add-Content $manifest $lines[1]
|
||||
}
|
||||
|
||||
Test-BuildFails 'leftover template placeholder' {
|
||||
param($copy)
|
||||
Add-Content (Join-Path $copy 'shared/global-instructions.md') "`n__NOT_A_REAL_PLACEHOLDER__"
|
||||
}
|
||||
|
||||
Test-BuildFails 'unknown placeholder beside an allowed installer placeholder' {
|
||||
param($copy)
|
||||
$path = Join-Path $copy 'adapters/claude/config/settings.json'
|
||||
(Get-Content $path -Raw).Replace('__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', '__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY____UNKNOWN_PLACEHOLDER__') | Set-Content $path
|
||||
}
|
||||
|
||||
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if ($Summary) { Write-Output 'Tests: PASS | build validation' } else { Write-Output 'PASS build validation: invalid JSON, duplicate agent, unknown Claude tool name, duplicate plugin, and leftover placeholders are all rejected' }
|
||||
exit 0
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
summary=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--summary) summary=true; shift ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
work=$(mktemp -d)
|
||||
cleanup() { rm -rf -- "$work"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
new_repo_copy() {
|
||||
local copy
|
||||
copy=$(mktemp -d "$work/case-XXXXXX")
|
||||
(cd "$root" && tar -cf - --exclude='.git' --exclude='generated' .) | (cd "$copy" && tar -xf -)
|
||||
printf '%s' "$copy"
|
||||
}
|
||||
|
||||
test_build_fails() {
|
||||
local label=$1 mutate_fn=$2 copy
|
||||
copy=$(new_repo_copy)
|
||||
"$mutate_fn" "$copy"
|
||||
if bash "$copy/scripts/commands/build.sh" >/dev/null 2>&1; then
|
||||
printf 'Expected build to fail for case "%s" but it succeeded.\n' "$label" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf -- "$copy"
|
||||
}
|
||||
|
||||
baseline=$(new_repo_copy)
|
||||
bash "$baseline/scripts/commands/build.sh" --summary >/dev/null
|
||||
|
||||
mutate_invalid_json() {
|
||||
printf '} this is not json {' >> "$1/adapters/claude/config/settings.json"
|
||||
}
|
||||
test_build_fails 'invalid Claude settings.json' mutate_invalid_json
|
||||
|
||||
mutate_duplicate_agent() {
|
||||
sed -i 's/^name:.*/name: implementer/' "$1/shared/agents/architect/agent.yml"
|
||||
}
|
||||
test_build_fails 'duplicate agent name' mutate_duplicate_agent
|
||||
|
||||
mutate_invalid_tool_name() {
|
||||
sed -i 's/^architect\t.*/architect\tShell/' "$1/adapters/claude/capabilities.tsv"
|
||||
}
|
||||
test_build_fails 'unknown Claude tool name in capability manifest' mutate_invalid_tool_name
|
||||
|
||||
mutate_duplicate_plugin() {
|
||||
local manifest="$1/adapters/plugins.tsv"
|
||||
sed -n '2p' "$manifest" >> "$manifest"
|
||||
}
|
||||
test_build_fails 'duplicate plugin name' mutate_duplicate_plugin
|
||||
|
||||
mutate_leftover_placeholder() {
|
||||
printf '\n__NOT_A_REAL_PLACEHOLDER__\n' >> "$1/shared/global-instructions.md"
|
||||
}
|
||||
test_build_fails 'leftover template placeholder' mutate_leftover_placeholder
|
||||
|
||||
mutate_mixed_placeholders() {
|
||||
sed -i 's/__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__/__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__ __UNKNOWN_PLACEHOLDER__/' "$1/adapters/claude/config/settings.json"
|
||||
}
|
||||
test_build_fails 'unknown placeholder beside an allowed installer placeholder' mutate_mixed_placeholders
|
||||
|
||||
if [ "$summary" = true ]; then printf 'Tests: PASS | build validation\n'; else printf 'PASS build validation: invalid JSON, duplicate agent, unknown Claude tool name, duplicate plugin, and leftover placeholders are all rejected\n'; fi
|
||||
@@ -0,0 +1,24 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $root 'scripts/lib/manifest.ps1')
|
||||
. (Join-Path $root 'scripts/lib/install-targets.ps1')
|
||||
|
||||
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-install-guard-test-" + [Guid]::NewGuid())
|
||||
try {
|
||||
$installed = Join-Path $work 'installed'
|
||||
$missing = Join-Path $work 'missing'
|
||||
New-Item $installed -ItemType Directory -Force | Out-Null
|
||||
New-Item (Get-ManagedManifestPath $installed) -ItemType File -Force | Out-Null
|
||||
|
||||
if (-not (Test-AnyManifestPresent @($installed, $missing))) { throw 'Expected an installed destination to be detected.' }
|
||||
if (Test-AnyManifestPresent @($missing)) { throw 'A destination without a manifest must not count as installed.' }
|
||||
if (Test-AllManifestsPresent @($installed, $missing)) { throw 'Update guard must fail when any selected destination is not installed.' }
|
||||
if (-not (Test-AllManifestsPresent @($installed))) { throw 'Update guard must pass when every selected destination is installed.' }
|
||||
|
||||
if ($Summary) { Write-Output 'Tests: PASS | install guards' } else { Write-Output 'PASS install guards: already-installed and not-installed detection' }
|
||||
} finally {
|
||||
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
exit 0
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
summary=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--summary) summary=true; shift ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
. "$root/scripts/lib/manifest.sh"
|
||||
. "$root/scripts/lib/install-targets.sh"
|
||||
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf -- "$work"' EXIT
|
||||
|
||||
installed="$work/installed"
|
||||
missing="$work/missing"
|
||||
mkdir -p "$installed"
|
||||
: > "$(manifest_path "$installed")"
|
||||
|
||||
any_manifest_present "$installed" "$missing" || { printf 'Expected an installed destination to be detected.\n' >&2; exit 1; }
|
||||
! any_manifest_present "$missing" || { printf 'A destination without a manifest must not count as installed.\n' >&2; exit 1; }
|
||||
! all_manifests_present "$installed" "$missing" || { printf 'Update guard must fail when any selected destination is not installed.\n' >&2; exit 1; }
|
||||
all_manifests_present "$installed" || { printf 'Update guard must pass when every selected destination is installed.\n' >&2; exit 1; }
|
||||
|
||||
if [ "$summary" = true ]; then printf 'Tests: PASS | install guards\n'; else printf 'PASS install guards: already-installed and not-installed detection\n'; fi
|
||||
@@ -0,0 +1,208 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location('install_options', ROOT / 'scripts/lib/install-options.py')
|
||||
OPTIONS = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(OPTIONS)
|
||||
|
||||
|
||||
class InstallOptionsTests(unittest.TestCase):
|
||||
def test_filter_preserves_other_claude_hooks_and_status(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'settings.json'
|
||||
settings = {'hooks': {'Stop': [{'hooks': [
|
||||
{'command': 'bash "root/hooks/flashbang.sh"'}, {'command': 'echo keep'}]}],
|
||||
'PreCompact': [{'hooks': [{'command': 'echo compact'}]}]},
|
||||
'statusLine': {'command': 'echo status'}}
|
||||
path.write_text(json.dumps(settings))
|
||||
result = json.loads(OPTIONS.filter_flashbang(path))
|
||||
self.assertEqual(result['hooks']['Stop'][0]['hooks'], [{'command': 'echo keep'}])
|
||||
self.assertEqual(result['hooks']['PreCompact'], settings['hooks']['PreCompact'])
|
||||
self.assertEqual(result['statusLine'], settings['statusLine'])
|
||||
|
||||
def test_filter_preserves_other_codex_stop_hooks_and_settings(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'config.toml'
|
||||
path.write_text('approval_policy = "on-request"\n[[hooks.Stop]]\n'
|
||||
'[[hooks.Stop.hooks]]\ncommand = \'bash "root/flashbang.sh"\'\n'
|
||||
'[[hooks.Stop.hooks]]\ncommand = "echo keep"\n'
|
||||
'[tui]\nstatus_line = ["model"]\n')
|
||||
result = tomllib.loads(OPTIONS.filter_flashbang(path))
|
||||
self.assertEqual(result['hooks']['Stop'][0]['hooks'], [{'command': 'echo keep'}])
|
||||
self.assertEqual(result['tui']['status_line'], ['model'])
|
||||
|
||||
def test_native_option_helpers_select_clients_and_restore_environment(self):
|
||||
shells = [('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash'))]
|
||||
for kind, executable in shells:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=kind):
|
||||
if kind == 'powershell':
|
||||
command = [executable, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/install-options.ps1; "
|
||||
"function Test-CodexProcessActive { $false }; function Test-GlobalNpmCodex { $false }; "
|
||||
"function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) "
|
||||
"Write-Output ($Command + ':' + ($Arguments -join ' ')) }; "
|
||||
"$env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE='original'; "
|
||||
"Update-SelectedAgentClis -Client Both; "
|
||||
"if ($env:CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE -ne 'original') { throw 'Environment leaked' }"]
|
||||
else:
|
||||
command = [executable, '-c',
|
||||
'. ./scripts/lib/install-options.sh; '
|
||||
'codex_process_active() { return 1; }; global_npm_codex() { return 1; }; '
|
||||
'run_plugin_command() { shift; printf "%s:%s\\n" "$1" "$2"; }; '
|
||||
'client=both; dry_run=false; summary=false; '
|
||||
'CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=original; update_selected_agent_clis; '
|
||||
'[ "$CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE" = original ]']
|
||||
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(), ['codex:update', 'claude:update'])
|
||||
|
||||
def test_running_codex_is_skipped_and_npm_install_is_used_when_available(self):
|
||||
shell = shutil.which('powershell') or shutil.which('pwsh')
|
||||
if shell:
|
||||
skipped = subprocess.run([
|
||||
shell, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/install-options.ps1; "
|
||||
"function Test-CodexProcessActive { $true }; function Test-GlobalNpmCodex { $true }; "
|
||||
"function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) Write-Output $Command }; "
|
||||
"Update-SelectedAgentClis -Client Both"
|
||||
], cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(skipped.returncode, 0, skipped.stdout + skipped.stderr)
|
||||
self.assertEqual(skipped.stdout.splitlines()[-1], 'claude')
|
||||
self.assertIn('Codex CLI update skipped', skipped.stdout + skipped.stderr)
|
||||
npm = subprocess.run([
|
||||
shell, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/install-options.ps1; "
|
||||
"function Test-CodexProcessActive { $false }; function Test-GlobalNpmCodex { $true }; "
|
||||
"function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) "
|
||||
"Write-Output ($Command + ':' + ($Arguments -join ' ')) }; Update-SelectedAgentClis -Client Codex"
|
||||
], cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(npm.returncode, 0, npm.stdout + npm.stderr)
|
||||
self.assertEqual(npm.stdout.strip(), 'npm:install -g @openai/codex@latest')
|
||||
|
||||
bash = (str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash'))
|
||||
if bash and Path(bash).exists():
|
||||
result = subprocess.run([
|
||||
bash, '-c',
|
||||
'. ./scripts/lib/install-options.sh; codex_process_active() { return 0; }; '
|
||||
'global_npm_codex() { return 0; }; run_plugin_command() { shift; printf "%s\\n" "$1"; }; '
|
||||
'client=both; dry_run=false; summary=false; update_selected_agent_clis'
|
||||
], cwd=ROOT, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(), ['claude'])
|
||||
self.assertIn('Codex CLI update skipped', result.stderr)
|
||||
|
||||
def test_flashbang_toggle_is_applied_by_both_manifest_writers(self):
|
||||
shells = [('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash'))]
|
||||
for kind, executable in shells:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=kind), tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory) / 'source'
|
||||
source.mkdir()
|
||||
destination = Path(directory) / 'destination'
|
||||
settings = {'hooks': {'Stop': [{'hooks': [{'command': 'bash "__AI_CONFIG_ROOT__/flashbang.sh"'}]}],
|
||||
'PreCompact': [{'hooks': [{'command': 'echo keep'}]}]}}
|
||||
(source / 'settings.json').write_text(json.dumps(settings))
|
||||
(source / 'config.toml').write_text('[[hooks.Stop]]\n[[hooks.Stop.hooks]]\n'
|
||||
'command = \'bash "__AI_CONFIG_ROOT__/flashbang.sh"\'\n')
|
||||
environment = dict(os.environ, OPTION_SOURCE=str(source), OPTION_DESTINATION=str(destination))
|
||||
for enabled in (False, True):
|
||||
if kind == 'powershell':
|
||||
command = [executable, '-NoProfile', '-Command',
|
||||
"$ErrorActionPreference='Stop'; . ./scripts/lib/manifest.ps1; "
|
||||
"Sync-ManagedDestination -Source $env:OPTION_SOURCE -Destination $env:OPTION_DESTINATION "
|
||||
"-Stamp test -AiConfigRoot root -ShellCommand bash -PowerShellCommand pwsh "
|
||||
"-Summary -FlashbangEnabled $" + str(enabled).lower()]
|
||||
else:
|
||||
command = [executable, '-c',
|
||||
'set -euo pipefail; . ./scripts/lib/manifest.sh; '
|
||||
'sync_managed_destination "$OPTION_SOURCE" "$OPTION_DESTINATION" test root bash pwsh false true '
|
||||
+ str(enabled).lower()]
|
||||
environment.update(OPTION_SOURCE=source.as_posix(), OPTION_DESTINATION=destination.as_posix())
|
||||
result = subprocess.run(command, cwd=ROOT, env=environment, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
installed = json.loads((destination / 'settings.json').read_text(encoding='utf-8-sig'))
|
||||
self.assertEqual('Stop' in installed['hooks'], enabled)
|
||||
self.assertIn('PreCompact', installed['hooks'])
|
||||
codex = tomllib.loads((destination / 'config.toml').read_text(encoding='utf-8-sig'))
|
||||
self.assertEqual(bool(codex.get('hooks', {}).get('Stop')), enabled)
|
||||
self.assertTrue((destination / '.ai-config-manifest.tsv').is_file())
|
||||
|
||||
def test_entrypoints_save_normal_choices_and_replay_quick_without_prompts(self):
|
||||
executables = [('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash'))]
|
||||
for kind, executable in executables:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=kind), tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory) / 'repo'
|
||||
scripts = root / 'scripts'
|
||||
shutil.copytree(ROOT / 'scripts/lib', scripts / 'lib', ignore=shutil.ignore_patterns('__pycache__'))
|
||||
home = Path(directory) / 'home'
|
||||
home.mkdir()
|
||||
(root / 'adapters').mkdir()
|
||||
shutil.copyfile(ROOT / 'adapters/plugins.tsv', root / 'adapters/plugins.tsv')
|
||||
for client in ('codex', 'claude'):
|
||||
package = root / 'generated' / (client + '-bash')
|
||||
(package / 'skills').mkdir(parents=True)
|
||||
(package / 'skills' / 'test.txt').write_text('managed')
|
||||
config = ('[[hooks.Stop]]\n[[hooks.Stop.hooks]]\ncommand = \'bash "__AI_CONFIG_ROOT__/flashbang.sh"\'\n'
|
||||
if client == 'codex' else '{"hooks":{"Stop":[{"hooks":[{"command":"bash flashbang.sh"}]}]}}')
|
||||
(package / ('config.toml' if client == 'codex' else 'settings.json')).write_text(config)
|
||||
suffix = 'ps1' if kind == 'powershell' else 'sh'
|
||||
for name in ('install', 'update'):
|
||||
content = (ROOT / 'scripts' / (name + '.' + suffix)).read_text(encoding='utf-8-sig')
|
||||
content = content.replace("[Environment]::GetFolderPath('UserProfile')", '$env:TEST_INSTALL_HOME')
|
||||
(scripts / (name + '.' + suffix)).write_text(content)
|
||||
(scripts / ('build.' + suffix)).write_text('param([switch]$Summary)' if kind == 'powershell' else 'exit 0\n')
|
||||
if kind == 'powershell':
|
||||
(scripts / 'lib/plugins.ps1').write_text(
|
||||
'function Select-ConfiguredPlugins { @{Selected=@(); Deselected=@()} }\n'
|
||||
'function Get-ConfiguredPluginEntries { @() }\n'
|
||||
'function Sync-ConfiguredPlugins {}\n'
|
||||
'function Invoke-PluginCommand { param($Command,$Arguments,[switch]$DryRun,[switch]$Summary) '
|
||||
'Write-Output "CLI-MOCK $Command" }\n')
|
||||
else:
|
||||
(scripts / 'lib/plugins.sh').write_text(
|
||||
'select_configured_plugins() { :; }\nsync_configured_plugins() { :; }\n'
|
||||
'run_plugin_command() { printf "CLI-MOCK %s\\n" "$2"; }\n')
|
||||
environment = dict(os.environ, HOME=home.as_posix(), TEST_INSTALL_HOME=str(home))
|
||||
for name, quick, answers in [('install', False, 'y\nn\nn\nn\n'), ('update', True, '')]:
|
||||
entry = str(scripts / (name + '.' + suffix))
|
||||
command = ([executable, '-NoProfile', '-File', entry, '-Shell', 'Bash', '-Client', 'Both', '-Summary']
|
||||
if kind == 'powershell' else [executable, entry, '--shell', 'bash', '--client', 'both', '--summary'])
|
||||
if quick:
|
||||
command.append('-Quick' if kind == 'powershell' else '--quick')
|
||||
result = subprocess.run(command, env=environment, input=answers, capture_output=True, text=True, timeout=25)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertTrue('CLI-MOCK codex' in result.stdout or
|
||||
'Codex CLI update skipped' in result.stdout + result.stderr)
|
||||
self.assertIn('CLI-MOCK claude', result.stdout)
|
||||
if quick:
|
||||
self.assertNotIn('Enable the Flashbang', result.stdout + result.stderr)
|
||||
state = json.loads((home / '.my-ai-configuration/selection.json').read_text())
|
||||
self.assertTrue(state['update_agents'])
|
||||
self.assertFalse(state['flashbang'])
|
||||
self.assertEqual(state['shell'], 'bash')
|
||||
self.assertNotIn('Stop', json.loads((home / '.claude/settings.json').read_text()).get('hooks', {}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,226 @@
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
spec = importlib.util.spec_from_file_location('extensions', Path(__file__).parent / 'lib' / 'managed-extensions.py')
|
||||
extensions = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(extensions)
|
||||
|
||||
|
||||
def plugin(name='Example'):
|
||||
return dict(name=name, codex_method='plugin', codex_plugin=name.lower() + '@test', codex_marketplace='-',
|
||||
claude_plugin=name.lower() + '@test', claude_marketplace='-')
|
||||
|
||||
|
||||
def skill():
|
||||
return dict(name='Humanizer', codex_method='skill', codex_source='blader/humanizer', codex_skill='humanizer', codex_plugin='-')
|
||||
|
||||
|
||||
class ManagedExtensionTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.home = Path(self.temporary.name)
|
||||
self.installed = {'codex': {}, 'claude': {}}
|
||||
self.commands = []
|
||||
self.addCleanup(patch.stopall)
|
||||
patch.object(extensions, 'installed_plugins', side_effect=lambda client, home: dict(self.installed[client])).start()
|
||||
patch.object(extensions, 'run_command', side_effect=self.command).start()
|
||||
patch.object(extensions, 'fetch_skill', return_value=('humanizer', {'SKILL.md': (b'original', False), 'references/old.md': (b'old', False)})).start()
|
||||
self.output = contextlib.redirect_stdout(io.StringIO())
|
||||
self.output.__enter__()
|
||||
self.addCleanup(self.output.__exit__, None, None, None)
|
||||
|
||||
def command(self, args, summary, home=None):
|
||||
self.commands.append(args)
|
||||
if len(args) > 3 and args[2] in ('add', 'install') and args[1] == 'plugin':
|
||||
self.installed[args[0]][args[3]] = {'scope': 'user'}
|
||||
if args[2] in ('remove', 'uninstall'):
|
||||
self.installed[args[0]].pop(args[3], None)
|
||||
return ''
|
||||
|
||||
def manager(self, **kwargs):
|
||||
return extensions.Manager(self.home, **kwargs)
|
||||
|
||||
def test_foreign_plugin_is_never_claimed_or_removed(self):
|
||||
self.installed['codex']['example@test'] = {}
|
||||
self.manager(update=True).sync([plugin()], ['codex'], {'Example'})
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual([], self.commands)
|
||||
self.assertEqual([], self.manager().state['resources'])
|
||||
|
||||
def test_owned_plugin_removed_after_manifest_entry_disappears(self):
|
||||
self.manager().sync([plugin()], ['codex'], {'Example'})
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual(['codex', 'plugin', 'remove', 'example@test'], self.commands[-1])
|
||||
self.assertEqual([], self.manager().state['resources'])
|
||||
|
||||
def test_client_scope_is_preserved(self):
|
||||
self.manager().sync([plugin()], ['claude', 'codex'], {'Example'})
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual(['claude'], [r['client'] for r in self.manager().state['resources']])
|
||||
|
||||
def test_partial_plugin_failure_retains_successful_ownership(self):
|
||||
with patch.object(extensions, 'run_command', side_effect=[None, ValueError('failure')]):
|
||||
with self.assertRaises(ValueError):
|
||||
self.manager().sync([plugin(), plugin('Second')], ['codex'], {'Example', 'Second'})
|
||||
self.assertEqual(['Example'], [r['name'] for r in self.manager().state['resources']])
|
||||
|
||||
def test_failed_removal_retains_ownership(self):
|
||||
self.manager().sync([plugin()], ['codex'], {'Example'})
|
||||
with patch.object(extensions, 'run_command', side_effect=ValueError('failure')):
|
||||
with self.assertRaises(ValueError):
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual(1, len(self.manager().state['resources']))
|
||||
|
||||
def test_foreign_skill_directory_is_preserved(self):
|
||||
target = self.home / '.agents/skills/humanizer/SKILL.md'
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text('foreign')
|
||||
self.manager(update=True).sync([skill()], ['codex'], {'Humanizer'})
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual('foreign', target.read_text())
|
||||
self.assertEqual([], self.manager().state['resources'])
|
||||
|
||||
def test_skill_removal_preserves_modified_and_foreign_files(self):
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
directory = self.home / '.agents/skills/humanizer'
|
||||
(directory / 'SKILL.md').write_text('modified')
|
||||
(directory / 'foreign.txt').write_text('foreign')
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual('modified', (directory / 'SKILL.md').read_text())
|
||||
self.assertTrue((directory / 'foreign.txt').exists())
|
||||
self.assertFalse((directory / 'references/old.md').exists())
|
||||
self.assertEqual(1, len(self.manager().state['resources'][0]['files']))
|
||||
|
||||
def test_skill_update_cleans_only_owned_stale_files(self):
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
directory = self.home / '.agents/skills/humanizer'
|
||||
(directory / 'foreign.txt').write_text('foreign')
|
||||
with patch.object(extensions, 'fetch_skill', return_value=('humanizer', {'SKILL.md': (b'new', False)})):
|
||||
self.manager(update=True).sync([skill()], ['codex'], {'Humanizer'})
|
||||
self.assertEqual('new', (directory / 'SKILL.md').read_text())
|
||||
self.assertTrue((directory / 'foreign.txt').exists())
|
||||
self.assertFalse((directory / 'references/old.md').exists())
|
||||
|
||||
def test_skill_records_resolved_revision(self):
|
||||
with patch.object(extensions, 'resolve_revision', return_value='a' * 40):
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
record = self.manager().state['resources'][0]
|
||||
self.assertEqual(record['source'], 'blader/humanizer')
|
||||
self.assertEqual(record['resolvedRevision'], 'a' * 40)
|
||||
|
||||
def test_skill_partial_failure_records_each_successful_file(self):
|
||||
original_write = extensions.atomic_write
|
||||
def write(path, content):
|
||||
if path.name == 'old.md':
|
||||
raise OSError('simulated disk failure')
|
||||
original_write(path, content)
|
||||
with patch.object(extensions, 'atomic_write', side_effect=write):
|
||||
with self.assertRaises(OSError):
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
self.assertEqual(['.agents/skills/humanizer/SKILL.md'], list(self.manager().state['resources'][0]['files']))
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
self.assertEqual(2, len(self.manager().state['resources'][0]['files']))
|
||||
self.assertTrue(self.manager().state['resources'][0]['complete'])
|
||||
|
||||
def test_dry_run_has_no_commands_network_or_writes(self):
|
||||
self.manager(dry_run=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'})
|
||||
self.assertEqual([], list(self.home.iterdir()))
|
||||
self.assertEqual([], self.commands)
|
||||
extensions.fetch_skill.assert_not_called()
|
||||
|
||||
def test_traversal_ledger_is_rejected(self):
|
||||
path = self.home / '.my-ai-configuration/extensions.json'
|
||||
path.parent.mkdir()
|
||||
path.write_text(json.dumps({'version': 1, 'resources': [dict(client='codex', name='Bad', kind='skill', directory='.agents/skills/bad', files={'.agents/skills/bad/../../../other': '0' * 64})]}))
|
||||
with self.assertRaises(ValueError):
|
||||
self.manager()
|
||||
|
||||
def test_marketplace_failure_never_removes_existing_source(self):
|
||||
entry = plugin()
|
||||
entry['codex_marketplace'] = 'owner/repository'
|
||||
with patch.object(extensions, 'run_command', side_effect=ValueError('different source')) as command:
|
||||
with self.assertRaises(ValueError):
|
||||
self.manager().sync([entry], ['codex'], {'Example'})
|
||||
self.assertEqual(1, command.call_count)
|
||||
self.assertEqual('add', command.call_args.args[0][3])
|
||||
|
||||
def test_owned_codex_update_upgrades_marketplace(self):
|
||||
entry = plugin()
|
||||
entry['codex_marketplace'] = 'owner/repository'
|
||||
self.manager().sync([entry], ['codex'], {'Example'})
|
||||
self.commands.clear()
|
||||
self.manager(update=True).sync([entry], ['codex'], {'Example'})
|
||||
self.assertEqual([['codex', 'plugin', 'marketplace', 'upgrade', 'test'], ['codex', 'plugin', 'add', 'example@test']], self.commands)
|
||||
|
||||
def test_owned_disabled_claude_is_enabled(self):
|
||||
self.manager().sync([plugin()], ['claude'], {'Example'})
|
||||
self.installed['claude']['example@test']['enabled'] = False
|
||||
self.manager(update=True).sync([plugin()], ['claude'], {'Example'})
|
||||
self.assertEqual(['claude', 'plugin', 'enable', 'example@test'], self.commands[-1])
|
||||
|
||||
def test_dry_run_summary_has_one_line(self):
|
||||
with contextlib.redirect_stdout(io.StringIO()) as output:
|
||||
self.manager(dry_run=True, summary=True).sync([plugin(), skill()], ['codex'], {'Example', 'Humanizer'})
|
||||
self.assertEqual('EXTENSIONS: reconciliation complete (dry run)\n', output.getvalue())
|
||||
|
||||
def test_qmd_installs_once_as_a_cli_without_registering_mcp(self):
|
||||
entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd')
|
||||
self.manager().sync([entry], ['codex'], {'QMD'})
|
||||
self.assertEqual([['npm', 'install', '--global', '@tobilu/qmd']], self.commands)
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertEqual(['npm', 'uninstall', '--global', '@tobilu/qmd'], self.commands[-1])
|
||||
|
||||
def test_qmd_installs_once_for_both_clients(self):
|
||||
entry = dict(name='QMD', codex_method='qmd', codex_source='@tobilu/qmd', codex_skill='qmd', codex_plugin='-', codex_marketplace='-', claude_plugin='qmd@qmd', claude_marketplace='tobi/qmd')
|
||||
self.manager().sync([entry], ['claude', 'codex'], {'QMD'})
|
||||
self.assertEqual(1, self.commands.count(['npm', 'install', '--global', '@tobilu/qmd']))
|
||||
self.assertIn(['claude', 'plugin', 'install', 'qmd@qmd', '--scope', 'user'], self.commands)
|
||||
self.assertNotIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands)
|
||||
|
||||
def test_foreign_empty_directory_survives_skill_removal(self):
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
foreign = self.home / '.agents/skills/humanizer/foreign-empty'
|
||||
foreign.mkdir()
|
||||
self.manager().sync([], ['codex'], set())
|
||||
self.assertTrue(foreign.is_dir())
|
||||
|
||||
def test_foreign_skill_link_is_preserved(self):
|
||||
foreign = self.home / 'foreign'
|
||||
foreign.mkdir()
|
||||
target = self.home / '.agents/skills/humanizer'
|
||||
target.parent.mkdir(parents=True)
|
||||
try:
|
||||
target.symlink_to(foreign, target_is_directory=True)
|
||||
except OSError:
|
||||
self.skipTest('Symbolic links are unavailable on this host.')
|
||||
self.manager().sync([skill()], ['codex'], {'Humanizer'})
|
||||
self.assertTrue(target.is_symlink())
|
||||
extensions.fetch_skill.assert_not_called()
|
||||
|
||||
|
||||
class CommandTests(unittest.TestCase):
|
||||
def test_subprocess_uses_intended_home_and_cmd_launcher(self):
|
||||
with tempfile.TemporaryDirectory(prefix='managed extensions ') as temporary:
|
||||
home = Path(temporary)
|
||||
executable = home / ('mock.cmd' if os.name == 'nt' else 'mock')
|
||||
if os.name == 'nt':
|
||||
executable.write_text('@echo off\necho %CODEX_HOME%\necho %CLAUDE_CONFIG_DIR%\n')
|
||||
else:
|
||||
executable.write_text('#!/bin/sh\nprintf "%s\\n" "$CODEX_HOME" "$CLAUDE_CONFIG_DIR"\n')
|
||||
executable.chmod(0o700)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
result = extensions.run_command([str(executable), 'plugin', 'list', '--json'], True, home)
|
||||
self.assertEqual([str(home / '.codex'), str(home / '.claude')], result.splitlines())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,109 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $root 'scripts/lib/manifest.ps1')
|
||||
. (Join-Path $root 'scripts/lib/install-targets.ps1')
|
||||
|
||||
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("ai-config-manifest-test-" + [Guid]::NewGuid())
|
||||
New-Item $work -ItemType Directory -Force | Out-Null
|
||||
try {
|
||||
$source = Join-Path $work 'source'
|
||||
$destination = Join-Path $work 'destination'
|
||||
New-Item $source -ItemType Directory -Force | Out-Null
|
||||
Set-Content (Join-Path $source 'a.md') 'A' -Encoding UTF8 -NoNewline
|
||||
Set-Content (Join-Path $source 'b.md') 'B' -Encoding UTF8 -NoNewline
|
||||
Set-Content (Join-Path $source 'c.md') 'C' -Encoding UTF8 -NoNewline
|
||||
|
||||
$out1 = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'run1')
|
||||
if (@($out1 | Where-Object { $_ -like 'CREATE*' }).Count -ne 3) { throw "Expected three CREATE lines on first install: $($out1 -join '; ')" }
|
||||
|
||||
$out2 = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'run2')
|
||||
if (@($out2 | Where-Object { $_ -notlike 'UNCHANGED*' -and $_ -notlike 'SOURCE *' }).Count -ne 0) { throw "Second identical install was not a no-op: $($out2 -join '; ')" }
|
||||
if (Test-Path (Join-Path $destination 'backups/run2')) { throw 'Idempotent install must not create a backup.' }
|
||||
|
||||
Set-Content (Join-Path $destination 'foreign.md') 'mine' -Encoding UTF8 -NoNewline
|
||||
|
||||
Remove-Item (Join-Path $source 'b.md') -Force
|
||||
Remove-Item (Join-Path $source 'c.md') -Force
|
||||
Set-Content (Join-Path $destination 'c.md') 'locally changed' -Encoding UTF8 -NoNewline
|
||||
|
||||
$out3 = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'run3')
|
||||
if (@($out3 | Where-Object { $_ -match 'REMOVE.*b\.md' }).Count -ne 1) { throw "Expected b.md to be removed: $($out3 -join '; ')" }
|
||||
if (Test-Path (Join-Path $destination 'b.md')) { throw 'b.md should have been removed: it was untouched by the user and dropped from source.' }
|
||||
if (@($out3 | Where-Object { $_ -match 'WARN.*c\.md.*changed locally' }).Count -ne 1) { throw "Expected c.md to be flagged as locally modified: $($out3 -join '; ')" }
|
||||
if (-not (Test-Path (Join-Path $destination 'c.md'))) { throw 'A locally modified stale file must not be removed.' }
|
||||
if ((Get-Content (Join-Path $destination 'c.md') -Raw) -ne 'locally changed') { throw 'c.md content must be preserved exactly.' }
|
||||
if ((Get-Content (Join-Path $destination 'foreign.md') -Raw) -ne 'mine') { throw 'A file this setup never installed must never be touched.' }
|
||||
if (-not (Test-Path (Join-Path $destination 'backups/run3/b.md'))) { throw 'b.md should have been backed up before removal.' }
|
||||
if (-not (Test-Path (Join-Path $destination 'backups/run3/c.md'))) { throw 'c.md should have been backed up even though it was left in place.' }
|
||||
|
||||
Remove-Item (Join-Path $source 'a.md') -Force
|
||||
$beforeDry = @(Get-ChildItem $destination -Recurse -File | Sort-Object FullName | ForEach-Object FullName)
|
||||
$outDry = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'dry' -DryRun)
|
||||
$afterDry = @(Get-ChildItem $destination -Recurse -File | Sort-Object FullName | ForEach-Object FullName)
|
||||
if (Compare-Object $beforeDry $afterDry) { throw 'A dry run must not change the destination at all.' }
|
||||
if (@($outDry | Where-Object { $_ -match 'DRYRUN REMOVE.*a\.md' }).Count -ne 1) { throw "Dry run should report the planned removal without performing it: $($outDry -join '; ')" }
|
||||
|
||||
$configPath = Join-Path $destination 'config.toml'
|
||||
$pluginState = @'
|
||||
[marketplaces.ponytail]
|
||||
source_type = "git"
|
||||
source = "https://github.com/DietrichGebert/ponytail.git"
|
||||
[plugins."ponytail@ponytail"]
|
||||
enabled = true
|
||||
[plugins."disabled@market"]
|
||||
enabled = false
|
||||
'@
|
||||
[System.IO.File]::WriteAllText((Join-Path $source 'config.toml'), "approvals_reviewer = 'auto_review'`n`n[tui]`n")
|
||||
[System.IO.File]::WriteAllText($configPath, "model = 'old'`nmodel_reasoning_effort = 'high'`napprovals_reviewer = 'user'`n$pluginState`n[other]`nvalue = true`n", (New-Object System.Text.UTF8Encoding($true)))
|
||||
$originalConfig = [System.IO.File]::ReadAllText($configPath)
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-dry' -DryRun
|
||||
if ([System.IO.File]::ReadAllText($configPath) -ne $originalConfig) { throw 'Dry run must preserve plugin configuration.' }
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins'
|
||||
$mergedConfig = [System.IO.File]::ReadAllText($configPath)
|
||||
if (-not ($mergedConfig -replace "`r`n", "`n").Contains(($pluginState -replace "`r`n", "`n"))) { throw 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.' }
|
||||
if (-not $mergedConfig.Contains("model = 'old'") -or -not $mergedConfig.Contains("model_reasoning_effort = 'high'") -or -not $mergedConfig.Contains("approvals_reviewer = 'auto_review'") -or $mergedConfig.Contains("approvals_reviewer = 'user'") -or $mergedConfig.Contains('[other]')) { throw 'Updating config.toml must preserve local model settings and enforce automatic review.' }
|
||||
if ($mergedConfig.IndexOf("model = 'old'") -gt $mergedConfig.IndexOf('[tui]') -or $mergedConfig.IndexOf("model_reasoning_effort = 'high'") -gt $mergedConfig.IndexOf('[tui]')) { throw 'Model settings must remain in the TOML root table.' }
|
||||
if ([System.IO.File]::ReadAllText((Join-Path $destination 'backups/plugins/config.toml')) -ne $originalConfig) { throw 'Original plugin configuration must be backed up.' }
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'plugins-repeat'
|
||||
if ([System.IO.File]::ReadAllText($configPath) -ne $mergedConfig -or (Test-Path (Join-Path $destination 'backups/plugins-repeat'))) { throw 'Preserving plugin configuration must be idempotent.' }
|
||||
|
||||
$outSummary = @(Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'summary' -Summary)
|
||||
if (@($outSummary | Where-Object { $_ -like 'CREATE*' -or $_ -like 'UPDATE*' -or $_ -like 'UNCHANGED*' -or $_ -like 'BACKUP*' }).Count -ne 0) { throw "Summary mode must not print per-file lines: $($outSummary -join '; ')" }
|
||||
if (@($outSummary | Where-Object { $_ -match '^SYNC .* unchanged' }).Count -ne 1) { throw "Summary mode must print exactly one SYNC tally line: $($outSummary -join '; ')" }
|
||||
|
||||
$generated = Join-Path $work 'generated'
|
||||
$package = Join-Path $generated 'codex-powershell'
|
||||
$testHome = Join-Path $work 'home'
|
||||
$legacy = Join-Path $testHome '.codex'
|
||||
New-Item (Join-Path $package 'skills/example') -ItemType Directory -Force | Out-Null
|
||||
Set-Content (Join-Path $package 'skills/example/SKILL.md') 'managed skill' -NoNewline
|
||||
New-Item (Join-Path $legacy 'skills/example') -ItemType Directory -Force | Out-Null
|
||||
Copy-Item (Join-Path $package 'skills/example/SKILL.md') (Join-Path $legacy 'skills/example/SKILL.md')
|
||||
Set-Content (Join-Path $legacy 'skills/example/custom.md') 'customized' -NoNewline
|
||||
Set-Content (Join-Path $legacy 'skills/example/foreign.md') 'foreign' -NoNewline
|
||||
Write-ManagedManifest (Get-ManagedManifestPath $legacy) @{
|
||||
'skills/example/SKILL.md' = (Get-Sha256Hash (Join-Path $legacy 'skills/example/SKILL.md'))
|
||||
'skills/example/custom.md' = ('0' * 64)
|
||||
}
|
||||
$targets = @(Get-InstallTargets -Generated $generated -HomePath $testHome -Shell PowerShell -Client Codex)
|
||||
if ($targets[0].Destination -ne (Join-Path $testHome '.agents/skills')) { throw 'Canonical skills must be installed before legacy copies are removed.' }
|
||||
foreach ($item in $targets) { $null = Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp 'migrate' }
|
||||
if (-not (Test-Path (Join-Path $testHome '.agents/skills/example/SKILL.md'))) { throw 'Canonical skill is missing.' }
|
||||
if (Test-Path (Join-Path $legacy 'skills/example/SKILL.md')) { throw 'Unmodified managed skill must not remain duplicated.' }
|
||||
if (-not (Test-Path (Join-Path $legacy 'backups/migrate/skills/example/SKILL.md'))) { throw 'Removed legacy skill must have a backup.' }
|
||||
foreach ($name in @('custom.md', 'foreign.md')) {
|
||||
if (-not (Test-Path (Join-Path $legacy "skills/example/$name"))) { throw 'Modified and foreign legacy skills must be preserved.' }
|
||||
}
|
||||
foreach ($item in $targets) { $null = Sync-ManagedDestination -Source $item.Source -Destination $item.Destination -Stamp 'migrate-repeat' }
|
||||
if (Test-Path (Join-Path $legacy 'backups/migrate-repeat')) { throw 'Skill migration must be idempotent.' }
|
||||
Set-Content (Join-Path $source 'concurrency.txt') '__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__' -NoNewline
|
||||
$null = Sync-ManagedDestination -Source $source -Destination $destination -Stamp 'concurrency' -ClaudeConcurrency '7'
|
||||
if ((Get-Content (Join-Path $destination 'concurrency.txt') -Raw) -ne '7') { throw 'Standalone concurrency placeholder must be resolved.' }
|
||||
|
||||
if ($Summary) { Write-Output 'Tests: PASS | managed install' } else { Write-Output 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free, skill migration' }
|
||||
} finally {
|
||||
Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
exit 0
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
summary=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--summary) summary=true; shift ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
. "$root/scripts/lib/manifest.sh"
|
||||
. "$root/scripts/lib/install-targets.sh"
|
||||
|
||||
work=$(mktemp -d)
|
||||
cleanup() { rm -rf -- "$work"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
source_dir="$work/source"
|
||||
destination="$work/destination"
|
||||
mkdir -p "$source_dir"
|
||||
printf 'A' > "$source_dir/a.md"
|
||||
printf 'B' > "$source_dir/b.md"
|
||||
printf 'C' > "$source_dir/c.md"
|
||||
|
||||
out1=$(sync_managed_destination "$source_dir" "$destination" run1 "$destination" bash bash false)
|
||||
[ "$(printf '%s\n' "$out1" | grep -c '^CREATE')" -eq 3 ] || { printf 'Expected three CREATE lines on first install:\n%s\n' "$out1" >&2; exit 1; }
|
||||
|
||||
out2=$(sync_managed_destination "$source_dir" "$destination" run2 "$destination" bash bash false)
|
||||
[ -z "$(printf '%s\n' "$out2" | grep -v '^UNCHANGED' | grep -v '^SOURCE ')" ] || { printf 'Second identical install was not a no-op:\n%s\n' "$out2" >&2; exit 1; }
|
||||
[ ! -d "$destination/backups/run2" ] || { printf 'Idempotent install must not create a backup.\n' >&2; exit 1; }
|
||||
|
||||
printf 'mine' > "$destination/foreign.md"
|
||||
|
||||
rm -f "$source_dir/b.md" "$source_dir/c.md"
|
||||
printf 'locally changed' > "$destination/c.md"
|
||||
|
||||
out3=$(sync_managed_destination "$source_dir" "$destination" run3 "$destination" bash bash false)
|
||||
printf '%s\n' "$out3" | grep -q '^REMOVE .*b\.md$' || { printf 'Expected b.md to be removed:\n%s\n' "$out3" >&2; exit 1; }
|
||||
[ ! -f "$destination/b.md" ] || { printf 'b.md should have been removed.\n' >&2; exit 1; }
|
||||
printf '%s\n' "$out3" | grep -q 'WARN .*c\.md.*changed locally' || { printf 'Expected c.md to be flagged as locally modified:\n%s\n' "$out3" >&2; exit 1; }
|
||||
[ -f "$destination/c.md" ] || { printf 'A locally modified stale file must not be removed.\n' >&2; exit 1; }
|
||||
[ "$(cat "$destination/c.md")" = 'locally changed' ] || { printf 'c.md content must be preserved exactly.\n' >&2; exit 1; }
|
||||
[ "$(cat "$destination/foreign.md")" = 'mine' ] || { printf 'A file this setup never installed must never be touched.\n' >&2; exit 1; }
|
||||
[ -f "$destination/backups/run3/b.md" ] || { printf 'b.md should have been backed up before removal.\n' >&2; exit 1; }
|
||||
[ -f "$destination/backups/run3/c.md" ] || { printf 'c.md should have been backed up even though it was left in place.\n' >&2; exit 1; }
|
||||
|
||||
rm -f "$source_dir/a.md"
|
||||
before_dry=$(find "$destination" -type f | sort)
|
||||
out_dry=$(sync_managed_destination "$source_dir" "$destination" dry "$destination" bash bash true)
|
||||
after_dry=$(find "$destination" -type f | sort)
|
||||
[ "$before_dry" = "$after_dry" ] || { printf 'A dry run must not change the destination at all.\n' >&2; exit 1; }
|
||||
printf '%s\n' "$out_dry" | grep -q '^DRYRUN REMOVE .*a\.md$' || { printf 'Dry run should report the planned removal without performing it:\n%s\n' "$out_dry" >&2; exit 1; }
|
||||
|
||||
plugin_state=' [marketplaces.ponytail]
|
||||
source_type = "git"
|
||||
source = "https://github.com/DietrichGebert/ponytail.git"
|
||||
[plugins."ponytail@ponytail"]
|
||||
enabled = true
|
||||
[plugins."disabled@market"]
|
||||
enabled = false'
|
||||
printf "approvals_reviewer = 'auto_review'\n\n[tui]\n" > "$source_dir/config.toml"
|
||||
printf "\357\273\277model = 'old'\nmodel_reasoning_effort = 'high'\napprovals_reviewer = 'user'\n%s\n[other]\nvalue = true\n" "$plugin_state" > "$destination/config.toml"
|
||||
original_config=$(cat "$destination/config.toml")
|
||||
sync_managed_destination "$source_dir" "$destination" plugins-dry "" "" "" true > /dev/null
|
||||
[ "$(cat "$destination/config.toml")" = "$original_config" ] || { printf 'Dry run changed plugin configuration.\n' >&2; exit 1; }
|
||||
sync_managed_destination "$source_dir" "$destination" plugins "" "" "" false > /dev/null
|
||||
merged_config=$(cat "$destination/config.toml")
|
||||
merged_config_normalized=${merged_config//$'\r'/}
|
||||
plugin_state_normalized=${plugin_state//$'\r'/}
|
||||
[[ "$merged_config_normalized" == *"$plugin_state_normalized"* ]] || { printf 'Updating config.toml must preserve local marketplace and plugin tables, including disabled plugins.\n' >&2; exit 1; }
|
||||
[[ "$merged_config" == *"model = 'old'"* && "$merged_config" == *"model_reasoning_effort = 'high'"* && "$merged_config" == *"approvals_reviewer = 'auto_review'"* && "$merged_config" != *"approvals_reviewer = 'user'"* && "$merged_config" != *'[other]'* ]] || { printf 'Updating config.toml must preserve local model settings and enforce automatic review.\n' >&2; exit 1; }
|
||||
[ "$(printf '%s\n' "$merged_config" | grep -n "model = 'old'" | cut -d: -f1)" -lt "$(printf '%s\n' "$merged_config" | grep -n '^\[tui\]$' | cut -d: -f1)" ] || { printf 'Model must remain in TOML root table.\n' >&2; exit 1; }
|
||||
[ "$(printf '%s\n' "$merged_config" | grep -n "model_reasoning_effort = 'high'" | cut -d: -f1)" -lt "$(printf '%s\n' "$merged_config" | grep -n '^\[tui\]$' | cut -d: -f1)" ] || { printf 'Model effort must remain in TOML root table.\n' >&2; exit 1; }
|
||||
[ "$(cat "$destination/backups/plugins/config.toml")" = "$original_config" ] || { printf 'Original plugin configuration must be backed up.\n' >&2; exit 1; }
|
||||
sync_managed_destination "$source_dir" "$destination" plugins-repeat "" "" "" false > /dev/null
|
||||
[ "$(cat "$destination/config.toml")" = "$merged_config" ] && [ ! -d "$destination/backups/plugins-repeat" ] || { printf 'Preserving plugin configuration must be idempotent.\n' >&2; exit 1; }
|
||||
|
||||
out_summary=$(sync_managed_destination "$source_dir" "$destination" "summary" "" "" "" false true)
|
||||
if printf '%s\n' "$out_summary" | grep -Eq '^(CREATE|UPDATE|UNCHANGED|BACKUP) '; then
|
||||
printf 'Summary mode must not print per-file lines: %s\n' "$out_summary" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(printf '%s\n' "$out_summary" | grep -c '^SYNC .* unchanged')" -ne 1 ]; then
|
||||
printf 'Summary mode must print exactly one SYNC tally line: %s\n' "$out_summary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
generated="$work/generated"
|
||||
package="$generated/codex-bash"
|
||||
test_home="$work/home"
|
||||
legacy="$test_home/.codex"
|
||||
mkdir -p "$package/skills/example" "$legacy/skills/example"
|
||||
printf 'managed skill' > "$package/skills/example/SKILL.md"
|
||||
cp "$package/skills/example/SKILL.md" "$legacy/skills/example/SKILL.md"
|
||||
printf 'customized' > "$legacy/skills/example/custom.md"
|
||||
printf 'foreign' > "$legacy/skills/example/foreign.md"
|
||||
declare -A legacy_manifest
|
||||
# shellcheck disable=SC2034
|
||||
legacy_manifest[skills/example/SKILL.md]=$(sha256_of_file "$legacy/skills/example/SKILL.md")
|
||||
# shellcheck disable=SC2034
|
||||
legacy_manifest[skills/example/custom.md]=$(printf '0%.0s' {1..64})
|
||||
write_managed_manifest "$(manifest_path "$legacy")" legacy_manifest
|
||||
mapfile -t targets < <(get_install_targets "$generated" "$test_home" bash codex)
|
||||
[[ "${targets[0]}" = *"|$test_home/.agents/skills" ]] || { printf 'Canonical skills must be installed first.\n' >&2; exit 1; }
|
||||
for target_pair in "${targets[@]}"; do
|
||||
sync_managed_destination "${target_pair%%|*}" "${target_pair#*|}" migrate "" "" "" false > /dev/null
|
||||
done
|
||||
[ -f "$test_home/.agents/skills/example/SKILL.md" ] && [ ! -f "$legacy/skills/example/SKILL.md" ] && [ -f "$legacy/backups/migrate/skills/example/SKILL.md" ] || { printf 'Skill migration must remove and back up the managed duplicate.\n' >&2; exit 1; }
|
||||
[ -f "$legacy/skills/example/custom.md" ] && [ -f "$legacy/skills/example/foreign.md" ] || { printf 'Skill migration must preserve modified and foreign files.\n' >&2; exit 1; }
|
||||
for target_pair in "${targets[@]}"; do
|
||||
sync_managed_destination "${target_pair%%|*}" "${target_pair#*|}" migrate-repeat "" "" "" false > /dev/null
|
||||
done
|
||||
[ ! -d "$legacy/backups/migrate-repeat" ] || { printf 'Skill migration must be idempotent.\n' >&2; exit 1; }
|
||||
printf '__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__' > "$source_dir/concurrency.txt"
|
||||
sync_managed_destination "$source_dir" "$destination" concurrency "" "" "" false false true 7 > /dev/null
|
||||
[ "$(cat "$destination/concurrency.txt")" = 7 ] || { printf 'Standalone concurrency placeholder must be resolved.\n' >&2; exit 1; }
|
||||
|
||||
if [ "$summary" = true ]; then printf 'Tests: PASS | managed install\n'; else printf 'PASS managed manifest: idempotent, stale removal, modified-file protection, foreign files preserved, dry run side-effect free, skill migration\n'; fi
|
||||
@@ -0,0 +1,66 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
function Write-Host { param($Object) if (-not $Summary) { Microsoft.PowerShell.Utility\Write-Host $Object } }
|
||||
. (Join-Path $root 'scripts/lib/plugins.ps1')
|
||||
|
||||
$script:answers = [System.Collections.Generic.Queue[string]]::new()
|
||||
$script:answers.Enqueue('1')
|
||||
$script:answers.Enqueue('2')
|
||||
$script:answers.Enqueue('done')
|
||||
function Read-Host { param($Prompt) $answers.Dequeue() }
|
||||
|
||||
$result = Read-PluginToggleSelection -Names @('A','B','C') -Checked @($true, $false, $true)
|
||||
if ($result[0] -ne $false) { throw 'Toggling checked entry 1 must uncheck it.' }
|
||||
if ($result[1] -ne $true) { throw 'Toggling unchecked entry 2 must check it.' }
|
||||
if ($result[2] -ne $true) { throw 'Entry 3 was never toggled and must stay unchanged.' }
|
||||
|
||||
$script:answers.Enqueue('')
|
||||
$confirmed = @(Read-PluginToggleSelection -Names @('A') -Checked @($true))
|
||||
if ($confirmed.Count -ne 1 -or -not $confirmed[0]) { throw 'Enter must confirm without changing the selection.' }
|
||||
function Test-PluginOutput {
|
||||
param([string]$Mode)
|
||||
Write-Output 'download progress'
|
||||
Write-Output 'WARN test warning'
|
||||
$global:LASTEXITCODE = if ($Mode -eq 'fail') { 7 } else { 0 }
|
||||
}
|
||||
$compact = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('success') -Summary)
|
||||
if ($compact.Count -ne 1 -or $compact[0] -ne 'WARN test warning') { throw 'Plugin summary must retain warnings and suppress progress.' }
|
||||
$detailed = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('success'))
|
||||
if ($detailed.Count -ne 2 -or $detailed[0] -ne 'download progress') { throw 'Detailed output must retain progress.' }
|
||||
$diagnostics = [System.Collections.Generic.List[string]]::new()
|
||||
$failed = $false
|
||||
$env:AI_CONFIG_VERBOSE = $null
|
||||
try { Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('fail') -Summary | ForEach-Object { $diagnostics.Add("$_") } }
|
||||
catch { $failed = $true }
|
||||
$env:AI_CONFIG_VERBOSE = $null
|
||||
if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' }
|
||||
$global:LASTEXITCODE = 0
|
||||
function codex { $script:observedCodexHome = $env:CODEX_HOME; $script:observedProfile = $env:USERPROFILE; '{"installed":[{"pluginId":"ponytail@ponytail"},{"pluginId":"i-have-adhd@i-have-adhd"},{"pluginId":"superpowers@openai-curated-remote"},{"pluginId":"context7@context7-marketplace"},{"pluginId":"caveman@thinkhome-caveman"}],"available":[]}' }
|
||||
$previousCodexHome = $env:CODEX_HOME
|
||||
$previousProfile = $env:USERPROFILE
|
||||
$null = Get-InstalledPlugins -Client Codex -HomePath 'C:/temporary profile'
|
||||
if ($observedCodexHome -ne (Join-Path 'C:/temporary profile' '.codex') -or $observedProfile -ne 'C:/temporary profile') { throw 'Plugin inspection must use the requested user home.' }
|
||||
if ($env:CODEX_HOME -ne $previousCodexHome -or $env:USERPROFILE -ne $previousProfile) { throw 'Plugin inspection must restore the process environment.' }
|
||||
$script:answers.Enqueue('done')
|
||||
$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update
|
||||
if (@($selection.Selected | Where-Object { $_.codex_method -eq 'plugin' }).Count -ne 5) { throw 'All installed Codex plugins must start checked when updating.' }
|
||||
$script:answers.Enqueue('4')
|
||||
$script:answers.Enqueue('done')
|
||||
$selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update
|
||||
if (@($selection.Deselected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'Only the explicitly unchecked installed plugin must be deselected.' }
|
||||
$script:managedArguments = @()
|
||||
function python { $script:managedArguments = @($args); $global:LASTEXITCODE = 0 }
|
||||
$entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' }
|
||||
Sync-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -HomePath 'C:/temporary home' -Update -Summary -Entries @($entry)
|
||||
if ($managedArguments -notcontains 'sync' -or $managedArguments -notcontains '--update' -or $managedArguments -notcontains 'Test' -or $managedArguments -notcontains 'C:/temporary home') { throw 'Managed reconciliation must receive selection, home, and update mode.' }
|
||||
Sync-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Entries @()
|
||||
if ($managedArguments[-1] -ne '--selected') { throw 'Empty selection must reach reconciliation for managed cleanup.' }
|
||||
function Get-InstalledPlugins { throw 'Empty selection must not query installed plugins.' }
|
||||
function Invoke-PluginCommand { throw 'Empty selection must not run plugin commands.' }
|
||||
Install-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Both -Entries @()
|
||||
Uninstall-DeselectedPlugins -Client Both -Entries @()
|
||||
|
||||
if ($Summary) { Write-Output 'Tests: PASS | plugin selection' } else { Write-Output 'PASS plugin toggle selection: toggle and confirm' }
|
||||
exit 0
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
summary=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--summary) summary=true; shift ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
. "$root/scripts/lib/plugins.sh"
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
names=(A B C)
|
||||
checked=(true false true)
|
||||
toggle_log=$(mktemp)
|
||||
trap 'rm -f -- "$toggle_log"' EXIT
|
||||
read_plugin_toggle_selection names checked <<< $'1\n2\ndone' > "$toggle_log"
|
||||
[ "$summary" = true ] || cat "$toggle_log"
|
||||
|
||||
[ "${checked[0]}" = false ] || { printf 'Toggling checked entry 1 must uncheck it.\n' >&2; exit 1; }
|
||||
[ "${checked[1]}" = true ] || { printf 'Toggling unchecked entry 2 must check it.\n' >&2; exit 1; }
|
||||
[ "${checked[2]}" = true ] || { printf 'Entry 3 was never toggled and must stay unchanged.\n' >&2; exit 1; }
|
||||
|
||||
read_plugin_toggle_selection names checked <<< '' > "$toggle_log"
|
||||
[ "${checked[0]}" = false ] && [ "${checked[1]}" = true ]
|
||||
test_plugin_output() {
|
||||
printf 'download progress\nWARN test warning\n'
|
||||
[ "$1" != fail ]
|
||||
}
|
||||
compact=$(summary=true run_plugin_command false test_plugin_output success)
|
||||
[ "$compact" = 'WARN test warning' ] || { printf 'Plugin summary lost warnings or leaked progress.\n' >&2; exit 1; }
|
||||
if diagnostics=$(summary=true run_plugin_command false test_plugin_output fail 2>&1); then
|
||||
printf 'Plugin failure must fail.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ "$diagnostics" == *'download progress'* ]] && [[ "$diagnostics" == *'WARN test warning'* ]]
|
||||
# shellcheck disable=SC2034
|
||||
selected=()
|
||||
plugin_installed() { printf 'Empty selection must not query installed plugins.\n' >&2; exit 1; }
|
||||
run_plugin_command() { printf 'Empty selection must not run commands.\n' >&2; exit 1; }
|
||||
install_configured_plugins "$root" both false false selected false
|
||||
uninstall_deselected_plugins "$root" both false selected false
|
||||
if read_plugin_toggle_selection names checked </dev/null > "$toggle_log" 2>&1; then
|
||||
printf 'Closed input must fail instead of looping.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$summary" = true ]; then printf 'Tests: PASS | plugin selection\n'; else printf 'PASS plugin toggle selection: toggle and confirm\n'; fi
|
||||
@@ -0,0 +1,29 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location('prompt_inventory', Path(__file__).parent / 'lib/prompt-inventory.py')
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class PromptInventoryTests(unittest.TestCase):
|
||||
def test_counts_metadata_separately_and_reports_duplicate_names(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
metadata = 'name: "example"\ndescription: Example skill.\n'
|
||||
for name in ('first', 'second'):
|
||||
directory = root / name
|
||||
directory.mkdir()
|
||||
(directory / 'SKILL.md').write_text(f'---\n{metadata}---\n\nBody\n---\nMore body', encoding='utf-8-sig')
|
||||
count, header_bytes, body_bytes, duplicates = MODULE.inventory([root, root / 'missing'])
|
||||
self.assertEqual(count, 2)
|
||||
self.assertEqual(header_bytes, 2 * len(metadata.encode()))
|
||||
self.assertGreater(body_bytes, header_bytes)
|
||||
self.assertEqual(duplicates, ['example'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
& python (Join-Path $PSScriptRoot 'test-quick-update.py')
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Write-Output 'Tests: PASS | quick update selection'
|
||||
@@ -0,0 +1,162 @@
|
||||
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', '--update-agents', 'false', '--flashbang', '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.assertFalse(migrated['update_agents'])
|
||||
self.assertTrue(migrated['flashbang'])
|
||||
|
||||
def test_boolean_options_are_required_and_preserved(self):
|
||||
self.save()
|
||||
state = json.loads(self.run_state('read').stdout)
|
||||
self.assertFalse(state['update_agents'])
|
||||
self.assertFalse(state['flashbang'])
|
||||
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', '--update-agents', 'false',
|
||||
'--flashbang', '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.assertIn('semantic_retrieval\ttrue', 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', '--update-agents', 'false', '--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 -UpdateAgents $false -Flashbang $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',
|
||||
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; update_agents=false; flashbang=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)
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
case "${1:-}" in ''|--summary) ;; *) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;; esac
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
python3 "$root/scripts/tests/test-quick-update.py"
|
||||
printf 'Tests: PASS | quick update selection\n'
|
||||
@@ -0,0 +1,351 @@
|
||||
import importlib.util
|
||||
from array import array
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location('retrieval_server', ROOT / 'shared/retrieval/server.py')
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
def tokenize(text, **kwargs):
|
||||
offsets = [match.span() for match in re.finditer(r'\S+', text)]
|
||||
return {'offset_mapping': offsets, 'input_ids': list(range(len(offsets)))}
|
||||
|
||||
|
||||
def embedding():
|
||||
return array('f', [1.0] + [0.0] * (MODULE.EMBEDDING_DIMENSIONS - 1))
|
||||
|
||||
|
||||
class RetrievalServerTests(unittest.TestCase):
|
||||
def test_runtime_python_uses_adjacent_virtualenv(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
script = Path(temporary) / 'semantic-retrieval' / 'server.py'
|
||||
runtime = script.parent / '.venv' / 'Scripts' / 'python.exe'
|
||||
runtime.parent.mkdir(parents=True)
|
||||
runtime.touch()
|
||||
|
||||
self.assertEqual(MODULE.runtime_python(script, 'C:/Python/python.exe', 'nt'), runtime.resolve())
|
||||
|
||||
def test_preferred_device_uses_cuda_with_cpu_fallback(self):
|
||||
with patch.dict('sys.modules', torch=SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: True))):
|
||||
self.assertEqual(MODULE.preferred_device(), 'cuda')
|
||||
with patch.dict('sys.modules', torch=SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: False))):
|
||||
self.assertEqual(MODULE.preferred_device(), 'cpu')
|
||||
|
||||
@unittest.skipUnless(os.environ.get('QWEN_TEST_MODEL_CACHE'), 'Set QWEN_TEST_MODEL_CACHE to run the real-model check.')
|
||||
def test_real_models(self):
|
||||
os.environ['HF_HOME'] = os.environ['QWEN_TEST_MODEL_CACHE']
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / 'authentication.md').write_text('Password reset tokens are created by create_reset_token. The reset_password endpoint sends the user a password reset email.', encoding='utf-8')
|
||||
(root / 'weather.md').write_text('The weather service fetches temperature and rainfall forecasts for a city.', encoding='utf-8')
|
||||
index = MODULE.Index(root, root / 'data')
|
||||
try:
|
||||
tokenizer = index.encoder().tokenizer
|
||||
for source in ['alpha beta gamma ' * 450, 'Grüße 世界 🙂 ' * 300]:
|
||||
windows = list(MODULE.chunks(source, 'sample.txt', tokenizer))
|
||||
self.assertGreater(len(windows), 1)
|
||||
self.assertTrue(all(len(tokenizer(value, add_special_tokens=False)['input_ids']) <= 400
|
||||
for _, value in windows))
|
||||
self.assertTrue(all('\ufffd' not in value for _, value in windows))
|
||||
for query, expected in [('Where are password reset tokens created?', 'authentication.md'),
|
||||
('Which service provides rainfall forecasts?', 'weather.md')]:
|
||||
started = time.monotonic()
|
||||
results = index.search(query, 2)
|
||||
self.assertEqual(results[0]['path'], expected)
|
||||
self.assertEqual(len(results), 2)
|
||||
self.assertGreater(results[0]['score'], results[1]['score'])
|
||||
print(f'Qwen real search: {expected}, {time.monotonic() - started:.2f}s, embedding device {index.encoder().device}, reranker device {index.reranker().device}', flush=True)
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
def test_isolation_freshness_and_incremental_encoding(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
first, second = root / 'first', root / 'second'
|
||||
first.mkdir()
|
||||
second.mkdir()
|
||||
file = first / 'a.md'
|
||||
file.write_text('alpha', encoding='utf-8')
|
||||
(second / 'a.md').write_text('other', encoding='utf-8')
|
||||
indexes = [MODULE.Index(repository, root / 'data') for repository in (first, second)]
|
||||
encoded = []
|
||||
|
||||
def encode(values, **kwargs):
|
||||
self.assertEqual(kwargs.get('prompt_name'), 'query' if values == ['query'] else None)
|
||||
encoded.extend(values)
|
||||
return [embedding() for _ in values]
|
||||
|
||||
for index in indexes:
|
||||
index.encoder = lambda: SimpleNamespace(encode=encode, tokenizer=tokenize)
|
||||
index.rerank = lambda query, rows, limit: rows[:limit]
|
||||
numpy = SimpleNamespace(dot=lambda a, b: sum(x * y for x, y in zip(a, b)),
|
||||
frombuffer=lambda blob, **kwargs: array('f', blob), float32=None)
|
||||
try:
|
||||
with patch.dict('sys.modules', numpy=numpy):
|
||||
self.assertEqual(indexes[0].search('query', 5)[0]['text'], 'alpha')
|
||||
self.assertEqual(indexes[1].search('query', 5)[0]['text'], 'other')
|
||||
encoded.clear()
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
self.assertEqual(executor.submit(indexes[0].search, 'query', 5).result()[0]['text'], 'alpha')
|
||||
self.assertEqual(encoded, [])
|
||||
timestamp = file.stat()
|
||||
file.write_text('bravo', encoding='utf-8')
|
||||
os.utime(file, ns=(timestamp.st_atime_ns, timestamp.st_mtime_ns))
|
||||
self.assertEqual(indexes[0].search('query', 5)[0]['text'], 'bravo')
|
||||
file.rename(first / 'renamed.md')
|
||||
self.assertEqual(indexes[0].search('query', 5)[0]['path'], 'renamed.md')
|
||||
(first / 'renamed.md').unlink()
|
||||
encoded.clear()
|
||||
self.assertEqual(indexes[0].search('query', 5), [])
|
||||
self.assertEqual(indexes[0].search('query', 5), [])
|
||||
self.assertEqual(encoded, [])
|
||||
self.assertEqual(indexes[1].search('query', 5)[0]['text'], 'other')
|
||||
finally:
|
||||
for index in indexes:
|
||||
index.db.close()
|
||||
|
||||
def test_failed_refresh_preserves_previous_index(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / 'a.md').write_text('before', encoding='utf-8')
|
||||
index = MODULE.Index(root, root / 'data')
|
||||
index.encoder = lambda: SimpleNamespace(encode=lambda values, **kwargs: [embedding() for _ in values], tokenizer=tokenize)
|
||||
try:
|
||||
index.rebuild()
|
||||
(root / 'a.md').write_text('after', encoding='utf-8')
|
||||
(root / 'b.md').write_text('fail', encoding='utf-8')
|
||||
|
||||
def encode(values, **kwargs):
|
||||
if any(value.endswith('\n\nfail') for value in values):
|
||||
raise RuntimeError('encoder failed')
|
||||
return [embedding() for _ in values]
|
||||
|
||||
index.encoder = lambda: SimpleNamespace(encode=encode, tokenizer=tokenize)
|
||||
with self.assertRaisesRegex(RuntimeError, 'encoder failed'):
|
||||
index.rebuild()
|
||||
self.assertEqual(index.db.execute('select path, text from chunks').fetchall(), [('a.md', 'before')])
|
||||
self.assertEqual(index.db.execute('select document from lexical').fetchall(),
|
||||
[('Path: a.md\nSymbol: <module>\n\nbefore',)])
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
def test_refresh_batches_embeddings_for_changed_files(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / 'first.md').write_text('first', encoding='utf-8')
|
||||
(root / 'second.md').write_text('second', encoding='utf-8')
|
||||
index = MODULE.Index(root, root / 'data')
|
||||
encoded = []
|
||||
|
||||
def encode(values, **kwargs):
|
||||
encoded.append(list(values))
|
||||
return [embedding() for _ in values]
|
||||
|
||||
index.encoder = lambda: SimpleNamespace(encode=encode, tokenizer=tokenize)
|
||||
try:
|
||||
index.rebuild()
|
||||
self.assertEqual(len(encoded), 1)
|
||||
self.assertEqual(len(encoded[0]), 2)
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
def test_scan_prunes_ignored_directories_and_warns_at_limit(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
index = MODULE.Index(root, root / 'data')
|
||||
(root / 'visible.md').write_text('visible', encoding='utf-8')
|
||||
for name in ('node_modules', '.git', 'generated', 'data'):
|
||||
directory = root / name
|
||||
directory.mkdir(exist_ok=True)
|
||||
(directory / 'hidden.md').write_text('hidden', encoding='utf-8')
|
||||
try:
|
||||
self.assertEqual(index.files(), [(root / 'visible.md').resolve()])
|
||||
filenames = ['visible.md'] * 2001
|
||||
with patch.object(MODULE.os, 'walk', return_value=iter([(str(root), [], filenames)])), \
|
||||
patch.object(MODULE.sys, 'stderr') as stderr:
|
||||
self.assertEqual(len(index.files()), 2000)
|
||||
self.assertIn('stopped scanning after 2000', stderr.write.call_args_list[0].args[0])
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
def test_max_files_is_configurable_via_environment(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
index = MODULE.Index(root, root / 'data')
|
||||
try:
|
||||
with patch.dict(os.environ, {MODULE.MAX_FILES_ENV: '3'}):
|
||||
self.assertEqual(MODULE.read_max_files(), 3)
|
||||
with patch.dict(os.environ, {MODULE.MAX_FILES_ENV: 'not-a-number'}):
|
||||
with self.assertRaises(ValueError):
|
||||
MODULE.read_max_files()
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
self.assertEqual(MODULE.read_max_files(), MODULE.DEFAULT_MAX_FILES)
|
||||
(root / 'visible.md').write_text('visible', encoding='utf-8')
|
||||
index.max_files = 3
|
||||
filenames = ['visible.md'] * 5
|
||||
with patch.object(MODULE.os, 'walk', return_value=iter([(str(root), [], filenames)])), \
|
||||
patch.object(MODULE.sys, 'stderr'):
|
||||
self.assertEqual(len(index.files()), 3)
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
def test_daemon_address_is_deterministic_and_platform_specific(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
data_dir = Path(temporary)
|
||||
first = MODULE.daemon_address(data_dir)
|
||||
second = MODULE.daemon_address(data_dir)
|
||||
self.assertEqual(first, second)
|
||||
if os.name == 'nt':
|
||||
self.assertTrue(first.startswith(r'\\.\pipe\ai-config-retrieval-'))
|
||||
else:
|
||||
self.assertTrue(first.startswith(str(data_dir)))
|
||||
|
||||
def test_shared_encoder_and_reranker_are_cached_module_singletons(self):
|
||||
with patch.object(MODULE, '_shared_encoder', None), patch.object(MODULE, '_shared_reranker', None), \
|
||||
patch.object(MODULE, 'preferred_device', lambda: 'cpu'):
|
||||
sentinel = SimpleNamespace()
|
||||
with patch.dict('sys.modules', sentence_transformers=SimpleNamespace(
|
||||
SentenceTransformer=lambda *args, **kwargs: sentinel,
|
||||
CrossEncoder=lambda *args, **kwargs: sentinel)):
|
||||
self.assertIs(MODULE.shared_encoder(), sentinel)
|
||||
self.assertIs(MODULE.shared_encoder(), MODULE.shared_encoder())
|
||||
self.assertIs(MODULE.shared_reranker(), sentinel)
|
||||
self.assertIs(MODULE.shared_reranker(), MODULE.shared_reranker())
|
||||
|
||||
def test_rerank_uses_cross_encoder_scores(self):
|
||||
index = MODULE.Index.__new__(MODULE.Index)
|
||||
|
||||
class Reranker:
|
||||
def predict(self, pairs, **kwargs):
|
||||
self.pairs = pairs
|
||||
return [0.1, 0.8, 0.5]
|
||||
|
||||
reranker = Reranker()
|
||||
index.reranker = lambda: reranker
|
||||
rows = [
|
||||
{'path': 'a.md', 'symbol': 'A', 'score': 0.9, 'text': 'first'},
|
||||
{'path': 'b.md', 'symbol': 'B', 'score': 0.8, 'text': 'second'},
|
||||
{'path': 'c.md', 'symbol': 'C', 'score': 0.7, 'text': 'third'},
|
||||
]
|
||||
|
||||
result = index.rerank('query', rows, 2)
|
||||
|
||||
self.assertEqual([item['path'] for item in result], ['b.md', 'c.md'])
|
||||
self.assertEqual(reranker.pairs, [('query', 'Path: a.md\nSymbol: A\n\nfirst'),
|
||||
('query', 'Path: b.md\nSymbol: B\n\nsecond'),
|
||||
('query', 'Path: c.md\nSymbol: C\n\nthird')])
|
||||
|
||||
def test_token_windows_have_exact_overlap_and_no_redundant_tail(self):
|
||||
for size, lengths in [(0, []), (400, [400]), (401, [400, 65]), (736, [400, 400]), (800, [400, 400, 128])]:
|
||||
words = [f'token{number}' for number in range(size)]
|
||||
result = list(MODULE.chunks(' '.join(words), 'plain.txt', tokenize))
|
||||
windows = [content.split() for _, content in result]
|
||||
self.assertEqual([len(window) for window in windows], lengths)
|
||||
for first, second in zip(windows, windows[1:]):
|
||||
self.assertEqual(first[-64:], second[:64])
|
||||
if windows:
|
||||
reconstructed = windows[0] + [word for window in windows[1:] for word in window[64:]]
|
||||
self.assertEqual(reconstructed, words)
|
||||
|
||||
def test_python_symbols_include_decorators_and_restore_parent(self):
|
||||
source = 'class Service:\n @cached\n def load(self):\n return 1\n\nvalue = 2\n\nasync def refresh():\n return value\n'
|
||||
result = list(MODULE.sections(source, 'service.py'))
|
||||
self.assertEqual([symbol for symbol, _ in result], ['Service', 'Service.load', '<module>', 'refresh'])
|
||||
self.assertIn('@cached', result[1][1])
|
||||
self.assertEqual(''.join(content for _, content in result), source)
|
||||
broken = 'def incomplete(\n' + 'word ' * 450
|
||||
self.assertEqual(len(list(MODULE.chunks(broken, 'broken.py', tokenize))), 2)
|
||||
|
||||
def test_markdown_sections_ignore_fenced_headings(self):
|
||||
source = '# Setup\nIntro\n## Install\n```sh\n# not a heading\n```\nSteps\n## Run\nStart\n'
|
||||
result = list(MODULE.chunks(source, 'guide.md', tokenize))
|
||||
self.assertEqual([symbol for symbol, _ in result], ['Setup', 'Setup / Install', 'Setup / Run'])
|
||||
self.assertIn('# not a heading', result[1][1])
|
||||
|
||||
def test_common_declarations_and_config_sections(self):
|
||||
for path, source, symbols in [
|
||||
('service.ts', 'export function load() {\n return 1;\n}\nexport const save = () => 2;\n', ['load', 'save']),
|
||||
('service.cs', 'public class Service\n{\n public void Load()\n {\n }\n}\n', ['Service', 'Load']),
|
||||
('setup.ps1', 'function Get-Result {\n return 1\n}\n', ['Get-Result']),
|
||||
('config.toml', '[server]\nport = 1\n[client]\nport = 2\n', ['server', 'client']),
|
||||
]:
|
||||
self.assertEqual([symbol for symbol, _ in MODULE.sections(source, path)], symbols)
|
||||
|
||||
def test_rrf_rewards_agreement_and_breaks_ties_deterministically(self):
|
||||
self.assertEqual(MODULE.reciprocal_rank_fusion([1, 2, 3], [4, 2, 3]), [2, 3, 1, 4])
|
||||
self.assertEqual(MODULE.reciprocal_rank_fusion([], [4, 2]), [4, 2])
|
||||
|
||||
def test_hybrid_candidates_and_document_metadata(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
for number in range(110):
|
||||
(root / f'{number:03}.md').write_text(f'# Symbol{number}\ncontent {number}', encoding='utf-8', newline='\n')
|
||||
index = MODULE.Index(root, root / 'data')
|
||||
documents = []
|
||||
|
||||
def encode(values, **kwargs):
|
||||
if kwargs.get('prompt_name') != 'query':
|
||||
documents.extend(values)
|
||||
return [embedding() for _ in values]
|
||||
|
||||
pairs = []
|
||||
index.encoder = lambda: SimpleNamespace(encode=encode, tokenizer=tokenize)
|
||||
index.reranker = lambda: SimpleNamespace(predict=lambda values, **kwargs:
|
||||
pairs.extend(values) or [float('109.md' in document) for _, document in values])
|
||||
numpy = SimpleNamespace(dot=lambda a, b: 1.0,
|
||||
frombuffer=lambda blob, **kwargs: array('f', blob), float32=None)
|
||||
original_fusion = MODULE.reciprocal_rank_fusion
|
||||
rankings = []
|
||||
|
||||
def fuse(*values):
|
||||
rankings.extend(values)
|
||||
return original_fusion(*values)
|
||||
|
||||
try:
|
||||
with patch.dict('sys.modules', numpy=numpy), patch.object(MODULE, 'reciprocal_rank_fusion', fuse):
|
||||
results = index.search('109.md Symbol109 content', 20)
|
||||
self.assertEqual([len(ranking) for ranking in rankings], [50, 50])
|
||||
self.assertEqual(len(pairs), 50)
|
||||
self.assertEqual(len(results), 5)
|
||||
self.assertEqual(results[0]['path'], '109.md')
|
||||
self.assertEqual(results[0]['symbol'], 'Symbol109')
|
||||
self.assertIn('Path: 109.md\nSymbol: Symbol109\n\n# Symbol109\ncontent 109', documents)
|
||||
self.assertTrue(all(document in documents for _, document in pairs))
|
||||
with patch.dict('sys.modules', numpy=numpy):
|
||||
self.assertEqual(len(index.search('" OR * () : -')), 5)
|
||||
self.assertEqual(len(index.search('***')), 5)
|
||||
(root / '109.md').unlink()
|
||||
index.search('Symbol109')
|
||||
self.assertEqual(index.db.execute('select count(*) from lexical where lexical match ?', ('"Symbol109"',)).fetchone()[0], 0)
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
def test_old_cache_is_preserved_but_not_reused(self):
|
||||
with TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
data = root / 'data'
|
||||
data.mkdir()
|
||||
identity = os.path.normcase(str(root.resolve())) + '\n' + MODULE.MODEL
|
||||
old = data / (MODULE.hashlib.sha256(identity.encode()).hexdigest() + '.sqlite3')
|
||||
old.write_bytes(b'old cache')
|
||||
index = MODULE.Index(root, data)
|
||||
try:
|
||||
self.assertNotEqual(Path(index.db.execute('pragma database_list').fetchone()[2]), old)
|
||||
self.assertEqual(old.read_bytes(), b'old cache')
|
||||
finally:
|
||||
index.db.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,77 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location('semantic_retrieval', ROOT / 'scripts/lib/semantic-retrieval.py')
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class SemanticRetrievalTests(unittest.TestCase):
|
||||
def test_sync_does_not_register_mcp(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory) / 'home'
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
MODULE.sync(ROOT, home, ['codex', 'claude'], True, True, False)
|
||||
self.assertNotIn('mcp', output.getvalue().lower())
|
||||
|
||||
def test_runtime_prefers_cuda_packages_for_nvidia_gpu(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
target = Path(directory)
|
||||
python = MODULE.python_path(target / '.venv')
|
||||
python.parent.mkdir(parents=True)
|
||||
python.touch()
|
||||
with patch.object(MODULE.shutil, 'which', return_value='nvidia-smi'), patch.object(MODULE.subprocess, 'run') as run:
|
||||
MODULE.ensure_runtime(target, False)
|
||||
|
||||
self.assertEqual(run.call_args.args[0][-2:], ['--extra-index-url', MODULE.CUDA_INDEX])
|
||||
|
||||
def test_runtime_falls_back_to_cpu_packages_when_cuda_install_fails(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
target = Path(directory)
|
||||
python = MODULE.python_path(target / '.venv')
|
||||
python.parent.mkdir(parents=True)
|
||||
python.touch()
|
||||
failure = MODULE.subprocess.CalledProcessError(1, 'pip')
|
||||
with patch.object(MODULE.shutil, 'which', return_value='nvidia-smi'), patch.object(MODULE.subprocess, 'run', side_effect=[failure, None]) as run:
|
||||
MODULE.ensure_runtime(target, False)
|
||||
|
||||
self.assertEqual(run.call_count, 2)
|
||||
self.assertNotIn('--extra-index-url', run.call_args.args[0])
|
||||
|
||||
def test_summary_dry_run_omits_details_without_writes(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory) / 'home'
|
||||
for enabled in (True, False):
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
MODULE.sync(ROOT, home, ['codex'], enabled, True, False, summary=True)
|
||||
self.assertEqual(output.getvalue(), '')
|
||||
self.assertFalse(home.exists())
|
||||
|
||||
def test_disable_removes_owned_runtime_without_cli_registration(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory) / 'home'
|
||||
target = home / '.my-ai-configuration/semantic-retrieval'
|
||||
target.mkdir(parents=True)
|
||||
for name in MODULE.FILES:
|
||||
(target / name).write_text(name)
|
||||
state_path = target.parent / 'semantic-retrieval.json'
|
||||
state_path.write_text(json.dumps({'version': 1, 'clients': ['codex'], 'files': {
|
||||
name: hashlib.sha256((target / name).read_bytes()).hexdigest() for name in MODULE.FILES}}))
|
||||
MODULE.sync(ROOT, home, ['codex'], False, False, False)
|
||||
self.assertFalse(target.exists())
|
||||
self.assertFalse(state_path.exists())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,191 @@
|
||||
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
|
||||
SHELLS = [
|
||||
('powershell', shutil.which('powershell') or shutil.which('pwsh')),
|
||||
('bash', str(Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Git/bin/bash.exe')
|
||||
if os.name == 'nt' else shutil.which('bash')),
|
||||
]
|
||||
|
||||
|
||||
class SessionStateHookTests(unittest.TestCase):
|
||||
def test_git_identity_and_stale_verification(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workspace = Path(directory)
|
||||
env = dict(os.environ, GIT_AUTHOR_NAME='Test', GIT_AUTHOR_EMAIL='test@example.invalid', GIT_COMMITTER_NAME='Test', GIT_COMMITTER_EMAIL='test@example.invalid')
|
||||
subprocess.run(['git', '-C', str(workspace), 'init', '-q'], check=True)
|
||||
(workspace / 'file').write_text('one')
|
||||
subprocess.run(['git', '-C', str(workspace), 'add', 'file'], check=True)
|
||||
subprocess.run(['git', '-C', str(workspace), 'commit', '-qm', 'one'], check=True, env=env)
|
||||
command = [sys.executable, str(ROOT / 'shared/hooks/scripts/session-state.py'), '--workspace', str(workspace)]
|
||||
self.assertEqual(subprocess.run(command + ['--verified', 'check'], capture_output=True).returncode, 0)
|
||||
state = json.loads((workspace / '.ai-session/state.json').read_text())
|
||||
self.assertTrue(state['head'])
|
||||
self.assertEqual(state['verification'][0]['head'], state['head'])
|
||||
(workspace / 'file').write_text('two')
|
||||
subprocess.run(['git', '-C', str(workspace), 'add', 'file'], check=True)
|
||||
subprocess.run(['git', '-C', str(workspace), 'commit', '-qm', 'two'], check=True, env=env)
|
||||
self.assertEqual(subprocess.run(command, capture_output=True).returncode, 0)
|
||||
state = json.loads((workspace / '.ai-session/state.json').read_text())
|
||||
self.assertTrue(state['verificationStale'])
|
||||
self.assertTrue(state['verification'][0]['stale'])
|
||||
|
||||
def test_writer_migrates_legacy_state_and_requires_reset_for_new_goal(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workspace = Path(directory)
|
||||
state_file = workspace / '.ai-session/state.json'
|
||||
state_file.parent.mkdir()
|
||||
state_file.write_text(json.dumps({'goal': 'Original', 'changedFiles': ['a'],
|
||||
'pending': ['b'], 'importantFindings': ['c']}))
|
||||
command = [sys.executable, str(ROOT / 'shared/hooks/scripts/session-state.py'),
|
||||
'--workspace', str(workspace)]
|
||||
result = subprocess.run(command + ['--verified', 'passed'], capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
state = json.loads(state_file.read_text())
|
||||
self.assertEqual(state['changed'], ['a'])
|
||||
self.assertEqual(state['open'], ['b'])
|
||||
self.assertEqual(state['decisions'], ['c'])
|
||||
self.assertFalse({'changedFiles', 'pending', 'importantFindings'} & state.keys())
|
||||
before = state_file.read_bytes()
|
||||
result = subprocess.run(command + ['--goal', 'New'], capture_output=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(state_file.read_bytes(), before)
|
||||
result = subprocess.run(command + ['--reset', '--goal', 'New'], capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
state = json.loads(state_file.read_text())
|
||||
self.assertEqual(state['goal'], 'New')
|
||||
self.assertEqual(state['verified'], [])
|
||||
self.assertEqual(state['changed'], [])
|
||||
state_file.write_text('{"goal":"New","verified":"invalid"}')
|
||||
before = state_file.read_bytes()
|
||||
result = subprocess.run(command + ['--open', 'next'], capture_output=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(state_file.read_bytes(), before)
|
||||
|
||||
def test_writers_preserve_partial_updates_and_escape_values(self):
|
||||
for shell, executable in SHELLS:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
repository = base / 'repository'
|
||||
shutil.copytree(ROOT / 'shared/hooks/scripts', repository / 'shared/hooks/scripts')
|
||||
(repository / 'scripts').mkdir()
|
||||
for filename in ('session-state.ps1', 'session-state.sh'):
|
||||
shutil.copy(ROOT / 'scripts' / filename, repository / 'scripts' / filename)
|
||||
workspace = base / 'workspace ü'
|
||||
workspace.mkdir()
|
||||
environment = dict(os.environ, TASK_WORKSPACE=str(workspace),
|
||||
TASK_GOAL='Repair "quotes"\n' + 'x' * 300)
|
||||
if shell == 'powershell':
|
||||
prefix = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command']
|
||||
commands = [
|
||||
'& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Goal $env:TASK_GOAL -Decisions @("keep", "line two") -Pending @("check")',
|
||||
'& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Verified @("passed")',
|
||||
'& ./scripts/commands/session-state.ps1 -Workspace $env:TASK_WORKSPACE -Pointer',
|
||||
]
|
||||
else:
|
||||
prefix = [executable, '-c']
|
||||
commands = [
|
||||
'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --goal "$TASK_GOAL" --decisions keep "line two" --open check',
|
||||
'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --verified passed',
|
||||
'bash ./scripts/commands/session-state.sh --workspace "$TASK_WORKSPACE" --pointer',
|
||||
]
|
||||
for command in commands:
|
||||
result = subprocess.run(prefix + [command], cwd=repository, capture_output=True,
|
||||
encoding='utf-8', timeout=20, env=environment)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
state_file = workspace / '.ai-session/state.json'
|
||||
self.assertIn(str(state_file), result.stdout)
|
||||
state = json.loads(state_file.read_text(encoding='utf-8-sig'))
|
||||
self.assertEqual(state['goal'], environment['TASK_GOAL'])
|
||||
self.assertEqual(state['decisions'], ['keep', 'line two'])
|
||||
self.assertEqual(state['open'], ['check'])
|
||||
self.assertEqual(state['verified'], ['passed'])
|
||||
self.assertEqual(state['workspace'], str(workspace))
|
||||
self.assertFalse((repository / '.ai-session').exists())
|
||||
self.assertEqual([p.name for p in state_file.parent.iterdir()], ['state.json'])
|
||||
|
||||
def run_hook(self, shell, executable, hook, config, workspace, payload):
|
||||
names = {'pointer': ('Show-SessionStatePointer.ps1', 'show-session-state-pointer.sh'),
|
||||
'compact': ('Record-Compact.ps1', 'record-compact.sh')}
|
||||
script = config / 'hooks/scripts' / names[hook][shell == 'bash']
|
||||
command = ([executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', str(script)]
|
||||
if shell == 'powershell' else [executable, str(script)])
|
||||
result = subprocess.run(command, cwd=workspace, input=payload, capture_output=True,
|
||||
encoding='utf-8', timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr, '')
|
||||
return result.stdout
|
||||
|
||||
def test_workspace_selection_and_read_only_state(self):
|
||||
for shell, executable in SHELLS:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
config = base / 'installed config'
|
||||
shutil.copytree(ROOT / 'shared/hooks/scripts', config / 'hooks/scripts')
|
||||
workspace = base / 'working project'
|
||||
selected = base / 'selected project ü'
|
||||
empty = base / 'empty project'
|
||||
for folder in (config, workspace, selected, empty):
|
||||
folder.mkdir(exist_ok=True)
|
||||
for folder in (config, workspace, selected):
|
||||
(folder / '.ai-session').mkdir()
|
||||
(folder / '.ai-session/state.json').write_text('private state content')
|
||||
for hook in ('pointer', 'compact'):
|
||||
for payload, expected in (
|
||||
('', workspace),
|
||||
('{}', workspace),
|
||||
(json.dumps({'cwd': str(selected)}, ensure_ascii=False), selected),
|
||||
(json.dumps({'cwd': str(empty)}), None),
|
||||
('{"cwd":"relative/path"}', None),
|
||||
('{"cwd":null}', None),
|
||||
('{"cwd":12}', None),
|
||||
('{"cwd":""}', None),
|
||||
('{bad json', None),
|
||||
('[]', None),
|
||||
):
|
||||
with self.subTest(hook=hook, payload=payload):
|
||||
output = self.run_hook(shell, executable, hook, config, workspace, payload)
|
||||
if expected is None:
|
||||
self.assertEqual(output, '')
|
||||
else:
|
||||
self.assertIn(str(expected / '.ai-session/state.json'), output)
|
||||
self.assertNotIn(str(config), output)
|
||||
self.assertNotIn('private state content', output)
|
||||
for folder in (config, workspace, selected):
|
||||
self.assertEqual((folder / '.ai-session/state.json').read_text(), 'private state content')
|
||||
self.assertFalse((empty / '.ai-session').exists())
|
||||
events = (config / '.ai-session/telemetry.jsonl').read_text(encoding='utf-8-sig').splitlines()
|
||||
self.assertEqual(len(events), 10)
|
||||
self.assertTrue(all(json.loads(line)['event'] == 'compact' for line in events))
|
||||
|
||||
def test_missing_workspace_state_succeeds_without_config_fallback(self):
|
||||
for shell, executable in SHELLS:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
config = base / 'config'
|
||||
shutil.copytree(ROOT / 'shared/hooks/scripts', config / 'hooks/scripts')
|
||||
(config / '.ai-session').mkdir()
|
||||
(config / '.ai-session/state.json').write_text('{}')
|
||||
workspace = base / 'workspace'
|
||||
workspace.mkdir()
|
||||
for hook in ('pointer', 'compact'):
|
||||
self.assertEqual(self.run_hook(shell, executable, hook, config, workspace, ''), '')
|
||||
self.assertEqual(list(workspace.iterdir()), [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,90 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$Summary)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$powerShellHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.ps1') -Raw
|
||||
$bashHook = Get-Content -LiteralPath (Join-Path $root 'shared/hooks/flashbang.sh') -Raw
|
||||
if ($powerShellHook -notmatch '\$HoldMs\s*=\s*250' -or $powerShellHook -notmatch '\$FadeMs\s*=\s*250' -or ([regex]::Matches($bashHook, 'default=250').Count -lt 2)) { throw 'Flashbang defaults must total 500 milliseconds on PowerShell and Bash.' }
|
||||
$statusInput = '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"' + $root.Replace('\','\\') + '","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}'
|
||||
$branch = & git -C $root branch --show-current
|
||||
$buildSummaryOutput = & (Join-Path $root 'scripts/commands/build.ps1') -Summary
|
||||
if (@($buildSummaryOutput | Where-Object { $_ -eq 'Build: PASS | 4 packages' }).Count -ne 1) { throw "build.ps1 -Summary must still print the PASS line: $($buildSummaryOutput -join '; ')" }
|
||||
$statusOutput = ($statusInput | & (Join-Path $root 'shared/statusline/statusline.ps1')) -join "`n"
|
||||
$plainStatusOutput = [regex]::Replace($statusOutput, [char]27 + '\[[0-9;]*m', '')
|
||||
$expectedStatusOutput = "Test Model $([char]0x00B7) Review auto $([char]0x00B7) Effort high $([char]0x00B7) TestRepo @ $branch`nCtx 200k $([char]0x00B7) Used 9% $([char]0x00B7) Tokens 16.7k"
|
||||
if ($buildSummaryOutput.Count -ne 1) { throw 'Build summary must contain only one success line.' }
|
||||
if ($plainStatusOutput -ne $expectedStatusOutput) { throw 'PowerShell status line output is incorrect.' }
|
||||
$sourceAgentCount = @(Get-ChildItem (Join-Path $root 'shared/agents') -Directory).Count
|
||||
$sourceSkillCount = @(Get-ChildItem (Join-Path $root 'shared/skills') -Filter 'SKILL.md' -Recurse).Count
|
||||
$ruleSkillCount = @(Import-Csv -LiteralPath (Join-Path $root 'adapters/rule-skills.tsv') -Delimiter ([char]9)).Count
|
||||
foreach ($shell in @('powershell','bash')) {
|
||||
foreach ($client in @('codex','claude')) {
|
||||
$package = Join-Path $root "generated/$client-$shell"
|
||||
$file = if ($client -eq 'codex') { 'config.toml' } else { 'settings.json' }
|
||||
$content = Get-Content -LiteralPath (Join-Path $package $file) -Raw
|
||||
if ($client -eq 'codex') {
|
||||
foreach ($entry in @(Import-Csv (Join-Path $root 'adapters/rule-skills.tsv') -Delimiter ([char]9))) {
|
||||
$rulePath = $entry.rule_file
|
||||
if (Test-Path (Join-Path $root "shared/rules/$($entry.rule_file)") -PathType Container) { $rulePath = "$($entry.rule_file)/index.md" }
|
||||
if ((Get-Content (Join-Path $package 'AGENTS.md') -Raw) -notmatch [regex]::Escape("- rules/$rulePath for $($entry.trigger).")) { throw "Codex rule loading is missing $($entry.rule_file) in $package" }
|
||||
}
|
||||
}
|
||||
if ($client -eq 'claude') { $settings = $content | ConvertFrom-Json }
|
||||
if (@(Get-ChildItem "$package/agents" -File).Count -ne $sourceAgentCount) { throw "Missing agents in $package" }
|
||||
$expectedReviewerShellTool = if ($shell -eq 'powershell') { 'PowerShell' } else { 'Bash' }
|
||||
if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,$expectedReviewerShellTool\r?`$") { throw "Reviewer capability profile is missing in $package" }
|
||||
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
|
||||
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
|
||||
$expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
|
||||
if ($content -notmatch $expected -or $content -match '__HOOK_|__POWERSHELL_HOOK_') { throw "Incorrect shell command in $package" }
|
||||
if ($shell -eq 'powershell' -and $content -match 'WindowStyle\s+Hidden') { throw "PowerShell hook hides the terminal in $package" }
|
||||
if ($client -eq 'codex') {
|
||||
$events = @([regex]::Matches($content, '(?m)^\[\[hooks\.([^.\]]+)\]\]\s*$') | ForEach-Object { $_.Groups[1].Value })
|
||||
if ($events.Count -ne 1 -or $events[0] -ne 'Stop' -or $content -match 'flashbang-if-input' -or $content -match '(?m)^async\s*=\s*true\s*$') { throw "Codex finish hook is incorrect in $package" }
|
||||
if ($content -notmatch 'approvals_reviewer\s*=\s*"auto_review"') { throw "Codex auto review is missing in $package" }
|
||||
if ($content -notmatch '(?m)^max_depth\s*=\s*1\s*$') { throw "Codex agent depth limit is missing in $package" }
|
||||
if ($content -notmatch 'status_line\s*=\s*\["model", "reasoning", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"\]') { throw "Codex status line is incorrect in $package" }
|
||||
} elseif ((($settings.hooks.PSObject.Properties.Name | Sort-Object) -join ',') -ne 'PreCompact,SessionStart,Stop' -or $content -match 'flashbang-if-input' -or $settings.hooks.Stop[0].hooks[0].async) {
|
||||
throw "Claude finish hook is incorrect in $package"
|
||||
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
|
||||
throw "Claude auto permission mode is missing in $package"
|
||||
} elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true) {
|
||||
throw "Claude strict sandbox is missing in $package"
|
||||
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") {
|
||||
throw "Claude status line is incorrect in $package"
|
||||
}
|
||||
$docFile = if ($client -eq 'codex') { 'AGENTS.md' } else { 'CLAUDE.md' }
|
||||
$docContent = Get-Content -LiteralPath (Join-Path $package $docFile) -Raw
|
||||
if (([regex]::Matches($docContent, [regex]::Escape('Apply instructions in this order'))).Count -ne 1) { throw "$client-$shell must embed the priority rule exactly once" }
|
||||
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
|
||||
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
|
||||
foreach ($splitRule in @('angular','wpf','ui-ux')) {
|
||||
$referencesDir = if ($client -eq 'codex') { Join-Path $package "rules/$splitRule/references" } else { Join-Path $package "skills/rules/rules-$splitRule/references" }
|
||||
if (-not (Test-Path $referencesDir) -or (Get-ChildItem $referencesDir -Filter '*.md').Count -eq 0) { throw "$package is missing reference files for $splitRule" }
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($shellSelection in @('1','2')) {
|
||||
foreach ($clientSelection in @('1','2','3')) {
|
||||
$script:answers = [System.Collections.Generic.Queue[string]]::new()
|
||||
$script:answers.Enqueue($shellSelection)
|
||||
$script:answers.Enqueue($clientSelection)
|
||||
function Read-Host { param($Prompt) $answers.Dequeue() }
|
||||
$output = & (Join-Path $root 'scripts/commands/install.ps1') -DryRun 6>$null
|
||||
if ($output -notcontains 'PASS install dry-run') { throw 'Dry-run did not finish.' }
|
||||
$shell = if ($shellSelection -eq '1') { 'powershell' } else { 'bash' }
|
||||
if (($output -join "`n") -notmatch "-$shell") { throw 'Incorrect selected package.' }
|
||||
}
|
||||
}
|
||||
$doctorSummaryOutput = & (Join-Path $root 'scripts/commands/doctor.ps1') -Summary
|
||||
if (@($doctorSummaryOutput | Where-Object { $_ -match '^PASS ' }).Count -ne 0) { throw 'Doctor summary mode must not print individual PASS lines.' }
|
||||
if (@($doctorSummaryOutput | Where-Object { $_ -match '^Doctor: PASS \| \d+ checks$' }).Count -ne 1) { throw "Doctor summary mode must print one 'Doctor: PASS | N checks' line: $($doctorSummaryOutput -join '; ')" }
|
||||
foreach ($entryPoint in @('install','update')) {
|
||||
$preview = @(& (Join-Path $root "scripts/commands/$entryPoint.ps1") -Summary -DryRun -Client Both -Shell PowerShell)
|
||||
if ($preview -match '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) ') { throw "$entryPoint summary leaked per-item output." }
|
||||
if (@($preview | Where-Object { $_ -match ('^' + $entryPoint + ': PASS') }).Count -ne 1) { throw "$entryPoint summary is missing." }
|
||||
}
|
||||
$doctorSummaryOutput | Where-Object { $_ -match '^WARN ' }
|
||||
if ($Summary) { Write-Output 'Tests: PASS | shell packages' } else { Write-Output 'PASS four shell packages and six PowerShell selections' }
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
summary=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--summary) summary=true; shift ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
grep -Eq '\$HoldMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
|
||||
grep -Eq '\$FadeMs[[:space:]]*=[[:space:]]*250' "$root/shared/hooks/flashbang.ps1"
|
||||
[ "$(grep -c 'default=250' "$root/shared/hooks/flashbang.sh")" -ge 2 ]
|
||||
branch=$(git -C "$root" branch --show-current)
|
||||
build_summary_output=$(bash "$root/scripts/commands/build.sh" --summary)
|
||||
if ! printf '%s\n' "$build_summary_output" | grep -qx 'Build: PASS | 4 packages'; then
|
||||
printf 'build.sh --summary must still print the PASS line: %s\n' "$build_summary_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ "$build_summary_output" = 'Build: PASS | 4 packages' ]
|
||||
status_output=$(printf '{"model":{"display_name":"Test Model"},"effort":{"level":"high"},"workspace":{"current_dir":"%s","repo":{"name":"TestRepo"}},"context_window":{"context_window_size":200000,"used_percentage":8.5,"total_input_tokens":15500,"total_output_tokens":1200}}' "$root" | bash "$root/shared/statusline/statusline.sh")
|
||||
plain_status_output=$(printf '%s' "$status_output" | sed -E $'s/\x1b\\[[0-9;]*m//g')
|
||||
expected_status_output=$'Test Model \xc2\xb7 Review auto \xc2\xb7 Effort high \xc2\xb7 TestRepo @ '"$branch"$'\nCtx 200k \xc2\xb7 Used 9% \xc2\xb7 Tokens 16.7k'
|
||||
[ "$plain_status_output" = "$expected_status_output" ]
|
||||
source_agent_count=$(find "$root/shared/agents" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||||
source_skill_count=$(find "$root/shared/skills" -name SKILL.md | wc -l)
|
||||
rule_skill_count=$(($(wc -l < "$root/adapters/rule-skills.tsv") - 1))
|
||||
for shell in powershell bash; do
|
||||
for client in codex claude; do
|
||||
package="$root/generated/$client-$shell"
|
||||
file=config.toml
|
||||
[ "$client" != claude ] || file=settings.json
|
||||
test -f "$package/$file"
|
||||
if [ "$client" = codex ]; then
|
||||
while IFS=$'\t' read -r rule_file _ trigger; do
|
||||
[ "$rule_file" = rule_file ] && continue
|
||||
[ -n "$rule_file" ] || continue
|
||||
rule_path=$rule_file
|
||||
[ -d "$root/shared/rules/$rule_file" ] && rule_path="$rule_file/index.md"
|
||||
grep -Fq -- "- rules/$rule_path for $trigger." "$package/AGENTS.md"
|
||||
done < "$root/adapters/rule-skills.tsv"
|
||||
fi
|
||||
test "$(find "$package/agents" -type f | wc -l)" -eq "$source_agent_count"
|
||||
expected_reviewer_shell_tool=Bash
|
||||
[ "$shell" != powershell ] || expected_reviewer_shell_tool=PowerShell
|
||||
if [ "$client" = claude ]; then grep -q "^tools: Read,Grep,Glob,$expected_reviewer_shell_tool\$" "$package/agents/reviewer.md"; fi
|
||||
expected_skill_count=$source_skill_count
|
||||
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
|
||||
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
|
||||
if [ "$shell" = powershell ]; then
|
||||
grep -q '__POWERSHELL_COMMAND__ .*flashbang.ps1' "$package/$file"
|
||||
! grep -Eq 'WindowStyle[[:space:]]+Hidden' "$package/$file"
|
||||
else
|
||||
grep -q 'bash .*flashbang.sh' "$package/$file"
|
||||
fi
|
||||
if [ "$client" = codex ]; then
|
||||
[ "$(sed -n 's/^\[\[hooks\.\([^].]*\)\]\]$/\1/p' "$package/$file")" = Stop ]
|
||||
! grep -q 'flashbang-if-input' "$package/$file"
|
||||
! grep -Eq '^async[[:space:]]*=[[:space:]]*true[[:space:]]*$' "$package/$file"
|
||||
grep -q 'approvals_reviewer[[:space:]]*=[[:space:]]*"auto_review"' "$package/$file"
|
||||
grep -Eq '^max_depth[[:space:]]*=[[:space:]]*1[[:space:]]*$' "$package/$file"
|
||||
grep -Fq 'status_line = ["model", "reasoning", "approval-mode", "project-name", "git-branch", "context-window-size", "context-used", "used-tokens"]' "$package/$file"
|
||||
else
|
||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert set(settings["hooks"]) == {"PreCompact", "SessionStart", "Stop"}' "$package/$file"
|
||||
! grep -q 'flashbang-if-input' "$package/$file"
|
||||
! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file"
|
||||
grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file"
|
||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}' "$package/$file"
|
||||
statusline_extension='sh'
|
||||
[ "$shell" != powershell ] || statusline_extension=ps1
|
||||
test -f "$package/statusline/statusline.$statusline_extension"
|
||||
grep -q "statusline.$statusline_extension" "$package/$file"
|
||||
fi
|
||||
if grep -Eq '__HOOK_|__POWERSHELL_HOOK_' "$package/$file"; then exit 1; fi
|
||||
doc_file=AGENTS.md
|
||||
[ "$client" != claude ] || doc_file=CLAUDE.md
|
||||
doc_content=$(cat "$package/$doc_file")
|
||||
occurrences=$(printf '%s' "$doc_content" | grep -o 'Apply instructions in this order' | wc -l)
|
||||
[ "$occurrences" -eq 1 ] || { printf '%s-%s must embed the priority rule exactly once\n' "$client" "$shell" >&2; exit 1; }
|
||||
if [ "$client" = codex ] && printf '%s' "$doc_content" | grep -q 'Always load and apply `rules/general\.md`'; then
|
||||
printf '%s must not still instruct loading general.md by path\n' "$package" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$client" = claude ] && [ -f "$package/rules/general.md" ]; then
|
||||
printf 'Claude must not automatically load a second copy of general rules.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
for split_rule in angular wpf ui-ux; do
|
||||
if [ "$client" = codex ]; then
|
||||
references_dir="$package/rules/$split_rule/references"
|
||||
else
|
||||
references_dir="$package/skills/rules/rules-$split_rule/references"
|
||||
fi
|
||||
if [ ! -d "$references_dir" ] || [ -z "$(find "$references_dir" -maxdepth 1 -name '*.md' -print -quit)" ]; then
|
||||
printf '%s is missing reference files for %s\n' "$package" "$split_rule" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
for shell_selection in 1 2; do
|
||||
for client_selection in 1 2 3; do
|
||||
output=$(printf '%s\n%s\n' "$shell_selection" "$client_selection" | bash "$root/scripts/commands/install.sh" --dry-run 2>&1) || { printf '%s\n' "$output" >&2; exit 1; }
|
||||
printf '%s\n' "$output" | grep -E 'WARN|FAIL|WARNING|ERROR' || true
|
||||
[[ "$output" == *'PASS install dry-run'* ]]
|
||||
shell=powershell
|
||||
[ "$shell_selection" != 2 ] || shell=bash
|
||||
[[ "$output" == *"-$shell"* ]]
|
||||
done
|
||||
done
|
||||
doctor_summary_output=$(bash "$root/scripts/commands/doctor.sh" --summary)
|
||||
if printf '%s\n' "$doctor_summary_output" | grep -q '^PASS '; then
|
||||
printf 'Doctor summary mode must not print individual PASS lines.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! printf '%s\n' "$doctor_summary_output" | grep -Eq '^Doctor: PASS \| [0-9]+ checks$'; then
|
||||
printf "Doctor summary mode must print one 'Doctor: PASS | N checks' line: %s\n" "$doctor_summary_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
for entry_point in install update; do
|
||||
preview=$(bash "$root/scripts/commands/$entry_point.sh" --summary --dry-run --client both --shell bash)
|
||||
if printf '%s\n' "$preview" | grep -Eq '^(SOURCE|CREATE|UPDATE|UNCHANGED|BACKUP|DRYRUN|PASS) '; then
|
||||
printf '%s summary leaked per-item output.\n' "$entry_point" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$preview" | grep -Eiq "^$entry_point: PASS"
|
||||
done
|
||||
printf '%s\n' "$doctor_summary_output" | grep '^WARN ' || true
|
||||
if [ "$summary" = true ]; then printf 'Tests: PASS | shell packages\n'; else printf 'PASS four shell packages and six Bash selections\n'; fi
|
||||
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
class TelemetryTests(unittest.TestCase):
|
||||
def test_bash_record_disable_and_summary(self):
|
||||
bash = shutil.which('bash')
|
||||
if not bash:
|
||||
self.skipTest('Bash unavailable')
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
script = root / 'scripts/commands/telemetry.sh'
|
||||
script.parent.mkdir()
|
||||
shutil.copy(ROOT / 'scripts/commands/telemetry.sh', script)
|
||||
env = dict(os.environ, AI_CONFIG_TELEMETRY='1')
|
||||
result = subprocess.run([bash, str(script), 'record', 'subagent-stop', '--role', 'reviewer', '--filesRead', '3', '--outcome', 'findings'], env=env, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
event = json.loads((root / '.ai-session/telemetry.jsonl').read_text().strip())
|
||||
self.assertEqual(event['role'], 'reviewer')
|
||||
self.assertEqual(event['filesRead'], 3)
|
||||
self.assertNotIn('prompt', event)
|
||||
disabled = dict(env, AI_CONFIG_TELEMETRY='0')
|
||||
subprocess.run([bash, str(script), 'record', 'ignored'], env=disabled, check=True)
|
||||
summary = subprocess.run([bash, str(script), 'summary'], env=env, capture_output=True, text=True, check=True)
|
||||
self.assertIn('reviewer: runs 1 findings 1', summary.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user