/**
* Reproducer tests for plugin-hook mutation bugs analogous to #26546
* (gemini-auth-plugin / `toPublicInfo`, fixed by #26550).
*
* Status:
* - Finding 4 (plugin.config): FIXED in this PR by projecting cfg
* through `toJsonSafe` at the HTTP boundary in
* `ConfigHttpApi.get`. The active reproducer below asserts the
* HTTP `/config` response, not Config.Service.get(); internal cfg
* is allowed to carry whatever plugins put there, the public API
* is the contract that matters.
* - Findings 1-3 (tool.definition, messages.transform, system.transform):
* reproducers SKIPPED. The `output`-mutation contract is documented
* public API (see packages/web/src/content/docs/plugins.mdx); blanket
* clone-and-return breaks plugins that mutate by reference, and
* blanket in-place scrub corrupts shared Effect Schema instances
* in `tool.parameters`. Per-hook design needed.
*/
import { afterAll, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import path from "path"
import { pathToFileURL } from "url"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
const { Plugin } = await import("../../src/plugin/index")
const { Config } = await import("../../src/config/config")
const { Server } = await import("../../src/server/server")
const it = testEffect(Layer.mergeAll(Plugin.defaultLayer, Config.defaultLayer, CrossSpawnSpawner.defaultLayer))
afterAll(() => {
if (disableDefault === undefined) {
delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
return
}
process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault
})
function withProject(
source: string,
self: Effect.Effect | ((dir: string) => Effect.Effect),
) {
return provideTmpdirInstance((dir) =>
Effect.gen(function* () {
const file = path.join(dir, "plugin.ts")
yield* Effect.all(
[
Effect.promise(() => Bun.write(file, source)),
Effect.promise(() =>
Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify(
{
$schema: "https://opencode.ai/config.json",
plugin: [pathToFileURL(file).href],
},
null,
2,
),
),
),
],
{ discard: true, concurrency: 2 },
)
return yield* typeof self === "function" ? self(dir) : self
}),
)
}
/** True if `value` contains only values that can cross JSON boundaries without loss. */
function isJsonSafe(value: unknown, seen = new WeakSet