Skip to content

Commit 1b5edf5

Browse files
committed
Add ultrawork logs command and acceptance test
New UltraworkStore.logs() method reads log tail with configurable max bytes New ultrawork logs <worker_id> [--bytes N] CLI subcommand New acceptance test covers start → list → show → logs → stop lifecycle Wire up handler exports and parser registration; update acceptance docs
1 parent f22824b commit 1b5edf5

7 files changed

Lines changed: 105 additions & 1 deletion

File tree

docs/acceptance.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ TeaAgent acceptance tests live under `tests/acceptance/` and verify user-facing
1111
- MCP client compatibility flow: bearer auth, session lifecycle, `tools/list`, `tools/call`, and session close.
1212
- A2A federation flow: remote discovery, partial endpoint failure, capability routing, delegation, context forwarding, and agent trace metadata.
1313
- Managed runtime flow: tool metadata context construction, workspace/request context forwarding, persisted managed task audit events, and result trace metadata.
14+
- Long-running worker flow: background worker start, list, show, log tail, and stop lifecycle.
1415

1516
## Next User Stories
1617

1718
- Live provider conformance gated by environment variables.
18-
- Long-running worker flow covering `ultrawork start`, heartbeat/status, logs, and stop.
1919
- Workspace edit flow covering hash reads, patch application, git status, test execution, and final diff summary.

teaagent/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
model_smoke,
4040
start_tui,
4141
ultrawork_list_command,
42+
ultrawork_logs_command,
4243
ultrawork_show_command,
4344
ultrawork_start_command,
4445
ultrawork_stop_command,
@@ -118,6 +119,7 @@ def build_parser() -> argparse.ArgumentParser:
118119
'graphqlite_migrate': graphqlite_migrate,
119120
'ultrawork_start': ultrawork_start_command,
120121
'ultrawork_list': ultrawork_list_command,
122+
'ultrawork_logs': ultrawork_logs_command,
121123
'ultrawork_show': ultrawork_show_command,
122124
'ultrawork_stop': ultrawork_stop_command,
123125
'workspace_tools': workspace_tools_metadata,

teaagent/cli/_handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
graphqlite_smoke,
3636
start_tui,
3737
ultrawork_list_command,
38+
ultrawork_logs_command,
3839
ultrawork_show_command,
3940
ultrawork_start_command,
4041
ultrawork_stop_command,
@@ -76,6 +77,7 @@
7677
'model_smoke',
7778
'start_tui',
7879
'ultrawork_list_command',
80+
'ultrawork_logs_command',
7981
'ultrawork_show_command',
8082
'ultrawork_start_command',
8183
'ultrawork_stop_command',

teaagent/cli/_handlers/_misc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ def ultrawork_show_command(args: argparse.Namespace) -> int:
100100
return 0
101101

102102

103+
def ultrawork_logs_command(args: argparse.Namespace) -> int:
104+
try:
105+
print_json(UltraworkStore(args.root).logs(args.worker_id, max_bytes=args.bytes))
106+
except FileNotFoundError as exc:
107+
print_json({'status': 'error', 'message': str(exc)})
108+
return 1
109+
return 0
110+
111+
103112
def ultrawork_stop_command(args: argparse.Namespace) -> int:
104113
try:
105114
print_json(UltraworkStore(args.root).stop(args.worker_id))

teaagent/cli/_misc_parsers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def register(
4040
handlers['ultrawork_start'],
4141
handlers['ultrawork_list'],
4242
handlers['ultrawork_show'],
43+
handlers['ultrawork_logs'],
4344
handlers['ultrawork_stop'],
4445
)
4546
_workspace(subparsers, handlers['workspace_tools'], handlers['workspace_openapi'])
@@ -268,6 +269,7 @@ def _ultrawork(
268269
start_handler: Callable,
269270
list_handler: Callable,
270271
show_handler: Callable,
272+
logs_handler: Callable,
271273
stop_handler: Callable,
272274
) -> None:
273275
ultrawork = subparsers.add_parser(
@@ -308,6 +310,14 @@ def _ultrawork(
308310
show.add_argument('--root', default='.', help='Workspace root.')
309311
show.set_defaults(func=show_handler)
310312

313+
logs = subs.add_parser('logs', help='Show one worker log tail.')
314+
logs.add_argument('worker_id', help='Worker id to inspect.')
315+
logs.add_argument('--root', default='.', help='Workspace root.')
316+
logs.add_argument(
317+
'--bytes', type=int, default=64_000, help='Maximum log bytes to return.'
318+
)
319+
logs.set_defaults(func=logs_handler)
320+
311321
stop = subs.add_parser('stop', help='Stop a running worker.')
312322
stop.add_argument('worker_id', help='Worker id to stop.')
313323
stop.add_argument('--root', default='.', help='Workspace root.')

teaagent/ultrawork.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,24 @@ def show(self, worker_id: str) -> dict[str, Any]:
102102
data['alive'] = self._is_alive(int(data['pid']))
103103
return data
104104

105+
def logs(self, worker_id: str, *, max_bytes: int = 64_000) -> dict[str, Any]:
106+
data = self.show(worker_id)
107+
log_path = Path(str(data['log_path']))
108+
if not log_path.exists():
109+
content = ''
110+
else:
111+
with log_path.open('rb') as fh:
112+
if max_bytes > 0:
113+
fh.seek(0, os.SEEK_END)
114+
size = fh.tell()
115+
fh.seek(max(0, size - max_bytes), os.SEEK_SET)
116+
content = fh.read().decode('utf-8', errors='replace')
117+
return {
118+
'worker_id': data['worker_id'],
119+
'log_path': data['log_path'],
120+
'content': content,
121+
}
122+
105123
def stop(self, worker_id: str, *, timeout_seconds: float = 2.0) -> dict[str, Any]:
106124
data = self.show(worker_id)
107125
pid = int(data['pid'])
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import io
4+
import json
5+
import sys
6+
import tempfile
7+
import time
8+
import unittest
9+
from contextlib import redirect_stdout
10+
11+
from teaagent import UltraworkStore
12+
from teaagent.cli import main
13+
14+
15+
class UltraworkFlowAcceptanceTests(unittest.TestCase):
16+
def test_ultrawork_start_list_show_logs_and_stop_flow(self) -> None:
17+
with tempfile.TemporaryDirectory() as tmp:
18+
store = UltraworkStore(tmp)
19+
record = store.start(
20+
[
21+
sys.executable,
22+
'-c',
23+
'import time; print("worker ready", flush=True); time.sleep(5)',
24+
],
25+
label='acceptance-worker',
26+
)
27+
time.sleep(0.2)
28+
29+
list_out = io.StringIO()
30+
with redirect_stdout(list_out):
31+
list_code = main(['ultrawork', 'list', '--root', tmp])
32+
33+
show_out = io.StringIO()
34+
with redirect_stdout(show_out):
35+
show_code = main(['ultrawork', 'show', record.worker_id, '--root', tmp])
36+
37+
logs_out = io.StringIO()
38+
with redirect_stdout(logs_out):
39+
logs_code = main(['ultrawork', 'logs', record.worker_id, '--root', tmp])
40+
41+
stop_out = io.StringIO()
42+
with redirect_stdout(stop_out):
43+
stop_code = main(['ultrawork', 'stop', record.worker_id, '--root', tmp])
44+
45+
list_payload = json.loads(list_out.getvalue())
46+
show_payload = json.loads(show_out.getvalue())
47+
logs_payload = json.loads(logs_out.getvalue())
48+
stop_payload = json.loads(stop_out.getvalue())
49+
50+
self.assertEqual(list_code, 0)
51+
self.assertEqual(show_code, 0)
52+
self.assertEqual(logs_code, 0)
53+
self.assertEqual(stop_code, 0)
54+
self.assertEqual(list_payload[0]['worker_id'], record.worker_id)
55+
self.assertEqual(show_payload['label'], 'acceptance-worker')
56+
self.assertTrue(show_payload['alive'])
57+
self.assertIn('worker ready', logs_payload['content'])
58+
self.assertFalse(stop_payload['alive'])
59+
self.assertIn(stop_payload['stop_signal'], {'SIGTERM', 'SIGKILL'})
60+
61+
62+
if __name__ == '__main__':
63+
unittest.main()

0 commit comments

Comments
 (0)