quickwin 1.3.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.en.md +18 -24
  2. package/README.md +17 -23
  3. package/examples/PdfCanvas.js +8 -37
  4. package/examples/PdfCanvas.tsx +11 -41
  5. package/examples/exec_server.js +20155 -1420
  6. package/examples/exec_server.tsx +489 -0
  7. package/examples/exec_server_worker.js +383 -33
  8. package/examples/exec_server_worker.ts +408 -30
  9. package/examples/pdf_preview2.js +25 -82
  10. package/examples/pdf_preview2.ts +27 -84
  11. package/examples/pdf_viewer.js +1424 -1180
  12. package/examples/pdf_viewer.tsx +16 -67
  13. package/examples/setres.js +22 -47
  14. package/examples/setres.ts +23 -57
  15. package/examples/test_gallery.js +1418 -1075
  16. package/examples/test_react_lazy.js +1391 -1059
  17. package/examples/test_react_listbox.js +1390 -1058
  18. package/examples/test_root_window.js +1384 -1052
  19. package/examples/test_tab.js +1391 -1059
  20. package/lib/abort.js +70 -0
  21. package/lib/abort.ts +69 -0
  22. package/lib/certs.js +219 -0
  23. package/lib/certs.ts +229 -0
  24. package/lib/exec-log.js +15 -0
  25. package/lib/exec-log.ts +30 -0
  26. package/lib/fetch.js +88 -7
  27. package/lib/fetch.ts +73 -7
  28. package/lib/ffi/bind.js +441 -0
  29. package/lib/ffi/bind.ts +539 -0
  30. package/lib/ffi/ctype.js +194 -0
  31. package/lib/ffi/ctype.ts +369 -0
  32. package/lib/ffi/index.js +6 -0
  33. package/lib/ffi/index.ts +6 -0
  34. package/lib/ffi/struct.js +490 -0
  35. package/lib/ffi/struct.ts +753 -0
  36. package/lib/http-common.js +43 -5
  37. package/lib/http-common.ts +48 -6
  38. package/lib/http-server.js +187 -29
  39. package/lib/http-server.ts +177 -26
  40. package/lib/react-qw/components/DateTimePicker.js +18 -37
  41. package/lib/react-qw/components/Input.js +3 -10
  42. package/lib/react-qw/components/Link.js +6 -19
  43. package/lib/react-qw/components/ListView.js +50 -154
  44. package/lib/react-qw/components/PathPicker.js +28 -88
  45. package/lib/react-qw/components/ScrollView.js +15 -8
  46. package/lib/react-qw/components/Slider.js +2 -1
  47. package/lib/react-qw/components/Tab.js +6 -22
  48. package/lib/react-qw/components/Tooltip.js +10 -21
  49. package/lib/react-qw/components/TreeView.js +8 -31
  50. package/lib/react-qw/index.js +5 -3
  51. package/lib/react-qw/index.ts +8 -3
  52. package/lib/react-qw/nmhdr.js +3 -24
  53. package/lib/react-qw/nmhdr.ts +5 -30
  54. package/lib/react-qw/props.js +7 -2
  55. package/lib/react-qw/props.ts +7 -3
  56. package/lib/react-qw/reconciler.js +23 -13
  57. package/lib/react-qw/reconciler.ts +22 -13
  58. package/lib/text-measure.js +20 -23
  59. package/lib/text-measure.ts +22 -26
  60. package/lib/websocket.js +44 -4
  61. package/lib/websocket.ts +40 -4
  62. package/lib/windows/advapi32.js +53 -0
  63. package/lib/windows/advapi32.ts +64 -0
  64. package/lib/windows/comctl32.js +18 -0
  65. package/lib/windows/comctl32.ts +19 -0
  66. package/lib/windows/comdlg32.js +8 -0
  67. package/lib/windows/comdlg32.ts +10 -0
  68. package/lib/windows/gdi32.js +392 -0
  69. package/lib/windows/gdi32.ts +420 -0
  70. package/lib/windows/kernel32.js +262 -0
  71. package/lib/windows/kernel32.ts +291 -0
  72. package/lib/windows/msimg32.js +38 -0
  73. package/lib/windows/msimg32.ts +45 -0
  74. package/lib/windows/ntdll.js +9 -0
  75. package/lib/windows/ntdll.ts +11 -0
  76. package/lib/windows/ole32.js +7 -0
  77. package/lib/windows/ole32.ts +9 -0
  78. package/lib/windows/psapi.js +32 -0
  79. package/lib/windows/psapi.ts +37 -0
  80. package/lib/windows/shell32.js +13 -0
  81. package/lib/windows/shell32.ts +16 -0
  82. package/lib/windows/structs.js +519 -0
  83. package/lib/windows/structs.ts +566 -0
  84. package/lib/windows/user32.js +572 -0
  85. package/lib/windows/user32.ts +607 -0
  86. package/lib/windows/version.js +29 -0
  87. package/lib/windows/version.ts +35 -0
  88. package/lib/windows/winspool.js +71 -0
  89. package/lib/windows/winspool.ts +80 -0
  90. package/package.json +1 -1
  91. package/quickwin.d.ts +31 -40
  92. package/quickwin_const.d.ts +137 -0
  93. package/qwin-nowasm-x86.exe +0 -0
  94. package/qwin-nowasm.exe +0 -0
  95. package/qwin-x86.exe +0 -0
  96. package/qwin.exe +0 -0
  97. package/test/bench_ffi_callraw.js +236 -0
  98. package/test/bench_ffi_callraw.ts +236 -0
  99. package/test/bench_ffi_overhead.js +139 -0
  100. package/test/bench_ffi_overhead.ts +141 -0
  101. package/test/bench_sendmessage.js +104 -0
  102. package/test/bench_sendmessage.ts +100 -0
  103. package/test/bench_sendmessage_nonnull.js +187 -0
  104. package/test/bench_sendmessage_nonnull.ts +188 -0
  105. package/test/run.js +19 -4
  106. package/test/run.ts +20 -5
  107. package/test/test_basic_fetch.js +29 -0
  108. package/test/test_basic_fetch.ts +26 -0
  109. package/test/test_ffi.js +27 -45
  110. package/test/test_ffi.ts +35 -54
  111. package/test/test_ffi_abi.js +290 -0
  112. package/test/test_ffi_abi.ts +274 -0
  113. package/test/test_ffi_array.js +116 -0
  114. package/test/test_ffi_array.ts +141 -0
  115. package/test/test_ffi_bind.js +620 -8
  116. package/test/test_ffi_bind.ts +630 -8
  117. package/test/test_ffi_struct.js +632 -88
  118. package/test/test_ffi_struct.ts +666 -91
  119. package/test/test_ffi_struct_layout.js +495 -0
  120. package/test/test_ffi_struct_layout.ts +543 -0
  121. package/test/test_ffi_user32.js +103 -0
  122. package/test/test_ffi_user32.ts +113 -0
  123. package/test/test_ffi_windows.js +101 -0
  124. package/test/test_ffi_windows.ts +113 -0
  125. package/test/test_files.js +102 -0
  126. package/test/test_files.ts +121 -0
  127. package/test/test_gdi_draw.js +681 -0
  128. package/test/test_gdi_draw.ts +725 -0
  129. package/test/test_helper.js +8 -1
  130. package/test/test_helper.ts +9 -1
  131. package/test/test_http_import.js +18 -0
  132. package/test/test_http_import.ts +19 -0
  133. package/test/test_http_server.js +73 -1
  134. package/test/test_http_server.ts +75 -1
  135. package/test/test_ipv6.js +2 -2
  136. package/test/test_ipv6.ts +2 -2
  137. package/test/test_net_fetch.js +74 -11
  138. package/test/test_net_fetch.ts +73 -10
  139. package/test/test_net_websocket.js +1 -1
  140. package/test/test_net_websocket.ts +1 -1
  141. package/test/test_printer.js +137 -0
  142. package/test/test_printer.ts +148 -0
  143. package/test/test_proc.js +141 -0
  144. package/test/test_proc.ts +163 -0
  145. package/test/test_react_complex.js +1401 -1069
  146. package/test/test_react_counter.js +1391 -1059
  147. package/test/test_react_flex.js +1411 -1072
  148. package/test/test_react_hidden.js +1394 -1062
  149. package/test/test_react_listview_link.js +1391 -1059
  150. package/test/test_react_notify.js +18638 -0
  151. package/test/test_react_render.js +1391 -1059
  152. package/test/test_registry.js +74 -0
  153. package/test/test_registry.ts +95 -0
  154. package/test/test_sysinfo.js +122 -0
  155. package/test/test_sysinfo.ts +141 -0
  156. package/test/test_wolfssl_read.js +45 -43
  157. package/test/test_wolfssl_read.ts +42 -40
  158. package/test/test_worker_net.js +35 -17
  159. package/test/test_worker_net.ts +27 -15
  160. package/tsconfig.json +1 -1
  161. package/examples/exec_server.ts +0 -80
  162. package/lib/ffi-bind.js +0 -85
  163. package/lib/ffi-bind.ts +0 -170
  164. package/lib/ffi-struct.js +0 -224
  165. package/lib/ffi-struct.ts +0 -239
  166. package/lib/win-common-structs.js +0 -39
  167. package/lib/win-common-structs.ts +0 -41
@@ -1,6 +1,7 @@
1
1
  import * as std from 'std';
2
2
  const GREEN = '\x1b[32m';
3
3
  const RED = '\x1b[31m';
4
+ const YELLOW = '\x1b[33m';
4
5
  const BOLD = '\x1b[1m';
5
6
  const RESET = '\x1b[0m';
6
7
  function formatDuration(ms) {
@@ -35,6 +36,7 @@ export function readWasmFile(path) {
35
36
  export class Tester {
36
37
  ok = 0;
37
38
  fail = 0;
39
+ skip = 0;
38
40
  startTime = Date.now();
39
41
  sectionStart = this.startTime;
40
42
  lastSection = '';
@@ -71,6 +73,10 @@ export class Tester {
71
73
  checkTrue(name, actual) {
72
74
  this.check(name, true, actual);
73
75
  }
76
+ skipCase(name) {
77
+ this.skip++;
78
+ std.printf(' %sSKIP:%s %s\n', YELLOW, RESET, name);
79
+ }
74
80
  summary() {
75
81
  const now = Date.now();
76
82
  if (this.lastSection) {
@@ -79,6 +85,7 @@ export class Tester {
79
85
  }
80
86
  const total = now - this.startTime;
81
87
  const color = this.fail > 0 ? RED : GREEN;
82
- std.printf('\n%s%d/%d passed (%s)%s\n', color, this.ok, this.ok + this.fail, formatDuration(total), RESET);
88
+ const skipped = this.skip > 0 ? ` (${this.skip} skipped)` : '';
89
+ std.printf('\n%s%d/%d passed%s (%s)%s\n', color, this.ok, this.ok + this.fail, skipped, formatDuration(total), RESET);
83
90
  }
84
91
  }
@@ -2,6 +2,7 @@ import * as std from 'std'
2
2
 
3
3
  const GREEN = '\x1b[32m'
4
4
  const RED = '\x1b[31m'
5
+ const YELLOW = '\x1b[33m'
5
6
  const BOLD = '\x1b[1m'
6
7
  const RESET = '\x1b[0m'
7
8
 
@@ -34,6 +35,7 @@ export function readWasmFile(path: string): ArrayBuffer | null {
34
35
  export class Tester {
35
36
  ok = 0
36
37
  fail = 0
38
+ skip = 0
37
39
  private startTime = Date.now()
38
40
  private sectionStart = this.startTime
39
41
  private lastSection = ''
@@ -67,6 +69,11 @@ export class Tester {
67
69
  this.check(name, true, actual)
68
70
  }
69
71
 
72
+ skipCase(name: string): void {
73
+ this.skip++
74
+ std.printf(' %sSKIP:%s %s\n', YELLOW, RESET, name)
75
+ }
76
+
70
77
  summary(): void {
71
78
  const now = Date.now()
72
79
  if (this.lastSection) {
@@ -75,6 +82,7 @@ export class Tester {
75
82
  }
76
83
  const total = now - this.startTime
77
84
  const color = this.fail > 0 ? RED : GREEN
78
- std.printf('\n%s%d/%d passed (%s)%s\n', color, this.ok, this.ok + this.fail, formatDuration(total), RESET)
85
+ const skipped = this.skip > 0 ? ` (${this.skip} skipped)` : ''
86
+ std.printf('\n%s%d/%d passed%s (%s)%s\n', color, this.ok, this.ok + this.fail, skipped, formatDuration(total), RESET)
79
87
  }
80
88
  }
@@ -60,6 +60,24 @@ export const suite = {
60
60
  return;
61
61
  assert('cache has body files', after.some(function (f) { return f.endsWith('.body'); }));
62
62
  assert('cache has meta files', after.some(function (f) { return f.endsWith('.meta'); }));
63
+ t.section('repeat https import (fresh url, forces TLS)');
64
+ // http_get_sync() short-circuits on a warm disk cache, so a second
65
+ // import of the same URL would never build a TLS connection. Use a
66
+ // distinct query string to force a real handshake into the
67
+ // process-wide CA store, then import once more to confirm repeated
68
+ // loadTrustedCerts() calls keep verifying.
69
+ const url2 = 'https://esm.sh/left-pad@1.3.0?twice=1';
70
+ let mod2;
71
+ try {
72
+ mod2 = await import(url2);
73
+ assert('repeat import succeeds', true);
74
+ assert('repeat default is function', typeof mod2.default === 'function');
75
+ assert('repeat left-pad works', mod2.default('hello', 8) === ' hello');
76
+ }
77
+ catch (e) {
78
+ assert('repeat import succeeds', false);
79
+ std.printf(' ERROR: %s\n', String(e));
80
+ }
63
81
  for (const f of after) {
64
82
  if (f.endsWith('.meta') && (err !== 0 || before.indexOf(f) < 0)) {
65
83
  std.printf(' meta: %s\n', f);
@@ -56,6 +56,25 @@ export const suite = {
56
56
  if (err2 !== 0) return
57
57
  assert('cache has body files', after.some(function(f: string) { return f.endsWith('.body') }))
58
58
  assert('cache has meta files', after.some(function(f: string) { return f.endsWith('.meta') }))
59
+
60
+ t.section('repeat https import (fresh url, forces TLS)')
61
+ // http_get_sync() short-circuits on a warm disk cache, so a second
62
+ // import of the same URL would never build a TLS connection. Use a
63
+ // distinct query string to force a real handshake into the
64
+ // process-wide CA store, then import once more to confirm repeated
65
+ // loadTrustedCerts() calls keep verifying.
66
+ const url2 = 'https://esm.sh/left-pad@1.3.0?twice=1'
67
+ let mod2: any
68
+ try {
69
+ mod2 = await import(url2)
70
+ assert('repeat import succeeds', true)
71
+ assert('repeat default is function', typeof mod2.default === 'function')
72
+ assert('repeat left-pad works', mod2.default('hello', 8) === ' hello')
73
+ } catch (e) {
74
+ assert('repeat import succeeds', false)
75
+ std.printf(' ERROR: %s\n', String(e))
76
+ }
77
+
59
78
  for (const f of after) {
60
79
  if (f.endsWith('.meta') && (err !== 0 || before.indexOf(f) < 0)) {
61
80
  std.printf(' meta: %s\n', f)
@@ -3,6 +3,7 @@ import * as std from 'std';
3
3
  import * as os from 'os';
4
4
  import * as sock from 'sock';
5
5
  import { createServer } from '../lib/http-server.js';
6
+ import '../lib/fetch.js';
6
7
  // ── Minimal raw HTTP client for testing ──
7
8
  function httpSend(host, port, raw, nResponses = 1) {
8
9
  return new Promise((resolve, reject) => {
@@ -82,6 +83,40 @@ export const suite = {
82
83
  else if (path === '/json') {
83
84
  return new Response(JSON.stringify({ method: req.method, q: Object.fromEntries(url.searchParams) }), { headers: { 'Content-Type': 'application/json' } });
84
85
  }
86
+ else if (path === '/stream') {
87
+ // length unknown: server must send chunked frames
88
+ return new Response(new ReadableStream({
89
+ start(control) {
90
+ control.enqueue(new TextEncoder().encode('chunkA/'));
91
+ control.enqueue(new TextEncoder().encode('chunkB'));
92
+ control.close();
93
+ }
94
+ }), { headers: { 'Content-Type': 'text/plain' } });
95
+ }
96
+ else if (path === '/file') {
97
+ // length known: server streams raw bytes with Content-Length
98
+ return new Response(new ReadableStream({
99
+ start(control) {
100
+ control.enqueue(new TextEncoder().encode('chunkA/'));
101
+ control.enqueue(new TextEncoder().encode('chunkB'));
102
+ control.close();
103
+ }
104
+ }), { headers: { 'Content-Type': 'text/plain', 'Content-Length': '13' } });
105
+ }
106
+ else if (path === '/stream-trailer') {
107
+ // chunked stream with trailing headers (Node addTrailers
108
+ // semantics: `Trailer` declaration + frames, then the footer)
109
+ return new Response(new ReadableStream({
110
+ start(control) {
111
+ control.enqueue(new TextEncoder().encode('chunkA/'));
112
+ control.enqueue(new TextEncoder().encode('chunkB'));
113
+ control.close();
114
+ }
115
+ }), {
116
+ headers: { 'Content-Type': 'text/plain', 'Trailer': 'X-Exit-Code' },
117
+ trailers: Promise.resolve({ 'X-Exit-Code': '3' })
118
+ });
119
+ }
85
120
  else {
86
121
  return new Response('not found', { status: 404 });
87
122
  }
@@ -101,6 +136,43 @@ export const suite = {
101
136
  t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
102
137
  t.checkTrue('body = hello world', r.endsWith('hello world'));
103
138
  t.checkTrue('content-type text', r.toLowerCase().indexOf('content-type: text/plain') >= 0);
139
+ t.checkTrue('static body gets content-length', r.toLowerCase().indexOf('content-length: 11') >= 0);
140
+ // ── streaming chunked response (length unknown) ──
141
+ t.section('streaming chunked response');
142
+ r = await httpSend('127.0.0.1', port, 'GET /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
143
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
144
+ t.checkTrue('transfer-encoding chunked', r.toLowerCase().indexOf('transfer-encoding: chunked') >= 0);
145
+ t.checkTrue('no content-length', r.toLowerCase().indexOf('content-length:') < 0);
146
+ t.checkTrue('chunk frames + terminator', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\n\r\n') >= 0);
147
+ // ── streaming raw response with known length ──
148
+ t.section('streaming known-length response');
149
+ r = await httpSend('127.0.0.1', port, 'GET /file HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
150
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
151
+ t.checkTrue('content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0);
152
+ t.checkTrue('no chunked encoding', r.toLowerCase().indexOf('transfer-encoding: chunked') < 0);
153
+ t.checkTrue('streamed raw body', r.includes('chunkA/chunkB'));
154
+ t.checkTrue('no chunk frame headers', r.indexOf('7\r\nchunkA/') < 0);
155
+ // ── HEAD of a streamed body: headers only, empty body ──
156
+ t.section('HEAD streaming response');
157
+ r = await httpSend('127.0.0.1', port, 'HEAD /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
158
+ t.checkTrue('HEAD 200', r.indexOf('HTTP/1.1 200') === 0);
159
+ t.checkTrue('HEAD content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0);
160
+ const headBodyIdx = r.indexOf('\r\n\r\n');
161
+ t.checkTrue('HEAD empty body', r.slice(headBodyIdx + 4).length === 0);
162
+ // ── streaming chunked response with trailing headers ──
163
+ t.section('streaming trailer response');
164
+ r = await httpSend('127.0.0.1', port, 'GET /stream-trailer HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n');
165
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0);
166
+ t.checkTrue('trailer declared in head', r.toLowerCase().indexOf('trailer: x-exit-code') >= 0);
167
+ t.checkTrue('terminator carries trailer', r.indexOf('0\r\nX-Exit-Code: 3\r\n\r\n') >= 0);
168
+ t.checkTrue('trailer footer after body', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\nX-Exit-Code: 3\r\n\r\n') >= 0);
169
+ // ── fetch client consumes chunked response with trailer footer ──
170
+ t.section('fetch consumes trailer response');
171
+ {
172
+ const rr = await fetch(`http://127.0.0.1:${port}/stream-trailer`);
173
+ t.checkTrue('status 200', rr.status === 200);
174
+ t.checkTrue('body decoded without trailer', await rr.text() === 'chunkA/chunkB');
175
+ }
104
176
  // ── POST with body ──
105
177
  t.section('POST with body');
106
178
  r = await httpSend('127.0.0.1', port, 'POST /echo HTTP/1.1\r\nHost: x\r\nContent-Length: 7\r\nConnection: close\r\n\r\npayload');
@@ -132,7 +204,7 @@ export const suite = {
132
204
  t.checkTrue('connection close on last', r.toLowerCase().indexOf('connection: close') >= 0);
133
205
  // ── server received expected requests ──
134
206
  t.section('request log');
135
- t.check('total requests', 7, requests.length);
207
+ t.check('total requests', 12, requests.length);
136
208
  t.checkTrue('saw GET /', requests.indexOf('GET /') >= 0);
137
209
  t.checkTrue('saw POST /echo', requests.indexOf('POST /echo') >= 0);
138
210
  server.close();
@@ -4,6 +4,7 @@ import * as os from 'os'
4
4
  import * as sock from 'sock'
5
5
  import { Tester } from './test_helper.js'
6
6
  import { createServer } from '../lib/http-server.js'
7
+ import '../lib/fetch.js'
7
8
 
8
9
  // ── Minimal raw HTTP client for testing ──
9
10
 
@@ -69,6 +70,37 @@ export const suite = {
69
70
  return new Response(req.method + ':' + await req.text(), { headers: { 'Content-Type': 'text/plain' } })
70
71
  } else if (path === '/json') {
71
72
  return new Response(JSON.stringify({ method: req.method, q: Object.fromEntries(url.searchParams) }), { headers: { 'Content-Type': 'application/json' } })
73
+ } else if (path === '/stream') {
74
+ // length unknown: server must send chunked frames
75
+ return new Response(new ReadableStream({
76
+ start(control) {
77
+ control.enqueue(new TextEncoder().encode('chunkA/'))
78
+ control.enqueue(new TextEncoder().encode('chunkB'))
79
+ control.close()
80
+ }
81
+ }), { headers: { 'Content-Type': 'text/plain' } })
82
+ } else if (path === '/file') {
83
+ // length known: server streams raw bytes with Content-Length
84
+ return new Response(new ReadableStream({
85
+ start(control) {
86
+ control.enqueue(new TextEncoder().encode('chunkA/'))
87
+ control.enqueue(new TextEncoder().encode('chunkB'))
88
+ control.close()
89
+ }
90
+ }), { headers: { 'Content-Type': 'text/plain', 'Content-Length': '13' } })
91
+ } else if (path === '/stream-trailer') {
92
+ // chunked stream with trailing headers (Node addTrailers
93
+ // semantics: `Trailer` declaration + frames, then the footer)
94
+ return new Response(new ReadableStream({
95
+ start(control) {
96
+ control.enqueue(new TextEncoder().encode('chunkA/'))
97
+ control.enqueue(new TextEncoder().encode('chunkB'))
98
+ control.close()
99
+ }
100
+ }), {
101
+ headers: { 'Content-Type': 'text/plain', 'Trailer': 'X-Exit-Code' },
102
+ trailers: Promise.resolve({ 'X-Exit-Code': '3' })
103
+ })
72
104
  } else {
73
105
  return new Response('not found', { status: 404 })
74
106
  }
@@ -87,6 +119,48 @@ export const suite = {
87
119
  t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
88
120
  t.checkTrue('body = hello world', r.endsWith('hello world'))
89
121
  t.checkTrue('content-type text', r.toLowerCase().indexOf('content-type: text/plain') >= 0)
122
+ t.checkTrue('static body gets content-length', r.toLowerCase().indexOf('content-length: 11') >= 0)
123
+
124
+ // ── streaming chunked response (length unknown) ──
125
+ t.section('streaming chunked response')
126
+ r = await httpSend('127.0.0.1', port, 'GET /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
127
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
128
+ t.checkTrue('transfer-encoding chunked', r.toLowerCase().indexOf('transfer-encoding: chunked') >= 0)
129
+ t.checkTrue('no content-length', r.toLowerCase().indexOf('content-length:') < 0)
130
+ t.checkTrue('chunk frames + terminator', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\n\r\n') >= 0)
131
+
132
+ // ── streaming raw response with known length ──
133
+ t.section('streaming known-length response')
134
+ r = await httpSend('127.0.0.1', port, 'GET /file HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
135
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
136
+ t.checkTrue('content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0)
137
+ t.checkTrue('no chunked encoding', r.toLowerCase().indexOf('transfer-encoding: chunked') < 0)
138
+ t.checkTrue('streamed raw body', r.includes('chunkA/chunkB'))
139
+ t.checkTrue('no chunk frame headers', r.indexOf('7\r\nchunkA/') < 0)
140
+
141
+ // ── HEAD of a streamed body: headers only, empty body ──
142
+ t.section('HEAD streaming response')
143
+ r = await httpSend('127.0.0.1', port, 'HEAD /stream HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
144
+ t.checkTrue('HEAD 200', r.indexOf('HTTP/1.1 200') === 0)
145
+ t.checkTrue('HEAD content-length 13', r.toLowerCase().indexOf('content-length: 13') >= 0)
146
+ const headBodyIdx = r.indexOf('\r\n\r\n')
147
+ t.checkTrue('HEAD empty body', r.slice(headBodyIdx + 4).length === 0)
148
+
149
+ // ── streaming chunked response with trailing headers ──
150
+ t.section('streaming trailer response')
151
+ r = await httpSend('127.0.0.1', port, 'GET /stream-trailer HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n')
152
+ t.checkTrue('200 status', r.indexOf('HTTP/1.1 200') === 0)
153
+ t.checkTrue('trailer declared in head', r.toLowerCase().indexOf('trailer: x-exit-code') >= 0)
154
+ t.checkTrue('terminator carries trailer', r.indexOf('0\r\nX-Exit-Code: 3\r\n\r\n') >= 0)
155
+ t.checkTrue('trailer footer after body', r.indexOf('7\r\nchunkA/\r\n6\r\nchunkB\r\n0\r\nX-Exit-Code: 3\r\n\r\n') >= 0)
156
+
157
+ // ── fetch client consumes chunked response with trailer footer ──
158
+ t.section('fetch consumes trailer response')
159
+ {
160
+ const rr = await fetch(`http://127.0.0.1:${port}/stream-trailer`)
161
+ t.checkTrue('status 200', rr.status === 200)
162
+ t.checkTrue('body decoded without trailer', await rr.text() === 'chunkA/chunkB')
163
+ }
90
164
 
91
165
  // ── POST with body ──
92
166
  t.section('POST with body')
@@ -124,7 +198,7 @@ export const suite = {
124
198
 
125
199
  // ── server received expected requests ──
126
200
  t.section('request log')
127
- t.check('total requests', 7, requests.length)
201
+ t.check('total requests', 12, requests.length)
128
202
  t.checkTrue('saw GET /', requests.indexOf('GET /') >= 0)
129
203
  t.checkTrue('saw POST /echo', requests.indexOf('POST /echo') >= 0)
130
204
 
package/test/test_ipv6.js CHANGED
@@ -79,7 +79,7 @@ export const suite = {
79
79
  // ── 4. fetch HTTPS IPv6 ──
80
80
  t.section('fetch https://[::1]');
81
81
  {
82
- const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000 });
82
+ const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000, rejectUnauthorized: false });
83
83
  assert('fetch IPv6 HTTPS status 200', r.status === 200);
84
84
  const body = await r.text();
85
85
  assert('fetch IPv6 HTTPS body', body === 'hello from test server');
@@ -110,7 +110,7 @@ export const suite = {
110
110
  // ── 6. WebSocket WSS IPv6 ──
111
111
  t.section('wss://[::1] echo');
112
112
  await new Promise((resolve, reject) => {
113
- const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`);
113
+ const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`, { rejectUnauthorized: false });
114
114
  const timeoutId = os.setTimeout(() => reject(new Error('WSS IPv6 timeout')), 10000);
115
115
  ws.onopen = () => {
116
116
  t.ok++;
package/test/test_ipv6.ts CHANGED
@@ -70,7 +70,7 @@ export const suite = {
70
70
  // ── 4. fetch HTTPS IPv6 ──
71
71
  t.section('fetch https://[::1]')
72
72
  {
73
- const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000 })
73
+ const r = await fetch(`https://[::1]:${HTTPS_PORT}/`, { timeout: 5000, rejectUnauthorized: false })
74
74
  assert('fetch IPv6 HTTPS status 200', r.status === 200)
75
75
  const body = await r.text()
76
76
  assert('fetch IPv6 HTTPS body', body === 'hello from test server')
@@ -104,7 +104,7 @@ export const suite = {
104
104
  // ── 6. WebSocket WSS IPv6 ──
105
105
  t.section('wss://[::1] echo')
106
106
  await new Promise<void>((resolve, reject) => {
107
- const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`)
107
+ const ws = new WebSocket(`wss://[::1]:${HTTPS_PORT}/`, { rejectUnauthorized: false })
108
108
  const timeoutId = os.setTimeout(() => reject(new Error('WSS IPv6 timeout')), 10000)
109
109
 
110
110
  ws.onopen = () => {
@@ -14,13 +14,24 @@ export const suite = {
14
14
  std.printf(' FAIL: %s\n', name);
15
15
  }
16
16
  }
17
+ // Retry: public-site reachability (baidu/jsdelivr/esm.sh) depends on
18
+ // CI network conditions, and a single dropped connection must not be
19
+ // reported as a certificate regression. The last error is printed so a
20
+ // real failure is still diagnosable.
17
21
  async function safeFetch(url, init) {
18
- try {
19
- return await fetch(url, init);
20
- }
21
- catch {
22
- return null;
22
+ let last = 'unknown';
23
+ for (let attempt = 0; attempt < 3; attempt++) {
24
+ try {
25
+ return await fetch(url, init);
26
+ }
27
+ catch (e) {
28
+ last = String(e?.message || e);
29
+ if (attempt < 2)
30
+ os.sleep(500);
31
+ }
23
32
  }
33
+ std.printf(' fetch %s failed after 3 attempts: %s\n', String(url), last);
34
+ return null;
24
35
  }
25
36
  const BASE = 'http://localhost:18923';
26
37
  // ── Plain HTTP GET (regression: slot 0 + !s bug) ──
@@ -247,7 +258,7 @@ export const suite = {
247
258
  }
248
259
  // ── HTTPS fetch ──
249
260
  t.section('HTTPS fetch');
250
- const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000 });
261
+ const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000, rejectUnauthorized: false });
251
262
  if (r11) {
252
263
  assert('https status 200', r11.status === 200);
253
264
  const body = await r11.text();
@@ -256,15 +267,67 @@ export const suite = {
256
267
  else {
257
268
  assert('https endpoint reachable', false);
258
269
  }
259
- // ── large HTTPS body (wolfSSL_read null regression) ──
260
- // Pre-fix: wolfSSL_read returned number -1 on WANT_READ →
261
- // receivedBytes became NaN → stream never closed → hang.
270
+ // ── GlobalSign R3 root chain (regression: ASN_SIG_CONFIRM_E -155) ──
271
+ // baidu.com and jsdelivr terminate in GlobalSign's 2018 re-keyed R3,
272
+ // which reuses the self-signed R3's subject-key-identifier with a
273
+ // different public key. Shipping the self-signed R3 in lib/certs.ts
274
+ // made wolfSSL match the wrong key and fail the handshake with -155.
275
+ // Both are public CDNs: a 500/429 or a dropped connection then counts
276
+ // as a retried attempt, not an immediate failure — only a persistent
277
+ // failure (cert regression or long-lived outage) should fail CI.
278
+ t.section('GlobalSign R3 root chain');
279
+ {
280
+ async function fetchR3(url) {
281
+ const init = { timeout: 10000 };
282
+ for (let attempt = 0; attempt < 3; attempt++) {
283
+ try {
284
+ const r = await fetch(url, init);
285
+ const body = await r.text();
286
+ if (r.status === 200 && body.length > 0)
287
+ return body;
288
+ std.printf(' fetch %s status=%s len=%d (attempt %d)\n', String(url), String(r.status), body.length, attempt);
289
+ }
290
+ catch (e) {
291
+ std.printf(' fetch %s error: %s (attempt %d)\n', String(url), String(e?.message || e), attempt);
292
+ }
293
+ os.sleep(1000);
294
+ }
295
+ return null;
296
+ }
297
+ const bb = await fetchR3('https://www.baidu.com/');
298
+ if (bb) {
299
+ assert('baidu (GlobalSign R3) status', true);
300
+ assert('baidu (GlobalSign R3) body non-empty', bb.length > 0);
301
+ }
302
+ else {
303
+ assert('baidu (GlobalSign R3) reachable', false);
304
+ }
305
+ const JSDELIVR_HOSTS = ['cdn.jsdelivr.net', 'fastly.jsdelivr.net', 'gcore.jsdelivr.net'];
306
+ for (const host of JSDELIVR_HOSTS) {
307
+ const bj = await fetchR3('https://' + host + '/npm/left-pad@1.3.0');
308
+ if (bj) {
309
+ assert('jsdelivr (GlobalSign R3) status', true);
310
+ assert('jsdelivr (GlobalSign R3) body non-empty', bj.length > 0);
311
+ break;
312
+ }
313
+ if (host !== JSDELIVR_HOSTS[JSDELIVR_HOSTS.length - 1]) {
314
+ std.printf(' jsdelivr host %s failed, trying next\n', host);
315
+ }
316
+ else {
317
+ assert('jsdelivr (GlobalSign R3) reachable', false);
318
+ }
319
+ }
320
+ }
321
+ // ── large HTTPS body (wolfSSL_read int contract) ──
322
+ // read returns the raw int; -1/WANT_READ must never leak into
323
+ // byte accounting (regression: a leaked -1 made receivedBytes
324
+ // NaN → stream never closed → hang).
262
325
  t.section('large HTTPS body');
263
326
  {
264
327
  const LARGE = 200000;
265
328
  try {
266
329
  const r = await Promise.race([
267
- fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000 }),
330
+ fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000, rejectUnauthorized: false }),
268
331
  new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body hang watchdog 15s')), 15000))
269
332
  ]);
270
333
  assert('large HTTPS status 200', r.status === 200);
@@ -304,7 +367,7 @@ export const suite = {
304
367
  let rejected = false;
305
368
  let msg = '';
306
369
  try {
307
- const r = await fetch('https://localhost:18924/stall', { timeout: 2000 });
370
+ const r = await fetch('https://localhost:18924/stall', { timeout: 2000, rejectUnauthorized: false });
308
371
  await Promise.race([
309
372
  r.arrayBuffer(),
310
373
  new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body timeout')), 8000))
@@ -11,12 +11,22 @@ export const suite = {
11
11
  else { t.fail++; std.printf(' FAIL: %s\n', name) }
12
12
  }
13
13
 
14
+ // Retry: public-site reachability (baidu/jsdelivr/esm.sh) depends on
15
+ // CI network conditions, and a single dropped connection must not be
16
+ // reported as a certificate regression. The last error is printed so a
17
+ // real failure is still diagnosable.
14
18
  async function safeFetch(url: any, init?: RequestInit) {
15
- try {
16
- return await fetch(url, init);
17
- } catch {
18
- return null;
19
+ let last = 'unknown'
20
+ for (let attempt = 0; attempt < 3; attempt++) {
21
+ try {
22
+ return await fetch(url, init)
23
+ } catch (e) {
24
+ last = String((e as Error)?.message || e)
25
+ if (attempt < 2) os.sleep(500)
26
+ }
19
27
  }
28
+ std.printf(' fetch %s failed after 3 attempts: %s\n', String(url), last)
29
+ return null
20
30
  }
21
31
  const BASE = 'http://localhost:18923'
22
32
 
@@ -227,7 +237,7 @@ export const suite = {
227
237
 
228
238
  // ── HTTPS fetch ──
229
239
  t.section('HTTPS fetch')
230
- const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000 })
240
+ const r11 = await safeFetch('https://localhost:18924/', { timeout: 5000, rejectUnauthorized: false })
231
241
  if (r11) {
232
242
  assert('https status 200', r11.status === 200)
233
243
  const body = await r11.text()
@@ -236,15 +246,68 @@ export const suite = {
236
246
  assert('https endpoint reachable', false)
237
247
  }
238
248
 
239
- // ── large HTTPS body (wolfSSL_read null regression) ──
240
- // Pre-fix: wolfSSL_read returned number -1 on WANT_READ →
241
- // receivedBytes became NaN → stream never closed → hang.
249
+ // ── GlobalSign R3 root chain (regression: ASN_SIG_CONFIRM_E -155) ──
250
+ // baidu.com and jsdelivr terminate in GlobalSign's 2018 re-keyed R3,
251
+ // which reuses the self-signed R3's subject-key-identifier with a
252
+ // different public key. Shipping the self-signed R3 in lib/certs.ts
253
+ // made wolfSSL match the wrong key and fail the handshake with -155.
254
+ // Both are public CDNs: a 500/429 or a dropped connection then counts
255
+ // as a retried attempt, not an immediate failure — only a persistent
256
+ // failure (cert regression or long-lived outage) should fail CI.
257
+ t.section('GlobalSign R3 root chain')
258
+ {
259
+ async function fetchR3(url: any): Promise<string | null> {
260
+ const init = { timeout: 10000 }
261
+ for (let attempt = 0; attempt < 3; attempt++) {
262
+ try {
263
+ const r = await fetch(url, init)
264
+ const body = await r.text()
265
+ if (r.status === 200 && body.length > 0) return body
266
+ std.printf(' fetch %s status=%s len=%d (attempt %d)\n',
267
+ String(url), String(r.status), body.length, attempt)
268
+ } catch (e) {
269
+ std.printf(' fetch %s error: %s (attempt %d)\n',
270
+ String(url), String((e as Error)?.message || e), attempt)
271
+ }
272
+ os.sleep(1000)
273
+ }
274
+ return null
275
+ }
276
+
277
+ const bb = await fetchR3('https://www.baidu.com/')
278
+ if (bb) {
279
+ assert('baidu (GlobalSign R3) status', true)
280
+ assert('baidu (GlobalSign R3) body non-empty', bb.length > 0)
281
+ } else {
282
+ assert('baidu (GlobalSign R3) reachable', false)
283
+ }
284
+
285
+ const JSDELIVR_HOSTS = ['cdn.jsdelivr.net', 'fastly.jsdelivr.net', 'gcore.jsdelivr.net']
286
+ for (const host of JSDELIVR_HOSTS) {
287
+ const bj = await fetchR3('https://' + host + '/npm/left-pad@1.3.0')
288
+ if (bj) {
289
+ assert('jsdelivr (GlobalSign R3) status', true)
290
+ assert('jsdelivr (GlobalSign R3) body non-empty', bj.length > 0)
291
+ break
292
+ }
293
+ if (host !== JSDELIVR_HOSTS[JSDELIVR_HOSTS.length - 1]) {
294
+ std.printf(' jsdelivr host %s failed, trying next\n', host)
295
+ } else {
296
+ assert('jsdelivr (GlobalSign R3) reachable', false)
297
+ }
298
+ }
299
+ }
300
+
301
+ // ── large HTTPS body (wolfSSL_read int contract) ──
302
+ // read returns the raw int; -1/WANT_READ must never leak into
303
+ // byte accounting (regression: a leaked -1 made receivedBytes
304
+ // NaN → stream never closed → hang).
242
305
  t.section('large HTTPS body')
243
306
  {
244
307
  const LARGE = 200000
245
308
  try {
246
309
  const r = await Promise.race([
247
- fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000 }),
310
+ fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000, rejectUnauthorized: false }),
248
311
  new Promise<never>((_, rej) =>
249
312
  os.setTimeout(() => rej(new Error('Body hang watchdog 15s')), 15000)
250
313
  )
@@ -292,7 +355,7 @@ export const suite = {
292
355
  let rejected = false
293
356
  let msg = ''
294
357
  try {
295
- const r = await fetch('https://localhost:18924/stall', { timeout: 2000 })
358
+ const r = await fetch('https://localhost:18924/stall', { timeout: 2000, rejectUnauthorized: false })
296
359
  await Promise.race([
297
360
  r.arrayBuffer(),
298
361
  new Promise<never>((_, rej) =>
@@ -158,7 +158,7 @@ export const suite = {
158
158
  t.section('wss basic text message');
159
159
  const p7 = waitForTest();
160
160
  let opened7 = false, received7 = false, closed7 = false;
161
- const ws7 = new WebSocket("wss://localhost:18924");
161
+ const ws7 = new WebSocket("wss://localhost:18924", { rejectUnauthorized: false });
162
162
  ws7.onopen = () => { opened7 = true; ws7.send("Hello WSS!"); };
163
163
  ws7.onmessage = () => { received7 = true; ws7.close(1000, "done"); };
164
164
  ws7.onclose = (e) => {
@@ -145,7 +145,7 @@ export const suite = {
145
145
  t.section('wss basic text message')
146
146
  const p7 = waitForTest()
147
147
  let opened7 = false, received7 = false, closed7 = false
148
- const ws7 = new WebSocket("wss://localhost:18924")
148
+ const ws7 = new WebSocket("wss://localhost:18924", { rejectUnauthorized: false })
149
149
  ws7.onopen = () => { opened7 = true; ws7.send("Hello WSS!") }
150
150
  ws7.onmessage = () => { received7 = true; ws7.close(1000, "done") }
151
151
  ws7.onclose = (e) => {