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-7540-af38ca99c13c0be4fbe69cc168c82d69aae36c56
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 0414dc2c851d2ce591a3d4b9a65a728aa4fb50ba
Author: Meng Wang <[email protected]>
AuthorDate: Mon Aug 10 23:42:40 2026 -0700

    test(amber): cover version-importance helpers and the email message builder 
(#7540)
    
    ### What changes were proposed in this PR?
    
    Covers the remaining unit-testable pure logic in two of the three
    classes the
    issue lists. No production code was changed.
    
    **`WorkflowVersionResource`** (+7 tests) — the version-importance
    helpers, all
    private and exercised through `PrivateMethodTester`:
    
    - `isSnapshotImportant` — a patch whose ops are all `replace` is
    unimportant, any
      other op makes it important, and an empty patch is unimportant.
    - `isVersionImportant` — a patch touching only `/operatorPositions/` is
      unimportant; anything else is important.
    - `isWithinTimeLimit` — holds inside the aggregate window, not outside
    it.
    - `encodeVersionImportance` — the latest version is always important, a
    version
    inside the aggregate window is folded in as unimportant, and one outside
    it is
      judged on its content.
    
    > The issue calls this gap `jsonTreeIterator`; that is a local variable
    inside
    > `isSnapshotImportant` / `isVersionImportant` rather than a method, so
    the tests
    > target those two methods (plus the rest of the family).
    
    **`WorkflowEmailNotifier`** (+2 tests)
    
    - `createEmailMessage` — the assembled message addresses the recipient
    and
    carries the same subject the dedicated builder produces, plus the
    workflow
      name, id and state in its content.
    - `sendStatusEmail` — the invalid-recipient arm returns before
    dispatching, so
      the test never reaches `GmailResource.sendEmail`.
    
    After this change (measured with `sbt WorkflowExecutionService/jacoco`):
    `WorkflowVersionResource` 116/121 lines (95%), `WorkflowEmailNotifier`
    40/43
    (93%). The lines that remain are not unit-reachable:
    
    - `updateLatestVersion` — defined but has no callers anywhere in the
    repo.
    - `sendStatusEmail`'s dispatch arm — it calls `GmailResource.sendEmail`,
    i.e. it
      would send real mail.
    - `cloneVersion`'s catch block — needs a database-layer failure to
    trigger.
    - `isSnapshotInRangeUnimportant`'s `lowerBound == UpperBound` early
    return is
    already asserted by an existing test, but jacoco still reports it unhit:
    the
    parameters are `java.lang.Integer`, so `==` compares references rather
    than
    values. Worth a look separately — this PR does not change production
    code.
    
    **`ResultExportService`** — no tests added. Its in-scope pure logic is
    already
    covered by the existing spec: `parseOperators` (valid / empty /
    malformed),
    `validateExportRequest` (both arms), `convertFieldToBytes` (all three
    cases),
    `generateFileName` (parquet→zip, path-separator stripping),
    `streamCellData`
    (all three guards), and the `errorMessages` accumulation the issue
    mentions
    (`exportToDataset` "collect one message per operator" / "turn a thrown
    per-operator failure into an error entry"). The `download.dat` default
    the issue
    asks for is unreachable: when `exportOperatorResultAsStream` returns no
    file
    name it also returns a null stream, and the preceding line throws. The
    remaining
    uncovered methods (`exportOperatorsAsZip`,
    `exportSingleOperatorToDataset`,
    `getOperatorDocument`) all go through `DocumentFactory`/storage, which
    the issue
    places out of scope.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7537
    
    ### How was this PR tested?
    
    Unit tests, run locally against embedded Postgres (`MockTexeraDB`). All
    pass, and
    the failure path was verified by breaking an assertion to confirm the
    suite goes
    red:
    
    ```
    sbt "WorkflowExecutionService/testOnly *WorkflowVersionResourceSpec 
*WorkflowEmailNotifierSpec"
    # Tests: succeeded 37, failed 0
    sbt "WorkflowExecutionService/Test/scalafmtCheck"      # clean
    sbt "WorkflowExecutionService/Test/scalafix --check"   # clean
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../workflow/WorkflowVersionResourceSpec.scala     | 73 +++++++++++++++++++++-
 .../web/service/WorkflowEmailNotifierSpec.scala    | 33 ++++++++++
 2 files changed, 105 insertions(+), 1 deletion(-)

diff --git 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowVersionResourceSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowVersionResourceSpec.scala
index b633ee83a4..15a3bb6654 100644
--- 
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowVersionResourceSpec.scala
+++ 
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowVersionResourceSpec.scala
@@ -41,7 +41,7 @@ import org.apache.texera.dao.jooq.generated.tables.pojos.{
 import org.jooq.impl.DSL
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
-import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, 
PrivateMethodTester}
 
 import java.sql.Timestamp
 import java.util.UUID
@@ -55,6 +55,7 @@ class WorkflowVersionResourceSpec
     with Matchers
     with BeforeAndAfterAll
     with BeforeAndAfterEach
+    with PrivateMethodTester
     with MockTexeraDB {
 
   private val testWorkflowWid = 2000 + scala.util.Random.nextInt(1000)
@@ -453,4 +454,74 @@ class WorkflowVersionResourceSpec
     cloned should not be null
     cloned.getName should include("_copy")
   }
+
+  // ─── version-importance helpers (pure JSON/timestamp logic) 
────────────────
+
+  private val isSnapshotImportant = 
PrivateMethod[Boolean](Symbol("isSnapshotImportant"))
+  private val isVersionImportant = 
PrivateMethod[Boolean](Symbol("isVersionImportant"))
+  private val isWithinTimeLimit = 
PrivateMethod[Boolean](Symbol("isWithinTimeLimit"))
+  private val encodeVersionImportance =
+    
PrivateMethod[List[WorkflowVersionResource.VersionEntry]](Symbol("encodeVersionImportance"))
+
+  "isSnapshotImportant" should "treat a patch whose ops are all 'replace' as 
unimportant" in {
+    val content = 
"""[{"op":"replace","path":"/operatorPositions/x","value":1}]"""
+    (WorkflowVersionResource invokePrivate isSnapshotImportant(content)) 
shouldBe false
+  }
+
+  it should "treat a patch containing any non-replace op as important" in {
+    val content = 
"""[{"op":"replace","path":"/a"},{"op":"add","path":"/operators/0"}]"""
+    (WorkflowVersionResource invokePrivate isSnapshotImportant(content)) 
shouldBe true
+  }
+
+  it should "treat an empty patch as unimportant" in {
+    (WorkflowVersionResource invokePrivate isSnapshotImportant("[]")) shouldBe 
false
+  }
+
+  "isVersionImportant" should "treat a patch touching only operator positions 
as unimportant" in {
+    val content = 
"""[{"op":"replace","path":"/operatorPositions/op-1/x","value":5}]"""
+    (WorkflowVersionResource invokePrivate isVersionImportant(content)) 
shouldBe false
+  }
+
+  it should "treat a patch touching anything else as important" in {
+    val content = 
"""[{"op":"replace","path":"/operators/0/properties","value":{}}]"""
+    (WorkflowVersionResource invokePrivate isVersionImportant(content)) 
shouldBe true
+  }
+
+  "isWithinTimeLimit" should "hold for timestamps closer together than the 
aggregate limit" in {
+    val later = new Timestamp(1_700_000_000_000L)
+    val earlier = new Timestamp(later.getTime - TimeUnit.SECONDS.toMillis(1))
+    (WorkflowVersionResource invokePrivate isWithinTimeLimit(later, earlier)) 
shouldBe true
+  }
+
+  it should "not hold once the gap exceeds the aggregate limit" in {
+    val later = new Timestamp(1_700_000_000_000L)
+    val earlier = new Timestamp(later.getTime - TimeUnit.DAYS.toMillis(30))
+    (WorkflowVersionResource invokePrivate isWithinTimeLimit(later, earlier)) 
shouldBe false
+  }
+
+  "encodeVersionImportance" should "always mark the latest version important 
and aggregate close ones" in {
+    val base = 1_700_000_000_000L
+    def version(vid: Int, offsetMillis: Long, content: String): 
WorkflowVersion = {
+      val v = new WorkflowVersion
+      v.setVid(vid)
+      v.setWid(testWorkflowWid)
+      v.setContent(content)
+      v.setCreationTime(new Timestamp(base - offsetMillis))
+      v
+    }
+
+    val positional = 
"""[{"op":"replace","path":"/operatorPositions/op-1/x","value":5}]"""
+    val meaningful = 
"""[{"op":"replace","path":"/operators/0/properties","value":{}}]"""
+
+    val encoded = WorkflowVersionResource invokePrivate 
encodeVersionImportance(
+      List(
+        version(3, 0L, positional), // latest — important regardless of content
+        version(2, TimeUnit.SECONDS.toMillis(1), meaningful), // within the 
aggregate window
+        version(1, TimeUnit.DAYS.toMillis(30), meaningful) // outside it, 
judged on content
+      )
+    )
+
+    encoded.map(_.vId) shouldBe List(3, 2, 1)
+    encoded.map(_.importance) shouldBe List(true, false, true)
+  }
 }
diff --git 
a/amber/src/test/scala/org/apache/texera/web/service/WorkflowEmailNotifierSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/service/WorkflowEmailNotifierSpec.scala
index af21c7f687..aecc3dc3fb 100644
--- 
a/amber/src/test/scala/org/apache/texera/web/service/WorkflowEmailNotifierSpec.scala
+++ 
b/amber/src/test/scala/org/apache/texera/web/service/WorkflowEmailNotifierSpec.scala
@@ -25,6 +25,7 @@ import org.apache.texera.dao.MockTexeraDB
 import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
 import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
 import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.apache.texera.web.resource.EmailMessage
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.{BeforeAndAfterAll, PrivateMethodTester}
 
@@ -64,6 +65,7 @@ class WorkflowEmailNotifierSpec
   private val testWorkflowName: String = "notifier_wf_" + 
UUID.randomUUID().toString.substring(0, 8)
   private val userEmail = "[email protected]"
 
+  private val createEmailMessage = 
PrivateMethod[EmailMessage](Symbol("createEmailMessage"))
   private val createDashboardUrl = 
PrivateMethod[String](Symbol("createDashboardUrl"))
   private val createEmailSubject = 
PrivateMethod[String](Symbol("createEmailSubject"))
   private val createEmailContent = 
PrivateMethod[String](Symbol("createEmailContent"))
@@ -216,4 +218,35 @@ class WorkflowEmailNotifierSpec
     }
     assert(ex.getMessage.contains(unseededWid.toString))
   }
+
+  // ─── createEmailMessage 
────────────────────────────────────────────────────
+
+  "createEmailMessage" should "address the recipient and carry the subject and 
content for the state" in {
+    val notifier = notifierFor("http://texera.example.com:8080/dashboard";)
+    val message = notifier invokePrivate createEmailMessage(KILLED)
+
+    assert(message.receiver == userEmail)
+    // the assembled subject must match what the dedicated builder produces 
for the same state
+    assert(message.subject == (notifier invokePrivate 
createEmailSubject(KILLED)))
+    assert(message.content.contains(testWorkflowName))
+    assert(message.content.contains(testWid.toString))
+    assert(message.content.contains(KILLED.name))
+  }
+
+  // ─── sendStatusEmail 
───────────────────────────────────────────────────────
+
+  // Only the invalid-recipient arm is driven here: it returns before reaching
+  // GmailResource.sendEmail, so no SMTP call is made. The valid-address arm 
would dispatch
+  // for real, so it belongs to the integration tier.
+  "sendStatusEmail" should "return without dispatching when the recipient 
address is invalid" in {
+    val notifier =
+      new WorkflowEmailNotifier(
+        testWid.toLong,
+        "not-an-email",
+        new URI("http://texera.example.com";)
+      )
+
+    notifier.sendStatusEmail(COMPLETED) // must not throw and must not reach 
Gmail
+    assert(!(notifier invokePrivate isValidEmail("not-an-email")))
+  }
 }

Reply via email to