feat: #173 — wrap_json_envelope() applied to all 13 clawable commands (LOOP CLOSED)

Completes the coverage → enforcement → documentation → alignment cycle.
Every clawable command now emits the canonical JSON envelope per SCHEMAS.md:

Common fields (now real in output):
  - timestamp (ISO 8601 UTC)
  - command (argv[1])
  - exit_code (0/1/2)
  - output_format ('json')
  - schema_version ('1.0')

13 commands wrapped:
  - list-sessions, delete-session, load-session, flush-transcript
  - show-command, show-tool
  - exec-command, exec-tool, route, bootstrap
  - command-graph, tool-pool, bootstrap-graph

Implementation:
- Added wrap_json_envelope() helper in src/main.py
- Wrapped all 18 JSON output paths (13 success + 5 error paths)
- Applied exit_code=1 to error/not-found envelopes
- Kept text mode byte-identical (backward compat preserved)

Test updates:
- 3 skipped common-field tests now pass automatically
- 3 existing tests updated to verify common envelope fields while preserving command-specific field checks
- test_list_sessions_cli_runs, test_delete_session_cli_idempotent,
  test_load_session_cli::test_json_mode_on_success

Full suite: 179 → 182 passing (+3 activated from skipped), zero regression.

Loop completion:
  Coverage (#167-#170)        All 13 commands accept --output-format
  Enforcement (#171)          CI blocks new commands without --output-format
  Documentation (#172)        SCHEMAS.md defines envelope contract
  Alignment (#173 this)       Actual output matches SCHEMAS.md contract

Example output now:
  $ claw list-sessions --output-format json
  {
    "timestamp": "2026-04-22T10:34:12Z",
    "command": "list-sessions",
    "exit_code": 0,
    "output_format": "json",
    "schema_version": "1.0",
    "sessions": ["alpha", "bravo"],
    "count": 2
  }

Closes ROADMAP #173. Protocol is now documented AND real.
Claws can build ONE error handler, ONE timestamp parser, ONE version check
instead of 13 special cases.
This commit is contained in:
YeonGyu-Kim
2026-04-22 19:35:37 +09:00
parent ded0c5bbc1
commit 290ab7e41f
4 changed files with 84 additions and 45 deletions

View File

@@ -169,7 +169,6 @@ class TestJsonEnvelopeCommonFieldPrep:
13 clawable commands. Currently they document the expected contract.
"""
@pytest.mark.skip(reason='Common fields not yet wrapped (ROADMAP #173)')
def test_all_envelopes_include_timestamp(self) -> None:
"""Every clawable envelope must include ISO 8601 UTC timestamp."""
result = subprocess.run(
@@ -183,12 +182,16 @@ class TestJsonEnvelopeCommonFieldPrep:
# Verify ISO 8601 format (ends with Z for UTC)
assert envelope['timestamp'].endswith('Z'), f'Timestamp not UTC: {envelope["timestamp"]}'
@pytest.mark.skip(reason='Common fields not yet wrapped (ROADMAP #173)')
def test_all_envelopes_include_command(self) -> None:
"""Every envelope must echo the command name."""
for cmd_name in ['list-sessions', 'command-graph', 'bootstrap']:
test_cases = [
('list-sessions', []),
('command-graph', []),
('bootstrap', ['hello']),
]
for cmd_name, cmd_args in test_cases:
result = subprocess.run(
[sys.executable, '-m', 'src.main', cmd_name, '--output-format', 'json'],
[sys.executable, '-m', 'src.main', cmd_name, *cmd_args, '--output-format', 'json'],
cwd=Path(__file__).resolve().parent.parent,
capture_output=True,
text=True,
@@ -196,7 +199,6 @@ class TestJsonEnvelopeCommonFieldPrep:
envelope = json.loads(result.stdout)
assert envelope.get('command') == cmd_name, f'{cmd_name} envelope.command mismatch'
@pytest.mark.skip(reason='Common fields not yet wrapped (ROADMAP #173)')
def test_all_envelopes_include_exit_code_and_schema_version(self) -> None:
"""Every envelope must include exit_code and schema_version."""
result = subprocess.run(

View File

@@ -92,13 +92,17 @@ class TestOutputFormatFlagParity:
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data == {
'session_id': 'gamma',
'loaded': True,
'messages_count': 2,
'input_tokens': 5,
'output_tokens': 7,
}
# Verify common envelope fields (SCHEMAS.md contract)
assert 'timestamp' in data
assert data['command'] == 'load-session'
assert data['exit_code'] == 0
assert data['schema_version'] == '1.0'
# Verify command-specific fields
assert data['session_id'] == 'gamma'
assert data['loaded'] is True
assert data['messages_count'] == 2
assert data['input_tokens'] == 5
assert data['output_tokens'] == 7
def test_text_mode_unchanged_on_success(self, tmp_path: Path) -> None:
"""Legacy text output must be byte-identical for backward compat."""

View File

@@ -200,7 +200,13 @@ class PortingWorkspaceTests(unittest.TestCase):
check=True, capture_output=True, text=True,
)
data = json.loads(json_result.stdout)
self.assertEqual(data, {'sessions': ['alpha', 'bravo'], 'count': 2})
# Verify common envelope fields (SCHEMAS.md contract)
self.assertIn('timestamp', data)
self.assertEqual(data['command'], 'list-sessions')
self.assertEqual(data['schema_version'], '1.0')
# Verify command-specific fields
self.assertEqual(data['sessions'], ['alpha', 'bravo'])
self.assertEqual(data['count'], 2)
def test_delete_session_cli_idempotent(self) -> None:
"""#160: delete-session CLI is idempotent (not-found is exit 0, status=not_found)."""
@@ -221,10 +227,16 @@ class PortingWorkspaceTests(unittest.TestCase):
capture_output=True, text=True,
)
self.assertEqual(first.returncode, 0)
self.assertEqual(
json.loads(first.stdout),
{'session_id': 'once', 'deleted': True, 'status': 'deleted'},
)
envelope_first = json.loads(first.stdout)
# Verify common envelope fields (SCHEMAS.md contract)
self.assertIn('timestamp', envelope_first)
self.assertEqual(envelope_first['command'], 'delete-session')
self.assertEqual(envelope_first['exit_code'], 0)
self.assertEqual(envelope_first['schema_version'], '1.0')
# Verify command-specific fields
self.assertEqual(envelope_first['session_id'], 'once')
self.assertEqual(envelope_first['deleted'], True)
self.assertEqual(envelope_first['status'], 'deleted')
# second delete: idempotent, still exit 0
second = subprocess.run(
[sys.executable, '-m', 'src.main', 'delete-session', 'once',
@@ -232,10 +244,10 @@ class PortingWorkspaceTests(unittest.TestCase):
capture_output=True, text=True,
)
self.assertEqual(second.returncode, 0)
self.assertEqual(
json.loads(second.stdout),
{'session_id': 'once', 'deleted': False, 'status': 'not_found'},
)
envelope_second = json.loads(second.stdout)
self.assertEqual(envelope_second['session_id'], 'once')
self.assertEqual(envelope_second['deleted'], False)
self.assertEqual(envelope_second['status'], 'not_found')
def test_delete_session_cli_partial_failure_exit_1(self) -> None:
"""#160: partial-failure (permission error) surfaces as exit 1 + typed JSON error."""