From 02f13397f5a24ecc5b9a4b6a7e753145fe9a6535 Mon Sep 17 00:00:00 2001 From: sdk-bot Date: Thu, 17 Sep 2026 10:49:12 -0300 Subject: [PATCH] [FAILED] sdk-sync: API 1.4.0+f19c2d4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests: The build fails as required, with compilation errors on the new `dteRange`/`dteIn` symbols that don't exist yet — confirming the tests correctly specify not-yet-implemented behavior. ## Summary Added unit tests to `src/test/java/com/marketdata/sdk/OptionsResourceTest.java` specifying the new `dte` query-parameter shapes for `GET /v1/options/chain/{underlying}/` (single number, range, comma-separated list), replacing the prior integer-only form: - URL translation tests (alongside the existing `chainExpirationFilterDteTranslatesToDteParam`): - `chainExpirationFilterDteRangeTranslatesToDashedDteParam` — `ExpirationFilter.dteRange(30, 45)` → `?dte=30-45` - `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` — `ExpirationFilter.dteIn(List.of(30, 45, 60))` → `?dte=30,45,60` - Factory validation tests (alongside the existing `expirationFilterDteRejectsNegative`): - `expirationFilterDteRangeRejectsNegativeBound`, `expirationFilterDteRangeRejectsMinGreaterThanMax` - `expirationFilterDteInRejectsEmptyList`, `expirationFilterDteInRejectsNegativeValue` These specify two new static factories, `ExpirationFilter.dteRange(int min, int max)` and `ExpirationFilter.dteIn(List days)`, following the same sealed-filter/factory-validation convention as the existing `dte`/`between`/`monthYear` and the sibling `StrikeFilter.range`. No existing tests were weakened. Confirmed `gradle test --no-daemon --console=plain` fails at `compileTestJava` since these symbols don't exist yet. implement: All within `src/main/java/` and `src/test/java/`. `gradle test` and `gradle build` both pass. ## Summary Implemented the `dte` query-parameter change for `GET /v1/options/chain/{underlying}/` (now a string accepting a single number, a range, or a comma-separated list, instead of a plain integer): - `ExpirationFilter.java`: `Dte` now wraps a formatted wire `String expression` instead of a raw `int days`. Added two new factories alongside the existing `dte(int)`: `dteRange(int min, int max)` (→ `min-max`, validates non-negative + `min <= max`) and `dteIn(List days)` (→ comma-joined, validates non-empty + all non-negative), following the same sealed-filter/factory-validation style as the sibling `StrikeFilter`. - `OptionsResource.java`: `applyExpirationFilter` now writes `v.expression()` for the `dte` query param instead of the old `v.days()`. Also fixed one test from phase 1: `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` asserted the URI with a literal comma, but the query encoder percent-encodes it (`%2C`); updated it to URL-decode before asserting, matching the existing convention used by `StocksResourceTest`'s `symbols=` assertions. No test was weakened — this only corrected an encoding-unaware expectation. `gradle test --no-daemon --console=plain` and `gradle build --no-daemon --console=plain` both pass, including Spotless formatting (applied via `spotlessApply` to reflow a javadoc comment) and JaCoCo checks. --- .sdk-sync/1.4.0-f19c2d4.md | 77 +++++++++++++++++++ .../com/marketdata/sdk/OptionsResource.java | 2 +- .../sdk/options/ExpirationFilter.java | 41 +++++++++- .../marketdata/sdk/OptionsResourceTest.java | 62 +++++++++++++++ 4 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 .sdk-sync/1.4.0-f19c2d4.md diff --git a/.sdk-sync/1.4.0-f19c2d4.md b/.sdk-sync/1.4.0-f19c2d4.md new file mode 100644 index 0000000..ea5418a --- /dev/null +++ b/.sdk-sync/1.4.0-f19c2d4.md @@ -0,0 +1,77 @@ +# sdk-auto-sync job log + +| | | +|---|---| +| API release | 1.4.0 (commit `f19c2d4`) | +| SDK | sdk-java | +| Branch | `sdk-sync/1.4.0-f19c2d4` | +| API surface | `9c18fbb1f58e` → `72b3d15f59ad` | +| Date | 2026-09-17 13:43:54Z | +| Outcome | **failed — the reviewer did not finish: exited with 1: {"duration_api_ms":79402,"stop_reason":"tool_use","session_id":"8022d31f-1598-4663-b7c4-7c3794e92bc3","total_cost_usd":0.5814339999999999,"usage":{"input_tokens":30,"cache_creation_input_tokens":74259,"cache_read_input_tokens":932425,"output_tokens":6999,"output_tokens_details":{"thinking_tokens":4447},"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":74259,"ephemeral_5m_input_tokens":0},"inference_geo":"not_available","iterations":[{"input_tokens":2,"output_tokens":296,"cache_read_input_tokens":73910,"cache_creation_input_tokens":349,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":349},"type":"message"}],"speed":"standard"},"modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":27788,"outputTokens":15,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.027863,"contextWindow":200000,"maxOutputTokens":32000,"thinkingTokens":0,"canonicalModel":"claude-haiku-4-5","provider":"firstParty","costBasis":"list"},"claude-sonnet-5":{"inputTokens":30,"outputTokens":6999,"cacheReadInputTokens":932425,"cacheCreationInputTokens":74259,"webSearchRequests":0,"costUSD":0.553571,"contextWindow":1000000,"maxOutputTokens":64000,"thinkingTokens":4447,"canonicalModel":"claude-sonnet-5","provider":"firstParty","costBasis":"list"}},"permission_denials":[],"terminal_reason":"max_turns","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subagent_stats":{"spawned":0,"requested":{"background":0,"foreground":0,"unset":0},"started_in_background":0,"max_depth":0,"spawned_by_subagents":0,"completed":0,"failed":0,"killed":{"parent":0,"user":0,"system":0},"refused":{"depth_limit":0,"concurrency_limit":0,"budget":0},"by_type":{}},"is_error":true,"num_turns":16,"subtype":"error_max_turns","errors":["Reached maximum number of turns (15)"],"type":"result","duration_ms":78783,"uuid":"f65dcfc7-ec80-41f3-ad10-b7ee2cd96800","queued_turn_count":0,"result_index":0}** | + +## Changes in the API surface + + ~ GET /v1/options/chain/{underlying}/ ?dte: string (was integer) + +## 1. tests + +Coder (24 turns, US$ 0.52): + +> The build fails as required, with compilation errors on the new `dteRange`/`dteIn` symbols that don't exist yet — confirming the tests correctly specify not-yet-implemented behavior. +> +> ## Summary +> +> Added unit tests to `src/test/java/com/marketdata/sdk/OptionsResourceTest.java` specifying the new `dte` query-parameter shapes for `GET /v1/options/chain/{underlying}/` (single number, range, comma-separated list), replacing the prior integer-only form: +> +> - **URL translation tests** (alongside the existing `chainExpirationFilterDteTranslatesToDteParam`): +> - `chainExpirationFilterDteRangeTranslatesToDashedDteParam` — `ExpirationFilter.dteRange(30, 45)` → `?dte=30-45` +> - `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` — `ExpirationFilter.dteIn(List.of(30, 45, 60))` → `?dte=30,45,60` +> - **Factory validation tests** (alongside the existing `expirationFilterDteRejectsNegative`): +> - `expirationFilterDteRangeRejectsNegativeBound`, `expirationFilterDteRangeRejectsMinGreaterThanMax` +> - `expirationFilterDteInRejectsEmptyList`, `expirationFilterDteInRejectsNegativeValue` +> +> These specify two new static factories, `ExpirationFilter.dteRange(int min, int max)` and `ExpirationFilter.dteIn(List days)`, following the same sealed-filter/factory-validation convention as the existing `dte`/`between`/`monthYear` and the sibling `StrikeFilter.range`. No existing tests were weakened. Confirmed `gradle test --no-daemon --console=plain` fails at `compileTestJava` since these symbols don't exist yet. + +Gates: + +- tests (expected to fail), attempt 1: failed — Execution failed for task ':compileTestJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin'). + +## 2. implement + +Coder (23 turns, US$ 0.46): + +> All within `src/main/java/**` and `src/test/java/**`. `gradle test` and `gradle build` both pass. +> +> ## Summary +> +> Implemented the `dte` query-parameter change for `GET /v1/options/chain/{underlying}/` (now a string accepting a single number, a range, or a comma-separated list, instead of a plain integer): +> +> - **`ExpirationFilter.java`**: `Dte` now wraps a formatted wire `String expression` instead of a raw `int days`. Added two new factories alongside the existing `dte(int)`: `dteRange(int min, int max)` (→ `min-max`, validates non-negative + `min <= max`) and `dteIn(List days)` (→ comma-joined, validates non-empty + all non-negative), following the same sealed-filter/factory-validation style as the sibling `StrikeFilter`. +> - **`OptionsResource.java`**: `applyExpirationFilter` now writes `v.expression()` for the `dte` query param instead of the old `v.days()`. +> +> Also fixed one test from phase 1: `chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam` asserted the URI with a literal comma, but the query encoder percent-encodes it (`%2C`); updated it to URL-decode before asserting, matching the existing convention used by `StocksResourceTest`'s `symbols=` assertions. No test was weakened — this only corrected an encoding-unaware expectation. +> +> `gradle test --no-daemon --console=plain` and `gradle build --no-daemon --console=plain` both pass, including Spotless formatting (applied via `spotlessApply` to reflow a javadoc comment) and JaCoCo checks. + +Gates: + +- tests, attempt 1: passed +- build, attempt 1: passed + +## 3. review 1 + +## Totals + +- Version: not assessed +- Coder runs: 2 +- Review runs: 0 +- Gates run: 3 (1 failed) +- Files changed by the agent: 3 +- Turns: 47 +- Tokens: 2,722,109 +- Estimated cost: US$ 0.98 + +### Agent runs + +- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 1,088,579 tokens (49,730 in, 1,027,400 cached, 11,449 out) · 24 turns · US$ 0.52 +- coder · claude-sonnet-5 · 1,633,530 tokens (19,282 in, 1,607,806 cached, 6,442 out) · 23 turns · US$ 0.46 diff --git a/src/main/java/com/marketdata/sdk/OptionsResource.java b/src/main/java/com/marketdata/sdk/OptionsResource.java index 81fc7ee..4c59b39 100644 --- a/src/main/java/com/marketdata/sdk/OptionsResource.java +++ b/src/main/java/com/marketdata/sdk/OptionsResource.java @@ -311,7 +311,7 @@ private static void applyExpirationFilter(RequestSpec.Builder b, ExpirationFilte if (f instanceof ExpirationFilter.OnDate v) { b.query("expiration", DateTimeFormatter.ISO_LOCAL_DATE.format(v.date())); } else if (f instanceof ExpirationFilter.Dte v) { - b.query("dte", v.days()); + b.query("dte", v.expression()); } else if (f instanceof ExpirationFilter.Between v) { b.query("from", DateTimeFormatter.ISO_LOCAL_DATE.format(v.from())); b.query("to", DateTimeFormatter.ISO_LOCAL_DATE.format(v.to())); diff --git a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java index 3b05379..87d9f7a 100644 --- a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java +++ b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java @@ -1,7 +1,9 @@ package com.marketdata.sdk.options; import java.time.LocalDate; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * Mutually-exclusive expiration filter for {@code /v1/options/chain/}. The chain endpoint's @@ -44,7 +46,37 @@ static Dte dte(int days) { if (days < 0) { throw new IllegalArgumentException("dte must be non-negative"); } - return new Dte(days); + return new Dte(Integer.toString(days)); + } + + /** + * Inclusive days-to-expiration range — wire form {@code ?dte=min-max}, e.g. {@code 30-45}. {@code + * min} must not exceed {@code max}. + */ + static Dte dteRange(int min, int max) { + if (min < 0 || max < 0) { + throw new IllegalArgumentException("dte must be non-negative"); + } + if (min > max) { + throw new IllegalArgumentException("min must be <= max"); + } + return new Dte(min + "-" + max); + } + + /** + * A discrete set of days-to-expiration values — wire form {@code ?dte=d1,d2,...}. {@code days} + * must not be empty. + */ + static Dte dteIn(List days) { + if (days.isEmpty()) { + throw new IllegalArgumentException("dte list must not be empty"); + } + for (int d : days) { + if (d < 0) { + throw new IllegalArgumentException("dte must be non-negative"); + } + } + return new Dte(days.stream().map(String::valueOf).collect(Collectors.joining(","))); } /** @@ -77,7 +109,12 @@ record OnDate(LocalDate date) implements ExpirationFilter { } } - record Dte(int days) implements ExpirationFilter {} + /** + * The {@code dte} wire expression — a single number ({@link #dte}), a dashed range ({@link + * #dteRange}), or a comma-separated list ({@link #dteIn}). Formatting is done at the factory so + * the record just carries the already-valid wire string. + */ + record Dte(String expression) implements ExpirationFilter {} record Between(LocalDate from, LocalDate to) implements ExpirationFilter {} diff --git a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java index 292ee83..7a4628a 100644 --- a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java +++ b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java @@ -16,10 +16,12 @@ import com.marketdata.sdk.options.StrikeFilter; import com.marketdata.sdk.options.StrikeRange; import java.net.URI; +import java.net.URLDecoder; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.Duration; import java.time.LocalDate; @@ -975,6 +977,38 @@ void chainExpirationFilterDteTranslatesToDteParam() { .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30"); } + @Test + void chainExpirationFilterDteRangeTranslatesToDashedDteParam() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL") + .expirationFilter(ExpirationFilter.dteRange(30, 45)) + .build()) + .join(); + + assertThat(client.captured.get(0).uri().toString()) + .isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30-45"); + } + + @Test + void chainExpirationFilterDteListTranslatesToCommaSeparatedDteParam() { + CapturingClient client = okWith(CANNED_CHAIN_BODY); + OptionsResource options = resourceWith(client); + + options + .chainAsync( + OptionsChainRequest.builder("AAPL") + .expirationFilter(ExpirationFilter.dteIn(List.of(30, 45, 60))) + .build()) + .join(); + + String url = URLDecoder.decode(client.captured.get(0).uri().toString(), StandardCharsets.UTF_8); + assertThat(url).isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30,45,60"); + } + @Test void chainExpirationFilterBetweenTranslatesToFromTo() { CapturingClient client = okWith(CANNED_CHAIN_BODY); @@ -1203,6 +1237,34 @@ void expirationFilterDteRejectsNegative() { .hasMessageContaining("dte must be non-negative"); } + @Test + void expirationFilterDteRangeRejectsNegativeBound() { + assertThatThrownBy(() -> ExpirationFilter.dteRange(-1, 45)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("dte must be non-negative"); + } + + @Test + void expirationFilterDteRangeRejectsMinGreaterThanMax() { + assertThatThrownBy(() -> ExpirationFilter.dteRange(45, 30)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("min must be <= max"); + } + + @Test + void expirationFilterDteInRejectsEmptyList() { + assertThatThrownBy(() -> ExpirationFilter.dteIn(List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("dte list must not be empty"); + } + + @Test + void expirationFilterDteInRejectsNegativeValue() { + assertThatThrownBy(() -> ExpirationFilter.dteIn(List.of(30, -1, 60))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("dte must be non-negative"); + } + @Test void expirationFilterBetweenRejectsReversedDates() { assertThatThrownBy(