Skip to content

Commit 933b9ff

Browse files
committed
Merge remote-tracking branch 'origin/dev' into fix/model-runtime-fallback
2 parents 1fded58 + 3d2eb6e commit 933b9ff

17 files changed

Lines changed: 278 additions & 46 deletions

File tree

src/cli/config-manager/add-plugin-to-opencode-config.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
7979

8080
const normalizedPlugins = [...otherPlugins]
8181

82-
if (canonicalEntries.length > 0 || legacyEntries.length > 0) {
83-
normalizedPlugins.push(pluginEntry)
84-
} else {
85-
normalizedPlugins.push(pluginEntry)
86-
}
82+
normalizedPlugins.push(pluginEntry)
8783

8884
config.plugin = normalizedPlugins
8985

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { afterEach, describe, expect, test } from "bun:test"
2+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
3+
import { tmpdir } from "node:os"
4+
import { join } from "node:path"
5+
6+
import { parseOpenCodeConfigFileWithError } from "./parse-opencode-config-file"
7+
8+
describe("parseOpenCodeConfigFileWithError", () => {
9+
const tempDirectories: string[] = []
10+
11+
afterEach(() => {
12+
for (const directory of tempDirectories.splice(0)) {
13+
rmSync(directory, { recursive: true, force: true })
14+
}
15+
})
16+
17+
test("#given a valid object config #when parsing the file #then it returns the parsed config", () => {
18+
// given
19+
const directory = mkdtempSync(join(tmpdir(), "omo-parse-config-"))
20+
tempDirectories.push(directory)
21+
const filePath = join(directory, "opencode.json")
22+
writeFileSync(filePath, '{"plugin": ["oh-my-openagent"]}\n', "utf-8")
23+
24+
// when
25+
const result = parseOpenCodeConfigFileWithError(filePath)
26+
27+
// then
28+
expect(result).toEqual({
29+
config: { plugin: ["oh-my-openagent"] },
30+
})
31+
})
32+
33+
test("#given a null config payload #when parsing the file #then it returns a null parse error", () => {
34+
// given
35+
const directory = mkdtempSync(join(tmpdir(), "omo-parse-config-"))
36+
tempDirectories.push(directory)
37+
const filePath = join(directory, "opencode.json")
38+
writeFileSync(filePath, "null\n", "utf-8")
39+
40+
// when
41+
const result = parseOpenCodeConfigFileWithError(filePath)
42+
43+
// then
44+
expect(result).toEqual({
45+
config: null,
46+
error: `Config file parsed to null/undefined: ${filePath}. Ensure it contains valid JSON.`,
47+
})
48+
})
49+
})

src/cli/config-manager/parse-opencode-config-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function parseOpenCodeConfigFileWithError(path: string): ParseConfigResul
3030

3131
const config = parseJsonc<OpenCodeConfig>(content)
3232

33-
if (config === null || config === undefined) {
33+
if (config == null) {
3434
return { config: null, error: `Config file parsed to null/undefined: ${path}. Ensure it contains valid JSON.` }
3535
}
3636

src/config/schema/dynamic-context-pruning.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { z } from "zod"
22

33
export const DynamicContextPruningConfigSchema = z.object({
4-
/** Enable dynamic context pruning (default: false) */
54
enabled: z.boolean().default(false),
6-
/** Notification level: off, minimal, or detailed (default: detailed) */
75
notification: z.enum(["off", "minimal", "detailed"]).default("detailed"),
86
/** Turn protection - prevent pruning recent tool outputs */
97
turn_protection: z

src/config/schema/ralph-loop.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { z } from "zod"
22

33
export const RalphLoopConfigSchema = z.object({
4-
/** Enable ralph loop functionality (default: false - opt-in feature) */
54
enabled: z.boolean().default(false),
6-
/** Default max iterations if not specified in command (default: 100) */
75
default_max_iterations: z.number().min(1).max(1000).default(100),
86
/** Custom state file directory relative to project root (default: .opencode/) */
97
state_dir: z.string().optional(),

src/config/schema/start-work.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { z } from "zod"
22

33
export const StartWorkConfigSchema = z.object({
4-
/** Enable auto-commit after each atomic task completion (default: true) */
54
auto_commit: z.boolean().default(true),
65
})
76

src/features/background-agent/loop-detector.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ describe("loop-detector", () => {
112112
expect(result).toBe("read")
113113
})
114114

115+
test("#given nullish inputs #when signatures are created #then null and undefined behave the same", () => {
116+
// given
117+
const undefinedInput = undefined
118+
const nullInput = null
119+
120+
// when
121+
const undefinedResult = createToolCallSignature("read", undefinedInput)
122+
const nullResult = createToolCallSignature("read", nullInput)
123+
124+
// then
125+
expect(undefinedResult).toBe("read")
126+
expect(nullResult).toBe(undefinedResult)
127+
})
128+
115129
test("#given tool with empty object input #when signature created #then returns bare tool name", () => {
116130
const result = createToolCallSignature("read", {})
117131

@@ -259,5 +273,24 @@ describe("loop-detector", () => {
259273
expect(result).toEqual({ triggered: false })
260274
})
261275
})
276+
277+
describe("#given nullish tool inputs", () => {
278+
test("#when recorded #then null and undefined produce the same unknown-input window", () => {
279+
// given
280+
const settings = resolveCircuitBreakerSettings()
281+
282+
// when
283+
const undefinedWindow = recordToolCall(undefined, "read", settings, undefined)
284+
const nullWindow = recordToolCall(undefined, "read", settings, null)
285+
286+
// then
287+
expect(undefinedWindow).toEqual(nullWindow)
288+
expect(undefinedWindow).toEqual({
289+
lastSignature: "read::__unknown-input__",
290+
consecutiveCount: 1,
291+
threshold: settings.consecutiveThreshold,
292+
})
293+
})
294+
})
262295
})
263296
})

src/features/background-agent/loop-detector.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function recordToolCall(
3636
settings: CircuitBreakerSettings,
3737
toolInput?: Record<string, unknown> | null
3838
): ToolCallWindow {
39-
if (toolInput === undefined || toolInput === null) {
39+
if (toolInput == null) {
4040
return {
4141
lastSignature: `${toolName}::__unknown-input__`,
4242
consecutiveCount: 1,
@@ -62,7 +62,7 @@ export function recordToolCall(
6262
}
6363

6464
function sortObject(obj: unknown): unknown {
65-
if (obj === null || obj === undefined) return obj
65+
if (obj == null) return obj
6666
if (typeof obj !== "object") return obj
6767
if (Array.isArray(obj)) return obj.map(sortObject)
6868

@@ -78,7 +78,7 @@ export function createToolCallSignature(
7878
toolName: string,
7979
toolInput?: Record<string, unknown> | null
8080
): string {
81-
if (toolInput === undefined || toolInput === null) {
81+
if (toolInput == null) {
8282
return toolName
8383
}
8484
if (Object.keys(toolInput).length === 0) {

src/features/claude-code-mcp-loader/env-expander.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function expandEnvVars(value: string, options: ExpandEnvVarsOptions = {})
3535
}
3636

3737
export function expandEnvVarsInObject<T>(obj: T, options: ExpandEnvVarsOptions = {}): T {
38-
if (obj === null || obj === undefined) return obj
38+
if (obj == null) return obj
3939
if (typeof obj === "string") return expandEnvVars(obj, options) as T
4040
if (Array.isArray(obj)) {
4141
return obj.map((item) => expandEnvVarsInObject(item, options)) as T
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, test } from "bun:test"
2+
3+
import { resolvePluginPath, resolvePluginPaths } from "./plugin-path-resolver"
4+
5+
describe("resolvePluginPath", () => {
6+
test("#given a plugin root placeholder #when resolving the path #then it replaces the placeholder", () => {
7+
// given
8+
const path = "${CLAUDE_PLUGIN_ROOT}/dist/index.js"
9+
10+
// when
11+
const result = resolvePluginPath(path, "/tmp/plugin-root")
12+
13+
// then
14+
expect(result).toBe("/tmp/plugin-root/dist/index.js")
15+
})
16+
})
17+
18+
describe("resolvePluginPaths", () => {
19+
test("#given a nested object #when resolving paths #then it rewrites every nested string path", () => {
20+
// given
21+
const value = {
22+
command: "node",
23+
args: ["${CLAUDE_PLUGIN_ROOT}/server.js"],
24+
nested: {
25+
config: "${CLAUDE_PLUGIN_ROOT}/config.json",
26+
},
27+
}
28+
29+
// when
30+
const result = resolvePluginPaths(value, "/tmp/plugin-root")
31+
32+
// then
33+
expect(result).toEqual({
34+
command: "node",
35+
args: ["/tmp/plugin-root/server.js"],
36+
nested: {
37+
config: "/tmp/plugin-root/config.json",
38+
},
39+
})
40+
})
41+
42+
test("#given nullish input #when resolving paths #then it returns the same nullish value", () => {
43+
// given
44+
const nullValue = null
45+
const undefinedValue = undefined
46+
47+
// when
48+
const nullResult = resolvePluginPaths(nullValue, "/tmp/plugin-root")
49+
const undefinedResult = resolvePluginPaths(undefinedValue, "/tmp/plugin-root")
50+
51+
// then
52+
expect(nullResult).toBeNull()
53+
expect(undefinedResult).toBeUndefined()
54+
})
55+
})

0 commit comments

Comments
 (0)