Skip to content

Commit b49d341

Browse files
committed
fix: preserve malformed YAML configs during MCP install
- Add dream.md to managed ByteRover skill files - Add regression coverage for skill template enumeration - Fail YAML MCP writes when existing config is malformed or non-mapping - Preserve existing YAML bytes instead of overwriting with a fresh config
1 parent 2eec288 commit b49d341

4 files changed

Lines changed: 53 additions & 8 deletions

File tree

src/server/infra/connectors/mcp/yaml-mcp-config-writer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ export class YamlMcpConfigWriter implements IMcpConfigWriter {
9191
if (await this.fileService.exists(filePath)) {
9292
try {
9393
data = parseYamlAsRecord(await this.fileService.read(filePath))
94-
} catch {
95-
// File exists but contains invalid/empty YAML — start fresh
94+
} catch (error) {
95+
const details = error instanceof Error ? error.message : String(error)
96+
throw new Error(`Cannot update YAML MCP config at ${filePath}: ${details}`)
9697
}
9798
}
9899

src/server/infra/connectors/skill/skill-connector-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export const SKILL_FILE_NAMES = [
138138
'review.md',
139139
'swarm.md',
140140
'vc.md',
141+
'dream.md',
141142
'history.md',
142143
'troubleshooting.md',
143144
] as const

test/unit/infra/connectors/mcp/yaml-mcp-config-writer.test.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,19 +173,52 @@ describe('YamlMcpConfigWriter', () => {
173173
expect(parsed.level1.level2.brv).to.deep.equal(serverConfig)
174174
})
175175

176-
it('starts fresh when existing YAML is malformed', async () => {
176+
it('throws and preserves existing content when YAML is malformed', async () => {
177177
const writer = new YamlMcpConfigWriter({
178178
fileService,
179179
serverKeyPath: ['mcp_servers', 'brv'],
180180
})
181181
const filePath = path.join(testDir, 'config.yaml')
182-
await writeFile(filePath, ':\n\t[bad: yaml')
182+
const originalContent = ':\n\t[bad: yaml'
183+
await writeFile(filePath, originalContent)
183184
const serverConfig = {command: 'brv'}
184185

185-
await writer.write(filePath, serverConfig)
186+
let error: unknown
187+
try {
188+
await writer.write(filePath, serverConfig)
189+
} catch (error_) {
190+
error = error_
191+
}
186192

187-
const parsed = yamlLoad(await readFile(filePath, 'utf8')) as Record<string, Record<string, unknown>>
188-
expect(parsed.mcp_servers.brv).to.deep.equal(serverConfig)
193+
expect(error).to.be.instanceOf(Error)
194+
if (!(error instanceof Error)) throw new Error('Expected write to throw')
195+
196+
expect(error.message).to.include('Cannot update YAML MCP config')
197+
expect(await readFile(filePath, 'utf8')).to.equal(originalContent)
198+
})
199+
200+
it('throws and preserves existing content when YAML root is not a mapping', async () => {
201+
const writer = new YamlMcpConfigWriter({
202+
fileService,
203+
serverKeyPath: ['mcp_servers', 'brv'],
204+
})
205+
const filePath = path.join(testDir, 'config.yaml')
206+
const originalContent = '- a\n- b\n'
207+
await writeFile(filePath, originalContent)
208+
const serverConfig = {command: 'brv'}
209+
210+
let error: unknown
211+
try {
212+
await writer.write(filePath, serverConfig)
213+
} catch (error_) {
214+
error = error_
215+
}
216+
217+
expect(error).to.be.instanceOf(Error)
218+
if (!(error instanceof Error)) throw new Error('Expected write to throw')
219+
220+
expect(error.message).to.include('Cannot update YAML MCP config')
221+
expect(await readFile(filePath, 'utf8')).to.equal(originalContent)
189222
})
190223
})
191224

test/unit/infra/connectors/skill/skill-connector.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {expect} from 'chai'
2-
import {mkdir, readFile, rm, writeFile} from 'node:fs/promises'
2+
import {mkdir, readdir, readFile, rm, writeFile} from 'node:fs/promises'
33
import {tmpdir} from 'node:os'
44
import path from 'node:path'
55

@@ -42,6 +42,16 @@ describe('SkillConnector', () => {
4242
})
4343
})
4444

45+
describe('managed skill files', () => {
46+
it('should enumerate every skill template markdown file', async () => {
47+
const templateFileNames = (await readdir(path.resolve('src/server/templates/skill')))
48+
.filter((fileName) => fileName.endsWith('.md'))
49+
.sort()
50+
51+
expect([...SKILL_FILE_NAMES].sort()).to.deep.equal(templateFileNames)
52+
})
53+
})
54+
4555
describe('getSupportedAgents', () => {
4656
it('should return agents that support skill connector', () => {
4757
const agents = skillConnector.getSupportedAgents()

0 commit comments

Comments
 (0)