Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ cycles-protocol-service/cycles-protocol-service-model/.project

# Claude Code local state (not for commit)
.claude/scheduled_tasks.lock
.codex-worktrees/
219 changes: 182 additions & 37 deletions AUDIT.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,37 @@ void shouldReturnReleasedAmountWhenActualLessThanReserved() {
assertThat(resp.getBody().get("released")).isNotNull();
}

@Test
void shouldSurfaceCommittedMetadataOnGetReservation() {
// End-to-end round-trip (real Redis + commit.lua, not mocked): a commit
// carrying metadata persists committed_metadata_json, and getReservation
// returns it as committed_metadata (cycles-server#197).
String reservationId = createReservationAndGetId(TENANT_A, API_KEY_SECRET_A, 1000);
Map<String, Object> body = commitBody(800);
body.put("metadata", Map.of("request_id", "req-abc-123"));
post("/v1/reservations/" + reservationId + "/commit", API_KEY_SECRET_A, body);

ResponseEntity<Map> resp = get("/v1/reservations/" + reservationId, API_KEY_SECRET_A);

assertThat(resp.getStatusCode().value()).isEqualTo(200);
@SuppressWarnings("unchecked")
Map<String, Object> committedMetadata = (Map<String, Object>) resp.getBody().get("committed_metadata");
assertThat(committedMetadata).isNotNull().containsEntry("request_id", "req-abc-123");
}

@Test
void shouldOmitCommittedMetadataWhenCommitHadNone() {
// NON_NULL wire omission: a commit without metadata leaves the field
// absent from the JSON entirely, not present-as-null.
String reservationId = createReservationAndGetId(TENANT_A, API_KEY_SECRET_A, 1000);
post("/v1/reservations/" + reservationId + "/commit", API_KEY_SECRET_A, commitBody(800));

ResponseEntity<Map> resp = get("/v1/reservations/" + reservationId, API_KEY_SECRET_A);

assertThat(resp.getStatusCode().value()).isEqualTo(200);
assertThat(resp.getBody().containsKey("committed_metadata")).isFalse();
}

@Test
void shouldRejectCommitWithUnitMismatch() {
String reservationId = createReservationAndGetId(TENANT_A, API_KEY_SECRET_A, 1000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1915,7 +1915,16 @@ private ReservationDetail buildReservationSummary(Map<String, String> fields) th
metadata = objectMapper.readValue(metadataJson, Map.class);
}

ReservationDetail detail = new ReservationDetail(committed, finalizedAtMs, metadata);
// Parse commit-time metadata if present. commit.lua persists the COMMIT
// request's metadata as committed_metadata_json; surface it as
// committed_metadata so it is readable, not write-only (cycles-server#197).
Map<String, Object> committedMetadata = null;
String committedMetadataJson = fields.get("committed_metadata_json");
if (committedMetadataJson != null && !committedMetadataJson.isEmpty()) {
committedMetadata = objectMapper.readValue(committedMetadataJson, Map.class);
}

ReservationDetail detail = new ReservationDetail(committed, finalizedAtMs, metadata, committedMetadata);
detail.setReservationId(fields.get("reservation_id"));
detail.setStatus(Enums.ReservationStatus.valueOf(stateStr));
detail.setIdempotencyKey(fields.get("idempotency_key"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,38 @@ void shouldParseMetadata() {
assertThat(detail.getMetadata()).containsEntry("model", "gpt-4");
}

@Test
void shouldParseCommittedMetadata() {
// commit.lua persists the COMMIT request's metadata as
// committed_metadata_json; getReservation must surface it as
// committed_metadata (cycles-server#197), distinct from reserve metadata.
when(jedisPool.getResource()).thenReturn(jedis);
doNothing().when(jedis).close();
Map<String, String> fields = reservationFields("res-cmeta", "COMMITTED");
fields.put("metadata_json", "{\"phase\":\"reserve\"}");
fields.put("committed_metadata_json", "{\"request_id\":\"req-abc-123\"}");
when(jedis.hgetAll("reservation:res_res-cmeta")).thenReturn(fields);

ReservationDetail detail = repository.getReservationById("res-cmeta");

assertThat(detail.getCommittedMetadata()).isNotNull();
assertThat(detail.getCommittedMetadata()).containsEntry("request_id", "req-abc-123");
// reserve-time metadata stays distinct
assertThat(detail.getMetadata()).containsEntry("phase", "reserve");
}

@Test
void shouldOmitCommittedMetadataWhenAbsent() {
when(jedisPool.getResource()).thenReturn(jedis);
doNothing().when(jedis).close();
Map<String, String> fields = reservationFields("res-nocmeta", "ACTIVE");
when(jedis.hgetAll("reservation:res_res-nocmeta")).thenReturn(fields);

ReservationDetail detail = repository.getReservationById("res-nocmeta");

assertThat(detail.getCommittedMetadata()).isNull();
}

@Test
void shouldThrowOnCorruptedData() {
when(jedisPool.getResource()).thenReturn(jedis);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public class ReservationDetail extends ReservationSummary {
@Valid @JsonProperty("committed") private Amount committed;
@JsonProperty("finalized_at_ms") private Long finalizedAtMs;
@JsonProperty("metadata") private Map<String, Object> metadata;
@JsonProperty("committed_metadata") private Map<String, Object> committedMetadata;
}
2 changes: 1 addition & 1 deletion cycles-protocol-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<module>cycles-protocol-service-api</module>
</modules>
<properties>
<revision>0.1.25.33</revision>
<revision>0.1.25.34</revision>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
Expand Down