From c30e225d4216f440d69539b1729f42f1064f6e75 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Mon, 8 Jun 2026 12:48:07 -0400 Subject: [PATCH 1/5] no-issue: Fix eagerly set inputBuilder from/schema to avoid validation errors in runtime Signed-off-by: Ricardo Zanini --- .../fluent/spec/InputBuilder.java | 16 +- .../fluent/spec/InputBuilderTest.java | 253 ++++++++++++++++++ impl/test/db-samples/running.db | Bin 24576 -> 0 bytes impl/test/db-samples/running_v1.db | Bin 24576 -> 0 bytes impl/test/db-samples/suspended.db | Bin 16384 -> 0 bytes impl/test/db-samples/suspended_v1.db | Bin 16384 -> 0 bytes 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java delete mode 100644 impl/test/db-samples/running.db delete mode 100644 impl/test/db-samples/running_v1.db delete mode 100644 impl/test/db-samples/suspended.db delete mode 100644 impl/test/db-samples/suspended_v1.db diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java index 1161c82ab..97c372c48 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java @@ -29,26 +29,32 @@ public class InputBuilder { InputBuilder() { this.input = new Input(); - this.input.setFrom(new InputFrom()); - this.input.setSchema(new SchemaUnion()); } public InputBuilder from(String expr) { + if (this.input.getFrom() == null) this.input.setFrom(new InputFrom()); + else this.input.getFrom().setObject(null); + this.input.getFrom().setString(expr); return this; } public InputBuilder from(Object object) { + if (this.input.getFrom() == null) this.input.setFrom(new InputFrom()); + else this.input.getFrom().setString(null); + this.input.getFrom().setObject(object); return this; } public InputBuilder schema(Object schema) { + if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion()); this.input.getSchema().setSchemaInline(new SchemaInline(schema)); return this; } public InputBuilder schema(String schema) { + if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion()); this.input .getSchema() .setSchemaExternal( @@ -61,6 +67,12 @@ public InputBuilder schema(String schema) { return this; } + public InputBuilder schemaAsJsonString(String schema) { + if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion()); + this.input.getSchema().setSchemaInline(new SchemaInline(schema)); + return this; + } + public Input build() { return this.input; } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java new file mode 100644 index 000000000..a4460e04c --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java @@ -0,0 +1,253 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed 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 io.serverlessworkflow.fluent.spec; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import io.serverlessworkflow.api.types.Input; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Unit tests for InputBuilder to verify lazy initialization and proper field handling. */ +public class InputBuilderTest { + + @Test + void testEmptyInputBuilder() { + // When no methods are called, from and schema should be null + Input input = new InputBuilder().build(); + + assertNotNull(input, "Input should not be null"); + assertNull(input.getFrom(), "From should be null when not set"); + assertNull(input.getSchema(), "Schema should be null when not set"); + } + + @Test + void testFromStringOnly() { + // Setting only from(String) should not initialize schema + Input input = new InputBuilder().from("$.data").build(); + + assertNotNull(input.getFrom(), "From should be set"); + assertEquals("$.data", input.getFrom().getString(), "From string should match"); + assertNull(input.getFrom().getObject(), "From object should be null"); + assertNull(input.getSchema(), "Schema should be null when not set"); + } + + @Test + void testFromObjectOnly() { + // Setting only from(Object) should not initialize schema + Map data = Map.of("key", "value"); + Input input = new InputBuilder().from(data).build(); + + assertNotNull(input.getFrom(), "From should be set"); + assertNotNull(input.getFrom().getObject(), "From object should be set"); + assertNull(input.getFrom().getString(), "From string should be null"); + assertEquals(data, input.getFrom().getObject(), "From object should match"); + assertNull(input.getSchema(), "Schema should be null when not set"); + } + + @Test + void testSchemaObjectOnly() { + // This is the key test case - setting only schema should not initialize from + Map schema = Map.of("type", "object", "properties", Map.of()); + Input input = new InputBuilder().schema(schema).build(); + + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set"); + assertNull(input.getFrom(), "From should be null when not set"); + } + + @Test + void testSchemaStringOnly() { + // Setting only schema(String) should not initialize from + String schemaUri = "http://example.com/schema.json"; + Input input = new InputBuilder().schema(schemaUri).build(); + + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaExternal(), "Schema external should be set"); + assertNull(input.getFrom(), "From should be null when not set"); + } + + @Test + void testSchemaAsJsonStringOnly() { + // Setting only schemaAsJsonString should not initialize from + String jsonSchema = "{\"type\":\"object\",\"properties\":{}}"; + Input input = new InputBuilder().schemaAsJsonString(jsonSchema).build(); + + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set"); + assertNull(input.getFrom(), "From should be null when not set"); + } + + @Test + void testFromStringThenObject() { + // Setting from(String) then from(Object) should clear the string + Map data = Map.of("foo", "bar"); + Input input = new InputBuilder().from("$.initial").from(data).build(); + + assertNotNull(input.getFrom(), "From should be set"); + assertNotNull(input.getFrom().getObject(), "From object should be set"); + assertNull(input.getFrom().getString(), "From string should be cleared"); + assertEquals(data, input.getFrom().getObject(), "From object should be the last set value"); + } + + @Test + void testFromObjectThenString() { + // Setting from(Object) then from(String) should clear the object + Map data = Map.of("foo", "bar"); + Input input = new InputBuilder().from(data).from("$.final").build(); + + assertNotNull(input.getFrom(), "From should be set"); + assertEquals( + "$.final", input.getFrom().getString(), "From string should be the last set value"); + assertNull(input.getFrom().getObject(), "From object should be cleared"); + } + + @Test + void testSchemaObjectThenString() { + // Setting schema(Object) then schema(String) sets external schema + // Note: SchemaUnion may keep both inline and external, last one set takes precedence + Map inlineSchema = Map.of("type", "object"); + String externalUri = "http://example.com/schema.json"; + Input input = new InputBuilder().schema(inlineSchema).schema(externalUri).build(); + + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaExternal(), "Schema external should be set"); + } + + @Test + void testSchemaStringThenObject() { + // Setting schema(String) then schema(Object) should replace external with inline + String externalUri = "http://example.com/schema.json"; + Map inlineSchema = Map.of("type", "object"); + Input input = new InputBuilder().schema(externalUri).schema(inlineSchema).build(); + + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set"); + // Note: SchemaUnion may keep both, but inline takes precedence in serialization + } + + @Test + void testBothFromAndSchema() { + // Setting both from and schema should work correctly + String fromExpr = "$.input"; + Map schema = Map.of("type", "object"); + Input input = new InputBuilder().from(fromExpr).schema(schema).build(); + + assertNotNull(input.getFrom(), "From should be set"); + assertEquals(fromExpr, input.getFrom().getString(), "From string should match"); + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set"); + } + + @Test + void testSchemaOnlyDoesNotCreateFrom() { + // Critical test: verifies the fix for the original issue + // When only schema is set, from should remain null to avoid validation errors + Map schema = + Map.of("type", "object", "properties", Map.of("name", Map.of("type", "string"))); + + Input input = new InputBuilder().schema(schema).build(); + + assertNull( + input.getFrom(), + "From must be null when only schema is set (this was the bug being fixed)"); + assertNotNull(input.getSchema(), "Schema should be set"); + } + + @Test + void testMultipleFromCalls() { + // Multiple calls to from() should keep updating correctly + Input input = + new InputBuilder().from("$.first").from(Map.of("second", true)).from("$.third").build(); + + assertNotNull(input.getFrom(), "From should be set"); + assertEquals("$.third", input.getFrom().getString(), "From should be the last value set"); + assertNull(input.getFrom().getObject(), "From object should be null"); + } + + @Test + void testMultipleSchemaCalls() { + // Multiple calls to schema methods should keep updating correctly + Input input = + new InputBuilder() + .schema(Map.of("type", "string")) + .schema("http://example.com/schema") + .schemaAsJsonString("{\"type\":\"number\"}") + .build(); + + assertNotNull(input.getSchema(), "Schema should be set"); + assertNotNull(input.getSchema().getSchemaInline(), "Last schema call was inline"); + } + + @Test + void testSchemaObjectInitializesLazily() { + // Verify that SchemaUnion is only created when schema() is called + InputBuilder builder = new InputBuilder(); + Input input1 = builder.build(); + assertNull(input1.getSchema(), "Schema should be null before any schema() call"); + + InputBuilder builder2 = new InputBuilder(); + builder2.schema(Map.of("type", "object")); + Input input2 = builder2.build(); + assertNotNull(input2.getSchema(), "Schema should be initialized after schema() call"); + } + + @Test + void testFromObjectInitializesLazily() { + // Verify that InputFrom is only created when from() is called + InputBuilder builder = new InputBuilder(); + Input input1 = builder.build(); + assertNull(input1.getFrom(), "From should be null before any from() call"); + + InputBuilder builder2 = new InputBuilder(); + builder2.from("$.test"); + Input input2 = builder2.build(); + assertNotNull(input2.getFrom(), "From should be initialized after from() call"); + } + + @Test + void testSchemaOnlyWithExplicitSetFromNull() { + // Simulates the original use case from AgenticFlow where users had to + // call setFrom(null) to avoid validation errors + // This test verifies that calling setFrom(null) after setting schema works + Map schema = Map.of("type", "object"); + Input input = new InputBuilder().schema(schema).build(); + + // Manually setting from to null (as users had to do before the fix) + input.setFrom(null); + + assertNull(input.getFrom(), "From should be null"); + assertNotNull(input.getSchema(), "Schema should still be set"); + } + + @Test + void testSchemaOnlyDoesNotRequireSetFromNull() { + // This test verifies the fix - when only schema is set, + // from is already null, so users don't need to call setFrom(null) + Map schema = Map.of("type", "object", "properties", Map.of()); + Input input = new InputBuilder().schema(schema).build(); + + // No need to call input.setFrom(null) anymore! + assertNull( + input.getFrom(), "From should be null without needing to explicitly call setFrom(null)"); + assertNotNull(input.getSchema(), "Schema should be set"); + + // Verify this would not cause "Both object and str are null" validation error + // (in the actual runtime, this input would be validated without errors) + } +} diff --git a/impl/test/db-samples/running.db b/impl/test/db-samples/running.db deleted file mode 100644 index 531b9ae9c90ff6740c2e3a88b0b3f6885172ec43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI4O>g5w7{|vceNnfRJ%EJZ0u@M564$SZZ3(eux3o*!ZJ||>5MniP#;se&4zbg1 zS*^4u4qS?W1L6oA_zIkn5C=X7ocIEqcsye}Nl~^zO;@S(_%LHTld=8tjOG7-p7ENa zmFq#|9_{#_Ge=bvrR=sDO<2QGZM)I%Y-ZNnvKO_RG;#E@7qG-VU~R`%sL`-YbC%$O z1dsp{Kmter2_OL^fCP{L5EHILdeu39%d&8?NA_P%_e$<5<9 ziP}s~PUJmtR;;l{gAUt0ZL!zhqb~etFNck$%9c<5*i=HQ@qsu7kC!6Sh(A!VGK78T3GxR-bu#!X-8lF}ZS(TNFAI1p{T^7&tvYoIIZAAOB%IkGRz0Uc1d0D3@yORFZ z8SXIUBX;T(&G488PTn}*;k8_Jy@UTRRV7W*B~vosc78JP>oa7MOvC-Avfr3WJ0n~a z&oq&lBCAU3tmvhdr?6~QgE*aR)o@)aTdl%gsQWP?Z$j^qZV|HaJLItKr@alf&*mYA z<*zS9jwupA0!RP}AOR$R1dsp{KmwO1(67I{g1RTpzM00vXV-aKc!_N&0Qcgx!kNhWx4sr z#nKdnY0|m=y7a?~)v>tJYzj$c#DG0CP%0Nvx5mZpKc!eAQ)HUAN?9F7^nNSlU5HO( zfpC_7D=YAi**3Se^upFk_icj3rDaXLn)_D+ovu#UiCJSg>0VN|t7fKGIda`j3mk}3 zN3}qTiw>jNrZS8azXei!TRcvF_~Y`A6|4aXAOR$R1dsp{Kmter2_S(nB+#$Fb5Wu7 z%(JJU|BWlw|9YQj68hiF#E~WRzsvQ1-BuN$|23m-)ojC53ll&WD$3+0z{t-SOn{P8 z`i4mJLzw{F3b2N=0&a2zzr4M@argG}?%l0>BO8iFZ=h#@{{MOd4!0Y6Zp%4aL3^UD zU}a+k3A6d%m*=U)j@Z)hpKjMxS~xxMDb`|I<$c!eT2nyy~YD z08&+8)r_iLXaGYP05CxSrU4jK8Oh*(UP&64d`4a!uUP;aPkRHs~1gK6>`?2J#tp{u7~LUK0~RC4|_WO zox9f>P>YV9qfBjrH0a05y*wTfG1&VXOi)Y)0voW0prZ8|0ynZ}ibgG(uN%y;^7Sh0 zhhG1fki{>>EQNb(VwS=oY^TdtnO7jTFhv4L00|%gB!C2v01`j~<4!>6|NIp_*Z)R$ O{GT$=|JIf1|9=6n=arfO diff --git a/impl/test/db-samples/running_v1.db b/impl/test/db-samples/running_v1.db deleted file mode 100644 index cc9a861055d1f49d842d03fcb759e43df0812e6f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI4O>f*p7{|vO@zz%Ucios+MjtUFWj| z7bJiLkN^@u0!RP}AOR$R1dsp{Kmtf$%n7Wc{vY!@gr|oDkN^@u0!RP}AOR$R1dsp{ zKmthM#R<3v?cm7K^F9wCZL)nHImLW~9XkfsAa6RQd@BrN$AqZ?n#P$~%Ew-VJ5;0j z4sS(XnCuwv<79R~9J)}j_|8a|WQM`FWP#*zWJWiwdb!T*8CS2Gb?TP$VQXJKplYKN z#bJx9@v*um&x+UfnBV5RCry51jk)me$^7ZU@SnO>J>+h@!eW*b9ij-F&X>xvGO3VD zZ)y6S^3J`Awyo{n`t%;%UDK$scc-$px;Z3Z#nabrT6C6mx*R4P(Kq^3xVA}zg_RHvRztq`;5D4G(bzPK*3u*rkn z77HS{htSxf55?A)M{yzIal!YZmW<$PLyp2wQwWcS9MXZ zBkOfe*DK08-Lk96uFi0W$sh3(r`sYs%y+VO`jM#Ryz4!Cf2E{Q#ZZch2{-eziGN-r zlVlohJyX5fRMIcuqC`ezR+L#u(Pw3cwRM-LtJKz5x@x+vm9CaxE7bjnkPo1vDK`n( z_ziOS?Ij%#et-7|O=(Hj8DpUM(Z zE}x?=g~lwuVvYI!`Fa%8XxzT|Vs!g_Ni4544IxR57_f)V)W=g7mF0UKUJh;bBVJpIdzaMf zs;TLf4_&w21l!@n(Jj#5vcpijs12jPZ-V}=$Y;n;zdz@*1b0IMNB{{S0VIF~kN^@u z0!U!I2@LA*OjOWboW0FJ|C=va{~H6MN$US%Dvm6v|6QT~tG2GCT`!V0j4vjVOO1;13OY}{U6+P%GXcVy$w=neFZ%0H_& z;0U{+@3x$>73foK1uGpm7si6}%fyyKrhCSMG#Kc`H5h(QZIATp9WeK%-}V3#q-%R5 zE*{M0f!>_i9+|HvCheF{vG=6w;m~eZKW&5brU22*`s=enGy`l9`P`t)rbF5w3*XBp z_OI8*`^k-GhXjxS5#xZNRe$rg1@*+ec8N1Kz-; z^zlNO#j3_2~{?9n*f9s{`|Nj72Qk7f) diff --git a/impl/test/db-samples/suspended.db b/impl/test/db-samples/suspended.db deleted file mode 100644 index 7f47c2833bda68b3a7ec055c4a35f451b1bd74b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI3&u`;I6vxNuwq3d6>vcld(ybEJ;9!2 z8&)eJPMko33le_^5=Rb*11C=0_!Iaacpj%|yC`Z`1gR3whk5qo+3)$|87tqX@iw*f zP9*$(JN%qe%QQ{hPdSr3pjNXP1l=ZgU0?5tG-i_4^lrqZ-{UE5njJgv>h21`4GACt zB!C2v01`j~NB{{S0VIF~kN^_6;RLo(|KIQ@gy|sxB!C2v01`j~NB{{S0VIF~kN^_+ z_XPajFzHiEk9Y#@fF1LUHuRXCQpY#J4ydcALP+Ys(g4%4EXUJj7{gKlM<4Mt3q^ij z!cX(X0aEzX>D1ny>y|Gt_=Z$TsYGr=^sd`w&D*}!ak{qe>LNWhdbSadGAUAS$W!B} z$chI?EE@8|@qoYcfcfz2xbnO+{AN7voN!-mG0Ae*Yq&ryw@K z(Z|kC!}6T_*4~5tkKFpo+7nZL8Aqjb@{bBo{wZGNc zB1HRR;ip^LDp`cFW!S2%sn$_#U9}C>y6@(R7_%W)CCmERlJffW4Q0fDCxOFWxM~`fVOGN=lPvLhcK2#u@RY->tQ?0)Aa=#EVW|XY zkpO)@0p&}lzODRJdcAz~J|ENRNFT8XCS5W9-&a?;3e4>DH?Pj3xYOB}&>EVp)wH_i zY7UHq43vg`PsH`#5R z_j=sVqkTch-QQp=-6Lf8E3k0f%a4C1|ns2_OL^fCP{L z5nUjcFOcF3JFoDg)Gl?lj0;cAyMUS}T1?wDPs2qY{wb^#tUrO3rN@9PB>2zj65J z@jfcbf7CMysXbc`&OuR9B}y+mhRK&GlY$PEnP%!?5To8jIjY@R_){axWLXVw!+HT( z`ki(*CDms!usr5+X)Fpy{6Lk-oN1XX@7sftneCjF%q9h~S(US4GR%0eAs`cng0J9)y3%@ diff --git a/impl/test/db-samples/suspended_v1.db b/impl/test/db-samples/suspended_v1.db deleted file mode 100644 index 135af823fe60c64cbf66e3a0e97080dc255debaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI3&vV;E6vua+=10>KIJ6hIFd3NX6j@oxvMlYT%@0Eg`Qc)d;l`|`b*xy{8fopu zbUMQ@960gw&V@5a?wt7pxbaW$7qD76cGHe0=`>7xcslHp)=zp*AMI-VeY4u7w$YA7 z&}&9tacU7lj3DJq@{n4-->KBv#A$P*Bho&Tv|@B(E`u&lY1^;3%yRfDf(-}&0U!Vb zfB+Bx0zd!=00AHX1b_e#c+Cmyg8qNapAe)61b_e#00KY&2mk>f00e*l5C8%|;N=Mf z-C@$BmJ#zrwF7p{GwK?Bc1oRqC_A7vBNakYM=cE?mgNx7kWpVP6|jvFPqRqm=Oz3! zUmPGsK-Fr2cdvBI7a072%BWOAE0*JRY8~dU1XkPW*g?$@>9N_h&HgBpBITw$HIIs{ zczDF(A#aTb{JjrZpne^%KkKZ1GY{J*Jdis~vfT9&E-IZb@fKdh=*HW``lQx;*sxsE zJRqOCw$s{n-OrD9$wRb;KhjU9`|tO+-$2z!m>EwMv6yGsBa!wxv3O)g{XuN@MaX0G zgay4!B<9D3-OoGK1xk;fsP;FEOG_%?JE7Ph8=+{5K9}7nIo{%GWd^U0cp_EAQ;e=} zAMQ8y>aF@t9{nYQ-+H{Bus&a>>nCg!@idZR{mD&)(VHp{Ya?KByd!e7DwI>o7M3qy zA`!aY{H)oiAME7$;lCGtzlLw3MK#P!TelV6I=Zdu*41t8{X7wKKJT(*Sub1CUSHhS zMhtk;N?DSrr>}--_JQt|Jd@>&%W@oLk|$*;$}Aa|iD{Xn93`1#Nx-u^7yBwtIn36y z<1h)uo;Wrwod7KopwA|ted*M%u5BNo%Msc=tUQ@i{eh_ zUjn<>#uZ$}HSDO7kX*aBh?dZ@8ludM!lnH6p~z@s7p7Vmw=gwqt?Jhc@wrbXyS5jw z$*vQ4-elLE_4<~VNBbI~JAbLMbRVI;@03-?-Te4Zgns%KA*A|${ZU!fb!$aiXn+6^ z00KY&2mk>f00e-*DRT)MJ=@i83kZs4~-B5v&xb_p}DZHx~ZE zXc;Z*;cZ$kphbVdXH!yt7NeHOoG*>V(TF#6nar7%$@0E^RWh@kvy$1QAU3aZHcW;Y z4>#rP)@Cz*QbaRE5DW*Z7%`?5q6Bg=|7_;lFDQ`jBXsxY;{Eo=+ADj{L0}*N1b_e# h00KY&2mk>f00a;^ufNY!fIa)p-e*exJO7*h{||bB=>Gr! From 85549ea45bf37dbdb08ae508abc5f79b8dff7ba9 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:57:09 -0400 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../fluent/spec/InputBuilder.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java index 97c372c48..afa742689 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java @@ -32,16 +32,32 @@ public class InputBuilder { } public InputBuilder from(String expr) { - if (this.input.getFrom() == null) this.input.setFrom(new InputFrom()); - else this.input.getFrom().setObject(null); + if (expr == null) { + this.input.setFrom(null); + return this; + } + + if (this.input.getFrom() == null) { + this.input.setFrom(new InputFrom()); + } else { + this.input.getFrom().setObject(null); + } this.input.getFrom().setString(expr); return this; } public InputBuilder from(Object object) { - if (this.input.getFrom() == null) this.input.setFrom(new InputFrom()); - else this.input.getFrom().setString(null); + if (object == null) { + this.input.setFrom(null); + return this; + } + + if (this.input.getFrom() == null) { + this.input.setFrom(new InputFrom()); + } else { + this.input.getFrom().setString(null); + } this.input.getFrom().setObject(object); return this; From 4a326c72ea579b3a0d611a49c11b11b465c36ba2 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:17:12 -0400 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../serverlessworkflow/fluent/spec/InputBuilderTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java index a4460e04c..ab4e68dfd 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java @@ -127,6 +127,9 @@ void testSchemaObjectThenString() { assertNotNull(input.getSchema(), "Schema should be set"); assertNotNull(input.getSchema().getSchemaExternal(), "Schema external should be set"); + assertNull( + input.getSchema().getSchemaInline(), + "Schema inline should be cleared when setting external schema"); } @Test @@ -138,7 +141,9 @@ void testSchemaStringThenObject() { assertNotNull(input.getSchema(), "Schema should be set"); assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set"); - // Note: SchemaUnion may keep both, but inline takes precedence in serialization + assertNull( + input.getSchema().getSchemaExternal(), + "Schema external should be cleared when setting inline schema"); } @Test From c0e3da8756c15a7f486969f4941120802103a835 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Mon, 8 Jun 2026 13:33:04 -0400 Subject: [PATCH 4/5] Fix co-pilot comments Signed-off-by: Ricardo Zanini --- .../fluent/spec/InputBuilder.java | 39 ++++++++++++-- .../fluent/spec/InputBuilderTest.java | 51 +++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java index afa742689..154291340 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java @@ -64,13 +64,35 @@ public InputBuilder from(Object object) { } public InputBuilder schema(Object schema) { - if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion()); + if (schema == null) { + this.input.setSchema(null); + return this; + } + + if (this.input.getSchema() == null) { + this.input.setSchema(new SchemaUnion()); + } else { + // Clear external schema when setting inline schema + this.input.getSchema().setSchemaExternal(null); + } + this.input.getSchema().setSchemaInline(new SchemaInline(schema)); return this; } public InputBuilder schema(String schema) { - if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion()); + if (schema == null) { + this.input.setSchema(null); + return this; + } + + if (this.input.getSchema() == null) { + this.input.setSchema(new SchemaUnion()); + } else { + // Clear inline schema when setting external schema + this.input.getSchema().setSchemaInline(null); + } + this.input .getSchema() .setSchemaExternal( @@ -84,7 +106,18 @@ public InputBuilder schema(String schema) { } public InputBuilder schemaAsJsonString(String schema) { - if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion()); + if (schema == null) { + this.input.setSchema(null); + return this; + } + + if (this.input.getSchema() == null) { + this.input.setSchema(new SchemaUnion()); + } else { + // Clear external schema when setting inline schema + this.input.getSchema().setSchemaExternal(null); + } + this.input.getSchema().setSchemaInline(new SchemaInline(schema)); return this; } diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java index ab4e68dfd..48378b642 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/InputBuilderTest.java @@ -119,8 +119,7 @@ void testFromObjectThenString() { @Test void testSchemaObjectThenString() { - // Setting schema(Object) then schema(String) sets external schema - // Note: SchemaUnion may keep both inline and external, last one set takes precedence + // Setting schema(Object) then schema(String) should clear inline and set external Map inlineSchema = Map.of("type", "object"); String externalUri = "http://example.com/schema.json"; Input input = new InputBuilder().schema(inlineSchema).schema(externalUri).build(); @@ -134,7 +133,7 @@ void testSchemaObjectThenString() { @Test void testSchemaStringThenObject() { - // Setting schema(String) then schema(Object) should replace external with inline + // Setting schema(String) then schema(Object) should clear external and set inline String externalUri = "http://example.com/schema.json"; Map inlineSchema = Map.of("type", "object"); Input input = new InputBuilder().schema(externalUri).schema(inlineSchema).build(); @@ -255,4 +254,50 @@ void testSchemaOnlyDoesNotRequireSetFromNull() { // Verify this would not cause "Both object and str are null" validation error // (in the actual runtime, this input would be validated without errors) } + + @Test + void testFromNullClearsFrom() { + // Passing null to from() should clear the from field + Input input = new InputBuilder().from("$.data").from((String) null).build(); + + assertNull(input.getFrom(), "From should be cleared when passing null"); + } + + @Test + void testFromObjectNullClearsFrom() { + // Passing null object to from() should clear the from field + Input input = new InputBuilder().from(Map.of("key", "value")).from((Object) null).build(); + + assertNull(input.getFrom(), "From should be cleared when passing null object"); + } + + @Test + void testSchemaNullClearsSchema() { + // Passing null to schema(Object) should clear the schema field + Input input = new InputBuilder().schema(Map.of("type", "object")).schema((Object) null).build(); + + assertNull(input.getSchema(), "Schema should be cleared when passing null"); + } + + @Test + void testSchemaStringNullClearsSchema() { + // Passing null to schema(String) should clear the schema field + Input input = + new InputBuilder().schema("http://example.com/schema").schema((String) null).build(); + + assertNull(input.getSchema(), "Schema should be cleared when passing null string"); + } + + @Test + void testSchemaAsJsonStringNullClearsSchema() { + // Passing null to schemaAsJsonString should clear the schema field + Input input = + new InputBuilder() + .schemaAsJsonString("{\"type\":\"object\"}") + .schemaAsJsonString(null) + .build(); + + assertNull( + input.getSchema(), "Schema should be cleared when passing null to schemaAsJsonString"); + } } From 1384d93c7cae2da30385b25f61fbcb8338cc6215 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 9 Jun 2026 10:21:26 -0400 Subject: [PATCH 5/5] Restore db-samples files to match main --- impl/test/db-samples/running.db | Bin 0 -> 24576 bytes impl/test/db-samples/running_v1.db | Bin 0 -> 24576 bytes impl/test/db-samples/suspended.db | Bin 0 -> 16384 bytes impl/test/db-samples/suspended_v1.db | Bin 0 -> 16384 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 impl/test/db-samples/running.db create mode 100644 impl/test/db-samples/running_v1.db create mode 100644 impl/test/db-samples/suspended.db create mode 100644 impl/test/db-samples/suspended_v1.db diff --git a/impl/test/db-samples/running.db b/impl/test/db-samples/running.db new file mode 100644 index 0000000000000000000000000000000000000000..531b9ae9c90ff6740c2e3a88b0b3f6885172ec43 GIT binary patch literal 24576 zcmeI4O>g5w7{|vceNnfRJ%EJZ0u@M564$SZZ3(eux3o*!ZJ||>5MniP#;se&4zbg1 zS*^4u4qS?W1L6oA_zIkn5C=X7ocIEqcsye}Nl~^zO;@S(_%LHTld=8tjOG7-p7ENa zmFq#|9_{#_Ge=bvrR=sDO<2QGZM)I%Y-ZNnvKO_RG;#E@7qG-VU~R`%sL`-YbC%$O z1dsp{Kmter2_OL^fCP{L5EHILdeu39%d&8?NA_P%_e$<5<9 ziP}s~PUJmtR;;l{gAUt0ZL!zhqb~etFNck$%9c<5*i=HQ@qsu7kC!6Sh(A!VGK78T3GxR-bu#!X-8lF}ZS(TNFAI1p{T^7&tvYoIIZAAOB%IkGRz0Uc1d0D3@yORFZ z8SXIUBX;T(&G488PTn}*;k8_Jy@UTRRV7W*B~vosc78JP>oa7MOvC-Avfr3WJ0n~a z&oq&lBCAU3tmvhdr?6~QgE*aR)o@)aTdl%gsQWP?Z$j^qZV|HaJLItKr@alf&*mYA z<*zS9jwupA0!RP}AOR$R1dsp{KmwO1(67I{g1RTpzM00vXV-aKc!_N&0Qcgx!kNhWx4sr z#nKdnY0|m=y7a?~)v>tJYzj$c#DG0CP%0Nvx5mZpKc!eAQ)HUAN?9F7^nNSlU5HO( zfpC_7D=YAi**3Se^upFk_icj3rDaXLn)_D+ovu#UiCJSg>0VN|t7fKGIda`j3mk}3 zN3}qTiw>jNrZS8azXei!TRcvF_~Y`A6|4aXAOR$R1dsp{Kmter2_S(nB+#$Fb5Wu7 z%(JJU|BWlw|9YQj68hiF#E~WRzsvQ1-BuN$|23m-)ojC53ll&WD$3+0z{t-SOn{P8 z`i4mJLzw{F3b2N=0&a2zzr4M@argG}?%l0>BO8iFZ=h#@{{MOd4!0Y6Zp%4aL3^UD zU}a+k3A6d%m*=U)j@Z)hpKjMxS~xxMDb`|I<$c!eT2nyy~YD z08&+8)r_iLXaGYP05CxSrU4jK8Oh*(UP&64d`4a!uUP;aPkRHs~1gK6>`?2J#tp{u7~LUK0~RC4|_WO zox9f>P>YV9qfBjrH0a05y*wTfG1&VXOi)Y)0voW0prZ8|0ynZ}ibgG(uN%y;^7Sh0 zhhG1fki{>>EQNb(VwS=oY^TdtnO7jTFhv4L00|%gB!C2v01`j~<4!>6|NIp_*Z)R$ O{GT$=|JIf1|9=6n=arfO literal 0 HcmV?d00001 diff --git a/impl/test/db-samples/running_v1.db b/impl/test/db-samples/running_v1.db new file mode 100644 index 0000000000000000000000000000000000000000..cc9a861055d1f49d842d03fcb759e43df0812e6f GIT binary patch literal 24576 zcmeI4O>f*p7{|vO@zz%Ucios+MjtUFWj| z7bJiLkN^@u0!RP}AOR$R1dsp{Kmtf$%n7Wc{vY!@gr|oDkN^@u0!RP}AOR$R1dsp{ zKmthM#R<3v?cm7K^F9wCZL)nHImLW~9XkfsAa6RQd@BrN$AqZ?n#P$~%Ew-VJ5;0j z4sS(XnCuwv<79R~9J)}j_|8a|WQM`FWP#*zWJWiwdb!T*8CS2Gb?TP$VQXJKplYKN z#bJx9@v*um&x+UfnBV5RCry51jk)me$^7ZU@SnO>J>+h@!eW*b9ij-F&X>xvGO3VD zZ)y6S^3J`Awyo{n`t%;%UDK$scc-$px;Z3Z#nabrT6C6mx*R4P(Kq^3xVA}zg_RHvRztq`;5D4G(bzPK*3u*rkn z77HS{htSxf55?A)M{yzIal!YZmW<$PLyp2wQwWcS9MXZ zBkOfe*DK08-Lk96uFi0W$sh3(r`sYs%y+VO`jM#Ryz4!Cf2E{Q#ZZch2{-eziGN-r zlVlohJyX5fRMIcuqC`ezR+L#u(Pw3cwRM-LtJKz5x@x+vm9CaxE7bjnkPo1vDK`n( z_ziOS?Ij%#et-7|O=(Hj8DpUM(Z zE}x?=g~lwuVvYI!`Fa%8XxzT|Vs!g_Ni4544IxR57_f)V)W=g7mF0UKUJh;bBVJpIdzaMf zs;TLf4_&w21l!@n(Jj#5vcpijs12jPZ-V}=$Y;n;zdz@*1b0IMNB{{S0VIF~kN^@u z0!U!I2@LA*OjOWboW0FJ|C=va{~H6MN$US%Dvm6v|6QT~tG2GCT`!V0j4vjVOO1;13OY}{U6+P%GXcVy$w=neFZ%0H_& z;0U{+@3x$>73foK1uGpm7si6}%fyyKrhCSMG#Kc`H5h(QZIATp9WeK%-}V3#q-%R5 zE*{M0f!>_i9+|HvCheF{vG=6w;m~eZKW&5brU22*`s=enGy`l9`P`t)rbF5w3*XBp z_OI8*`^k-GhXjxS5#xZNRe$rg1@*+ec8N1Kz-; z^zlNO#j3_2~{?9n*f9s{`|Nj72Qk7f) literal 0 HcmV?d00001 diff --git a/impl/test/db-samples/suspended.db b/impl/test/db-samples/suspended.db new file mode 100644 index 0000000000000000000000000000000000000000..7f47c2833bda68b3a7ec055c4a35f451b1bd74b1 GIT binary patch literal 16384 zcmeI3&u`;I6vxNuwq3d6>vcld(ybEJ;9!2 z8&)eJPMko33le_^5=Rb*11C=0_!Iaacpj%|yC`Z`1gR3whk5qo+3)$|87tqX@iw*f zP9*$(JN%qe%QQ{hPdSr3pjNXP1l=ZgU0?5tG-i_4^lrqZ-{UE5njJgv>h21`4GACt zB!C2v01`j~NB{{S0VIF~kN^_6;RLo(|KIQ@gy|sxB!C2v01`j~NB{{S0VIF~kN^_+ z_XPajFzHiEk9Y#@fF1LUHuRXCQpY#J4ydcALP+Ys(g4%4EXUJj7{gKlM<4Mt3q^ij z!cX(X0aEzX>D1ny>y|Gt_=Z$TsYGr=^sd`w&D*}!ak{qe>LNWhdbSadGAUAS$W!B} z$chI?EE@8|@qoYcfcfz2xbnO+{AN7voN!-mG0Ae*Yq&ryw@K z(Z|kC!}6T_*4~5tkKFpo+7nZL8Aqjb@{bBo{wZGNc zB1HRR;ip^LDp`cFW!S2%sn$_#U9}C>y6@(R7_%W)CCmERlJffW4Q0fDCxOFWxM~`fVOGN=lPvLhcK2#u@RY->tQ?0)Aa=#EVW|XY zkpO)@0p&}lzODRJdcAz~J|ENRNFT8XCS5W9-&a?;3e4>DH?Pj3xYOB}&>EVp)wH_i zY7UHq43vg`PsH`#5R z_j=sVqkTch-QQp=-6Lf8E3k0f%a4C1|ns2_OL^fCP{L z5nUjcFOcF3JFoDg)Gl?lj0;cAyMUS}T1?wDPs2qY{wb^#tUrO3rN@9PB>2zj65J z@jfcbf7CMysXbc`&OuR9B}y+mhRK&GlY$PEnP%!?5To8jIjY@R_){axWLXVw!+HT( z`ki(*CDms!usr5+X)Fpy{6Lk-oN1XX@7sftneCjF%q9h~S(US4GR%0eAs`cng0J9)y3%@ literal 0 HcmV?d00001 diff --git a/impl/test/db-samples/suspended_v1.db b/impl/test/db-samples/suspended_v1.db new file mode 100644 index 0000000000000000000000000000000000000000..135af823fe60c64cbf66e3a0e97080dc255debaa GIT binary patch literal 16384 zcmeI3&vV;E6vua+=10>KIJ6hIFd3NX6j@oxvMlYT%@0Eg`Qc)d;l`|`b*xy{8fopu zbUMQ@960gw&V@5a?wt7pxbaW$7qD76cGHe0=`>7xcslHp)=zp*AMI-VeY4u7w$YA7 z&}&9tacU7lj3DJq@{n4-->KBv#A$P*Bho&Tv|@B(E`u&lY1^;3%yRfDf(-}&0U!Vb zfB+Bx0zd!=00AHX1b_e#c+Cmyg8qNapAe)61b_e#00KY&2mk>f00e*l5C8%|;N=Mf z-C@$BmJ#zrwF7p{GwK?Bc1oRqC_A7vBNakYM=cE?mgNx7kWpVP6|jvFPqRqm=Oz3! zUmPGsK-Fr2cdvBI7a072%BWOAE0*JRY8~dU1XkPW*g?$@>9N_h&HgBpBITw$HIIs{ zczDF(A#aTb{JjrZpne^%KkKZ1GY{J*Jdis~vfT9&E-IZb@fKdh=*HW``lQx;*sxsE zJRqOCw$s{n-OrD9$wRb;KhjU9`|tO+-$2z!m>EwMv6yGsBa!wxv3O)g{XuN@MaX0G zgay4!B<9D3-OoGK1xk;fsP;FEOG_%?JE7Ph8=+{5K9}7nIo{%GWd^U0cp_EAQ;e=} zAMQ8y>aF@t9{nYQ-+H{Bus&a>>nCg!@idZR{mD&)(VHp{Ya?KByd!e7DwI>o7M3qy zA`!aY{H)oiAME7$;lCGtzlLw3MK#P!TelV6I=Zdu*41t8{X7wKKJT(*Sub1CUSHhS zMhtk;N?DSrr>}--_JQt|Jd@>&%W@oLk|$*;$}Aa|iD{Xn93`1#Nx-u^7yBwtIn36y z<1h)uo;Wrwod7KopwA|ted*M%u5BNo%Msc=tUQ@i{eh_ zUjn<>#uZ$}HSDO7kX*aBh?dZ@8ludM!lnH6p~z@s7p7Vmw=gwqt?Jhc@wrbXyS5jw z$*vQ4-elLE_4<~VNBbI~JAbLMbRVI;@03-?-Te4Zgns%KA*A|${ZU!fb!$aiXn+6^ z00KY&2mk>f00e-*DRT)MJ=@i83kZs4~-B5v&xb_p}DZHx~ZE zXc;Z*;cZ$kphbVdXH!yt7NeHOoG*>V(TF#6nar7%$@0E^RWh@kvy$1QAU3aZHcW;Y z4>#rP)@Cz*QbaRE5DW*Z7%`?5q6Bg=|7_;lFDQ`jBXsxY;{Eo=+ADj{L0}*N1b_e# h00KY&2mk>f00a;^ufNY!fIa)p-e*exJO7*h{||bB=>Gr! literal 0 HcmV?d00001