Skip to content

Commit b6f3ede

Browse files
authored
Merge pull request #6781 from Jazzcort/fix-block-duplication
Fix: Remove duplicate blocks and provide config warnings
2 parents 826c08e + bc3375e commit b6f3ede

7 files changed

Lines changed: 242 additions & 11 deletions

File tree

packages/config-yaml/src/__tests__/index.test.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,11 @@ describe("E2E Scenarios", () => {
9393
const registry: Registry = {
9494
getContent: async function (id: PackageIdentifier): Promise<string> {
9595
const slug = packageIdentifierToShorthandSlug(id);
96-
return fs
97-
.readFileSync(`./src/__tests__/packages/${slug}.yaml`)
98-
.toString();
96+
const filePath =
97+
id.uriType === "slug"
98+
? `./src/__tests__/packages/${slug}.yaml`
99+
: id.filePath;
100+
return fs.readFileSync(filePath).toString();
99101
},
100102
};
101103

@@ -230,7 +232,7 @@ describe("E2E Scenarios", () => {
230232
orgScopeId: "test-org",
231233
currentUserSlug: "test-user",
232234
onPremProxyUrl: null,
233-
// Add an injected block
235+
// Add injected blocks
234236
injectBlocks: [
235237
{
236238
uriType: "slug",
@@ -240,11 +242,16 @@ describe("E2E Scenarios", () => {
240242
versionSlug: "latest",
241243
},
242244
},
245+
{
246+
uriType: "file",
247+
filePath: "./src/__tests__/local-files/rules.yaml",
248+
},
243249
],
244250
},
245251
);
246252

247253
const config = unrolledConfig.config;
254+
const errors = unrolledConfig.errors;
248255

249256
// The original rules array should have two items
250257
expect(config?.rules?.length).toBe(3); // Now 3 with the injected block
@@ -255,7 +262,72 @@ describe("E2E Scenarios", () => {
255262
);
256263

257264
// Check the injected doc block was added
258-
expect(config?.rules?.[2]).toBe("Be kind");
265+
expect(
266+
typeof config?.rules?.[2] !== "string" &&
267+
config?.rules?.[2]?.rule === "Be humble",
268+
);
269+
270+
// Check if we receive one error that is caused by duplicate rules
271+
expect(errors?.length).toBe(1);
272+
expect(errors?.[0].message.includes("Duplicate rules detected"));
273+
});
274+
275+
it("duplicate detection should happen in the assistant config first and then the intected blocks", async () => {
276+
const unrolledConfig = await unrollAssistant(
277+
{
278+
uriType: "file",
279+
filePath: "./src/__tests__/local-files/duplicate-test-assistant.yaml",
280+
},
281+
registry,
282+
{
283+
renderSecrets: true,
284+
platformClient,
285+
orgScopeId: "test-org",
286+
currentUserSlug: "test-user",
287+
onPremProxyUrl: null,
288+
// Add injected blocks
289+
injectBlocks: [
290+
{
291+
uriType: "file",
292+
filePath: "./src/__tests__/local-files/rules.yaml",
293+
},
294+
{
295+
uriType: "file",
296+
filePath: "./src/__tests__/local-files/mcpServer.yaml",
297+
},
298+
{
299+
uriType: "file",
300+
filePath: "./src/__tests__/local-files/prompt.yaml",
301+
},
302+
],
303+
},
304+
);
305+
306+
const config = unrolledConfig.config;
307+
const errors = unrolledConfig.errors;
308+
309+
// Check if all the duplicate blocks get removed
310+
expect(config?.models?.length).toBe(1);
311+
expect(config?.context?.length).toBe(1);
312+
expect(config?.mcpServers?.length).toBe(1);
313+
expect(config?.rules?.length).toBe(1);
314+
expect(config?.prompts?.length).toBe(1);
315+
expect(config?.docs?.length).toBe(1);
316+
317+
// Check if there are 8 duplication detected
318+
expect(errors?.length).toBe(8);
319+
320+
// Beginning of the assistant config duplication check
321+
expect(
322+
errors?.[0].message.includes("Duplicate models named gpt-5 detected"),
323+
).toBe(true);
324+
// Beginning of the injected blocks duplication check
325+
expect(errors?.[4].message.includes("Duplicate rules detected")).toBe(true);
326+
expect(
327+
errors?.[6].message.includes(
328+
"Duplicate mcpServers named Browser search detected",
329+
),
330+
).toBe(true);
259331
});
260332

261333
it.skip("should prioritize org over user / package secrets", () => {});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Duplicate Test Assistant
2+
version: 0.0.1
3+
schema: v1
4+
5+
docs:
6+
- name: React
7+
startUrl: https://react.dev
8+
- name: React
9+
startUrl: https://react.dev
10+
11+
rules:
12+
- Be humble
13+
- uses: ./src/__tests__/local-files/rules.yaml
14+
15+
models:
16+
- name: gpt-5
17+
provider: openai
18+
model: gpt-5
19+
apiKey: ${{ secrets.OPENAI_API_KEY }}
20+
roles:
21+
- chat
22+
23+
- name: gpt-5
24+
provider: openai
25+
model: gpt-5
26+
apiKey: ${{ secrets.OPENAI_API_KEY }}
27+
roles:
28+
- chat
29+
30+
mcpServers:
31+
- uses: ./src/__tests__/local-files/mcpServer.yaml
32+
33+
prompts:
34+
- uses: ./src/__tests__/local-files/prompt.yaml
35+
- uses: ./src/__tests__/local-files/prompt.yaml
36+
37+
context:
38+
- provider: doc
39+
- provider: doc
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: Local MCP Server
2+
version: 0.0.1
3+
schema: v1
4+
mcpServers:
5+
- name: Browser search
6+
command: npx
7+
args:
8+
- "@playwright/mcp@latest"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: Local prompt
2+
version: 0.0.1
3+
schema: v1
4+
prompts:
5+
- name: duplication test
6+
description: test purpose
7+
prompt: Please check that this prompt should only appear once
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: Rules
2+
version: 0.0.1
3+
schema: v1
4+
5+
rules:
6+
- Be humble
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { BLOCK_TYPES, BlockType } from "./getBlockType.js";
2+
3+
export class BlockDuplicationDetector {
4+
private records: Map<string, Set<string>>;
5+
6+
constructor() {
7+
this.records = new Map();
8+
for (const blockType of BLOCK_TYPES) {
9+
this.records.set(blockType, new Set());
10+
}
11+
}
12+
13+
private isRuleDuplicated(rule: any): boolean {
14+
if (typeof rule === "string") {
15+
return this.check(rule, "rules");
16+
} else {
17+
return this.check(rule.name, "rules");
18+
}
19+
}
20+
21+
private isContextDuplicated(context: any): boolean {
22+
return this.check(context.provider, "context");
23+
}
24+
25+
private isCommonBlockDuplicated(block: any, blockType: BlockType): boolean {
26+
return this.check(block.name, blockType);
27+
}
28+
29+
private check(identifier: string, blockType: BlockType): boolean {
30+
if (this.records.get(blockType)!.has(identifier)) {
31+
return true;
32+
} else {
33+
this.records.get(blockType)!.add(identifier);
34+
return false;
35+
}
36+
}
37+
38+
// Check if the name is duplicated within the same blockType
39+
isDuplicated(
40+
block: any,
41+
blockType: BlockType,
42+
injectError?: (errorMsg: string) => void,
43+
): boolean {
44+
// Not checking any null or undefined object
45+
if (block === null || block === undefined) {
46+
return false;
47+
}
48+
49+
switch (blockType) {
50+
case "rules":
51+
if (this.isRuleDuplicated(block)) {
52+
injectError?.(
53+
`Duplicate rules${typeof block === "string" ? "" : ` named ${block.name}`} detected. The duplicate one has been deleted for preventing unexpected behavior.`,
54+
);
55+
return true;
56+
}
57+
return false;
58+
case "context":
59+
if (this.isContextDuplicated(block)) {
60+
injectError?.(
61+
`Duplicate ${block.provider} context providers detected. The duplicate one has been deleted for preventing unexpected behavior.`,
62+
);
63+
return true;
64+
}
65+
return false;
66+
default:
67+
if (this.isCommonBlockDuplicated(block, blockType)) {
68+
injectError?.(
69+
`Duplicate ${blockType} named ${block.name} detected. The duplicate one has been deleted for preventing unexpected behavior.`,
70+
);
71+
return true;
72+
}
73+
return false;
74+
}
75+
}
76+
}

packages/config-yaml/src/load/unroll.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
Rule,
2121
} from "../schemas/index.js";
2222
import { ConfigResult, ConfigValidationError } from "../validation.js";
23+
import { BlockDuplicationDetector } from "./blockDuplicationDetector.js";
2324
import {
2425
packageIdentifierToShorthandSlug,
2526
useProxyForUnrenderedSecrets,
@@ -306,6 +307,13 @@ export async function unrollBlocks(
306307
): Promise<ConfigResult<AssistantUnrolled>> {
307308
const errors: ConfigValidationError[] = [];
308309

310+
function injectDuplicationError(errorMsg: string) {
311+
errors.push({
312+
fatal: false,
313+
message: errorMsg,
314+
});
315+
}
316+
309317
const unrolledAssistant: AssistantUnrolled = {
310318
name: assistant.name,
311319
version: assistant.version,
@@ -484,7 +492,7 @@ export async function unrollBlocks(
484492
const injectedResults = await Promise.all(injectedBlockPromises);
485493
const injectedErrors: ConfigValidationError[] = [];
486494
const injectedBlocks: {
487-
blockType: string;
495+
blockType: BlockType;
488496
resolvedBlock: any;
489497
source?: string;
490498
}[] = [];
@@ -521,16 +529,27 @@ export async function unrollBlocks(
521529
errors.push(...rulesResult.errors);
522530
errors.push(...injectedResult.errors);
523531

532+
const detector = new BlockDuplicationDetector();
533+
524534
// Assign section results
525535
for (const sectionResult of sectionResults) {
526536
if (sectionResult.blocks) {
527-
unrolledAssistant[sectionResult.section] = sectionResult.blocks;
537+
unrolledAssistant[sectionResult.section] = sectionResult.blocks.filter(
538+
(block) =>
539+
!detector.isDuplicated(
540+
block,
541+
sectionResult.section,
542+
injectDuplicationError,
543+
),
544+
);
528545
}
529546
}
530547

531548
// Assign rules result
532549
if (rulesResult.rules) {
533-
unrolledAssistant.rules = rulesResult.rules;
550+
unrolledAssistant.rules = rulesResult.rules.filter(
551+
(rule) => !detector.isDuplicated(rule, "rules", injectDuplicationError),
552+
);
534553
}
535554

536555
// Add injected blocks
@@ -539,16 +558,20 @@ export async function unrollBlocks(
539558
resolvedBlock,
540559
source,
541560
} of injectedResult.injectedBlocks) {
542-
const key = blockType as BlockType;
561+
const key = blockType;
543562
if (!unrolledAssistant[key]) {
544563
unrolledAssistant[key] = [];
545564
}
546-
const blocksWithSourceFiles = injectLocalSourceFile(
565+
566+
const filteredBlocks = injectLocalSourceFile(
547567
key,
548568
resolvedBlock,
549569
source,
570+
).filter(
571+
(block: any) =>
572+
!detector.isDuplicated(block, blockType, injectDuplicationError),
550573
);
551-
unrolledAssistant[key]?.push(...blocksWithSourceFiles);
574+
unrolledAssistant[key]?.push(...filteredBlocks);
552575
}
553576

554577
const configResult: ConfigResult<AssistantUnrolled> = {

0 commit comments

Comments
 (0)