mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 12:25:29 +08:00
189 lines
8.8 KiB
Python
189 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
import zipfile
|
|
|
|
import yaml
|
|
|
|
from check_catalog import ROOT, check_catalog
|
|
from package_plugin import package_plugin
|
|
|
|
CORE = ROOT.parent
|
|
spec = importlib.util.spec_from_file_location('core_artifacts', CORE / 'scripts/check_core_artifacts.py')
|
|
core_artifacts = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(core_artifacts)
|
|
|
|
|
|
def workflow(name):
|
|
return yaml.safe_load((CORE / '.github/workflows' / name).read_text())
|
|
|
|
|
|
class RepositoryTests(unittest.TestCase):
|
|
def test_imported_debug_configuration_is_not_lost(self):
|
|
relative = 'misc/AgenticRAG/.vscode/launch.json'
|
|
self.assertTrue((ROOT / relative).is_file())
|
|
self.assertIn('!misc/AgenticRAG/.vscode/', (ROOT / '.gitignore').read_text())
|
|
|
|
def test_release_zip_excludes_sources_and_packages(self):
|
|
data = workflow('build-release-artifacts.yaml')
|
|
steps = data['jobs']['build-artifacts']['steps']
|
|
script = next(step['run'] for step in steps if step.get('name') == 'Upload To Release')
|
|
command = next(line for line in script.splitlines() if line.strip().startswith('zip -r '))
|
|
command = command.replace('${{ steps.check_version.outputs.version }}', 'test')
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
source = root / 'source'
|
|
(source / 'plugins/misc/HelloPlugin').mkdir(parents=True)
|
|
(source / 'plugins/misc/HelloPlugin/main.py').write_text('plugin')
|
|
(source / 'unexpected.lbpkg').write_text('package')
|
|
(source / 'main.py').write_text('core')
|
|
subprocess.run(
|
|
['bash', '-c', command],
|
|
cwd=source,
|
|
env={**os.environ, 'RUNNER_TEMP': directory},
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
artifact = root / 'langbot-test-all.zip'
|
|
core_artifacts.check_artifact(artifact)
|
|
with zipfile.ZipFile(artifact) as archive:
|
|
self.assertIn('main.py', archive.namelist())
|
|
|
|
def test_fnos_real_staging_command_excludes_sources_and_packages(self):
|
|
script = (CORE / 'packaging/fnos/build.sh').read_text()
|
|
command = 'rsync -a ' + script.split('rsync -a ', 1)[1].split('\n\n', 1)[0]
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
source = root / 'source'
|
|
destination = root / 'fnos'
|
|
(source / 'plugins/Runner/LocalAgent').mkdir(parents=True)
|
|
(source / 'plugins/Runner/LocalAgent/main.py').write_text('plugin')
|
|
(source / 'unexpected.lbpkg').write_text('package')
|
|
(source / 'main.py').write_text('core')
|
|
(destination / 'app/langbot').mkdir(parents=True)
|
|
subprocess.run(
|
|
['bash', '-c', command],
|
|
cwd=source,
|
|
env={**os.environ, 'FPK_DIR': str(destination)},
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
core_artifacts.check_artifact(destination / 'app/langbot')
|
|
self.assertTrue((destination / 'app/langbot/main.py').is_file())
|
|
|
|
def test_catalog_and_readme_cover_all_plugins(self):
|
|
catalog = check_catalog()
|
|
self.assertEqual(json.loads((ROOT / 'catalog.json').read_text()), catalog)
|
|
readme = (ROOT / 'README.md').read_text()
|
|
for entry in catalog:
|
|
self.assertIn(f'({entry["path"]}/README.md)', readme)
|
|
|
|
def test_original_identities_are_preserved(self):
|
|
imported = json.loads((ROOT / 'SOURCE.json').read_text())
|
|
current = {entry['id']: entry['path'] for entry in check_catalog()}
|
|
for identity, relative in imported['plugins'].items():
|
|
self.assertEqual(current[identity], relative)
|
|
|
|
def test_plugin_workflows_do_not_release_core_or_checkout_old_repo(self):
|
|
for path in (CORE / '.github/workflows').glob('plugins-*.yml'):
|
|
text = path.read_text()
|
|
data = workflow(path.name)
|
|
events = data.get('on', data.get(True))
|
|
self.assertNotIn('release', events)
|
|
self.assertNotIn('langbot-app/langbot-plugins', text)
|
|
self.assertNotIn('secrets.', text)
|
|
for event in ('push', 'pull_request'):
|
|
if event in events:
|
|
self.assertTrue(
|
|
all(
|
|
pattern.startswith(('plugins/', '.github/workflows/plugins-'))
|
|
for pattern in events[event]['paths']
|
|
)
|
|
)
|
|
for job in data['jobs'].values():
|
|
default = job.get('defaults', {}).get('run', {}).get('working-directory', '.')
|
|
self.assertTrue((CORE / default).is_dir())
|
|
for step in job['steps']:
|
|
directory = step.get('working-directory', default)
|
|
self.assertTrue((CORE / directory).is_dir(), directory)
|
|
self.assertNotIn('uv publish', step.get('run', ''))
|
|
|
|
def test_every_plugin_category_has_independent_ci(self):
|
|
for name in ('runner', 'local-agent', 'langrag', 'misc', 'knowledgeengine-shared'):
|
|
data = workflow(f'plugins-{name}-tests.yml')
|
|
self.assertIn('workflow_dispatch', data.get('on', data.get(True)))
|
|
self.assertEqual(data['permissions'], {'contents': 'read'})
|
|
|
|
def test_core_release_jobs_reject_plugin_tags(self):
|
|
for filename, job in (
|
|
('build-release-artifacts.yaml', 'build-artifacts'),
|
|
('build-fnos-fpk.yaml', 'build-fnos-fpk'),
|
|
('build-docker-image.yml', 'publish-docker-image'),
|
|
('publish-to-pypi.yml', 'build-and-publish'),
|
|
('discord-release.yml', 'announce'),
|
|
):
|
|
condition = workflow(filename)['jobs'][job]['if']
|
|
self.assertIn("!startsWith(github.event.release.tag_name, 'plugins/')", condition)
|
|
|
|
def test_core_consumer_hashes_match_in_tree_sources(self):
|
|
lock = json.loads((CORE / 'tests/fixtures/legacy_plugin_identity_sources.json').read_text())
|
|
self.assertEqual(lock['root'], 'plugins')
|
|
self.assertEqual(lock['repository'], 'langbot-app/LangBot')
|
|
for key in ('files', 'local_agent_files'):
|
|
for relative, expected in lock[key].items():
|
|
self.assertEqual(hashlib.sha256((ROOT / relative).read_bytes()).hexdigest(), expected, relative)
|
|
|
|
def test_packaging_exclusions_are_present(self):
|
|
self.assertIn('prune plugins', (CORE / 'MANIFEST.in').read_text())
|
|
self.assertIn('plugins/', (CORE / '.dockerignore').read_text().splitlines())
|
|
self.assertIn('/plugins export-ignore', (CORE / '.gitattributes').read_text())
|
|
self.assertIn("--exclude='/plugins/'", (CORE / 'packaging/fnos/build.sh').read_text())
|
|
release = (CORE / '.github/workflows/build-release-artifacts.yaml').read_text()
|
|
self.assertIn('!plugins/**', release)
|
|
self.assertIn("-x 'plugins/*' '*.lbpkg'", release)
|
|
|
|
def test_core_artifact_checker_rejects_plugin_content(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
for member in (
|
|
'plugins/misc/HelloPlugin/main.py',
|
|
'langbot-1/plugins/catalog.json',
|
|
'nested/HelloPlugin.lbpkg',
|
|
):
|
|
artifact = Path(directory) / 'core.zip'
|
|
with zipfile.ZipFile(artifact, 'w') as archive:
|
|
archive.writestr(member, 'unexpected')
|
|
with self.assertRaises(ValueError):
|
|
core_artifacts.check_artifact(artifact)
|
|
|
|
def test_core_artifact_checker_accepts_host_plugin_bridge(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
artifact = Path(directory) / 'core.zip'
|
|
with zipfile.ZipFile(artifact, 'w') as archive:
|
|
archive.writestr('langbot/pkg/plugin/connector.py', 'host')
|
|
core_artifacts.check_artifact(artifact)
|
|
|
|
def test_package_selection_rejects_traversal_and_root(self):
|
|
with tempfile.TemporaryDirectory() as directory, patch('package_plugin.subprocess.run') as build:
|
|
for selected in ('..', '.', 'Runner', 'Runner/LocalAgent/../LocalAgent', '/tmp/plugin'):
|
|
with self.assertRaises(ValueError):
|
|
package_plugin(selected, Path(directory))
|
|
build.assert_not_called()
|
|
|
|
def test_no_plugin_build_output_inside_sources(self):
|
|
with patch('package_plugin.subprocess.run') as build:
|
|
with self.assertRaises(ValueError):
|
|
package_plugin('Runner/LocalAgent', ROOT / 'dist')
|
|
build.assert_not_called()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|