Merge pull request #3348 from code-yeongyu/refactor/ulw-repo-cleanup-20260411
refactor: simplify nullish guards and remove dead no-op paths
This commit is contained in:
@@ -79,11 +79,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
|
|||||||
|
|
||||||
const normalizedPlugins = [...otherPlugins]
|
const normalizedPlugins = [...otherPlugins]
|
||||||
|
|
||||||
if (canonicalEntries.length > 0 || legacyEntries.length > 0) {
|
normalizedPlugins.push(pluginEntry)
|
||||||
normalizedPlugins.push(pluginEntry)
|
|
||||||
} else {
|
|
||||||
normalizedPlugins.push(pluginEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.plugin = normalizedPlugins
|
config.plugin = normalizedPlugins
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
import { parseOpenCodeConfigFileWithError } from "./parse-opencode-config-file"
|
||||||
|
|
||||||
|
describe("parseOpenCodeConfigFileWithError", () => {
|
||||||
|
const tempDirectories: string[] = []
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const directory of tempDirectories.splice(0)) {
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given a valid object config #when parsing the file #then it returns the parsed config", () => {
|
||||||
|
// given
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), "omo-parse-config-"))
|
||||||
|
tempDirectories.push(directory)
|
||||||
|
const filePath = join(directory, "opencode.json")
|
||||||
|
writeFileSync(filePath, '{"plugin": ["oh-my-openagent"]}\n', "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = parseOpenCodeConfigFileWithError(filePath)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
config: { plugin: ["oh-my-openagent"] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given a null config payload #when parsing the file #then it returns a null parse error", () => {
|
||||||
|
// given
|
||||||
|
const directory = mkdtempSync(join(tmpdir(), "omo-parse-config-"))
|
||||||
|
tempDirectories.push(directory)
|
||||||
|
const filePath = join(directory, "opencode.json")
|
||||||
|
writeFileSync(filePath, "null\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = parseOpenCodeConfigFileWithError(filePath)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
config: null,
|
||||||
|
error: `Config file parsed to null/undefined: ${filePath}. Ensure it contains valid JSON.`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -30,7 +30,7 @@ export function parseOpenCodeConfigFileWithError(path: string): ParseConfigResul
|
|||||||
|
|
||||||
const config = parseJsonc<OpenCodeConfig>(content)
|
const config = parseJsonc<OpenCodeConfig>(content)
|
||||||
|
|
||||||
if (config === null || config === undefined) {
|
if (config == null) {
|
||||||
return { config: null, error: `Config file parsed to null/undefined: ${path}. Ensure it contains valid JSON.` }
|
return { config: null, error: `Config file parsed to null/undefined: ${path}. Ensure it contains valid JSON.` }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const DynamicContextPruningConfigSchema = z.object({
|
export const DynamicContextPruningConfigSchema = z.object({
|
||||||
/** Enable dynamic context pruning (default: false) */
|
|
||||||
enabled: z.boolean().default(false),
|
enabled: z.boolean().default(false),
|
||||||
/** Notification level: off, minimal, or detailed (default: detailed) */
|
|
||||||
notification: z.enum(["off", "minimal", "detailed"]).default("detailed"),
|
notification: z.enum(["off", "minimal", "detailed"]).default("detailed"),
|
||||||
/** Turn protection - prevent pruning recent tool outputs */
|
/** Turn protection - prevent pruning recent tool outputs */
|
||||||
turn_protection: z
|
turn_protection: z
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const RalphLoopConfigSchema = z.object({
|
export const RalphLoopConfigSchema = z.object({
|
||||||
/** Enable ralph loop functionality (default: false - opt-in feature) */
|
|
||||||
enabled: z.boolean().default(false),
|
enabled: z.boolean().default(false),
|
||||||
/** Default max iterations if not specified in command (default: 100) */
|
|
||||||
default_max_iterations: z.number().min(1).max(1000).default(100),
|
default_max_iterations: z.number().min(1).max(1000).default(100),
|
||||||
/** Custom state file directory relative to project root (default: .opencode/) */
|
/** Custom state file directory relative to project root (default: .opencode/) */
|
||||||
state_dir: z.string().optional(),
|
state_dir: z.string().optional(),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const StartWorkConfigSchema = z.object({
|
export const StartWorkConfigSchema = z.object({
|
||||||
/** Enable auto-commit after each atomic task completion (default: true) */
|
|
||||||
auto_commit: z.boolean().default(true),
|
auto_commit: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,20 @@ describe("loop-detector", () => {
|
|||||||
expect(result).toBe("read")
|
expect(result).toBe("read")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("#given nullish inputs #when signatures are created #then null and undefined behave the same", () => {
|
||||||
|
// given
|
||||||
|
const undefinedInput = undefined
|
||||||
|
const nullInput = null
|
||||||
|
|
||||||
|
// when
|
||||||
|
const undefinedResult = createToolCallSignature("read", undefinedInput)
|
||||||
|
const nullResult = createToolCallSignature("read", nullInput)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(undefinedResult).toBe("read")
|
||||||
|
expect(nullResult).toBe(undefinedResult)
|
||||||
|
})
|
||||||
|
|
||||||
test("#given tool with empty object input #when signature created #then returns bare tool name", () => {
|
test("#given tool with empty object input #when signature created #then returns bare tool name", () => {
|
||||||
const result = createToolCallSignature("read", {})
|
const result = createToolCallSignature("read", {})
|
||||||
|
|
||||||
@@ -259,5 +273,24 @@ describe("loop-detector", () => {
|
|||||||
expect(result).toEqual({ triggered: false })
|
expect(result).toEqual({ triggered: false })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("#given nullish tool inputs", () => {
|
||||||
|
test("#when recorded #then null and undefined produce the same unknown-input window", () => {
|
||||||
|
// given
|
||||||
|
const settings = resolveCircuitBreakerSettings()
|
||||||
|
|
||||||
|
// when
|
||||||
|
const undefinedWindow = recordToolCall(undefined, "read", settings, undefined)
|
||||||
|
const nullWindow = recordToolCall(undefined, "read", settings, null)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(undefinedWindow).toEqual(nullWindow)
|
||||||
|
expect(undefinedWindow).toEqual({
|
||||||
|
lastSignature: "read::__unknown-input__",
|
||||||
|
consecutiveCount: 1,
|
||||||
|
threshold: settings.consecutiveThreshold,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function recordToolCall(
|
|||||||
settings: CircuitBreakerSettings,
|
settings: CircuitBreakerSettings,
|
||||||
toolInput?: Record<string, unknown> | null
|
toolInput?: Record<string, unknown> | null
|
||||||
): ToolCallWindow {
|
): ToolCallWindow {
|
||||||
if (toolInput === undefined || toolInput === null) {
|
if (toolInput == null) {
|
||||||
return {
|
return {
|
||||||
lastSignature: `${toolName}::__unknown-input__`,
|
lastSignature: `${toolName}::__unknown-input__`,
|
||||||
consecutiveCount: 1,
|
consecutiveCount: 1,
|
||||||
@@ -62,7 +62,7 @@ export function recordToolCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortObject(obj: unknown): unknown {
|
function sortObject(obj: unknown): unknown {
|
||||||
if (obj === null || obj === undefined) return obj
|
if (obj == null) return obj
|
||||||
if (typeof obj !== "object") return obj
|
if (typeof obj !== "object") return obj
|
||||||
if (Array.isArray(obj)) return obj.map(sortObject)
|
if (Array.isArray(obj)) return obj.map(sortObject)
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export function createToolCallSignature(
|
|||||||
toolName: string,
|
toolName: string,
|
||||||
toolInput?: Record<string, unknown> | null
|
toolInput?: Record<string, unknown> | null
|
||||||
): string {
|
): string {
|
||||||
if (toolInput === undefined || toolInput === null) {
|
if (toolInput == null) {
|
||||||
return toolName
|
return toolName
|
||||||
}
|
}
|
||||||
if (Object.keys(toolInput).length === 0) {
|
if (Object.keys(toolInput).length === 0) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function expandEnvVars(value: string, options: ExpandEnvVarsOptions = {})
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function expandEnvVarsInObject<T>(obj: T, options: ExpandEnvVarsOptions = {}): T {
|
export function expandEnvVarsInObject<T>(obj: T, options: ExpandEnvVarsOptions = {}): T {
|
||||||
if (obj === null || obj === undefined) return obj
|
if (obj == null) return obj
|
||||||
if (typeof obj === "string") return expandEnvVars(obj, options) as T
|
if (typeof obj === "string") return expandEnvVars(obj, options) as T
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
return obj.map((item) => expandEnvVarsInObject(item, options)) as T
|
return obj.map((item) => expandEnvVarsInObject(item, options)) as T
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
|
import { resolvePluginPath, resolvePluginPaths } from "./plugin-path-resolver"
|
||||||
|
|
||||||
|
describe("resolvePluginPath", () => {
|
||||||
|
test("#given a plugin root placeholder #when resolving the path #then it replaces the placeholder", () => {
|
||||||
|
// given
|
||||||
|
const path = "${CLAUDE_PLUGIN_ROOT}/dist/index.js"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = resolvePluginPath(path, "/tmp/plugin-root")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe("/tmp/plugin-root/dist/index.js")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolvePluginPaths", () => {
|
||||||
|
test("#given a nested object #when resolving paths #then it rewrites every nested string path", () => {
|
||||||
|
// given
|
||||||
|
const value = {
|
||||||
|
command: "node",
|
||||||
|
args: ["${CLAUDE_PLUGIN_ROOT}/server.js"],
|
||||||
|
nested: {
|
||||||
|
config: "${CLAUDE_PLUGIN_ROOT}/config.json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = resolvePluginPaths(value, "/tmp/plugin-root")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toEqual({
|
||||||
|
command: "node",
|
||||||
|
args: ["/tmp/plugin-root/server.js"],
|
||||||
|
nested: {
|
||||||
|
config: "/tmp/plugin-root/config.json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given nullish input #when resolving paths #then it returns the same nullish value", () => {
|
||||||
|
// given
|
||||||
|
const nullValue = null
|
||||||
|
const undefinedValue = undefined
|
||||||
|
|
||||||
|
// when
|
||||||
|
const nullResult = resolvePluginPaths(nullValue, "/tmp/plugin-root")
|
||||||
|
const undefinedResult = resolvePluginPaths(undefinedValue, "/tmp/plugin-root")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(nullResult).toBeNull()
|
||||||
|
expect(undefinedResult).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -5,7 +5,7 @@ export function resolvePluginPath(path: string, pluginRoot: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePluginPaths<T>(obj: T, pluginRoot: string): T {
|
export function resolvePluginPaths<T>(obj: T, pluginRoot: string): T {
|
||||||
if (obj === null || obj === undefined) return obj
|
if (obj == null) return obj
|
||||||
if (typeof obj === "string") {
|
if (typeof obj === "string") {
|
||||||
return resolvePluginPath(obj, pluginRoot) as T
|
return resolvePluginPath(obj, pluginRoot) as T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ interface ToolExecuteOutput {
|
|||||||
metadata: unknown;
|
metadata: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolExecuteBeforeOutput {
|
interface DirectoryAgentsInjectorHook {
|
||||||
args: unknown;
|
"tool.execute.before"?: (input: ToolExecuteInput, output: { args: unknown }) => Promise<void>;
|
||||||
|
"tool.execute.after": (input: ToolExecuteInput, output: ToolExecuteOutput) => Promise<void>;
|
||||||
|
event: (input: EventInput) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventInput {
|
interface EventInput {
|
||||||
@@ -30,7 +32,7 @@ interface EventInput {
|
|||||||
export function createDirectoryAgentsInjectorHook(
|
export function createDirectoryAgentsInjectorHook(
|
||||||
ctx: PluginInput,
|
ctx: PluginInput,
|
||||||
modelCacheState?: { anthropicContext1MEnabled: boolean },
|
modelCacheState?: { anthropicContext1MEnabled: boolean },
|
||||||
) {
|
): DirectoryAgentsInjectorHook {
|
||||||
const sessionCaches = new Map<string, Set<string>>();
|
const sessionCaches = new Map<string, Set<string>>();
|
||||||
const truncator = createDynamicTruncator(ctx, modelCacheState);
|
const truncator = createDynamicTruncator(ctx, modelCacheState);
|
||||||
|
|
||||||
@@ -50,14 +52,6 @@ export function createDirectoryAgentsInjectorHook(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolExecuteBefore = async (
|
|
||||||
input: ToolExecuteInput,
|
|
||||||
output: ToolExecuteBeforeOutput,
|
|
||||||
): Promise<void> => {
|
|
||||||
void input;
|
|
||||||
void output;
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventHandler = async ({ event }: EventInput) => {
|
const eventHandler = async ({ event }: EventInput) => {
|
||||||
const props = event.properties as Record<string, unknown> | undefined;
|
const props = event.properties as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
@@ -80,7 +74,6 @@ export function createDirectoryAgentsInjectorHook(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": toolExecuteBefore,
|
|
||||||
"tool.execute.after": toolExecuteAfter,
|
"tool.execute.after": toolExecuteAfter,
|
||||||
event: eventHandler,
|
event: eventHandler,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ interface ToolExecuteOutput {
|
|||||||
metadata: unknown;
|
metadata: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToolExecuteBeforeOutput {
|
interface DirectoryReadmeInjectorHook {
|
||||||
args: unknown;
|
"tool.execute.before"?: (input: ToolExecuteInput, output: { args: unknown }) => Promise<void>;
|
||||||
|
"tool.execute.after": (input: ToolExecuteInput, output: ToolExecuteOutput) => Promise<void>;
|
||||||
|
event: (input: EventInput) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventInput {
|
interface EventInput {
|
||||||
@@ -30,7 +32,7 @@ interface EventInput {
|
|||||||
export function createDirectoryReadmeInjectorHook(
|
export function createDirectoryReadmeInjectorHook(
|
||||||
ctx: PluginInput,
|
ctx: PluginInput,
|
||||||
modelCacheState?: { anthropicContext1MEnabled: boolean },
|
modelCacheState?: { anthropicContext1MEnabled: boolean },
|
||||||
) {
|
): DirectoryReadmeInjectorHook {
|
||||||
const sessionCaches = new Map<string, Set<string>>();
|
const sessionCaches = new Map<string, Set<string>>();
|
||||||
const truncator = createDynamicTruncator(ctx, modelCacheState);
|
const truncator = createDynamicTruncator(ctx, modelCacheState);
|
||||||
|
|
||||||
@@ -50,14 +52,6 @@ export function createDirectoryReadmeInjectorHook(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolExecuteBefore = async (
|
|
||||||
input: ToolExecuteInput,
|
|
||||||
output: ToolExecuteBeforeOutput,
|
|
||||||
): Promise<void> => {
|
|
||||||
void input;
|
|
||||||
void output;
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventHandler = async ({ event }: EventInput) => {
|
const eventHandler = async ({ event }: EventInput) => {
|
||||||
const props = event.properties as Record<string, unknown> | undefined;
|
const props = event.properties as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
@@ -80,7 +74,6 @@ export function createDirectoryReadmeInjectorHook(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": toolExecuteBefore,
|
|
||||||
"tool.execute.after": toolExecuteAfter,
|
"tool.execute.after": toolExecuteAfter,
|
||||||
event: eventHandler,
|
event: eventHandler,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { normalizeSDKResponse } from "../shared/normalize-sdk-response"
|
|
||||||
import { getSessionPromptParams } from "../shared/session-prompt-params-state"
|
import { getSessionPromptParams } from "../shared/session-prompt-params-state"
|
||||||
import { getModelCapabilities, resolveCompatibleModelSettings } from "../shared"
|
import { getModelCapabilities, resolveCompatibleModelSettings } from "../shared"
|
||||||
|
|
||||||
@@ -58,8 +57,6 @@ function buildChatParamsInput(raw: unknown): ChatParamsHookInput | null {
|
|||||||
? model.id
|
? model.id
|
||||||
: undefined
|
: undefined
|
||||||
const providerId = provider.id
|
const providerId = provider.id
|
||||||
const variant = message.variant
|
|
||||||
|
|
||||||
if (typeof providerID !== "string") return null
|
if (typeof providerID !== "string") return null
|
||||||
if (typeof modelID !== "string") return null
|
if (typeof modelID !== "string") return null
|
||||||
if (typeof providerId !== "string") return null
|
if (typeof providerId !== "string") return null
|
||||||
@@ -71,7 +68,6 @@ function buildChatParamsInput(raw: unknown): ChatParamsHookInput | null {
|
|||||||
provider: { id: providerId },
|
provider: { id: providerId },
|
||||||
message,
|
message,
|
||||||
rawMessage: message,
|
rawMessage: message,
|
||||||
...(typeof variant === "string" ? {} : {}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function normalizeSDKResponse<TData>(
|
|||||||
fallback: TData,
|
fallback: TData,
|
||||||
options?: NormalizeSDKResponseOptions,
|
options?: NormalizeSDKResponseOptions,
|
||||||
): TData {
|
): TData {
|
||||||
if (response === null || response === undefined) {
|
if (response == null) {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export function normalizeSDKResponse<TData>(
|
|||||||
|
|
||||||
if (typeof response === "object" && "data" in response) {
|
if (typeof response === "object" && "data" in response) {
|
||||||
const data = (response as { data?: unknown }).data
|
const data = (response as { data?: unknown }).data
|
||||||
if (data !== null && data !== undefined) {
|
if (data != null) {
|
||||||
return data as TData
|
return data as TData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user