Skip to content

Commit 37baa2f

Browse files
zimegClaude
andcommitted
test: probe ListRecord.Field message wire shape against live API
Adds an experimental remote-API test (messageFieldArrayShapeProbe) to slacklists_Test that creates a list with a message-type column, sets the field with a real message permalink, then reads the item back and observes the actual response shape of the `message` field. It captures the raw items.info JSON to assert whether `message` is a JSON array or object, and separately probes whether the typed (production Gson) deserialization throws on that real shape. Additive, test-only; no SDK source changes. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
1 parent 2bc6aff commit 37baa2f

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

‎slack-api-client/src/test/java/test_with_remote_apis/methods/slacklists_Test.java‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package test_with_remote_apis.methods;
22

33
import com.slack.api.Slack;
4+
import com.slack.api.methods.MethodsClient;
45
import com.slack.api.methods.SlackApiException;
56
import com.slack.api.methods.response.auth.AuthTestResponse;
7+
import com.slack.api.methods.response.chat.ChatGetPermalinkResponse;
8+
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
9+
import com.slack.api.util.http.SlackHttpClient;
10+
import okhttp3.FormBody;
11+
import okhttp3.Response;
612
import com.slack.api.methods.response.slack_lists.SlackListsAccessDeleteResponse;
713
import com.slack.api.methods.response.slack_lists.SlackListsAccessSetResponse;
814
import com.slack.api.methods.response.slack_lists.SlackListsCreateResponse;
@@ -388,4 +394,156 @@ public void itemsDeleteMultiple() throws IOException, SlackApiException {
388394
assertThat(deleteMultipleResponse.isOk(), is(true));
389395
}
390396

397+
// ------------------------------------------------------------------------
398+
// EXPERIMENTAL — proof probe for the ListRecord.Field.message wire shape.
399+
//
400+
// Context: slackapi/java-slack-sdk#1587 reports that when a list item
401+
// carries a "message"-type field, the API can return `message` as an ARRAY
402+
// of message objects, which the production Gson path fails to deserialize
403+
// (JsonSyntaxException: Expected BEGIN_OBJECT but was BEGIN_ARRAY). The
404+
// docs (slackLists.items.create#field-types) show only the request shape
405+
// (an array of permalink URL strings) and give no response example, so the
406+
// response shape is unverified in public. This probe observes the real
407+
// production response shape empirically before we commit to a fix.
408+
//
409+
// It does two independent things:
410+
// 1) captures the RAW JSON of slackLists.items.info and asserts whether
411+
// the `message` field value is a JSON array `[` or an object `{`;
412+
// 2) attempts the TYPED deserialization via the production client and
413+
// records whether it throws on that real shape.
414+
// This method is intentionally standalone and does not modify any SDK
415+
// source; it is safe to revert wholesale.
416+
// ------------------------------------------------------------------------
417+
@Test
418+
public void messageFieldArrayShapeProbe() throws IOException, SlackApiException {
419+
MethodsClient methods = slack.methods();
420+
421+
AuthTestResponse auth = methods.authTest(r -> r.token(botToken));
422+
assertThat(auth.getError(), is(nullValue()));
423+
String teamHost = auth.getUrl(); // e.g. https://<team>.slack.com/
424+
425+
// 1) Post a real message so we have a genuine permalink to reference.
426+
ChatPostMessageResponse posted = methods.chatPostMessage(r -> r
427+
.token(botToken)
428+
.channel(channelId)
429+
.text("List message-field probe " + System.currentTimeMillis()));
430+
assertThat(posted.getError(), is(nullValue()));
431+
assertThat(posted.isOk(), is(true));
432+
String messageTs = posted.getTs();
433+
434+
ChatGetPermalinkResponse permalinkResp = methods.chatGetPermalink(r -> r
435+
.token(botToken)
436+
.channel(channelId)
437+
.messageTs(messageTs));
438+
assertThat(permalinkResp.getError(), is(nullValue()));
439+
String permalink = permalinkResp.getPermalink();
440+
assertThat(permalink, is(notNullValue()));
441+
log.info("[probe] message permalink: {}", permalink);
442+
443+
// 2) Create a list with a message-type column.
444+
ListColumn titleCol = ListColumn.builder()
445+
.key("title").name("Title").type("text").primaryColumn(true).build();
446+
ListColumn messageCol = ListColumn.builder()
447+
.key("linked_message").name("Linked Message").type("message").build();
448+
449+
SlackListsCreateResponse createResponse = methods.slackListsCreate(r -> r
450+
.token(botToken)
451+
.name("Message-field probe list")
452+
.schema(Arrays.asList(titleCol, messageCol)));
453+
assertThat(createResponse.getError(), is(nullValue()));
454+
assertThat(createResponse.isOk(), is(true));
455+
String listId = createResponse.getListId();
456+
457+
Map<String, String> keyToId = new HashMap<>();
458+
createResponse.getListMetadata().getSchema().forEach(c -> keyToId.put(c.getKey(), c.getId()));
459+
String messageColId = keyToId.get("linked_message");
460+
assertThat(messageColId, is(notNullValue()));
461+
462+
// grant the bot's channel write access so items can be created
463+
methods.slackListsAccessSet(r -> r
464+
.token(botToken).listId(listId).accessLevel("write")
465+
.channelIds(Arrays.asList(channelId)));
466+
467+
// 3) Create an item with a primary text field. The message field is
468+
// set separately below via items.update using the docs-correct request
469+
// shape (an array of message permalink URL strings), sent as a raw form
470+
// param so the request carries exactly `message: ["<permalink>"]`.
471+
SlackListsItemsCreateResponse createItemResponse = methods.slackListsItemsCreate(r -> r
472+
.token(botToken)
473+
.listId(listId)
474+
.initialFields(Arrays.asList(
475+
ListRecord.Field.builder().columnId(keyToId.get("title")).text("probe row").build()
476+
)));
477+
assertThat(createItemResponse.getError(), is(nullValue()));
478+
assertThat(createItemResponse.isOk(), is(true));
479+
String itemId = createItemResponse.getItem().getId();
480+
481+
// Set the message field on the row via items.update (request = URL array).
482+
// Sent as a raw form param so the request carries the exact docs shape.
483+
SlackHttpClient http = slack.getHttpClient();
484+
FormBody updateForm = new FormBody.Builder()
485+
.add("list_id", listId)
486+
.add("cells", "[{\"row_id\":\"" + itemId + "\",\"column_id\":\"" + messageColId
487+
+ "\",\"message\":[\"" + permalink + "\"]}]")
488+
.build();
489+
try (Response updRaw = http.postFormWithBearerHeader(
490+
MethodsClient.ENDPOINT_URL_PREFIX + "slackLists.items.update", botToken, updateForm)) {
491+
String updBody = updRaw.body() != null ? updRaw.body().string() : "";
492+
log.info("[probe] items.update raw ok? contains \\\"ok\\\":true -> {}", updBody.contains("\"ok\":true"));
493+
}
494+
495+
// 4) RAW-capture the read-back shape of the message field.
496+
FormBody infoForm = new FormBody.Builder()
497+
.add("list_id", listId)
498+
.add("id", itemId)
499+
.build();
500+
String rawBody;
501+
try (Response infoRaw = http.postFormWithBearerHeader(
502+
MethodsClient.ENDPOINT_URL_PREFIX + "slackLists.items.info", botToken, infoForm)) {
503+
rawBody = infoRaw.body() != null ? infoRaw.body().string() : "";
504+
}
505+
log.info("[probe] raw items.info body (truncated 4k): {}",
506+
rawBody.length() > 4000 ? rawBody.substring(0, 4000) : rawBody);
507+
508+
int msgIdx = rawBody.indexOf("\"message\"");
509+
assertThat("expected a message field in the raw response", msgIdx, is(not(-1)));
510+
// Skip past `"message"` and any whitespace / colon to the value's first char.
511+
int valStart = msgIdx + "\"message\"".length();
512+
while (valStart < rawBody.length()
513+
&& (rawBody.charAt(valStart) == ':' || Character.isWhitespace(rawBody.charAt(valStart)))) {
514+
valStart++;
515+
}
516+
char firstValChar = rawBody.charAt(valStart);
517+
boolean isArray = firstValChar == '[';
518+
boolean isObject = firstValChar == '{';
519+
log.info("[probe] message field first value char = '{}' => isArray={}, isObject={}",
520+
firstValChar, isArray, isObject);
521+
522+
// 5) Probe TYPED (production Gson) deserialization on this real shape.
523+
boolean typedThrew = false;
524+
String typedError = null;
525+
try {
526+
SlackListsItemsInfoResponse typed = methods.slackListsItemsInfo(r -> r
527+
.token(botToken).listId(listId).id(itemId));
528+
log.info("[probe] typed items.info deserialized ok={}, error={}",
529+
typed.isOk(), typed.getError());
530+
} catch (Exception e) {
531+
typedThrew = true;
532+
typedError = e.getClass().getSimpleName() + ": " + e.getMessage();
533+
log.warn("[probe] typed items.info THREW during deserialization: {}", typedError);
534+
}
535+
log.info("[probe] SUMMARY host={} messageIsArray={} messageIsObject={} typedDeserializationThrew={} typedError={}",
536+
teamHost, isArray, isObject, typedThrew, typedError);
537+
538+
// The point of the probe is observation, not a pass/fail gate on the
539+
// shape. We only assert the response actually carried a message value
540+
// (either shape), and log the empirical result for the issue.
541+
assertThat("message value should be an array or object", isArray || isObject, is(true));
542+
543+
// cleanup
544+
methods.slackListsItemsDelete(r -> r.token(botToken).listId(listId).id(itemId));
545+
methods.slackListsAccessDelete(r -> r.token(botToken).listId(listId)
546+
.channelIds(Arrays.asList(channelId)));
547+
}
548+
391549
}

0 commit comments

Comments
 (0)