mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-05 08:33:10 +00:00
- Rename packages/opencode → packages/tfcode (directory only) - Rename bin/opencode → bin/tfcode (CLI binary) - Rename .opencode → .tfcode (config directory) - Update package.json name and bin field - Update config directory path references (.tfcode) - Keep internal code references as 'opencode' for easy upstream sync - Keep @opencode-ai/* workspace package names This minimal branding approach allows clean merges from upstream opencode repository while providing tfcode branding for users.
327 lines
9.8 KiB
TypeScript
327 lines
9.8 KiB
TypeScript
import { expect } from "bun:test"
|
|
import { Effect, Layer, Option } from "effect"
|
|
|
|
import { AccountRepo } from "../../src/account/repo"
|
|
import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
|
|
import { Database } from "../../src/storage/db"
|
|
import { testEffect } from "../lib/effect"
|
|
|
|
const truncate = Layer.effectDiscard(
|
|
Effect.sync(() => {
|
|
const db = Database.Client()
|
|
db.run(/*sql*/ `DELETE FROM account_state`)
|
|
db.run(/*sql*/ `DELETE FROM account`)
|
|
}),
|
|
)
|
|
|
|
const it = testEffect(Layer.merge(AccountRepo.layer, truncate))
|
|
|
|
it.effect("list returns empty when no accounts exist", () =>
|
|
Effect.gen(function* () {
|
|
const accounts = yield* AccountRepo.use((r) => r.list())
|
|
expect(accounts).toEqual([])
|
|
}),
|
|
)
|
|
|
|
it.effect("active returns none when no accounts exist", () =>
|
|
Effect.gen(function* () {
|
|
const active = yield* AccountRepo.use((r) => r.active())
|
|
expect(Option.isNone(active)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.effect("persistAccount inserts and getRow retrieves", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
yield* AccountRepo.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((r) => r.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((r) => r.active())
|
|
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-1"))
|
|
}),
|
|
)
|
|
|
|
it.effect("persistAccount sets the active account and org", () =>
|
|
Effect.gen(function* () {
|
|
const id1 = AccountID.make("user-1")
|
|
const id2 = AccountID.make("user-2")
|
|
|
|
yield* AccountRepo.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.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((r) => r.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.effect("list returns all accounts", () =>
|
|
Effect.gen(function* () {
|
|
const id1 = AccountID.make("user-1")
|
|
const id2 = AccountID.make("user-2")
|
|
|
|
yield* AccountRepo.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.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((r) => r.list())
|
|
expect(accounts.length).toBe(2)
|
|
expect(accounts.map((a) => a.email).sort()).toEqual(["a@example.com", "b@example.com"])
|
|
}),
|
|
)
|
|
|
|
it.effect("remove deletes an account", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.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((r) => r.remove(id))
|
|
|
|
const row = yield* AccountRepo.use((r) => r.getRow(id))
|
|
expect(Option.isNone(row)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.effect("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.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.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.use((r) => r.use(id1, Option.some(OrgID.make("org-99"))))
|
|
const active1 = yield* AccountRepo.use((r) => r.active())
|
|
expect(Option.getOrThrow(active1).id).toBe(id1)
|
|
expect(Option.getOrThrow(active1).active_org_id).toBe(OrgID.make("org-99"))
|
|
|
|
yield* AccountRepo.use((r) => r.use(id1, Option.none()))
|
|
const active2 = yield* AccountRepo.use((r) => r.active())
|
|
expect(Option.getOrThrow(active2).active_org_id).toBeNull()
|
|
}),
|
|
)
|
|
|
|
it.effect("persistToken updates token fields", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.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.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((r) => r.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.effect("persistToken with no expiry sets token_expiry to null", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.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.use((r) =>
|
|
r.persistToken({
|
|
accountID: id,
|
|
accessToken: AccessToken.make("new_token"),
|
|
refreshToken: RefreshToken.make("new_refresh"),
|
|
expiry: Option.none(),
|
|
}),
|
|
)
|
|
|
|
const row = yield* AccountRepo.use((r) => r.getRow(id))
|
|
expect(Option.getOrThrow(row).token_expiry).toBeNull()
|
|
}),
|
|
)
|
|
|
|
it.effect("persistAccount upserts on conflict", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.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.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((r) => r.list())
|
|
expect(accounts.length).toBe(1)
|
|
|
|
const row = yield* AccountRepo.use((r) => r.getRow(id))
|
|
const value = Option.getOrThrow(row)
|
|
expect(value.access_token).toBe(AccessToken.make("at_v2"))
|
|
|
|
const active = yield* AccountRepo.use((r) => r.active())
|
|
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2"))
|
|
}),
|
|
)
|
|
|
|
it.effect("remove clears active state when deleting the active account", () =>
|
|
Effect.gen(function* () {
|
|
const id = AccountID.make("user-1")
|
|
|
|
yield* AccountRepo.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((r) => r.remove(id))
|
|
|
|
const active = yield* AccountRepo.use((r) => r.active())
|
|
expect(Option.isNone(active)).toBe(true)
|
|
}),
|
|
)
|
|
|
|
it.effect("getRow returns none for nonexistent account", () =>
|
|
Effect.gen(function* () {
|
|
const row = yield* AccountRepo.use((r) => r.getRow(AccountID.make("nope")))
|
|
expect(Option.isNone(row)).toBe(true)
|
|
}),
|
|
)
|