2929 // pyodide can't be imported as ESM — skip Python tests
3030}
3131
32+ /**
33+ * Find a line in the screen output that exactly matches the expected text.
34+ * Excludes lines containing the command echo (prompt line).
35+ */
36+ function findOutputLine ( screen : string , expected : string ) : string | undefined {
37+ return screen . split ( '\n' ) . find (
38+ ( l ) => l . trim ( ) === expected && ! l . includes ( PROMPT ) ,
39+ ) ;
40+ }
41+
3242// ---------------------------------------------------------------------------
3343// Node cross-runtime terminal tests
3444// ---------------------------------------------------------------------------
@@ -42,21 +52,58 @@ describe.skipIf(wasmSkip)('cross-runtime terminal: node', () => {
4252 await ctx ?. dispose ( ) ;
4353 } ) ;
4454
45- it ( 'node -e "console.log(42)" → 42 appears on screen ' , async ( ) => {
55+ it ( 'node -e stdout appears as actual output (not just command echo) ' , async ( ) => {
4656 ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
4757 harness = new TerminalHarness ( ctx . kernel ) ;
4858
4959 await harness . waitFor ( PROMPT ) ;
50- await harness . type ( 'node -e "console.log(42)"\n' ) ;
60+ // Use XYZZY — unique string that does NOT appear in the command text
61+ await harness . type ( 'node -e "console.log(\'XYZZY\')"\n' ) ;
5162 await harness . waitFor ( PROMPT , 2 , 10_000 ) ;
5263
5364 const screen = harness . screenshotTrimmed ( ) ;
54- expect ( screen ) . toContain ( '42' ) ;
65+ // Verify output on its own line (not just embedded in command echo)
66+ expect ( findOutputLine ( screen , 'XYZZY' ) ) . toBeDefined ( ) ;
5567 // Verify prompt returned
5668 const lines = screen . split ( '\n' ) ;
5769 expect ( lines [ lines . length - 1 ] ) . toBe ( PROMPT ) ;
5870 } , 15_000 ) ;
5971
72+ it ( 'node -e multiple console.log lines appear in order' , async ( ) => {
73+ ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
74+ harness = new TerminalHarness ( ctx . kernel ) ;
75+
76+ await harness . waitFor ( PROMPT ) ;
77+ await harness . type ( 'node -e "console.log(\'AAA\'); console.log(\'BBB\')"\n' ) ;
78+ await harness . waitFor ( PROMPT , 2 , 10_000 ) ;
79+
80+ const screen = harness . screenshotTrimmed ( ) ;
81+ expect ( findOutputLine ( screen , 'AAA' ) ) . toBeDefined ( ) ;
82+ expect ( findOutputLine ( screen , 'BBB' ) ) . toBeDefined ( ) ;
83+
84+ // Verify order: AAA before BBB
85+ const aaaIdx = screen . indexOf ( 'AAA' ) ;
86+ const bbbIdx = screen . indexOf ( 'BBB' ) ;
87+ // Both must appear after command echo
88+ const promptIdx = screen . indexOf ( PROMPT ) ;
89+ expect ( aaaIdx ) . toBeGreaterThan ( promptIdx ) ;
90+ expect ( bbbIdx ) . toBeGreaterThan ( aaaIdx ) ;
91+ } , 15_000 ) ;
92+
93+ it ( 'WARN message does not suppress real stdout' , async ( ) => {
94+ ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
95+ harness = new TerminalHarness ( ctx . kernel ) ;
96+
97+ await harness . waitFor ( PROMPT ) ;
98+ await harness . type ( 'node -e "console.log(\'HELLO\')"\n' ) ;
99+ await harness . waitFor ( PROMPT , 2 , 10_000 ) ;
100+
101+ const screen = harness . screenshotTrimmed ( ) ;
102+ // Both the WARN and actual output must coexist
103+ expect ( screen ) . toContain ( 'WARN' ) ;
104+ expect ( findOutputLine ( screen , 'HELLO' ) ) . toBeDefined ( ) ;
105+ } , 15_000 ) ;
106+
60107 it ( '^C during node -e — shell survives and prompt returns' , async ( ) => {
61108 ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
62109 harness = new TerminalHarness ( ctx . kernel ) ;
@@ -81,6 +128,47 @@ describe.skipIf(wasmSkip)('cross-runtime terminal: node', () => {
81128 } , 20_000 ) ;
82129} ) ;
83130
131+ // ---------------------------------------------------------------------------
132+ // Node kernel.exec() stdout tests
133+ // ---------------------------------------------------------------------------
134+
135+ describe . skipIf ( wasmSkip ) ( 'cross-runtime exec: node' , ( ) => {
136+ let ctx : IntegrationKernelResult ;
137+
138+ afterEach ( async ( ) => {
139+ await ctx ?. dispose ( ) ;
140+ } ) ;
141+
142+ it ( 'kernel.exec node -e stdout contains output' , async ( ) => {
143+ ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
144+ const result = await ctx . kernel . exec ( 'node -e "console.log(42)"' ) ;
145+ expect ( result . stdout ) . toContain ( '42' ) ;
146+ expect ( result . exitCode ) . toBe ( 0 ) ;
147+ } ) ;
148+
149+ it ( 'kernel.exec node -e multi-line stdout in order' , async ( ) => {
150+ ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
151+ const result = await ctx . kernel . exec (
152+ 'node -e "console.log(1); console.log(2)"' ,
153+ ) ;
154+ const lines = result . stdout . split ( '\n' ) . map ( ( l : string ) => l . trim ( ) ) . filter ( Boolean ) ;
155+ expect ( lines ) . toContain ( '1' ) ;
156+ expect ( lines ) . toContain ( '2' ) ;
157+ expect ( lines . indexOf ( '1' ) ) . toBeLessThan ( lines . indexOf ( '2' ) ) ;
158+ } ) ;
159+
160+ it ( 'kernel.exec node -e large stdout does not truncate' , async ( ) => {
161+ ctx = await createIntegrationKernel ( { runtimes : [ 'wasmvm' , 'node' ] } ) ;
162+ // Generate >64KB of output (100 lines of 700 chars each ≈ 70KB)
163+ const code = `for(let i=0;i<100;i++) console.log('L'+i+' '+'x'.repeat(700))` ;
164+ const result = await ctx . kernel . exec ( `node -e "${ code } "` ) ;
165+ // Verify first and last lines present
166+ expect ( result . stdout ) . toContain ( 'L0 ' ) ;
167+ expect ( result . stdout ) . toContain ( 'L99 ' ) ;
168+ expect ( result . exitCode ) . toBe ( 0 ) ;
169+ } , 15_000 ) ;
170+ } ) ;
171+
84172// ---------------------------------------------------------------------------
85173// Python cross-runtime terminal tests
86174// ---------------------------------------------------------------------------
0 commit comments