| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052 |
- import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
- import { ConfigV1 } from "@kirincode-ai/core/v1/config/config"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { httpClient } from "@kirincode-ai/core/effect/app-node-platform"
- import { Cause, Effect, Exit, Layer, Option } from "effect"
- import { NamedError } from "@kirincode-ai/core/util/error"
- import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
- import { Config } from "@/config/config"
- import { ConfigManaged } from "@/config/managed"
- import { ConfigParse } from "../../src/config/parse"
- import { Npm } from "@kirincode-ai/core/npm"
- import { InstanceRef } from "../../src/effect/instance-ref"
- import type { InstanceContext } from "../../src/project/instance-context"
- import { Auth } from "../../src/auth"
- import { Account } from "../../src/account/account"
- import { AccessToken, AccountID, OrgID } from "../../src/account/schema"
- import { FSUtil } from "@kirincode-ai/core/fs-util"
- import { Env } from "../../src/env"
- import {
- provideTmpdirInstance,
- TestInstance,
- tmpdir,
- tmpdirScoped,
- withTestInstance,
- provideInstanceEffect,
- testInstanceStoreLayer,
- } from "../fixture/fixture"
- import { InstanceRuntime } from "@/project/instance-runtime"
- import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
- import { testEffect } from "../lib/effect"
- import path from "path"
- import fs from "fs/promises"
- import os from "os"
- import { pathToFileURL } from "url"
- import { Global } from "@kirincode-ai/core/global"
- import { ProjectV2 } from "@kirincode-ai/core/project"
- import { Filesystem } from "@/util/filesystem"
- import { ConfigPlugin } from "@/config/plugin"
- import { ConfigPluginV1 } from "@kirincode-ai/core/v1/config/plugin"
- import { AccountTest } from "../fake/account"
- import { AuthTest } from "../fake/auth"
- import { NpmTest } from "../fake/npm"
- const unexpectedHttp = HttpClient.make((request) =>
- Effect.die(`unexpected http request: ${request.method} ${request.url}`),
- )
- const json = (request: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
- HttpClientResponse.fromWeb(
- request,
- new Response(JSON.stringify(body), {
- status,
- headers: { "content-type": "application/json" },
- }),
- )
- const wellKnownAuth = (url: string) =>
- Layer.mock(Auth.Service)({
- all: () =>
- Effect.succeed({
- [url]: new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
- }),
- })
- function remoteConfigClient(input: {
- wellKnown: unknown
- remote?: unknown
- remoteHtml?: string
- seen: { wellKnown?: string; remote?: string; authorization?: string }
- }) {
- return HttpClient.make((request) => {
- if (request.url.includes(".well-known/opencode")) {
- input.seen.wellKnown = request.url
- return Effect.succeed(json(request, input.wellKnown))
- }
- if (request.url.includes("config.example.com") && (input.remote !== undefined || input.remoteHtml !== undefined)) {
- input.seen.remote = request.url
- input.seen.authorization = request.headers.authorization
- if (input.remoteHtml !== undefined) {
- return Effect.succeed(
- HttpClientResponse.fromWeb(
- request,
- new Response(input.remoteHtml, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }),
- ),
- )
- }
- return Effect.succeed(json(request, input.remote))
- }
- return Effect.succeed(json(request, {}, 404))
- })
- }
- const configLayer = (
- options: {
- auth?: Layer.Layer<Auth.Service>
- account?: Layer.Layer<Account.Service>
- client?: HttpClient.HttpClient
- } = {},
- ) =>
- LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
- [Auth.node, options.auth ?? AuthTest.empty],
- [Account.node, options.account ?? AccountTest.empty],
- [Npm.node, NpmTest.noop],
- [httpClient, Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)],
- ])
- const layer = configLayer()
- const it = testEffect(layer)
- const configIt = (options?: Parameters<typeof configLayer>[0]) => testEffect(configLayer(options))
- const schemaConfig = (config: object) => ({ $schema: "https://kirincode.ai/config.json", ...config })
- const provideCurrentInstance = <A, E, R>(effect: Effect.Effect<A, E, R>, ctx: InstanceContext) =>
- effect.pipe(Effect.provideService(InstanceRef, ctx))
- const load = (ctx: InstanceContext) =>
- Effect.runPromise(
- Config.Service.use((svc) => provideCurrentInstance(svc.get(), ctx)).pipe(Effect.scoped, Effect.provide(layer)),
- )
- const clearEffect = (wait = false) =>
- Config.use
- .invalidate()
- .pipe(
- Effect.scoped,
- Effect.provide(layer),
- Effect.andThen(wait ? Effect.promise(() => InstanceRuntime.disposeAllInstances()) : Effect.void),
- )
- const clear = (wait = false) => Effect.runPromise(clearEffect(wait))
- // Get managed config directory from environment (set in preload.ts)
- const managedConfigDir = process.env.KIRINCODE_TEST_MANAGED_CONFIG_DIR!
- const originalTestToken = process.env.TEST_TOKEN
- const originalConsoleToken = process.env.KIRINCODE_CONSOLE_TOKEN
- beforeEach(async () => {
- await clear(true)
- })
- afterEach(async () => {
- await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
- if (originalTestToken === undefined) delete process.env.TEST_TOKEN
- else process.env.TEST_TOKEN = originalTestToken
- if (originalConsoleToken === undefined) delete process.env.KIRINCODE_CONSOLE_TOKEN
- else process.env.KIRINCODE_CONSOLE_TOKEN = originalConsoleToken
- await clear(true)
- })
- const writeManagedSettingsEffect = (settings: object, filename?: string) =>
- FSUtil.use.writeWithDirs(path.join(managedConfigDir, filename ?? "kirincode.json"), JSON.stringify(settings))
- async function writeConfig(dir: string, config: object, name = "kirincode.json") {
- await Filesystem.write(path.join(dir, name), JSON.stringify(config))
- }
- const writeConfigEffect = (dir: string, config: object, name = "kirincode.json") =>
- FSUtil.use.writeWithDirs(path.join(dir, name), JSON.stringify(config))
- const withInstanceDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
- effect.pipe(
- Effect.provideService(TestInstance, { directory: dir }),
- provideInstanceEffect(dir),
- Effect.provide(testInstanceStoreLayer),
- Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
- )
- const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
- Effect.acquireUseRelease(
- Effect.gen(function* () {
- const previous = Global.Path.config
- ;(Global.Path as { config: string }).config = dir
- yield* clearEffect(true)
- return previous
- }),
- () => effect,
- (previous) =>
- Effect.gen(function* () {
- ;(Global.Path as { config: string }).config = previous
- yield* clearEffect(true)
- }),
- )
- const withGlobalConfig = <A, E, R>(
- input: { config?: object; name?: string },
- fn: (input: { dir: string }) => Effect.Effect<A, E, R>,
- ) =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped()
- if (input.config) yield* writeConfigEffect(dir, schemaConfig(input.config), input.name)
- return yield* withGlobalConfigDir(dir, fn({ dir }))
- })
- const withConfigTree = <A, E, R>(
- input: { global?: object; project?: object; local?: object },
- effect: Effect.Effect<A, E, R>,
- ) =>
- Effect.gen(function* () {
- const root = yield* tmpdirScoped()
- const global = yield* tmpdirScoped()
- const directory = path.join(root, "project")
- yield* Effect.all(
- [
- input.global ? writeConfigEffect(global, schemaConfig(input.global)) : undefined,
- input.project ? writeConfigEffect(directory, schemaConfig(input.project)) : undefined,
- input.local ? writeConfigEffect(path.join(directory, ".kirincode"), schemaConfig(input.local)) : undefined,
- ].filter((effect): effect is Effect.Effect<void, FSUtil.Error, FSUtil.Service> => effect !== undefined),
- { concurrency: "unbounded" },
- )
- return yield* withGlobalConfigDir(global, withInstanceDir(directory, effect))
- })
- const wellKnown = (input: {
- authUrl?: string
- config?: unknown
- remoteConfig?: { url: string; headers?: Record<string, string> }
- remote?: unknown
- remoteHtml?: string
- wellKnown?: unknown
- }) => {
- const seen: { wellKnown?: string; remote?: string; authorization?: string } = {}
- const client = remoteConfigClient({
- seen,
- wellKnown: input.wellKnown ?? {
- ...(input.config !== undefined ? { config: input.config } : {}),
- ...(input.remoteConfig !== undefined ? { remote_config: input.remoteConfig } : {}),
- },
- remote: input.remote,
- remoteHtml: input.remoteHtml,
- })
- return {
- seen,
- it: configIt({ auth: wellKnownAuth(input.authUrl ?? "https://example.com"), client }),
- }
- }
- function withProcessEnv<A, E, R>(key: string, value: string | undefined, effect: Effect.Effect<A, E, R>) {
- return withProcessEnvs({ [key]: value }, effect)
- }
- function withProcessEnvs<A, E, R>(entries: Record<string, string | undefined>, effect: Effect.Effect<A, E, R>) {
- return Effect.acquireUseRelease(
- Effect.sync(() => {
- const originals: Record<string, string | undefined> = {}
- for (const [key, value] of Object.entries(entries)) {
- originals[key] = process.env[key]
- if (value === undefined) delete process.env[key]
- else process.env[key] = value
- }
- return originals
- }),
- () => effect,
- (originals) =>
- Effect.sync(() => {
- for (const [key, original] of Object.entries(originals)) {
- if (original !== undefined) process.env[key] = original
- else delete process.env[key]
- }
- }),
- )
- }
- async function check(map: (dir: string) => string) {
- if (process.platform !== "win32") return
- await using globalTmp = await tmpdir()
- await using tmp = await tmpdir({ git: true, config: { snapshot: true } })
- const prev = Global.Path.config
- ;(Global.Path as { config: string }).config = globalTmp.path
- await clear()
- try {
- await writeConfig(globalTmp.path, {
- $schema: "https://kirincode.ai/config.json",
- snapshot: false,
- })
- await withTestInstance({
- directory: map(tmp.path),
- fn: async (ctx) => {
- const cfg = await load(ctx)
- expect(cfg.snapshot).toBe(true)
- expect(ctx.directory).toBe(Filesystem.resolve(tmp.path))
- expect(ctx.project.id).not.toBe(ProjectV2.ID.global)
- },
- })
- } finally {
- await InstanceRuntime.disposeAllInstances()
- ;(Global.Path as { config: string }).config = prev
- await clear()
- }
- }
- it.instance("loads config with defaults when no files exist", () =>
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.username).toBeDefined()
- }),
- )
- it.instance("falls back to generic username when system user info is unavailable", () =>
- Effect.gen(function* () {
- const userInfo = spyOn(os, "userInfo").mockImplementation(() => {
- throw Object.assign(new Error("missing passwd entry"), { code: "ENOENT" })
- })
- try {
- const config = yield* Config.use.get()
- expect(config.username).toBe("user")
- } finally {
- userInfo.mockRestore()
- }
- }),
- )
- it.effect("creates global jsonc config with schema when no global configs exist", () =>
- withGlobalConfig({}, ({ dir }) =>
- Effect.gen(function* () {
- yield* Config.use.get().pipe(provideInstanceEffect(dir))
- const content = yield* FSUtil.use.readFileString(path.join(dir, "kirincode.jsonc"))
- expect(content).toContain('"$schema": "https://kirincode.ai/config.json"')
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
- ),
- )
- it.effect("does not create global config when KIRINCODE_CONFIG_DIR is set", () =>
- Effect.gen(function* () {
- const custom = yield* tmpdirScoped()
- yield* withGlobalConfig({}, ({ dir }) =>
- withProcessEnv(
- "KIRINCODE_CONFIG_DIR",
- custom,
- Effect.gen(function* () {
- yield* Config.use.get().pipe(provideInstanceEffect(dir))
- expect(yield* FSUtil.use.existsSafe(path.join(dir, "kirincode.jsonc"))).toBe(false)
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
- ),
- )
- }),
- )
- it.instance(
- "loads JSON config file",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.model).toBe("test/model")
- expect(config.username).toBe("testuser")
- }),
- { config: { model: "test/model", username: "testuser" } },
- )
- it.instance(
- "loads shell config field",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.shell).toBe("bash")
- }),
- { config: { shell: "bash" } },
- )
- it.instance("updates config and preserves empty shell sentinel", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(
- test.directory,
- { $schema: "https://kirincode.ai/config.json", shell: "bash" },
- "config.json",
- )
- yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(ConfigV1.Info, { shell: "" }, "test:config")))
- const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
- expect(writtenConfig).toMatchObject({ shell: "" })
- }),
- )
- it.effect("updates global config and omits empty shell key in json", () =>
- withGlobalConfig({ config: { shell: "bash" } }, ({ dir }) =>
- Effect.gen(function* () {
- yield* Config.use.updateGlobal({ shell: "" })
- const writtenConfig = yield* FSUtil.use.readJson(path.join(dir, "kirincode.json"))
- expect(writtenConfig).not.toHaveProperty("shell")
- }),
- ),
- )
- it.effect("updates global config and omits empty shell key in jsonc", () =>
- withGlobalConfig({ config: { shell: "bash", model: "test/model" }, name: "kirincode.jsonc" }, ({ dir }) =>
- Effect.gen(function* () {
- yield* Config.use.updateGlobal({ shell: "" })
- const file = path.join(dir, "kirincode.jsonc")
- const writtenConfig = yield* FSUtil.use.readFileString(file)
- const parsed = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(writtenConfig, file), file)
- expect(writtenConfig).not.toContain('"shell"')
- expect(parsed.shell).toBeUndefined()
- expect(parsed.model).toBe("test/model")
- }),
- ),
- )
- it.instance(
- "loads formatter boolean config",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.formatter).toBe(true)
- }),
- { config: { formatter: true } },
- )
- it.instance(
- "loads lsp boolean config",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.lsp).toBe(true)
- }),
- { config: { lsp: true } },
- )
- test("loads project config from Git Bash and MSYS2 paths on Windows", async () => {
- // Git Bash and MSYS2 both use /<drive>/... paths on Windows.
- await check((dir) => {
- const drive = dir[0].toLowerCase()
- const rest = dir.slice(2).replaceAll("\\", "/")
- return `/${drive}${rest}`
- })
- })
- test("loads project config from Cygwin paths on Windows", async () => {
- await check((dir) => {
- const drive = dir[0].toLowerCase()
- const rest = dir.slice(2).replaceAll("\\", "/")
- return `/cygdrive/${drive}${rest}`
- })
- })
- it.instance("ignores legacy tui keys in kirincode config", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- model: "test/model",
- theme: "legacy",
- tui: { scroll_speed: 4 },
- })
- const config = yield* Config.use.get()
- expect(config.model).toBe("test/model")
- expect((config as Record<string, unknown>).theme).toBeUndefined()
- expect((config as Record<string, unknown>).tui).toBeUndefined()
- }),
- )
- it.instance("loads JSONC config file", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, "kirincode.jsonc"),
- `{
- // This is a comment
- "$schema": "https://kirincode.ai/config.json",
- "model": "test/model",
- "username": "testuser"
- }`,
- )
- const config = yield* Config.use.get()
- expect(config.model).toBe("test/model")
- expect(config.username).toBe("testuser")
- }),
- )
- it.instance("jsonc overrides json in the same directory", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(
- test.directory,
- {
- $schema: "https://kirincode.ai/config.json",
- model: "base",
- username: "base",
- },
- "kirincode.jsonc",
- )
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- model: "override",
- })
- const config = yield* Config.use.get()
- expect(config.model).toBe("base")
- expect(config.username).toBe("base")
- }),
- )
- it.instance("handles environment variable substitution", () =>
- withProcessEnv(
- "TEST_VAR",
- "test-user",
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- username: "{env:TEST_VAR}",
- })
- const config = yield* Config.use.get()
- expect(config.username).toBe("test-user")
- }),
- ),
- )
- it.instance("preserves env variables when adding $schema to config", () =>
- withProcessEnv(
- "PRESERVE_VAR",
- "secret_value",
- Effect.gen(function* () {
- const test = yield* TestInstance
- // Config without $schema - should trigger auto-add
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, "kirincode.json"),
- JSON.stringify({ username: "{env:PRESERVE_VAR}" }),
- )
- const config = yield* Config.use.get()
- expect(config.username).toBe("secret_value")
- // Read the file to verify the env variable was preserved
- const content = yield* FSUtil.use.readFileString(path.join(test.directory, "kirincode.json"))
- expect(content).toContain("{env:PRESERVE_VAR}")
- expect(content).not.toContain("secret_value")
- expect(content).toContain("$schema")
- }),
- ),
- )
- it.instance("handles file inclusion substitution", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.txt"), "test-user")
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- username: "{file:included.txt}",
- })
- const config = yield* Config.use.get()
- expect(config.username).toBe("test-user")
- }),
- )
- it.instance("handles file inclusion with replacement tokens", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "included.md"), "const out = await Bun.$`echo hi`")
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- username: "{file:included.md}",
- })
- const config = yield* Config.use.get()
- expect(config.username).toBe("const out = await Bun.$`echo hi`")
- }),
- )
- const accountTokenIt = configIt({
- account: Layer.mock(Account.Service)({
- active: () =>
- Effect.succeed(
- Option.some({
- id: AccountID.make("account-1"),
- email: "user@example.com",
- url: "https://control.example.com",
- active_org_id: OrgID.make("org-1"),
- }),
- ),
- activeOrg: () =>
- Effect.succeed(
- Option.some({
- account: {
- id: AccountID.make("account-1"),
- email: "user@example.com",
- url: "https://control.example.com",
- active_org_id: OrgID.make("org-1"),
- },
- org: {
- id: OrgID.make("org-1"),
- name: "Example Org",
- },
- }),
- ),
- config: () =>
- Effect.succeed(
- Option.some({
- provider: { opencode: { options: { apiKey: "{env:KIRINCODE_CONSOLE_TOKEN}" } } },
- }),
- ),
- token: () => Effect.succeed(Option.some(AccessToken.make("st_test_token"))),
- }),
- })
- accountTokenIt.instance("resolves env templates in account config with account token", () =>
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.provider?.["kirincode"]?.options?.apiKey).toBe("st_test_token")
- }),
- )
- it.instance("validates config schema and throws on invalid fields", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- invalid_field: "should cause error",
- })
- const exit = yield* Config.use.get().pipe(Effect.exit)
- expect(Exit.isFailure(exit)).toBe(true)
- }),
- )
- it.instance("throws error for invalid JSON", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "kirincode.json"), "{ invalid json }")
- const exit = yield* Config.use.get().pipe(Effect.exit)
- expect(Exit.isFailure(exit)).toBe(true)
- }),
- )
- it.instance("handles agent configuration", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: {
- test_agent: {
- model: "test/model",
- temperature: 0.7,
- description: "test agent",
- },
- },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test_agent"]).toEqual(
- expect.objectContaining({
- model: "test/model",
- temperature: 0.7,
- description: "test agent",
- }),
- )
- }),
- )
- it.instance("treats agent variant as model-scoped setting (not provider option)", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: {
- test_agent: {
- model: "openai/gpt-5.2",
- variant: "xhigh",
- max_tokens: 123,
- },
- },
- })
- const config = yield* Config.use.get()
- const agent = config.agent?.["test_agent"]
- expect(agent?.variant).toBe("xhigh")
- expect(agent?.options).toMatchObject({
- max_tokens: 123,
- })
- expect(agent?.options).not.toHaveProperty("variant")
- }),
- )
- it.instance("handles command configuration", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- command: {
- test_command: {
- template: "test template",
- description: "test command",
- agent: "test_agent",
- },
- },
- })
- const config = yield* Config.use.get()
- expect(config.command?.["test_command"]).toEqual({
- template: "test template",
- description: "test command",
- agent: "test_agent",
- })
- }),
- )
- it.instance("migrates autoshare to share field", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- autoshare: true,
- })
- const config = yield* Config.use.get()
- expect(config.share).toBe("auto")
- expect(config.autoshare).toBe(true)
- }),
- )
- it.instance("migrates mode field to agent field", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- mode: {
- test_mode: {
- model: "test/model",
- temperature: 0.5,
- },
- },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test_mode"]).toEqual({
- model: "test/model",
- temperature: 0.5,
- mode: "primary",
- options: {},
- permission: {},
- })
- }),
- )
- it.instance("accepts the deprecated reference field", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- reference: {
- local: { path: "../library" },
- sdk: { repository: "github.com/example/sdk", branch: "main" },
- shorthand: "github.com/example/docs",
- },
- })
- const config = yield* Config.use.get()
- expect(config.reference).toEqual({
- local: { path: "../library" },
- sdk: { repository: "github.com/example/sdk", branch: "main" },
- shorthand: "github.com/example/docs",
- })
- }),
- )
- it.instance("loads config from .kirincode directory", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "agent", "test.md"),
- `---
- model: test/model
- ---
- Test agent prompt`,
- )
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]).toEqual(
- expect.objectContaining({
- name: "test",
- model: "test/model",
- prompt: "Test agent prompt",
- }),
- )
- }),
- )
- it.instance("agent markdown permission config preserves user key order", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "agent", "ordered.md"),
- `---
- permission:
- bash: allow
- "*": deny
- edit: ask
- ---
- Ordered permissions`,
- )
- const config = yield* Config.use.get()
- expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"])
- }),
- )
- it.instance("loads agents from .kirincode/agents (plural)", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "agents", "helper.md"),
- `---
- model: test/model
- mode: subagent
- ---
- Helper agent prompt`,
- )
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "agents", "nested", "child.md"),
- `---
- model: test/model
- mode: subagent
- ---
- Nested agent prompt`,
- )
- const config = yield* Config.use.get()
- expect(config.agent?.["helper"]).toMatchObject({
- name: "helper",
- model: "test/model",
- mode: "subagent",
- prompt: "Helper agent prompt",
- })
- expect(config.agent?.["nested/child"]).toMatchObject({
- name: "nested/child",
- model: "test/model",
- mode: "subagent",
- prompt: "Nested agent prompt",
- })
- }),
- )
- it.instance("loads commands from .kirincode/command (singular)", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "command", "hello.md"),
- `---
- description: Test command
- ---
- Hello from singular command`,
- )
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "command", "nested", "child.md"),
- `---
- description: Nested command
- ---
- Nested command template`,
- )
- const config = yield* Config.use.get()
- expect(config.command?.["hello"]).toEqual({
- description: "Test command",
- template: "Hello from singular command",
- })
- expect(config.command?.["nested/child"]).toEqual({
- description: "Nested command",
- template: "Nested command template",
- })
- }),
- )
- it.instance("loads commands from .kirincode/commands (plural)", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "commands", "hello.md"),
- `---
- description: Test command
- ---
- Hello from plural commands`,
- )
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "commands", "nested", "child.md"),
- `---
- description: Nested command
- ---
- Nested command template`,
- )
- const config = yield* Config.use.get()
- expect(config.command?.["hello"]).toEqual({
- description: "Test command",
- template: "Hello from plural commands",
- })
- expect(config.command?.["nested/child"]).toEqual({
- description: "Nested command",
- template: "Nested command template",
- })
- }),
- )
- it.instance("updates config and writes to file", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* Config.Service.use((svc) =>
- svc.update(ConfigParse.schema(ConfigV1.Info, { model: "updated/model" }, "test:config")),
- )
- const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
- expect(writtenConfig).toMatchObject({ model: "updated/model" })
- }),
- )
- it.instance("gets config directories", () =>
- Effect.gen(function* () {
- const dirs = yield* Config.use.directories()
- expect(dirs.length).toBeGreaterThanOrEqual(1)
- }),
- )
- it.effect("does not try to install dependencies in read-only KIRINCODE_CONFIG_DIR", () =>
- Effect.gen(function* () {
- if (process.platform === "win32") return
- const dir = yield* tmpdirScoped()
- const readonly = path.join(dir, "readonly")
- yield* FSUtil.use.ensureDir(readonly)
- yield* FSUtil.use.chmod(readonly, 0o555)
- yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore))
- yield* withProcessEnv("KIRINCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
- )
- it.effect("ignores an inaccessible KIRINCODE_CONFIG_DIR", () =>
- Effect.gen(function* () {
- if (process.platform === "win32") return
- const dir = yield* tmpdirScoped()
- const configDir = path.join(dir, "inaccessible")
- yield* FSUtil.use.ensureDir(configDir)
- yield* FSUtil.use.chmod(configDir, 0o000)
- yield* Effect.addFinalizer(() => FSUtil.use.chmod(configDir, 0o755).pipe(Effect.ignore))
- yield* withProcessEnv("KIRINCODE_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir)))
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
- )
- it.effect("creates a missing KIRINCODE_CONFIG_DIR", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped()
- const configDir = path.join(dir, "configdir")
- yield* withProcessEnv("KIRINCODE_CONFIG_DIR", configDir, Config.use.get().pipe(provideInstanceEffect(dir)))
- expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("node_modules")
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
- )
- it.effect("installs dependencies in writable KIRINCODE_CONFIG_DIR", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped()
- const configDir = path.join(dir, "configdir")
- yield* FSUtil.use.ensureDir(configDir)
- yield* withProcessEnv(
- "KIRINCODE_CONFIG_DIR",
- configDir,
- Config.Service.use((svc) => svc.get().pipe(Effect.andThen(svc.waitForDependencies()))).pipe(
- provideInstanceEffect(dir),
- ),
- )
- expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
- }).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
- )
- // Note: deduplication and serialization of npm installs is now handled by the
- // core Npm.Service (via EffectFlock). Those behaviors are tested in the core
- // package's npm tests, not here.
- it.instance("resolves scoped npm plugins in config", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- const pluginDir = path.join(test.directory, "node_modules", "@scope", "plugin")
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, "package.json"),
- JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2),
- )
- yield* FSUtil.use.writeWithDirs(
- path.join(pluginDir, "package.json"),
- JSON.stringify(
- {
- name: "@scope/plugin",
- version: "1.0.0",
- type: "module",
- main: "./index.js",
- },
- null,
- 2,
- ),
- )
- yield* FSUtil.use.writeWithDirs(path.join(pluginDir, "index.js"), "export default {}\n")
- yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] })
- const config = yield* Config.use.get()
- expect(config.plugin ?? []).toContain("@scope/plugin")
- }),
- )
- it.effect("merges plugin arrays from global and local configs", () =>
- withConfigTree(
- {
- global: { plugin: ["global-plugin-1", "global-plugin-2"] },
- local: { plugin: ["local-plugin-1"] },
- },
- Effect.gen(function* () {
- const plugins = (yield* Config.use.get()).plugin ?? []
- expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true)
- expect(plugins.some((p) => p.includes("global-plugin-2"))).toBe(true)
- expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true)
- expect(
- plugins.filter((p) => p.includes("global-plugin") || p.includes("local-plugin")).length,
- ).toBeGreaterThanOrEqual(3)
- }),
- ),
- )
- it.effect("global config remains global when project config is disabled", () =>
- withConfigTree(
- {
- global: { model: "global/model", plugin: ["global-plugin"] },
- project: { model: "project/model" },
- local: { model: "local/model" },
- },
- withProcessEnv(
- "KIRINCODE_DISABLE_PROJECT_CONFIG",
- "true",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.model).toBe("global/model")
- expect(config.plugin_origins?.find((item) => item.spec === "global-plugin")?.scope).toBe("global")
- }),
- ),
- ),
- )
- it.instance("does not error when only custom agent is a subagent", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "agent", "helper.md"),
- `---
- model: test/model
- mode: subagent
- ---
- Helper subagent prompt`,
- )
- const config = yield* Config.use.get()
- expect(config.agent?.["helper"]).toMatchObject({
- name: "helper",
- model: "test/model",
- mode: "subagent",
- prompt: "Helper subagent prompt",
- })
- }),
- )
- it.effect("merges instructions arrays from global and local configs", () =>
- withConfigTree(
- {
- global: { instructions: ["global-instructions.md", "shared-rules.md"] },
- local: { instructions: ["local-instructions.md"] },
- },
- Effect.gen(function* () {
- expect((yield* Config.use.get()).instructions).toEqual([
- "global-instructions.md",
- "shared-rules.md",
- "local-instructions.md",
- ])
- }),
- ),
- )
- it.effect("deduplicates duplicate instructions from global and local configs", () =>
- withConfigTree(
- {
- global: { instructions: ["duplicate.md", "global-only.md"] },
- local: { instructions: ["duplicate.md", "local-only.md"] },
- },
- Effect.gen(function* () {
- expect((yield* Config.use.get()).instructions).toEqual(["duplicate.md", "global-only.md", "local-only.md"])
- }),
- ),
- )
- it.effect("deduplicates duplicate plugins from global and local configs", () =>
- withConfigTree(
- {
- global: { plugin: ["duplicate-plugin", "global-plugin-1"] },
- local: { plugin: ["duplicate-plugin", "local-plugin-1"] },
- },
- Effect.gen(function* () {
- const plugins = (yield* Config.use.get()).plugin ?? []
- expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true)
- expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true)
- expect(plugins.filter((p) => p.includes("duplicate-plugin")).length).toBe(1)
- expect(
- plugins.filter(
- (p) => p.includes("global-plugin") || p.includes("local-plugin") || p.includes("duplicate-plugin"),
- ).length,
- ).toBe(3)
- }),
- ),
- )
- it.effect("keeps plugin origins aligned with merged plugin list", () =>
- withConfigTree(
- {
- global: { plugin: [["shared-plugin@1.0.0", { source: "global" }], "global-only@1.0.0"] },
- local: { plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"] },
- },
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- const plugins = config.plugin ?? []
- const origins = config.plugin_origins ?? []
- const names = plugins.map((item) => ConfigPlugin.pluginSpecifier(item))
- expect(names).toContain("shared-plugin@2.0.0")
- expect(names).not.toContain("shared-plugin@1.0.0")
- expect(names).toContain("global-only@1.0.0")
- expect(names).toContain("local-only@1.0.0")
- expect(origins.map((item) => item.spec)).toEqual(plugins)
- expect(origins.find((item) => ConfigPlugin.pluginSpecifier(item.spec) === "shared-plugin@2.0.0")?.scope).toBe(
- "local",
- )
- }),
- ),
- )
- // Legacy tools migration tests
- it.instance("migrates legacy tools config to permissions - allow", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { tools: { bash: true, read: true } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({
- bash: "allow",
- read: "allow",
- })
- }),
- )
- it.instance("migrates legacy tools config to permissions - deny", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { tools: { bash: false, webfetch: false } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({
- bash: "deny",
- webfetch: "deny",
- })
- }),
- )
- it.instance("migrates legacy write tool to edit permission", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { tools: { write: true } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" })
- }),
- )
- // Managed settings tests
- // Note: preload.ts sets KIRINCODE_TEST_MANAGED_CONFIG which Global.Path.managedConfig uses
- it.instance(
- "managed settings override user settings",
- Effect.gen(function* () {
- yield* writeManagedSettingsEffect({
- $schema: "https://kirincode.ai/config.json",
- model: "managed/model",
- share: "disabled",
- })
- const config = yield* Config.use.get()
- expect(config.model).toBe("managed/model")
- expect(config.share).toBe("disabled")
- expect(config.username).toBe("testuser")
- }),
- { config: { model: "user/model", share: "auto", username: "testuser" } },
- )
- it.instance(
- "managed settings override project settings",
- Effect.gen(function* () {
- yield* writeManagedSettingsEffect({
- $schema: "https://kirincode.ai/config.json",
- autoupdate: false,
- disabled_providers: ["openai"],
- })
- const config = yield* Config.use.get()
- expect(config.autoupdate).toBe(false)
- expect(config.disabled_providers).toEqual(["openai"])
- }),
- { config: { autoupdate: true, disabled_providers: [] } },
- )
- it.instance("managed jsonc settings override managed json settings", () =>
- Effect.gen(function* () {
- yield* writeManagedSettingsEffect({ model: "managed/json" })
- yield* writeManagedSettingsEffect({ model: "managed/jsonc" }, "kirincode.jsonc")
- const config = yield* Config.use.get()
- expect(config.model).toBe("managed/jsonc")
- }),
- )
- it.instance(
- "missing managed settings file is not an error",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.model).toBe("user/model")
- }),
- { config: { model: "user/model" } },
- )
- it.instance("migrates legacy edit tool to edit permission", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { tools: { edit: false } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny" })
- }),
- )
- it.instance("migrates legacy patch tool to edit permission", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { tools: { patch: true } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" })
- }),
- )
- it.instance("migrates mixed legacy tools config", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({
- bash: "allow",
- edit: "allow",
- read: "deny",
- webfetch: "allow",
- })
- }),
- )
- it.instance("merges legacy tools with existing permission config", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- agent: { test: { permission: { glob: "allow" }, tools: { bash: true } } },
- })
- const config = yield* Config.use.get()
- expect(config.agent?.["test"]?.permission).toEqual({
- glob: "allow",
- bash: "allow",
- })
- }),
- )
- it.instance("permission config preserves user key order", () =>
- // Permission precedence follows the order users write in config, so parsing
- // must not canonicalise known keys ahead of wildcard or custom keys.
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- permission: {
- "*": "deny",
- edit: "ask",
- write: "ask",
- external_directory: "ask",
- read: "allow",
- todowrite: "allow",
- "thoughts_*": "allow",
- "reasoning_model_*": "allow",
- "tools_*": "allow",
- "pr_comments_*": "allow",
- },
- })
- const config = yield* Config.use.get()
- expect(Object.keys(config.permission!)).toEqual([
- "*",
- "edit",
- "write",
- "external_directory",
- "read",
- "todowrite",
- "thoughts_*",
- "reasoning_model_*",
- "tools_*",
- "pr_comments_*",
- ])
- }),
- )
- test("config parser preserves permission order while rejecting unknown top-level keys", () => {
- const config = ConfigParse.schema(
- ConfigV1.Info,
- {
- permission: {
- bash: "allow",
- "*": "deny",
- edit: "ask",
- },
- },
- "test",
- )
- expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"])
- try {
- ConfigParse.schema(ConfigV1.Info, { invalid_field: true }, "test")
- throw new Error("expected config parse to fail")
- } catch (err) {
- const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } }
- expect(error.data?.issues?.[0]).toMatchObject({ code: "unrecognized_keys", keys: ["invalid_field"], path: [] })
- }
- })
- // MCP config merging tests
- it.instance("project config can override MCP server enabled status", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- // Simulates a base config (like from remote .well-known) with disabled MCP.
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- mcp: {
- jira: {
- type: "remote",
- url: "https://jira.example.com/mcp",
- enabled: false,
- },
- wiki: {
- type: "remote",
- url: "https://wiki.example.com/mcp",
- enabled: false,
- },
- },
- })
- // Project config enables just jira.
- yield* writeConfigEffect(
- test.directory,
- {
- $schema: "https://kirincode.ai/config.json",
- mcp: {
- jira: {
- type: "remote",
- url: "https://jira.example.com/mcp",
- enabled: true,
- },
- },
- },
- "kirincode.jsonc",
- )
- const config = yield* Config.use.get()
- expect(config.mcp?.jira).toEqual({
- type: "remote",
- url: "https://jira.example.com/mcp",
- enabled: true,
- })
- expect(config.mcp?.wiki).toEqual({
- type: "remote",
- url: "https://wiki.example.com/mcp",
- enabled: false,
- })
- }),
- )
- it.instance("MCP config deep merges preserving base config properties", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- mcp: {
- myserver: {
- type: "remote",
- url: "https://myserver.example.com/mcp",
- enabled: false,
- headers: {
- "X-Custom-Header": "value",
- },
- },
- },
- })
- yield* writeConfigEffect(
- test.directory,
- {
- $schema: "https://kirincode.ai/config.json",
- mcp: {
- myserver: {
- type: "remote",
- url: "https://myserver.example.com/mcp",
- enabled: true,
- },
- },
- },
- "kirincode.jsonc",
- )
- const config = yield* Config.use.get()
- expect(config.mcp?.myserver).toEqual({
- type: "remote",
- url: "https://myserver.example.com/mcp",
- enabled: true,
- headers: {
- "X-Custom-Header": "value",
- },
- })
- }),
- )
- it.instance("local .kirincode config can override MCP from project config", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* writeConfigEffect(test.directory, {
- $schema: "https://kirincode.ai/config.json",
- mcp: {
- docs: {
- type: "remote",
- url: "https://docs.example.com/mcp",
- enabled: false,
- },
- },
- })
- yield* FSUtil.use.ensureDir(path.join(test.directory, ".kirincode"))
- yield* writeConfigEffect(
- path.join(test.directory, ".kirincode"),
- {
- $schema: "https://kirincode.ai/config.json",
- mcp: {
- docs: {
- type: "remote",
- url: "https://docs.example.com/mcp",
- enabled: true,
- },
- },
- },
- "kirincode.json",
- )
- const config = yield* Config.use.get()
- expect(config.mcp?.docs?.enabled).toBe(true)
- }),
- )
- const remoteProjectOverride = wellKnown({
- config: {
- mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
- },
- })
- remoteProjectOverride.it.instance(
- "project config overrides remote well-known config",
- () =>
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(remoteProjectOverride.seen.wellKnown).toBe("https://example.com/.well-known/opencode")
- expect(config.mcp?.jira?.enabled).toBe(true)
- }),
- {
- git: true,
- config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
- },
- )
- const trailingSlashWellKnown = wellKnown({
- authUrl: "https://example.com/",
- config: {
- mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } },
- },
- })
- trailingSlashWellKnown.it.instance("wellknown URL with trailing slash is normalized", () =>
- Effect.gen(function* () {
- yield* Config.use.get()
- expect(trailingSlashWellKnown.seen.wellKnown).toBe("https://example.com/.well-known/opencode")
- }),
- )
- test("remote well-known config can use FetchHttpClient layer", async () => {
- let fetchedUrl: string | undefined
- const server = Bun.serve({
- port: 0,
- fetch: (request) => {
- fetchedUrl = request.url
- return new Response(
- JSON.stringify({
- config: {
- mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } },
- },
- }),
- { status: 200, headers: { "content-type": "application/json" } },
- )
- },
- })
- try {
- await provideTmpdirInstance(
- () =>
- Config.Service.use((svc) =>
- Effect.gen(function* () {
- const config = yield* svc.get()
- expect(fetchedUrl).toBe(`${server.url.origin}/.well-known/opencode`)
- expect(config.mcp?.jira?.enabled).toBe(true)
- }),
- ),
- { git: true },
- ).pipe(
- Effect.scoped,
- Effect.provide(
- Layer.mergeAll(
- LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
- [Auth.node, wellKnownAuth(server.url.origin)],
- [Account.node, AccountTest.empty],
- [Npm.node, NpmTest.noop],
- [httpClient, FetchHttpClient.layer],
- ]),
- testInstanceStoreLayer,
- ),
- ),
- Effect.runPromise,
- )
- } finally {
- await server.stop(true)
- }
- })
- const templatedHeaderWellKnown = wellKnown({
- remoteConfig: {
- url: "https://config.example.com/kirincode.json",
- headers: { Authorization: "Bearer {env:TEST_TOKEN}" },
- },
- remote: {
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
- },
- })
- templatedHeaderWellKnown.it.instance("wellknown remote_config supports templated env vars in headers", () =>
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(templatedHeaderWellKnown.seen.wellKnown).toBe("https://example.com/.well-known/opencode")
- expect(templatedHeaderWellKnown.seen.remote).toBe("https://config.example.com/kirincode.json")
- expect(templatedHeaderWellKnown.seen.authorization).toBe("Bearer test-token")
- expect(config.mcp?.confluence?.enabled).toBe(true)
- }),
- )
- const remotePrecedenceWellKnown = wellKnown({
- config: {
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: false } },
- },
- remoteConfig: { url: "https://config.example.com/{env:TEST_TOKEN}/kirincode.json" },
- remote: {
- config: { mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } } },
- },
- })
- remotePrecedenceWellKnown.it.instance(
- "wellknown remote_config url tokens and nested config override embedded config",
- () =>
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(remotePrecedenceWellKnown.seen.remote).toBe("https://config.example.com/test-token/kirincode.json")
- expect(config.mcp?.confluence?.enabled).toBe(true)
- }),
- )
- const envIsolationWellKnown = wellKnown({
- remoteConfig: {
- url: "https://config.example.com/kirincode.json",
- headers: { Authorization: "Bearer {env:TEST_TOKEN}" },
- },
- remote: {
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
- },
- })
- envIsolationWellKnown.it.instance(
- "wellknown token env substitution does not mutate process env",
- () =>
- Effect.gen(function* () {
- process.env.TEST_TOKEN = "preexisting-token"
- const config = yield* Config.use.get()
- expect(envIsolationWellKnown.seen.authorization).toBe("Bearer test-token")
- expect(config.username).toBe("test-token")
- expect(process.env.TEST_TOKEN).toBe("preexisting-token")
- }),
- { git: true, config: { username: "{env:TEST_TOKEN}" } },
- )
- const nullConfigWellKnown = wellKnown({
- wellKnown: {
- config: null,
- remote_config: { url: "https://config.example.com/kirincode.json" },
- },
- remote: {
- mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
- },
- })
- nullConfigWellKnown.it.instance("wellknown config null is treated as absent", () =>
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(nullConfigWellKnown.seen.remote).toBe("https://config.example.com/kirincode.json")
- expect(config.mcp?.confluence?.enabled).toBe(true)
- }),
- )
- const invalidRemoteWellKnown = wellKnown({
- remoteConfig: { url: "https://config.example.com/kirincode.json" },
- remote: "not an object",
- })
- invalidRemoteWellKnown.it.instance("wellknown remote_config rejects non-object config responses", () =>
- Effect.gen(function* () {
- const exit = yield* Config.use.get().pipe(Effect.exit)
- expect(invalidRemoteWellKnown.seen.remote).toBe("https://config.example.com/kirincode.json")
- expect(Exit.isFailure(exit)).toBe(true)
- }),
- )
- const loginPageWellKnown = wellKnown({
- remoteConfig: { url: "https://config.example.com/kirincode.json" },
- remoteHtml: "<!DOCTYPE html><html><head><title>Sign in</title></head><body>Login required</body></html>",
- })
- loginPageWellKnown.it.instance(
- "wellknown remote_config surfaces an actionable auth error when the gateway returns an HTML login page",
- () =>
- Effect.gen(function* () {
- const exit = yield* Config.use.get().pipe(Effect.exit)
- expect(loginPageWellKnown.seen.remote).toBe("https://config.example.com/kirincode.json")
- expect(Exit.isFailure(exit)).toBe(true)
- const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined
- expect(NamedError.hasName(error, "ConfigRemoteAuthError")).toBe(true)
- expect((error as { data?: { url?: string } }).data?.url).toBe("https://example.com")
- }),
- )
- describe("resolvePluginSpec", () => {
- test("keeps package specs unchanged", async () => {
- await using tmp = await tmpdir()
- const file = path.join(tmp.path, "kirincode.json")
- expect(await ConfigPlugin.resolvePluginSpec("oh-my-opencode@2.4.3", file)).toBe("oh-my-opencode@2.4.3")
- expect(await ConfigPlugin.resolvePluginSpec("@scope/pkg", file)).toBe("@scope/pkg")
- })
- test("resolves windows-style relative plugin directory specs", async () => {
- if (process.platform !== "win32") return
- await using tmp = await tmpdir({
- init: async (dir) => {
- const plugin = path.join(dir, "plugin")
- await fs.mkdir(plugin, { recursive: true })
- await Filesystem.write(path.join(plugin, "index.ts"), "export default {}")
- },
- })
- const file = path.join(tmp.path, "kirincode.json")
- const hit = await ConfigPlugin.resolvePluginSpec(".\\plugin", file)
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
- })
- test("resolves relative file plugin paths to file urls", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Filesystem.write(path.join(dir, "plugin.ts"), "export default {}")
- },
- })
- const file = path.join(tmp.path, "kirincode.json")
- const hit = await ConfigPlugin.resolvePluginSpec("./plugin.ts", file)
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin.ts")).href)
- })
- test("resolves plugin directory paths to directory urls", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- const plugin = path.join(dir, "plugin")
- await fs.mkdir(plugin, { recursive: true })
- await Filesystem.writeJson(path.join(plugin, "package.json"), {
- name: "demo-plugin",
- type: "module",
- main: "./index.ts",
- })
- await Filesystem.write(path.join(plugin, "index.ts"), "export default {}")
- },
- })
- const file = path.join(tmp.path, "kirincode.json")
- const hit = await ConfigPlugin.resolvePluginSpec("./plugin", file)
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin")).href)
- })
- test("resolves plugin directories without package.json to index.ts", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- const plugin = path.join(dir, "plugin")
- await fs.mkdir(plugin, { recursive: true })
- await Filesystem.write(path.join(plugin, "index.ts"), "export default {}")
- },
- })
- const file = path.join(tmp.path, "kirincode.json")
- const hit = await ConfigPlugin.resolvePluginSpec("./plugin", file)
- expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
- })
- })
- describe("deduplicatePluginOrigins", () => {
- const dedupe = (plugins: ConfigPluginV1.Spec[]) =>
- ConfigPlugin.deduplicatePluginOrigins(
- plugins.map((spec) => ({
- spec,
- source: "",
- scope: "global" as const,
- })),
- ).map((item) => item.spec)
- test("removes duplicates keeping higher priority (later entries)", () => {
- const plugins = ["global-plugin@1.0.0", "shared-plugin@1.0.0", "local-plugin@2.0.0", "shared-plugin@2.0.0"]
- const result = dedupe(plugins)
- expect(result).toContain("global-plugin@1.0.0")
- expect(result).toContain("local-plugin@2.0.0")
- expect(result).toContain("shared-plugin@2.0.0")
- expect(result).not.toContain("shared-plugin@1.0.0")
- expect(result.length).toBe(3)
- })
- test("keeps path plugins separate from package plugins", () => {
- const plugins = ["oh-my-opencode@2.4.3", "file:///project/.kirincode/plugin/oh-my-opencode.js"]
- const result = dedupe(plugins)
- expect(result).toEqual(plugins)
- })
- test("deduplicates direct path plugins by exact spec", () => {
- const plugins = ["file:///project/.kirincode/plugin/demo.ts", "file:///project/.kirincode/plugin/demo.ts"]
- const result = dedupe(plugins)
- expect(result).toEqual(["file:///project/.kirincode/plugin/demo.ts"])
- })
- test("preserves order of remaining plugins", () => {
- const plugins = ["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"]
- const result = dedupe(plugins)
- expect(result).toEqual(["a-plugin@1.0.0", "b-plugin@1.0.0", "c-plugin@1.0.0"])
- })
- it.effect("loads auto-discovered local plugins as file urls", () =>
- withConfigTree(
- { global: { plugin: ["my-plugin@1.0.0"] } },
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "plugin", "my-plugin.js"),
- "export default {}",
- )
- const plugins = (yield* Config.use.get()).plugin ?? []
- expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true)
- expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p).startsWith("file://"))).toBe(true)
- }),
- ),
- )
- })
- describe("KIRINCODE_DISABLE_PROJECT_CONFIG", () => {
- it.instance(
- "skips project config files when flag is set",
- () =>
- withProcessEnv(
- "KIRINCODE_DISABLE_PROJECT_CONFIG",
- "true",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.model).not.toBe("project/model")
- expect(config.username).not.toBe("project-user")
- }),
- ),
- { config: { model: "project/model", username: "project-user" } },
- )
- it.instance("skips project .kirincode/ directories when flag is set", () =>
- withProcessEnv(
- "KIRINCODE_DISABLE_PROJECT_CONFIG",
- "true",
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(
- path.join(test.directory, ".kirincode", "command", "test-cmd.md"),
- "# Test Command\nThis is a test command.",
- )
- const directories = yield* Config.use.directories()
- expect(directories.some((d) => d.startsWith(test.directory))).toBe(false)
- }),
- ),
- )
- it.instance("still loads global config when flag is set", () =>
- withProcessEnv(
- "KIRINCODE_DISABLE_PROJECT_CONFIG",
- "true",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config).toBeDefined()
- expect(config.username).toBeDefined()
- }),
- ),
- )
- it.instance(
- "skips relative instructions with warning when flag is set but no config dir",
- () =>
- withProcessEnvs(
- { KIRINCODE_CONFIG_DIR: undefined, KIRINCODE_DISABLE_PROJECT_CONFIG: "true" },
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "CUSTOM.md"), "# Custom Instructions")
- // The relative instruction should be skipped without error
- const config = yield* Config.use.get()
- expect(config).toBeDefined()
- }),
- ),
- { config: { instructions: ["./CUSTOM.md"] } },
- )
- it.instance(
- "KIRINCODE_CONFIG_DIR still works when flag is set",
- () =>
- Effect.gen(function* () {
- const configDir = yield* tmpdirScoped({ config: { model: "configdir/model" } })
- yield* withProcessEnvs(
- { KIRINCODE_DISABLE_PROJECT_CONFIG: "true", KIRINCODE_CONFIG_DIR: configDir },
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.model).toBe("configdir/model")
- }),
- )
- }),
- { config: { model: "project/model" } },
- )
- })
- // Regression for #28206: malformed KIRINCODE_PERMISSION JSON used to crash
- // the app on startup with an unhandled SyntaxError. Loading the config with
- // an invalid JSON value in this env var should not throw.
- describe("KIRINCODE_PERMISSION env var", () => {
- it.instance("does not crash when KIRINCODE_PERMISSION contains invalid JSON", () =>
- withProcessEnv(
- "KIRINCODE_PERMISSION",
- "{invalid",
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- // Regression: load() used to throw before returning anything.
- expect(config).toBeDefined()
- }),
- ),
- )
- })
- describe("KIRINCODE_CONFIG_CONTENT token substitution", () => {
- it.instance("substitutes {env:} tokens in KIRINCODE_CONFIG_CONTENT", () =>
- withProcessEnv(
- "TEST_CONFIG_VAR",
- "test_api_key_12345",
- withProcessEnv(
- "KIRINCODE_CONFIG_CONTENT",
- JSON.stringify({
- $schema: "https://kirincode.ai/config.json",
- username: "{env:TEST_CONFIG_VAR}",
- }),
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.username).toBe("test_api_key_12345")
- }),
- ),
- ),
- )
- it.instance("substitutes {file:} tokens in KIRINCODE_CONFIG_CONTENT", () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* FSUtil.use.writeWithDirs(path.join(test.directory, "api_key.txt"), "secret_key_from_file")
- yield* withProcessEnv(
- "KIRINCODE_CONFIG_CONTENT",
- JSON.stringify({
- $schema: "https://kirincode.ai/config.json",
- username: "{file:./api_key.txt}",
- }),
- Effect.gen(function* () {
- const config = yield* Config.use.get()
- expect(config.username).toBe("secret_key_from_file")
- }),
- )
- }),
- )
- })
- // parseManagedPlist unit tests — pure function, no OS interaction
- test("parseManagedPlist strips MDM metadata keys", async () => {
- const config = ConfigParse.schema(
- ConfigV1.Info,
- ConfigParse.jsonc(
- await ConfigManaged.parseManagedPlist(
- JSON.stringify({
- PayloadDisplayName: "KirinCode Managed",
- PayloadIdentifier: "ai.kirincode.managed.test",
- PayloadType: "ai.kirincode.managed",
- PayloadUUID: "AAAA-BBBB-CCCC",
- PayloadVersion: 1,
- _manualProfile: true,
- share: "disabled",
- model: "mdm/model",
- }),
- ),
- "test:mobileconfig",
- ),
- "test:mobileconfig",
- )
- expect(config.share).toBe("disabled")
- expect(config.model).toBe("mdm/model")
- // MDM keys must not leak into the parsed config
- expect((config as any).PayloadUUID).toBeUndefined()
- expect((config as any).PayloadType).toBeUndefined()
- expect((config as any)._manualProfile).toBeUndefined()
- })
- test("parseManagedPlist parses server settings", async () => {
- const config = ConfigParse.schema(
- ConfigV1.Info,
- ConfigParse.jsonc(
- await ConfigManaged.parseManagedPlist(
- JSON.stringify({
- $schema: "https://kirincode.ai/config.json",
- server: { hostname: "127.0.0.1", mdns: false },
- autoupdate: true,
- }),
- ),
- "test:mobileconfig",
- ),
- "test:mobileconfig",
- )
- expect(config.server?.hostname).toBe("127.0.0.1")
- expect(config.server?.mdns).toBe(false)
- expect(config.autoupdate).toBe(true)
- })
- test("parseManagedPlist parses permission rules", async () => {
- const config = ConfigParse.schema(
- ConfigV1.Info,
- ConfigParse.jsonc(
- await ConfigManaged.parseManagedPlist(
- JSON.stringify({
- $schema: "https://kirincode.ai/config.json",
- permission: {
- "*": "ask",
- bash: { "*": "ask", "rm -rf *": "deny", "curl *": "deny" },
- grep: "allow",
- glob: "allow",
- webfetch: "ask",
- "~/.ssh/*": "deny",
- },
- }),
- ),
- "test:mobileconfig",
- ),
- "test:mobileconfig",
- )
- expect(config.permission?.["*"]).toBe("ask")
- expect(config.permission?.grep).toBe("allow")
- expect(config.permission?.webfetch).toBe("ask")
- expect(config.permission?.["~/.ssh/*"]).toBe("deny")
- const bash = config.permission?.bash as Record<string, string>
- expect(bash?.["rm -rf *"]).toBe("deny")
- expect(bash?.["curl *"]).toBe("deny")
- })
- test("parseManagedPlist parses enabled_providers", async () => {
- const config = ConfigParse.schema(
- ConfigV1.Info,
- ConfigParse.jsonc(
- await ConfigManaged.parseManagedPlist(
- JSON.stringify({
- $schema: "https://kirincode.ai/config.json",
- enabled_providers: ["anthropic", "google"],
- }),
- ),
- "test:mobileconfig",
- ),
- "test:mobileconfig",
- )
- expect(config.enabled_providers).toEqual(["anthropic", "google"])
- })
- test("parseManagedPlist handles empty config", async () => {
- const config = ConfigParse.schema(
- ConfigV1.Info,
- ConfigParse.jsonc(
- await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://kirincode.ai/config.json" })),
- "test:mobileconfig",
- ),
- "test:mobileconfig",
- )
- expect(config.$schema).toBe("https://kirincode.ai/config.json")
- })
|