Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 22 additions & 14 deletions tests/api/test_client_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def _log_429(fn, *args, **kwargs):
raise


def _to_bytes(files: dict) -> dict:
"""Convert a str-valued dict to bytes-valued for upload_file()."""
return {
k: (v.encode("utf-8") if isinstance(v, str) else v)
for k, v in files.items()
}


# ---------------------------------------------------------------------------
# Framework-specific mock file sets
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -194,7 +202,7 @@ def test_05_check_repo_bool(self):
# 03. Upload + Create (nanobot — richest file set)
# -----------------------------------------------------------------------
def test_06_upload_and_create(self):
file_id = _log_429(self.client.upload_file, NANOBOT_FILES)
file_id = _log_429(self.client.upload_file, _to_bytes(NANOBOT_FILES))
self.assertTrue(file_id)

result = _log_429(
Expand All @@ -211,7 +219,7 @@ def test_06_upload_and_create(self):
def test_07_repeated_upload(self):
_wait_server(3)
for i in range(2):
fid = _log_429(self.client.upload_file, NANOBOT_FILES)
fid = _log_429(self.client.upload_file, _to_bytes(NANOBOT_FILES))
self.assertTrue(fid)
result = _log_429(
self.client.create_repo,
Expand All @@ -229,7 +237,7 @@ def test_08_modify_and_reupload(self):
modified["SOUL.md"] += "\n## Custom Section\nUser added this.\n"
modified["new_file.md"] = "# New File\nAdded in update.\n"

fid = _log_429(self.client.upload_file, modified)
fid = _log_429(self.client.upload_file, _to_bytes(modified))
self.assertTrue(fid)

result = _log_429(
Expand Down Expand Up @@ -299,7 +307,7 @@ def test_14_repeated_download(self):
# 09. E2E roundtrip
# -----------------------------------------------------------------------
def test_15_e2e_roundtrip(self):
fid = _log_429(self.client.upload_file, NANOBOT_FILES)
fid = _log_429(self.client.upload_file, _to_bytes(NANOBOT_FILES))
self.assertTrue(fid)
_log_429(
self.client.create_repo,
Expand Down Expand Up @@ -329,7 +337,7 @@ def test_16_multi_framework_upload(self):
for fw, files in ALL_FRAMEWORK_FILES.items():
with self.subTest(framework=fw):
agent = f"{AGENT_NAME}-{fw}"
fid = _log_429(self.client.upload_file, files)
fid = _log_429(self.client.upload_file, _to_bytes(files))
self.assertTrue(fid)
try:
result = _log_429(
Expand All @@ -351,7 +359,7 @@ def test_17_cross_framework_convert(self):
source_files = ALL_FRAMEWORK_FILES[source_fw]
agent = f"{AGENT_NAME}-conv-{source_fw}"

fid = _log_429(self.client.upload_file, source_files)
fid = _log_429(self.client.upload_file, _to_bytes(source_files))
try:
_log_429(
self.client.create_repo,
Expand Down Expand Up @@ -406,14 +414,14 @@ def test_18_empty_zip(self):
fid = _log_429(self.client.upload_file, {})
# Accepted is fine; empty file_id is also acceptable
except ApiError:
pass # server may reject empty zips
pass # server may reject empty uploads

# -----------------------------------------------------------------------
# 13. Edge: large file
# -----------------------------------------------------------------------
def test_19_large_file(self):
large_content = "x" * (500 * 1024)
files = {"SOUL.md": "# Soul\nLarge file test.\n", "data/large.txt": large_content}
files = {"SOUL.md": b"# Soul\nLarge file test.\n", "data/large.txt": large_content.encode("utf-8")}
fid = _log_429(self.client.upload_file, files)
self.assertTrue(fid)

Expand All @@ -422,9 +430,9 @@ def test_19_large_file(self):
# -----------------------------------------------------------------------
def test_20_special_chars_path(self):
files = {
"SOUL.md": "# Soul\nSpecial chars test.\n",
"memory/user-notes (1).md": "# Notes\nParentheses in filename.\n",
"skills/web-search-v2/SKILL.md": "# Web Search v2\nHyphen in skill name.\n",
"SOUL.md": b"# Soul\nSpecial chars test.\n",
"memory/user-notes (1).md": b"# Notes\nParentheses in filename.\n",
"skills/web-search-v2/SKILL.md": b"# Web Search v2\nHyphen in skill name.\n",
}
fid = _log_429(self.client.upload_file, files)
self.assertTrue(fid)
Expand All @@ -435,7 +443,7 @@ def test_20_special_chars_path(self):
def test_21_visibility_variants(self):
for vis in ["public", "private"]:
with self.subTest(visibility=vis):
files = {"SOUL.md": f"# Soul\nVisibility={vis} test.\n"}
files = {"SOUL.md": f"# Soul\nVisibility={vis} test.\n".encode("utf-8")}
fid = _log_429(self.client.upload_file, files)
self.assertTrue(fid)
result = _log_429(
Expand All @@ -451,7 +459,7 @@ def test_21_visibility_variants(self):
# 16. Edge: upload then immediate download
# -----------------------------------------------------------------------
def test_22_immediate_download(self):
files = {"SOUL.md": "# Soul\nImmediate download test.\n", "README.md": "# README\n"}
files = {"SOUL.md": b"# Soul\nImmediate download test.\n", "README.md": b"# README\n"}
fid = _log_429(self.client.upload_file, files)
_log_429(
self.client.create_repo,
Expand Down Expand Up @@ -479,7 +487,7 @@ def test_23_framework_structure(self):
with self.subTest(framework=fw):
files = ALL_FRAMEWORK_FILES[fw]
agent = f"{AGENT_NAME}-struct-{fw}"
fid = _log_429(self.client.upload_file, files)
fid = _log_429(self.client.upload_file, _to_bytes(files))
try:
_log_429(
self.client.create_repo,
Expand Down
28 changes: 16 additions & 12 deletions tests/api/test_upload_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from types import SimpleNamespace

from ultron.cli.client import ApiError, UltronClient
from ultron.cli.commands import cmd_download, cmd_upload, _repo_name
from ultron.cli.commands import cmd_download, cmd_list, cmd_upload, _repo_name
from ultron.cli import config as cli_config
from ultron.services.harness.allowlist import (
ALL_AGENT_NAME,
Expand Down Expand Up @@ -161,21 +161,22 @@ def setUp(self):
# -----------------------------------------------------------------------
# Helper: build args namespace for cmd_upload / cmd_download
# -----------------------------------------------------------------------
def _upload_args(self, framework, name, local_dir=None, dry_run=False, list_=False):
def _upload_args(self, framework, name, local_dir=None, dry_run=False, repo=None):
return SimpleNamespace(
framework=framework,
name=name,
repo=repo,
local_dir=local_dir,
server=SERVER,
token=TOKEN,
message=None,
list=list_,
dry_run=dry_run,
)

def _download_args(self, name, framework=None, target=None, local_dir=None, dry_run=False):
def _download_args(self, name, framework=None, target=None, local_dir=None, dry_run=False, repo=None):
return SimpleNamespace(
name=name,
repo=repo or _repo_name(framework or "", name or ""),
framework=framework,
target=target,
local_dir=local_dir,
Expand All @@ -190,7 +191,10 @@ def _create_local_workspace(self, files: dict) -> str:
for rel, content in files.items():
fp = Path(tmpdir) / rel
fp.parent.mkdir(parents=True, exist_ok=True)
fp.write_text(content, encoding="utf-8")
if isinstance(content, bytes):
fp.write_bytes(content)
else:
fp.write_text(content, encoding="utf-8")
return tmpdir

def _cleanup_dir(self, path: str):
Expand Down Expand Up @@ -273,24 +277,24 @@ def test_05_upload_dry_run(self):
self._cleanup_dir(local)

# -----------------------------------------------------------------------
# 06. Upload: --list
# 06. List: list sub-agents
# -----------------------------------------------------------------------
def test_06_upload_list(self):
"""--list should enumerate sub-agents on disk and return 0."""
"""cmd_list should enumerate sub-agents on disk and return 0."""
local = self._create_local_workspace(QODER_ALL_FILES)
try:
args = self._upload_args("qoder", None, local_dir=local, list_=True)
rc = cmd_upload(args)
args = SimpleNamespace(framework="qoder", local_dir=local)
rc = cmd_list(args)
self.assertEqual(rc, 0)
finally:
self._cleanup_dir(local)

# -----------------------------------------------------------------------
# 07. Upload: missing --name → error
# 07. Upload: missing --name with multiple agents → error
# -----------------------------------------------------------------------
def test_07_upload_missing_name(self):
"""Upload without --name (and not --list) should fail."""
local = self._create_local_workspace(QODER_INDIVIDUAL_FILES)
"""Upload without --name when multiple agents exist should fail."""
local = self._create_local_workspace(QODER_ALL_FILES)
try:
args = self._upload_args("qoder", None, local_dir=local)
rc = cmd_upload(args)
Expand Down
9 changes: 8 additions & 1 deletion tests/api/test_watch_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ def _cleanup(self, path: str):

def _upload_remote(self, name: str, framework: str, files: dict):
"""Upload files directly to remote (simulates remote-side changes)."""
file_id = self.client.upload_file(files)
# Convert str values to bytes for the new upload_file API
byte_files = {
k: (v.encode("utf-8") if isinstance(v, str) else v)
for k, v in files.items()
}
file_id = self.client.upload_file(byte_files)
self.client.create_repo(self.username, name, framework, system_prompt_files=file_id)

def _start_watch(self, framework: str, agent_name: str, local_dir: str, repo_name: str, push_only: bool = True) -> multiprocessing.Process:
Expand Down Expand Up @@ -557,10 +562,12 @@ def test_11_qoder_individual_watch_rejected(self):
args = SimpleNamespace(
framework="qoder",
name="reviewer",
repo=None,
local_dir=None,
server=SERVER,
token=TOKEN,
interval=60,
pull=False,
sessions_dir=None,
)
rc = cmd_watch(args)
Expand Down
66 changes: 63 additions & 3 deletions tests/cli/test_download_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def tearDown(self):
@mock.patch.object(commands, "UltronClient", _DownloadStub)
def test_download_writes_files(self, *_):
rc = _run([
"download", "--name", "nano", "--framework", "nanobot",
"download", "--repo", "nano", "--framework", "nanobot",
"--local_dir", str(self.out),
])
self.assertEqual(rc, 0)
Expand All @@ -62,7 +62,7 @@ def test_download_writes_files(self, *_):
def test_download_with_conversion(self, *_):
# nanobot -> hermes: USER.md must land at hermes' memories/USER.md.
rc = _run([
"download", "--name", "nano", "--framework", "nanobot",
"download", "--repo", "nano", "--framework", "nanobot",
"--target", "hermes", "--local_dir", str(self.out),
])
self.assertEqual(rc, 0)
Expand All @@ -72,10 +72,70 @@ def test_download_with_conversion(self, *_):
@mock.patch.object(commands.config, "resolve_server", return_value=None)
@mock.patch.object(commands.config, "resolve_token", return_value=None)
def test_download_without_login_fails(self, *_):
rc = _run(["download", "--name", "nano", "--framework", "nanobot",
rc = _run(["download", "--repo", "nano", "--framework", "nanobot",
"--local_dir", str(self.out)])
self.assertEqual(rc, 1)

def test_download_repo_required(self):
"""Download without --repo should fail at argparse level."""
import sys
from io import StringIO
stderr = StringIO()
with self.assertRaises(SystemExit):
_run(["download", "--framework", "nanobot", "--local_dir", str(self.out)])

@mock.patch.object(commands.config, "resolve_username", return_value="u")
@mock.patch.object(commands.config, "resolve_token", return_value="tok")
@mock.patch.object(commands.config, "resolve_server", return_value="http://s")
@mock.patch.object(commands, "UltronClient", _DownloadStub)
def test_download_with_name_creates_agent(self, *_):
"""Download with --name should write files for that local agent."""
rc = _run([
"download", "--repo", "nano", "--framework", "nanobot",
"--name", "myagent", "--local_dir", str(self.out),
])
self.assertEqual(rc, 0)
# Files should still be written (nanobot shared files match).
self.assertTrue((self.out / "SOUL.md").is_file())

@mock.patch.object(commands.config, "resolve_username", return_value="u")
@mock.patch.object(commands.config, "resolve_token", return_value="tok")
@mock.patch.object(commands.config, "resolve_server", return_value="http://s")
@mock.patch.object(commands, "UltronClient", _DownloadStub)
def test_download_filters_by_allowlist(self, *_):
"""Files not matching the allowlist patterns should be skipped."""
# Add a file that won't match any pattern.
_DownloadStub.STORE = {
"SOUL.md": "soul",
"random/junk.txt": "junk",
"memory/MEMORY.md": "mem",
}
rc = _run([
"download", "--repo", "nano", "--framework", "nanobot",
"--local_dir", str(self.out),
])
self.assertEqual(rc, 0)
# random/junk.txt should NOT be written.
self.assertFalse((self.out / "random" / "junk.txt").exists())
# Valid files should be written.
self.assertTrue((self.out / "SOUL.md").is_file())
# Restore original store.
_DownloadStub.STORE = {"SOUL.md": "soul", "USER.md": "user", "memory/MEMORY.md": "mem"}

@mock.patch.object(commands.config, "resolve_username", return_value="u")
@mock.patch.object(commands.config, "resolve_token", return_value="tok")
@mock.patch.object(commands.config, "resolve_server", return_value="http://s")
@mock.patch.object(commands, "UltronClient", _DownloadStub)
def test_download_repo_with_slash(self, *_):
"""--repo with '/' uses the specified group instead of username."""
rc = _run([
"download", "--repo", "othergroup/nano", "--framework", "nanobot",
"--local_dir", str(self.out),
])
self.assertEqual(rc, 0)
# Should still write files (stub doesn't care about group).
self.assertTrue((self.out / "SOUL.md").is_file())


class TestConvert(unittest.TestCase):
def setUp(self):
Expand Down
Loading
Loading