test(form-field): pin conditional_logic activity log payload contract

ARCH §8.6 specifies a dual-event contract on logic changes — a
`field.updated` row carrying old/new diffs of the reconstructed JSON
shape, plus a semantic `field.conditional_logic_replaced` row from
inside `replaceLogic()`. The semantic event is already pinned by
`FormFieldConditionalLogicServiceTest`. The diff payload contract was
documented but unasserted.

Two new tests:

  - `test_field_updated_activity_log_contains_conditional_logic_diff_when_tree_changes`
    Pins old/new payload shapes via byte-equal `json_encode` comparison
    (mirrors ConditionalLogicSnapshotAndResourceParityTest's
    associative-array key-order trap). Both rows share the same
    causer_id.
  - `test_field_updated_without_logic_change_does_not_emit_conditional_logic_diff`
    Pins the negative: bare label-only updates must NOT carry a
    `conditional_logic` key in the field.updated payload, and must NOT
    emit a semantic `field.conditional_logic_replaced` row.

The first test passed against the original implementation; the second
required `FormFieldService::update()` to filter `conditional_logic`
out of the activity-log payload when the reconstructed shape didn't
change between pre- and post-write. Adjustment lands in this commit:
the `$before` / `$new` arrays now only carry the key when
`$currentConditionalShape !== $newConditionalShape`.

Tests: 1148 → 1150 green (3099 → 3110 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:52:57 +02:00
parent 9e181092fc
commit 64f5855fdb
2 changed files with 167 additions and 8 deletions

View File

@@ -100,7 +100,6 @@ final class FormFieldService
$before = [
'binding' => $currentBindingShape,
'conditional_logic' => $currentConditionalShape,
'is_filterable' => $field->is_filterable,
'is_pii' => $field->is_pii,
'field_type' => $field->field_type,
@@ -123,15 +122,26 @@ final class FormFieldService
$this->schemaService->bumpVersion($schema);
$newConditionalShape = $this->conditionalLogicService->toJsonShape($field->fresh()?->rootConditionalLogicGroup());
$new = [
'binding' => $this->bindingService->toJsonShape($field->bindings()->first()),
'is_filterable' => $field->is_filterable,
'is_pii' => $field->is_pii,
'field_type' => $field->field_type,
];
// ARCH §8.6: include conditional_logic in the field.updated diff only
// when the tree actually changed. Bare label/sort_order updates and
// payloads that did not touch conditional_logic must not carry the
// key — otherwise downstream activity-log consumers see noise.
if ($currentConditionalShape !== $newConditionalShape) {
$before['conditional_logic'] = $currentConditionalShape;
$new['conditional_logic'] = $newConditionalShape;
}
$field->logFieldChange('field.updated', [
'old' => $before,
'new' => [
'binding' => $this->bindingService->toJsonShape($field->bindings()->first()),
'conditional_logic' => $this->conditionalLogicService->toJsonShape($field->fresh()?->rootConditionalLogicGroup()),
'is_filterable' => $field->is_filterable,
'is_pii' => $field->is_pii,
'field_type' => $field->field_type,
],
'new' => $new,
]);
if ($before['is_filterable'] !== $field->is_filterable) {