Showing the changes to apps/server/src/workspace/WorkspaceFileSystem.test.ts (1 of 8 files). Show all
Unified · Split
| @@ -104,14 +104,89 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i · 103 unmodified lines | ||
| 104 | 104 | const error = yield* workspaceFileSystem |
| 105 | 105 | .readFile({ cwd, relativePath: "linked-secret.txt" }) |
| 106 | 106 | .pipe(Effect.flip); |
| 107 | + const resolvedWorkspaceRoot = yield* fileSystem.realPath(cwd); | |
| 108 | + const resolvedPath = yield* fileSystem.realPath(path.join(outsideDir, "secret.txt")); | |
| 107 | 109 | |
| 108 | − expect(error.message).toBe( | |
| 109 | − `Workspace file operation 'workspaceFileSystem.readFile' failed for 'linked-secret.txt' in '${cwd}'.`, | |
| 110 | − ); | |
| 110 | + expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceFilePathEscapeError); | |
| 111 | + expect(error).toMatchObject({ | |
| 112 | + workspaceRoot: cwd, | |
| 113 | + relativePath: "linked-secret.txt", | |
| 114 | + resolvedWorkspaceRoot, | |
| 115 | + resolvedPath, | |
| 116 | + }); | |
| 117 | + expect("cause" in error).toBe(false); | |
| 118 | + }), | |
| 119 | + ); | |
| 120 | + | |
| 121 | + it.effect("rejects directories without manufacturing an I/O cause", () => | |
| 122 | + Effect.gen(function* () { | |
| 123 | + const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; | |
| 124 | + const fileSystem = yield* FileSystem.FileSystem; | |
| 125 | + const path = yield* Path.Path; | |
| 126 | + const cwd = yield* makeTempDir; | |
| 127 | + yield* fileSystem.makeDirectory(path.join(cwd, "src")); | |
| 128 | + | |
| 129 | + const error = yield* workspaceFileSystem | |
| 130 | + .readFile({ cwd, relativePath: "src" }) | |
| 131 | + .pipe(Effect.flip); | |
| 132 | + const resolvedPath = yield* fileSystem.realPath(path.join(cwd, "src")); | |
| 133 | + | |
| 134 | + expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError); | |
| 135 | + expect(error).toMatchObject({ | |
| 136 | + workspaceRoot: cwd, | |
| 137 | + relativePath: "src", | |
| 138 | + resolvedPath, | |
| 139 | + }); | |
| 140 | + expect("cause" in error).toBe(false); | |
| 141 | + }), | |
| 142 | + ); | |
| 143 | + | |
| 144 | + it.effect("rejects binary files without leaking their contents into the error", () => | |
| 145 | + Effect.gen(function* () { | |
| 146 | + const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; | |
| 147 | + const fileSystem = yield* FileSystem.FileSystem; | |
| 148 | + const path = yield* Path.Path; | |
| 149 | + const cwd = yield* makeTempDir; | |
| 150 | + const absolutePath = path.join(cwd, "asset.bin"); | |
| 151 | + yield* fileSystem.writeFile(absolutePath, Uint8Array.from([0x61, 0, 0x62])); | |
| 152 | + | |
| 153 | + const error = yield* workspaceFileSystem | |
| 154 | + .readFile({ cwd, relativePath: "asset.bin" }) | |
| 155 | + .pipe(Effect.flip); | |
| 156 | + const resolvedPath = yield* fileSystem.realPath(absolutePath); | |
| 157 | + | |
| 158 | + expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceBinaryFileError); | |
| 159 | + expect(error).toMatchObject({ | |
| 160 | + workspaceRoot: cwd, | |
| 161 | + relativePath: "asset.bin", | |
| 162 | + resolvedPath, | |
| 163 | + }); | |
| 164 | + expect("cause" in error).toBe(false); | |
| 165 | + expect("contents" in error).toBe(false); | |
| 166 | + }), | |
| 167 | + ); | |
| 168 | + | |
| 169 | + it.effect("preserves the real cause and path for I/O failures", () => | |
| 170 | + Effect.gen(function* () { | |
| 171 | + const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem; | |
| 172 | + const path = yield* Path.Path; | |
| 173 | + const cwd = yield* makeTempDir; | |
| 174 | + const resolvedPath = path.join(cwd, "missing.txt"); | |
| 175 | + | |
| 176 | + const error = yield* workspaceFileSystem | |
| 177 | + .readFile({ cwd, relativePath: "missing.txt" }) | |
| 178 | + .pipe(Effect.flip); | |
| 179 | + | |
| 180 | + expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspaceFileSystemOperationError); | |
| 181 | + expect(error).toMatchObject({ | |
| 182 | + workspaceRoot: cwd, | |
| 183 | + relativePath: "missing.txt", | |
| 184 | + resolvedPath, | |
| 185 | + operationPath: resolvedPath, | |
| 186 | + operation: "realpath-target", | |
| 187 | + }); | |
| 111 | 188 | expect(error.cause).toBeInstanceOf(Error); |
| 112 | − expect((error.cause as Error).message).toBe( | |
| 113 | − "Workspace file path resolves outside the project root.", | |
| 114 | − ); | |
| 189 | + expect((error.cause as NodeJS.ErrnoException).code).toBe("ENOENT"); | |
| 115 | 190 | }), |
| 116 | 191 | ); |
| 117 | 192 | }); |