Ship the 2026-08-23 API contract: preference variation images + writable audience exclusions (0.9.0) - #25
Merged
Conversation
…ble audience exclusions (0.9.0) Two API gaps closed, both found while building a four-cell monadic study. Preference options are variations, not choices. They are PreferenceVariation records — one full-size image each — which is what GET /tests/:id has always read them back as, and since this API release it is the key writes take too. The CLI could only ever send option names, so every variation came back asset_id: null and the test could never clear its "Add an image" launch blocker. New --variations <json|@file> on add-question/edit-question takes strings, objects {name (or text), asset_id?, site_link?}, or a mix; --choices survives as a legacy alias and errors are named for whichever key the caller used. Closing that also exposed a read-back gap: preview and walkthrough both read options off variations[0].choices, which is empty on a preference question — so a preference question rendered with no options at all. Both now list the options and flag any with an empty image slot, and preview JSON emits `variations` (feedable straight back into a write) rather than the always-empty `choices`. exclude_test_ids is now writable, so a run of tests that must not share participants no longer has to be wired up by hand in the editor. --exclude-tests on create/update, --clear-exclude-tests on update. The three API gates split by what the CLI can actually see: the panel-audiences-only rule, duplicates and self-exclusion are local errors, while the beta_group flag and the 5/30 cap are warnings — the CLI cannot see the account, and erroring would falsely block an internal_group account that legitimately gets 30. This matches how the Enterprise branching gate is already surfaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships against the 2026-08-23 Public API release. Two gaps, both surfaced while building a four-cell monadic study from the API.
Preference options are
variations, and can carry an imageA preference question's options are
PreferenceVariationrecords — one full-size image each — notChoicerecords.GET /tests/:idhas always read them back undervariations, and as of this release that is the key writes take too.The CLI could only ever send option names, so every variation came back
asset_id: nulland the test could never clear its "Add an image" launch blocker.--variations <json|@file>ontests add-questionandtests edit-question. Takes plain strings, objects{name (or text), asset_id?, site_link?}, or a mix.--choicesstill works on a preference question as a legacy alias, so existing scripts keep running. Passing both is rejected locally, and errors are named for whichever key the caller actually used (variations[0]vschoices[0]).variationson any other question type is rejected with a pointer tochoices, rather than silently dropped.asset_idsarray — which the API used to accept with a 200 and ignore — is now rejected by name, pointing at per-variationasset_id.GETgets one error listing its read-only keys, not one error per key. (Mirroring a GET into a write is the exact anti-pattern this whole change came from, so it's worth a legible message rather than a wall.)Read-back gap this exposed
previewandwalkthroughboth read options offvariations[0].choices, which is empty on a preference question — so a preference question previewed with no options at all. Both now list the options and flag any with an empty image slot, since that's a launch blocker the participant view gives no hint of.exclude_test_idsis writablePreviously readable but not writable, so a run of tests that must not share participants had to be wired up by hand in the editor.
--exclude-tests <ids...>ontests createandtests update;--clear-exclude-testson update.--audience-type— unlike--audiences, the API applies exclusions to whatever quota the test already has.The three API gates are split by what the CLI can actually see:
--audience-type--dry-runbeta_groupflaginternal_group)The warning treatment matches how the Enterprise branching gate is already surfaced.
--clear-exclude-testsbypasses everything, mirroring the API's own exemption so a test is always undoable.Reviewer notes
Breaking (JSON output shape) — called out in the changelog:
tests preview --output jsonemitsvariationsinstead ofchoicesfor preference questions. The oldchoiceskey was always[]on these, and the new shape is what a write takes, so preview output now feeds straight back intocreate/add-question. Every other type is unchanged.tests walkthrough --output jsongainspreference_optionson every question screen (nullon non-preference types).Verification — 332 tests pass (up from 292); typecheck and build clean. New coverage for preference variation validation, the
preferenceOptionsread-back, walkthrough preference rendering, the exclusion validator/warnings, and guide-JSON contract assertions. Each new path was also exercised against the built binary: schema output, dual-key rejection, GET-mirrored payloads, wrong-typevariations, exclusions on panel vs. open audiences, self-exclusion, and flag conflicts.One gap worth naming: the
printSectionQuestionstext renderer for preference has no unit test. Its logic (preferenceOptions) and its walkthrough twin are both tested, and every console printer in this file is untested by existing convention — but that path was verified by inspection only, not execution, since it needs a live API response.Also folded in:
tests question-types --type <type>now prints the type'snotesin text mode. JSON consumers always saw them; text mode dropped them, which hid rules that live nowhere else — including thevariations/choicesaliasing this PR introduces.🤖 Generated with Claude Code