mng-tutor 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ .venv-*
142
+ env/
143
+ venv/
144
+ ENV/
145
+ env.bak/
146
+ venv.bak/
147
+
148
+ # Spyder project settings
149
+ .spyderproject
150
+ .spyproject
151
+
152
+ # Rope project settings
153
+ .ropeproject
154
+
155
+ # mkdocs documentation
156
+ /site
157
+
158
+ # mypy
159
+ .mypy_cache/
160
+ .dmypy.json
161
+ dmypy.json
162
+
163
+ # Pyre type checker
164
+ .pyre/
165
+
166
+ # pytype static type analyzer
167
+ .pytype/
168
+
169
+ # Cython debug symbols
170
+ cython_debug/
171
+
172
+ # PyCharm
173
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
176
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
177
+ .idea/
178
+
179
+ # Abstra
180
+ # Abstra is an AI-powered process automation framework.
181
+ # Ignore directories containing user credentials, local state, and settings.
182
+ # Learn more at https://abstra.io/docs
183
+ .abstra/
184
+
185
+ # Visual Studio Code
186
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
187
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
188
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
189
+ # you could uncomment the following to ignore the entire vscode folder
190
+ # .vscode/
191
+
192
+ # Ruff stuff:
193
+ .ruff_cache/
194
+
195
+ # PyPI configuration file
196
+ .pypirc
197
+
198
+ # Cursor
199
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
200
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
201
+ # refer to https://docs.cursor.com/context/ignore-files
202
+ .cursorignore
203
+ .cursorindexingignore
204
+
205
+ # Marimo
206
+ marimo/_static/
207
+ marimo/_lsp/
208
+ __marimo__/
209
+
210
+ # task folders for local custom claude workflow
211
+ */*/_tasks/
212
+ .sesskey
213
+
214
+ # Node.js
215
+ node_modules/
216
+
217
+ # Frontend build artifacts
218
+ frontend-dist/
219
+
220
+ # ignore review files
221
+ .reviews/
222
+
223
+ # files necessary to smuggle session id into reviewers
224
+ .claude/sessionid
225
+
226
+ # Claude Code local settings (session-specific permissions)
227
+ .claude/settings.local.json
228
+
229
+ # PR status and URL files written by main_claude_stop_hook.sh for status line display
230
+ .claude/pr_url
231
+ .claude/pr_status
232
+
233
+ # History of commits reviewed by main_claude_stop_hook.sh (to detect stuck agents)
234
+ .claude/reviewed_commits
235
+
236
+ # Local Claude settings backup files
237
+ .claude/*.bak
238
+
239
+ # Local mng settings (not committed)
240
+ .mng/settings.local.toml
241
+
242
+ # so the user can make their own notification script
243
+ scripts/notify_user.local.sh
244
+
245
+ # Test output files (slow tests, coverage summaries)
246
+ .test_output/
247
+
248
+ # Active session marker file (used to detect interrupted sessions)
249
+ .claude/active
250
+
251
+ # we stick the image build artifacts here
252
+ .mng/dev/build/
253
+ .mng/dev/secrets/
254
+
255
+ # Offload caches and local files
256
+ .offload/**
257
+ test-results/**
258
+ current.tar.gz
259
+
260
+ # Changelings deploy-time build artifacts
261
+ .changelings/
262
+
263
+ # Autofix working artifacts
264
+ .autofix/
265
+
266
+ # Demo recordings (asciinema .cast, .txt, and scripts)
267
+ .demos/
268
+
269
+ # for git worktrees from other repos
270
+ .external_worktrees/
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: mng-tutor
3
+ Version: 0.1.1
4
+ Summary: Interactive tutorial plugin for mng
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: click-option-group>=0.5.6
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: mng==0.1.6
@@ -0,0 +1,15 @@
1
+ """Project-level conftest for mng-tutor.
2
+
3
+ When running tests from libs/mng_tutor/, this conftest provides the common pytest hooks
4
+ that would otherwise come from the monorepo root conftest.py (which is not discovered
5
+ when pytest runs from a subdirectory).
6
+
7
+ When running from the monorepo root, the root conftest.py registers the hooks first,
8
+ and this file's register_conftest_hooks() call is a no-op (guarded by a module-level flag).
9
+ """
10
+
11
+ from imbue.imbue_common.conftest_hooks import register_conftest_hooks
12
+ from imbue.mng.utils.logging import suppress_warnings
13
+
14
+ suppress_warnings()
15
+ register_conftest_hooks(globals())
File without changes
@@ -0,0 +1,95 @@
1
+ import subprocess
2
+ from typing import assert_never
3
+
4
+ from loguru import logger
5
+
6
+ from imbue.mng.api.list import list_agents
7
+ from imbue.mng.config.data_types import MngContext
8
+ from imbue.mng.errors import BaseMngError
9
+ from imbue.mng.interfaces.data_types import AgentDetails
10
+ from imbue.mng.primitives import AgentLifecycleState
11
+ from imbue.mng.primitives import AgentName
12
+ from imbue.mng.primitives import ErrorBehavior
13
+ from imbue.mng_tutor.data_types import AgentExistsCheck
14
+ from imbue.mng_tutor.data_types import AgentInStateCheck
15
+ from imbue.mng_tutor.data_types import AgentNotExistsCheck
16
+ from imbue.mng_tutor.data_types import FileExistsInAgentWorkDirCheck
17
+ from imbue.mng_tutor.data_types import StepCheck
18
+ from imbue.mng_tutor.data_types import TmuxSessionHasClientsCheck
19
+
20
+
21
+ def _find_agent_by_name(agent_name: AgentName, mng_ctx: MngContext) -> AgentDetails | None:
22
+ """Find an agent by name, returning None if not found."""
23
+ result = list_agents(mng_ctx, is_streaming=False, error_behavior=ErrorBehavior.CONTINUE)
24
+ for agent in result.agents:
25
+ if agent.name == agent_name:
26
+ return agent
27
+ return None
28
+
29
+
30
+ def _check_agent_exists(agent_name: AgentName, mng_ctx: MngContext) -> bool:
31
+ """Check if an agent with the given name exists."""
32
+ return _find_agent_by_name(agent_name, mng_ctx) is not None
33
+
34
+
35
+ def _check_agent_in_state(
36
+ agent_name: AgentName,
37
+ expected_states: tuple[AgentLifecycleState, ...],
38
+ mng_ctx: MngContext,
39
+ ) -> bool:
40
+ """Check if an agent is in one of the expected lifecycle states."""
41
+ agent = _find_agent_by_name(agent_name, mng_ctx)
42
+ if agent is None:
43
+ return False
44
+ return agent.state in expected_states
45
+
46
+
47
+ def _check_file_exists_in_work_dir(
48
+ agent_name: AgentName,
49
+ file_path: str,
50
+ mng_ctx: MngContext,
51
+ ) -> bool:
52
+ """Check if a file exists in the agent's working directory."""
53
+ agent = _find_agent_by_name(agent_name, mng_ctx)
54
+ if agent is None:
55
+ return False
56
+ full_path = agent.work_dir / file_path
57
+ return full_path.exists()
58
+
59
+
60
+ def _check_tmux_session_has_clients(agent_name: AgentName, mng_ctx: MngContext) -> bool:
61
+ """Check if the agent's tmux session has at least one attached client."""
62
+ session_name = f"{mng_ctx.config.prefix}{agent_name}"
63
+ result = subprocess.run(
64
+ ["tmux", "list-clients", "-t", session_name],
65
+ capture_output=True,
66
+ text=True,
67
+ )
68
+ if result.returncode != 0:
69
+ return False
70
+ return len(result.stdout.strip()) > 0
71
+
72
+
73
+ def _execute_check(check: StepCheck, mng_ctx: MngContext) -> bool:
74
+ """Execute the check logic for a single step."""
75
+ if isinstance(check, AgentExistsCheck):
76
+ return _check_agent_exists(check.agent_name, mng_ctx)
77
+ elif isinstance(check, AgentNotExistsCheck):
78
+ return not _check_agent_exists(check.agent_name, mng_ctx)
79
+ elif isinstance(check, AgentInStateCheck):
80
+ return _check_agent_in_state(check.agent_name, check.expected_states, mng_ctx)
81
+ elif isinstance(check, FileExistsInAgentWorkDirCheck):
82
+ return _check_file_exists_in_work_dir(check.agent_name, check.file_path, mng_ctx)
83
+ elif isinstance(check, TmuxSessionHasClientsCheck):
84
+ return _check_tmux_session_has_clients(check.agent_name, mng_ctx)
85
+ else:
86
+ assert_never(check)
87
+
88
+
89
+ def run_check(check: StepCheck, mng_ctx: MngContext) -> bool:
90
+ """Execute a step check and return whether it passes."""
91
+ try:
92
+ return _execute_check(check, mng_ctx)
93
+ except (BaseMngError, OSError):
94
+ logger.debug("Check failed with exception for check type: {}", type(check).__name__)
95
+ return False
@@ -0,0 +1,43 @@
1
+ import pytest
2
+
3
+ from imbue.mng.config.data_types import MngContext
4
+ from imbue.mng.primitives import AgentLifecycleState
5
+ from imbue.mng.primitives import AgentName
6
+ from imbue.mng_tutor.checks import run_check
7
+ from imbue.mng_tutor.data_types import AgentExistsCheck
8
+ from imbue.mng_tutor.data_types import AgentInStateCheck
9
+ from imbue.mng_tutor.data_types import AgentNotExistsCheck
10
+ from imbue.mng_tutor.data_types import FileExistsInAgentWorkDirCheck
11
+ from imbue.mng_tutor.data_types import TmuxSessionHasClientsCheck
12
+
13
+
14
+ def test_agent_exists_check_returns_false_when_no_agents(temp_mng_ctx: MngContext) -> None:
15
+ check = AgentExistsCheck(agent_name=AgentName("nonexistent"))
16
+ assert run_check(check, temp_mng_ctx) is False
17
+
18
+
19
+ def test_agent_not_exists_check_returns_true_when_no_agents(temp_mng_ctx: MngContext) -> None:
20
+ check = AgentNotExistsCheck(agent_name=AgentName("nonexistent"))
21
+ assert run_check(check, temp_mng_ctx) is True
22
+
23
+
24
+ def test_agent_in_state_check_returns_false_when_no_agents(temp_mng_ctx: MngContext) -> None:
25
+ check = AgentInStateCheck(
26
+ agent_name=AgentName("nonexistent"),
27
+ expected_states=(AgentLifecycleState.RUNNING,),
28
+ )
29
+ assert run_check(check, temp_mng_ctx) is False
30
+
31
+
32
+ def test_file_exists_check_returns_false_when_no_agents(temp_mng_ctx: MngContext) -> None:
33
+ check = FileExistsInAgentWorkDirCheck(
34
+ agent_name=AgentName("nonexistent"),
35
+ file_path="hello.txt",
36
+ )
37
+ assert run_check(check, temp_mng_ctx) is False
38
+
39
+
40
+ @pytest.mark.tmux
41
+ def test_tmux_session_has_clients_check_returns_false_when_no_session(temp_mng_ctx: MngContext) -> None:
42
+ check = TmuxSessionHasClientsCheck(agent_name=AgentName("nonexistent"))
43
+ assert run_check(check, temp_mng_ctx) is False
@@ -0,0 +1,56 @@
1
+ from typing import Any
2
+
3
+ import click
4
+
5
+ from imbue.mng.cli.common_opts import CommonCliOptions
6
+ from imbue.mng.cli.common_opts import add_common_options
7
+ from imbue.mng.cli.common_opts import setup_command_context
8
+ from imbue.mng.cli.help_formatter import CommandHelpMetadata
9
+ from imbue.mng.cli.help_formatter import add_pager_help_option
10
+ from imbue.mng_tutor.lessons import ALL_LESSONS
11
+ from imbue.mng_tutor.tui import run_lesson_runner
12
+ from imbue.mng_tutor.tui import run_lesson_selector
13
+
14
+
15
+ class TutorCliOptions(CommonCliOptions):
16
+ """Options for the tutor command."""
17
+
18
+
19
+ @click.command()
20
+ @add_common_options
21
+ @click.pass_context
22
+ def tutor(ctx: click.Context, **kwargs: Any) -> None:
23
+ mng_ctx, output_opts, opts = setup_command_context(
24
+ ctx=ctx,
25
+ command_name="tutor",
26
+ command_class=TutorCliOptions,
27
+ )
28
+
29
+ # Loop: select a lesson, run it, return to selector when done
30
+ lesson = run_lesson_selector(ALL_LESSONS)
31
+ while lesson is not None:
32
+ run_lesson_runner(lesson, mng_ctx)
33
+ lesson = run_lesson_selector(ALL_LESSONS)
34
+
35
+
36
+ CommandHelpMetadata(
37
+ key="tutor",
38
+ one_line_description="Interactive tutorial for learning mng commands",
39
+ synopsis="mng tutor [OPTIONS]",
40
+ description="""Launches an interactive tutorial that guides you through learning
41
+ mng commands step by step. Run this in a separate terminal from your
42
+ main working terminal.
43
+
44
+ Each lesson contains a series of steps with automatic completion detection.
45
+ Follow the instructions in the tutor terminal and complete each step in
46
+ your other terminal. The tutor automatically detects when each step is
47
+ complete and advances to the next one.""",
48
+ examples=(("Start the interactive tutor", "mng tutor"),),
49
+ see_also=(
50
+ ("create", "Create a new agent"),
51
+ ("connect", "Connect to an agent"),
52
+ ("list", "List agents"),
53
+ ),
54
+ ).register()
55
+
56
+ add_pager_help_option(tutor)
@@ -0,0 +1,9 @@
1
+ """Test fixtures for mng-tutor.
2
+
3
+ Uses shared plugin test fixtures from mng for common setup (plugin manager,
4
+ environment isolation, git repos, temp_mng_ctx, local_provider, etc.).
5
+ """
6
+
7
+ from imbue.mng.utils.plugin_testing import register_plugin_test_fixtures
8
+
9
+ register_plugin_test_fixtures(globals())
@@ -0,0 +1,72 @@
1
+ from typing import Annotated
2
+ from typing import Literal
3
+
4
+ from pydantic import Discriminator
5
+ from pydantic import Field
6
+
7
+ from imbue.imbue_common.frozen_model import FrozenModel
8
+ from imbue.mng.primitives import AgentLifecycleState
9
+ from imbue.mng.primitives import AgentName
10
+
11
+
12
+ class AgentExistsCheck(FrozenModel):
13
+ """Check that an agent with the given name exists."""
14
+
15
+ check_type: Literal["agent_exists"] = "agent_exists"
16
+ agent_name: AgentName = Field(description="Name of the agent to check for")
17
+
18
+
19
+ class AgentNotExistsCheck(FrozenModel):
20
+ """Check that an agent with the given name does not exist."""
21
+
22
+ check_type: Literal["agent_not_exists"] = "agent_not_exists"
23
+ agent_name: AgentName = Field(description="Name of the agent to check for")
24
+
25
+
26
+ class AgentInStateCheck(FrozenModel):
27
+ """Check that an agent is in one of the expected lifecycle states."""
28
+
29
+ check_type: Literal["agent_in_state"] = "agent_in_state"
30
+ agent_name: AgentName = Field(description="Name of the agent to check")
31
+ expected_states: tuple[AgentLifecycleState, ...] = Field(description="Acceptable lifecycle states")
32
+
33
+
34
+ class FileExistsInAgentWorkDirCheck(FrozenModel):
35
+ """Check that a file exists in an agent's working directory."""
36
+
37
+ check_type: Literal["file_exists_in_work_dir"] = "file_exists_in_work_dir"
38
+ agent_name: AgentName = Field(description="Name of the agent whose work_dir to check")
39
+ file_path: str = Field(description="Relative path within the agent's work_dir")
40
+
41
+
42
+ class TmuxSessionHasClientsCheck(FrozenModel):
43
+ """Check that an agent's tmux session has at least one attached client."""
44
+
45
+ check_type: Literal["tmux_session_has_clients"] = "tmux_session_has_clients"
46
+ agent_name: AgentName = Field(description="Name of the agent whose tmux session to check")
47
+
48
+
49
+ StepCheck = Annotated[
50
+ AgentExistsCheck
51
+ | AgentNotExistsCheck
52
+ | AgentInStateCheck
53
+ | FileExistsInAgentWorkDirCheck
54
+ | TmuxSessionHasClientsCheck,
55
+ Discriminator("check_type"),
56
+ ]
57
+
58
+
59
+ class LessonStep(FrozenModel):
60
+ """A single step in a tutorial lesson."""
61
+
62
+ heading: str = Field(description="Short heading for the step")
63
+ details: str = Field(description="Detailed instructions for completing the step")
64
+ check: StepCheck = Field(description="How to verify the step is complete")
65
+
66
+
67
+ class Lesson(FrozenModel):
68
+ """A complete tutorial lesson with ordered steps."""
69
+
70
+ title: str = Field(description="Title of the lesson")
71
+ description: str = Field(description="Brief description of what this lesson teaches")
72
+ steps: tuple[LessonStep, ...] = Field(description="Ordered steps to complete")
@@ -0,0 +1,101 @@
1
+ from imbue.mng.primitives import AgentLifecycleState
2
+ from imbue.mng.primitives import AgentName
3
+ from imbue.mng_tutor.data_types import AgentExistsCheck
4
+ from imbue.mng_tutor.data_types import AgentInStateCheck
5
+ from imbue.mng_tutor.data_types import AgentNotExistsCheck
6
+ from imbue.mng_tutor.data_types import FileExistsInAgentWorkDirCheck
7
+ from imbue.mng_tutor.data_types import Lesson
8
+ from imbue.mng_tutor.data_types import LessonStep
9
+ from imbue.mng_tutor.data_types import TmuxSessionHasClientsCheck
10
+
11
+
12
+ def test_agent_exists_check_construction() -> None:
13
+ check = AgentExistsCheck(agent_name=AgentName("test-agent"))
14
+ assert check.check_type == "agent_exists"
15
+ assert check.agent_name == AgentName("test-agent")
16
+
17
+
18
+ def test_agent_not_exists_check_construction() -> None:
19
+ check = AgentNotExistsCheck(agent_name=AgentName("test-agent"))
20
+ assert check.check_type == "agent_not_exists"
21
+ assert check.agent_name == AgentName("test-agent")
22
+
23
+
24
+ def test_agent_in_state_check_construction() -> None:
25
+ check = AgentInStateCheck(
26
+ agent_name=AgentName("test-agent"),
27
+ expected_states=(AgentLifecycleState.RUNNING,),
28
+ )
29
+ assert check.check_type == "agent_in_state"
30
+ assert check.agent_name == AgentName("test-agent")
31
+ assert check.expected_states == (AgentLifecycleState.RUNNING,)
32
+
33
+
34
+ def test_agent_in_state_check_accepts_multiple_states() -> None:
35
+ check = AgentInStateCheck(
36
+ agent_name=AgentName("test-agent"),
37
+ expected_states=(AgentLifecycleState.RUNNING, AgentLifecycleState.WAITING),
38
+ )
39
+ assert len(check.expected_states) == 2
40
+
41
+
42
+ def test_file_exists_check_construction() -> None:
43
+ check = FileExistsInAgentWorkDirCheck(
44
+ agent_name=AgentName("test-agent"),
45
+ file_path="hello.txt",
46
+ )
47
+ assert check.check_type == "file_exists_in_work_dir"
48
+ assert check.agent_name == AgentName("test-agent")
49
+ assert check.file_path == "hello.txt"
50
+
51
+
52
+ def test_tmux_session_has_clients_check_construction() -> None:
53
+ check = TmuxSessionHasClientsCheck(agent_name=AgentName("test-agent"))
54
+ assert check.check_type == "tmux_session_has_clients"
55
+ assert check.agent_name == AgentName("test-agent")
56
+
57
+
58
+ def test_lesson_step_with_agent_exists_check() -> None:
59
+ step = LessonStep(
60
+ heading="Create an agent",
61
+ details="Run `mng create test-agent`.",
62
+ check=AgentExistsCheck(agent_name=AgentName("test-agent")),
63
+ )
64
+ assert step.heading == "Create an agent"
65
+ assert isinstance(step.check, AgentExistsCheck)
66
+
67
+
68
+ def test_lesson_step_with_agent_in_state_check() -> None:
69
+ step = LessonStep(
70
+ heading="Stop the agent",
71
+ details="Run `mng stop test-agent`.",
72
+ check=AgentInStateCheck(
73
+ agent_name=AgentName("test-agent"),
74
+ expected_states=(AgentLifecycleState.STOPPED,),
75
+ ),
76
+ )
77
+ assert isinstance(step.check, AgentInStateCheck)
78
+ assert step.check.expected_states == (AgentLifecycleState.STOPPED,)
79
+
80
+
81
+ def test_lesson_construction() -> None:
82
+ lesson = Lesson(
83
+ title="Test Lesson",
84
+ description="A test lesson.",
85
+ steps=(
86
+ LessonStep(
87
+ heading="Step 1",
88
+ details="Do step 1.",
89
+ check=AgentExistsCheck(agent_name=AgentName("agent-1")),
90
+ ),
91
+ LessonStep(
92
+ heading="Step 2",
93
+ details="Do step 2.",
94
+ check=AgentNotExistsCheck(agent_name=AgentName("agent-1")),
95
+ ),
96
+ ),
97
+ )
98
+ assert lesson.title == "Test Lesson"
99
+ assert len(lesson.steps) == 2
100
+ assert isinstance(lesson.steps[0].check, AgentExistsCheck)
101
+ assert isinstance(lesson.steps[1].check, AgentNotExistsCheck)