Skip to content

Commit 799d897

Browse files
committed
Fix bug with empty prompt components
1 parent 5c00e9c commit 799d897

File tree

5 files changed

+215
-8
lines changed

5 files changed

+215
-8
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, it, expect } from "vitest"
2+
import { getPromptComponent } from "../system"
3+
import type { CustomModePrompts } from "@roo-code/types"
4+
5+
describe("getPromptComponent", () => {
6+
it("should return undefined for empty objects", () => {
7+
const customModePrompts: CustomModePrompts = {
8+
architect: {},
9+
}
10+
11+
const result = getPromptComponent(customModePrompts, "architect")
12+
expect(result).toBeUndefined()
13+
})
14+
15+
it("should return the component for objects with any properties", () => {
16+
const customModePrompts: CustomModePrompts = {
17+
architect: {
18+
foo: "bar",
19+
baz: 123,
20+
} as any,
21+
}
22+
23+
const result = getPromptComponent(customModePrompts, "architect")
24+
expect(result).toEqual({ foo: "bar", baz: 123 })
25+
})
26+
27+
it("should return undefined for missing mode", () => {
28+
const customModePrompts: CustomModePrompts = {}
29+
30+
const result = getPromptComponent(customModePrompts, "architect")
31+
expect(result).toBeUndefined()
32+
})
33+
34+
it("should return undefined when customModePrompts is undefined", () => {
35+
const result = getPromptComponent(undefined, "architect")
36+
expect(result).toBeUndefined()
37+
})
38+
39+
it.each([
40+
["roleDefinition", { roleDefinition: "Test role" }],
41+
["customInstructions", { customInstructions: "Test instructions" }],
42+
["whenToUse", { whenToUse: "Test when to use" }],
43+
["description", { description: "Test description" }],
44+
])("should return the component when it has %s", (property, component) => {
45+
const customModePrompts: CustomModePrompts = {
46+
architect: component,
47+
}
48+
49+
const result = getPromptComponent(customModePrompts, "architect")
50+
expect(result).toEqual(component)
51+
})
52+
53+
it("should return the component when it has multiple properties", () => {
54+
const customModePrompts: CustomModePrompts = {
55+
architect: {
56+
roleDefinition: "Test role",
57+
customInstructions: "Test instructions",
58+
whenToUse: "Test when to use",
59+
description: "Test description",
60+
},
61+
}
62+
63+
const result = getPromptComponent(customModePrompts, "architect")
64+
expect(result).toEqual({
65+
roleDefinition: "Test role",
66+
customInstructions: "Test instructions",
67+
whenToUse: "Test when to use",
68+
description: "Test description",
69+
})
70+
})
71+
72+
it("should return the component when it has both relevant and irrelevant properties", () => {
73+
const customModePrompts: CustomModePrompts = {
74+
architect: {
75+
roleDefinition: "Test role",
76+
foo: "bar",
77+
baz: 123,
78+
} as any,
79+
}
80+
81+
const result = getPromptComponent(customModePrompts, "architect")
82+
expect(result).toEqual({
83+
roleDefinition: "Test role",
84+
foo: "bar",
85+
baz: 123,
86+
})
87+
})
88+
})

src/core/prompts/system.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ModeConfig, PromptComponent, CustomModePrompts, TodoItem } from "@
66
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
77
import { DiffStrategy } from "../../shared/tools"
88
import { formatLanguage } from "../../shared/language"
9+
import { isEmpty } from "../../utils/object"
910

1011
import { McpHub } from "../../services/mcp/McpHub"
1112
import { CodeIndexManager } from "../../services/code-index/manager"
@@ -26,6 +27,19 @@ import {
2627
markdownFormattingSection,
2728
} from "./sections"
2829

30+
// Helper function to get prompt component, filtering out empty objects
31+
export function getPromptComponent(
32+
customModePrompts: CustomModePrompts | undefined,
33+
mode: string,
34+
): PromptComponent | undefined {
35+
const component = customModePrompts?.[mode]
36+
// Return undefined if component is empty
37+
if (isEmpty(component)) {
38+
return undefined
39+
}
40+
return component
41+
}
42+
2943
async function generatePrompt(
3044
context: vscode.ExtensionContext,
3145
cwd: string,
@@ -129,13 +143,6 @@ export const SYSTEM_PROMPT = async (
129143
throw new Error("Extension context is required for generating system prompt")
130144
}
131145

132-
const getPromptComponent = (value: unknown) => {
133-
if (typeof value === "object" && value !== null) {
134-
return value as PromptComponent
135-
}
136-
return undefined
137-
}
138-
139146
// Try to load custom system prompt from file
140147
const variablesForPrompt: PromptVariables = {
141148
workspace: cwd,
@@ -147,7 +154,7 @@ export const SYSTEM_PROMPT = async (
147154
const fileCustomSystemPrompt = await loadSystemPromptFile(cwd, mode, variablesForPrompt)
148155

149156
// Check if it's a custom mode
150-
const promptComponent = getPromptComponent(customModePrompts?.[mode])
157+
const promptComponent = getPromptComponent(customModePrompts, mode)
151158

152159
// Get full mode config from custom modes or fall back to built-in modes
153160
const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect } from "vitest"
2+
import { getModeSelection, modes } from "../modes"
3+
import type { PromptComponent } from "@roo-code/types"
4+
5+
describe("getModeSelection with empty promptComponent", () => {
6+
it("should use built-in mode instructions when promptComponent is undefined", () => {
7+
const architectMode = modes.find((m) => m.slug === "architect")!
8+
9+
// Test with undefined promptComponent (which is what getPromptComponent returns for empty objects)
10+
const result = getModeSelection("architect", undefined, [])
11+
12+
// Should use built-in mode values
13+
expect(result.roleDefinition).toBe(architectMode.roleDefinition)
14+
expect(result.baseInstructions).toBe(architectMode.customInstructions)
15+
expect(result.baseInstructions).toContain("Do some information gathering")
16+
})
17+
18+
it("should use built-in mode instructions when promptComponent is null", () => {
19+
const debugMode = modes.find((m) => m.slug === "debug")!
20+
21+
// Test with null promptComponent
22+
const result = getModeSelection("debug", null as any, [])
23+
24+
// Should use built-in mode values
25+
expect(result.roleDefinition).toBe(debugMode.roleDefinition)
26+
expect(result.baseInstructions).toBe(debugMode.customInstructions)
27+
expect(result.baseInstructions).toContain("Reflect on 5-7 different possible sources")
28+
})
29+
30+
it("should use promptComponent when it has actual content", () => {
31+
// Test with promptComponent that has actual content
32+
const validPromptComponent: PromptComponent = {
33+
roleDefinition: "Custom role",
34+
customInstructions: "Custom instructions",
35+
}
36+
const result = getModeSelection("architect", validPromptComponent, [])
37+
38+
// Should use promptComponent values
39+
expect(result.roleDefinition).toBe("Custom role")
40+
expect(result.baseInstructions).toBe("Custom instructions")
41+
})
42+
43+
it("should use promptComponent when it has partial content", () => {
44+
const architectMode = modes.find((m) => m.slug === "architect")!
45+
46+
// Test with promptComponent that only has customInstructions
47+
const partialPromptComponent: PromptComponent = {
48+
customInstructions: "Only custom instructions",
49+
}
50+
const result = getModeSelection("architect", partialPromptComponent, [])
51+
52+
// Should use promptComponent since it has some content
53+
expect(result.roleDefinition).toBe("") // No roleDefinition in promptComponent
54+
expect(result.baseInstructions).toBe("Only custom instructions")
55+
})
56+
})

src/utils/__tests__/object.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, it, expect } from "vitest"
2+
import { isEmpty } from "../object"
3+
4+
describe("isEmpty", () => {
5+
describe("should return true for empty values", () => {
6+
it.each([
7+
["empty object", {}],
8+
["empty array", []],
9+
["null", null],
10+
["undefined", undefined],
11+
["string", "string"],
12+
["number", 123],
13+
["boolean true", true],
14+
["boolean false", false],
15+
])("%s", (_, value) => {
16+
expect(isEmpty(value)).toBe(true)
17+
})
18+
})
19+
20+
describe("should return false for non-empty values", () => {
21+
it.each([
22+
["object with properties", { a: 1 }],
23+
["object with multiple properties", { a: 1, b: 2 }],
24+
["array with one item", [1]],
25+
["array with multiple items", [1, 2, 3]],
26+
])("%s", (_, value) => {
27+
expect(isEmpty(value)).toBe(false)
28+
})
29+
})
30+
31+
it("should handle objects with null prototype", () => {
32+
const obj = Object.create(null)
33+
expect(isEmpty(obj)).toBe(true)
34+
35+
obj.prop = "value"
36+
expect(isEmpty(obj)).toBe(false)
37+
})
38+
})

src/utils/object.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Check if an object is empty (has no own enumerable properties)
3+
* @param obj The object to check
4+
* @returns true if the object is empty, false otherwise
5+
*/
6+
export function isEmpty(obj: unknown): boolean {
7+
if (!obj || typeof obj !== "object") {
8+
return true
9+
}
10+
11+
// Check if it's an array
12+
if (Array.isArray(obj)) {
13+
return obj.length === 0
14+
}
15+
16+
// Check if it's an object with no own properties
17+
return Object.keys(obj).length === 0
18+
}

0 commit comments

Comments
 (0)