1919
2020import { describe , expect , test } from "bun:test" ;
2121import { formatOperatorResult } from "./result-formatting" ;
22- import { WorkflowState } from "../workflow-state" ;
23- import type { OperatorInfo } from "../../types/execution" ;
24- import type { OperatorPredicate , OperatorLink , PortDescription } from "../../types/workflow" ;
25-
26- function makeOpInfo ( overrides : Partial < OperatorInfo > = { } ) : OperatorInfo {
27- return {
28- state : "completed" ,
29- inputTuples : 0 ,
30- outputTuples : 0 ,
31- resultMode : "table" ,
32- ...overrides ,
33- } ;
22+ import type { OperatorExecutionSummary , OperatorState , SampleRow } from "../../types/execution" ;
23+
24+ // Convert flat test rows (with an optional embedded __row_index__) into the
25+ // structured SampleRow[] the summary now carries.
26+ function toSampleRows ( rows : Record < string , any > [ ] ) : SampleRow [ ] {
27+ return rows . map ( ( row , i ) => {
28+ const { __row_index__, ...tuple } = row ;
29+ return { rowIndex : typeof __row_index__ === "number" ? __row_index__ : i , tuple } ;
30+ } ) ;
3431}
3532
36- function makeOperator ( id : string , inputPortIDs : string [ ] = [ ] ) : OperatorPredicate {
37- const inputPorts : PortDescription [ ] = inputPortIDs . map ( ( portID , i ) => ( {
38- portID,
39- displayName : `Input ${ i } ` ,
40- } ) ) ;
41- return {
42- operatorID : id ,
43- operatorType : "TestOp" ,
44- operatorVersion : "1.0" ,
45- operatorProperties : { } ,
46- inputPorts,
47- outputPorts : [ { portID : "output-0" , displayName : "Output 0" } ] ,
48- showAdvanced : false ,
49- } ;
33+ // Test convenience: accept the (old) flat fields and assemble the structured
34+ // OperatorExecutionSummary, so the cases below stay terse.
35+ interface OpInfoOverrides {
36+ state ?: OperatorState ;
37+ error ?: string ;
38+ outputTuples ?: number ;
39+ totalRowCount ?: number ;
40+ warnings ?: string [ ] ;
41+ result ?: Record < string , any > [ ] ;
5042}
5143
52- function makeLink ( linkID : string , source : [ string , string ] , target : [ string , string ] ) : OperatorLink {
53- return {
54- linkID,
55- source : { operatorID : source [ 0 ] , portID : source [ 1 ] } ,
56- target : { operatorID : target [ 0 ] , portID : target [ 1 ] } ,
44+ function makeOpInfo ( overrides : OpInfoOverrides = { } ) : OperatorExecutionSummary {
45+ const summary : OperatorExecutionSummary = {
46+ state : overrides . state ?? "Completed" ,
47+ errorMessages : overrides . error ? [ { type : "EXECUTION_FAILURE" , message : overrides . error } ] : [ ] ,
5748 } ;
49+ // The result summary is present only when the operator produced a result.
50+ if ( overrides . result !== undefined ) {
51+ summary . resultSummary = {
52+ resultMode : "table" ,
53+ // Non-arrays are passed through to exercise the "(no result data)" guard.
54+ sampleTuples : Array . isArray ( overrides . result ) ? toSampleRows ( overrides . result ) : ( overrides . result as any ) ,
55+ totalRowCount : overrides . totalRowCount ?? overrides . outputTuples ?? 0 ,
56+ } ;
57+ }
58+ if ( overrides . warnings ) {
59+ // Warnings are derived from console messages whose title is "WARNING: ...".
60+ summary . consoleLogsSummary = {
61+ messages : overrides . warnings . map ( w => ( { msgType : "PRINT" , title : w , message : "" } ) ) ,
62+ } ;
63+ }
64+ return summary ;
5865}
5966
60- const EMPTY_STATE = new WorkflowState ( ) ;
61-
6267describe ( "formatOperatorResult - early returns" , ( ) => {
6368 test ( "returns [ERROR] prefix when error field is set" , ( ) => {
64- const out = formatOperatorResult ( "op1" , makeOpInfo ( { error : "boom" } ) , EMPTY_STATE ) ;
69+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { error : "boom" } ) ) ;
6570 expect ( out ) . toBe ( "[ERROR] boom" ) ;
6671 } ) ;
6772
6873 test ( "treats empty-string error as falsy and continues to result path" , ( ) => {
69- const out = formatOperatorResult ( "op1" , makeOpInfo ( { error : "" } ) , EMPTY_STATE ) ;
74+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { error : "" } ) ) ;
7075 expect ( out ) . not . toContain ( "[ERROR]" ) ;
7176 expect ( out ) . toContain ( "(no result data)" ) ;
7277 } ) ;
7378
7479 test ( "returns (no result data) when result is undefined" , ( ) => {
75- const out = formatOperatorResult ( "op1" , makeOpInfo ( ) , EMPTY_STATE ) ;
80+ const out = formatOperatorResult ( "op1" , makeOpInfo ( ) ) ;
7681 expect ( out ) . toBe ( "(no result data)" ) ;
7782 } ) ;
7883
7984 test ( "returns (no result data) when result is not an array" , ( ) => {
80- const out = formatOperatorResult (
81- "op1" ,
82- makeOpInfo ( { result : { rows : [ ] } as unknown as Record < string , any > [ ] } ) ,
83- EMPTY_STATE
84- ) ;
85+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { result : { rows : [ ] } as unknown as Record < string , any > [ ] } ) ) ;
8586 expect ( out ) . toBe ( "(no result data)" ) ;
8687 } ) ;
8788
8889 test ( "empty array result emits brief summary plus zero-column shape only" , ( ) => {
89- const out = formatOperatorResult ( "op1" , makeOpInfo ( { result : [ ] , outputTuples : 0 } ) , EMPTY_STATE ) ;
90+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { result : [ ] , outputTuples : 0 } ) ) ;
9091 expect ( out . split ( "\n" ) ) . toEqual ( [ "Executed operator op1" , "Output table shape: (0, 0)" ] ) ;
9192 } ) ;
9293} ) ;
9394
9495describe ( "formatOperatorResult - table shape and metadata" , ( ) => {
9596 test ( "uses outputTuples for row count when totalRowCount missing" , ( ) => {
96- const out = formatOperatorResult ( "op1" , makeOpInfo ( { outputTuples : 7 , result : [ { a : 1 , b : 2 } ] } ) , EMPTY_STATE ) ;
97+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { outputTuples : 7 , result : [ { a : 1 , b : 2 } ] } ) ) ;
9798 expect ( out ) . toContain ( "Output table shape: (7, 2)" ) ;
9899 } ) ;
99100
100101 test ( "totalRowCount overrides outputTuples in output shape" , ( ) => {
101102 const out = formatOperatorResult (
102103 "op1" ,
103- makeOpInfo ( { outputTuples : 7 , totalRowCount : 999 , result : [ { a : 1 , b : 2 } ] } ) ,
104- EMPTY_STATE
104+ makeOpInfo ( { outputTuples : 7 , totalRowCount : 999 , result : [ { a : 1 , b : 2 } ] } )
105105 ) ;
106106 expect ( out ) . toContain ( "Output table shape: (999, 2)" ) ;
107107 } ) ;
@@ -112,8 +112,7 @@ describe("formatOperatorResult - table shape and metadata", () => {
112112 makeOpInfo ( {
113113 outputTuples : 1 ,
114114 result : [ { __is_visualization__ : true , "html-content" : "<x/>" } ] ,
115- } ) ,
116- EMPTY_STATE
115+ } )
117116 ) ;
118117 // 1 visible column ("html-content") since __is_visualization__ is filtered.
119118 expect ( out ) . toContain ( "Output table shape: (1, 1)" ) ;
@@ -125,85 +124,14 @@ describe("formatOperatorResult - table shape and metadata", () => {
125124 makeOpInfo ( {
126125 outputTuples : 1 ,
127126 result : [ { a : 1 } ] ,
128- warnings : [ "truncated to 1 row" , "something else" ] ,
129- } ) ,
130- EMPTY_STATE
127+ warnings : [ "WARNING: truncated to 1 row" , "WARNING: something else" ] ,
128+ } )
131129 ) ;
132130 const lines = out . split ( "\n" ) ;
133131 expect ( lines [ 0 ] ) . toBe ( "Executed operator op1" ) ;
134132 expect ( lines [ 1 ] ) . toBe ( "Output table shape: (1, 1)" ) ;
135- expect ( lines [ 2 ] ) . toBe ( "truncated to 1 row" ) ;
136- expect ( lines [ 3 ] ) . toBe ( "something else" ) ;
137- } ) ;
138- } ) ;
139-
140- describe ( "formatOperatorResult - input port metadata" , ( ) => {
141- test ( "omits input metadata when inputPortShapes is missing" , ( ) => {
142- const out = formatOperatorResult ( "op1" , makeOpInfo ( { outputTuples : 1 , result : [ { a : 1 } ] } ) , EMPTY_STATE ) ;
143- expect ( out ) . not . toContain ( "Input operator" ) ;
144- } ) ;
145-
146- test ( "omits input metadata when inputPortShapes is empty" , ( ) => {
147- const out = formatOperatorResult (
148- "op1" ,
149- makeOpInfo ( { outputTuples : 1 , result : [ { a : 1 } ] , inputPortShapes : [ ] } ) ,
150- EMPTY_STATE
151- ) ;
152- expect ( out ) . not . toContain ( "Input operator" ) ;
153- } ) ;
154-
155- test ( "falls back to inputN placeholder when no upstream link matches the port" , ( ) => {
156- const out = formatOperatorResult (
157- "op1" ,
158- makeOpInfo ( {
159- outputTuples : 1 ,
160- result : [ { a : 1 } ] ,
161- inputPortShapes : [ { portIndex : 0 , rows : 5 , columns : 3 } ] ,
162- } ) ,
163- EMPTY_STATE
164- ) ;
165- expect ( out ) . toContain ( "Input operator(table shape): input0(5, 3)" ) ;
166- } ) ;
167-
168- test ( "uses upstream operator id when an input link matches the port" , ( ) => {
169- const state = new WorkflowState ( ) ;
170- state . addOperator ( makeOperator ( "upstream" ) ) ;
171- state . addOperator ( makeOperator ( "op1" , [ "input-0" ] ) ) ;
172- state . addLink ( makeLink ( "l1" , [ "upstream" , "output-0" ] , [ "op1" , "input-0" ] ) ) ;
173-
174- const out = formatOperatorResult (
175- "op1" ,
176- makeOpInfo ( {
177- outputTuples : 4 ,
178- result : [ { a : 1 , b : 2 } ] ,
179- inputPortShapes : [ { portIndex : 0 , rows : 10 , columns : 2 } ] ,
180- } ) ,
181- state
182- ) ;
183- expect ( out ) . toContain ( "Input operator(table shape): upstream(10, 2)" ) ;
184- } ) ;
185-
186- test ( "sorts multiple input ports by portIndex regardless of input order" , ( ) => {
187- const state = new WorkflowState ( ) ;
188- state . addOperator ( makeOperator ( "up0" ) ) ;
189- state . addOperator ( makeOperator ( "up1" ) ) ;
190- state . addOperator ( makeOperator ( "op1" , [ "input-0" , "input-1" ] ) ) ;
191- state . addLink ( makeLink ( "l0" , [ "up0" , "output-0" ] , [ "op1" , "input-0" ] ) ) ;
192- state . addLink ( makeLink ( "l1" , [ "up1" , "output-0" ] , [ "op1" , "input-1" ] ) ) ;
193-
194- const out = formatOperatorResult (
195- "op1" ,
196- makeOpInfo ( {
197- outputTuples : 1 ,
198- result : [ { a : 1 } ] ,
199- inputPortShapes : [
200- { portIndex : 1 , rows : 2 , columns : 2 } ,
201- { portIndex : 0 , rows : 1 , columns : 1 } ,
202- ] ,
203- } ) ,
204- state
205- ) ;
206- expect ( out ) . toContain ( "Input operator(table shape): up0(1, 1), up1(2, 2)" ) ;
133+ expect ( lines [ 2 ] ) . toBe ( "WARNING: truncated to 1 row" ) ;
134+ expect ( lines [ 3 ] ) . toBe ( "WARNING: something else" ) ;
207135 } ) ;
208136} ) ;
209137
@@ -221,8 +149,7 @@ describe("formatOperatorResult - visualization rows", () => {
221149 label : "chart" ,
222150 } ,
223151 ] ,
224- } ) ,
225- EMPTY_STATE
152+ } )
226153 ) ;
227154 expect ( out ) . toContain ( "<skipped: visualization content>" ) ;
228155 expect ( out ) . not . toContain ( "<div>hidden</div>" ) ;
@@ -236,8 +163,7 @@ describe("formatOperatorResult - visualization rows", () => {
236163 makeOpInfo ( {
237164 outputTuples : 1 ,
238165 result : [ { __is_visualization__ : false , "html-content" : "<keep/>" } ] ,
239- } ) ,
240- EMPTY_STATE
166+ } )
241167 ) ;
242168 expect ( out ) . toContain ( "<keep/>" ) ;
243169 expect ( out ) . not . toContain ( "<skipped: visualization content>" ) ;
@@ -249,8 +175,7 @@ describe("formatOperatorResult - visualization rows", () => {
249175 makeOpInfo ( {
250176 outputTuples : 1 ,
251177 result : [ { __is_visualization__ : false , value : 1 } ] ,
252- } ) ,
253- EMPTY_STATE
178+ } )
254179 ) ;
255180 const lines = out . split ( "\n" ) ;
256181 expect ( out ) . toContain ( "Output table shape: (1, 1)" ) ;
@@ -262,8 +187,8 @@ describe("formatOperatorResult - visualization rows", () => {
262187} ) ;
263188
264189describe ( "jsonToTableFormat - cell coercion via formatOperatorResult" , ( ) => {
265- function tableLines ( opInfo : Partial < OperatorInfo > ) : string [ ] {
266- const out = formatOperatorResult ( "op1" , makeOpInfo ( { outputTuples : 1 , ...opInfo } ) , EMPTY_STATE ) ;
190+ function tableLines ( opInfo : OpInfoOverrides ) : string [ ] {
191+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { outputTuples : 1 , ...opInfo } ) ) ;
267192 // Skip brief summary + shape line.
268193 return out . split ( "\n" ) . slice ( 2 ) ;
269194 }
@@ -305,8 +230,7 @@ describe("jsonToTableFormat - row index gaps", () => {
305230 { __row_index__ : 0 , v : "a" } ,
306231 { __row_index__ : 5 , v : "b" } ,
307232 ] ,
308- } ) ,
309- EMPTY_STATE
233+ } )
310234 ) ;
311235 const lines = out . split ( "\n" ) ;
312236 // header, row0, gap marker, row5
@@ -325,18 +249,13 @@ describe("jsonToTableFormat - row index gaps", () => {
325249 { __row_index__ : 0 , v : "a" } ,
326250 { __row_index__ : 1 , v : "b" } ,
327251 ] ,
328- } ) ,
329- EMPTY_STATE
252+ } )
330253 ) ;
331254 expect ( out ) . not . toContain ( "...\t..." ) ;
332255 } ) ;
333256
334257 test ( "non-zero starting __row_index__ does not emit a leading gap marker" , ( ) => {
335- const out = formatOperatorResult (
336- "op1" ,
337- makeOpInfo ( { outputTuples : 1 , result : [ { __row_index__ : 9 , v : "z" } ] } ) ,
338- EMPTY_STATE
339- ) ;
258+ const out = formatOperatorResult ( "op1" , makeOpInfo ( { outputTuples : 1 , result : [ { __row_index__ : 9 , v : "z" } ] } ) ) ;
340259 expect ( out ) . not . toContain ( "...\t..." ) ;
341260 expect ( out . endsWith ( "9\tz" ) ) . toBe ( true ) ;
342261 } ) ;
0 commit comments