lemming-cli 0.3.0__tar.gz → 0.3.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.
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/PKG-INFO +1 -1
- lemming_cli-0.3.1/docs/screenshots/dashboard-desktop.png +0 -0
- lemming_cli-0.3.1/docs/screenshots/dashboard-mobile.png +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/pyproject.toml +1 -1
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/models.py +2 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/models_test.py +2 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/runner.py +9 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/runner_test.py +2 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/__init__.py +3 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/lifecycle.py +36 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/lifecycle_test.py +21 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/operations.py +6 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/dashboard.spec.js +37 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/dashboard.test.js +46 -1
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/index.js +15 -1
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/uv.lock +1 -1
- lemming_cli-0.3.0/docs/screenshots/dashboard-desktop.png +0 -0
- lemming_cli-0.3.0/docs/screenshots/dashboard-mobile.png +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/.github/workflows/ci.yml +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/.github/workflows/publish.yml +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/.github/workflows/screenshots.yml +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/.gitignore +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/.prettierignore +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/.prettierrc +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/Dockerfile +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/LICENSE +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/README.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/biome.json +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/docker-compose.yml +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/docs/EVALS.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/docs/HOOKS.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/docs/screenshots/task-log-desktop.png +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/docs/screenshots/task-log-mobile.png +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/package-lock.json +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/package.json +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/playwright.config.js +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/__init__.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/__init__.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/auth.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/auth_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/config.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/config_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/conftest.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/context.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/context_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/directories.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/directories_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/files.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/files_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/hooks.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/hooks_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/logging.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/logging_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/loop.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/loop_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/main.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/main_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/tasks.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api/tasks_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/api_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/__init__.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/config.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/config_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/goal.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/goal_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/hooks.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/hooks_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/main.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/main_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/operations.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/operations_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/progress.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/progress_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/readability_cli.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/readability_cli_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/tasks.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli/tasks_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/cli_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/conftest.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/__init__.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/__main__.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/cli.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/cli_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/container.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/container_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/fixtures.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/fixtures_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/harness.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/harness_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/readability.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/readability_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/roadmap.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/roadmap_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/scenarios.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/scenarios_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/suites.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/trial.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/evals/trial_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/hooks.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/hooks_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/integration_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/main.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/main_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/orchestrator.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/orchestrator_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/paths.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/paths_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/persistence.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/persistence_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts/hooks/50-readability.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts/hooks/60-testing.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts/hooks/70-ux.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts/hooks/90-roadmap.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts/taskrunner.md +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/prompts_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/providers.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/providers_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/operations_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/progress.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/progress_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/queries.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/tasks/queries_test.py +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/favicon.js +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/favicon.spec.js +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/files.html +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/files.spec.js +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/index.html +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/logs.html +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/logs.test.js +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/mancha.js +0 -0
- {lemming_cli-0.3.0 → lemming_cli-0.3.1}/src/lemming/web/screenshots.spec.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lemming-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: An autonomous, iterative task runner for AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/owahltinez/lemming
|
|
6
6
|
Project-URL: Repository, https://github.com/owahltinez/lemming
|
|
Binary file
|
|
Binary file
|
|
@@ -60,6 +60,8 @@ class Task(pydantic.BaseModel):
|
|
|
60
60
|
created_at: float = pydantic.Field(default_factory=time.time)
|
|
61
61
|
run_time: float = 0.0
|
|
62
62
|
execution_times: dict[str, float] | None = None
|
|
63
|
+
active_execution_component: str | None = None
|
|
64
|
+
active_execution_started_at: float | None = None
|
|
63
65
|
pid: int | None = None
|
|
64
66
|
last_heartbeat: float | None = None
|
|
65
67
|
has_runner_log: bool = False
|
|
@@ -19,6 +19,8 @@ def test_task_model_defaults():
|
|
|
19
19
|
assert task.progress == []
|
|
20
20
|
assert task.run_time == 0.0
|
|
21
21
|
assert task.execution_times is None
|
|
22
|
+
assert task.active_execution_component is None
|
|
23
|
+
assert task.active_execution_started_at is None
|
|
22
24
|
assert task.superseded_at is None
|
|
23
25
|
assert task.superseded_reason is None
|
|
24
26
|
assert task.created_at > 0
|
|
@@ -306,6 +306,15 @@ def run_with_heartbeat(
|
|
|
306
306
|
if header and header.startswith("Hook: ")
|
|
307
307
|
else "runner"
|
|
308
308
|
)
|
|
309
|
+
try:
|
|
310
|
+
tasks.mark_execution_started(tasks_file, task_id, component)
|
|
311
|
+
except Exception:
|
|
312
|
+
logger.warning(
|
|
313
|
+
"Could not mark %s execution as active for task %s",
|
|
314
|
+
component,
|
|
315
|
+
task_id,
|
|
316
|
+
exc_info=True,
|
|
317
|
+
)
|
|
309
318
|
|
|
310
319
|
full_log: list[str] = []
|
|
311
320
|
|
|
@@ -302,6 +302,8 @@ def test_run_with_heartbeat_records_runner_and_hook_times(tmp_path):
|
|
|
302
302
|
assert task.execution_times is not None
|
|
303
303
|
assert task.execution_times["runner"] > 0
|
|
304
304
|
assert task.execution_times["hook:testing"] > 0
|
|
305
|
+
assert task.active_execution_component is None
|
|
306
|
+
assert task.active_execution_started_at is None
|
|
305
307
|
|
|
306
308
|
|
|
307
309
|
def test_run_with_heartbeat_interruption_cleanup(tmp_path):
|
|
@@ -25,6 +25,9 @@ from .lifecycle import (
|
|
|
25
25
|
from .lifecycle import (
|
|
26
26
|
is_task_active as is_task_active,
|
|
27
27
|
)
|
|
28
|
+
from .lifecycle import (
|
|
29
|
+
mark_execution_started as mark_execution_started,
|
|
30
|
+
)
|
|
28
31
|
from .lifecycle import (
|
|
29
32
|
mark_task_in_progress as mark_task_in_progress,
|
|
30
33
|
)
|
|
@@ -83,6 +83,34 @@ def record_execution_time(
|
|
|
83
83
|
task.execution_times[component] = (
|
|
84
84
|
task.execution_times.get(component, 0.0) + duration
|
|
85
85
|
)
|
|
86
|
+
if task.active_execution_component == component:
|
|
87
|
+
task.active_execution_component = None
|
|
88
|
+
task.active_execution_started_at = None
|
|
89
|
+
persistence.save_tasks(tasks_file, data)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def mark_execution_started(
|
|
93
|
+
tasks_file: pathlib.Path,
|
|
94
|
+
task_id: str,
|
|
95
|
+
component: str,
|
|
96
|
+
started_at: float | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Records the component whose elapsed time is currently accumulating.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
tasks_file: Path to the tasks YAML file.
|
|
102
|
+
task_id: ID of the task whose subprocess started.
|
|
103
|
+
component: Stable component key, such as ``runner``.
|
|
104
|
+
started_at: Optional wall-clock timestamp, defaulting to now.
|
|
105
|
+
"""
|
|
106
|
+
with persistence.lock_tasks(tasks_file):
|
|
107
|
+
data = persistence.load_tasks(tasks_file)
|
|
108
|
+
task = next((t for t in data.tasks if t.id == task_id), None)
|
|
109
|
+
if task is None:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
task.active_execution_component = component
|
|
113
|
+
task.active_execution_started_at = started_at or time.time()
|
|
86
114
|
persistence.save_tasks(tasks_file, data)
|
|
87
115
|
|
|
88
116
|
|
|
@@ -141,6 +169,8 @@ def _mark_task_in_progress(
|
|
|
141
169
|
if task.started_at is None:
|
|
142
170
|
task.started_at = now
|
|
143
171
|
task.last_started_at = now
|
|
172
|
+
task.active_execution_component = None
|
|
173
|
+
task.active_execution_started_at = None
|
|
144
174
|
if pid is not None:
|
|
145
175
|
task.pid = pid
|
|
146
176
|
return True
|
|
@@ -259,6 +289,8 @@ def revert_task_to_pending(
|
|
|
259
289
|
task.requested_status = None
|
|
260
290
|
task.pid = None
|
|
261
291
|
task.last_heartbeat = None
|
|
292
|
+
task.active_execution_component = None
|
|
293
|
+
task.active_execution_started_at = None
|
|
262
294
|
|
|
263
295
|
persistence.save_tasks(tasks_file, data)
|
|
264
296
|
return task
|
|
@@ -376,6 +408,8 @@ def cancel_task(tasks_file: pathlib.Path, task_id: str) -> bool:
|
|
|
376
408
|
task.pid = None
|
|
377
409
|
task.last_heartbeat = None
|
|
378
410
|
task.requested_status = None
|
|
411
|
+
task.active_execution_component = None
|
|
412
|
+
task.active_execution_started_at = None
|
|
379
413
|
|
|
380
414
|
persistence.save_tasks(tasks_file, data)
|
|
381
415
|
|
|
@@ -414,6 +448,8 @@ def reset_task(tasks_file: pathlib.Path, task_id: str) -> models.Task:
|
|
|
414
448
|
target.progress = []
|
|
415
449
|
target.run_time = 0.0
|
|
416
450
|
target.execution_times = None
|
|
451
|
+
target.active_execution_component = None
|
|
452
|
+
target.active_execution_started_at = None
|
|
417
453
|
target.completed_at = None
|
|
418
454
|
target.superseded_at = None
|
|
419
455
|
target.superseded_reason = None
|
|
@@ -61,6 +61,27 @@ def test_record_execution_time_accumulates_by_component(tmp_path):
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def test_active_execution_is_recorded_then_cleared(tmp_path):
|
|
65
|
+
tasks_file = tmp_path / "tasks.yml"
|
|
66
|
+
persistence.save_tasks(
|
|
67
|
+
tasks_file,
|
|
68
|
+
models.Roadmap(tasks=[models.Task(id="1", description="test")]),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
lifecycle.mark_execution_started(
|
|
72
|
+
tasks_file, "1", "hook:testing", started_at=123.0
|
|
73
|
+
)
|
|
74
|
+
active_task = persistence.load_tasks(tasks_file).tasks[0]
|
|
75
|
+
assert active_task.active_execution_component == "hook:testing"
|
|
76
|
+
assert active_task.active_execution_started_at == 123.0
|
|
77
|
+
|
|
78
|
+
lifecycle.record_execution_time(tasks_file, "1", "hook:testing", 5.0)
|
|
79
|
+
finished_task = persistence.load_tasks(tasks_file).tasks[0]
|
|
80
|
+
assert finished_task.execution_times == {"hook:testing": 5.0}
|
|
81
|
+
assert finished_task.active_execution_component is None
|
|
82
|
+
assert finished_task.active_execution_started_at is None
|
|
83
|
+
|
|
84
|
+
|
|
64
85
|
def test_mark_task_in_progress(tmp_path):
|
|
65
86
|
tasks_file = tmp_path / "tasks.yml"
|
|
66
87
|
data = models.Roadmap(
|
|
@@ -216,6 +216,8 @@ def supersede_task(
|
|
|
216
216
|
target.pid = None
|
|
217
217
|
target.last_heartbeat = None
|
|
218
218
|
target.requested_status = None
|
|
219
|
+
target.active_execution_component = None
|
|
220
|
+
target.active_execution_started_at = None
|
|
219
221
|
|
|
220
222
|
persistence.save_tasks(tasks_file, data)
|
|
221
223
|
return target
|
|
@@ -320,12 +322,16 @@ def update_task(
|
|
|
320
322
|
target.pid = None
|
|
321
323
|
target.last_heartbeat = None
|
|
322
324
|
target.requested_status = None
|
|
325
|
+
target.active_execution_component = None
|
|
326
|
+
target.active_execution_started_at = None
|
|
323
327
|
elif status == models.TaskStatus.PENDING:
|
|
324
328
|
target.completed_at = None
|
|
325
329
|
target.superseded_at = None
|
|
326
330
|
target.superseded_reason = None
|
|
327
331
|
target.attempts = 0
|
|
328
332
|
target.requested_status = None
|
|
333
|
+
target.active_execution_component = None
|
|
334
|
+
target.active_execution_started_at = None
|
|
329
335
|
elif target.completed_at is not None:
|
|
330
336
|
target.completed_at = None
|
|
331
337
|
|
|
@@ -163,6 +163,43 @@ test.describe('Dashboard E2E', () => {
|
|
|
163
163
|
expect(new Set(measurements.map((segment) => segment.color)).size).toBe(3);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
test('advances the active execution segment while a task is running', async ({
|
|
167
|
+
page,
|
|
168
|
+
}) => {
|
|
169
|
+
const activeStartedAt = Date.now() / 1000 - 5;
|
|
170
|
+
await page.route('**/api/data', async (route) => {
|
|
171
|
+
await route.fulfill({
|
|
172
|
+
contentType: 'application/json',
|
|
173
|
+
json: {
|
|
174
|
+
cwd: '/mock/cwd',
|
|
175
|
+
loop_running: true,
|
|
176
|
+
goal: 'Live timing project',
|
|
177
|
+
tasks: [
|
|
178
|
+
{
|
|
179
|
+
id: 'running',
|
|
180
|
+
description: 'Task with live execution timing',
|
|
181
|
+
status: 'in_progress',
|
|
182
|
+
attempts: 1,
|
|
183
|
+
progress: [],
|
|
184
|
+
active_execution_component: 'runner',
|
|
185
|
+
active_execution_started_at: activeStartedAt,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
config: { retries: 3, runner: 'agy', time_limit: 60 },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await gotoAndAwaitMancha(page);
|
|
194
|
+
await page.getByRole('button', { name: 'Show details' }).click();
|
|
195
|
+
|
|
196
|
+
const timingBar = page.getByTestId('execution-breakdown').getByRole('img');
|
|
197
|
+
const initialLabel = await timingBar.getAttribute('aria-label');
|
|
198
|
+
await expect
|
|
199
|
+
.poll(() => timingBar.getAttribute('aria-label'))
|
|
200
|
+
.not.toBe(initialLabel);
|
|
201
|
+
});
|
|
202
|
+
|
|
166
203
|
// --- Environment Overrides Tests ---
|
|
167
204
|
|
|
168
205
|
test('adds an environment override, stores it in localStorage, and sends it on run', async ({
|
|
@@ -98,7 +98,21 @@ const createInitialState = (overrides = {}) => {
|
|
|
98
98
|
'bg-purple-600',
|
|
99
99
|
'bg-red-600',
|
|
100
100
|
];
|
|
101
|
-
const
|
|
101
|
+
const executionTimes = { ...(task.execution_times || {}) };
|
|
102
|
+
if (
|
|
103
|
+
task.status === 'in_progress' &&
|
|
104
|
+
task.active_execution_component &&
|
|
105
|
+
task.active_execution_started_at
|
|
106
|
+
) {
|
|
107
|
+
const activeDuration = Math.max(
|
|
108
|
+
0,
|
|
109
|
+
Date.now() / 1000 - task.active_execution_started_at,
|
|
110
|
+
);
|
|
111
|
+
executionTimes[task.active_execution_component] =
|
|
112
|
+
Number(executionTimes[task.active_execution_component] || 0) +
|
|
113
|
+
activeDuration;
|
|
114
|
+
}
|
|
115
|
+
const entries = Object.entries(executionTimes).filter(
|
|
102
116
|
([, duration]) => Number.isFinite(Number(duration)) && duration > 0,
|
|
103
117
|
);
|
|
104
118
|
entries.sort(([a], [b]) => {
|
|
@@ -894,6 +908,37 @@ describe('Lemming Web Dashboard', () => {
|
|
|
894
908
|
assert.ok(breakdown.textContent.includes('testing 20s'));
|
|
895
909
|
});
|
|
896
910
|
|
|
911
|
+
test('adds live elapsed time to the active execution segment', () => {
|
|
912
|
+
const originalDateNow = Date.now;
|
|
913
|
+
let now = 1_000_000;
|
|
914
|
+
Date.now = () => now;
|
|
915
|
+
try {
|
|
916
|
+
const task = {
|
|
917
|
+
status: 'in_progress',
|
|
918
|
+
execution_times: { runner: 30 },
|
|
919
|
+
active_execution_component: 'hook:testing',
|
|
920
|
+
active_execution_started_at: 990,
|
|
921
|
+
};
|
|
922
|
+
const initialState = createInitialState();
|
|
923
|
+
|
|
924
|
+
let segments = initialState.getExecutionSegments(task);
|
|
925
|
+
assert.deepStrictEqual(
|
|
926
|
+
segments.map(({ key, duration }) => [key, duration]),
|
|
927
|
+
[
|
|
928
|
+
['runner', 30],
|
|
929
|
+
['hook:testing', 10],
|
|
930
|
+
],
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
now += 5_000;
|
|
934
|
+
segments = initialState.getExecutionSegments(task);
|
|
935
|
+
assert.strictEqual(segments[1].duration, 15);
|
|
936
|
+
assert.ok(Math.abs(segments[1].percent - 100 / 3) < 0.001);
|
|
937
|
+
} finally {
|
|
938
|
+
Date.now = originalDateNow;
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
897
942
|
test('hides execution breakdown when timing data is unavailable', async () => {
|
|
898
943
|
const tasks = [
|
|
899
944
|
{
|
|
@@ -193,7 +193,21 @@
|
|
|
193
193
|
'bg-purple-600',
|
|
194
194
|
'bg-red-600',
|
|
195
195
|
];
|
|
196
|
-
const
|
|
196
|
+
const executionTimes = { ...(task.execution_times || {}) };
|
|
197
|
+
if (
|
|
198
|
+
task.status === 'in_progress' &&
|
|
199
|
+
task.active_execution_component &&
|
|
200
|
+
task.active_execution_started_at
|
|
201
|
+
) {
|
|
202
|
+
const activeDuration = Math.max(
|
|
203
|
+
0,
|
|
204
|
+
Date.now() / 1000 - task.active_execution_started_at,
|
|
205
|
+
);
|
|
206
|
+
executionTimes[task.active_execution_component] =
|
|
207
|
+
Number(executionTimes[task.active_execution_component] || 0) +
|
|
208
|
+
activeDuration;
|
|
209
|
+
}
|
|
210
|
+
const entries = Object.entries(executionTimes).filter(
|
|
197
211
|
([, duration]) => Number.isFinite(Number(duration)) && duration > 0,
|
|
198
212
|
);
|
|
199
213
|
entries.sort(([a], [b]) => {
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|