Adopts the round's main features into the v2 architecture: the #9023 media rework (streamed videos, media-file assets, protocol-relative links), #9098 shared live-activity row folded into the v2 working and thinking rows, the #9084/#9078 Claude model catalog for v2 consumers, a native #9005 OpenCode child-session abort in the v2 adapter, #9013's landed LegendList patch, and per-environment sidebar provider entries. For #8600 the server-side pieces land, but auto-settle evaluation stays client-side (reading the new server-owned settings) until the v2 orchestrator grows its own settlement reactor; main's v1-only reactor and coalescer additions are dropped with the rest of the v1 path.
Showing the changes to apps/server/src/http.test.ts (1 of 47 files). Show all
Unified · Split
| @@ -1,12 +1,13 @@ | |||
| 1 | import { expect, it } from "@effect/vitest"; | 1 | import { expect, it } from "@effect/vitest"; |
| 2 | import { describe, vi } from "vite-plus/test"; | ||
| 3 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; | ||
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 2 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 5 | import * as Path from "effect/Path"; | 8 | import * as Path from "effect/Path"; |
| 6 | import { HttpRouter, HttpServerResponse } from "effect/unstable/http"; | 9 | import { HttpRouter, HttpServerResponse } from "effect/unstable/http"; |
| 7 | import { describe } from "vite-plus/test"; | 10 | import { openMediaFile } from "./assets/MediaFile.ts"; |
| 8 | import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; | ||
| 9 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 10 | 11 | ||
| 11 | import { ORCHESTRATION_PROTOCOL_HEADER } from "@t3tools/contracts"; | 12 | import { ORCHESTRATION_PROTOCOL_HEADER } from "@t3tools/contracts"; |
| 12 | 13 | ||
| @@ -70,6 +71,179 @@ describe("browser API CORS", () => { · 57 unmodified lines | |||
| 70 | const fileResponseLayer = Layer.mergeAll(NodeHttpPlatform.layer, NodeServices.layer); | 71 | const fileResponseLayer = Layer.mergeAll(NodeHttpPlatform.layer, NodeServices.layer); |
| 71 | 72 | ||
| 72 | describe("video asset byte ranges", () => { | 73 | describe("video asset byte ranges", () => { |
| 74 | it.effect("uses current descriptor metadata after an in-place truncate or extension", () => | ||
| 75 | Effect.gen(function* () { | ||
| 76 | const fs = yield* FileSystem.FileSystem; | ||
| 77 | const path = yield* Path.Path; | ||
| 78 | const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-guarded-current-stat-" }); | ||
| 79 | const filePath = path.join(directory, "clip.mp4"); | ||
| 80 | for (const [contents, range, method, expected, status, contentRange] of [ | ||
| 81 | ["1234", undefined, "GET", "1234", 200, null], | ||
| 82 | ["0123456789abcdef", undefined, "GET", "0123456789abcdef", 200, null], | ||
| 83 | ["1234", "bytes=4-", "GET", "", 416, "bytes */4"], | ||
| 84 | ["1234", "bytes=1-20", "GET", "234", 206, "bytes 1-3/4"], | ||
| 85 | ["0123456789abcdef", "bytes=10-", "GET", "abcdef", 206, "bytes 10-15/16"], | ||
| 86 | ["0123456789abcdef", undefined, "HEAD", "", 200, null], | ||
| 87 | ["", undefined, "GET", "", 200, null], | ||
| 88 | ["", "bytes=0-1", "GET", "", 416, "bytes */0"], | ||
| 89 | ] as const) { | ||
| 90 | yield* fs.writeFileString(filePath, "0123456789"); | ||
| 91 | const canonicalPath = yield* fs.realPath(filePath); | ||
| 92 | const file = yield* openMediaFile(canonicalPath); | ||
| 93 | if (!file) throw new Error("Expected an opened media file"); | ||
| 94 | yield* fs.writeFileString(filePath, contents); | ||
| 95 | const response = HttpServerResponse.toWeb( | ||
| 96 | yield* assetFileResponse( | ||
| 97 | { path: canonicalPath, file, mimeType: "video/mp4" }, | ||
| 98 | range, | ||
| 99 | undefined, | ||
| 100 | method, | ||
| 101 | ), | ||
| 102 | ); | ||
| 103 | expect(response.status).toBe(status); | ||
| 104 | expect(response.headers.get("content-range")).toBe(contentRange); | ||
| 105 | if (status !== 416) { | ||
| 106 | expect(response.headers.get("content-length")).toBe( | ||
| 107 | String(method === "HEAD" ? contents.length : expected.length), | ||
| 108 | ); | ||
| 109 | } | ||
| 110 | expect(yield* Effect.promise(() => response.text())).toBe(expected); | ||
| 111 | } | ||
| 112 | }).pipe(Effect.provide(fileResponseLayer)), | ||
| 113 | ); | ||
| 114 | |||
| 115 | it.effect( | ||
| 116 | "rejects unaddressable ranges before streaming and preserves small ranges on large files", | ||
| 117 | () => | ||
| 118 | Effect.gen(function* () { | ||
| 119 | const fs = yield* FileSystem.FileSystem; | ||
| 120 | const path = yield* Path.Path; | ||
| 121 | const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-guarded-offset-limit-" }); | ||
| 122 | const filePath = path.join(directory, "clip.mp4"); | ||
| 123 | yield* fs.writeFileString(filePath, "0123456789"); | ||
| 124 | const canonicalPath = yield* fs.realPath(filePath); | ||
| 125 | const unsafeOffset = BigInt(Number.MAX_SAFE_INTEGER) + 1n; | ||
| 126 | const size = unsafeOffset + 32n; | ||
| 127 | for (const [range, status] of [ | ||
| 128 | [`bytes=${unsafeOffset}-${unsafeOffset}`, 416], | ||
| 129 | [`bytes=0-${unsafeOffset}`, 416], | ||
| 130 | ["bytes=-1", 416], | ||
| 131 | [undefined, 413], | ||
| 132 | ["bytes=0-1", 206], | ||
| 133 | ] as const) { | ||
| 134 | const file = yield* openMediaFile(canonicalPath); | ||
| 135 | if (!file) throw new Error("Expected an opened media file"); | ||
| 136 | // Model a sparse file beyond the native stream's numeric addressing limit. | ||
| 137 | const info = yield* Effect.promise(() => file.handle.stat({ bigint: true })); | ||
| 138 | info.size = size; | ||
| 139 | const statSpy = vi.spyOn(file.handle, "stat").mockResolvedValue(info); | ||
| 140 | yield* Effect.addFinalizer(() => Effect.sync(() => statSpy.mockRestore())); | ||
| 141 | const response = HttpServerResponse.toWeb( | ||
| 142 | yield* assetFileResponse({ path: canonicalPath, file, mimeType: "video/mp4" }, range), | ||
| 143 | ); | ||
| 144 | expect(response.status).toBe(status); | ||
| 145 | if (status === 416) { | ||
| 146 | expect(response.headers.get("content-range")).toBe(`bytes */${size}`); | ||
| 147 | expect(yield* Effect.promise(() => response.text())).toBe(""); | ||
| 148 | } else if (status === 206) { | ||
| 149 | expect(response.headers.get("content-range")).toBe(`bytes 0-1/${size}`); | ||
| 150 | expect(yield* Effect.promise(() => response.text())).toBe("01"); | ||
| 151 | } else { | ||
| 152 | expect(yield* Effect.promise(() => response.text())).toBe( | ||
| 153 | "File is too large to preview.", | ||
| 154 | ); | ||
| 155 | } | ||
| 156 | } | ||
| 157 | }).pipe(Effect.provide(fileResponseLayer)), | ||
| 158 | ); | ||
| 159 | |||
| 160 | it.effect("streams guarded file ranges, including suffixes and conditional requests", () => | ||
| 161 | Effect.gen(function* () { | ||
| 162 | const fs = yield* FileSystem.FileSystem; | ||
| 163 | const path = yield* Path.Path; | ||
| 164 | const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-guarded-range-" }); | ||
| 165 | const filePath = path.join(directory, "clip.mp4"); | ||
| 166 | yield* fs.writeFileString(filePath, "0123456789"); | ||
| 167 | const canonicalPath = yield* fs.realPath(filePath); | ||
| 168 | for (const [range, ifRange, expected, status, contentRange] of [ | ||
| 169 | [undefined, undefined, "0123456789", 200, null], | ||
| 170 | ["bytes=0-1", undefined, "01", 206, "bytes 0-1/10"], | ||
| 171 | ["bytes=4-", undefined, "456789", 206, "bytes 4-9/10"], | ||
| 172 | ["bytes=-3", undefined, "789", 206, "bytes 7-9/10"], | ||
| 173 | ["bytes=-999999999999999999999999", undefined, "0123456789", 206, "bytes 0-9/10"], | ||
| 174 | ["bytes=10-", undefined, "", 416, "bytes */10"], | ||
| 175 | ["bytes=0-1", '"old-etag"', "0123456789", 200, null], | ||
| 176 | ["bytes=0-1", "", "0123456789", 200, null], | ||
| 177 | ] as const) { | ||
| 178 | const file = yield* openMediaFile(canonicalPath); | ||
| 179 | if (!file) throw new Error("Expected an opened media file"); | ||
| 180 | const response = HttpServerResponse.toWeb( | ||
| 181 | yield* assetFileResponse( | ||
| 182 | { path: canonicalPath, file, mimeType: "video/mp4" }, | ||
| 183 | range, | ||
| 184 | ifRange, | ||
| 185 | ), | ||
| 186 | ); | ||
| 187 | expect(response.status).toBe(status); | ||
| 188 | expect(response.headers.get("accept-ranges")).toBe("bytes"); | ||
| 189 | expect(response.headers.get("content-range")).toBe(contentRange); | ||
| 190 | expect(response.headers.get("cache-control")).toBe("private, no-store"); | ||
| 191 | expect(response.headers.get("etag")).toBeNull(); | ||
| 192 | expect(response.headers.get("last-modified")).toBeNull(); | ||
| 193 | if (status !== 416) | ||
| 194 | expect(response.headers.get("content-length")).toBe(String(expected.length)); | ||
| 195 | expect(yield* Effect.promise(() => response.text())).toBe(expected); | ||
| 196 | } | ||
| 197 | }).pipe(Effect.provide(fileResponseLayer)), | ||
| 198 | ); | ||
| 199 | |||
| 200 | it.effect("closes guarded descriptors after full, HEAD, rejected, and cancelled responses", () => | ||
| 201 | Effect.gen(function* () { | ||
| 202 | const fs = yield* FileSystem.FileSystem; | ||
| 203 | const path = yield* Path.Path; | ||
| 204 | const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-guarded-cleanup-" }); | ||
| 205 | const filePath = path.join(directory, "clip.mp4"); | ||
| 206 | const bytes = new Uint8Array(1024 * 1024).fill(42); | ||
| 207 | yield* fs.writeFile(filePath, bytes); | ||
| 208 | const canonicalPath = yield* fs.realPath(filePath); | ||
| 209 | for (const mode of ["full", "HEAD", "rejected", "cancelled"] as const) { | ||
| 210 | const file = yield* Effect.scoped( | ||
| 211 | Effect.gen(function* () { | ||
| 212 | const file = yield* openMediaFile(canonicalPath); | ||
| 213 | if (!file) throw new Error("Expected an opened media file"); | ||
| 214 | const response = HttpServerResponse.toWeb( | ||
| 215 | yield* assetFileResponse( | ||
| 216 | { path: canonicalPath, file, mimeType: "video/mp4" }, | ||
| 217 | mode === "rejected" ? `bytes=${bytes.length}-` : "bytes=0-", | ||
| 218 | undefined, | ||
| 219 | mode === "HEAD" ? "HEAD" : "GET", | ||
| 220 | ), | ||
| 221 | ); | ||
| 222 | if (mode === "HEAD") { | ||
| 223 | expect(response.status).toBe(200); | ||
| 224 | expect(response.headers.get("content-length")).toBe(String(bytes.length)); | ||
| 225 | expect(response.headers.get("content-range")).toBeNull(); | ||
| 226 | expect(yield* Effect.promise(() => response.text())).toBe(""); | ||
| 227 | } else if (mode === "rejected") { | ||
| 228 | expect(response.status).toBe(416); | ||
| 229 | expect(yield* Effect.promise(() => response.text())).toBe(""); | ||
| 230 | } else if (mode === "cancelled") { | ||
| 231 | const reader = response.body!.getReader(); | ||
| 232 | const first = yield* Effect.promise(() => reader.read()); | ||
| 233 | expect(first.done).toBe(false); | ||
| 234 | expect(first.value!.byteLength).toBeLessThan(bytes.length); | ||
| 235 | yield* Effect.promise(() => reader.cancel()); | ||
| 236 | } else { | ||
| 237 | expect(yield* Effect.promise(() => response.arrayBuffer())).toEqual(bytes.buffer); | ||
| 238 | } | ||
| 239 | return file; | ||
| 240 | }), | ||
| 241 | ); | ||
| 242 | expect(file.handle.fd).toBe(-1); | ||
| 243 | } | ||
| 244 | }).pipe(Effect.provide(fileResponseLayer)), | ||
| 245 | ); | ||
| 246 | |||
| 73 | it.effect("streams exactly the requested bytes and leaves full downloads intact", () => | 247 | it.effect("streams exactly the requested bytes and leaves full downloads intact", () => |
| 74 | Effect.gen(function* () { | 248 | Effect.gen(function* () { |
| 75 | const fs = yield* FileSystem.FileSystem; | 249 | const fs = yield* FileSystem.FileSystem; |