You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/06/15 06:33:26 UTC
(texera) branch gh-readonly-queue/main/pr-5697-0f79251c56d36ca9b4c05e53cf2625a6ad49a131 created (now 612a79cbe4)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-5697-0f79251c56d36ca9b4c05e53cf2625a6ad49a131
in repository https://gitbox.apache.org/repos/asf/texera.git
at 612a79cbe4 test(workflow-operator): add unit test coverage for visualization chart-config classes (#5697)
This branch includes the following new commits:
new 612a79cbe4 test(workflow-operator): add unit test coverage for visualization chart-config classes (#5697)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(texera) 01/01: test(workflow-operator): add unit test coverage for visualization chart-config classes (#5697)
Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5697-0f79251c56d36ca9b4c05e53cf2625a6ad49a131
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 612a79cbe48dc915e2975d856541847a918322b4
Author: Xinyuan Lin <xi...@uci.edu>
AuthorDate: Sun Jun 14 23:33:05 2026 -0700
test(workflow-operator): add unit test coverage for visualization chart-config classes (#5697)
### What changes were proposed in this PR?
Pin behavior of four previously-uncovered Jackson-annotated config
classes that back visualization operators in
`common/workflow-operator/operator/visualization/`. No production-code
changes.
| Spec | Source class | Tests |
| --- | --- | --- |
| `TablesConfigSpec` | `TablesConfig` | 7 |
| `NestedTableConfigSpec` | `NestedTableConfig` | 9 |
| `FigureFactoryTableConfigSpec` | `FigureFactoryTableConfig` | 7 |
| `DumbbellDotConfigSpec` | `DumbbellDotConfig` | 9 |
All four spec files follow the `<srcClassName>Spec.scala` one-to-one
convention.
**Behavior pinned (each class)**
| Surface | Contract |
| --- | --- |
| Default field values | empty strings on a fresh instance |
| Mutability | `var` fields are assignable post-construction |
| JSON round-trip via `objectMapper.writeValueAsString` + `readValue` |
preserves every field |
| `@JsonProperty(required = true)` annotation | present on every
documented required field — verified via reflection |
| Distinct instances | no static-field leakage across `new` calls |
**Per-class specifics**
| Spec | Additional pins |
| --- | --- |
| `TablesConfigSpec` | `@NotNull` annotation on `attributeName` (jakarta
validation) |
| `NestedTableConfigSpec` | `newName` serializes under wire-key `name`
(per `@JsonProperty(value = "name")`); the field name `newName` MUST NOT
appear as a JSON key; deserialization from `{"name":...}` re-populates
`newName` |
| `FigureFactoryTableConfigSpec` | `@AutofillAttributeName` annotation
on `attributeName` (UI dropdown contract) |
| `DumbbellDotConfigSpec` | `dotValue` serializes under wire-key `dot`;
`@NotNull` + `@AutofillAttributeName` annotations; class-level
`@JsonSchemaInject` restricts `dot` to integer/long/double attribute
types |
### Any related issues, documentation, discussions?
Closes #5694.
### How was this PR tested?
Pure unit-test additions; verified locally with:
- `sbt "WorkflowOperator/testOnly
org.apache.texera.amber.operator.visualization.tablesChart.TablesConfigSpec
org.apache.texera.amber.operator.visualization.nestedTable.NestedTableConfigSpec
org.apache.texera.amber.operator.visualization.figureFactoryTable.FigureFactoryTableConfigSpec
org.apache.texera.amber.operator.visualization.dumbbellPlot.DumbbellDotConfigSpec"`
— 32 tests, all green
- `sbt scalafmtCheckAll` — clean
- CI to confirm
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.7 [1M context])
---
.../dumbbellPlot/DumbbellDotConfigSpec.scala | 134 +++++++++++++++++++++
.../FigureFactoryTableConfigSpec.scala | 99 +++++++++++++++
.../nestedTable/NestedTableConfigSpec.scala | 132 ++++++++++++++++++++
.../tablesChart/TablesConfigSpec.scala | 108 +++++++++++++++++
4 files changed, 473 insertions(+)
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfigSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfigSpec.scala
new file mode 100644
index 0000000000..d1761817d5
--- /dev/null
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dumbbellPlot/DumbbellDotConfigSpec.scala
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.dumbbellPlot
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject
+import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+import javax.validation.constraints.NotNull
+
+class DumbbellDotConfigSpec extends AnyFlatSpec {
+
+ // ---------------------------------------------------------------------------
+ // Default state
+ // ---------------------------------------------------------------------------
+
+ "DumbbellDotConfig" should "default dotValue to the empty string" in {
+ val c = new DumbbellDotConfig
+ assert(c.dotValue == "")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Mutability
+ // ---------------------------------------------------------------------------
+
+ it should "allow dotValue to be assigned post-construction" in {
+ val c = new DumbbellDotConfig
+ c.dotValue = "numeric-col"
+ assert(c.dotValue == "numeric-col")
+ }
+
+ // ---------------------------------------------------------------------------
+ // JSON round-trip — verify both the Scala field name AND the wire key
+ // ---------------------------------------------------------------------------
+
+ "DumbbellDotConfig JSON round-trip" should
+ "preserve dotValue via the wire-key `dot` (per @JsonProperty(value = \"dot\"))" in {
+ val original = new DumbbellDotConfig
+ original.dotValue = "amount"
+ val json = objectMapper.writeValueAsString(original)
+ // Parse the JSON into a tree and assert on field presence + value
+ // directly — this stays robust to formatting changes (spaces, key
+ // ordering) that pure substring matching would mistake for drift.
+ val tree = objectMapper.readTree(json)
+ assert(tree.has("dot"), s"expected wire-key 'dot' in JSON, got: $json")
+ assert(tree.get("dot").asText() == "amount")
+ assert(
+ !tree.has("dotValue"),
+ s"field name 'dotValue' must NOT appear as a JSON key, got: $json"
+ )
+ val restored = objectMapper.readValue(json, classOf[DumbbellDotConfig])
+ assert(restored.dotValue == "amount")
+ }
+
+ it should "deserialize from the wire-key `dot` back into dotValue" in {
+ val json = """{"dot":"amount"}"""
+ val restored = objectMapper.readValue(json, classOf[DumbbellDotConfig])
+ assert(restored.dotValue == "amount")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Annotations
+ // ---------------------------------------------------------------------------
+
+ "DumbbellDotConfig#dotValue" should
+ "carry @JsonProperty(value = 'dot', required = true)" in {
+ val jp = classOf[DumbbellDotConfig]
+ .getDeclaredField("dotValue")
+ .getAnnotation(classOf[JsonProperty])
+ assert(jp != null)
+ val actualValue = jp.value
+ assert(actualValue == "dot", s"expected value='dot', got: '$actualValue'")
+ assert(jp.required, "dotValue must be marked required")
+ }
+
+ it should "carry @NotNull (javax.validation contract)" in {
+ val notNull = classOf[DumbbellDotConfig]
+ .getDeclaredField("dotValue")
+ .getAnnotation(classOf[NotNull])
+ assert(notNull != null, "dotValue must carry @NotNull for javax.validation")
+ }
+
+ it should "carry @AutofillAttributeName (UI populates the dropdown from the input schema)" in {
+ val ann = classOf[DumbbellDotConfig]
+ .getDeclaredField("dotValue")
+ .getAnnotation(classOf[AutofillAttributeName])
+ assert(ann != null)
+ }
+
+ "DumbbellDotConfig (class-level)" should
+ "carry @JsonSchemaInject restricting `dot` to integer/long/double attribute types" in {
+ // The class-level @JsonSchemaInject is what tells the UI to filter the
+ // attribute dropdown to numeric columns only. Drift here would silently
+ // accept non-numeric attributes and break the chart at render time.
+ val ann = classOf[DumbbellDotConfig].getAnnotation(classOf[JsonSchemaInject])
+ assert(ann != null, "class-level @JsonSchemaInject must be present")
+ val payload = ann.json
+ assert(payload.contains("attributeTypeRules"))
+ assert(payload.contains("\"dot\""))
+ assert(payload.contains("integer"))
+ assert(payload.contains("long"))
+ assert(payload.contains("double"))
+ }
+
+ // ---------------------------------------------------------------------------
+ // Instance independence
+ // ---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new DumbbellDotConfig
+ val b = new DumbbellDotConfig
+ a.dotValue = "first"
+ assert(b.dotValue == "")
+ }
+}
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfigSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfigSpec.scala
new file mode 100644
index 0000000000..e7a6329159
--- /dev/null
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/figureFactoryTable/FigureFactoryTableConfigSpec.scala
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.figureFactoryTable
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class FigureFactoryTableConfigSpec extends AnyFlatSpec {
+
+ // ---------------------------------------------------------------------------
+ // Default state
+ // ---------------------------------------------------------------------------
+
+ "FigureFactoryTableConfig" should "default attributeName to the empty string" in {
+ val c = new FigureFactoryTableConfig
+ assert(c.attributeName == "")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Mutability
+ // ---------------------------------------------------------------------------
+
+ it should "allow attributeName to be assigned post-construction" in {
+ val c = new FigureFactoryTableConfig
+ c.attributeName = "col-z"
+ assert(c.attributeName == "col-z")
+ }
+
+ // ---------------------------------------------------------------------------
+ // JSON round-trip
+ // ---------------------------------------------------------------------------
+
+ "FigureFactoryTableConfig JSON round-trip" should "preserve attributeName" in {
+ val original = new FigureFactoryTableConfig
+ original.attributeName = "x"
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(original),
+ classOf[FigureFactoryTableConfig]
+ )
+ assert(restored.attributeName == "x")
+ }
+
+ it should "round-trip the default empty attributeName" in {
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(new FigureFactoryTableConfig),
+ classOf[FigureFactoryTableConfig]
+ )
+ assert(restored.attributeName == "")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Annotations
+ // ---------------------------------------------------------------------------
+
+ "FigureFactoryTableConfig#attributeName" should "carry @JsonProperty(required = true)" in {
+ val jp = classOf[FigureFactoryTableConfig]
+ .getDeclaredField("attributeName")
+ .getAnnotation(classOf[JsonProperty])
+ assert(jp != null && jp.required)
+ }
+
+ it should
+ "carry @AutofillAttributeName (UI relies on this to populate a dropdown)" in {
+ val ann = classOf[FigureFactoryTableConfig]
+ .getDeclaredField("attributeName")
+ .getAnnotation(classOf[AutofillAttributeName])
+ assert(ann != null, "@AutofillAttributeName must be present so the UI auto-populates")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Instance independence
+ // ---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new FigureFactoryTableConfig
+ val b = new FigureFactoryTableConfig
+ a.attributeName = "first"
+ assert(b.attributeName == "")
+ }
+}
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfigSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfigSpec.scala
new file mode 100644
index 0000000000..199efab00c
--- /dev/null
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/nestedTable/NestedTableConfigSpec.scala
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.nestedTable
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+class NestedTableConfigSpec extends AnyFlatSpec {
+
+ // ---------------------------------------------------------------------------
+ // Default state
+ // ---------------------------------------------------------------------------
+
+ "NestedTableConfig" should "default every field to the empty string" in {
+ val c = new NestedTableConfig
+ assert(c.attributeGroup == "")
+ assert(c.originalName == "")
+ assert(c.newName == "")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Mutability
+ // ---------------------------------------------------------------------------
+
+ it should "allow every field to be assigned post-construction" in {
+ val c = new NestedTableConfig
+ c.attributeGroup = "g"
+ c.originalName = "orig"
+ c.newName = "renamed"
+ assert(c.attributeGroup == "g")
+ assert(c.originalName == "orig")
+ assert(c.newName == "renamed")
+ }
+
+ // ---------------------------------------------------------------------------
+ // JSON round-trip
+ // ---------------------------------------------------------------------------
+
+ "NestedTableConfig JSON round-trip" should "preserve all three fields" in {
+ val original = new NestedTableConfig
+ original.attributeGroup = "group-1"
+ original.originalName = "src-name"
+ original.newName = "dst-name"
+ val json = objectMapper.writeValueAsString(original)
+ val restored = objectMapper.readValue(json, classOf[NestedTableConfig])
+ assert(restored.attributeGroup == "group-1")
+ assert(restored.originalName == "src-name")
+ assert(restored.newName == "dst-name")
+ }
+
+ it should
+ "serialize newName under the JSON key `name` (per @JsonProperty(value = \"name\"))" in {
+ // The wire-key for `newName` is `name`, not `newName` — a regression
+ // that drifted to `newName` would silently break every workflow JSON
+ // that carries this config. Parse the JSON into a tree so the
+ // assertion is robust to Jackson formatting (spaces, key ordering)
+ // and unambiguous about which key carries which value.
+ val c = new NestedTableConfig
+ c.newName = "renamed"
+ val tree = objectMapper.readTree(objectMapper.writeValueAsString(c))
+ assert(tree.has("name"), s"expected wire-key 'name' in JSON tree: $tree")
+ assert(tree.get("name").asText() == "renamed")
+ assert(
+ !tree.has("newName"),
+ s"field name 'newName' must NOT appear as a JSON key, got: $tree"
+ )
+ }
+
+ it should "deserialize from the wire-key `name` back into newName" in {
+ val json = """{"attributeGroup":"g","originalName":"orig","name":"renamed"}"""
+ val restored = objectMapper.readValue(json, classOf[NestedTableConfig])
+ assert(restored.newName == "renamed")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Annotations — required/optional via reflection
+ // ---------------------------------------------------------------------------
+
+ "NestedTableConfig#attributeGroup" should "carry @JsonProperty(required = true)" in {
+ val jp = classOf[NestedTableConfig]
+ .getDeclaredField("attributeGroup")
+ .getAnnotation(classOf[JsonProperty])
+ assert(jp != null && jp.required)
+ }
+
+ "NestedTableConfig#originalName" should "carry @JsonProperty(required = true)" in {
+ val jp = classOf[NestedTableConfig]
+ .getDeclaredField("originalName")
+ .getAnnotation(classOf[JsonProperty])
+ assert(jp != null && jp.required)
+ }
+
+ "NestedTableConfig#newName" should
+ "carry @JsonProperty(value = 'name', required = false) — the optional renamed field" in {
+ val jp = classOf[NestedTableConfig]
+ .getDeclaredField("newName")
+ .getAnnotation(classOf[JsonProperty])
+ assert(jp != null)
+ val actualValue = jp.value
+ assert(actualValue == "name", s"expected value='name', got: '$actualValue'")
+ assert(!jp.required, "newName must NOT be marked required")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Instance independence
+ // ---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new NestedTableConfig
+ val b = new NestedTableConfig
+ a.attributeGroup = "first"
+ assert(b.attributeGroup == "")
+ }
+}
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfigSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfigSpec.scala
new file mode 100644
index 0000000000..48ea3fe136
--- /dev/null
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/tablesChart/TablesConfigSpec.scala
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.visualization.tablesChart
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.scalatest.flatspec.AnyFlatSpec
+
+import javax.validation.constraints.NotNull
+
+class TablesConfigSpec extends AnyFlatSpec {
+
+ // ---------------------------------------------------------------------------
+ // Default state
+ // ---------------------------------------------------------------------------
+
+ "TablesConfig" should "default attributeName to the empty string" in {
+ val c = new TablesConfig
+ assert(c.attributeName == "")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Mutability — the @JsonProperty bag is `var`-based by design
+ // ---------------------------------------------------------------------------
+
+ it should "allow attributeName to be assigned post-construction" in {
+ val c = new TablesConfig
+ c.attributeName = "col-a"
+ assert(c.attributeName == "col-a")
+ }
+
+ // ---------------------------------------------------------------------------
+ // JSON round-trip preserves the field
+ // ---------------------------------------------------------------------------
+
+ "TablesConfig JSON round-trip" should
+ "serialize and deserialize attributeName unchanged" in {
+ val original = new TablesConfig
+ original.attributeName = "my-attr"
+ val json = objectMapper.writeValueAsString(original)
+ val restored = objectMapper.readValue(json, classOf[TablesConfig])
+ assert(restored.attributeName == "my-attr")
+ }
+
+ it should "round-trip the default empty attributeName" in {
+ val original = new TablesConfig
+ val restored = objectMapper.readValue(
+ objectMapper.writeValueAsString(original),
+ classOf[TablesConfig]
+ )
+ assert(restored.attributeName == "")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Independent instances — no static-field leakage
+ // ---------------------------------------------------------------------------
+
+ it should "construct two independent instances (no static state shared)" in {
+ val a = new TablesConfig
+ val b = new TablesConfig
+ a.attributeName = "first"
+ assert(b.attributeName == "", "second instance must not see first instance's mutation")
+ }
+
+ // ---------------------------------------------------------------------------
+ // Annotations — verified via reflection (the Jackson + validation
+ // contract is what consumers actually depend on)
+ // ---------------------------------------------------------------------------
+
+ "TablesConfig#attributeName" should "carry @JsonProperty(required = true)" in {
+ val field = classOf[TablesConfig].getDeclaredField("attributeName")
+ val jp = field.getAnnotation(classOf[JsonProperty])
+ assert(jp != null, "attributeName must carry @JsonProperty")
+ assert(jp.required, "attributeName must be marked required")
+ }
+
+ it should "carry @NotNull (javax.validation contract)" in {
+ val field = classOf[TablesConfig].getDeclaredField("attributeName")
+ val notNull = field.getAnnotation(classOf[NotNull])
+ assert(notNull != null, "attributeName must carry @NotNull for javax.validation")
+ }
+
+ it should
+ "carry @AutofillAttributeName (UI populates the dropdown from the input schema)" in {
+ val ann = classOf[TablesConfig]
+ .getDeclaredField("attributeName")
+ .getAnnotation(classOf[AutofillAttributeName])
+ assert(ann != null, "@AutofillAttributeName must be present so the UI auto-populates")
+ }
+}