Skip to content
Open
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
13 changes: 13 additions & 0 deletions scripts/union-characterization
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "json"

openai_lib = File.expand_path(ARGV.shift || File.expand_path("../lib", __dir__))
abort("Usage: scripts/union-characterization [openai-lib-directory]") unless ARGV.empty?

require File.join(openai_lib, "openai")

require_relative "../test/openai/internal/type/union_characterization"

puts(JSON.pretty_generate(OpenAI::Test::UnionCharacterization.observe))
45 changes: 45 additions & 0 deletions scripts/union-characterization-differential
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash

set -euo pipefail

cd -- "$(dirname -- "$0")/.."

releases=(v0.78.0 v0.80.0)
missing_releases=()
for release in "${releases[@]}"; do
if ! git rev-parse --verify --quiet "refs/tags/${release}^{commit}" >/dev/null; then
missing_releases+=("$release")
fi
done

if ((${#missing_releases[@]} > 0)); then
printf 'Missing required release tags: %s\n' "${missing_releases[*]}" >&2
printf 'Fetch them with `git fetch --tags` before running this differential.\n' >&2
exit 1
fi

tmp_dir=$(mktemp -d)
trap 'rm -rf -- "$tmp_dir"' EXIT

for release in "${releases[@]}"; do
mkdir "$tmp_dir/$release"
git archive "refs/tags/$release" | tar -x -C "$tmp_dir/$release"
ruby scripts/union-characterization "$tmp_dir/$release/lib" >"$tmp_dir/$release.json"
done

echo "Comparing published releases v0.78.0 and v0.80.0..."
diff -u "$tmp_dir/v0.78.0.json" "$tmp_dir/v0.80.0.json"

ruby scripts/union-characterization >"$tmp_dir/main.json"

echo "Published releases agree. Their differential from the current checkout follows:"
if diff -u --label v0.78.0-and-v0.80.0 --label current-checkout \
"$tmp_dir/v0.80.0.json" "$tmp_dir/main.json"
then
echo "No differences."
else
status=$?
if ((status > 1)); then
exit "$status"
fi
fi
17 changes: 17 additions & 0 deletions test/fixtures/union_characterization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Union characterization corpus

This corpus records current behavior; it does not declare every selected class to be semantically correct.

- `discriminator_evidence` is an explicit fixture fact: missing, known and unique, known but duplicated, or explicitly unknown.
- `exact_candidates` lists every generated variant whose independent coercion has no `no` or `maybe` evidence.
- `structural_candidates` lists every generated variant whose independent coercion has no `no` evidence.
- `best_ranked_fallbacks` applies the current `Union#coerce` fallback score only when no exact candidate exists.
- `compatibility_selected` is the class selected by the current runtime. It is a compatibility observation, not an endorsement.
- `serialized_body` proves that accepted request shorthand and unknown fields retain their wire representation regardless of materialization.
- `public_boundary` records context-specific behavior when it differs from the shared union. It is currently available for the Realtime connection introduced after v0.80.0.

The fixtures deliberately pair wire-string hashes with idiomatic Ruby symbol hashes. Their different candidate sets are part of the observed contract. Focused tests retain the compatibility-sensitive selections and serialized bodies without checking in full generated output snapshots.

Run `scripts/union-characterization` to print every current observation using this checkout's `lib`. Run `scripts/union-characterization-differential` under the repository Ruby to generate observations from temporary v0.78.0 and v0.80.0 tag archives, verify that those published releases agree, and print their differential from the current checkout. The differential reports any unavailable release tags together before attempting to archive them; fetch the tags when working from a shallow or no-tags checkout.

The two published baselines agree for this corpus. Current `main` retains the request behavior and serialized bodies, but newer generated Responses/Beta variants change which known event wins structural fallback for an explicit unknown stream tag. Current Realtime adds a public connection boundary that returns `UnknownServerEvent`, even though direct shared-union coercion can still select a known event for symbolized input.
320 changes: 320 additions & 0 deletions test/fixtures/union_characterization/cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
[
{
"id": "responses_string_message_without_type",
"union": "responses_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "missing"
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"role": "user",
"content": "hello"
},
"serialization": "responses_request"
},
{
"id": "responses_typed_message_without_type",
"union": "responses_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "missing"
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
]
},
"serialization": "responses_request"
},
{
"id": "responses_typed_message_without_type_symbols",
"union": "responses_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "missing"
},
"uniquely_resolvable": false,
"input_form": "ruby_symbols",
"input": {
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
]
},
"serialization": "responses_request"
},
{
"id": "responses_typed_message_with_duplicate_tag",
"union": "responses_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "known_duplicate",
"matching_variants": 3
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
]
},
"serialization": "responses_request"
},
{
"id": "responses_typed_message_with_duplicate_tag_symbols",
"union": "responses_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "known_duplicate",
"matching_variants": 3
},
"uniquely_resolvable": false,
"input_form": "ruby_symbols",
"input": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
]
},
"serialization": "responses_request"
},
{
"id": "responses_typed_message_with_unknown_field",
"union": "responses_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "missing"
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
],
"future_field": {
"preserved": true
}
},
"serialization": "responses_request"
},
{
"id": "beta_typed_message_without_type",
"union": "beta_response_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "missing"
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
]
},
"serialization": "beta_responses_request"
},
{
"id": "beta_typed_message_with_duplicate_tag_symbols",
"union": "beta_response_input_item",
"context": "request",
"discriminator": {
"property": "type",
"state": "known_duplicate",
"matching_variants": 3
},
"uniquely_resolvable": false,
"input_form": "ruby_symbols",
"input": {
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "hello"
}
]
},
"serialization": "beta_responses_request"
},
{
"id": "responses_unknown_stream_event",
"union": "responses_stream_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "explicitly_unknown",
"matching_variants": 0
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"type": "future.unmodeled.event",
"sequence_number": 9
},
"serialization": "union"
},
{
"id": "responses_unknown_stream_event_symbols",
"union": "responses_stream_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "explicitly_unknown",
"matching_variants": 0
},
"uniquely_resolvable": false,
"input_form": "ruby_symbols",
"input": {
"type": "future.unmodeled.event",
"sequence_number": 9
},
"serialization": "union"
},
{
"id": "beta_unknown_stream_event",
"union": "beta_response_stream_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "explicitly_unknown",
"matching_variants": 0
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"type": "future.unmodeled.event",
"sequence_number": 9
},
"serialization": "union"
},
{
"id": "beta_unknown_stream_event_symbols",
"union": "beta_response_stream_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "explicitly_unknown",
"matching_variants": 0
},
"uniquely_resolvable": false,
"input_form": "ruby_symbols",
"input": {
"type": "future.unmodeled.event",
"sequence_number": 9
},
"serialization": "union"
},
{
"id": "realtime_unknown_server_event",
"union": "realtime_server_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "explicitly_unknown",
"matching_variants": 0
},
"uniquely_resolvable": false,
"input_form": "wire",
"input": {
"type": "future.unmodeled.event",
"sequence_number": 9
},
"serialization": "union",
"public_boundary": "realtime_connection"
},
{
"id": "realtime_unknown_server_event_symbols",
"union": "realtime_server_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "explicitly_unknown",
"matching_variants": 0
},
"uniquely_resolvable": false,
"input_form": "ruby_symbols",
"input": {
"type": "future.unmodeled.event",
"sequence_number": 9
},
"serialization": "union",
"public_boundary": "realtime_connection"
},
{
"id": "realtime_known_unique_server_event",
"union": "realtime_server_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "known_unique",
"matching_variants": 1
},
"uniquely_resolvable": true,
"input_form": "wire",
"input": {
"type": "conversation.created",
"event_id": "evt_1",
"conversation": {
"id": "conv_1",
"object": "realtime.conversation"
}
},
"serialization": "union"
},
{
"id": "realtime_known_unique_server_event_symbols",
"union": "realtime_server_event",
"context": "response_stream",
"discriminator": {
"property": "type",
"state": "known_unique",
"matching_variants": 1
},
"uniquely_resolvable": true,
"input_form": "ruby_symbols",
"input": {
"type": "conversation.created",
"event_id": "evt_1",
"conversation": {
"id": "conv_1",
"object": "realtime.conversation"
}
},
"serialization": "union"
}
]
Loading