mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-24 12:15:51 -04:00
207 lines
8.5 KiB
TypeScript
207 lines
8.5 KiB
TypeScript
import path from "path"
|
|
import { describe, expect } from "bun:test"
|
|
import { Effect, Fiber, Layer, Stream } from "effect"
|
|
import { Credential } from "@opencode-ai/core/credential"
|
|
import { Connector } from "@opencode-ai/core/connector"
|
|
import { Database } from "@opencode-ai/core/database/database"
|
|
import { EventV2 } from "@opencode-ai/core/event"
|
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
import { tmpdir } from "./fixture/tmpdir"
|
|
import { testEffect } from "./lib/effect"
|
|
|
|
const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))
|
|
|
|
function testLayer(directory: string) {
|
|
return Credential.layer.pipe(
|
|
Layer.fresh,
|
|
Layer.provide(Database.layerFromPath(path.join(directory, "credential.db")).pipe(Layer.fresh)),
|
|
Layer.provideMerge(EventV2.defaultLayer),
|
|
)
|
|
}
|
|
|
|
describe("Credential", () => {
|
|
it.live("imports supported legacy auth.json credentials once", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) =>
|
|
Effect.gen(function* () {
|
|
yield* Effect.promise(() =>
|
|
Bun.write(
|
|
path.join(tmp.path, "auth.json"),
|
|
JSON.stringify({
|
|
openai: {
|
|
type: "oauth",
|
|
refresh: "refresh",
|
|
access: "access",
|
|
expires: 123,
|
|
accountId: "account",
|
|
},
|
|
azure: { type: "api", key: "key", metadata: { resourceName: "resource" } },
|
|
ignored: { type: "wellknown", key: "TOKEN", token: "secret" },
|
|
}),
|
|
),
|
|
)
|
|
const database = Database.layerFromPath(path.join(tmp.path, "credential.db")).pipe(Layer.fresh)
|
|
const global = Global.layerWith({ data: tmp.path })
|
|
const importer = Credential.legacyImportLayer.pipe(
|
|
Layer.provide(database),
|
|
Layer.provide(FSUtil.defaultLayer),
|
|
Layer.provide(global),
|
|
)
|
|
const credentials = Credential.layer.pipe(
|
|
Layer.provide(database),
|
|
Layer.provide(EventV2.defaultLayer),
|
|
Layer.provideMerge(importer),
|
|
)
|
|
const result = yield* Effect.gen(function* () {
|
|
const service = yield* Credential.Service
|
|
return yield* service.all()
|
|
}).pipe(Effect.provide(credentials), Effect.scoped)
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
connectorID: Connector.ID.make("openai"),
|
|
methodID: Connector.MethodID.make("chatgpt-browser"),
|
|
label: "Imported",
|
|
value: expect.objectContaining({
|
|
type: "oauth",
|
|
refresh: "refresh",
|
|
access: "access",
|
|
expires: 123,
|
|
metadata: { accountID: "account" },
|
|
}),
|
|
}),
|
|
)
|
|
expect(result).toContainEqual(
|
|
expect.objectContaining({
|
|
connectorID: Connector.ID.make("azure"),
|
|
methodID: Connector.MethodID.make("api-key"),
|
|
value: expect.objectContaining({ type: "key", key: "key", metadata: { resourceName: "resource" } }),
|
|
}),
|
|
)
|
|
|
|
yield* importer.pipe(Layer.build, Effect.scoped)
|
|
const after = yield* Effect.gen(function* () {
|
|
return yield* (yield* Credential.Service).all()
|
|
}).pipe(Effect.provide(credentials), Effect.scoped)
|
|
expect(after).toHaveLength(2)
|
|
}),
|
|
),
|
|
),
|
|
)
|
|
|
|
it.live("emits credential lifecycle events", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) =>
|
|
Effect.gen(function* () {
|
|
const credentials = yield* Credential.Service
|
|
const eventSvc = yield* EventV2.Service
|
|
const addedFiber = yield* eventSvc
|
|
.subscribe(Credential.Event.Added)
|
|
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
|
const switchedFiber = yield* eventSvc
|
|
.subscribe(Credential.Event.Switched)
|
|
.pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped)
|
|
const removedFiber = yield* eventSvc
|
|
.subscribe(Credential.Event.Removed)
|
|
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
|
|
|
yield* Effect.yieldNow
|
|
|
|
const first = yield* credentials.create({
|
|
connectorID: Connector.ID.make("lifecycle"),
|
|
methodID: Connector.MethodID.make("key"),
|
|
value: new Credential.Key({ type: "key", key: "raw-key" }),
|
|
})
|
|
expect(first).toBeDefined()
|
|
if (!first) return
|
|
expect(first.label).toBe("default")
|
|
expect(first.value.type).toBe("key")
|
|
if (first.value.type === "key") expect(first.value.key).toBe("raw-key")
|
|
|
|
yield* credentials.update(first.id, { label: "keep" })
|
|
const updated = yield* credentials.get(first.id)
|
|
expect(updated?.label).toBe("keep")
|
|
expect(updated?.value.type).toBe("key")
|
|
if (updated?.value.type === "key") expect(updated.value.key).toBe("raw-key")
|
|
|
|
const second = yield* credentials.create({
|
|
connectorID: Connector.ID.make("lifecycle"),
|
|
methodID: Connector.MethodID.make("key"),
|
|
value: new Credential.Key({ type: "key", key: "second-key" }),
|
|
})
|
|
expect(second).toBeDefined()
|
|
if (!second) return
|
|
|
|
yield* credentials.remove(second.id)
|
|
const added = Array.from(yield* Fiber.join(addedFiber))
|
|
const switched = Array.from(yield* Fiber.join(switchedFiber))
|
|
const removed = Array.from(yield* Fiber.join(removedFiber))
|
|
expect(added.map((event) => event.data.credential.id)).toEqual([first.id, second.id])
|
|
expect(switched.map((event) => event.data)).toEqual([
|
|
{ connectorID: Connector.ID.make("lifecycle"), from: undefined, to: first.id },
|
|
{ connectorID: Connector.ID.make("lifecycle"), from: first.id, to: second.id },
|
|
{ connectorID: Connector.ID.make("lifecycle"), from: second.id, to: first.id },
|
|
])
|
|
expect(removed[0]?.data.credential.id).toBe(second.id)
|
|
}).pipe(Effect.provide(testLayer(tmp.path))),
|
|
),
|
|
),
|
|
)
|
|
|
|
it.live("always switches to newly created credentials", () =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(() => tmpdir()),
|
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
).pipe(
|
|
Effect.flatMap((tmp) =>
|
|
Effect.gen(function* () {
|
|
const credentials = yield* Credential.Service
|
|
const eventSvc = yield* EventV2.Service
|
|
const switchedFiber = yield* eventSvc
|
|
.subscribe(Credential.Event.Switched)
|
|
.pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped)
|
|
|
|
yield* Effect.yieldNow
|
|
|
|
const first = yield* credentials.create({
|
|
connectorID: Connector.ID.make("switch"),
|
|
methodID: Connector.MethodID.make("key"),
|
|
value: new Credential.Key({ type: "key", key: "first-key" }),
|
|
})
|
|
const second = yield* credentials.create({
|
|
connectorID: Connector.ID.make("switch"),
|
|
methodID: Connector.MethodID.make("key"),
|
|
value: new Credential.Key({ type: "key", key: "second-key" }),
|
|
})
|
|
const third = yield* credentials.create({
|
|
connectorID: Connector.ID.make("switch"),
|
|
methodID: Connector.MethodID.make("key"),
|
|
value: new Credential.Key({ type: "key", key: "third-key" }),
|
|
})
|
|
|
|
expect(first).toBeDefined()
|
|
expect(second).toBeDefined()
|
|
expect(third).toBeDefined()
|
|
if (!first || !second || !third) return
|
|
|
|
expect((yield* credentials.active(Connector.ID.make("switch")))?.id).toBe(third.id)
|
|
expect(Array.from(yield* Fiber.join(switchedFiber)).map((event) => event.data)).toEqual([
|
|
{ connectorID: Connector.ID.make("switch"), from: undefined, to: first.id },
|
|
{ connectorID: Connector.ID.make("switch"), from: first.id, to: second.id },
|
|
{ connectorID: Connector.ID.make("switch"), from: second.id, to: third.id },
|
|
])
|
|
}).pipe(Effect.provide(testLayer(tmp.path))),
|
|
),
|
|
),
|
|
)
|
|
})
|