kneifftools / tests /test_cli_project.py
kneiff's picture
chore(history)!: publish anonymous repository root
2857cf3
Raw
History Blame Contribute Delete
36.2 kB
from __future__ import annotations
import json
from pathlib import Path
import shutil
import subprocess
import apprc as rc
import pytest
from typer.testing import CliRunner
import kneiff.cli.comfy as cli_comfy
import kneiff.project_scaffold as project_scaffold_module
from kneiff.config import KNEIFF_RC
from kneiff.project_layout import (
DEFAULT_ANIMA_CONFIG_FILENAME,
DEFAULT_FLUX2_KLEIN_9B_CONFIG_FILENAME,
DEFAULT_SOURCE_DIR_NAMES,
PROJECT_DEFAULT_TAGS_FILENAME,
PROJECT_GITATTRIBUTES_NAME,
PROJECT_GITIGNORE_NAME,
PROJECT_PROMPTS_FILENAME,
PROJECT_VOCABULARY_FILENAME,
)
from kneiff.project_scaffold import (
DEFAULT_GIT_USER_NAME,
ensure_project_resources,
PROJECT_SCAFFOLD_DIR_NAMES,
)
from kneiff.project_scaffold_assets import (
PROJECT_GITATTRIBUTES_RULES,
PROJECT_GITIGNORE_ENTRIES,
)
from kneiff.training.lora.config_templates import (
read_default_anima_starter_config,
read_default_flux2_klein_9b_starter_config,
)
from tests._cli_helpers import invoke_cli, set_test_config_home
def test_project_init_list_use_show_and_validate(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
monkeypatch.delenv("KNF_STORAGE", raising=False)
runner = CliRunner()
project_root = tmp_path / "alpha"
init_result = invoke_cli(
runner,
[
"project",
"init",
str(project_root),
"--name",
"alpha",
"--activation-token",
"Alpha_Character",
"--species-token",
"alpha_species",
"--yes",
],
)
assert init_result.exit_code == 0, init_result.output
assert (project_root / PROJECT_VOCABULARY_FILENAME).is_file()
assert (project_root / PROJECT_PROMPTS_FILENAME).is_file()
assert (project_root / PROJECT_PROMPTS_FILENAME).read_text(encoding="utf-8") == (
"version: 1\nprompts: []\n"
)
assert not (project_root / "kneiff.project.yaml").exists()
assert not (project_root / "resources").exists()
assert (project_root / "configs").is_dir()
assert (project_root / "SOURCE").is_dir()
assert (project_root / "configs" / DEFAULT_ANIMA_CONFIG_FILENAME).is_file()
assert (project_root / "configs" / DEFAULT_FLUX2_KLEIN_9B_CONFIG_FILENAME).is_file()
for dirname in DEFAULT_SOURCE_DIR_NAMES:
assert (project_root / "SOURCE" / dirname).is_dir()
assert (project_root / "HF").is_dir()
assert (project_root / "TRAINING").is_dir()
assert not (project_root / "HF" / ".gitkeep").exists()
assert not (project_root / "TRAINING" / ".gitkeep").exists()
assert (project_root / PROJECT_DEFAULT_TAGS_FILENAME).read_text(
encoding="utf-8"
) == "kneiff\nAlpha_Character\nalpha_species\n"
assert (project_root / PROJECT_GITATTRIBUTES_NAME).is_file()
assert (project_root / ".git").is_dir()
branch = subprocess.run(
["git", "branch", "--show-current"],
cwd=project_root,
check=True,
capture_output=True,
text=True,
).stdout.strip()
assert branch == "main"
git_user_name = subprocess.run(
["git", "config", "--local", "--get", "user.name"],
cwd=project_root,
check=True,
capture_output=True,
text=True,
).stdout.strip()
assert git_user_name == DEFAULT_GIT_USER_NAME
assert (project_root / KNEIFF_RC.spec.storage_env_filename).read_text(
encoding="utf-8"
) == ""
gitignore_text = (project_root / PROJECT_GITIGNORE_NAME).read_text(encoding="utf-8")
assert ".env.apprc-storage" in gitignore_text
assert "HF/" in gitignore_text
assert "TRAINING/" in gitignore_text
for ignored_path in ("HF", "TRAINING"):
ignored = subprocess.run(
["git", "check-ignore", ignored_path],
cwd=project_root,
check=False,
capture_output=True,
)
assert ignored.returncode == 0
gitattributes_rules = {
line.strip()
for line in (project_root / PROJECT_GITATTRIBUTES_NAME)
.read_text(encoding="utf-8")
.splitlines()
}
assert set(PROJECT_GITATTRIBUTES_RULES).issubset(gitattributes_rules)
assert "Registered project: alpha" in init_result.output
list_result = invoke_cli(runner, ["project", "list", "--json"])
assert list_result.exit_code == 0, list_result.output
assert json.loads(list_result.output)["projects"] == [
{
"error": None,
"name": "alpha",
"root": str(project_root.resolve()),
"status": "ready",
"valid": True,
}
]
use_result = invoke_cli(runner, ["project", "use", "alpha"])
assert use_result.exit_code == 0, use_result.output
assert "persisted_default_project: alpha" in use_result.output
show_result = invoke_cli(runner, ["project", "show", "--json"])
assert show_result.exit_code == 0, show_result.output
payload = json.loads(show_result.output)
assert payload["name"] == "alpha"
assert payload["root"] == str(project_root.resolve())
assert payload["status"] == "ready"
assert payload["identity"] == {
"activation_token": "Alpha_Character",
"species_token": "alpha_species",
}
assert payload["prompt_counts"]["core"] > 0
assert payload["prompt_counts"]["project"] == 0
assert payload["prompt_counts"]["resolved"] == payload["prompt_counts"]["core"]
assert payload["paths"]["vocabulary"] == {
"path": str(project_root / PROJECT_VOCABULARY_FILENAME),
"type": "file",
"status": "present",
}
assert payload["paths"]["storage_env"] == {
"path": str(project_root / KNEIFF_RC.spec.storage_env_filename),
"type": "file",
"status": "present",
}
assert payload["paths"]["configs"]["status"] == "present"
assert payload["paths"]["anima_config"] == {
"path": str(project_root / "configs" / DEFAULT_ANIMA_CONFIG_FILENAME),
"type": "file",
"status": "present",
}
assert payload["paths"]["flux2_klein_9b_config"] == {
"path": str(project_root / "configs" / DEFAULT_FLUX2_KLEIN_9B_CONFIG_FILENAME),
"type": "file",
"status": "present",
}
assert payload["paths"]["source"]["status"] == "present"
assert payload["paths"]["source_fullbody"]["status"] == "present"
assert payload["paths"]["source_head"]["status"] == "present"
assert payload["paths"]["default_tags"]["status"] == "present"
assert payload["paths"]["gitignore"]["status"] == "present"
assert payload["paths"]["gitattributes"]["status"] == "present"
assert payload["paths"]["git_repository"]["status"] == "present"
assert payload["paths"]["manifest_workbook"]["status"] == "missing"
assert payload["paths"]["manifest_yaml"]["status"] == "missing"
assert payload["paths"]["llm_promptgen"] == {
"path": str(project_root / ".llm_promptgen"),
"type": "directory",
"status": "missing",
}
assert payload["paths"]["llm_prompt_results"] == {
"path": str(project_root / ".llm_promptgen" / "results"),
"type": "directory",
"status": "missing",
}
assert payload["paths"]["hf_exports"]["status"] == "present"
assert payload["paths"]["training"]["status"] == "present"
show_text_result = invoke_cli(runner, ["project", "show"])
assert show_text_result.exit_code == 0, show_text_result.output
assert "activation_token: Alpha_Character" in show_text_result.output
assert "species_token: alpha_species" in show_text_result.output
assert "prompts_core:" in show_text_result.output
assert "prompts_project: 0" in show_text_result.output
assert "prompts_resolved:" in show_text_result.output
assert f"storage_env: {project_root / KNEIFF_RC.spec.storage_env_filename}" in (
show_text_result.output
)
assert f"manifest_workbook: {project_root / 'MANIFEST.knf.xlsx'}" in (
show_text_result.output
)
assert f"llm_promptgen: {project_root / '.llm_promptgen'} (missing)" in (
show_text_result.output
)
assert f"training: {project_root / 'TRAINING'} (present)" in (
show_text_result.output
)
assert (
f"anima_config: {project_root / 'configs' / DEFAULT_ANIMA_CONFIG_FILENAME}"
in (show_text_result.output)
)
assert (
"flux2_klein_9b_config: "
f"{project_root / 'configs' / DEFAULT_FLUX2_KLEIN_9B_CONFIG_FILENAME}"
in show_text_result.output
)
validate_result = invoke_cli(runner, ["project", "validate"])
assert validate_result.exit_code == 0, validate_result.output
assert "activation_token: Alpha_Character" in validate_result.output
assert "species_token: alpha_species" in validate_result.output
assert "prompts_core:" in validate_result.output
assert "prompts_project: 0" in validate_result.output
assert "prompts_resolved:" in validate_result.output
assert f"anima_config: {DEFAULT_ANIMA_CONFIG_FILENAME}" in validate_result.output
assert (
f"flux2_klein_9b_config: {DEFAULT_FLUX2_KLEIN_9B_CONFIG_FILENAME}"
in validate_result.output
)
assert "starter_source_directories: 0-FULLBODY, 2-HEAD" in validate_result.output
(project_root / PROJECT_PROMPTS_FILENAME).unlink()
optional_show_result = invoke_cli(runner, ["project", "show", "--json"])
optional_list_result = invoke_cli(runner, ["project", "list", "--json"])
optional_validate_result = invoke_cli(runner, ["project", "validate"])
assert optional_show_result.exit_code == 0, optional_show_result.output
optional_payload = json.loads(optional_show_result.output)
assert optional_payload["status"] == "ready"
assert optional_payload["paths"]["prompts"]["status"] == "missing"
assert optional_payload["prompt_counts"]["project"] == 0
assert optional_list_result.exit_code == 0, optional_list_result.output
assert json.loads(optional_list_result.output)["projects"][0]["valid"] is True
assert optional_validate_result.exit_code == 0, optional_validate_result.output
assert "prompts_project: 0" in optional_validate_result.output
def test_project_init_requires_identity_for_a_new_vocabulary(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
runner = CliRunner()
result = invoke_cli(
runner,
["project", "init", str(tmp_path / "missing-identity"), "--yes"],
)
assert result.exit_code == 1
assert "requires both an activation token and a species token" in result.output
assert not (tmp_path / "missing-identity").exists()
def test_project_show_and_validate_report_missing_project_resources(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "plain-storage"
root.mkdir()
for dirname in PROJECT_SCAFFOLD_DIR_NAMES:
(root / dirname).mkdir()
rc.storage.register_storage(
name="plain",
root=root,
path=rc.storage.index_path_for_create(KNEIFF_RC.spec),
storage_env_filename=KNEIFF_RC.spec.storage_env_filename,
)
monkeypatch.setenv("KNF_STORAGE", "plain")
result = invoke_cli(CliRunner(), ["project", "show", "--json"])
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["paths"]["vocabulary"]["status"] == "missing"
assert payload["paths"]["prompts"]["status"] == "missing"
list_result = invoke_cli(CliRunner(), ["project", "list", "--json"])
assert list_result.exit_code == 0, list_result.output
assert json.loads(list_result.output)["projects"][0]["valid"] is False
validate_result = invoke_cli(CliRunner(), ["project", "validate"])
assert validate_result.exit_code == 2
assert PROJECT_VOCABULARY_FILENAME in "".join(validate_result.output.split())
@pytest.mark.parametrize(
"dirname",
("configs", "SOURCE", "HF", "TRAINING", ".old_manifests"),
)
def test_project_commands_report_missing_required_scaffold_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
dirname: str,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
runner = CliRunner()
project_root = tmp_path / "incomplete"
init_result = invoke_cli(
runner,
[
"project",
"init",
str(project_root),
"--name",
"incomplete",
"--activation-token",
"Incomplete_Character",
"--species-token",
"incomplete_species",
"--yes",
],
)
assert init_result.exit_code == 0, init_result.output
shutil.rmtree(project_root / dirname)
monkeypatch.setenv("KNF_STORAGE", "incomplete")
show_result = invoke_cli(runner, ["project", "show", "--json"])
list_result = invoke_cli(runner, ["project", "list", "--json"])
validate_result = invoke_cli(runner, ["project", "validate"])
assert show_result.exit_code == 0, show_result.output
show_payload = json.loads(show_result.output)
assert show_payload["status"] == "invalid"
assert dirname in show_payload["error"]
assert list_result.exit_code == 0, list_result.output
[list_payload] = json.loads(list_result.output)["projects"]
assert list_payload["valid"] is False
assert list_payload["status"] == "invalid"
assert dirname in list_payload["error"]
assert validate_result.exit_code == 2
validation_output = "".join(validate_result.output.replace("│", "").split())
assert dirname in validation_output
def test_project_registry_rejects_symlinked_registered_root(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
project_root = tmp_path / "real-project"
ensure_project_resources(
project_root,
activation_token="Demo_Character",
species_token="demo_species",
)
registered_root = tmp_path / "registered-project"
registered_root.symlink_to(project_root, target_is_directory=True)
registry_path = rc.storage.index_path_for_create(KNEIFF_RC.spec)
registry_path.parent.mkdir(parents=True, exist_ok=True)
rc.storage.write_storage_registry(
rc.storage.StorageRegistry(
path=registry_path,
storages={
"linked": rc.storage.StorageRecord(
name="linked",
root=registered_root,
)
},
)
)
list_result = invoke_cli(CliRunner(), ["project", "list", "--json"])
use_result = invoke_cli(CliRunner(), ["project", "use", "linked"])
assert list_result.exit_code == 0, list_result.output
[row] = json.loads(list_result.output)["projects"]
assert row["valid"] is False
assert "Project root must not be a symlink" in row["error"]
assert use_result.exit_code == 2
assert "Cannot select project 'linked'" in use_result.output
def test_project_init_preserves_existing_repository_git_identity(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "adopted"
root.mkdir()
subprocess.run(["git", "init"], cwd=root, check=True, capture_output=True)
subprocess.run(
["git", "config", "--local", "user.name", "Existing Author"],
cwd=root,
check=True,
)
subprocess.run(
[
"git",
"config",
"--local",
"user.email",
"kneiff@users.noreply.huggingface.co",
],
cwd=root,
check=True,
)
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--activation-token",
"Adopted_Character",
"--species-token",
"adopted_species",
"--yes",
],
)
assert result.exit_code == 0, result.output
name = subprocess.run(
["git", "config", "--local", "--get", "user.name"],
cwd=root,
check=True,
capture_output=True,
text=True,
).stdout.strip()
email = subprocess.run(
["git", "config", "--local", "--get", "user.email"],
cwd=root,
check=True,
capture_output=True,
text=True,
).stdout.strip()
assert (name, email) == (
"Existing Author",
"kneiff@users.noreply.huggingface.co",
)
def test_project_init_sets_requested_local_git_identity_without_initial_commit(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "custom-git-user"
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--git-user-name",
"Kneiff Starter Author",
"--yes",
],
)
assert result.exit_code == 0, result.output
user_name = subprocess.run(
["git", "config", "--local", "--get", "user.name"],
cwd=root,
check=True,
capture_output=True,
text=True,
).stdout.strip()
no_local_email = subprocess.run(
["git", "config", "--local", "--get", "user.email"],
cwd=root,
check=False,
capture_output=True,
)
no_initial_commit = subprocess.run(
["git", "rev-parse", "--verify", "HEAD"],
cwd=root,
check=False,
capture_output=True,
)
remotes = subprocess.run(
["git", "remote"],
cwd=root,
check=True,
capture_output=True,
text=True,
).stdout
assert user_name == "Kneiff Starter Author"
assert no_local_email.returncode != 0
assert no_initial_commit.returncode != 0
assert remotes == ""
def test_project_init_adopts_project_owned_files_without_replacing_them(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "adopted-content"
root.mkdir()
subprocess.run(
["git", "init", "--initial-branch", "main"],
cwd=root,
check=True,
capture_output=True,
)
subprocess.run(
["git", "config", "--local", "user.name", "Existing Author"],
cwd=root,
check=True,
)
subprocess.run(
[
"git",
"config",
"--local",
"user.email",
"kneiff@users.noreply.huggingface.co",
],
cwd=root,
check=True,
)
ensure_project_resources(
root,
activation_token="Adopted_Character",
species_token="adopted_species",
)
configs_path = root / "configs"
configs_path.mkdir()
anima_config_path = configs_path / DEFAULT_ANIMA_CONFIG_FILENAME
anima_config_path.write_text(
"# Existing project config.\n" + read_default_anima_starter_config(),
encoding="utf-8",
)
flux2_klein_9b_config_path = configs_path / DEFAULT_FLUX2_KLEIN_9B_CONFIG_FILENAME
flux2_klein_9b_config_path.write_text(
"# Existing Flux2 project config.\n"
+ read_default_flux2_klein_9b_starter_config(),
encoding="utf-8",
)
default_tags_path = root / PROJECT_DEFAULT_TAGS_FILENAME
default_tags_path.write_text("existing tags\n", encoding="utf-8")
git_config_path = root / ".git" / "config"
expected_bytes = {
path: path.read_bytes()
for path in (
root / PROJECT_VOCABULARY_FILENAME,
root / PROJECT_PROMPTS_FILENAME,
anima_config_path,
flux2_klein_9b_config_path,
default_tags_path,
git_config_path,
)
}
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--git-user-name",
"Must Not Replace Existing Author",
"--yes",
],
)
assert result.exit_code == 0, result.output
assert {path: path.read_bytes() for path in expected_bytes} == expected_bytes
for dirname in DEFAULT_SOURCE_DIR_NAMES:
assert (root / "SOURCE" / dirname).is_dir()
assert (root / "SOURCE" / "2-HEAD").is_dir()
def test_project_selection_precedence(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
monkeypatch.delenv("KNF_STORAGE", raising=False)
runner = CliRunner()
alpha_root = tmp_path / "alpha"
beta_root = tmp_path / "beta"
for name, root in (("alpha", alpha_root), ("beta", beta_root)):
result = invoke_cli(
runner,
[
"project",
"init",
str(root),
"--name",
name,
"--activation-token",
f"{name}_Character",
"--species-token",
f"{name}_species",
"--yes",
],
)
assert result.exit_code == 0, result.output
use_result = invoke_cli(runner, ["project", "use", "alpha"])
assert use_result.exit_code == 0, use_result.output
monkeypatch.setenv("KNF_STORAGE", "beta")
root_override = invoke_cli(
runner,
["--storage", "alpha", "project", "show", "--json"],
)
assert root_override.exit_code == 0, root_override.output
assert json.loads(root_override.output)["root"] == str(alpha_root.resolve())
shell_override = invoke_cli(runner, ["project", "show", "--json"])
assert shell_override.exit_code == 0, shell_override.output
assert json.loads(shell_override.output)["root"] == str(beta_root.resolve())
def test_project_init_rejects_invalid_name_before_writing(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "invalid-name"
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--name",
"bad name",
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 1
assert "Project names may contain only" in result.output
assert not root.exists()
def test_project_init_rejects_selector_and_root_rebinding(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
runner = CliRunner()
alpha = tmp_path / "alpha"
beta = tmp_path / "beta"
first = invoke_cli(
runner,
[
"project",
"init",
str(alpha),
"--name",
"alpha",
"--activation-token",
"Alpha_Character",
"--species-token",
"alpha_species",
"--yes",
],
)
assert first.exit_code == 0, first.output
name_collision = invoke_cli(
runner,
[
"project",
"init",
str(beta),
"--name",
"alpha",
"--activation-token",
"Beta_Character",
"--species-token",
"beta_species",
"--yes",
],
)
root_collision = invoke_cli(
runner,
["project", "init", str(alpha), "--name", "alias", "--yes"],
)
assert name_collision.exit_code == 1
assert "refusing to rebind" in name_collision.output
assert not beta.exists()
assert root_collision.exit_code == 1
assert "already registered as 'alpha'" in root_collision.output
def test_project_init_rejects_invalid_existing_prompts_without_mutation(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "invalid-prompts"
root.mkdir()
(root / PROJECT_VOCABULARY_FILENAME).write_text(
"""
version: 1
custom_tokens:
character:
Demo_Character:
text: Demo_Character
species:
demo_species:
text: demo_species
axes: []
""",
encoding="utf-8",
)
(root / PROJECT_PROMPTS_FILENAME).mkdir()
before = sorted(path.name for path in root.iterdir())
result = invoke_cli(
CliRunner(),
["project", "init", str(root), "--name", "broken", "--yes"],
)
assert result.exit_code == 1
assert "exists and is not a file" in result.output
assert sorted(path.name for path in root.iterdir()) == before
registry = rc.storage.load_storage_registry_or_empty(
rc.storage.index_path_for_create(KNEIFF_RC.spec)
)
assert "broken" not in registry.storages
def test_project_init_rejects_resource_symlinks_without_following_them(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "linked"
root.mkdir()
external_vocabulary = tmp_path / "external-vocabulary.yaml"
(root / PROJECT_VOCABULARY_FILENAME).symlink_to(external_vocabulary)
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 1
assert "must not be a symlink" in result.output
assert not external_vocabulary.exists()
@pytest.mark.parametrize(
("invalid_kind", "error"),
(
("symlink", "must not be a symlink"),
("directory", "exists and is not a file"),
),
)
def test_project_init_rejects_invalid_storage_environment_before_registration(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
invalid_kind: str,
error: str,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "invalid-storage-env"
root.mkdir()
storage_env_path = root / KNEIFF_RC.spec.storage_env_filename
external_env = tmp_path / "external.env"
if invalid_kind == "symlink":
external_env.write_text("KNF_WORKERS=7\n", encoding="utf-8")
storage_env_path.symlink_to(external_env)
else:
storage_env_path.mkdir()
registration_called = False
def unexpected_registration(**_kwargs: object) -> None:
nonlocal registration_called
registration_called = True
monkeypatch.setattr(rc.storage, "register_storage", unexpected_registration)
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--name",
"invalid-storage-env",
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 1
assert error in result.output
assert registration_called is False
if invalid_kind == "symlink":
assert external_env.read_text(encoding="utf-8") == "KNF_WORKERS=7\n"
def test_project_init_rejects_gitignore_symlink_without_rewriting_target(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "linked-ignore"
ensure_project_resources(
root,
activation_token="Demo_Character",
species_token="demo_species",
)
external_gitignore = tmp_path / "external.gitignore"
external_gitignore.write_text("keep-this\n", encoding="utf-8")
(root / ".gitignore").symlink_to(external_gitignore)
result = invoke_cli(
CliRunner(),
["project", "init", str(root), "--yes"],
)
assert result.exit_code == 1
assert "must not be a symlink" in result.output
assert external_gitignore.read_text(encoding="utf-8") == "keep-this\n"
def test_project_init_gitignore_covers_generated_project_artifacts(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "ignored"
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 0, result.output
entries = set((root / ".gitignore").read_text(encoding="utf-8").splitlines())
assert set(PROJECT_GITIGNORE_ENTRIES).issubset(entries)
def test_project_init_rolls_back_new_scaffold_when_registration_fails(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "new-parent" / "new-project"
def fail_registration(**_kwargs: object) -> None:
raise OSError("registry write failed")
monkeypatch.setattr(rc.storage, "register_storage", fail_registration)
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--name",
"new-project",
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 1
assert "AppRC registration failed" in result.output
assert not root.parent.exists()
def test_project_init_rolls_back_git_metadata_when_local_identity_setup_fails(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "new-parent" / "new-project"
real_run = subprocess.run
def fail_git_config(
arguments: tuple[str, ...],
*,
check: bool,
capture_output: bool,
text: bool,
) -> subprocess.CompletedProcess[str]:
if "config" in arguments:
return subprocess.CompletedProcess(
arguments,
returncode=1,
stdout="",
stderr="forced local identity failure",
)
return real_run(
arguments,
check=check,
capture_output=capture_output,
text=text,
)
monkeypatch.setattr(project_scaffold_module.subprocess, "run", fail_git_config)
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 1
assert "forced local identity failure" in result.output
assert not root.parent.exists()
def test_project_init_restores_adopted_root_when_registration_fails(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
root = tmp_path / "adopted"
root.mkdir()
sentinel = root / "keep.txt"
sentinel.write_text("keep\n", encoding="utf-8")
gitignore_path = root / ".gitignore"
gitignore_path.write_bytes(b"custom-entry\n")
gitattributes_path = root / ".gitattributes"
gitattributes_path.write_bytes(b"*.custom filter=custom\n")
def fail_registration(**_kwargs: object) -> None:
raise OSError("registry write failed")
monkeypatch.setattr(rc.storage, "register_storage", fail_registration)
result = invoke_cli(
CliRunner(),
[
"project",
"init",
str(root),
"--name",
"adopted",
"--activation-token",
"Demo_Character",
"--species-token",
"demo_species",
"--yes",
],
)
assert result.exit_code == 1
assert sentinel.read_text(encoding="utf-8") == "keep\n"
assert gitignore_path.read_bytes() == b"custom-entry\n"
assert gitattributes_path.read_bytes() == b"*.custom filter=custom\n"
assert sorted(path.name for path in root.iterdir()) == [
".gitattributes",
".gitignore",
"keep.txt",
]
def test_project_registry_commands_report_errors_without_tracebacks(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
missing = invoke_cli(CliRunner(), ["project", "use", "missing"])
assert missing.exit_code == 2
assert "Cannot select project 'missing'" in missing.output
assert "Traceback" not in missing.output
registry_path = rc.storage.index_path_for_create(KNEIFF_RC.spec)
registry_path.parent.mkdir(parents=True, exist_ok=True)
registry_path.write_text("not = [valid", encoding="utf-8")
malformed = invoke_cli(CliRunner(), ["project", "list"])
assert malformed.exit_code == 2
assert "Cannot load project registry" in malformed.output
assert "Traceback" not in malformed.output
def test_project_use_rejects_invalid_registered_project(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
set_test_config_home(monkeypatch, tmp_path)
runner = CliRunner()
project_root = tmp_path / "broken"
init_result = invoke_cli(
runner,
[
"project",
"init",
str(project_root),
"--name",
"broken",
"--activation-token",
"Broken_Character",
"--species-token",
"broken_species",
"--yes",
],
)
assert init_result.exit_code == 0, init_result.output
(project_root / PROJECT_PROMPTS_FILENAME).write_text(
"version: 1\nprompts: not-a-list\n",
encoding="utf-8",
)
result = invoke_cli(runner, ["project", "use", "broken"])
assert result.exit_code == 2
assert "Cannot select project 'broken'" in result.output
normalized_output = " ".join(result.output.replace("│", "").split())
assert "Prompt catalog must contain a prompts list." in normalized_output
@pytest.mark.parametrize(
"root_options",
[
["--storage", "ignored"],
["--skip-dotenv-layers"],
["--log-level", "DEBUG"],
],
)
def test_project_list_rejects_ignored_runtime_options(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
root_options: list[str],
) -> None:
set_test_config_home(monkeypatch, tmp_path)
result = invoke_cli(CliRunner(), [*root_options, "project", "list"])
assert result.exit_code == 2
assert "do not apply to" in result.output
assert "runtime-independent" in result.output
assert "project list" in result.output
def test_comfy_upscale_does_not_require_project_resources(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
isolated_cli_storage: Path,
) -> None:
import kneiff.infer.comfy.upscale as upscale_module
class FakeModelClient:
def __init__(self, _server_url: str) -> None:
pass
def input_options(self, _node_class: str, _input_name: str) -> tuple[str, ...]:
return ("local-upscaler.safetensors",)
monkeypatch.setattr(cli_comfy, "ComfyUiClient", FakeModelClient)
monkeypatch.setattr(upscale_module, "run_upscale", lambda *_args, **_kwargs: ())
image_path = tmp_path / "source.png"
image_path.write_bytes(b"image")
result = invoke_cli(
CliRunner(),
[
"comfy",
"upscale",
str(image_path),
"--fast",
"--model",
"local-upscaler.safetensors",
],
)
assert result.exit_code == 0, result.output
assert not (isolated_cli_storage / PROJECT_VOCABULARY_FILENAME).exists()
assert not (isolated_cli_storage / PROJECT_PROMPTS_FILENAME).exists()