mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-25 13:44:06 +08:00
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:
parent
ded0c5bbc1
commit
290ab7e41f
69
src/main.py
69
src/main.py
@ -25,6 +25,20 @@ from .tool_pool import assemble_tool_pool
|
|||||||
from .tools import execute_tool, get_tool, get_tools, render_tool_index
|
from .tools import execute_tool, get_tool, get_tools, render_tool_index
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_json_envelope(data: dict, command: str, exit_code: int = 0) -> dict:
|
||||||
|
"""Wrap command output in canonical JSON envelope per SCHEMAS.md."""
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
now_utc = datetime.now(timezone.utc).isoformat(timespec='seconds').replace('+00:00', 'Z')
|
||||||
|
return {
|
||||||
|
'timestamp': now_utc,
|
||||||
|
'command': command,
|
||||||
|
'exit_code': exit_code,
|
||||||
|
'output_format': 'json',
|
||||||
|
'schema_version': '1.0',
|
||||||
|
**data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description='Python porting workspace for the Claude Code rewrite effort')
|
parser = argparse.ArgumentParser(description='Python porting workspace for the Claude Code rewrite effort')
|
||||||
subparsers = parser.add_subparsers(dest='command', required=True)
|
subparsers = parser.add_subparsers(dest='command', required=True)
|
||||||
@ -212,7 +226,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'plugin_like': [{'name': m.name, 'source_hint': m.source_hint} for m in graph.plugin_like],
|
'plugin_like': [{'name': m.name, 'source_hint': m.source_hint} for m in graph.plugin_like],
|
||||||
'skill_like': [{'name': m.name, 'source_hint': m.source_hint} for m in graph.skill_like],
|
'skill_like': [{'name': m.name, 'source_hint': m.source_hint} for m in graph.skill_like],
|
||||||
}
|
}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
else:
|
else:
|
||||||
print(graph.as_markdown())
|
print(graph.as_markdown())
|
||||||
return 0
|
return 0
|
||||||
@ -226,7 +240,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'tool_count': len(pool.tools),
|
'tool_count': len(pool.tools),
|
||||||
'tools': [{'name': t.name, 'source_hint': t.source_hint} for t in pool.tools],
|
'tools': [{'name': t.name, 'source_hint': t.source_hint} for t in pool.tools],
|
||||||
}
|
}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
else:
|
else:
|
||||||
print(pool.as_markdown())
|
print(pool.as_markdown())
|
||||||
return 0
|
return 0
|
||||||
@ -235,7 +249,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json
|
import json
|
||||||
envelope = {'stages': graph.as_markdown().split('\n'), 'note': 'bootstrap-graph is markdown-only in this version'}
|
envelope = {'stages': graph.as_markdown().split('\n'), 'note': 'bootstrap-graph is markdown-only in this version'}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
else:
|
else:
|
||||||
print(graph.as_markdown())
|
print(graph.as_markdown())
|
||||||
return 0
|
return 0
|
||||||
@ -281,7 +295,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
for m in matches
|
for m in matches
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
return 0
|
return 0
|
||||||
if not matches:
|
if not matches:
|
||||||
print('No mirrored command/tool matches found.')
|
print('No mirrored command/tool matches found.')
|
||||||
@ -321,7 +335,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
},
|
},
|
||||||
'persisted_session_path': session.persisted_session_path,
|
'persisted_session_path': session.persisted_session_path,
|
||||||
}
|
}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
return 0
|
return 0
|
||||||
print(session.as_markdown())
|
print(session.as_markdown())
|
||||||
return 0
|
return 0
|
||||||
@ -355,14 +369,15 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
path = engine.persist_session(directory)
|
path = engine.persist_session(directory)
|
||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
print(_json.dumps({
|
_env = {
|
||||||
'session_id': engine.session_id,
|
'session_id': engine.session_id,
|
||||||
'path': path,
|
'path': path,
|
||||||
'flushed': engine.transcript_store.flushed,
|
'flushed': engine.transcript_store.flushed,
|
||||||
'messages_count': len(engine.mutable_messages),
|
'messages_count': len(engine.mutable_messages),
|
||||||
'input_tokens': engine.total_usage.input_tokens,
|
'input_tokens': engine.total_usage.input_tokens,
|
||||||
'output_tokens': engine.total_usage.output_tokens,
|
'output_tokens': engine.total_usage.output_tokens,
|
||||||
}))
|
}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command)))
|
||||||
else:
|
else:
|
||||||
# #166: legacy text output preserved byte-for-byte for backward compat.
|
# #166: legacy text output preserved byte-for-byte for backward compat.
|
||||||
print(path)
|
print(path)
|
||||||
@ -379,7 +394,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
resolved_dir = str(directory) if directory else '.port_sessions'
|
resolved_dir = str(directory) if directory else '.port_sessions'
|
||||||
print(_json.dumps({
|
_env = {
|
||||||
'session_id': args.session_id,
|
'session_id': args.session_id,
|
||||||
'loaded': False,
|
'loaded': False,
|
||||||
'error': {
|
'error': {
|
||||||
@ -388,7 +403,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'directory': resolved_dir,
|
'directory': resolved_dir,
|
||||||
'retryable': False,
|
'retryable': False,
|
||||||
},
|
},
|
||||||
}))
|
}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command, exit_code=1)))
|
||||||
else:
|
else:
|
||||||
print(f'error: {exc}')
|
print(f'error: {exc}')
|
||||||
return 1
|
return 1
|
||||||
@ -398,7 +414,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
resolved_dir = str(directory) if directory else '.port_sessions'
|
resolved_dir = str(directory) if directory else '.port_sessions'
|
||||||
print(_json.dumps({
|
_env = {
|
||||||
'session_id': args.session_id,
|
'session_id': args.session_id,
|
||||||
'loaded': False,
|
'loaded': False,
|
||||||
'error': {
|
'error': {
|
||||||
@ -407,19 +423,21 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'directory': resolved_dir,
|
'directory': resolved_dir,
|
||||||
'retryable': True,
|
'retryable': True,
|
||||||
},
|
},
|
||||||
}))
|
}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command, exit_code=1)))
|
||||||
else:
|
else:
|
||||||
print(f'error: {exc}')
|
print(f'error: {exc}')
|
||||||
return 1
|
return 1
|
||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
print(_json.dumps({
|
_env = {
|
||||||
'session_id': session.session_id,
|
'session_id': session.session_id,
|
||||||
'loaded': True,
|
'loaded': True,
|
||||||
'messages_count': len(session.messages),
|
'messages_count': len(session.messages),
|
||||||
'input_tokens': session.input_tokens,
|
'input_tokens': session.input_tokens,
|
||||||
'output_tokens': session.output_tokens,
|
'output_tokens': session.output_tokens,
|
||||||
}))
|
}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command)))
|
||||||
else:
|
else:
|
||||||
print(f'{session.session_id}\n{len(session.messages)} messages\nin={session.input_tokens} out={session.output_tokens}')
|
print(f'{session.session_id}\n{len(session.messages)} messages\nin={session.input_tokens} out={session.output_tokens}')
|
||||||
return 0
|
return 0
|
||||||
@ -429,7 +447,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
ids = list_sessions(directory)
|
ids = list_sessions(directory)
|
||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
print(_json.dumps({'sessions': ids, 'count': len(ids)}))
|
_env = {'sessions': ids, 'count': len(ids)}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command)))
|
||||||
else:
|
else:
|
||||||
if not ids:
|
if not ids:
|
||||||
print('(no sessions)')
|
print('(no sessions)')
|
||||||
@ -445,7 +464,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
except SessionDeleteError as exc:
|
except SessionDeleteError as exc:
|
||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
print(_json.dumps({
|
_env = {
|
||||||
'session_id': args.session_id,
|
'session_id': args.session_id,
|
||||||
'deleted': False,
|
'deleted': False,
|
||||||
'error': {
|
'error': {
|
||||||
@ -453,17 +472,19 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'message': str(exc),
|
'message': str(exc),
|
||||||
'retryable': True,
|
'retryable': True,
|
||||||
},
|
},
|
||||||
}))
|
}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command, exit_code=1)))
|
||||||
else:
|
else:
|
||||||
print(f'error: {exc}')
|
print(f'error: {exc}')
|
||||||
return 1
|
return 1
|
||||||
if args.output_format == 'json':
|
if args.output_format == 'json':
|
||||||
import json as _json
|
import json as _json
|
||||||
print(_json.dumps({
|
_env = {
|
||||||
'session_id': args.session_id,
|
'session_id': args.session_id,
|
||||||
'deleted': deleted,
|
'deleted': deleted,
|
||||||
'status': 'deleted' if deleted else 'not_found',
|
'status': 'deleted' if deleted else 'not_found',
|
||||||
}))
|
}
|
||||||
|
print(_json.dumps(wrap_json_envelope(_env, args.command)))
|
||||||
else:
|
else:
|
||||||
if deleted:
|
if deleted:
|
||||||
print(f'deleted: {args.session_id}')
|
print(f'deleted: {args.session_id}')
|
||||||
@ -501,7 +522,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'retryable': False,
|
'retryable': False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
print(json.dumps(error_envelope))
|
print(json.dumps(wrap_json_envelope(error_envelope, args.command, exit_code=1)))
|
||||||
else:
|
else:
|
||||||
print(f'Command not found: {args.name}')
|
print(f'Command not found: {args.name}')
|
||||||
return 1
|
return 1
|
||||||
@ -513,7 +534,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'source_hint': module.source_hint,
|
'source_hint': module.source_hint,
|
||||||
'responsibility': module.responsibility,
|
'responsibility': module.responsibility,
|
||||||
}
|
}
|
||||||
print(json.dumps(output))
|
print(json.dumps(wrap_json_envelope(output, args.command)))
|
||||||
else:
|
else:
|
||||||
print('\n'.join([module.name, module.source_hint, module.responsibility]))
|
print('\n'.join([module.name, module.source_hint, module.responsibility]))
|
||||||
return 0
|
return 0
|
||||||
@ -531,7 +552,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'retryable': False,
|
'retryable': False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
print(json.dumps(error_envelope))
|
print(json.dumps(wrap_json_envelope(error_envelope, args.command, exit_code=1)))
|
||||||
else:
|
else:
|
||||||
print(f'Tool not found: {args.name}')
|
print(f'Tool not found: {args.name}')
|
||||||
return 1
|
return 1
|
||||||
@ -543,7 +564,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'source_hint': module.source_hint,
|
'source_hint': module.source_hint,
|
||||||
'responsibility': module.responsibility,
|
'responsibility': module.responsibility,
|
||||||
}
|
}
|
||||||
print(json.dumps(output))
|
print(json.dumps(wrap_json_envelope(output, args.command)))
|
||||||
else:
|
else:
|
||||||
print('\n'.join([module.name, module.source_hint, module.responsibility]))
|
print('\n'.join([module.name, module.source_hint, module.responsibility]))
|
||||||
return 0
|
return 0
|
||||||
@ -571,7 +592,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'handled': True,
|
'handled': True,
|
||||||
'message': result.message,
|
'message': result.message,
|
||||||
}
|
}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
else:
|
else:
|
||||||
print(result.message)
|
print(result.message)
|
||||||
return 0 if result.handled else 1
|
return 0 if result.handled else 1
|
||||||
@ -599,7 +620,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
'handled': True,
|
'handled': True,
|
||||||
'message': result.message,
|
'message': result.message,
|
||||||
}
|
}
|
||||||
print(json.dumps(envelope))
|
print(json.dumps(wrap_json_envelope(envelope, args.command)))
|
||||||
else:
|
else:
|
||||||
print(result.message)
|
print(result.message)
|
||||||
return 0 if result.handled else 1
|
return 0 if result.handled else 1
|
||||||
|
|||||||
@ -169,7 +169,6 @@ class TestJsonEnvelopeCommonFieldPrep:
|
|||||||
13 clawable commands. Currently they document the expected contract.
|
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:
|
def test_all_envelopes_include_timestamp(self) -> None:
|
||||||
"""Every clawable envelope must include ISO 8601 UTC timestamp."""
|
"""Every clawable envelope must include ISO 8601 UTC timestamp."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@ -183,12 +182,16 @@ class TestJsonEnvelopeCommonFieldPrep:
|
|||||||
# Verify ISO 8601 format (ends with Z for UTC)
|
# Verify ISO 8601 format (ends with Z for UTC)
|
||||||
assert envelope['timestamp'].endswith('Z'), f'Timestamp not UTC: {envelope["timestamp"]}'
|
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:
|
def test_all_envelopes_include_command(self) -> None:
|
||||||
"""Every envelope must echo the command name."""
|
"""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(
|
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,
|
cwd=Path(__file__).resolve().parent.parent,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -196,7 +199,6 @@ class TestJsonEnvelopeCommonFieldPrep:
|
|||||||
envelope = json.loads(result.stdout)
|
envelope = json.loads(result.stdout)
|
||||||
assert envelope.get('command') == cmd_name, f'{cmd_name} envelope.command mismatch'
|
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:
|
def test_all_envelopes_include_exit_code_and_schema_version(self) -> None:
|
||||||
"""Every envelope must include exit_code and schema_version."""
|
"""Every envelope must include exit_code and schema_version."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
@ -92,13 +92,17 @@ class TestOutputFormatFlagParity:
|
|||||||
)
|
)
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
assert data == {
|
# Verify common envelope fields (SCHEMAS.md contract)
|
||||||
'session_id': 'gamma',
|
assert 'timestamp' in data
|
||||||
'loaded': True,
|
assert data['command'] == 'load-session'
|
||||||
'messages_count': 2,
|
assert data['exit_code'] == 0
|
||||||
'input_tokens': 5,
|
assert data['schema_version'] == '1.0'
|
||||||
'output_tokens': 7,
|
# 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:
|
def test_text_mode_unchanged_on_success(self, tmp_path: Path) -> None:
|
||||||
"""Legacy text output must be byte-identical for backward compat."""
|
"""Legacy text output must be byte-identical for backward compat."""
|
||||||
|
|||||||
@ -200,7 +200,13 @@ class PortingWorkspaceTests(unittest.TestCase):
|
|||||||
check=True, capture_output=True, text=True,
|
check=True, capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
data = json.loads(json_result.stdout)
|
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:
|
def test_delete_session_cli_idempotent(self) -> None:
|
||||||
"""#160: delete-session CLI is idempotent (not-found is exit 0, status=not_found)."""
|
"""#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,
|
capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
self.assertEqual(first.returncode, 0)
|
self.assertEqual(first.returncode, 0)
|
||||||
self.assertEqual(
|
envelope_first = json.loads(first.stdout)
|
||||||
json.loads(first.stdout),
|
# Verify common envelope fields (SCHEMAS.md contract)
|
||||||
{'session_id': 'once', 'deleted': True, 'status': 'deleted'},
|
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 delete: idempotent, still exit 0
|
||||||
second = subprocess.run(
|
second = subprocess.run(
|
||||||
[sys.executable, '-m', 'src.main', 'delete-session', 'once',
|
[sys.executable, '-m', 'src.main', 'delete-session', 'once',
|
||||||
@ -232,10 +244,10 @@ class PortingWorkspaceTests(unittest.TestCase):
|
|||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
self.assertEqual(second.returncode, 0)
|
self.assertEqual(second.returncode, 0)
|
||||||
self.assertEqual(
|
envelope_second = json.loads(second.stdout)
|
||||||
json.loads(second.stdout),
|
self.assertEqual(envelope_second['session_id'], 'once')
|
||||||
{'session_id': 'once', 'deleted': False, 'status': 'not_found'},
|
self.assertEqual(envelope_second['deleted'], False)
|
||||||
)
|
self.assertEqual(envelope_second['status'], 'not_found')
|
||||||
|
|
||||||
def test_delete_session_cli_partial_failure_exit_1(self) -> None:
|
def test_delete_session_cli_partial_failure_exit_1(self) -> None:
|
||||||
"""#160: partial-failure (permission error) surfaces as exit 1 + typed JSON error."""
|
"""#160: partial-failure (permission error) surfaces as exit 1 + typed JSON error."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user