M2.8: switch over to the new loop
nisse-dev now runs only the conversational loop. The gated state machine it was built alongside is gone: - Removed control.py, planner.py, executor.py, approvals.py, gates.py (command_form moves to loop/reader.py), publish.py and poller.py, the old assembly in agent.py (its shared helpers move to loop/build.py), the store's state-machine view (get_state, transition), their tests, the old end-to-end and live planning/milestone/gate tests, and specs/orchestrator.qnt with its scenarios. - Roles are driver, helper, expert and classifier; driver is required. loop:, roles.planner, roles.executor and tuning.executor are refused with what to use instead; tuning.driver takes the turn cap and failure limit. - The benchmark and the capability matrix measure the driver: the loop's own system prompt and tools, the task's plan as the agreed restatement. Task plans end with finish_milestone, so earlier runs show as (old); stored runs' executor tuning reads as the driver's. - A delegation on the loop records what its helper's alias resolved to (L9), as the old path's did. - The dump shows a work item's revision and what it waits for. - Behaviour the old tests covered and the loop shares stays covered: driver runs that fail on a model call, stop mid-turn, reset the failure count; go-aheads ignoring case and punctuation; a request too big to fit is never sent; the assembly tests, now on build_loop_agent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
09232dcbdd
commit
6ae2375194
89 files changed
+951
-8592
No files matched your search
+134
-194
@@ -11,10 +11,12 @@ import httpx
|
||||
import pytest
|
||||
from test_config import SECRETS, P, tree
|
||||
|
||||
from nisse_dev.agent import build_agent, discover_models, prepare_workspace
|
||||
from nisse_dev.catalog import ModelCatalog, UnknownModel
|
||||
from nisse_dev.config import AgentConfig
|
||||
from nisse_dev.execution import SSHHost
|
||||
from nisse_dev.llm import AnthropicPassthroughClient, LiteLLMClient
|
||||
from nisse_dev.loop.build import RunningLoop, build_loop_agent, discover_models
|
||||
from nisse_dev.loop.build import prepare_workspace as prepare
|
||||
from nisse_dev.oplog import OpLog
|
||||
|
||||
# What the test roles resolve to, as LiteLLM would report them.
|
||||
@@ -92,124 +94,115 @@ def origin(tmp_path: Path) -> Path:
|
||||
return remote
|
||||
|
||||
|
||||
def prepare_workspace(config: AgentConfig, origin: Path) -> Any:
|
||||
[project] = config.projects
|
||||
return prepare(HOST, config, "nisse-dev-testing", str(origin), None, project)
|
||||
|
||||
|
||||
def test_workspace_is_cloned_on_the_build_host_when_absent(
|
||||
tmp_path: Path, origin: Path
|
||||
) -> None:
|
||||
config = config_for(tmp_path)
|
||||
repo = prepare_workspace(HOST, config, "nisse-dev-testing", str(origin), None)
|
||||
|
||||
assert (config.workspace / "README.md").exists()
|
||||
repo = prepare_workspace(config, origin)
|
||||
workspace = config.projects[0].workspace
|
||||
assert (workspace / "README.md").exists()
|
||||
assert repo.current_branch() == "main"
|
||||
# The clone's configuration holds the plain URL it was given (#89).
|
||||
assert str(origin) in (config.workspace / ".git" / "config").read_text()
|
||||
assert str(origin) in (workspace / ".git" / "config").read_text()
|
||||
|
||||
|
||||
def test_workspace_is_reused_when_already_cloned(tmp_path: Path, origin: Path) -> None:
|
||||
config = config_for(tmp_path)
|
||||
prepare_workspace(HOST, config, "nisse-dev-testing", str(origin), None)
|
||||
(config.workspace / "local-work.txt").write_text("in progress")
|
||||
workspace = config.projects[0].workspace
|
||||
prepare_workspace(config, origin)
|
||||
(workspace / "local-work.txt").write_text("in progress")
|
||||
|
||||
prepare_workspace(HOST, config, "nisse-dev-testing", str(origin), None)
|
||||
prepare_workspace(config, origin)
|
||||
|
||||
# Re-cloning would silently destroy uncommitted work.
|
||||
assert (config.workspace / "local-work.txt").exists()
|
||||
assert (workspace / "local-work.txt").exists()
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self) -> None:
|
||||
self.sent: list[str] = []
|
||||
self.started = False
|
||||
self.closed = False
|
||||
|
||||
def send_message(self, text: str) -> None:
|
||||
self.sent.append(text)
|
||||
|
||||
def send_approval_request(self, text: str) -> None:
|
||||
self.sent.append(text)
|
||||
|
||||
def ask_question(self, text: str) -> None:
|
||||
def post(self, text: str, thread: str | None = None) -> str:
|
||||
self.sent.append(text)
|
||||
return f"$event{len(self.sent)}"
|
||||
|
||||
def on_message(self, callback: Any) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def start(self) -> None:
|
||||
self.started = True
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def build_offline(tmp_path: Path, origin: Path, monkeypatch: Any) -> Any:
|
||||
"""Assemble the real graph with only the network boundaries faked."""
|
||||
config = config_for(tmp_path)
|
||||
def offline(monkeypatch: Any, origins: dict[str, str] | str, catalog=None) -> None:
|
||||
"""Only the network boundaries faked: Gitea, LiteLLM's model list, Matrix."""
|
||||
# The proxy here doesn't exist: a call to it fails at once rather than
|
||||
# waiting out an outage's backoff (L5).
|
||||
monkeypatch.setattr("nisse_dev.llm.OUTAGE_BACKOFF_S", ())
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.GiteaClient.whoami", lambda self: "nisse-dev-testing"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.GiteaClient.clone_url",
|
||||
lambda self, repo: origins if isinstance(origins, str) else origins[repo],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: catalog or TEST_CATALOG),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.MatrixTransport", lambda cfg: FakeTransport()
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.GiteaClient.whoami", lambda self: "nisse-dev-testing"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: TEST_CATALOG),
|
||||
)
|
||||
monkeypatch.setattr("nisse_dev.agent.MatrixTransport", lambda cfg: FakeTransport())
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.GiteaClient.clone_url", lambda self, repo: str(origin)
|
||||
)
|
||||
return build_agent(config)
|
||||
|
||||
def build_offline(
|
||||
tmp_path: Path, origin: Path, monkeypatch: Any, config: AgentConfig | None = None
|
||||
) -> RunningLoop:
|
||||
offline(monkeypatch, str(origin))
|
||||
return build_loop_agent(config or config_for(tmp_path))
|
||||
|
||||
|
||||
def test_the_whole_graph_assembles(tmp_path: Path, origin: Path, monkeypatch) -> None:
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
orchestrator = agent.orchestrator
|
||||
|
||||
# Every component that was previously unreachable is now connected.
|
||||
assert orchestrator._executor is not None
|
||||
assert orchestrator._publisher is not None
|
||||
assert orchestrator._board is not None
|
||||
assert orchestrator._delegate is not None
|
||||
assert orchestrator._poller_factory is not None
|
||||
assert orchestrator._executor._delegate is not None # L9 escalation path
|
||||
assert orchestrator._executor._expert is not None # L9 consult path
|
||||
agent.stop()
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
loop = running.loop
|
||||
try:
|
||||
assert running.account == "nisse-dev-testing"
|
||||
[project] = loop._projects.values()
|
||||
assert project.repo == "don/demo"
|
||||
assert (tmp_path / "work" / "README.md").exists() # cloned on the build host
|
||||
assert project.milestones is not None
|
||||
assert loop._driver is not None
|
||||
assert loop._reader is not None # plain-language go-aheads (L5)
|
||||
assert loop._helpers is not None # delegation and ask_expert (L9)
|
||||
finally:
|
||||
running.stop()
|
||||
|
||||
|
||||
def test_start_announces_where_it_resumed(
|
||||
def test_start_announces_the_account_and_projects(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
agent.start()
|
||||
|
||||
assert agent.transport.started
|
||||
readiness = agent.transport.sent[-1]
|
||||
assert "nisse-dev up as nisse-dev-testing" in readiness
|
||||
assert "greet" in readiness # the project
|
||||
assert "claude-opus-5" in readiness # the pinned planner
|
||||
agent.stop()
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
running.loop.start = lambda: None # type: ignore[method-assign] # no polling
|
||||
running.start()
|
||||
readiness = running.transport.sent[-1] # type: ignore[attr-defined]
|
||||
assert "nisse-dev up as nisse-dev-testing on don/demo" in readiness
|
||||
running.stop()
|
||||
|
||||
|
||||
def test_stop_closes_everything_and_releases_the_wait(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
agent.start()
|
||||
agent.stop()
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
running.stop()
|
||||
|
||||
assert agent.transport.closed
|
||||
agent.wait() # returns immediately rather than hanging
|
||||
assert agent.store.closed
|
||||
|
||||
|
||||
def test_poller_factory_ignores_the_agents_own_comments(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
# Without this the agent answers its own PR replies in a loop.
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
poller = agent.orchestrator._poller_factory(lambda c: None)
|
||||
assert "nisse-dev-testing" in poller._ignore
|
||||
agent.stop()
|
||||
assert running.transport.closed # type: ignore[attr-defined]
|
||||
running.wait() # returns immediately rather than hanging
|
||||
assert running.store.closed
|
||||
|
||||
|
||||
def test_delegation_uses_the_passthrough_route(
|
||||
@@ -217,24 +210,46 @@ def test_delegation_uses_the_passthrough_route(
|
||||
) -> None:
|
||||
# L9: delegations run vendor-native; the normalized route is for the
|
||||
# places where fidelity is cheap.
|
||||
from nisse_dev.llm import AnthropicPassthroughClient, LiteLLMClient
|
||||
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
delegation = agent.orchestrator._delegate
|
||||
assert isinstance(delegation._client._inner, AnthropicPassthroughClient)
|
||||
assert isinstance(agent.orchestrator._executor._client._inner, LiteLLMClient)
|
||||
agent.stop()
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
helpers = running.loop._helpers
|
||||
assert helpers is not None
|
||||
helper = helpers.delegate_client("claude-haiku-4-5")
|
||||
assert isinstance(helper._inner, AnthropicPassthroughClient) # type: ignore[attr-defined]
|
||||
assert isinstance(running.loop._driver._client._inner, LiteLLMClient) # type: ignore[attr-defined]
|
||||
running.stop()
|
||||
|
||||
|
||||
def test_httpx_clients_are_not_shared_between_routes(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
normalized = agent.orchestrator._executor._client._inner._http
|
||||
passthrough = agent.orchestrator._delegate._client._inner._http
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
helpers = running.loop._helpers
|
||||
assert helpers is not None
|
||||
normalized = running.loop._driver._client._inner._http # type: ignore[attr-defined]
|
||||
passthrough = helpers.delegate_client("claude-haiku-4-5")._inner._http # type: ignore[attr-defined]
|
||||
assert isinstance(normalized, httpx.Client)
|
||||
assert normalized is not passthrough # different base URLs and headers
|
||||
agent.stop()
|
||||
running.stop()
|
||||
|
||||
|
||||
def test_delegations_record_what_the_alias_resolved_to(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
helpers = running.loop._helpers
|
||||
assert helpers is not None and helpers.backing is not None
|
||||
assert helpers.backing("claude-haiku-4-5") == "anthropic/claude-haiku-4-5"
|
||||
running.stop()
|
||||
|
||||
|
||||
def test_the_configured_roles_are_what_items_pin(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
running = build_offline(tmp_path, origin, monkeypatch)
|
||||
pinned = running.loop._roles
|
||||
assert pinned == running.config.roles.pinned()
|
||||
assert set(pinned) == {"driver", "helper", "expert", "classifier"}
|
||||
running.stop()
|
||||
|
||||
|
||||
# --- startup model discovery (L9) ------------------------------------
|
||||
@@ -243,12 +258,12 @@ def test_httpx_clients_are_not_shared_between_routes(
|
||||
def test_a_typod_role_refuses_to_boot(tmp_path: Path, monkeypatch) -> None:
|
||||
# Misconfiguration should cost a second at startup, not be discovered ten
|
||||
# minutes into a milestone.
|
||||
config = config_for(tmp_path, roles__planner="clause-opus-5")
|
||||
config = config_for(tmp_path, roles__driver="clause-opus-5")
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
"nisse_dev.loop.build.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: TEST_CATALOG),
|
||||
)
|
||||
with pytest.raises(UnknownModel, match="planner='clause-opus-5'"):
|
||||
with pytest.raises(UnknownModel, match="driver='clause-opus-5'"):
|
||||
discover_models(config, OpLog("test"))
|
||||
|
||||
|
||||
@@ -257,12 +272,12 @@ def test_a_role_this_key_cannot_reach_refuses_to_boot(
|
||||
) -> None:
|
||||
# /model/info is scoped per key, so the same check catches a role pointed
|
||||
# at a real model that this token has no access to.
|
||||
config = config_for(tmp_path, roles__executor="local-fast")
|
||||
config = config_for(tmp_path, roles__helper="local-fast")
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
"nisse_dev.loop.build.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: TEST_CATALOG),
|
||||
)
|
||||
with pytest.raises(UnknownModel, match="executor='local-fast'"):
|
||||
with pytest.raises(UnknownModel, match="helper='local-fast'"):
|
||||
discover_models(config, OpLog("test"))
|
||||
|
||||
|
||||
@@ -288,7 +303,7 @@ def test_a_tool_less_model_warns_but_still_boots(tmp_path: Path, monkeypatch) ->
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
"nisse_dev.loop.build.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: tool_less),
|
||||
)
|
||||
stream = io.StringIO()
|
||||
@@ -296,36 +311,10 @@ def test_a_tool_less_model_warns_but_still_boots(tmp_path: Path, monkeypatch) ->
|
||||
|
||||
logged = stream.getvalue()
|
||||
assert "model_tools_unsupported" in logged
|
||||
assert '"role": "executor"' in logged
|
||||
assert '"role": "driver"' in logged
|
||||
assert '"role": "expert"' not in logged # ask_expert drives no tools
|
||||
|
||||
|
||||
def test_the_resolved_model_is_pinned_at_task_start(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
# The store belongs to the worker thread, so the work has to run there.
|
||||
agent.worker.call(lambda: agent.orchestrator._begin_project("greet"))
|
||||
|
||||
pinned = agent.orchestrator._data["models"]
|
||||
assert pinned["planner"] == {
|
||||
"alias": "claude-opus-5",
|
||||
"backing": "anthropic/claude-opus-5",
|
||||
"provider": "anthropic",
|
||||
}
|
||||
agent.stop()
|
||||
|
||||
|
||||
def test_delegation_records_what_the_alias_resolved_to(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
# L9 eval history keyed on an alias alone would average across whatever
|
||||
# that alias pointed at over time.
|
||||
agent = build_offline(tmp_path, origin, monkeypatch)
|
||||
assert agent.orchestrator._delegate._backing_model == "anthropic/claude-opus-5"
|
||||
agent.stop()
|
||||
|
||||
|
||||
def test_tuning_reaches_every_component(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
@@ -333,8 +322,9 @@ def test_tuning_reaches_every_component(
|
||||
|
||||
from nisse_dev.config import (
|
||||
DelegationTuning,
|
||||
ExecutorTuning,
|
||||
DriverTuning,
|
||||
ExpertTuning,
|
||||
LoopTuning,
|
||||
ToolsTuning,
|
||||
Tuning,
|
||||
)
|
||||
@@ -343,83 +333,47 @@ def test_tuning_reaches_every_component(
|
||||
config_for(tmp_path),
|
||||
max_output_tokens=8192,
|
||||
tuning=Tuning(
|
||||
executor=ExecutorTuning(max_turns=40, failure_limit=3),
|
||||
driver=DriverTuning(max_turns=40, failure_limit=3),
|
||||
expert=ExpertTuning(consult_budget=5),
|
||||
delegation=DelegationTuning(
|
||||
max_turns=12, max_tokens=50_000, wall_clock_s=60
|
||||
),
|
||||
tools=ToolsTuning(command_timeout_s=300, max_output_chars=2000),
|
||||
loop=LoopTuning(bug_grace_s=120, general_idle_s=900),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.GiteaClient.whoami", lambda self: "nisse-dev-testing"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: TEST_CATALOG),
|
||||
)
|
||||
monkeypatch.setattr("nisse_dev.agent.MatrixTransport", lambda cfg: FakeTransport())
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.GiteaClient.clone_url", lambda self, repo: str(origin)
|
||||
)
|
||||
agent = build_agent(config)
|
||||
executor = agent.orchestrator._executor
|
||||
delegation = agent.orchestrator._delegate
|
||||
running = build_offline(tmp_path, origin, monkeypatch, config)
|
||||
loop = running.loop
|
||||
driver = loop._driver
|
||||
helpers = loop._helpers
|
||||
assert helpers is not None
|
||||
[project] = loop._projects.values()
|
||||
|
||||
assert (executor._max_turns, executor._failure_limit) == (40, 3)
|
||||
assert executor._expert._budget == 5
|
||||
assert delegation._caps.turns == 12
|
||||
assert delegation._caps.tokens == 50_000
|
||||
assert delegation._caps.wall_clock_s == 60
|
||||
assert executor._workspace._timeout_s == 300
|
||||
assert executor._workspace._max_output_chars == 2000
|
||||
assert executor._client._inner._max_tokens == 8192
|
||||
assert delegation._client._inner._max_tokens == 8192
|
||||
assert executor._client._max_output_tokens == 8192
|
||||
assert (driver._max_turns, driver._failure_limit) == (40, 3)
|
||||
assert helpers.expert_budget == 5
|
||||
assert (helpers.caps.turns, helpers.caps.tokens) == (12, 50_000)
|
||||
assert helpers.caps.wall_clock_s == 60
|
||||
assert project.workspace is not None
|
||||
assert project.workspace._timeout_s == 300
|
||||
assert project.workspace._max_output_chars == 2000
|
||||
assert (loop._grace, loop._idle) == (120, 900)
|
||||
client = driver._client
|
||||
assert client._inner._max_tokens == 8192 # type: ignore[attr-defined]
|
||||
assert client._max_output_tokens == 8192 # type: ignore[attr-defined]
|
||||
helper = helpers.delegate_client("claude-haiku-4-5")
|
||||
assert helper._inner._max_tokens == 8192 # type: ignore[attr-defined]
|
||||
# tool_choice is asked of exactly the models LiteLLM declares enforce it.
|
||||
assert executor._client._enforces_tool_choice("claude-opus-5") is False
|
||||
agent.stop()
|
||||
|
||||
|
||||
def test_the_new_loop_assembles(tmp_path: Path, origin: Path, monkeypatch) -> None:
|
||||
"""`loop: new` builds the LoopAgent over the same services."""
|
||||
from nisse_dev.loop.build import build_loop_agent
|
||||
|
||||
config = config_for(tmp_path, loop="new")
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.GiteaClient.whoami", lambda self: "nisse-dev-testing"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.GiteaClient.clone_url", lambda self, repo: str(origin)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: TEST_CATALOG),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.MatrixTransport", lambda cfg: FakeTransport()
|
||||
)
|
||||
running = build_loop_agent(config)
|
||||
try:
|
||||
assert running.account == "nisse-dev-testing"
|
||||
[project] = running.loop._projects.values()
|
||||
assert project.repo == config.gitea_repo
|
||||
assert (config.workspace / "README.md").exists() # cloned on the build host
|
||||
finally:
|
||||
running.worker.call(running.store.close)
|
||||
running.worker.stop(timeout=5)
|
||||
assert client._enforces_tool_choice("claude-opus-5") is False # type: ignore[attr-defined]
|
||||
running.stop()
|
||||
|
||||
|
||||
def test_each_project_gets_its_own_clone_and_secrets(
|
||||
tmp_path: Path, origin: Path, monkeypatch
|
||||
) -> None:
|
||||
"""D8: a second project is only a second entry."""
|
||||
from nisse_dev.loop.build import build_loop_agent
|
||||
|
||||
second = tmp_path / "second"
|
||||
config = config_for(
|
||||
tmp_path,
|
||||
loop="new",
|
||||
projects__other={
|
||||
"repo": "don/other",
|
||||
"workspace": str(tmp_path / "other-work"),
|
||||
@@ -436,20 +390,7 @@ def test_each_project_gets_its_own_clone_and_secrets(
|
||||
},
|
||||
)
|
||||
git("clone", "-q", "--bare", str(origin), str(second))
|
||||
origins = {"don/demo": str(origin), "don/other": str(second)}
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.GiteaClient.whoami", lambda self: "nisse-dev-testing"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.GiteaClient.clone_url", lambda self, repo: origins[repo]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.agent.ModelCatalog.discover",
|
||||
classmethod(lambda cls, *a, **kw: TEST_CATALOG),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"nisse_dev.loop.build.MatrixTransport", lambda cfg: FakeTransport()
|
||||
)
|
||||
offline(monkeypatch, {"don/demo": str(origin), "don/other": str(second)})
|
||||
running = build_loop_agent(config)
|
||||
try:
|
||||
projects = running.loop._projects
|
||||
@@ -460,5 +401,4 @@ def test_each_project_gets_its_own_clone_and_secrets(
|
||||
assert projects["other"].workspace._secrets == {"API_KEY": "sk-x"}
|
||||
assert projects["greet"].workspace._secrets == {}
|
||||
finally:
|
||||
running.worker.call(running.store.close)
|
||||
running.worker.stop(timeout=5)
|
||||
running.stop()
|
||||
Reference in new issue
Block a user