mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 04:33:25 +00:00
489 lines
19 KiB
TypeScript
489 lines
19 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Duration, Schema } from "effect"
|
|
import { FastCheck } from "effect/testing"
|
|
import { ConfigNormalize } from "@opencode-ai/core/config/normalize"
|
|
import { Info } from "@opencode-ai/schema/config"
|
|
|
|
const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
|
|
|
function normalized(input: unknown) {
|
|
const result = ConfigNormalize.normalize(input)
|
|
expect(result.type).toBe("normalized")
|
|
if (result.type !== "normalized") throw new Error("expected normalized config")
|
|
return result
|
|
}
|
|
|
|
function decoded(input: unknown) {
|
|
return Schema.decodeUnknownSync(Info, options)(normalized(input).encoded)
|
|
}
|
|
|
|
function withoutEmptyCompatibilityContainers(input: Record<string, unknown>) {
|
|
const result = structuredClone(input)
|
|
if (typeof result.mcp === "object" && result.mcp !== null && !Array.isArray(result.mcp)) {
|
|
const mcp = result.mcp as Record<string, unknown>
|
|
const originallyEmpty = !Object.keys(mcp).length
|
|
for (const key of ["servers", "timeout"]) {
|
|
if (
|
|
typeof mcp[key] === "object" &&
|
|
mcp[key] !== null &&
|
|
!Array.isArray(mcp[key]) &&
|
|
!Object.keys(mcp[key]).length
|
|
)
|
|
delete mcp[key]
|
|
}
|
|
if (!originallyEmpty && !Object.keys(mcp).length) delete result.mcp
|
|
}
|
|
if (typeof result.compaction === "object" && result.compaction !== null && !Array.isArray(result.compaction)) {
|
|
const compaction = result.compaction as Record<string, unknown>
|
|
const originallyEmpty = !Object.keys(compaction).length
|
|
if (
|
|
typeof compaction.keep === "object" &&
|
|
compaction.keep !== null &&
|
|
!Array.isArray(compaction.keep) &&
|
|
!Object.keys(compaction.keep).length
|
|
)
|
|
delete compaction.keep
|
|
if (!originallyEmpty && !Object.keys(compaction).length) delete result.compaction
|
|
}
|
|
return result
|
|
}
|
|
|
|
describe("ConfigNormalize", () => {
|
|
test("rejects every non-object root with one root diagnostic", () => {
|
|
for (const input of [null, [], "config", true, 1]) {
|
|
expect(ConfigNormalize.normalize(input)).toEqual({
|
|
type: "rejected",
|
|
diagnostics: [
|
|
{
|
|
kind: "invalid",
|
|
path: ["$"],
|
|
message: "rejected configuration because its root is not an object",
|
|
},
|
|
],
|
|
})
|
|
}
|
|
})
|
|
|
|
test("keeps unrelated native fields when a legacy field is present", () => {
|
|
const result = decoded({ snapshot: false, agents: { reviewer: { system: "Use V2" } } })
|
|
expect(result.snapshots).toBe(false)
|
|
expect(result.agents?.reviewer?.system).toBe("Use V2")
|
|
})
|
|
|
|
test("canonicalizes transformed native values through decode then encode", () => {
|
|
const result = normalized({ warming: { interval: "4 minutes", duration: "30 minutes" } })
|
|
expect(result.encoded.warming).toEqual({ interval: "240000 millis", duration: "1800000 millis" })
|
|
const info = Schema.decodeUnknownSync(Info)(result.encoded)
|
|
if (typeof info.warming === "boolean" || info.warming === undefined) throw new Error("expected warming info")
|
|
expect(Duration.toMillis(info.warming.interval ?? Duration.zero)).toBe(240_000)
|
|
expect(Duration.toMillis(info.warming.duration ?? Duration.zero)).toBe(1_800_000)
|
|
})
|
|
|
|
test("preserves arbitrary JSON-round-tripped native configuration", () => {
|
|
FastCheck.assert(
|
|
FastCheck.property(Schema.toArbitrary(Info), (info) => {
|
|
const source = JSON.parse(JSON.stringify(Schema.encodeSync(Info)(info)))
|
|
const result = normalized(source)
|
|
expect(Schema.decodeUnknownSync(Info)(result.encoded)).toEqual(
|
|
Schema.decodeUnknownSync(Info)(withoutEmptyCompatibilityContainers(source)),
|
|
)
|
|
}),
|
|
{ numRuns: 100 },
|
|
)
|
|
})
|
|
|
|
test("merges named maps by entry and gives valid native entries precedence", () => {
|
|
const result = normalized({
|
|
reference: { legacy: { path: "../legacy" }, duplicate: { path: "../old" } },
|
|
references: { native: { path: "../native" }, duplicate: { path: "../new" } },
|
|
command: { legacy: { template: "legacy" }, duplicate: { template: "old" } },
|
|
commands: { native: { template: "native" }, duplicate: { template: "new" } },
|
|
})
|
|
expect(result.encoded.references).toEqual({
|
|
legacy: { path: "../legacy" },
|
|
native: { path: "../native" },
|
|
duplicate: { path: "../new" },
|
|
})
|
|
expect(result.encoded.commands).toEqual({
|
|
legacy: { template: "legacy" },
|
|
native: { template: "native" },
|
|
duplicate: { template: "new" },
|
|
})
|
|
expect(result.diagnostics.filter((item) => item.kind === "conflict").map((item) => item.path)).toEqual([
|
|
["references", "duplicate"],
|
|
["commands", "duplicate"],
|
|
])
|
|
})
|
|
|
|
test("does not report canonical-equal duplicates as conflicts", () => {
|
|
const result = normalized({
|
|
snapshot: false,
|
|
snapshots: false,
|
|
reference: { docs: { path: "../docs" } },
|
|
references: { docs: { path: "../docs" } },
|
|
agent: { reviewer: { prompt: "same" } },
|
|
agents: { reviewer: { system: "same" } },
|
|
provider: { custom: { name: "same" } },
|
|
providers: { custom: { name: "same" } },
|
|
compaction: { preserve_recent_tokens: 1000, keep: { tokens: 1000 } },
|
|
})
|
|
expect(result.diagnostics.filter((item) => item.kind === "conflict")).toEqual([])
|
|
})
|
|
|
|
test("uses agent then mode then native agent precedence", () => {
|
|
const result = normalized({
|
|
agent: { reviewer: { prompt: "agent" }, agentOnly: { prompt: "agent-only" } },
|
|
mode: { reviewer: { prompt: "mode" }, modeOnly: { prompt: "mode-only" } },
|
|
agents: { reviewer: { system: "native" }, nativeOnly: { system: "native-only" } },
|
|
})
|
|
expect(result.encoded.agents).toEqual({
|
|
reviewer: { system: "native" },
|
|
agentOnly: { system: "agent-only" },
|
|
modeOnly: { system: "mode-only", mode: "primary" },
|
|
nativeOnly: { system: "native-only" },
|
|
})
|
|
expect(result.diagnostics.filter((item) => item.kind === "conflict").map((item) => item.path)).toEqual([
|
|
["agents", "reviewer"],
|
|
["agents", "reviewer"],
|
|
])
|
|
expect(() => Schema.decodeUnknownSync(Info)(result.encoded)).not.toThrow()
|
|
})
|
|
|
|
test("recovers malformed named entries and retains a valid legacy collision", () => {
|
|
const result = normalized({
|
|
command: { fallback: { template: "legacy" } },
|
|
commands: {
|
|
fallback: { template: 1 },
|
|
valid: { template: "native" },
|
|
invalid: { template: false },
|
|
},
|
|
providers: {
|
|
valid: { name: "Valid" },
|
|
invalid: { env: [1] },
|
|
},
|
|
})
|
|
expect(result.encoded.commands).toEqual({ fallback: { template: "legacy" }, valid: { template: "native" } })
|
|
expect(result.encoded.providers).toEqual({ valid: { name: "Valid" } })
|
|
expect(result.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toEqual([
|
|
["commands", "fallback"],
|
|
["commands", "invalid"],
|
|
["providers", "invalid"],
|
|
])
|
|
})
|
|
|
|
test("uses a valid retired provider alias when the canonical legacy entry is malformed", () => {
|
|
const result = normalized({
|
|
provider: {
|
|
"azure-cognitive-services": { models: { deployment: {} } },
|
|
azure: { env: [1] },
|
|
},
|
|
})
|
|
expect(result.encoded.providers).toHaveProperty("azure.models.deployment")
|
|
expect(result.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toContainEqual([
|
|
"provider",
|
|
"azure",
|
|
])
|
|
})
|
|
|
|
test("preserves permission source order and appends native rules", () => {
|
|
expect(
|
|
normalized({
|
|
tools: { bash: true, write: false },
|
|
permission: { read: "allow", custom: { first: "deny", second: "ask" }, task: "allow" },
|
|
permissions: [{ action: "native", resource: "*", effect: "deny" }],
|
|
}).encoded.permissions,
|
|
).toEqual([
|
|
{ action: "shell", resource: "*", effect: "allow" },
|
|
{ action: "edit", resource: "*", effect: "deny" },
|
|
{ action: "read", resource: "*", effect: "allow" },
|
|
{ action: "custom", resource: "first", effect: "deny" },
|
|
{ action: "custom", resource: "second", effect: "ask" },
|
|
{ action: "subagent", resource: "*", effect: "allow" },
|
|
{ action: "native", resource: "*", effect: "deny" },
|
|
])
|
|
})
|
|
|
|
test("redacts permission resource keys from invalid diagnostics", () => {
|
|
const result = normalized({
|
|
permission: { bash: { "curl -H Authorization:Bearer TOPSECRET *": "bogus" } },
|
|
})
|
|
expect(result.diagnostics).toEqual([
|
|
{
|
|
kind: "invalid",
|
|
path: ["permission", "bash", "0"],
|
|
message: "skipped malformed recognized value",
|
|
},
|
|
])
|
|
expect(JSON.stringify(result.diagnostics)).not.toContain("TOPSECRET")
|
|
})
|
|
|
|
test("recovers list items for skills, plugins, instructions, and permissions", () => {
|
|
const result = normalized({
|
|
skills: { paths: ["./skills", 1], urls: [false, "https://example.com/skills"] },
|
|
plugin: ["legacy", ["tuple", {}], [1, {}]],
|
|
plugins: ["native", { package: "object" }, { package: 1 }],
|
|
instructions: ["one", 2, "three"],
|
|
permissions: [
|
|
{ action: "read", resource: "*", effect: "allow" },
|
|
{ action: "read", resource: "*", effect: "invalid" },
|
|
],
|
|
})
|
|
expect(result.encoded.skills).toEqual(["./skills", "https://example.com/skills"])
|
|
expect(result.encoded.plugins).toEqual([
|
|
"legacy",
|
|
{ package: "tuple", options: {} },
|
|
"native",
|
|
{ package: "object" },
|
|
])
|
|
expect(result.encoded.instructions).toEqual(["one", "three"])
|
|
expect(result.encoded.permissions).toEqual([{ action: "read", resource: "*", effect: "allow" }])
|
|
expect(result.diagnostics.filter((item) => item.kind === "invalid")).toHaveLength(6)
|
|
})
|
|
|
|
test("omits malformed collection roots instead of synthesizing empty values", () => {
|
|
const result = normalized({
|
|
commands: [],
|
|
providers: "invalid",
|
|
references: false,
|
|
agents: 1,
|
|
plugins: {},
|
|
permissions: {},
|
|
instructions: {},
|
|
})
|
|
expect(result.encoded).toEqual({})
|
|
expect(result.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toEqual([
|
|
["references"],
|
|
["commands"],
|
|
["agents"],
|
|
["providers"],
|
|
["permissions"],
|
|
["plugins"],
|
|
["instructions"],
|
|
])
|
|
})
|
|
|
|
test("omits all-invalid formatter and LSP maps while preserving explicit empty maps", () => {
|
|
const invalid = normalized({
|
|
formatter: { prettier: { command: [1] } },
|
|
lsp: { typescript: { command: [1] } },
|
|
})
|
|
expect(invalid.encoded).not.toHaveProperty("formatter")
|
|
expect(invalid.encoded).not.toHaveProperty("lsp")
|
|
expect(invalid.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toEqual([
|
|
["formatter", "prettier"],
|
|
["lsp", "typescript"],
|
|
])
|
|
|
|
expect(normalized({ formatter: {}, lsp: {} }).encoded).toMatchObject({ formatter: {}, lsp: {} })
|
|
})
|
|
|
|
test("combines legacy and native MCP servers and merges timeout leaves", () => {
|
|
const result = normalized({
|
|
experimental: { mcp_timeout: 5000 },
|
|
mcp: {
|
|
legacy: { type: "local", command: ["legacy"] },
|
|
duplicate: { type: "remote", url: "https://legacy.example.com" },
|
|
servers: {
|
|
native: { type: "local", command: ["native"] },
|
|
duplicate: { type: "remote", url: "https://native.example.com" },
|
|
invalid: { type: "local", command: [1] },
|
|
},
|
|
timeout: { startup: 1000, catalog: 6000 },
|
|
},
|
|
})
|
|
expect(result.encoded.mcp).toEqual({
|
|
timeout: { catalog: 6000, execution: 5000, startup: 1000 },
|
|
servers: {
|
|
legacy: { type: "local", command: ["legacy"], disabled: undefined, timeout: undefined },
|
|
duplicate: { type: "remote", url: "https://native.example.com" },
|
|
native: { type: "local", command: ["native"] },
|
|
},
|
|
})
|
|
expect(
|
|
result.diagnostics.some((item) => item.kind === "conflict" && item.path.join(".") === "mcp.servers.duplicate"),
|
|
).toBe(true)
|
|
expect(
|
|
result.diagnostics.some((item) => item.kind === "conflict" && item.path.join(".") === "mcp.timeout.catalog"),
|
|
).toBe(true)
|
|
expect(
|
|
result.diagnostics.some((item) => item.kind === "invalid" && item.path.join(".") === "mcp.servers.invalid"),
|
|
).toBe(true)
|
|
})
|
|
|
|
test("uses raw MCP discriminators for reserved server names", () => {
|
|
const result = normalized({
|
|
mcp: {
|
|
servers: { type: "local", command: ["reserved-servers"] },
|
|
timeout: { type: "remote", url: "https://reserved.example.com" },
|
|
},
|
|
})
|
|
expect((result.encoded.mcp as { servers: Record<string, unknown> }).servers).toEqual({
|
|
servers: { type: "local", command: ["reserved-servers"], disabled: undefined, timeout: undefined },
|
|
timeout: { type: "remote", url: "https://reserved.example.com", disabled: undefined, timeout: undefined },
|
|
})
|
|
|
|
const enabledOnly = normalized({ mcp: { servers: { enabled: true }, timeout: { enabled: false } } })
|
|
expect(enabledOnly.encoded.mcp).toBeUndefined()
|
|
expect(enabledOnly.diagnostics.map((item) => [item.kind, item.path])).toEqual([
|
|
["unsupported", ["mcp", "servers"]],
|
|
["unsupported", ["mcp", "timeout"]],
|
|
])
|
|
})
|
|
|
|
test("merges bounded compaction leaves and omits unsupported leaves", () => {
|
|
const result = normalized({
|
|
compaction: {
|
|
auto: false,
|
|
preserve_recent_tokens: 1000,
|
|
keep: { tokens: 2000 },
|
|
reserved: 3000,
|
|
buffer: 4000,
|
|
tail_turns: 2,
|
|
prune: true,
|
|
},
|
|
})
|
|
expect(result.encoded.compaction).toEqual({ auto: false, keep: { tokens: 2000 }, buffer: 4000 })
|
|
expect(result.diagnostics.map((item) => [item.kind, item.path])).toEqual([
|
|
["unsupported", ["compaction", "tail_turns"]],
|
|
["unsupported", ["compaction", "prune"]],
|
|
["conflict", ["compaction", "keep", "tokens"]],
|
|
["conflict", ["compaction", "buffer"]],
|
|
])
|
|
})
|
|
|
|
test("distinguishes empty, mixed, and wholly malformed enabled provider lists", () => {
|
|
expect(normalized({ enabled_providers: [] }).encoded.experimental).toEqual({
|
|
policies: [{ action: "provider.use", resource: "*", effect: "deny" }],
|
|
})
|
|
expect(normalized({ enabled_providers: [1, "anthropic", false] }).encoded.experimental).toEqual({
|
|
policies: [
|
|
{ action: "provider.use", resource: "*", effect: "deny" },
|
|
{ action: "provider.use", resource: "anthropic", effect: "allow" },
|
|
],
|
|
})
|
|
expect(normalized({ enabled_providers: [1, false] }).encoded.experimental).toBeUndefined()
|
|
expect(normalized({ enabled_providers: "anthropic" }).encoded.experimental).toBeUndefined()
|
|
})
|
|
|
|
test("appends native policies after migrated provider policies", () => {
|
|
expect(
|
|
normalized({
|
|
enabled_providers: ["anthropic"],
|
|
disabled_providers: ["openai"],
|
|
experimental: {
|
|
subagent_depth: 0,
|
|
policies: [{ action: "provider.use", resource: "custom", effect: "allow" }],
|
|
},
|
|
}).encoded.experimental,
|
|
).toEqual({
|
|
subagent_depth: 0,
|
|
policies: [
|
|
{ action: "provider.use", resource: "*", effect: "deny" },
|
|
{ action: "provider.use", resource: "anthropic", effect: "allow" },
|
|
{ action: "provider.use", resource: "openai", effect: "deny" },
|
|
{ action: "provider.use", resource: "custom", effect: "allow" },
|
|
],
|
|
})
|
|
})
|
|
|
|
test("reports unsupported legacy settings without including their values", () => {
|
|
const secret = "do-not-log-this-value"
|
|
const result = normalized({
|
|
logLevel: "DEBUG",
|
|
small_model: secret,
|
|
agent: { reviewer: { name: secret, prompt: "review" } },
|
|
provider: {
|
|
custom: {
|
|
id: secret,
|
|
whitelist: ["model"],
|
|
models: {
|
|
model: {
|
|
release_date: secret,
|
|
status: "active",
|
|
interleaved: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
experimental: { openTelemetry: true },
|
|
})
|
|
expect(result.diagnostics.filter((item) => item.kind === "unsupported").map((item) => item.path)).toEqual([
|
|
["logLevel"],
|
|
["small_model"],
|
|
["agent", "reviewer", "name"],
|
|
["provider", "custom", "id"],
|
|
["provider", "custom", "whitelist"],
|
|
["provider", "custom", "models", "model", "release_date"],
|
|
["provider", "custom", "models", "model", "status"],
|
|
["provider", "custom", "models", "model", "interleaved"],
|
|
["experimental", "openTelemetry"],
|
|
])
|
|
expect(JSON.stringify(result.diagnostics)).not.toContain(secret)
|
|
})
|
|
|
|
test("diagnoses unsupported legacy model selections without dropping their entries", () => {
|
|
const result = normalized({
|
|
command: {
|
|
invalidModel: { template: "one", model: "invalid" },
|
|
invalidVariant: { template: "two", model: "anthropic/model", variant: "bad#variant" },
|
|
missingModel: { template: "three", variant: "high" },
|
|
},
|
|
agent: { invalid: { prompt: "agent", model: "invalid", variant: "" } },
|
|
})
|
|
expect(Object.keys(result.encoded.commands as Record<string, unknown>)).toEqual([
|
|
"invalidModel",
|
|
"invalidVariant",
|
|
"missingModel",
|
|
])
|
|
expect(Object.keys(result.encoded.agents as Record<string, unknown>)).toEqual(["invalid"])
|
|
expect(result.diagnostics.filter((item) => item.kind === "unsupported").map((item) => item.path)).toEqual([
|
|
["command", "invalidModel", "model"],
|
|
["command", "invalidVariant", "variant"],
|
|
["command", "missingModel", "variant"],
|
|
["agent", "invalid", "model"],
|
|
["agent", "invalid", "variant"],
|
|
])
|
|
})
|
|
|
|
test("invalid legacy provider overlays skip only that provider", () => {
|
|
const result = normalized({
|
|
provider: {
|
|
headers: { options: { headers: { valid: "yes", invalid: 1 } } },
|
|
body: { options: { body: "not-an-object" } },
|
|
valid: { options: { headers: { valid: "yes" }, body: { trace: true } } },
|
|
},
|
|
})
|
|
expect(result.encoded.providers).toEqual({
|
|
valid: { settings: {}, headers: { valid: "yes" }, body: { trace: true } },
|
|
})
|
|
expect(result.diagnostics.filter((item) => item.kind === "invalid").map((item) => item.path)).toEqual([
|
|
["provider", "headers", "options", "headers"],
|
|
["provider", "body", "options", "body"],
|
|
])
|
|
})
|
|
|
|
test("preserves explicit false, zero, empty list, and empty map presence", () => {
|
|
const result = normalized({
|
|
snapshot: false,
|
|
autoshare: false,
|
|
references: {},
|
|
commands: {},
|
|
agents: {},
|
|
providers: {},
|
|
plugins: [],
|
|
instructions: [],
|
|
experimental: { subagent_depth: 0 },
|
|
})
|
|
expect(result.encoded).toMatchObject({
|
|
snapshots: false,
|
|
references: {},
|
|
commands: {},
|
|
agents: {},
|
|
providers: {},
|
|
plugins: [],
|
|
instructions: [],
|
|
experimental: { subagent_depth: 0 },
|
|
})
|
|
expect(result.encoded.share).toBeUndefined()
|
|
})
|
|
})
|