| author | |
| committer | |
| log | b123cbb317ca47ff080a15b44c4bc0c99126f729 |
| tree | b8e86d7d8b1878a70acaafc03cddaa9014bcc6e7 |
| parent | 5f4c7161fa7782ea93b63fe934d43229742e7ec8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Code <noreply@anthropic.com>9 files changed, 125 insertions(+), 75 deletions(-)
apps/server/src/bootstrap.test.ts+21-11| ... | ... | @@ -57,6 +57,22 @@ vi.mock("node:fs", async (importOriginal) => { |
| 57 | 57 | |
| 58 | 58 | const windowsHost = HostProcessPlatform.defaultValue() === "win32"; |
| 59 | 59 | const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null"; |
| 60 | const closeIfOpen = (fd: number) => { | |
| 61 | try { | |
| 62 | NodeFS.closeSync(fd); | |
| 63 | } catch (error) { | |
| 64 | if ((error as NodeJS.ErrnoException).code !== "EBADF") throw error; | |
| 65 | } | |
| 66 | }; | |
| 67 | ||
| 68 | // A successful Windows read streams the inherited fd with autoClose. POSIX | |
| 69 | // reopens the fd through /proc or /dev, so the test still owns the original. | |
| 70 | const openBootstrapInputFd = (filePath: string) => | |
| 71 | Effect.acquireRelease( | |
| 72 | Effect.sync(() => NodeFS.openSync(filePath, "r")), | |
| 73 | (fd) => (windowsHost ? Effect.void : Effect.sync(() => closeIfOpen(fd))), | |
| 74 | ); | |
| 75 | ||
| 60 | 76 | const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String }); |
| 61 | 77 | const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema)); |
| 62 | 78 | |
| ... | ... | @@ -71,10 +87,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 71 | 87 | `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`, |
| 72 | 88 | ); |
| 73 | 89 | |
| 74 | const fd = yield* Effect.acquireRelease( | |
| 75 | Effect.sync(() => NodeFS.openSync(filePath, "r")), | |
| 76 | (fd) => Effect.sync(() => NodeFS.closeSync(fd)), | |
| 77 | ); | |
| 90 | const fd = yield* openBootstrapInputFd(filePath); | |
| 78 | 91 | |
| 79 | 92 | const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 }); |
| 80 | 93 | assertSome(payload, { |
| ... | ... | @@ -119,7 +132,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 119 | 132 | const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" }); |
| 120 | 133 | const fd = yield* Effect.acquireRelease( |
| 121 | 134 | Effect.sync(() => NodeFS.openSync(filePath, "r")), |
| 122 | (fd) => Effect.sync(() => NodeFS.closeSync(fd)), | |
| 135 | (fd) => Effect.sync(() => closeIfOpen(fd)), | |
| 123 | 136 | ); |
| 124 | 137 | const fdPath = `/proc/self/fd/${fd}`; |
| 125 | 138 | |
| ... | ... | @@ -160,7 +173,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 160 | 173 | Effect.gen(function* () { |
| 161 | 174 | const fd = yield* Effect.acquireRelease( |
| 162 | 175 | Effect.sync(() => NodeFS.openSync(nullDevice, "r")), |
| 163 | (fd) => Effect.sync(() => NodeFS.closeSync(fd)), | |
| 176 | (fd) => Effect.sync(() => closeIfOpen(fd)), | |
| 164 | 177 | ); |
| 165 | 178 | |
| 166 | 179 | fstatSyncInterceptor.failFd = fd; |
| ... | ... | @@ -185,10 +198,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 185 | 198 | const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" }); |
| 186 | 199 | yield* fs.writeFileString(filePath, '{"mode":42}\n'); |
| 187 | 200 | |
| 188 | const fd = yield* Effect.acquireRelease( | |
| 189 | Effect.sync(() => NodeFS.openSync(filePath, "r")), | |
| 190 | (fd) => Effect.sync(() => NodeFS.closeSync(fd)), | |
| 191 | ); | |
| 201 | const fd = yield* openBootstrapInputFd(filePath); | |
| 192 | 202 | const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { |
| 193 | 203 | timeoutMs: 100, |
| 194 | 204 | }).pipe(Effect.flip); |
| ... | ... | @@ -228,7 +238,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 228 | 238 | |
| 229 | 239 | const fd = yield* Effect.acquireRelease( |
| 230 | 240 | Effect.sync(() => NodeFS.openSync(fifoPath, "r")), |
| 231 | (fd) => Effect.sync(() => NodeFS.closeSync(fd)), | |
| 241 | (fd) => Effect.sync(() => closeIfOpen(fd)), | |
| 232 | 242 | ); |
| 233 | 243 | |
| 234 | 244 | const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { |
apps/server/src/cli/config.test.ts+15-4| ... | ... | @@ -60,7 +60,16 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { |
| 60 | 60 | yield* fs.writeFileString(filePath, `${encoded}\n`); |
| 61 | 61 | return yield* Effect.acquireRelease( |
| 62 | 62 | Effect.sync(() => NodeFS.openSync(filePath, "r")), |
| 63 | (fd) => Effect.sync(() => NodeFS.closeSync(fd)), | |
| 63 | // Without a /proc or /dev/fd path to reopen, the reader consumes the fd | |
| 64 | // itself (autoClose), so on Windows it is already closed here. | |
| 65 | (fd) => | |
| 66 | Effect.sync(() => { | |
| 67 | try { | |
| 68 | NodeFS.closeSync(fd); | |
| 69 | } catch (error) { | |
| 70 | if ((error as NodeJS.ErrnoException).code !== "EBADF") throw error; | |
| 71 | } | |
| 72 | }), | |
| 64 | 73 | ); |
| 65 | 74 | }); |
| 66 | 75 | |
| ... | ... | @@ -281,13 +290,15 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { |
| 281 | 290 | |
| 282 | 291 | it.effect("uses bootstrap envelope values as fallbacks when flags and env are absent", () => |
| 283 | 292 | Effect.gen(function* () { |
| 284 | const { join } = yield* Path.Path; | |
| 285 | const baseDir = "/tmp/t3-bootstrap-home"; | |
| 293 | const { join, resolve } = yield* Path.Path; | |
| 294 | // The resolver absolutises the configured home, so the expectation must | |
| 295 | // carry the host's drive on Windows. | |
| 296 | const baseDir = resolve("/tmp/t3-bootstrap-home"); | |
| 286 | 297 | const fd = yield* openBootstrapFd( |
| 287 | 298 | makeDesktopBootstrap({ |
| 288 | 299 | port: 4888, |
| 289 | 300 | host: "127.0.0.2", |
| 290 | t3Home: baseDir, | |
| 301 | t3Home: "/tmp/t3-bootstrap-home", | |
| 291 | 302 | noBrowser: true, |
| 292 | 303 | desktopBootstrapToken: "desktop-token", |
| 293 | 304 | desktopTelemetryFd: 4, |
apps/server/src/cloud/bootService.test.ts+6-3| ... | ... | @@ -529,11 +529,14 @@ it.layer(NodeServices.layer)("boot service install", (it) => { |
| 529 | 529 | it.effect("installs, reports current state, and uninstalls on macOS", () => |
| 530 | 530 | Effect.gen(function* () { |
| 531 | 531 | const { service, fs, statePath, commands, timeouts } = yield* makeHarness("darwin"); |
| 532 | const path = yield* Path.Path; | |
| 532 | 533 | const plan = yield* service.install(); |
| 533 | 534 | |
| 534 | expect(plan.unitPath.endsWith("Library/LaunchAgents/com.t3tools.t3code.service.plist")).toBe( | |
| 535 | true, | |
| 536 | ); | |
| 535 | expect( | |
| 536 | plan.unitPath.endsWith( | |
| 537 | path.join("Library", "LaunchAgents", "com.t3tools.t3code.service.plist"), | |
| 538 | ), | |
| 539 | ).toBe(true); | |
| 537 | 540 | expect(yield* fs.readFileString(plan.unitPath)).toContain( |
| 538 | 541 | ` <key>PATH</key>\n <string>${macInstallerPath}:/usr/local/bin:/usr/sbin:/sbin</string>`, |
| 539 | 542 | ); |
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+2-1| ... | ... | @@ -421,7 +421,8 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 421 | 421 | modelSelection: { instanceId, model: nativeAlternative }, |
| 422 | 422 | }); |
| 423 | 423 | expect(second.model).toBe(nativeAlternative); |
| 424 | expect(second.cwd).toBe("/tmp"); | |
| 424 | // The adapter resolves the cwd it was given through the host Path. | |
| 425 | expect(second.cwd).toBe((yield* Path.Path).resolve("/tmp")); | |
| 425 | 426 | expect(h.launches[1]?.resumeSessionId).toBe(nativeSessionId); |
| 426 | 427 | expect(h.calls).toEqual([ |
| 427 | 428 | "start", |
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+13-4| ... | ... | @@ -24,6 +24,7 @@ |
| 24 | 24 | */ |
| 25 | 25 | import { describe, expect, it } from "@effect/vitest"; |
| 26 | 26 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 27 | import * as Path from "effect/Path"; | |
| 27 | 28 | import { |
| 28 | 29 | type ClaudeSettings, |
| 29 | 30 | type CodexSettings, |
| ... | ... | @@ -214,15 +215,19 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => { |
| 214 | 215 | expect(personalSnapshot.instanceId).toBe(personalId); |
| 215 | 216 | expect(personalSnapshot.driver).toBe(codexDriverKind); |
| 216 | 217 | expect(personalSnapshot.enabled).toBe(false); |
| 218 | // The layout resolves the configured home through the host Path. | |
| 219 | const path = yield* Path.Path; | |
| 217 | 220 | expect(personalSnapshot.continuation?.groupKey).toBe( |
| 218 | "codex:home:/home/julius/.codex_personal", | |
| 221 | `codex:home:${path.resolve("/home/julius/.codex_personal")}`, | |
| 219 | 222 | ); |
| 220 | 223 | |
| 221 | 224 | const workSnapshot = yield* work!.snapshot.getSnapshot; |
| 222 | 225 | expect(workSnapshot.instanceId).toBe(workId); |
| 223 | 226 | expect(workSnapshot.driver).toBe(codexDriverKind); |
| 224 | 227 | expect(workSnapshot.enabled).toBe(false); |
| 225 | expect(workSnapshot.continuation?.groupKey).toBe("codex:home:/home/julius/.codex"); | |
| 228 | expect(workSnapshot.continuation?.groupKey).toBe( | |
| 229 | `codex:home:${path.resolve("/home/julius/.codex")}`, | |
| 230 | ); | |
| 226 | 231 | |
| 227 | 232 | // Nothing goes to the unavailable bucket — both drivers are registered. |
| 228 | 233 | const unavailable = yield* registry.listUnavailable; |
| ... | ... | @@ -451,13 +456,17 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => { |
| 451 | 456 | expect(codexSnapshot.instanceId).toBe(codexId); |
| 452 | 457 | expect(codexSnapshot.driver).toBe(codexDriverKind); |
| 453 | 458 | expect(codexSnapshot.enabled).toBe(false); |
| 454 | expect(codexSnapshot.continuation?.groupKey).toBe("codex:home:/home/julius/.codex"); | |
| 459 | expect(codexSnapshot.continuation?.groupKey).toBe( | |
| 460 | `codex:home:${(yield* Path.Path).resolve("/home/julius/.codex")}`, | |
| 461 | ); | |
| 455 | 462 | |
| 456 | 463 | const claudeSnapshot = yield* claude!.snapshot.getSnapshot; |
| 457 | 464 | expect(claudeSnapshot.instanceId).toBe(claudeId); |
| 458 | 465 | expect(claudeSnapshot.driver).toBe(claudeDriverKind); |
| 459 | 466 | expect(claudeSnapshot.enabled).toBe(false); |
| 460 | expect(claudeSnapshot.continuation?.groupKey).toBe("claude:home:/home/julius/.claude-work"); | |
| 467 | expect(claudeSnapshot.continuation?.groupKey).toBe( | |
| 468 | `claude:home:${(yield* Path.Path).resolve("/home/julius/.claude-work")}`, | |
| 469 | ); | |
| 461 | 470 | |
| 462 | 471 | const cursorSnapshot = yield* cursor!.snapshot.getSnapshot; |
| 463 | 472 | expect(cursorSnapshot.instanceId).toBe(cursorId); |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+32-31| ... | ... | @@ -3,6 +3,7 @@ import { describe, it, assert } from "@effect/vitest"; |
| 3 | 3 | import * as DateTime from "effect/DateTime"; |
| 4 | 4 | import * as Deferred from "effect/Deferred"; |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Path from "effect/Path"; | |
| 6 | 7 | import * as Exit from "effect/Exit"; |
| 7 | 8 | import * as Fiber from "effect/Fiber"; |
| 8 | 9 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -346,6 +347,21 @@ function makeMutableServerSettingsService( |
| 346 | 347 | }); |
| 347 | 348 | } |
| 348 | 349 | |
| 350 | // The registry writes the status cache and only then publishes the change, so | |
| 351 | // a subscriber that sees `checkedAt` on the stream knows the file is on disk. | |
| 352 | // Subscribed before the publish that triggers it; a spin on the file would | |
| 353 | // race the write and lose on a slow host. | |
| 354 | const awaitPersistedProvider = ( | |
| 355 | registry: ProviderRegistry.ProviderRegistry["Service"], | |
| 356 | checkedAt: string, | |
| 357 | ) => | |
| 358 | registry.streamChanges.pipe( | |
| 359 | Stream.filter((providers) => providers.some((provider) => provider.checkedAt === checkedAt)), | |
| 360 | Stream.take(1), | |
| 361 | Stream.runDrain, | |
| 362 | Effect.forkScoped, | |
| 363 | ); | |
| 364 | ||
| 349 | 365 | it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))( |
| 350 | 366 | "ProviderRegistry", |
| 351 | 367 | (it) => { |
| ... | ... | @@ -1752,18 +1768,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1752 | 1768 | assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [ |
| 1753 | 1769 | ...initialProvider.models, |
| 1754 | 1770 | ]); |
| 1771 | const persisted = yield* awaitPersistedProvider(registry, refreshedProvider.checkedAt); | |
| 1755 | 1772 | yield* PubSub.publish(changes, refreshedProvider); |
| 1756 | ||
| 1757 | let cachedProvider = yield* readProviderStatusCache(filePath); | |
| 1758 | for ( | |
| 1759 | let attempt = 0; | |
| 1760 | attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt; | |
| 1761 | attempt += 1 | |
| 1762 | ) { | |
| 1763 | yield* TestClock.adjust("10 millis"); | |
| 1764 | yield* Effect.yieldNow; | |
| 1765 | cachedProvider = yield* readProviderStatusCache(filePath); | |
| 1766 | } | |
| 1773 | yield* Fiber.join(persisted); | |
| 1774 | const cachedProvider = yield* readProviderStatusCache(filePath); | |
| 1767 | 1775 | |
| 1768 | 1776 | assert.deepStrictEqual(cachedProvider, { |
| 1769 | 1777 | ...refreshedProvider, |
| ... | ... | @@ -1878,31 +1886,23 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1878 | 1886 | instanceId: openCodeInstanceId, |
| 1879 | 1887 | }); |
| 1880 | 1888 | |
| 1889 | const authoritativePersisted = yield* awaitPersistedProvider( | |
| 1890 | registry, | |
| 1891 | authoritativeProvider.checkedAt, | |
| 1892 | ); | |
| 1881 | 1893 | yield* PubSub.publish(changes, authoritativeProvider); |
| 1882 | ||
| 1894 | yield* Fiber.join(authoritativePersisted); | |
| 1883 | 1895 | let cachedProvider = yield* readProviderStatusCache(filePath); |
| 1884 | for ( | |
| 1885 | let attempt = 0; | |
| 1886 | attempt < 50 && cachedProvider?.checkedAt !== authoritativeProvider.checkedAt; | |
| 1887 | attempt += 1 | |
| 1888 | ) { | |
| 1889 | yield* TestClock.adjust("10 millis"); | |
| 1890 | yield* Effect.yieldNow; | |
| 1891 | cachedProvider = yield* readProviderStatusCache(filePath); | |
| 1892 | } | |
| 1893 | 1896 | |
| 1894 | 1897 | assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]); |
| 1895 | 1898 | |
| 1899 | const failedPersisted = yield* awaitPersistedProvider( | |
| 1900 | registry, | |
| 1901 | failedProvider.checkedAt, | |
| 1902 | ); | |
| 1896 | 1903 | yield* PubSub.publish(changes, failedProvider); |
| 1897 | for ( | |
| 1898 | let attempt = 0; | |
| 1899 | attempt < 50 && cachedProvider?.checkedAt !== failedProvider.checkedAt; | |
| 1900 | attempt += 1 | |
| 1901 | ) { | |
| 1902 | yield* TestClock.adjust("10 millis"); | |
| 1903 | yield* Effect.yieldNow; | |
| 1904 | cachedProvider = yield* readProviderStatusCache(filePath); | |
| 1905 | } | |
| 1904 | yield* Fiber.join(failedPersisted); | |
| 1905 | cachedProvider = yield* readProviderStatusCache(filePath); | |
| 1906 | 1906 | |
| 1907 | 1907 | assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]); |
| 1908 | 1908 | assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [ |
| ... | ... | @@ -2715,9 +2715,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 2715 | 2715 | claudeCapabilities(), |
| 2716 | 2716 | ); |
| 2717 | 2717 | assert.strictEqual(status.status, "ready"); |
| 2718 | // The home is resolved through the host Path before it reaches the env. | |
| 2718 | 2719 | assert.deepStrictEqual( |
| 2719 | 2720 | recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR), |
| 2720 | [claudeConfigDir], | |
| 2721 | [(yield* Path.Path).resolve(claudeConfigDir)], | |
| 2721 | 2722 | ); |
| 2722 | 2723 | }).pipe(Effect.provide(recorded.layer)); |
| 2723 | 2724 | }); |
apps/server/src/server.test.ts+10-1| ... | ... | @@ -1855,12 +1855,18 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 1855 | 1855 | const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-replace-" }); |
| 1856 | 1856 | const beforeOpenPath = path.join(staticDir, "before-open.txt"); |
| 1857 | 1857 | const afterOpenPath = path.join(staticDir, "after-open.txt"); |
| 1858 | const afterOpenSnapshotPath = path.join(staticDir, "after-open-snapshot.txt"); | |
| 1859 | const windowsHost = HostProcessPlatform.defaultValue() === "win32"; | |
| 1858 | 1860 | const original = "original bytes"; |
| 1859 | 1861 | const replacement = "replacement bytes with a different size"; |
| 1860 | 1862 | for (const filePath of [beforeOpenPath, afterOpenPath]) { |
| 1861 | 1863 | yield* fileSystem.writeFileString(filePath, original); |
| 1862 | 1864 | yield* fileSystem.writeFileString(`${filePath}.next`, replacement); |
| 1863 | 1865 | } |
| 1866 | if (windowsHost) { | |
| 1867 | // Windows cannot replace an open destination, so model the race with its original handle. | |
| 1868 | yield* fileSystem.writeFileString(afterOpenSnapshotPath, original); | |
| 1869 | } | |
| 1864 | 1870 | const replaced = new Set<string>(); |
| 1865 | 1871 | const replaceOnce = Effect.fnUntraced(function* (filePath: string) { |
| 1866 | 1872 | if (replaced.has(filePath)) return; |
| ... | ... | @@ -1877,7 +1883,10 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 1877 | 1883 | ), |
| 1878 | 1884 | open: (filePath, options) => |
| 1879 | 1885 | fileSystem |
| 1880 | .open(filePath, options) | |
| 1886 | .open( | |
| 1887 | filePath === afterOpenPath && windowsHost ? afterOpenSnapshotPath : filePath, | |
| 1888 | options, | |
| 1889 | ) | |
| 1881 | 1890 | .pipe( |
| 1882 | 1891 | Effect.tap(() => (filePath === afterOpenPath ? replaceOnce(filePath) : Effect.void)), |
| 1883 | 1892 | ), |
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+3-1| ... | ... | @@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 3 | import { assert, it } from "@effect/vitest"; |
| 4 | 4 | import * as Effect from "effect/Effect"; |
| 5 | 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; | |
| 6 | 7 | import * as Layer from "effect/Layer"; |
| 7 | 8 | import * as PlatformError from "effect/PlatformError"; |
| 8 | 9 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| ... | ... | @@ -153,10 +154,11 @@ it.effect("preserves provider failures without deriving the repository message f |
| 153 | 154 | it.effect("clones a looked-up repository into the requested destination", () => |
| 154 | 155 | Effect.gen(function* () { |
| 155 | 156 | const fs = yield* FileSystem.FileSystem; |
| 157 | const path = yield* Path.Path; | |
| 156 | 158 | const parent = yield* fs.makeTempDirectoryScoped({ |
| 157 | 159 | prefix: "t3-source-control-clone-parent-", |
| 158 | 160 | }); |
| 159 | const destinationPath = `${parent}/t3code`; | |
| 161 | const destinationPath = path.join(parent, "t3code"); | |
| 160 | 162 | const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = []; |
| 161 | 163 | |
| 162 | 164 | yield* Effect.gen(function* () { |
apps/server/src/workspace/WorkspaceFileSystem.test.ts+23-19| ... | ... | @@ -14,6 +14,7 @@ import * as VcsProcess from "../vcs/VcsProcess.ts"; |
| 14 | 14 | import * as WorkspaceEntries from "./WorkspaceEntries.ts"; |
| 15 | 15 | import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts"; |
| 16 | 16 | import * as WorkspacePaths from "./WorkspacePaths.ts"; |
| 17 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 17 | 18 | import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; |
| 18 | 19 | |
| 19 | 20 | const ProjectLayer = WorkspaceFileSystem.layer.pipe( |
| ... | ... | @@ -100,28 +101,31 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i |
| 100 | 101 | }), |
| 101 | 102 | ); |
| 102 | 103 | |
| 103 | it.effect("rejects a FIFO without blocking on open", () => | |
| 104 | Effect.gen(function* () { | |
| 105 | const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; | |
| 106 | const path = yield* Path.Path; | |
| 107 | const cwd = yield* makeTempDir; | |
| 108 | const outsideDir = yield* makeTempDir; | |
| 109 | const fifoPath = path.join(outsideDir, "pipe"); | |
| 110 | yield* Effect.promise( | |
| 111 | () => | |
| 112 | new Promise<void>((resolve, reject) => | |
| 113 | NodeChildProcess.execFile("mkfifo", [fifoPath], (error) => | |
| 114 | error ? reject(error) : resolve(), | |
| 104 | // Needs mkfifo; Windows has no FIFOs to reject. | |
| 105 | it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( | |
| 106 | "rejects a FIFO without blocking on open", | |
| 107 | () => | |
| 108 | Effect.gen(function* () { | |
| 109 | const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; | |
| 110 | const path = yield* Path.Path; | |
| 111 | const cwd = yield* makeTempDir; | |
| 112 | const outsideDir = yield* makeTempDir; | |
| 113 | const fifoPath = path.join(outsideDir, "pipe"); | |
| 114 | yield* Effect.promise( | |
| 115 | () => | |
| 116 | new Promise<void>((resolve, reject) => | |
| 117 | NodeChildProcess.execFile("mkfifo", [fifoPath], (error) => | |
| 118 | error ? reject(error) : resolve(), | |
| 119 | ), | |
| 115 | 120 | ), |
| 116 | ), | |
| 117 | ); | |
| 121 | ); | |
| 118 | 122 | |
| 119 | const error = yield* workspaceFileSystem | |
| 120 | .readFile({ cwd, relativePath: fifoPath }) | |
| 121 | .pipe(Effect.flip); | |
| 123 | const error = yield* workspaceFileSystem | |
| 124 | .readFile({ cwd, relativePath: fifoPath }) | |
| 125 | .pipe(Effect.flip); | |
| 122 | 126 | |
| 123 | expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError); | |
| 124 | }), | |
| 127 | expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError); | |
| 128 | }), | |
| 125 | 129 | ); |
| 126 | 130 | |
| 127 | 131 | it.effect("rejects reads outside the workspace root", () => |