+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { Effect, Layer, Option } from "effect"
|
|
import { sql } from "drizzle-orm"
|
|
|
|
import { AccountRepo } from "../../src/account/repo"
|
|
import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
|
|
import { Database } from "@opencode-ai/core/database/database"
|
|
import { testEffect } from "../lib/effect"
|
|
|
|
const truncate = Layer.effectDiscard(
|
|
Effect.gen(function* () {
|
|
const { db } = yield* Database.Service
|
|
yield* db.run(sql`DELETE FROM account_state`)
|
|
yield* db.run(sql`DELETE FROM account`)
|
|
}),
|
|
)
|
|
const truncateNode = LayerNode.make({ name: "truncate-account", layer: truncate, deps: [Database.node] })
|
|
|
|
const it = testEffect(LayerNode.compile(LayerNode.group([AccountRepo.node, truncateNode])))
|
|
|
|
it.live("list returns empty when no accounts exist", () =>
|
|
Effect.gen(function* () {
|
|
const accounts = yield* AccountRepo.use.list()
|
|
expect(accounts).toEqual([])
|
|
}),
|
|
)
|
|
|
|
it.live("active returns none when no accounts exist", () =>
|
|
Effect.gen(function* () {
|
|
const active = yield* AccountRepo.use.active()
|
|
expect(Option.isNone(active)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.live("persistAccount inserts and getRow retrieves", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_123"),
|
|
refreshToken: RefreshToken.make("rt_456"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.some(OrgID.make("org-1")),
|
|
}),
|
|
)
|
|
|
|
const row = yield* AccountRepo.use.getRow(id)
|
|
expect(Option.isSome(row)).toBe(true)
|
|
const value = Option.getOrThrow(row)
|
|
expect(value.id).toBe(AccountID.make("user-1"))
|
|
expect(value.email).toBe("test@example.com")
|
|
|
|
const active = yield* AccountRepo.use.active()
|
|
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-1"))
|
|
}),
|
|
)
|
|
|
|
it.live("persistAccount normalizes trailing slashes in stored server URLs", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com/",
|
|
accessToken: AccessToken.make("at_123"),
|
|
refreshToken: RefreshToken.make("rt_456"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
const row = yield* AccountRepo.use.getRow(id)
|
|
const active = yield* AccountRepo.use.active()
|
|
const list = yield* AccountRepo.use.list()
|
|
|
|
expect(Option.getOrThrow(row).url).toBe("https://control.example.com")
|
|
expect(Option.getOrThrow(active).url).toBe("https://control.example.com")
|
|
expect(list[0]?.url).toBe("https://control.example.com")
|
|
}),
|
|
)
|
|
|
|
it.live("persistAccount sets the active account and org", () =>
|
|
Effect.gen(function* () {
|
|
const id1 = AccountID.make("user-1")
|
|
const id2 = AccountID.make("user-2")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id: id1,
|
|
email: "first@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_1"),
|
|
refreshToken: RefreshToken.make("rt_1"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.some(OrgID.make("org-1")),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id: id2,
|
|
email: "second@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_2"),
|
|
refreshToken: RefreshToken.make("rt_2"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.some(OrgID.make("org-2")),
|
|
}),
|
|
)
|
|
|
|
// Last persisted account is active with its org
|
|
const active = yield* AccountRepo.use.active()
|
|
expect(Option.isSome(active)).toBe(true)
|
|
expect(Option.getOrThrow(active).id).toBe(AccountID.make("user-2"))
|
|
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2"))
|
|
}),
|
|
)
|
|
|
|
it.live("list returns all accounts", () =>
|
|
Effect.gen(function* () {
|
|
const id1 = AccountID.make("user-1")
|
|
const id2 = AccountID.make("user-2")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id: id1,
|
|
email: "a@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_1"),
|
|
refreshToken: RefreshToken.make("rt_1"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id: id2,
|
|
email: "b@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_2"),
|
|
refreshToken: RefreshToken.make("rt_2"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.some(OrgID.make("org-1")),
|
|
}),
|
|
)
|
|
|
|
const accounts = yield* AccountRepo.use.list()
|
|
expect(accounts.length).toBe(2)
|
|
expect(accounts.map((a) => a.email).sort()).toEqual(["a@example.com", "b@example.com"])
|
|
}),
|
|
)
|
|
|
|
it.live("remove deletes an account", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_1"),
|
|
refreshToken: RefreshToken.make("rt_1"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.use.remove(id)
|
|
|
|
const row = yield* AccountRepo.use.getRow(id)
|
|
expect(Option.isNone(row)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.live("use stores the selected org and marks the account active", () =>
|
|
Effect.gen(function* () {
|
|
const id1 = AccountID.make("user-1")
|
|
const id2 = AccountID.make("user-2")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id: id1,
|
|
email: "first@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_1"),
|
|
refreshToken: RefreshToken.make("rt_1"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id: id2,
|
|
email: "second@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_2"),
|
|
refreshToken: RefreshToken.make("rt_2"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.Service.use((r) => r.use(id1, Option.some(OrgID.make("org-99"))))
|
|
const active1 = yield* AccountRepo.use.active()
|
|
expect(Option.getOrThrow(active1).id).toBe(id1)
|
|
expect(Option.getOrThrow(active1).active_org_id).toBe(OrgID.make("org-99"))
|
|
|
|
yield* AccountRepo.Service.use((r) => r.use(id1, Option.none()))
|
|
const active2 = yield* AccountRepo.use.active()
|
|
expect(Option.getOrThrow(active2).active_org_id).toBeNull()
|
|
}),
|
|
)
|
|
|
|
it.live("persistToken updates token fields", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("old_token"),
|
|
refreshToken: RefreshToken.make("old_refresh"),
|
|
expiry: 1000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
const expiry = Date.now() + 7200_000
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistToken({
|
|
accountID: id,
|
|
accessToken: AccessToken.make("new_token"),
|
|
refreshToken: RefreshToken.make("new_refresh"),
|
|
expiry: Option.some(expiry),
|
|
}),
|
|
)
|
|
|
|
const row = yield* AccountRepo.use.getRow(id)
|
|
const value = Option.getOrThrow(row)
|
|
expect(value.access_token).toBe(AccessToken.make("new_token"))
|
|
expect(value.refresh_token).toBe(RefreshToken.make("new_refresh"))
|
|
expect(value.token_expiry).toBe(expiry)
|
|
}),
|
|
)
|
|
|
|
it.live("persistToken with no expiry sets token_expiry to null", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("old_token"),
|
|
refreshToken: RefreshToken.make("old_refresh"),
|
|
expiry: 1000,
|
|
orgID: Option.none(),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistToken({
|
|
accountID: id,
|
|
accessToken: AccessToken.make("new_token"),
|
|
refreshToken: RefreshToken.make("new_refresh"),
|
|
expiry: Option.none(),
|
|
}),
|
|
)
|
|
|
|
const row = yield* AccountRepo.use.getRow(id)
|
|
expect(Option.getOrThrow(row).token_expiry).toBeNull()
|
|
}),
|
|
)
|
|
|
|
it.live("persistAccount upserts on conflict", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_v1"),
|
|
refreshToken: RefreshToken.make("rt_v1"),
|
|
expiry: 1000,
|
|
orgID: Option.some(OrgID.make("org-1")),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_v2"),
|
|
refreshToken: RefreshToken.make("rt_v2"),
|
|
expiry: 2000,
|
|
orgID: Option.some(OrgID.make("org-2")),
|
|
}),
|
|
)
|
|
|
|
const accounts = yield* AccountRepo.use.list()
|
|
expect(accounts.length).toBe(1)
|
|
|
|
const row = yield* AccountRepo.use.getRow(id)
|
|
const value = Option.getOrThrow(row)
|
|
expect(value.access_token).toBe(AccessToken.make("at_v2"))
|
|
|
|
const active = yield* AccountRepo.use.active()
|
|
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2"))
|
|
}),
|
|
)
|
|
|
|
it.live("remove clears active state when deleting the active account", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.Service.use((r) =>
|
|
r.persistAccount({
|
|
id,
|
|
email: "test@example.com",
|
|
url: "https://control.example.com",
|
|
accessToken: AccessToken.make("at_1"),
|
|
refreshToken: RefreshToken.make("rt_1"),
|
|
expiry: Date.now() + 3600_000,
|
|
orgID: Option.some(OrgID.make("org-1")),
|
|
}),
|
|
)
|
|
|
|
yield* AccountRepo.use.remove(id)
|
|
|
|
const active = yield* AccountRepo.use.active()
|
|
expect(Option.isNone(active)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.live("getRow returns none for nonexistent account", () =>
|
|
Effect.gen(function* () {
|
|
const row = yield* AccountRepo.Service.use((r) => r.getRow(AccountID.make("nope")))
|
|
expect(Option.isNone(row)).toBe(true)
|
|
}),
|
|
)
|