项目文件夹

文件
Ali Khokhar 71a78a0c5a Move runtime packages under src namespace (#1029)
## Problem

Runtime modules were published as generic top-level packages like `api`,
`cli`, and `providers`. That shape is fragile for PyPI packaging and
weakens explicit ownership boundaries.

## Changes

| Before | After |
| --- | --- |
| Runtime code lived in root-level packages. | Runtime code lives under
`src/free_claude_code/`. |
| Console scripts targeted top-level modules. | Console scripts target
namespaced modules. |
| Tests and smoke helpers imported old package roots. | Tests and smoke
helpers import `free_claude_code.*`. |
| Packaging listed six root packages. | Packaging builds the single
namespaced package. |
| Contracts allowed old root package directories. | Contracts require
the src namespace and reject old root imports. |

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR moves the runtime packages into the `src/free_claude_code`
namespace. The main changes are:

- Console scripts now point to `free_claude_code.*` entrypoints.
- Runtime imports, tests, and smoke helpers now use the namespaced
package.
- Packaging now builds the single `src/free_claude_code` package.
- Contract tests now reject old top-level runtime package roots and
imports.
</details>

<h3>Confidence Score: 5/5</h3>

This PR is safe to merge with minimal risk.

The changes are a broad but mostly mechanical namespace and
package-layout migration with updated packaging, tests, and contract
coverage.

No files require special attention.

<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>

**What T-Rex did**
- Reviewed the primary contract validation by examining the namespace
validation log, which documents the exact commands executed, the working
directory, exit codes, pytest output, wheel build output, install
output, and import/entrypoint resolution.
- Verified the wheel listing by inspecting the wheel listing artifact,
confirming the available wheel filenames for the namespace validation.
- Ran and inspected the isolated import/entrypoint validation harness
saved as package-installed-import-check.py to validate import resolution
and entrypoint exposure.
- Captured and noted the wheel filename record in
package-wheel-filename.txt to enable traceability of the observed
artifact.

<a
href="https://app.greptile.com/trex/runs/13810533/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a>

<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>

<details open><summary><h3>Important Files Changed</h3></summary>

| Filename | Overview |
|----------|----------|
| pyproject.toml | Updates packaging to build the single
`src/free_claude_code` package and retargets console scripts to
namespaced modules. |
| src/free_claude_code/config/env_template.py | Loads `.env.example`
from packaged resources with a source-checkout fallback after the
runtime package move. |
| src/free_claude_code/cli/entrypoints.py | Updates CLI entrypoint
imports to `free_claude_code.*` and continues to use the shared env
template loader. |
| src/free_claude_code/api/routes.py | Retargets API route dependencies
and handlers to the namespaced package without changing route behavior.
|
| src/free_claude_code/api/app.py | Updates app factory imports to the
namespaced package while preserving middleware, routers, and exception
handling. |
| src/free_claude_code/providers/runtime/factory.py | Updates lazy
provider factory imports to `free_claude_code.providers.*` under the new
package layout. |
| tests/contracts/test_import_boundaries.py | Adds contract coverage
requiring runtime packages to live under `src/free_claude_code` and
rejecting old top-level imports. |
| smoke/lib/child_process.py | Updates smoke child-process helpers to
import CLI entrypoints from the namespaced package. |
| README.md | Updates the project layout and extension guidance to refer
to `src/free_claude_code` and importable `free_claude_code.*` modules. |
| uv.lock | Reflects the package version bump associated with the
runtime packaging move. |

</details>

<details open><summary><h3>Sequence Diagram</h3></summary>

<a href="#gh-light-mode-only">

```mermaid
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User as User / CLI
participant Script as Console script
participant Pkg as free_claude_code package
participant API as free_claude_code.api
participant Runtime as free_claude_code.providers.runtime
participant Provider as Provider adapter

User->>Script: run fcc-server / free-claude-code
Script->>Pkg: load free_claude_code.cli.entrypoints:serve
Pkg->>API: create FastAPI app and routes
API->>Runtime: resolve configured provider
Runtime->>Provider: instantiate namespaced adapter
Provider-->>Runtime: stream/model responses
Runtime-->>API: provider result
API-->>User: Anthropic/OpenAI-compatible response
```

</a>
<a href="#gh-dark-mode-only">

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User as User / CLI
participant Script as Console script
participant Pkg as free_claude_code package
participant API as free_claude_code.api
participant Runtime as free_claude_code.providers.runtime
participant Provider as Provider adapter

User->>Script: run fcc-server / free-claude-code
Script->>Pkg: load free_claude_code.cli.entrypoints:serve
Pkg->>API: create FastAPI app and routes
API->>Runtime: resolve configured provider
Runtime->>Provider: instantiate namespaced adapter
Provider-->>Runtime: stream/model responses
Runtime-->>API: provider result
API-->>User: Anthropic/OpenAI-compatible response
```

</a>
</details>

<sub>Reviews (2): Last reviewed commit: ["Fix documented package import
paths"](https://github.com/alishahryar1/free-claude-code/commit/bfa9f2704c45f3684da39657d5e13f3814e5d450)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=42950471)</sub>

<!-- /greptile_comment -->
2026-07-09 01:19:05 -07:00

556 行
18 KiB
Python

import json
import subprocess
from pathlib import Path
from typing import cast
from free_claude_code.config.settings import Settings
from smoke.lib.claude_cli_matrix import (
ClaudeCliRun,
_build_claude_cli_command,
_subagent_probe_options,
make_outcome,
regression_failures,
run_claude_cli,
write_matrix_report,
)
from smoke.lib.config import DEFAULT_TARGETS, SmokeConfig
from smoke.lib.server import RunningServer
def _smoke_config(tmp_path: Path) -> SmokeConfig:
return SmokeConfig(
root=tmp_path,
results_dir=tmp_path / ".smoke-results",
live=False,
interactive=False,
targets=DEFAULT_TARGETS,
provider_matrix=frozenset(),
timeout_s=45.0,
prompt="Reply with exactly: FCC_SMOKE_PONG",
claude_bin="claude",
worker_id="test-worker",
settings=Settings.model_construct(anthropic_auth_token=""),
)
def test_nvidia_nim_cli_matrix_report_shape_and_redaction(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.setenv("NVIDIA_NIM_API_KEY", "secret-nim-key")
run = ClaudeCliRun(
command=("claude", "-p", "redacted"),
returncode=0,
stdout="FCC_NIM_BASIC secret-nim-key",
stderr="",
duration_s=1.25,
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="basic_text",
marker="FCC_NIM_BASIC",
run=run,
log_delta='POST /v1/messages HTTP/1.1" 200 OK secret-nim-key',
log_path=tmp_path / "server.log",
)
path = write_matrix_report(
_smoke_config(tmp_path),
[outcome],
target="nvidia_nim_cli",
filename_prefix="nvidia-nim-cli",
)
payload = json.loads(path.read_text(encoding="utf-8"))
assert path.name.startswith("nvidia-nim-cli-matrix-test-worker-")
assert payload["target"] == "nvidia_nim_cli"
assert payload["models"] == ["nvidia_nim/z-ai/glm-5.2"]
saved = payload["outcomes"][0]
assert saved["feature"] == "basic_text"
assert saved["classification"] == "passed"
assert saved["request_count"] == 1
assert saved["token_evidence"]["marker_present"] is True
assert saved["token_evidence"]["agent_catalog_present"] is False
assert saved["token_evidence"]["agent_tool_count"] == 0
assert saved["token_evidence"]["agent_result_count"] == 0
assert "secret-nim-key" not in path.read_text(encoding="utf-8")
def test_cli_matrix_normalizes_missing_captured_output(
tmp_path: Path, monkeypatch
) -> None:
def fake_run_captured_text(
command: list[str],
**_kwargs: object,
) -> subprocess.CompletedProcess[str]:
return cast(
subprocess.CompletedProcess[str],
subprocess.CompletedProcess(
args=command,
returncode=0,
stdout=None,
stderr=None,
),
)
monkeypatch.setattr(
"smoke.lib.claude_cli_matrix.run_captured_text",
fake_run_captured_text,
)
server = RunningServer(
base_url="http://127.0.0.1:9999",
port=9999,
log_path=tmp_path / "server.log",
process=cast(subprocess.Popen[bytes], object()),
)
run = run_claude_cli(
claude_bin="claude",
server=server,
config=_smoke_config(tmp_path),
cwd=tmp_path / "workspace",
prompt="hello",
tools="",
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="basic_text",
marker="FCC_NIM_BASIC",
run=run,
log_delta='POST /v1/messages HTTP/1.1" 200 OK',
log_path=tmp_path / "server.log",
)
assert run.stdout == ""
assert run.stderr == ""
assert outcome.stdout_excerpt == ""
assert outcome.stderr_excerpt == ""
def test_openrouter_free_cli_matrix_report_shape_and_redaction(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.setenv("OPENROUTER_API_KEY", "secret-openrouter-key")
run = ClaudeCliRun(
command=("claude", "-p", "redacted"),
returncode=0,
stdout="FCC_OPENROUTER_FREE_BASIC secret-openrouter-key",
stderr="",
duration_s=1.25,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="basic_text",
marker="FCC_OPENROUTER_FREE_BASIC",
run=run,
log_delta='POST /v1/messages HTTP/1.1" 200 OK secret-openrouter-key',
log_path=tmp_path / "server.log",
)
path = write_matrix_report(
_smoke_config(tmp_path),
[outcome],
target="openrouter_free_cli",
filename_prefix="openrouter-free-cli",
)
payload = json.loads(path.read_text(encoding="utf-8"))
assert path.name.startswith("openrouter-free-cli-matrix-test-worker-")
assert payload["target"] == "openrouter_free_cli"
assert payload["models"] == ["open_router/openai/gpt-oss-120b:free"]
saved = payload["outcomes"][0]
assert saved["feature"] == "basic_text"
assert saved["classification"] == "passed"
assert saved["request_count"] == 1
assert saved["token_evidence"]["marker_present"] is True
assert saved["token_evidence"]["agent_catalog_present"] is False
assert saved["token_evidence"]["agent_tool_count"] == 0
assert saved["token_evidence"]["agent_result_count"] == 0
assert "secret-openrouter-key" not in path.read_text(encoding="utf-8")
def test_nvidia_nim_cli_matrix_regression_detection(tmp_path: Path) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="basic_text",
marker="FCC_NIM_BASIC",
run=run,
log_delta='POST /v1/messages HTTP/1.1" 500 Internal Server Error',
log_path=tmp_path / "server.log",
)
assert outcome.classification == "product_failure"
assert regression_failures([outcome]) == [
"nvidia_nim/z-ai/glm-5.2 basic_text: product_failure"
]
def test_nvidia_nim_cli_matrix_model_feature_failures_do_not_regress(
tmp_path: Path,
) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="ordinary answer",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="tool_use_roundtrip",
marker="FCC_NIM_TOOL",
run=run,
log_delta='POST /v1/messages HTTP/1.1" 200 OK',
log_path=tmp_path / "server.log",
requires_tool_result=True,
)
assert outcome.classification == "model_feature_failure"
assert regression_failures([outcome]) == []
def test_nvidia_nim_cli_raw_payload_log_counts_as_proxy_request(
tmp_path: Path,
) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="ordinary answer",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="subagent_task",
marker="FCC_NIM_TASK",
run=run,
log_delta="API_REQUEST: request_id=req_1 model=z-ai/glm-5.2 messages=2",
log_path=tmp_path / "server.log",
requires_task=True,
)
assert outcome.classification == "model_feature_failure"
assert outcome.request_count == 1
assert regression_failures([outcome]) == []
def test_cli_matrix_missing_agent_catalog_is_harness_bug(tmp_path: Path) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="ordinary answer",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="subagent_task",
marker="FCC_OPENROUTER_FREE_TASK",
run=run,
log_delta=(
"API_REQUEST: request_id=req_1 model=openai/gpt-oss-120b:free "
"messages=1\n"
"FULL_PAYLOAD [req_1]: {'messages': [], 'tools': [{'name': 'Read'}], "
"'tool_choice': None}"
),
log_path=tmp_path / "server.log",
requires_agent=True,
)
assert outcome.classification == "harness_bug"
assert outcome.token_evidence["agent_catalog_present"] is False
def test_cli_matrix_agent_catalog_without_agent_use_is_model_feature_failure(
tmp_path: Path,
) -> None:
marker = "FCC_OPENROUTER_FREE_TASK"
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout=(
f'{marker}\n{{"type":"tool_use","name":"Read"}}\n{{"type":"tool_result"}}'
),
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="subagent_task",
marker=marker,
run=run,
log_delta=(
"API_REQUEST: request_id=req_1 model=openai/gpt-oss-120b:free "
"messages=1\n"
"FULL_PAYLOAD [req_1]: {'messages': [], 'tools': "
"[{'name': 'Agent'}, {'name': 'Read'}], 'tool_choice': None}"
),
log_path=tmp_path / "server.log",
requires_tool_result=True,
requires_agent=True,
)
assert outcome.classification == "model_feature_failure"
assert outcome.token_evidence["agent_catalog_present"] is True
assert outcome.token_evidence["agent_tool_count"] == 0
def test_cli_matrix_agent_use_result_and_marker_pass(tmp_path: Path) -> None:
marker = "FCC_OPENROUTER_FREE_TASK"
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout=(
f'{marker}\n{{"type":"tool_use","name":"Agent"}}\n'
'{"type":"tool_result","content":"agentId: abc123"}'
),
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="subagent_task",
marker=marker,
run=run,
log_delta=(
"API_REQUEST: request_id=req_1 model=openai/gpt-oss-120b:free "
"messages=1\n"
"FULL_PAYLOAD [req_1]: {'messages': [], 'tools': "
"[{'name': 'Agent'}, {'name': 'Read'}], 'tool_choice': None}"
),
log_path=tmp_path / "server.log",
requires_tool_result=True,
requires_agent=True,
)
assert outcome.classification == "passed"
assert outcome.token_evidence["agent_catalog_present"] is True
assert outcome.token_evidence["agent_tool_count"] == 1
assert outcome.token_evidence["agent_result_count"] == 1
def test_cli_matrix_agent_prompt_text_without_tool_evidence_does_not_pass(
tmp_path: Path,
) -> None:
marker = "FCC_OPENROUTER_FREE_TASK"
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout=f"{marker}\nAgent should read the file.",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="subagent_task",
marker=marker,
run=run,
log_delta=(
"API_REQUEST: request_id=req_1 model=openai/gpt-oss-120b:free "
"messages=1\n"
"FULL_PAYLOAD [req_1]: {'messages': [], 'tools': "
"[{'name': 'Agent'}, {'name': 'Read'}], 'tool_choice': None}"
),
log_path=tmp_path / "server.log",
requires_agent=True,
)
assert outcome.classification == "model_feature_failure"
assert outcome.token_evidence["agent_catalog_present"] is True
assert outcome.token_evidence["agent_tool_count"] == 0
def test_cli_matrix_structured_provider_error_is_upstream_unavailable(
tmp_path: Path,
) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="Provider API request failed. (request_id=req_123)",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="poolside/laguna-m.1:free",
full_model="open_router/poolside/laguna-m.1:free",
source="openrouter_free_cli_default",
feature="tool_use_roundtrip",
marker="FCC_OPENROUTER_FREE_TOOL",
run=run,
log_delta=(
'{"event": "free_claude_code.api.request.received", "http_method": "POST", '
'"http_path": "/v1/messages"}\n'
'{"event": "provider.response.error", "exc_type": "HTTPStatusError"}'
),
log_path=tmp_path / "server.log",
requires_tool_result=True,
)
assert outcome.classification == "upstream_unavailable"
assert outcome.request_count == 1
def test_nvidia_nim_cli_timeout_is_not_model_missing(
tmp_path: Path,
) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=None,
stdout='{"type":"assistant","content":[{"type":"text","text":"FCC_NIM_TOOL"}]}',
stderr="",
duration_s=45.0,
timed_out=True,
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="tool_use_roundtrip",
marker="FCC_NIM_TOOL",
run=run,
log_delta="API_REQUEST: request_id=req_1 model=z-ai/glm-5.2 messages=2",
log_path=tmp_path / "server.log",
)
assert outcome.classification == "probe_timeout"
assert outcome.token_evidence["timed_out"] is True
assert regression_failures([outcome]) == []
def test_nvidia_nim_cli_success_beats_verbose_timeout_words(tmp_path: Path) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="FCC_NIM_THINK",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="z-ai/glm-5.2",
full_model="nvidia_nim/z-ai/glm-5.2",
source="nvidia_nim_cli_default",
feature="thinking",
marker="FCC_NIM_THINK",
run=run,
log_delta=(
"API_REQUEST: request_id=req_1 model=z-ai/glm-5.2 messages=1 "
"read_timeout_s=300"
),
log_path=tmp_path / "server.log",
)
assert outcome.classification == "passed"
assert outcome.request_count == 1
def test_cli_matrix_uuid_429_does_not_count_as_upstream_unavailable(
tmp_path: Path,
) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout='{"uuid":"d3c76eea-3634-4299-aec0-e7634b3716da"}',
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="subagent_task",
marker="FCC_OPENROUTER_FREE_TASK",
run=run,
log_delta="API_REQUEST: request_id=req_1 model=openai/gpt-oss-120b:free messages=2",
log_path=tmp_path / "server.log",
requires_task=True,
)
assert outcome.classification == "model_feature_failure"
def test_cli_matrix_real_http_429_counts_as_upstream_unavailable(
tmp_path: Path,
) -> None:
run = ClaudeCliRun(
command=("claude", "-p", "x"),
returncode=0,
stdout="ordinary answer",
stderr="",
duration_s=0.1,
)
outcome = make_outcome(
model="openai/gpt-oss-120b:free",
full_model="open_router/openai/gpt-oss-120b:free",
source="openrouter_free_cli_default",
feature="subagent_task",
marker="FCC_OPENROUTER_FREE_TASK",
run=run,
log_delta=(
"API_REQUEST: request_id=req_1 model=openai/gpt-oss-120b:free "
'messages=2 upstream HTTP/1.1" 429 Too Many Requests'
),
log_path=tmp_path / "server.log",
requires_task=True,
)
assert outcome.classification == "upstream_unavailable"
def test_cli_matrix_default_command_uses_bare_mode() -> None:
command = _build_claude_cli_command(
claude_bin="claude",
prompt="hello",
tools="Read",
)
assert command[:2] == ("claude", "--bare")
assert "--tools" in command
assert "Read" in command
def test_cli_matrix_subagent_command_uses_agent_without_bare_or_task() -> None:
bare, tools, pre_tool_args, extra_args = _subagent_probe_options("{}")
command = _build_claude_cli_command(
claude_bin="claude",
prompt="hello",
tools=tools,
bare=bare,
pre_tool_args=pre_tool_args,
extra_args=extra_args,
)
assert "--bare" not in command
assert command[command.index("--setting-sources") + 1] == "local"
assert "--strict-mcp-config" in command
assert command[command.index("--mcp-config") + 1] == '{"mcpServers":{}}'
assert command[command.index("--tools") + 1] == "Agent,Read"
assert command[command.index("--allowedTools") + 1] == "Agent,Read"
assert command[command.index("--agents") + 1] == "{}"
assert "Task,Read" not in command