Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
0e72683
add registry-related client and tests
psilabs-dev Mar 24, 2026
87b3347
switch to git provider
psilabs-dev Mar 24, 2026
f8156d1
add test_plugin_install_conflict
psilabs-dev Mar 24, 2026
0fca8bc
update plugin tests
psilabs-dev Mar 25, 2026
d74153c
use plugin update structure
psilabs-dev Mar 25, 2026
8ed21f1
update registry endpoint
psilabs-dev Mar 26, 2026
95c0846
update test target
psilabs-dev Mar 26, 2026
ec2a751
apply ratelimit flags to registry tests
psilabs-dev Mar 26, 2026
c6bcb17
add ratelimit to registry tests
psilabs-dev Mar 26, 2026
2e3b3e9
add optional registry field for GetAvailablePluginsResponsePlugin
psilabs-dev Mar 31, 2026
4597539
add some commented test cases
psilabs-dev Mar 31, 2026
8c230d0
add plugin priority-related tests
psilabs-dev Mar 31, 2026
e5457f7
add more plugin priority-related tests
psilabs-dev Apr 1, 2026
df73ac2
fix registry tests
psilabs-dev Apr 2, 2026
345f1fb
add test_plugin_uninstall_not_listed
psilabs-dev Apr 2, 2026
a95141e
add ratelimit flags
psilabs-dev Apr 2, 2026
b317a87
wording
psilabs-dev Apr 11, 2026
ab4009b
add more registry integration tests
psilabs-dev Apr 12, 2026
e9034fa
extend test_registry_update_relink
psilabs-dev Apr 12, 2026
05adca2
add enabled to UpdatePluginConfigRequest
psilabs-dev Apr 13, 2026
08f7536
add registry and plugin integration tests
psilabs-dev Apr 13, 2026
7dd0efe
update registry tests and add status code expectations
psilabs-dev Apr 13, 2026
475c024
add test_sideloaded_script_replaces_managed_duplicate_without_duplica…
psilabs-dev Apr 13, 2026
d6a2650
split registry integration tests
psilabs-dev Apr 14, 2026
17a3e57
add sideloaded test
psilabs-dev Apr 15, 2026
4db6572
update registry tests
psilabs-dev Apr 17, 2026
af2fe3e
add a registry upgrade test case
psilabs-dev Apr 17, 2026
0f576e7
update registry tests
psilabs-dev Apr 19, 2026
ed42707
increase registry test coverage
psilabs-dev Apr 22, 2026
32ff239
update tests to spec for existing logic
psilabs-dev Apr 25, 2026
3288a9b
update tests
psilabs-dev Apr 26, 2026
1b3b712
stop should not be clearing data
psilabs-dev Apr 26, 2026
bc2b825
add transactional tests, extend registry
psilabs-dev Apr 29, 2026
78bfffd
remove sideloaded tests
psilabs-dev Apr 30, 2026
8cef172
clean up sideloaded hacks
psilabs-dev May 2, 2026
bca427d
remove channel (and add a stub)
psilabs-dev May 5, 2026
6b43088
extend registry installation tests
psilabs-dev May 6, 2026
da639e2
add default registry integration test
psilabs-dev May 6, 2026
39c4549
add test_composite_registry
psilabs-dev May 6, 2026
0766fa3
remove single-registry limit
psilabs-dev May 7, 2026
cfd524c
add mutation survivor tests
psilabs-dev May 7, 2026
2d1df85
read from the right database
psilabs-dev May 7, 2026
ffdb872
undisable transactional registry tests
psilabs-dev May 8, 2026
8a37551
UpdateMetadataPluginConfig
psilabs-dev May 8, 2026
1d1b047
cleanup more gracefully
psilabs-dev May 9, 2026
ab3dc1f
strengthen the test and dont do lottery
psilabs-dev May 9, 2026
0e325c1
strengthen registry tests
psilabs-dev May 9, 2026
8268068
separate getters from setters
psilabs-dev May 9, 2026
76338ef
add created and updated
psilabs-dev May 9, 2026
11632b3
add test case for https://github.com/Difegue/LANraragi/pull/1511\#dis…
psilabs-dev May 13, 2026
de93e06
tighten the test cases
psilabs-dev May 13, 2026
1cc6a29
align registry tests to PR
psilabs-dev May 13, 2026
036a4c9
remove metadata plugin features
psilabs-dev May 14, 2026
5d16503
cdn-related tests
psilabs-dev May 13, 2026
808d577
hibernate metadata plugin tests
psilabs-dev May 14, 2026
d727823
xfail test_plugin_not_available
psilabs-dev May 16, 2026
becba2a
remove all traces of installed
psilabs-dev May 16, 2026
611f774
consolidate all to provider
psilabs-dev May 17, 2026
3bb3405
remove cleared index assumption
psilabs-dev May 18, 2026
03ff215
switch target lrr to dev-registry/backend
psilabs-dev May 24, 2026
0c225ee
enforce absolute path for local registry
psilabs-dev May 25, 2026
4baa86d
add plugin test that captures provenance wipe
psilabs-dev May 26, 2026
087b485
add server status api method and tests
psilabs-dev Jun 30, 2026
f1503d0
add restart and close connections for registry
psilabs-dev Jun 14, 2026
ec07cd9
server restart flag is in server info
psilabs-dev Jul 12, 2026
85133b0
add local registry test
psilabs-dev Jul 8, 2026
040af3f
move install plugin to minion
psilabs-dev Jul 12, 2026
4b1e56b
worker can be null
psilabs-dev Jul 13, 2026
edd6604
missing migrations to minion
psilabs-dev Jul 13, 2026
d19d171
rename default registry to ougi
psilabs-dev Jul 13, 2026
a5a9721
divorce registry from deployment
psilabs-dev Jul 14, 2026
72f182f
WIP: registry integration tests (archived, unfinished)
psilabs-dev Sep 6, 2026
7e19f1f
Merge branch 'dev' into dev-registry/main
psilabs-dev Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ on:
lrr_repository:
description: 'LANraragi repository path'
required: true
default: 'difegue/LANraragi'
default: 'psilabs-dev/LANraragi'
lrr_ref:
description: 'LANraragi ref (branch, tag, or commit SHA)'
required: true
default: 'dev'
default: 'dev-registry/backend'

env:
LRR_REPOSITORY: ${{ github.event.inputs.lrr_repository || 'difegue/LANraragi' }}
LRR_REF: ${{ github.event.inputs.lrr_ref || 'dev' }}
LRR_REPOSITORY: ${{ github.event.inputs.lrr_repository || 'psilabs-dev/LANraragi' }}
LRR_REF: ${{ github.event.inputs.lrr_ref || 'dev-registry/backend' }}

jobs:

Expand Down Expand Up @@ -104,7 +104,8 @@ jobs:
--server-logs "$GITHUB_WORKSPACE/server-logs" \
--playwright \
--no-rate-limit \
--npseed 42
--npseed 42 \
--dev registry
env:
DOCKER_HOST: unix:///var/run/docker.sock

Expand Down Expand Up @@ -264,7 +265,8 @@ jobs:
--server-logs "$env:GITHUB_WORKSPACE\server-logs" `
--playwright `
--no-rate-limit `
--npseed 42
--npseed 42 `
--dev registry

- name: Upload server logs
if: failure()
Expand Down
16 changes: 16 additions & 0 deletions integration_tests/src/aio_lanraragi_tests/deployment/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ def redis_dir(self) -> Path:
redis_dirname = self.resource_prefix + "redis"
return self.staging_dir / redis_dirname

@property
def shared_dir(self) -> Path:
"""
Host directory bind-mounted into LRR, for test fixtures that LRR must be
able to read. Contents are visible to LRR at
``lrr_mount_path``.
"""
return self.staging_dir / (self.resource_prefix + "shared")

@abc.abstractmethod
def lrr_mount_path(self, host_path: Path) -> str:
"""
Map a host path under ``shared_dir`` to the path at which LRR reads it
(a container path under Docker, the host path itself on native runs).
"""

@property
def redis_client(self) -> redis.Redis:
"""
Expand Down
59 changes: 59 additions & 0 deletions integration_tests/src/aio_lanraragi_tests/deployment/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from aio_lanraragi_tests.utils.docker import set_pdeathsig

DEFAULT_LANRARAGI_DOCKER_TAG = "difegue/lanraragi"
LRR_SHARED_CONTAINER_PATH = "/srv/shared"

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -223,6 +224,26 @@ def plugins_root_dir(self) -> Path:
dirname = self.resource_prefix + "plugins"
return self.staging_dir / dirname

@property
def plugin_managed_dir(self) -> Path:
"""
Bind mount for LRR container:/home/koyomi/lanraragi/lib/LANraragi/Plugin/Managed.
"""
dirname = self.resource_prefix + "plugin_managed"
return self.staging_dir / dirname

@property
def plugin_sideloaded_dir(self) -> Path:
"""
Bind mount for LRR container:/home/koyomi/lanraragi/lib/LANraragi/Plugin/Sideloaded.
"""
dirname = self.resource_prefix + "plugin_sideloaded"
return self.staging_dir / dirname

def lrr_mount_path(self, host_path: Path) -> str:
rel = Path(host_path).relative_to(self.shared_dir)
return f"{LRR_SHARED_CONTAINER_PATH}/{rel.as_posix()}"

@property
def docker_client(self) -> docker.DockerClient:
return self._docker_client
Expand Down Expand Up @@ -516,6 +537,9 @@ def setup(
thumb_dir = self.thumb_dir
logs_dir = self.logs_dir
redis_dir = self.redis_dir
plugin_managed_dir = self.plugin_managed_dir
plugin_sideloaded_dir = self.plugin_sideloaded_dir
shared_dir = self.shared_dir
if contents_dir.exists():
self.logger.debug(f"Contents directory exists: {contents_dir}")
else:
Expand All @@ -541,6 +565,27 @@ def setup(
# newly created directory before it is used as a bind mount source.
if sys.platform == "darwin":
time.sleep(1)
if plugin_managed_dir.exists():
self.logger.debug(f"Plugin managed directory exists: {plugin_managed_dir}")
else:
self.logger.debug(f"Creating plugin managed dir: {plugin_managed_dir}")
plugin_managed_dir.mkdir(parents=True, exist_ok=False)
if sys.platform == "darwin":
time.sleep(1)
if plugin_sideloaded_dir.exists():
self.logger.debug(f"Plugin sideloaded directory exists: {plugin_sideloaded_dir}")
else:
self.logger.debug(f"Creating plugin sideloaded dir: {plugin_sideloaded_dir}")
plugin_sideloaded_dir.mkdir(parents=True, exist_ok=False)
if sys.platform == "darwin":
time.sleep(1)
if shared_dir.exists():
self.logger.debug(f"Shared directory exists: {shared_dir}")
else:
self.logger.debug(f"Creating shared dir: {shared_dir}")
shared_dir.mkdir(parents=True, exist_ok=False)
if sys.platform == "darwin":
time.sleep(1)

# log the setup resource allocations for user to see
# the docker image is not included, haven't decided how to classify it yet.
Expand Down Expand Up @@ -716,6 +761,9 @@ def setup(
str(contents_dir): {"bind": "/home/koyomi/lanraragi/content", "mode": "rw"},
str(thumb_dir): {"bind": "/home/koyomi/lanraragi/thumb", "mode": "rw"},
str(logs_dir): {"bind": "/home/koyomi/lanraragi/log", "mode": "rw"},
str(plugin_managed_dir): {"bind": "/home/koyomi/lanraragi/lib/LANraragi/Plugin/Managed", "mode": "rw"},
str(plugin_sideloaded_dir): {"bind": "/home/koyomi/lanraragi/lib/LANraragi/Plugin/Sideloaded", "mode": "rw"},
str(self.shared_dir): {"bind": LRR_SHARED_CONTAINER_PATH, "mode": "ro"},
}
lrr_volumes.update(plugin_volumes)
self.lrr_container = self.docker_client.containers.create(
Expand Down Expand Up @@ -910,6 +958,8 @@ def _reset_test_env(self, remove_data: bool=False):
self.lrr_container.exec_run(["sh", "-c", 'rm -rf /home/koyomi/lanraragi/content/*'], user='root')
self.lrr_container.exec_run(["sh", "-c", 'rm -rf /home/koyomi/lanraragi/thumb/*'], user='root')
self.lrr_container.exec_run(["sh", "-c", 'rm -rf /home/koyomi/lanraragi/log/*'], user='root')
self.lrr_container.exec_run(["sh", "-c", 'rm -rf /home/koyomi/lanraragi/lib/LANraragi/Plugin/Managed/*'], user='root')
self.lrr_container.exec_run(["sh", "-c", 'rm -rf /home/koyomi/lanraragi/lib/LANraragi/Plugin/Sideloaded/*'], user='root')
else:
self.logger.info(f"Container not running with status {status} (no teardown commands run): {self.lrr_container_name}")
if self.lrr_container:
Expand Down Expand Up @@ -948,6 +998,15 @@ def _reset_test_env(self, remove_data: bool=False):
if self.plugins_root_dir.exists():
shutil.rmtree(self.plugins_root_dir)
self.logger.debug(f"Removed plugins directory: {self.plugins_root_dir}")
if self.plugin_managed_dir.exists():
shutil.rmtree(self.plugin_managed_dir)
self.logger.debug(f"Removed plugin managed directory: {self.plugin_managed_dir}")
if self.plugin_sideloaded_dir.exists():
shutil.rmtree(self.plugin_sideloaded_dir)
self.logger.debug(f"Removed plugin sideloaded directory: {self.plugin_sideloaded_dir}")
if self.shared_dir.exists():
shutil.rmtree(self.shared_dir)
self.logger.debug(f"Removed shared directory: {self.shared_dir}")
redis_conf_staging = self.staging_dir / (self.resource_prefix + "redis.conf")
if redis_conf_staging.exists():
redis_conf_staging.unlink()
Expand Down
22 changes: 22 additions & 0 deletions integration_tests/src/aio_lanraragi_tests/deployment/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ def lrr_lanraragi_path(self) -> Path:
def lrr_plugin_dir(self) -> Path:
return self.windist_dir / "lib" / "LANraragi" / "Plugin"

@property
def plugin_managed_dir(self) -> Path:
return self.lrr_plugin_dir / "Managed"

@property
def plugin_sideloaded_dir(self) -> Path:
return self.lrr_plugin_dir / "Sideloaded"

def lrr_mount_path(self, host_path: Path) -> str:
return str(host_path)

def __init__(
self, windist_path: str, staging_directory: str, resource_prefix: str, port_offset: int,
logger: logging.Logger | None=None
Expand Down Expand Up @@ -244,6 +255,7 @@ def setup(
log_dir = self.logs_dir
pid_dir = self.pid_dir
redis_dir = self.redis_dir
shared_dir = self.shared_dir
if contents_dir.exists():
self.logger.debug(f"Contents directory exists: {contents_dir}")
else:
Expand Down Expand Up @@ -274,6 +286,11 @@ def setup(
else:
self.logger.debug(f"Creating Redis directory: {redis_dir}")
redis_dir.mkdir(parents=True, exist_ok=False)
if shared_dir.exists():
self.logger.debug(f"Shared directory exists: {shared_dir}")
else:
self.logger.debug(f"Creating shared directory: {shared_dir}")
shared_dir.mkdir(parents=True, exist_ok=False)

# we need to handle cases where existing services are running.
# Unlike docker, we have no idea whether we can skip recreation of
Expand Down Expand Up @@ -391,6 +408,7 @@ def teardown(self, remove_data: bool=False):
windist_dir = self.windist_dir
redis_dir = self.redis_dir
temp_dir = self.temp_dir
shared_dir = self.shared_dir
self.stop()
if hasattr(self, "_redis_client") and self._redis_client is not None:
self._redis_client.close()
Expand Down Expand Up @@ -419,6 +437,10 @@ def teardown(self, remove_data: bool=False):
self._remove_ro(temp_dir)
shutil.rmtree(temp_dir)
self.logger.debug(f"Removed temp directory: {temp_dir}")
if shared_dir.exists():
self._remove_ro(shared_dir)
shutil.rmtree(shared_dir)
self.logger.debug(f"Removed shared directory: {shared_dir}")

@override
def start_lrr(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Module for registry data structures, APIs, and utilities.
"""
80 changes: 80 additions & 0 deletions integration_tests/src/aio_lanraragi_tests/registries/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@

import abc
import hashlib

REGISTRY_SCHEMA_VERSION = 1


class AbstractRegistry(abc.ABC):

def __init__(self, generated_at: str = "2026-01-01T00:00:00Z"):
self._generated_at = generated_at
# namespace -> {"type": str, "versions": {version: version_record}}
self._plugins: dict[str, dict] = {}
# (namespace, version) -> artifact bytes staged for generate_manifest()
self._artifacts: dict[tuple[str, str], bytes] = {}

def add_plugin(
self,
namespace: str,
plugin_type: str,
version: str,
*,
name: str,
author: str,
description: str,
artifact_content: bytes | str | None = None,
artifact_relpath: str | None = None,
published_at: str | None = None,
sha256: str | None = None,
) -> None:
artifact = artifact_relpath or f"artifacts/{namespace}/{version}/{namespace}.pm"
if artifact_content is not None:
content = artifact_content.encode("utf-8") if isinstance(artifact_content, str) else artifact_content
self._artifacts[(namespace, version)] = content
if sha256 is None:
sha256 = hashlib.sha256(content).hexdigest()
elif sha256 is None:
raise ValueError("sha256 must be provided when artifact_content is None")

record = self._plugins.setdefault(namespace, {"type": plugin_type, "versions": {}})
record["type"] = plugin_type
record["versions"][version] = {
"version": version,
"name": name,
"author": author,
"description": description,
"artifact": artifact,
"sha256": sha256,
"published_at": published_at or self._generated_at,
}

def remove_plugin(self, namespace: str, version: str | None = None) -> None:
if namespace not in self._plugins:
return
if version is None:
del self._plugins[namespace]
self._artifacts = {k: v for k, v in self._artifacts.items() if k[0] != namespace}
return
self._plugins[namespace]["versions"].pop(version, None)
self._artifacts.pop((namespace, version), None)
if not self._plugins[namespace]["versions"]:
del self._plugins[namespace]

def manifest(self) -> dict:
return {
"version": REGISTRY_SCHEMA_VERSION,
"generated_at": self._generated_at,
"plugins": {
namespace: {
"namespace": namespace,
"type": record["type"],
"versions": record["versions"],
}
for namespace, record in self._plugins.items()
},
}

@abc.abstractmethod
def generate_manifest(self) -> None:
"""Flush the manifest and any staged artifacts to the registry's backing store."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import json
from pathlib import Path

from aio_lanraragi_tests.registries.base import AbstractRegistry


class LocalRegistry(AbstractRegistry):

def __init__(self, name: str, root: Path):
super().__init__()
self.name = name
self._root = Path(root)

@property
def root(self) -> Path:
return self._root

@property
def registry_json_path(self) -> Path:
return self._root / "registry.json"

def generate_manifest(self) -> None:
self._root.mkdir(parents=True, exist_ok=True)
for (namespace, version), content in self._artifacts.items():
relpath = self._plugins[namespace]["versions"][version]["artifact"]
artifact_path = self._root / relpath
artifact_path.parent.mkdir(parents=True, exist_ok=True)
artifact_path.write_bytes(content)
self.registry_json_path.write_text(json.dumps(self.manifest()), encoding="utf-8")
Loading
Loading