goscript 0.1.3 → 0.2.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 (330) hide show
  1. package/README.md +5 -2
  2. package/cmd/go_js_wasm_exec/main.go +201 -0
  3. package/cmd/go_js_wasm_exec/main_test.go +83 -0
  4. package/cmd/goscript/{cmd_compile.go → cmd-compile.go} +35 -8
  5. package/cmd/goscript/cmd-test.go +14 -0
  6. package/cmd/goscript/cmd-test_test.go +1 -1
  7. package/cmd/goscript/cmd_compile_test.go +105 -6
  8. package/compiler/build-flags.go +9 -10
  9. package/compiler/compile-request.go +12 -9
  10. package/compiler/compliance_test.go +0 -1
  11. package/compiler/config.go +2 -0
  12. package/compiler/gotest/request.go +28 -0
  13. package/compiler/gotest/runner.go +353 -27
  14. package/compiler/gotest/runner_test.go +400 -1
  15. package/compiler/gotest/testdata/browserapi/browserapi_test.go +20 -0
  16. package/compiler/gotest/testdata/browserapi/go.mod +3 -0
  17. package/compiler/lowered-program.go +24 -17
  18. package/compiler/lowering.go +988 -263
  19. package/compiler/lowering_bench_test.go +364 -0
  20. package/compiler/override-facts.go +15 -0
  21. package/compiler/override-parity-verifier.go +450 -0
  22. package/compiler/override-parity.go +122 -0
  23. package/compiler/override-registry_test.go +559 -0
  24. package/compiler/package-graph.go +61 -4
  25. package/compiler/package-graph_test.go +30 -0
  26. package/compiler/protobuf-ts-binding.go +514 -0
  27. package/compiler/protobuf-ts-binding_test.go +172 -0
  28. package/compiler/semantic-model-types.go +17 -4
  29. package/compiler/semantic-model.go +709 -72
  30. package/compiler/semantic-model_test.go +219 -0
  31. package/compiler/service.go +20 -1
  32. package/compiler/skeleton_test.go +1008 -20
  33. package/compiler/typescript-emitter.go +147 -15
  34. package/dist/gs/builtin/builtin.d.ts +2 -2
  35. package/dist/gs/builtin/builtin.js +20 -0
  36. package/dist/gs/builtin/builtin.js.map +1 -1
  37. package/dist/gs/builtin/slice.d.ts +2 -1
  38. package/dist/gs/builtin/slice.js +34 -4
  39. package/dist/gs/builtin/slice.js.map +1 -1
  40. package/dist/gs/builtin/type.d.ts +14 -6
  41. package/dist/gs/builtin/type.js +224 -64
  42. package/dist/gs/builtin/type.js.map +1 -1
  43. package/dist/gs/builtin/varRef.d.ts +11 -0
  44. package/dist/gs/builtin/varRef.js +57 -2
  45. package/dist/gs/builtin/varRef.js.map +1 -1
  46. package/dist/gs/bytes/buffer.gs.js +1 -1
  47. package/dist/gs/bytes/buffer.gs.js.map +1 -1
  48. package/dist/gs/bytes/reader.gs.js +1 -1
  49. package/dist/gs/bytes/reader.gs.js.map +1 -1
  50. package/dist/gs/compress/zlib/index.d.ts +13 -6
  51. package/dist/gs/compress/zlib/index.js +131 -35
  52. package/dist/gs/compress/zlib/index.js.map +1 -1
  53. package/dist/gs/crypto/sha1/index.js +2 -5
  54. package/dist/gs/crypto/sha1/index.js.map +1 -1
  55. package/dist/gs/crypto/sha256/index.js +2 -5
  56. package/dist/gs/crypto/sha256/index.js.map +1 -1
  57. package/dist/gs/crypto/sha512/index.js +2 -5
  58. package/dist/gs/crypto/sha512/index.js.map +1 -1
  59. package/dist/gs/embed/index.d.ts +6 -0
  60. package/dist/gs/embed/index.js +210 -5
  61. package/dist/gs/embed/index.js.map +1 -1
  62. package/dist/gs/encoding/json/index.d.ts +114 -0
  63. package/dist/gs/encoding/json/index.js +544 -36
  64. package/dist/gs/encoding/json/index.js.map +1 -1
  65. package/dist/gs/fmt/fmt.d.ts +3 -3
  66. package/dist/gs/fmt/fmt.js +29 -16
  67. package/dist/gs/fmt/fmt.js.map +1 -1
  68. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.d.ts +100 -0
  69. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js +564 -0
  70. package/dist/gs/github.com/aperturerobotics/protobuf-go-lite/index.js.map +1 -1
  71. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.d.ts +45 -0
  72. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js +229 -0
  73. package/dist/gs/github.com/go-git/go-billy/v6/osfs/index.js.map +1 -0
  74. package/dist/gs/github.com/pkg/errors/errors.js +54 -30
  75. package/dist/gs/github.com/pkg/errors/errors.js.map +1 -1
  76. package/dist/gs/go/scanner/index.d.ts +2 -0
  77. package/dist/gs/go/scanner/index.js +29 -5
  78. package/dist/gs/go/scanner/index.js.map +1 -1
  79. package/dist/gs/go/token/index.js +22 -6
  80. package/dist/gs/go/token/index.js.map +1 -1
  81. package/dist/gs/hash/index.d.ts +6 -0
  82. package/dist/gs/hash/index.js +20 -0
  83. package/dist/gs/hash/index.js.map +1 -1
  84. package/dist/gs/internal/goarch/index.d.ts +43 -3
  85. package/dist/gs/internal/goarch/index.js +42 -10
  86. package/dist/gs/internal/goarch/index.js.map +1 -1
  87. package/dist/gs/io/fs/fs.js +26 -14
  88. package/dist/gs/io/fs/fs.js.map +1 -1
  89. package/dist/gs/io/fs/readdir.js +8 -4
  90. package/dist/gs/io/fs/readdir.js.map +1 -1
  91. package/dist/gs/io/fs/sub.js +8 -1
  92. package/dist/gs/io/fs/sub.js.map +1 -1
  93. package/dist/gs/io/io.d.ts +12 -6
  94. package/dist/gs/io/io.js +87 -42
  95. package/dist/gs/io/io.js.map +1 -1
  96. package/dist/gs/math/bits/index.d.ts +31 -5
  97. package/dist/gs/math/bits/index.js +29 -28
  98. package/dist/gs/math/bits/index.js.map +1 -1
  99. package/dist/gs/mime/index.d.ts +16 -0
  100. package/dist/gs/mime/index.js +315 -6
  101. package/dist/gs/mime/index.js.map +1 -1
  102. package/dist/gs/net/http/httptest/index.d.ts +12 -0
  103. package/dist/gs/net/http/httptest/index.js +85 -6
  104. package/dist/gs/net/http/httptest/index.js.map +1 -1
  105. package/dist/gs/net/http/index.d.ts +303 -6
  106. package/dist/gs/net/http/index.js +1615 -58
  107. package/dist/gs/net/http/index.js.map +1 -1
  108. package/dist/gs/os/dir_unix.gs.js +1 -1
  109. package/dist/gs/os/dir_unix.gs.js.map +1 -1
  110. package/dist/gs/os/error.gs.js +1 -1
  111. package/dist/gs/os/error.gs.js.map +1 -1
  112. package/dist/gs/os/exec.gs.d.ts +1 -0
  113. package/dist/gs/os/exec.gs.js +4 -8
  114. package/dist/gs/os/exec.gs.js.map +1 -1
  115. package/dist/gs/os/exec_posix.gs.js +1 -1
  116. package/dist/gs/os/exec_posix.gs.js.map +1 -1
  117. package/dist/gs/os/index.d.ts +1 -1
  118. package/dist/gs/os/index.js +1 -1
  119. package/dist/gs/os/index.js.map +1 -1
  120. package/dist/gs/os/proc.gs.d.ts +4 -0
  121. package/dist/gs/os/proc.gs.js +12 -6
  122. package/dist/gs/os/proc.gs.js.map +1 -1
  123. package/dist/gs/os/root_js.gs.js +1 -1
  124. package/dist/gs/os/root_js.gs.js.map +1 -1
  125. package/dist/gs/os/types.gs.js +1 -1
  126. package/dist/gs/os/types.gs.js.map +1 -1
  127. package/dist/gs/os/types_js.gs.d.ts +6 -2
  128. package/dist/gs/os/types_js.gs.js +170 -9
  129. package/dist/gs/os/types_js.gs.js.map +1 -1
  130. package/dist/gs/os/types_unix.gs.js +1 -1
  131. package/dist/gs/os/types_unix.gs.js.map +1 -1
  132. package/dist/gs/path/path.js +11 -7
  133. package/dist/gs/path/path.js.map +1 -1
  134. package/dist/gs/reflect/index.d.ts +5 -4
  135. package/dist/gs/reflect/index.js +4 -3
  136. package/dist/gs/reflect/index.js.map +1 -1
  137. package/dist/gs/reflect/map.js +15 -0
  138. package/dist/gs/reflect/map.js.map +1 -1
  139. package/dist/gs/reflect/type.d.ts +26 -6
  140. package/dist/gs/reflect/type.js +1498 -279
  141. package/dist/gs/reflect/type.js.map +1 -1
  142. package/dist/gs/reflect/types.d.ts +14 -6
  143. package/dist/gs/reflect/types.js +35 -1
  144. package/dist/gs/reflect/types.js.map +1 -1
  145. package/dist/gs/reflect/value.d.ts +1 -0
  146. package/dist/gs/reflect/value.js +83 -41
  147. package/dist/gs/reflect/value.js.map +1 -1
  148. package/dist/gs/reflect/visiblefields.js +4 -140
  149. package/dist/gs/reflect/visiblefields.js.map +1 -1
  150. package/dist/gs/runtime/pprof/index.d.ts +8 -2
  151. package/dist/gs/runtime/pprof/index.js +50 -30
  152. package/dist/gs/runtime/pprof/index.js.map +1 -1
  153. package/dist/gs/runtime/runtime.js +5 -4
  154. package/dist/gs/runtime/runtime.js.map +1 -1
  155. package/dist/gs/runtime/trace/index.js +5 -19
  156. package/dist/gs/runtime/trace/index.js.map +1 -1
  157. package/dist/gs/strconv/atoi.gs.js +1 -1
  158. package/dist/gs/strconv/atoi.gs.js.map +1 -1
  159. package/dist/gs/strconv/complex.gs.d.ts +3 -0
  160. package/dist/gs/strconv/complex.gs.js +148 -0
  161. package/dist/gs/strconv/complex.gs.js.map +1 -0
  162. package/dist/gs/strconv/index.d.ts +1 -0
  163. package/dist/gs/strconv/index.js +1 -0
  164. package/dist/gs/strconv/index.js.map +1 -1
  165. package/dist/gs/strings/builder.js +1 -1
  166. package/dist/gs/strings/reader.d.ts +1 -1
  167. package/dist/gs/strings/reader.js +11 -7
  168. package/dist/gs/strings/reader.js.map +1 -1
  169. package/dist/gs/strings/replace.js +15 -7
  170. package/dist/gs/strings/replace.js.map +1 -1
  171. package/dist/gs/strings/strings.d.ts +5 -0
  172. package/dist/gs/strings/strings.js +57 -5
  173. package/dist/gs/strings/strings.js.map +1 -1
  174. package/dist/gs/sync/atomic/type.gs.js +9 -9
  175. package/dist/gs/sync/atomic/type.gs.js.map +1 -1
  176. package/dist/gs/sync/atomic/value.gs.js +2 -2
  177. package/dist/gs/sync/atomic/value.gs.js.map +1 -1
  178. package/dist/gs/sync/sync.d.ts +2 -1
  179. package/dist/gs/sync/sync.js +37 -16
  180. package/dist/gs/sync/sync.js.map +1 -1
  181. package/dist/gs/syscall/env.js +22 -14
  182. package/dist/gs/syscall/env.js.map +1 -1
  183. package/dist/gs/syscall/js/index.js +9 -0
  184. package/dist/gs/syscall/js/index.js.map +1 -1
  185. package/dist/gs/testing/testing.js +59 -15
  186. package/dist/gs/testing/testing.js.map +1 -1
  187. package/dist/gs/time/time.d.ts +24 -1
  188. package/dist/gs/time/time.js +43 -3
  189. package/dist/gs/time/time.js.map +1 -1
  190. package/dist/gs/unique/index.js +7 -1
  191. package/dist/gs/unique/index.js.map +1 -1
  192. package/go.mod +3 -3
  193. package/go.sum +16 -0
  194. package/gs/builtin/builtin.ts +25 -2
  195. package/gs/builtin/runtime-contract.test.ts +260 -18
  196. package/gs/builtin/slice.ts +51 -4
  197. package/gs/builtin/type.ts +310 -63
  198. package/gs/builtin/varRef.ts +85 -2
  199. package/gs/bytes/buffer.gs.ts +1 -1
  200. package/gs/bytes/reader.gs.ts +1 -1
  201. package/gs/compress/zlib/index.test.ts +159 -1
  202. package/gs/compress/zlib/index.ts +164 -37
  203. package/gs/compress/zlib/meta.json +4 -1
  204. package/gs/compress/zlib/parity.json +51 -0
  205. package/gs/crypto/sha1/index.test.ts +19 -2
  206. package/gs/crypto/sha1/index.ts +3 -6
  207. package/gs/crypto/sha256/index.test.ts +14 -2
  208. package/gs/crypto/sha256/index.ts +3 -6
  209. package/gs/crypto/sha512/index.test.ts +17 -2
  210. package/gs/crypto/sha512/index.ts +3 -6
  211. package/gs/embed/index.test.ts +87 -0
  212. package/gs/embed/index.ts +229 -5
  213. package/gs/encoding/json/index.test.ts +360 -6
  214. package/gs/encoding/json/index.ts +679 -38
  215. package/gs/encoding/json/parity.json +81 -0
  216. package/gs/fmt/fmt.test.ts +41 -3
  217. package/gs/fmt/fmt.ts +40 -17
  218. package/gs/fmt/meta.json +6 -1
  219. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.test.ts +211 -3
  220. package/gs/github.com/aperturerobotics/protobuf-go-lite/index.ts +857 -1
  221. package/gs/github.com/go-git/go-billy/v6/osfs/index.test.ts +110 -0
  222. package/gs/github.com/go-git/go-billy/v6/osfs/index.ts +280 -0
  223. package/gs/github.com/go-git/go-billy/v6/osfs/meta.json +8 -0
  224. package/gs/github.com/pkg/errors/errors.ts +54 -30
  225. package/gs/go/scanner/index.test.ts +39 -56
  226. package/gs/go/scanner/index.ts +33 -5
  227. package/gs/go/scanner/parity.json +27 -0
  228. package/gs/go/token/index.ts +22 -6
  229. package/gs/hash/index.test.ts +20 -33
  230. package/gs/hash/index.ts +28 -0
  231. package/gs/hash/parity.json +21 -0
  232. package/gs/internal/goarch/index.test.ts +32 -0
  233. package/gs/internal/goarch/index.ts +45 -13
  234. package/gs/internal/goarch/parity.json +144 -0
  235. package/gs/io/fs/fs.ts +26 -14
  236. package/gs/io/fs/readdir.test.ts +38 -0
  237. package/gs/io/fs/readdir.ts +8 -4
  238. package/gs/io/fs/sub.ts +8 -1
  239. package/gs/io/io.test.ts +77 -6
  240. package/gs/io/io.ts +115 -52
  241. package/gs/io/meta.json +7 -1
  242. package/gs/io/parity.json +162 -0
  243. package/gs/math/bits/index.test.ts +14 -1
  244. package/gs/math/bits/index.ts +75 -32
  245. package/gs/math/bits/parity.json +156 -0
  246. package/gs/mime/index.test.ts +90 -0
  247. package/gs/mime/index.ts +369 -6
  248. package/gs/mime/parity.json +36 -0
  249. package/gs/net/http/httptest/index.test.ts +98 -2
  250. package/gs/net/http/httptest/index.ts +101 -6
  251. package/gs/net/http/httptest/parity.json +15 -0
  252. package/gs/net/http/index.test.ts +797 -12
  253. package/gs/net/http/index.ts +1874 -136
  254. package/gs/net/http/meta.json +16 -1
  255. package/gs/net/http/parity.json +193 -0
  256. package/gs/os/dir_unix.gs.ts +1 -1
  257. package/gs/os/error.gs.ts +1 -1
  258. package/gs/os/exec.gs.ts +4 -8
  259. package/gs/os/exec_posix.gs.ts +1 -1
  260. package/gs/os/file_unix_js.test.ts +52 -0
  261. package/gs/os/index.test.ts +9 -0
  262. package/gs/os/index.ts +1 -0
  263. package/gs/os/meta.json +4 -0
  264. package/gs/os/parity.json +9 -0
  265. package/gs/os/proc.gs.ts +18 -5
  266. package/gs/os/proc.test.ts +26 -0
  267. package/gs/os/readdir.test.ts +56 -0
  268. package/gs/os/root_js.gs.ts +1 -1
  269. package/gs/os/types.gs.ts +1 -1
  270. package/gs/os/types_js.gs.ts +170 -9
  271. package/gs/os/types_unix.gs.ts +1 -1
  272. package/gs/path/path.ts +11 -7
  273. package/gs/reflect/deepequal.test.ts +10 -1
  274. package/gs/reflect/field.test.ts +37 -15
  275. package/gs/reflect/function-types.test.ts +518 -22
  276. package/gs/reflect/index.ts +8 -6
  277. package/gs/reflect/map.ts +20 -0
  278. package/gs/reflect/meta.json +6 -4
  279. package/gs/reflect/parity.json +234 -0
  280. package/gs/reflect/sliceat.test.ts +156 -0
  281. package/gs/reflect/structof.test.ts +401 -0
  282. package/gs/reflect/type.ts +1980 -365
  283. package/gs/reflect/typefor.test.ts +540 -10
  284. package/gs/reflect/types.ts +43 -18
  285. package/gs/reflect/value.ts +105 -45
  286. package/gs/reflect/visiblefields.ts +5 -168
  287. package/gs/runtime/parity.json +24 -0
  288. package/gs/runtime/pprof/index.test.ts +29 -7
  289. package/gs/runtime/pprof/index.ts +56 -30
  290. package/gs/runtime/pprof/parity.json +27 -0
  291. package/gs/runtime/runtime.test.ts +3 -1
  292. package/gs/runtime/runtime.ts +4 -3
  293. package/gs/runtime/trace/index.test.ts +5 -3
  294. package/gs/runtime/trace/index.ts +8 -20
  295. package/gs/runtime/trace/parity.json +36 -0
  296. package/gs/strconv/atoi.gs.ts +1 -1
  297. package/gs/strconv/complex.gs.ts +174 -0
  298. package/gs/strconv/complex.test.ts +65 -0
  299. package/gs/strconv/index.ts +1 -0
  300. package/gs/strconv/parity.json +120 -0
  301. package/gs/strings/builder.ts +1 -1
  302. package/gs/strings/meta.json +5 -2
  303. package/gs/strings/parity.json +186 -0
  304. package/gs/strings/reader.test.ts +2 -2
  305. package/gs/strings/reader.ts +11 -7
  306. package/gs/strings/replace.ts +15 -7
  307. package/gs/strings/strings.test.ts +22 -2
  308. package/gs/strings/strings.ts +64 -6
  309. package/gs/sync/atomic/type.gs.ts +9 -9
  310. package/gs/sync/atomic/value.gs.ts +2 -2
  311. package/gs/sync/meta.json +1 -0
  312. package/gs/sync/sync.test.ts +41 -1
  313. package/gs/sync/sync.ts +41 -16
  314. package/gs/syscall/env.ts +29 -14
  315. package/gs/syscall/js/index.test.ts +18 -0
  316. package/gs/syscall/js/index.ts +12 -0
  317. package/gs/testing/testing.test.ts +99 -3
  318. package/gs/testing/testing.ts +95 -24
  319. package/gs/time/parity.json +225 -0
  320. package/gs/time/time.test.ts +20 -2
  321. package/gs/time/time.ts +49 -7
  322. package/gs/unique/index.ts +7 -1
  323. package/package.json +4 -2
  324. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.d.ts +0 -217
  325. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js +0 -814
  326. package/dist/gs/github.com/aperturerobotics/starpc/srpc/index.js.map +0 -1
  327. package/gs/github.com/aperturerobotics/starpc/srpc/index.test.ts +0 -31
  328. package/gs/github.com/aperturerobotics/starpc/srpc/index.ts +0 -1233
  329. package/gs/github.com/aperturerobotics/starpc/srpc/meta.json +0 -46
  330. /package/compiler/{wasm_api.go → wasm-api.go} +0 -0
@@ -1,25 +1,72 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
2
 
3
3
  import { varRef } from '../../builtin/varRef.js'
4
+ import * as $ from '../../builtin/index.js'
5
+ import * as bytes from '../../bytes/index.js'
6
+ import * as context from '../../context/index.js'
7
+ import * as io from '../../io/index.js'
8
+ import * as strings from '../../strings/index.js'
4
9
  import {
10
+ CanonicalHeaderKey,
5
11
  Client,
12
+ Cookie,
13
+ DefaultClient,
14
+ DefaultMaxHeaderBytes,
15
+ DefaultMaxIdleConnsPerHost,
16
+ DefaultServeMux,
6
17
  DefaultTransport,
18
+ DetectContentType,
19
+ ErrNotSupported,
20
+ ErrServerClosed,
7
21
  File,
22
+ FileServer,
8
23
  FileSystem,
24
+ FS,
25
+ Get,
26
+ Handle,
9
27
  Header,
10
28
  Header_Add,
29
+ Header_Clone,
11
30
  Header_Del,
12
31
  Header_Get,
13
32
  Header_Set,
33
+ Header_Values,
34
+ Header_Write,
14
35
  HandlerFunc_ServeHTTP,
15
- Get,
36
+ ListenAndServe,
37
+ MaxBytesError,
38
+ MaxBytesHandler,
39
+ MaxBytesReader,
40
+ NewFileTransport,
41
+ MethodGet,
42
+ MethodHead,
43
+ MethodOptions,
16
44
  MethodPost,
17
45
  MethodDelete,
46
+ MethodPatch,
47
+ MethodPut,
48
+ NewCrossOriginProtection,
18
49
  NewRequest,
50
+ NewRequestWithContext,
51
+ NewResponseController,
52
+ NoBody,
19
53
  NotFound,
54
+ NotFoundHandler,
55
+ ParseCookie,
56
+ ParseHTTPVersion,
57
+ ParseSetCookie,
20
58
  ParseTime,
59
+ PostForm,
60
+ Protocols,
61
+ RegisterInProcessServer,
62
+ SameSiteStrictMode,
63
+ SetCookie,
64
+ StatusBadGateway,
65
+ Request,
21
66
  Response,
22
67
  ResponseWriter,
68
+ ServeContent,
69
+ ServeFile,
23
70
  Server,
24
71
  StatusCreated,
25
72
  StatusForbidden,
@@ -32,9 +79,42 @@ import {
32
79
  StatusTooManyRequests,
33
80
  StatusUnauthorized,
34
81
  StatusUnsupportedMediaType,
82
+ StatusNetworkAuthenticationRequired,
83
+ TimeFormat,
84
+ TrailerPrefix,
85
+ Transport,
86
+ UnregisterInProcessServer,
35
87
  } from './index.js'
36
88
 
89
+ const originalFetch = globalThis.fetch
90
+
91
+ class testResponseWriter implements ResponseWriter {
92
+ public Code = 0
93
+ public Body = new bytes.Buffer()
94
+ private headerMap = new Header()
95
+
96
+ public Header(): Header {
97
+ return this.headerMap
98
+ }
99
+
100
+ public Write(p: $.Slice<number>): [number, $.GoError] {
101
+ return this.Body.Write(p)
102
+ }
103
+
104
+ public WriteHeader(statusCode: number): void {
105
+ this.Code = statusCode
106
+ }
107
+ }
108
+
37
109
  describe('net/http override', () => {
110
+ afterEach(() => {
111
+ Object.defineProperty(globalThis, 'fetch', {
112
+ configurable: true,
113
+ writable: true,
114
+ value: originalFetch,
115
+ })
116
+ })
117
+
38
118
  it('exports response status helpers', () => {
39
119
  const resp = new Response({ StatusCode: StatusOK })
40
120
 
@@ -47,38 +127,521 @@ describe('net/http override', () => {
47
127
  expect(StatusText(StatusUnsupportedMediaType)).toBe('Unsupported Media Type')
48
128
  expect(StatusText(StatusTeapot)).toBe("I'm a teapot")
49
129
  expect(StatusText(StatusTooManyRequests)).toBe('Too Many Requests')
130
+ expect(StatusText(StatusBadGateway)).toBe('Bad Gateway')
50
131
  expect(StatusText(StatusServiceUnavailable)).toBe('Service Unavailable')
132
+ expect(StatusText(StatusNetworkAuthenticationRequired)).toBe('Network Authentication Required')
51
133
  expect(StatusText(599)).toBe('')
134
+ Header_Set(resp.Header, 'X-Test', 'ok')
135
+ resp.Body = io.NopCloser(bytes.NewReader($.stringToBytes('body')))
136
+ const written = new bytes.Buffer()
137
+ expect(resp.Write(written)).toBeNull()
138
+ expect(Buffer.from(written.Bytes()).toString('utf8')).toBe(
139
+ 'HTTP/1.1 200 OK\r\nX-Test: ok\r\n\r\nbody',
140
+ )
141
+ expect(MethodGet).toBe('GET')
142
+ expect(MethodHead).toBe('HEAD')
52
143
  expect(MethodPost).toBe('POST')
144
+ expect(MethodPut).toBe('PUT')
145
+ expect(MethodPatch).toBe('PATCH')
53
146
  expect(MethodDelete).toBe('DELETE')
54
147
  expect(StatusCreated).toBe(201)
148
+ expect(DefaultMaxHeaderBytes).toBe(1 << 20)
149
+ expect(DefaultMaxIdleConnsPerHost).toBe(2)
150
+ expect(TimeFormat).toBe('Mon, 02 Jan 2006 15:04:05 GMT')
151
+ expect(TrailerPrefix).toBe('Trailer:')
152
+ expect(ErrServerClosed.Error()).toBe('http: Server closed')
55
153
  })
56
154
 
57
- it('returns an explicit unsupported error for Get', () => {
58
- const [resp, err] = Get('https://example.invalid')
155
+ it('exports common header and protocol utility surfaces', () => {
156
+ const header = new Header()
157
+ Header_Add(header, 'content-type', 'text/plain')
158
+ Header_Add(header, 'Content-Type', 'charset=utf-8')
159
+ const cloned = Header_Clone(header)
160
+ Header_Add(cloned, 'Content-Type', 'copy')
59
161
 
60
- expect(resp).toBeNull()
61
- expect(err?.Error()).toBe('net/http: Get is not implemented in GoScript')
162
+ expect(CanonicalHeaderKey('content-type')).toBe('Content-Type')
163
+ expect(Array.from(Header_Values(header, 'CONTENT-TYPE') ?? [])).toEqual([
164
+ 'text/plain',
165
+ 'charset=utf-8',
166
+ ])
167
+ expect(Array.from(Header_Values(cloned, 'Content-Type') ?? [])).toContain('copy')
168
+ expect(Array.from(Header_Values(header, 'Content-Type') ?? [])).not.toContain('copy')
169
+
170
+ const written = new bytes.Buffer()
171
+ expect(Header_Write(header, written)).toBeNull()
172
+ expect(Buffer.from(written.Bytes()).toString('utf8')).toContain('Content-Type: text/plain\r\n')
173
+
174
+ expect(ParseHTTPVersion('HTTP/2.0')).toEqual([2, 0, true])
175
+ expect(ParseHTTPVersion('h2')).toEqual([0, 0, false])
176
+
177
+ const protocols = new Protocols()
178
+ protocols.SetHTTP1(true)
179
+ protocols.SetUnencryptedHTTP2(true)
180
+ expect(protocols.HTTP1()).toBe(true)
181
+ expect(protocols.HTTP2()).toBe(false)
182
+ expect(protocols.String()).toBe('{HTTP1,UnencryptedHTTP2}')
183
+ })
184
+
185
+ it('validates outgoing request construction', () => {
186
+ const [req, reqErr] = NewRequestWithContext(
187
+ context.Background(),
188
+ '',
189
+ 'https://example.invalid/path?q=1',
190
+ null,
191
+ )
192
+ expect(reqErr).toBeNull()
193
+ expect(req?.Method).toBe(MethodGet)
194
+ expect(req?.Host).toBe('example.invalid')
195
+ expect(req?.URL?.Path).toBe('/path')
196
+ expect(req?.URL?.RawQuery).toBe('q=1')
197
+
198
+ expect(NewRequestWithContext(context.Background(), 'bad method', 'https://example.invalid/', null)[1]?.Error()).toBe(
199
+ 'net/http: invalid method "bad method"',
200
+ )
201
+ expect(NewRequestWithContext(null, MethodGet, 'https://example.invalid/', null)[1]?.Error()).toBe(
202
+ 'net/http: nil Context',
203
+ )
204
+ expect(NewRequestWithContext(context.Background(), MethodGet, 'http://[::1', null)[1]).not.toBeNull()
205
+ expect(NewRequestWithContext(context.Background(), MethodGet, 'http://x/%zz', null)[1]).not.toBeNull()
206
+
207
+ const [bodyReq, bodyReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', bytes.NewReader($.stringToBytes('abc')))
208
+ expect(bodyReqErr).toBeNull()
209
+ expect(bodyReq?.ContentLength).toBe(3)
210
+ const [stringReq, stringReqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', strings.NewReader('abcd'))
211
+ expect(stringReqErr).toBeNull()
212
+ expect(stringReq?.ContentLength).toBe(4)
213
+ const [noBodyReq, noBodyErr] = NewRequest(MethodPost, 'https://example.invalid/upload', NoBody)
214
+ expect(noBodyErr).toBeNull()
215
+ expect(noBodyReq?.ContentLength).toBe(0)
216
+ expect(noBodyReq?.Body).toBe(NoBody)
217
+ })
218
+
219
+ it('applies cross-origin protection checks', () => {
220
+ const protection = NewCrossOriginProtection()
221
+ const [sameOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
222
+ Header_Set(sameOrigin!.Header, 'Sec-Fetch-Site', 'same-origin')
223
+ expect(protection.Check(sameOrigin)).toBeNull()
224
+
225
+ const [noHeaders] = NewRequest(MethodPost, 'https://example.invalid/update', null)
226
+ expect(protection.Check(noHeaders)).toBeNull()
227
+
228
+ const [safe] = NewRequest(MethodOptions, 'https://example.invalid/update', null)
229
+ Header_Set(safe!.Header, 'Sec-Fetch-Site', 'cross-site')
230
+ expect(protection.Check(safe)).toBeNull()
231
+
232
+ const [matchingOrigin] = NewRequest(MethodPost, 'https://example.invalid/update', null)
233
+ Header_Set(matchingOrigin!.Header, 'Origin', 'https://example.invalid')
234
+ expect(protection.Check(matchingOrigin)).toBeNull()
235
+
236
+ const [crossSite] = NewRequest(MethodPost, 'https://example.invalid/update', null)
237
+ Header_Set(crossSite!.Header, 'Sec-Fetch-Site', 'cross-site')
238
+ expect(protection.Check(crossSite)?.Error()).toContain('Sec-Fetch-Site')
239
+
240
+ const [oldBrowser] = NewRequest(MethodPost, 'https://example.invalid/update', null)
241
+ Header_Set(oldBrowser!.Header, 'Origin', 'https://attacker.invalid')
242
+ expect(protection.Check(oldBrowser)?.Error()).toContain('Origin does not match Host')
243
+
244
+ expect(protection.AddTrustedOrigin('https://trusted.invalid')).toBeNull()
245
+ expect(protection.AddTrustedOrigin('https://trusted.invalid/')).not.toBeNull()
246
+ const [trusted] = NewRequest(MethodPost, 'https://example.invalid/update', null)
247
+ Header_Set(trusted!.Header, 'Origin', 'https://trusted.invalid')
248
+ Header_Set(trusted!.Header, 'Sec-Fetch-Site', 'cross-site')
249
+ expect(protection.Check(trusted)).toBeNull()
250
+
251
+ protection.AddInsecureBypassPattern('/bypass/')
252
+ protection.AddInsecureBypassPattern('POST /post-only/')
253
+ const [bypass] = NewRequest(MethodPost, 'https://example.invalid/bypass/ok', null)
254
+ Header_Set(bypass!.Header, 'Origin', 'https://attacker.invalid')
255
+ Header_Set(bypass!.Header, 'Sec-Fetch-Site', 'cross-site')
256
+ expect(protection.Check(bypass)).toBeNull()
257
+
258
+ const [methodBypass] = NewRequest(MethodPost, 'https://example.invalid/post-only/', null)
259
+ Header_Set(methodBypass!.Header, 'Origin', 'https://attacker.invalid')
260
+ expect(protection.Check(methodBypass)).toBeNull()
261
+ })
262
+
263
+ it('routes cross-origin protection handlers through deny and success paths', () => {
264
+ const protection = NewCrossOriginProtection()
265
+ let served = false
266
+ const handler = protection.Handler({
267
+ ServeHTTP(w) {
268
+ served = true
269
+ w?.WriteHeader(StatusOK)
270
+ },
271
+ })
272
+ const [blocked] = NewRequest(MethodPost, 'https://example.invalid/update', null)
273
+ Header_Set(blocked!.Header, 'Sec-Fetch-Site', 'cross-site')
274
+ const blockedWriter = new testResponseWriter()
275
+
276
+ handler.ServeHTTP(blockedWriter, blocked)
277
+
278
+ expect(served).toBe(false)
279
+ expect(blockedWriter.Code).toBe(StatusForbidden)
280
+
281
+ protection.SetDenyHandler({
282
+ ServeHTTP(w) {
283
+ w?.WriteHeader(StatusTeapot)
284
+ },
285
+ })
286
+ const deniedWriter = new testResponseWriter()
287
+ handler.ServeHTTP(deniedWriter, blocked)
288
+ expect(deniedWriter.Code).toBe(StatusTeapot)
289
+
290
+ const [safe] = NewRequest(MethodGet, 'https://example.invalid/update', null)
291
+ const allowedWriter = new testResponseWriter()
292
+ handler.ServeHTTP(allowedWriter, safe)
293
+ expect(served).toBe(true)
294
+ expect(allowedWriter.Code).toBe(StatusOK)
295
+ })
296
+
297
+ it('parses cookies and reports syntax errors', () => {
298
+ const [cookies, cookieErr] = ParseCookie('Cookie-1="v$1"; c2=v2')
299
+ expect(cookieErr).toBeNull()
300
+ expect(cookies?.[0]?.Name).toBe('Cookie-1')
301
+ expect(cookies?.[0]?.Value).toBe('v$1')
302
+ expect(cookies?.[0]?.Quoted).toBe(true)
303
+ expect(cookies?.[1]?.Name).toBe('c2')
304
+ expect(cookies?.[1]?.Value).toBe('v2')
305
+
306
+ expect(ParseCookie('')[1]?.Error()).toBe('http: blank cookie')
307
+ expect(ParseCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
308
+ expect(ParseCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
309
+ expect(ParseCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
310
+
311
+ const [setCookie, setErr] = ParseSetCookie(
312
+ 'sid=abc; Path=/app; Domain=example.invalid; HttpOnly; Secure; SameSite=Strict; Max-Age=60; Partitioned',
313
+ )
314
+ expect(setErr).toBeNull()
315
+ expect(setCookie?.Name).toBe('sid')
316
+ expect(setCookie?.Value).toBe('abc')
317
+ expect(setCookie?.Path).toBe('/app')
318
+ expect(setCookie?.Domain).toBe('example.invalid')
319
+ expect(setCookie?.HttpOnly).toBe(true)
320
+ expect(setCookie?.Secure).toBe(true)
321
+ expect(setCookie?.SameSite).toBe(SameSiteStrictMode)
322
+ expect(setCookie?.MaxAge).toBe(60)
323
+ expect(setCookie?.Partitioned).toBe(true)
324
+ expect(setCookie?.Raw).toContain('sid=abc')
325
+
326
+ const [spaced, spacedErr] = ParseSetCookie('special-9 =","')
327
+ expect(spacedErr).toBeNull()
328
+ expect(spaced?.Name).toBe('special-9')
329
+ expect(spaced?.Value).toBe(',')
330
+ expect(spaced?.Quoted).toBe(true)
331
+
332
+ expect(ParseSetCookie('')[1]?.Error()).toBe('http: blank cookie')
333
+ expect(ParseSetCookie('missing-equals')[1]?.Error()).toBe("http: '=' not found in cookie")
334
+ expect(ParseSetCookie('=v1')[1]?.Error()).toBe('http: invalid cookie name')
335
+ expect(ParseSetCookie('k1=\\')[1]?.Error()).toBe('http: invalid cookie value')
336
+ })
337
+
338
+ it('exports no-body, limit-reader, and unsupported controller surfaces', async () => {
339
+ const empty = new Uint8Array(1)
340
+ const [n, err] = NoBody.Read(empty)
341
+ expect(n).toBe(0)
342
+ expect(err).toBe(io.EOF)
343
+ expect(NoBody.Close()).toBeNull()
344
+
345
+ const limited = MaxBytesReader(null, {
346
+ Read: (p: Uint8Array) => {
347
+ p[0] = 1
348
+ if (p.length > 1) {
349
+ p[1] = 2
350
+ }
351
+ return [1, null]
352
+ },
353
+ Close: () => null,
354
+ }, 1)
355
+ const limitedBuf = new Uint8Array(2)
356
+ expect(limited.Read(limitedBuf)).toEqual([1, null])
357
+ expect(limitedBuf[0]).toBe(1)
358
+ const [, limitErr] = limited.Read(new Uint8Array(1))
359
+ expect(limitErr).toBeInstanceOf(MaxBytesError)
360
+
361
+ const exactReader = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('ok'))), 2)
362
+ const [exactData, exactErr] = await io.ReadAll(exactReader)
363
+ expect(exactErr).toBeNull()
364
+ expect(Buffer.from(exactData ?? []).toString('utf8')).toBe('ok')
365
+
366
+ const tooLarge = MaxBytesReader(null, io.NopCloser(bytes.NewReader($.stringToBytes('toolarge'))), 2)
367
+ const tooLargeBuf = new Uint8Array(8)
368
+ const [tooLargeN, tooLargeErr] = tooLarge.Read(tooLargeBuf)
369
+ expect(tooLargeN).toBe(2)
370
+ expect(tooLargeErr).toBeInstanceOf(MaxBytesError)
371
+ expect((tooLargeErr as MaxBytesError).Limit).toBe(2)
372
+ expect(Buffer.from(tooLargeBuf.slice(0, tooLargeN)).toString('utf8')).toBe('to')
373
+
374
+ const controller = NewResponseController(null)
375
+ expect(controller.Hijack()[2]).toBe(ErrNotSupported)
376
+ expect(controller.SetReadDeadline({} as any)).toBe(ErrNotSupported)
377
+ expect(ListenAndServe(':0', null)).toBe(ErrNotSupported)
378
+ expect(new Transport().Clone()).toBeInstanceOf(Transport)
379
+ })
380
+
381
+ it('wraps a cloned request body in MaxBytesHandler', () => {
382
+ const body = io.NopCloser(bytes.NewReader($.stringToBytes('ok')))
383
+ const [req] = NewRequest(MethodPost, 'http://example.invalid/upload', body)
384
+ let servedReq: any = null
385
+ const handler = MaxBytesHandler({
386
+ ServeHTTP(_w, r) {
387
+ servedReq = $.pointerValue(r)
388
+ Header_Set(servedReq.Header, 'X-Shared', 'true')
389
+ },
390
+ }, 1)
391
+
392
+ handler.ServeHTTP(null, req)
393
+
394
+ expect(servedReq).not.toBe(req)
395
+ expect(servedReq.Body).not.toBe(body)
396
+ expect(req!.Body).toBe(body)
397
+ expect(Header_Get(req!.Header, 'X-Shared')).toBe('true')
398
+ })
399
+
400
+ it('routes Get through fetch-backed DefaultTransport', async () => {
401
+ Object.defineProperty(globalThis, 'fetch', {
402
+ configurable: true,
403
+ writable: true,
404
+ value: async () =>
405
+ new globalThis.Response('hello', {
406
+ status: StatusOK,
407
+ statusText: 'OK',
408
+ headers: { 'Content-Length': '5', 'X-Test': 'ok' },
409
+ }),
410
+ })
411
+
412
+ const [resp, err] = await Get('https://example.invalid')
413
+
414
+ expect(err).toBeNull()
415
+ expect(resp?.StatusCode).toBe(StatusOK)
416
+ expect(Header_Get(resp!.Header, 'x-test')).toBe('ok')
62
417
  })
63
418
 
64
419
  it('accepts VarRef requests for client calls', async () => {
420
+ Object.defineProperty(globalThis, 'fetch', {
421
+ configurable: true,
422
+ writable: true,
423
+ value: async () => {
424
+ throw new Error('network down')
425
+ },
426
+ })
65
427
  const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid', null)
66
428
  expect(reqErr).toBeNull()
67
429
  expect((req!.URL as any).Path).toBe('/')
68
- expect(req!.RequestURI).toBe('/')
430
+ expect(req!.Host).toBe('example.invalid')
431
+ expect(req!.RequestURI).toBe('')
69
432
 
70
433
  const [resp, err] = await new Client().Do(varRef(req!))
71
434
  expect(resp).toBeNull()
72
- expect(err?.Error()).toBe('net/http: Client.Do is not implemented in GoScript')
435
+ expect(err?.Error()).toContain('network down')
73
436
  })
74
437
 
75
- it('exports the default transport surface', async () => {
76
- const [req] = NewRequest(MethodPost, 'https://example.invalid', null)
438
+ it('wraps request body readers and keeps response metadata', () => {
439
+ const reader = {
440
+ Read: (p: Uint8Array) => [p.length, null] as [number, null],
441
+ }
442
+
443
+ const [req, reqErr] = NewRequest(MethodPost, 'https://example.invalid/upload', reader)
444
+
445
+ expect(reqErr).toBeNull()
446
+ expect(req!.Body).not.toBeNull()
447
+ expect(req!.Body!.Close()).toBeNull()
448
+
449
+ const resp = new Response({ StatusCode: StatusCreated, ContentLength: -1, Request: varRef(req!) })
450
+ expect(resp.ContentLength).toBe(-1)
451
+ expect((resp.Request as any).value).toBe(req)
452
+ })
453
+
454
+ it('exports fetch-backed default transport surface', async () => {
455
+ let requestBodyClosed = false
456
+ Object.defineProperty(globalThis, 'fetch', {
457
+ configurable: true,
458
+ writable: true,
459
+ value: async (input: RequestInfo | URL, init?: RequestInit) => {
460
+ expect(String(input)).toBe('https://example.invalid/upload')
461
+ expect(init?.method).toBe(MethodPost)
462
+ const headers = init?.headers as Headers
463
+ expect(headers.get('Range')).toBe('bytes=0-9')
464
+ expect(headers.get('Authorization')).toBe('Bearer test')
465
+ expect(Buffer.from((init?.body as Uint8Array) ?? []).toString('utf8')).toBe('payload')
466
+ return new globalThis.Response('accepted', {
467
+ status: StatusCreated,
468
+ statusText: 'Created',
469
+ headers: { 'Content-Length': '8', 'X-Reply': 'yes' },
470
+ })
471
+ },
472
+ })
473
+ const payload = bytes.NewReader($.stringToBytes('payload'))
474
+ const requestBody = {
475
+ Read: payload.Read.bind(payload),
476
+ Close: () => {
477
+ requestBodyClosed = true
478
+ return null
479
+ },
480
+ }
481
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', requestBody)
482
+ Header_Set(req!.Header, 'Range', 'bytes=0-9')
483
+ Header_Set(req!.Header, 'Authorization', 'Bearer test')
484
+
485
+ const [resp, err] = await DefaultTransport.RoundTrip(req)
486
+
487
+ expect(err).toBeNull()
488
+ expect(resp?.StatusCode).toBe(StatusCreated)
489
+ expect(resp?.ContentLength).toBe(8)
490
+ expect(Header_Get(resp!.Header, 'x-reply')).toBe('yes')
491
+ expect(requestBodyClosed).toBe(true)
492
+ })
493
+
494
+ it('closes request bodies when fetch body reads fail', async () => {
495
+ let requestBodyClosed = false
496
+ Object.defineProperty(globalThis, 'fetch', {
497
+ configurable: true,
498
+ writable: true,
499
+ value: async () => {
500
+ throw new Error('fetch should not run')
501
+ },
502
+ })
503
+ const readErr = $.newError('read failed')
504
+ const [req] = NewRequest(MethodPost, 'https://example.invalid/upload', {
505
+ Read: () => [0, readErr],
506
+ Close: () => {
507
+ requestBodyClosed = true
508
+ return null
509
+ },
510
+ })
77
511
 
78
512
  const [resp, err] = await DefaultTransport.RoundTrip(req)
79
513
 
80
514
  expect(resp).toBeNull()
81
- expect(err?.Error()).toBe('net/http: Client.Do is not implemented in GoScript')
515
+ expect(err).toBe(readErr)
516
+ expect(requestBodyClosed).toBe(true)
517
+ })
518
+
519
+ it('closes request bodies before unsupported and canceled requests return', async () => {
520
+ let unsupportedClosed = false
521
+ Object.defineProperty(globalThis, 'fetch', {
522
+ configurable: true,
523
+ writable: true,
524
+ value: undefined,
525
+ })
526
+ const [unsupportedReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
527
+ Read: () => [0, null],
528
+ Close: () => {
529
+ unsupportedClosed = true
530
+ return null
531
+ },
532
+ })
533
+
534
+ const [unsupportedResp, unsupportedErr] = await DefaultTransport.RoundTrip(unsupportedReq)
535
+
536
+ expect(unsupportedResp).toBeNull()
537
+ expect(unsupportedErr?.Error()).toContain('Client.Do is not implemented')
538
+ expect(unsupportedClosed).toBe(true)
539
+
540
+ let canceledClosed = false
541
+ Object.defineProperty(globalThis, 'fetch', {
542
+ configurable: true,
543
+ writable: true,
544
+ value: async () => {
545
+ throw new Error('fetch should not run')
546
+ },
547
+ })
548
+ const [ctx, cancel] = context.WithCancel(context.Background())
549
+ cancel?.()
550
+ const [canceledReq] = NewRequest(MethodPost, 'https://example.invalid/upload', {
551
+ Read: () => [0, null],
552
+ Close: () => {
553
+ canceledClosed = true
554
+ return null
555
+ },
556
+ })
557
+
558
+ const [canceledResp, canceledErr] = await DefaultTransport.RoundTrip(canceledReq!.WithContext(ctx))
559
+
560
+ expect(canceledResp).toBeNull()
561
+ expect(canceledErr).toBe(context.Canceled)
562
+ expect(canceledClosed).toBe(true)
563
+ })
564
+
565
+ it('closes request bodies for methods that do not send a fetch body', async () => {
566
+ let requestBodyClosed = false
567
+ Object.defineProperty(globalThis, 'fetch', {
568
+ configurable: true,
569
+ writable: true,
570
+ value: async (_input: RequestInfo | URL, init?: RequestInit) => {
571
+ expect(init?.method).toBe(MethodGet)
572
+ expect(init?.body).toBeUndefined()
573
+ return new globalThis.Response('ok', { status: StatusOK })
574
+ },
575
+ })
576
+ const [req] = NewRequest(MethodGet, 'https://example.invalid/read', {
577
+ Read: () => {
578
+ throw new Error('GET body should not be read')
579
+ },
580
+ Close: () => {
581
+ requestBodyClosed = true
582
+ return null
583
+ },
584
+ })
585
+
586
+ const [resp, err] = await DefaultTransport.RoundTrip(req)
587
+
588
+ expect(err).toBeNull()
589
+ expect(resp?.StatusCode).toBe(StatusOK)
590
+ expect(requestBodyClosed).toBe(true)
591
+ })
592
+
593
+ it('closes request bodies after in-process handlers return', async () => {
594
+ let handlerSawBody = false
595
+ let requestBodyClosed = false
596
+ const url = RegisterInProcessServer({
597
+ ServeHTTP: (w, r) => {
598
+ const req = $.pointerValue(r)
599
+ handlerSawBody = req?.Body != null
600
+ expect(req?.RequestURI).toBe('/close?q=1')
601
+ expect(req?.URL.Host).toBe('')
602
+ expect(req?.URL.Scheme).toBe('')
603
+ w?.WriteHeader(StatusOK)
604
+ },
605
+ })
606
+ try {
607
+ const [req] = NewRequest(MethodPost, url + '/close?q=1', {
608
+ Read: () => [0, null],
609
+ Close: () => {
610
+ requestBodyClosed = true
611
+ return null
612
+ },
613
+ })
614
+
615
+ const [resp, err] = await DefaultTransport.RoundTrip(req)
616
+
617
+ expect(err).toBeNull()
618
+ expect(resp?.StatusCode).toBe(StatusOK)
619
+ expect(handlerSawBody).toBe(true)
620
+ expect(requestBodyClosed).toBe(true)
621
+ } finally {
622
+ UnregisterInProcessServer(url)
623
+ }
624
+ })
625
+
626
+ it('suppresses in-process response bodies for HEAD requests', async () => {
627
+ const url = RegisterInProcessServer({
628
+ ServeHTTP(w) {
629
+ w?.WriteHeader(StatusOK)
630
+ w?.Write($.stringToBytes('hidden'))
631
+ },
632
+ })
633
+ try {
634
+ const [req] = NewRequest(MethodHead, url + '/head', null)
635
+
636
+ const [resp, err] = await new Transport().RoundTrip(req)
637
+
638
+ expect(err).toBeNull()
639
+ const [n, readErr] = resp!.Body!.Read(new Uint8Array(8))
640
+ expect(n).toBe(0)
641
+ expect(readErr).toBe(io.EOF)
642
+ } finally {
643
+ UnregisterInProcessServer(url)
644
+ }
82
645
  })
83
646
 
84
647
  it('delegates client calls through RoundTripper implementations', async () => {
@@ -93,7 +656,7 @@ describe('net/http override', () => {
93
656
  const request = (got as any).value ?? got
94
657
  expect(request.UserAgent()).toBe('goscript-test')
95
658
  expect(request.RemoteAddr).toBe('127.0.0.1:1234')
96
- expect(request.RequestURI).toBe('/path')
659
+ expect(request.RequestURI).toBe('')
97
660
  expect(request.ContentLength).toBe(42)
98
661
  return [new Response({ StatusCode: StatusOK }), null]
99
662
  },
@@ -105,6 +668,39 @@ describe('net/http override', () => {
105
668
  expect(resp?.StatusCode).toBe(StatusOK)
106
669
  })
107
670
 
671
+ it('posts URL-encoded forms through clients and package helper', async () => {
672
+ const transport = {
673
+ async RoundTrip(got: Request | $.VarRef<Request> | null): Promise<[Response | null, $.GoError]> {
674
+ const request = $.pointerValue<Request>(got)
675
+ expect(request.Method).toBe(MethodPost)
676
+ expect(Header_Get(request.Header, 'Content-Type')).toBe('application/x-www-form-urlencoded')
677
+ const [data, err] = await io.ReadAll(request.Body!)
678
+ expect(err).toBeNull()
679
+ expect($.bytesToString(data)).toBe('a=one&a=two&space=x+y')
680
+ return [new Response({ StatusCode: StatusOK }), null]
681
+ },
682
+ }
683
+ const form = new Map<string, $.Slice<string>>([
684
+ ['space', $.arrayToSlice(['x y'])],
685
+ ['a', $.arrayToSlice(['one', 'two'])],
686
+ ])
687
+ const client = new Client({ Transport: transport })
688
+
689
+ const [clientResp, clientErr] = await client.PostForm('https://example.invalid/form', form)
690
+ expect(clientErr).toBeNull()
691
+ expect(clientResp?.StatusCode).toBe(StatusOK)
692
+
693
+ const oldTransport = DefaultClient.Transport
694
+ DefaultClient.Transport = transport
695
+ try {
696
+ const [resp, err] = await PostForm('https://example.invalid/form', form)
697
+ expect(err).toBeNull()
698
+ expect(resp?.StatusCode).toBe(StatusOK)
699
+ } finally {
700
+ DefaultClient.Transport = oldTransport
701
+ }
702
+ })
703
+
108
704
  it('canonicalizes header keys for case-insensitive lookup', () => {
109
705
  const header = new Header()
110
706
 
@@ -153,11 +749,83 @@ describe('net/http override', () => {
153
749
  expect(writes).toEqual(['status:404', '404 page not found\n'])
154
750
  })
155
751
 
752
+ it('routes through default mux and handler helper exports', () => {
753
+ const writes: string[] = []
754
+ const writer: ResponseWriter = {
755
+ Header: () => new Header(),
756
+ Write: (p) => {
757
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
758
+ return [p?.length ?? 0, null]
759
+ },
760
+ WriteHeader: (code) => writes.push(`status:${code}`),
761
+ }
762
+ const [req] = NewRequest(MethodGet, 'https://example.invalid/default', null)
763
+ Handle('/default', {
764
+ ServeHTTP(w) {
765
+ w!.WriteHeader(StatusOK)
766
+ w!.Write($.stringToBytes('default mux'))
767
+ },
768
+ })
769
+
770
+ DefaultServeMux.ServeHTTP(writer, req)
771
+ NotFoundHandler().ServeHTTP(writer, req)
772
+
773
+ expect(writes).toEqual([
774
+ 'status:200',
775
+ 'default mux',
776
+ 'status:404',
777
+ '404 page not found\n',
778
+ ])
779
+ })
780
+
781
+ it('formats Set-Cookie headers for browser bootstrap routes', () => {
782
+ const header = new Header()
783
+ const writer: ResponseWriter = {
784
+ Header: () => header,
785
+ Write: (p) => [p?.length ?? 0, null],
786
+ WriteHeader: () => undefined,
787
+ }
788
+
789
+ SetCookie(writer, new Cookie({
790
+ Name: 'spacewave_local_capability',
791
+ Value: 'token',
792
+ Path: '/',
793
+ MaxAge: 300,
794
+ HttpOnly: true,
795
+ Secure: true,
796
+ SameSite: SameSiteStrictMode,
797
+ }))
798
+
799
+ expect(Array.from(header.get('Set-Cookie') ?? [])).toEqual([
800
+ 'spacewave_local_capability=token; Path=/; Max-Age=300; HttpOnly; Secure; SameSite=Strict',
801
+ ])
802
+ })
803
+
156
804
  it('parses HTTP dates', () => {
157
805
  const [parsed, err] = ParseTime('Sun, 06 Nov 1994 08:49:37 GMT')
158
806
 
159
807
  expect(err).toBeNull()
160
808
  expect(parsed.Unix()).toBe(784111777)
809
+ expect(DetectContentType($.stringToBytes('<HTML>ok'))).toBe('text/html; charset=utf-8')
810
+ expect(
811
+ DetectContentType(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
812
+ ).toBe('image/png')
813
+ expect(DetectContentType(new Uint8Array([0xff, 0xd8, 0xff, 0x00]))).toBe('image/jpeg')
814
+ expect(DetectContentType($.stringToBytes('%PDF-1.7'))).toBe('application/pdf')
815
+ expect(DetectContentType($.stringToBytes('RIFFxxxxWEBPVP'))).toBe('image/webp')
816
+ expect(DetectContentType($.stringToBytes('FORMxxxxAIFF'))).toBe('audio/aiff')
817
+ expect(DetectContentType($.stringToBytes('ID3payload'))).toBe('audio/mpeg')
818
+ expect(DetectContentType($.stringToBytes('wOFFpayload'))).toBe('font/woff')
819
+ expect(DetectContentType(new Uint8Array([
820
+ 0x00, 0x00, 0x00, 0x18,
821
+ 0x66, 0x74, 0x79, 0x70,
822
+ 0x69, 0x73, 0x6f, 0x6d,
823
+ 0x00, 0x00, 0x00, 0x00,
824
+ 0x6d, 0x70, 0x34, 0x31,
825
+ 0x00, 0x00, 0x00, 0x00,
826
+ ]))).toBe('video/mp4')
827
+ expect(DetectContentType(new Uint8Array([0x00, 0x01, 0x02]))).toBe('application/octet-stream')
828
+ expect(DetectContentType(new Uint8Array())).toBe('text/plain; charset=utf-8')
161
829
  })
162
830
 
163
831
  it('exports file server interface shapes', () => {
@@ -175,4 +843,121 @@ describe('net/http override', () => {
175
843
  expect(fsys.Open('ok')[0]).toBe(file)
176
844
  expect(fsys.Open('missing')[1]?.message).toBe('missing')
177
845
  })
846
+
847
+ it('serves files and omits HEAD response bodies', async () => {
848
+ const opened: string[] = []
849
+ const makeFile = () => {
850
+ const reader = bytes.NewReader($.stringToBytes('hello'))
851
+ return {
852
+ Close: () => null,
853
+ Read: (p: Uint8Array) => reader.Read(p),
854
+ Seek: (offset: number, whence: number) => reader.Seek(offset, whence),
855
+ Readdir: () => [null, null] as [null, null],
856
+ Stat: () => [
857
+ {
858
+ IsDir: () => false,
859
+ ModTime: () => null as never,
860
+ Mode: () => 0,
861
+ Name: () => 'file.txt',
862
+ Size: () => 5,
863
+ Sys: () => null,
864
+ },
865
+ null,
866
+ ] as const,
867
+ }
868
+ }
869
+ const root = FS({
870
+ Open: (name) => {
871
+ opened.push(name)
872
+ return name === 'file.txt' ? [makeFile(), null] : [null, new Error('missing')]
873
+ },
874
+ })
875
+ const writes: string[] = []
876
+ const header = new Header()
877
+ const writer: ResponseWriter = {
878
+ Header: () => header,
879
+ Write: (p) => {
880
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
881
+ return [p?.length ?? 0, null]
882
+ },
883
+ WriteHeader: (code) => writes.push(`status:${code}`),
884
+ }
885
+ const handler = FileServer(root)
886
+ const [getReq] = NewRequest(MethodGet, 'http://example.invalid/../file.txt', null)
887
+
888
+ await handler.ServeHTTP(writer, getReq)
889
+
890
+ expect(opened).toEqual(['file.txt'])
891
+ expect(writes).toEqual(['status:200', 'hello'])
892
+ expect(Header_Get(header, 'Content-Length')).toBe('5')
893
+
894
+ writes.length = 0
895
+ opened.length = 0
896
+ const [headReq] = NewRequest(MethodHead, 'http://example.invalid/file.txt', null)
897
+
898
+ await handler.ServeHTTP(writer, headReq)
899
+
900
+ expect(opened).toEqual(['file.txt'])
901
+ expect(writes).toEqual(['status:200'])
902
+ })
903
+
904
+ it('awaits ServeContent writes before returning', async () => {
905
+ const writes: string[] = []
906
+ const writer: ResponseWriter = {
907
+ Header: () => new Header(),
908
+ Write: (p) => {
909
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
910
+ return [p?.length ?? 0, null]
911
+ },
912
+ WriteHeader: (code) => writes.push(`status:${code}`),
913
+ }
914
+ const [req] = NewRequest(MethodGet, 'http://example.invalid/content.txt', null)
915
+
916
+ await ServeContent(writer, req, 'content.txt', null as never, bytes.NewReader($.stringToBytes('served')))
917
+
918
+ expect(writes).toEqual(['status:200', 'served'])
919
+
920
+ writes.length = 0
921
+ const [headReq] = NewRequest(MethodHead, 'http://example.invalid/content.txt', null)
922
+ await ServeContent(writer, headReq, 'content.txt', null as never, bytes.NewReader($.stringToBytes('hidden')))
923
+
924
+ expect(writes).toEqual(['status:200'])
925
+ })
926
+
927
+ it('closes request bodies after file transport requests', async () => {
928
+ const root = FS({
929
+ Open: () => [null, new Error('missing')],
930
+ })
931
+ let closed = false
932
+ const [req] = NewRequest(MethodGet, 'file:///missing.txt', {
933
+ Read: () => [0, io.EOF],
934
+ Close: () => {
935
+ closed = true
936
+ return null
937
+ },
938
+ })
939
+
940
+ const [resp, err] = await NewFileTransport(root).RoundTrip(req)
941
+
942
+ expect(err).toBeNull()
943
+ expect(resp?.StatusCode).toBe(StatusNotFound)
944
+ expect(closed).toBe(true)
945
+ })
946
+
947
+ it('exports ServeFile for browser builds without local filesystem access', () => {
948
+ const writes: string[] = []
949
+ const writer: ResponseWriter = {
950
+ Header: () => new Header(),
951
+ Write: (p) => {
952
+ writes.push(Buffer.from(p ?? []).toString('utf8'))
953
+ return [p?.length ?? 0, null]
954
+ },
955
+ WriteHeader: (code) => writes.push(`status:${code}`),
956
+ }
957
+ const [req] = NewRequest(MethodGet, 'http://example.invalid/eval/missing.js', null)
958
+
959
+ ServeFile(writer, req, '/host-only/missing.js')
960
+
961
+ expect(writes[0]).toBe('status:404')
962
+ })
178
963
  })