fix: #3585027 Use token replacement for the token input and mask fields
Closes #3585027 (closed)
Follows the pattern !21 (merged) (#3534289 (closed)) set for the Chat files field. The token input and mask fields switch to #eca_token_replacement. Every reader goes through one resolver on AiActionBase: it asks the ECA token stack first, so list and object data survive, and only then calls replace(). The name guard is negative on purpose. ECA's normalizeKey() already reads my.sub as my:sub, so a strict regex would break names that resolve today.
token_result keeps #eca_token_reference, deviating from the issue title. It names the token an action writes to, and every output-name field in ECA core is a reference passed to addTokenData() raw (see key versus token_name in eca_base's EcaStateRead). Replacing it would resolve [my_result] to a value and write to a token named after that value.
The conversion exposed a latent bug in Image to Image. The mask guard used hasTokenData(), which only sees the ECA stack, so a Drupal token or a literal path was dropped silently. The mask is now resolved before the empty check. One behavior change: a mistyped token input used to resolve to an empty string and is now sent as literal text. 34 new kernel tests. The bracket-rule check asks whichever component enforces it: Eca::validatePlugin() on ECA 2.x, the modeler model owner on 3.x.
LLM-summary - Click to expand
Scope. After !21 (merged) (#3534289 (closed)), three form elements carried #eca_token_reference: token_input and token_result in src/Plugin/Action/AiActionBase.php, and token_mask in src/Plugin/Action/ImageToImage.php. token_input and token_mask convert; token_result stays.
Why the flags matter. Neither flag is read by JavaScript. eca/src/Plugin/ECA/PluginFormTrait.php only adjusts the description. On ECA 2.x, Eca::validatePlugin() rejects a bracketed value for #eca_token_reference fields; on ECA 3.x that check lives in the eca model owner plugin (eca_ui), called by modeler_api before validation. TokenDecoratorTrait::normalizeKey() already strips brackets, lowercases, and maps . to : on every getTokenData() / addTokenData() key, so bracketed ECA token names worked at runtime before this change; only model validation refused them. Runtime work was needed only to support Drupal tokens such as [node:body] and literal text.
Shared resolver on AiActionBase (all protected):
tokenNameFrom(string): ?string- unwraps[name]; returns NULL for an empty value or one containing whitespace or/; otherwise returns the value as a candidate name. Lifted out ofChat, which now uses it.resolveTokenValue(string): mixed- trims; empty gives NULL; iftokenNameFrom()yields a name andgetTokenData()is non-NULL, returns that raw data; otherwisetrim(replace($configured)), empty gives NULL.resolveTokenString(string): string-tokenValueToString(resolveTokenValue(...)).tokenValueToString(mixed): string- Entity to ID, TypedData togetString(), scalar to string, else''.getTokenString(string)is kept as a thin wrapper with unchanged semantics for subclasses outside this module.
Call sites updated to resolveTokenString(token_input): AudioToAudio, Embedding, ImageClassification, ImageToVideo, Moderation, ObjectDetection, SpeechToSpeech, SpeechToText, TextToSpeech, TranslateText, and ImageToImage (input and mask). Chat and TextToImage unset token_input and are unchanged there. Chat::resolveFileValues() now delegates to resolveTokenValue().
Image to Image mask. Title "Mask token" becomes "Mask". execute() resolves the mask to a string first and then tests for emptiness, replacing the !empty() && hasTokenData() guard. Optional-mask exception handling is unchanged.
Tests (tests/src/Kernel/Plugin/Action/):
TokenInputResolutionTest(13 tests) - resolution order fortoken_inputthrough Embedding: bare name, bracketed name, dotted name, Drupal token, literal text, and raw typed data reaching the provider, asserted on theEmbeddingsInputcaptured fromPreGenerateResponseEvent.ImageToImageMaskTest(8 tests) - mask as bracketed token, bare name, file entity, literal path, literal with an embedded token, absent, unreadable; plus the form flags.EcaModelTokenFieldTest(13 tests) - bracketed values are accepted byEca::addAction()fortoken_inputandfiles, and bare values still are. Three tests go through abracketError()helper that routes to the enforcement point of the installed ECA: on 3.x it calls theecamodel owner'sprepareFormFieldForValidation()on the built form element, on 2.x it adds the action to a model and reads the config entity's errors.token_resultis refused on Embedding and on Image to Image (the positive controls),token_maskis not. The class enableseca_uiso the generation probe is truthful.ModelerTokenFieldTestgains four rows (token_inputandtoken_mask, bracketed and bare). It skips on ECA 2.x, where the model owner plugin does not exist;EcaModelTokenFieldTestis the complementary half that runs on both generations.
Docs. docs/usage.md lists the three accepted forms for Token input, contrasts Token result, and adds a subsection on the Image to Image mask. AGENTS.md gains a "Reading a configured field" section and updated code samples.
Verification. Locally (ECA 2.x): phpcs 0 errors; phpstan 1 pre-existing error, untouched (src/Service/AiFileHelper.php:64); phpunit 338 tests, 2431 assertions, 0 failures, 6 pre-existing ModelerAssistant* browser-test errors without SIMPLETEST_BASE_URL; all 34 new tests executed. Pipeline 956111 (ECA 3.1.7, modeler_api 1.1.6, ai 1.4.8) was green on every validate job including cspell and exposed the 2.x-only assumption in the two control tests, which this MR's second commit fixes. Between the local run and CI, both branches of the version guard execute.
Out of scope, noted for follow-up. AiActionBase::validateConfigurationForm() checks that model splits on __ but not that it is one of the #options.
AI-Generated: Yes (agent wrote the code, tests and documentation changes and drafted this description from a specification and decisions by jurgenhaas; reviewed by jurgenhaas).