mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-13 04:04:44 +00:00
effectify Installation service, drop Effect suffix from namespaces (#18266)
This commit is contained in:
@@ -3,7 +3,7 @@ import { Duration, Effect, Layer, Option, Schema } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
|
||||
import { AccountRepo } from "../../src/account/repo"
|
||||
import { AccountEffect } from "../../src/account/effect"
|
||||
import { Account } from "../../src/account/effect"
|
||||
import { AccessToken, AccountID, DeviceCode, Login, Org, OrgID, RefreshToken, UserCode } from "../../src/account/schema"
|
||||
import { Database } from "../../src/storage/db"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -19,7 +19,7 @@ const truncate = Layer.effectDiscard(
|
||||
const it = testEffect(Layer.merge(AccountRepo.layer, truncate))
|
||||
|
||||
const live = (client: HttpClient.HttpClient) =>
|
||||
AccountEffect.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client)))
|
||||
Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client)))
|
||||
|
||||
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
|
||||
HttpClientResponse.fromWeb(
|
||||
@@ -52,7 +52,7 @@ const deviceTokenClient = (body: unknown, status = 400) =>
|
||||
)
|
||||
|
||||
const poll = (body: unknown, status = 400) =>
|
||||
AccountEffect.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
|
||||
Account.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(deviceTokenClient(body, status))))
|
||||
|
||||
it.effect("orgsByAccount groups orgs per account", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -97,7 +97,7 @@ it.effect("orgsByAccount groups orgs per account", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
const rows = yield* AccountEffect.Service.use((s) => s.orgsByAccount()).pipe(Effect.provide(live(client)))
|
||||
const rows = yield* Account.Service.use((s) => s.orgsByAccount()).pipe(Effect.provide(live(client)))
|
||||
|
||||
expect(rows.map((row) => [row.account.id, row.orgs.map((org) => org.id)]).map(([id, orgs]) => [id, orgs])).toEqual([
|
||||
[AccountID.make("user-1"), [OrgID.make("org-1")]],
|
||||
@@ -135,7 +135,7 @@ it.effect("token refresh persists the new token", () =>
|
||||
),
|
||||
)
|
||||
|
||||
const token = yield* AccountEffect.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client)))
|
||||
const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client)))
|
||||
|
||||
expect(Option.getOrThrow(token)).toBeDefined()
|
||||
expect(String(Option.getOrThrow(token))).toBe("at_new")
|
||||
@@ -178,7 +178,7 @@ it.effect("config sends the selected org header", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
const cfg = yield* AccountEffect.Service.use((s) => s.config(id, OrgID.make("org-9"))).pipe(
|
||||
const cfg = yield* Account.Service.use((s) => s.config(id, OrgID.make("org-9"))).pipe(
|
||||
Effect.provide(live(client)),
|
||||
)
|
||||
|
||||
@@ -209,7 +209,7 @@ it.effect("poll stores the account and first org on success", () =>
|
||||
),
|
||||
)
|
||||
|
||||
const res = yield* AccountEffect.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(client)))
|
||||
const res = yield* Account.Service.use((s) => s.poll(login())).pipe(Effect.provide(live(client)))
|
||||
|
||||
expect(res._tag).toBe("PollSuccess")
|
||||
if (res._tag === "PollSuccess") {
|
||||
|
||||
@@ -1,47 +1,155 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { Installation } from "../../src/installation"
|
||||
|
||||
const fetch0 = globalThis.fetch
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = fetch0
|
||||
})
|
||||
function mockHttpClient(handler: (request: HttpClientRequest.HttpClientRequest) => Response) {
|
||||
const client = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler(request))))
|
||||
return Layer.succeed(HttpClient.HttpClient, client)
|
||||
}
|
||||
|
||||
function mockSpawner(handler: (cmd: string, args: readonly string[]) => string = () => "") {
|
||||
const spawner = ChildProcessSpawner.make((command) => {
|
||||
const std = ChildProcess.isStandardCommand(command) ? command : undefined
|
||||
const output = handler(std?.command ?? "", std?.args ?? [])
|
||||
return Effect.succeed(
|
||||
ChildProcessSpawner.makeHandle({
|
||||
pid: ChildProcessSpawner.ProcessId(0),
|
||||
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
|
||||
isRunning: Effect.succeed(false),
|
||||
kill: () => Effect.void,
|
||||
stdin: { [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") } as any,
|
||||
stdout: output ? Stream.make(encoder.encode(output)) : Stream.empty,
|
||||
stderr: Stream.empty,
|
||||
all: Stream.empty,
|
||||
getInputFd: () => ({ [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") }) as any,
|
||||
getOutputFd: () => Stream.empty,
|
||||
}),
|
||||
)
|
||||
})
|
||||
return Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
function testLayer(
|
||||
httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
|
||||
spawnHandler?: (cmd: string, args: readonly string[]) => string,
|
||||
) {
|
||||
return Installation.layer.pipe(
|
||||
Layer.provide(mockHttpClient(httpHandler)),
|
||||
Layer.provide(mockSpawner(spawnHandler)),
|
||||
)
|
||||
}
|
||||
|
||||
describe("installation", () => {
|
||||
test("reads release version from GitHub releases", async () => {
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ tag_name: "v1.2.3" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as unknown as typeof fetch
|
||||
describe("latest", () => {
|
||||
test("reads release version from GitHub releases", async () => {
|
||||
const layer = testLayer(() => jsonResponse({ tag_name: "v1.2.3" }))
|
||||
|
||||
expect(await Installation.latest("unknown")).toBe("1.2.3")
|
||||
})
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("unknown")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("1.2.3")
|
||||
})
|
||||
|
||||
test("reads scoop manifest versions", async () => {
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify({ version: "2.3.4" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})) as unknown as typeof fetch
|
||||
test("strips v prefix from GitHub release tag", async () => {
|
||||
const layer = testLayer(() => jsonResponse({ tag_name: "v4.0.0-beta.1" }))
|
||||
|
||||
expect(await Installation.latest("scoop")).toBe("2.3.4")
|
||||
})
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("curl")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("4.0.0-beta.1")
|
||||
})
|
||||
|
||||
test("reads chocolatey feed versions", async () => {
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
d: {
|
||||
results: [{ Version: "3.4.5" }],
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
test("reads npm registry versions", async () => {
|
||||
const layer = testLayer(
|
||||
() => jsonResponse({ version: "1.5.0" }),
|
||||
(cmd, args) => {
|
||||
if (cmd === "npm" && args.includes("registry")) return "https://registry.npmjs.org\n"
|
||||
return ""
|
||||
},
|
||||
)) as unknown as typeof fetch
|
||||
)
|
||||
|
||||
expect(await Installation.latest("choco")).toBe("3.4.5")
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("npm")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("1.5.0")
|
||||
})
|
||||
|
||||
test("reads npm registry versions for bun method", async () => {
|
||||
const layer = testLayer(
|
||||
() => jsonResponse({ version: "1.6.0" }),
|
||||
() => "",
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("bun")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("1.6.0")
|
||||
})
|
||||
|
||||
test("reads scoop manifest versions", async () => {
|
||||
const layer = testLayer(() => jsonResponse({ version: "2.3.4" }))
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("scoop")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("2.3.4")
|
||||
})
|
||||
|
||||
test("reads chocolatey feed versions", async () => {
|
||||
const layer = testLayer(() => jsonResponse({ d: { results: [{ Version: "3.4.5" }] } }))
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("choco")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("3.4.5")
|
||||
})
|
||||
|
||||
test("reads brew formulae API versions", async () => {
|
||||
const layer = testLayer(
|
||||
() => jsonResponse({ versions: { stable: "2.0.0" } }),
|
||||
(cmd, args) => {
|
||||
// getBrewFormula: return core formula (no tap)
|
||||
if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return ""
|
||||
if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode"
|
||||
return ""
|
||||
},
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("2.0.0")
|
||||
})
|
||||
|
||||
test("reads brew tap info JSON via CLI", async () => {
|
||||
const brewInfoJson = JSON.stringify({
|
||||
formulae: [{ versions: { stable: "2.1.0" } }],
|
||||
})
|
||||
const layer = testLayer(
|
||||
() => jsonResponse({}), // HTTP not used for tap formula
|
||||
(cmd, args) => {
|
||||
if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula"))
|
||||
return "opencode"
|
||||
if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson
|
||||
return ""
|
||||
},
|
||||
)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)),
|
||||
)
|
||||
expect(result).toBe("2.1.0")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, test, expect } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Effect, FileSystem, Layer } from "effect"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { TruncateEffect } from "../../src/tool/truncate-effect"
|
||||
import { Truncate as TruncateSvc } from "../../src/tool/truncate-effect"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { Process } from "../../src/util/process"
|
||||
import { Filesystem } from "../../src/util/filesystem"
|
||||
@@ -139,7 +139,7 @@ describe("Truncate", () => {
|
||||
|
||||
describe("cleanup", () => {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
const it = testEffect(Layer.mergeAll(TruncateEffect.defaultLayer, NodeFileSystem.layer))
|
||||
const it = testEffect(Layer.mergeAll(TruncateSvc.defaultLayer, NodeFileSystem.layer))
|
||||
|
||||
it.effect("deletes files older than 7 days and preserves recent files", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -152,7 +152,7 @@ describe("Truncate", () => {
|
||||
|
||||
yield* writeFileStringScoped(old, "old content")
|
||||
yield* writeFileStringScoped(recent, "recent content")
|
||||
yield* TruncateEffect.Service.use((s) => s.cleanup())
|
||||
yield* TruncateSvc.Service.use((s) => s.cleanup())
|
||||
|
||||
expect(yield* fs.exists(old)).toBe(false)
|
||||
expect(yield* fs.exists(recent)).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user