Skip to content

Commit 36a68cb

Browse files
committed
fix(api): address project inventory review notes
1 parent 8885511 commit 36a68cb

4 files changed

Lines changed: 101 additions & 17 deletions

File tree

packages/api/src/http.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ export const makeRouter = () => {
806806
"/health",
807807
Effect.gen(function*(_) {
808808
const cwd = yield* _(resolveWorkspaceRoot(process.cwd()).pipe(Effect.orElseSucceed(() => process.cwd())))
809-
const projectsRoot = defaultProjectsRoot(process.cwd())
809+
const projectsRoot = defaultProjectsRoot(cwd)
810810
return yield* _(jsonResponse({ ok: true, revision: controllerRevision, cwd, projectsRoot }, 200))
811811
}).pipe(Effect.catchAll(errorResponse))
812812
),

packages/api/tests/api-console-routes.test.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { PlatformError } from "@effect/platform/Error"
66
import { NodeContext } from "@effect/platform-node"
77
import { describe, expect, it } from "@effect/vitest"
88
import { Effect } from "effect"
9+
import type * as ParseResult from "effect/ParseResult"
10+
import * as Schema from "effect/Schema"
911
import * as Scope from "effect/Scope"
1012

1113
import { makeRouter } from "../src/http.js"
@@ -20,6 +22,29 @@ const requestApiRoute = (path: string) =>
2022
catch: (cause) => new Error(String(cause))
2123
})
2224

25+
const HealthResponseSchema = Schema.Struct({
26+
cwd: Schema.String,
27+
ok: Schema.Boolean,
28+
projectsRoot: Schema.String,
29+
revision: Schema.NullOr(Schema.String)
30+
})
31+
32+
type HealthResponse = Schema.Schema.Type<typeof HealthResponseSchema>
33+
34+
/**
35+
* Creates a scoped temporary directory and provides its path to the supplied effect.
36+
*
37+
* @param use - Effect factory that receives the temporary directory path.
38+
* @returns Effect that yields the factory result and finalizes the temporary directory scope.
39+
*
40+
* @pure false
41+
* @effect FileSystem service for scoped directory allocation and cleanup
42+
* @invariant the temporary directory lifetime is bounded by the returned Effect scope
43+
* @precondition FileSystem service is available in the environment
44+
* @postcondition the temporary directory scope is finalized after success or failure
45+
* @complexity O(1) allocation; cleanup is O(n) in created filesystem entries
46+
* @throws Never - filesystem and user errors are represented in the Effect error channel
47+
*/
2348
const withTempDir = <A, E, R>(
2449
use: (tempDir: string) => Effect.Effect<A, E, R>
2550
): Effect.Effect<A, E | PlatformError, FileSystem.FileSystem | Exclude<R, Scope.Scope>> =>
@@ -35,6 +60,22 @@ const withTempDir = <A, E, R>(
3560
})
3661
)
3762

63+
/**
64+
* Temporarily sets or unsets an environment variable for the duration of an effect.
65+
*
66+
* @param key - Environment variable name to modify.
67+
* @param value - Temporary value, or undefined to remove the variable.
68+
* @param effect - Effect evaluated while the temporary environment binding is active.
69+
* @returns Effect that yields the supplied effect result and restores the previous binding.
70+
*
71+
* @pure false
72+
* @effect process environment mutation inside acquire/release
73+
* @invariant the previous environment value is restored exactly once during finalization
74+
* @precondition key is a non-empty environment variable name accepted by the runtime
75+
* @postcondition process.env[key] equals its previous value after the scope finalizes
76+
* @complexity O(1) time and space
77+
* @throws Never - user effect errors are represented in the Effect error channel
78+
*/
3879
const withEnvVar = <A, E, R>(
3980
key: string,
4081
value: string | undefined,
@@ -62,14 +103,11 @@ const withEnvVar = <A, E, R>(
62103
).pipe(Effect.flatMap(() => effect))
63104
)
64105

65-
const readResponseJson = (response: Response) =>
106+
const readHealthResponse = (response: Response): Effect.Effect<HealthResponse, Error | ParseResult.ParseError> =>
66107
Effect.tryPromise({
67108
try: () => response.json(),
68109
catch: (cause) => new Error(String(cause))
69-
})
70-
71-
const objectOrNull = (value: unknown): object | null =>
72-
typeof value === "object" && value !== null && !Array.isArray(value) ? value : null
110+
}).pipe(Effect.flatMap(Schema.decodeUnknown(HealthResponseSchema)))
73111

74112
describe("api console routes", () => {
75113
it.effect("does not serve the legacy built-in API console", () =>
@@ -90,12 +128,10 @@ describe("api console routes", () => {
90128
const response = yield* _(
91129
withEnvVar("DOCKER_GIT_PROJECTS_ROOT", projectsRoot, requestApiRoute("/health"))
92130
)
93-
const payload = yield* _(readResponseJson(response))
94-
const objectPayload = objectOrNull(payload)
131+
const payload = yield* _(readHealthResponse(response))
95132

96133
expect(response.status).toBe(200)
97-
expect(objectPayload).not.toBeNull()
98-
expect(Reflect.get(objectPayload ?? {}, "projectsRoot")).toBe(projectsRoot)
134+
expect(payload.projectsRoot).toBe(projectsRoot)
99135
})
100136
).pipe(Effect.provide(NodeContext.layer)))
101137
})

packages/api/tests/projects.test.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
12
import * as FileSystem from "@effect/platform/FileSystem"
23
import type { PlatformError } from "@effect/platform/Error"
34
import * as Path from "@effect/platform/Path"
@@ -129,29 +130,61 @@ const gitEnv: Readonly<Record<string, string>> = {
129130
GIT_TERMINAL_PROMPT: "0"
130131
}
131132

133+
/**
134+
* Runs a git command with the deterministic test git environment.
135+
*
136+
* @param cwd - Working directory for the git process.
137+
* @param args - Git CLI arguments passed without shell interpolation.
138+
* @returns Effect that yields captured stdout or fails with a typed command/platform error.
139+
*
140+
* @pure false
141+
* @effect CommandExecutor service for process execution
142+
* @complexity O(1) process spawn plus O(git operation)
143+
*/
132144
const runGit = (
133145
cwd: string,
134146
args: ReadonlyArray<string>
135-
) =>
147+
): Effect.Effect<string, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
136148
runCommandCapture(
137149
{ cwd, command: "git", args, env: gitEnv },
138150
[0],
139151
(exitCode) => new CommandFailedError({ command: `git ${args[0] ?? ""}`, exitCode })
140152
)
141153

154+
/**
155+
* Runs a shell script with the deterministic test git environment.
156+
*
157+
* @param cwd - Working directory for the shell process.
158+
* @param script - POSIX shell script to execute.
159+
* @returns Effect that yields captured stdout or fails with a typed command/platform error.
160+
*
161+
* @pure false
162+
* @effect CommandExecutor service for process execution
163+
* @complexity O(1) process spawn plus O(script)
164+
*/
142165
const runShell = (
143166
cwd: string,
144167
script: string
145-
) =>
168+
): Effect.Effect<string, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
146169
runCommandCapture(
147170
{ cwd, command: "sh", args: ["-c", script], env: gitEnv },
148171
[0],
149172
(exitCode) => new CommandFailedError({ command: "sh -c", exitCode })
150173
)
151174

175+
/**
176+
* Creates a bare state remote and seeds an initial main branch commit.
177+
*
178+
* @param root - Directory that will contain the remote and seed repositories.
179+
* @returns Effect that yields the bare remote path.
180+
*
181+
* @pure false
182+
* @effect Path and CommandExecutor services for path construction and git processes
183+
* @complexity O(1) filesystem paths plus O(git init + commit + push)
184+
*/
152185
const makeStateRemote = (
153186
root: string
154-
) =>
187+
): Effect.Effect<string, CommandFailedError | PlatformError, Path.Path | CommandExecutor.CommandExecutor> =>
155188
Effect.gen(function*(_) {
156189
const path = yield* _(Path.Path)
157190
const remotePath = path.join(root, "remote.git")
@@ -178,11 +211,24 @@ const makeStateRemote = (
178211
return remotePath
179212
})
180213

214+
/**
215+
* Clones a seeded state remote into a target directory.
216+
*
217+
* @param root - Working directory for the clone command.
218+
* @param remoteUrl - Remote repository URL or path.
219+
* @param target - Target clone directory.
220+
* @returns Effect that yields captured git stdout or fails with a typed command/platform error.
221+
*
222+
* @pure false
223+
* @effect CommandExecutor service for git process execution
224+
* @complexity O(1) process spawn plus O(repository size)
225+
*/
181226
const cloneStateRemote = (
182227
root: string,
183228
remoteUrl: string,
184229
target: string
185-
) => runGit(root, ["clone", remoteUrl, target])
230+
): Effect.Effect<string, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
231+
runGit(root, ["clone", remoteUrl, target])
186232

187233
describe("projects service", () => {
188234
it.effect("seeds host SSH keys into the controller managed authorized_keys file", () =>
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { describe, expect, it } from "@effect/vitest"
2+
import { Effect } from "effect"
23

34
import rootPackage from "../../../../package.json" with { type: "json" }
45
import appPackage from "../../package.json" with { type: "json" }
56

67
describe("Gridland React singleton contract", () => {
7-
it("pins React across workspace dependencies for the Gridland renderer", () => {
8-
expect(rootPackage.overrides.react).toBe(appPackage.dependencies.react.replace(/^\^/u, ""))
9-
})
8+
it.effect("pins React across workspace dependencies for the Gridland renderer", () =>
9+
Effect.sync(() => {
10+
expect(rootPackage.overrides.react).toBe(appPackage.dependencies.react.replace(/^\^/u, ""))
11+
}))
1012
})

0 commit comments

Comments
 (0)