personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(sandbox): preserve profile preflight and exit contract

+63 -31
+2 -6
solstone/think/sandbox_profile/cli.py
··· 288 288 ), 289 289 json_output=json_output, 290 290 ) 291 - ctx_or_error = _marker_context(action) 292 - if isinstance(ctx_or_error, envelope.Envelope): 293 - _emit(ctx_or_error, json_output=json_output) 294 - ctx = ctx_or_error 295 291 result = envelope.Envelope( 296 292 action=action, 297 - profile=ctx.profile, 298 - run_id=ctx.run_id, 293 + profile=manifest.PROFILE, 294 + run_id=None, 299 295 state=envelope.TOP_OK, 300 296 capabilities=envelope.empty_capabilities(), 301 297 next_actions=_supported_contract_action(),
+13 -8
solstone/think/sandbox_profile/envelope.py
··· 15 15 TOP_ERROR = "error" 16 16 TOP_CLEANUP_FAILED = "cleanup_failed" 17 17 18 - EXIT_BY_STATE: dict[str, int] = { 19 - TOP_OK: 0, 20 - TOP_DEGRADED: 1, 21 - TOP_ERROR: 2, 22 - TOP_CLEANUP_FAILED: 3, 23 - } 18 + TOP_STATES = frozenset({TOP_OK, TOP_DEGRADED, TOP_ERROR, TOP_CLEANUP_FAILED}) 19 + EXIT_COMPLETE = 0 20 + EXIT_INTERNAL_FAILURE = 1 21 + EXIT_REFUSED = 2 22 + EXIT_RESIDUAL = 3 24 23 25 24 CAP_NOT_APPLIED = "not_applied" 26 25 CAP_READY = "ready" ··· 144 143 contract_version: int = field(default=manifest.CONTRACT_VERSION) 145 144 146 145 def to_json(self) -> dict[str, object]: 147 - if self.state not in EXIT_BY_STATE: 146 + if self.state not in TOP_STATES: 148 147 raise ValueError(f"unsupported top-level state: {self.state!r}") 149 148 return { 150 149 "contract_version": self.contract_version, ··· 159 158 160 159 @property 161 160 def exit_code(self) -> int: 162 - return EXIT_BY_STATE[self.state] 161 + if self.state == TOP_OK: 162 + return EXIT_COMPLETE 163 + if self.state == TOP_ERROR: 164 + if self.error is not None and self.error.code == "internal_error": 165 + return EXIT_INTERNAL_FAILURE 166 + return EXIT_REFUSED 167 + return EXIT_RESIDUAL 163 168 164 169 165 170 def empty_capabilities() -> tuple[CapabilityEnvelope, ...]:
+4 -4
tests/sandbox_profile/test_apply.py
··· 86 86 cap for cap in status_body["capabilities"] if cap["name"] == capability 87 87 ) 88 88 89 - assert result.exit_code == 2 89 + assert result.exit_code == 1 90 90 if fail_on_replace == 1: 91 91 assert cap_status["state"] == "not_applied" 92 92 else: 93 - assert status.exit_code == 1 93 + assert status.exit_code == 3 94 94 assert cap_status["state"] == "degraded" 95 95 assert residual in cap_status["residuals"] 96 96 ··· 131 131 status_body = output_json(status) 132 132 spb = next(cap for cap in status_body["capabilities"] if cap["name"] == "spb") 133 133 134 - assert failed.exit_code == 2 135 - assert status.exit_code == 1 134 + assert failed.exit_code == 1 135 + assert status.exit_code == 3 136 136 assert spb["state"] == "degraded" 137 137 assert residual in spb["residuals"] 138 138
+14
tests/sandbox_profile/test_cli_registration.py
··· 47 47 48 48 assert result.exit_code == 2 49 49 assert body["error"]["code"] == "unknown_capability" 50 + 51 + 52 + def test_describe_is_marker_free_preflight(tmp_path, monkeypatch) -> None: 53 + journal = tmp_path / "unmarked" 54 + journal.mkdir() 55 + monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal)) 56 + 57 + result = invoke(["describe", "--json"]) 58 + body = output_json(result) 59 + 60 + assert result.exit_code == 0 61 + assert body["run_id"] is None 62 + assert body["state"] == "ok" 63 + assert not (journal / ".solstone-sandbox.json").exists()
+27 -10
tests/sandbox_profile/test_envelope.py
··· 33 33 assert raw["contract_version"] == 1 34 34 assert raw["action"] == "describe" 35 35 assert raw["profile"] == "full" 36 - assert isinstance(raw["run_id"], str) 36 + assert raw["run_id"] is None 37 37 assert [cap["name"] for cap in raw["capabilities"]] == [ 38 38 "scout", 39 39 "spl", ··· 45 45 assert not (journal / "config" / "journal.json").exists() 46 46 47 47 48 - def test_state_exit_mapping_is_bijective() -> None: 49 - assert envelope.EXIT_BY_STATE == { 50 - "ok": 0, 51 - "degraded": 1, 52 - "error": 2, 53 - "cleanup_failed": 3, 54 - } 55 - assert len(set(envelope.EXIT_BY_STATE.values())) == len(envelope.EXIT_BY_STATE) 48 + def test_exit_mapping_distinguishes_internal_failure_from_known_outcomes() -> None: 49 + caps = envelope.empty_capabilities() 50 + assert envelope.Envelope("status", "full", None, "ok", caps).exit_code == 0 51 + assert envelope.Envelope("status", "full", None, "degraded", caps).exit_code == 3 52 + assert ( 53 + envelope.Envelope("status", "full", None, "cleanup_failed", caps).exit_code == 3 54 + ) 55 + assert ( 56 + envelope.error_envelope( 57 + action="status", 58 + code="payload_invalid", 59 + message="invalid", 60 + run_id=None, 61 + ).exit_code 62 + == 2 63 + ) 64 + assert ( 65 + envelope.error_envelope( 66 + action="status", 67 + code="internal_error", 68 + message="failed", 69 + run_id=None, 70 + ).exit_code 71 + == 1 72 + ) 56 73 57 74 58 75 def test_capability_serializer_enforces_closed_vocabulary() -> None: ··· 83 100 cleanup = invoke(["disable", "spb", "--json"]) 84 101 85 102 assert ok.exit_code == 0 86 - assert degraded.exit_code == 1 103 + assert degraded.exit_code == 3 87 104 assert error.exit_code == 2 88 105 assert cleanup.exit_code == 3 89 106 assert output_json(cleanup)["state"] == "cleanup_failed"
+1 -1
tests/sandbox_profile/test_redaction.py
··· 36 36 ) 37 37 body = output_json(result) 38 38 39 - assert result.exit_code == 2 39 + assert result.exit_code == 1 40 40 assert body["error"]["code"] == "internal_error" 41 41 assert secret not in result.output 42 42 assert secret not in caplog.text
+2 -2
tests/sandbox_profile/test_status.py
··· 49 49 body = output_json(status) 50 50 scout = next(cap for cap in body["capabilities"] if cap["name"] == "scout") 51 51 52 - assert status.exit_code == 1 52 + assert status.exit_code == 3 53 53 assert body["state"] == "degraded" 54 54 assert scout["state"] == "degraded" 55 55 assert "scout_block_missing" in scout["residuals"] ··· 82 82 def _assert_degraded_residual(result, name: str, residual: str) -> None: 83 83 body = output_json(result) 84 84 cap = _capability(body, name) 85 - assert result.exit_code == 1 85 + assert result.exit_code == 3 86 86 assert body["state"] == "degraded" 87 87 assert cap["state"] == "degraded" 88 88 assert residual in cap["residuals"]