fix(athena): add schema validation for unique names and sanitization
This commit is contained in:
@@ -181,6 +181,86 @@ describe("CouncilMemberSchema", () => {
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("trims leading and trailing whitespace from name", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: " member-a " }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.name).toBe("member-a")
|
||||
}
|
||||
})
|
||||
|
||||
test("accepts name with spaces like 'Claude Opus 4'", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: "Claude Opus 4" }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test("accepts name with dots like 'Claude 4.6'", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: "Claude 4.6" }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test("accepts name with hyphens like 'my-model-1'", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: "my-model-1" }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
test("rejects name with special characters like '@'", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: "member@1" }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects name with exclamation mark", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: "member!" }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects name starting with a space after trim", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6", name: " " }
|
||||
|
||||
//#when
|
||||
const result = CouncilMemberSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("CouncilConfigSchema", () => {
|
||||
@@ -249,13 +329,60 @@ describe("CouncilConfigSchema", () => {
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects council with duplicate member names", () => {
|
||||
//#given
|
||||
const config = {
|
||||
members: [
|
||||
{ model: "anthropic/claude-opus-4-6", name: "analyst" },
|
||||
{ model: "openai/gpt-5.3-codex", name: "analyst" },
|
||||
],
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = CouncilConfigSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects council with case-insensitive duplicate names", () => {
|
||||
//#given
|
||||
const config = {
|
||||
members: [
|
||||
{ model: "anthropic/claude-opus-4-6", name: "Claude" },
|
||||
{ model: "openai/gpt-5.3-codex", name: "claude" },
|
||||
],
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = CouncilConfigSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts council with unique member names", () => {
|
||||
//#given
|
||||
const config = {
|
||||
members: [
|
||||
{ model: "anthropic/claude-opus-4-6", name: "analyst-a" },
|
||||
{ model: "openai/gpt-5.3-codex", name: "analyst-b" },
|
||||
],
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = CouncilConfigSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("AthenaConfigSchema", () => {
|
||||
test("accepts Athena config with model and council", () => {
|
||||
test("accepts Athena config with council", () => {
|
||||
//#given
|
||||
const config = {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
council: {
|
||||
members: [
|
||||
{ model: "openai/gpt-5.3-codex", name: "member-a" },
|
||||
@@ -273,7 +400,26 @@ describe("AthenaConfigSchema", () => {
|
||||
|
||||
test("rejects Athena config without council", () => {
|
||||
//#given
|
||||
const config = { model: "anthropic/claude-opus-4-6" }
|
||||
const config = {}
|
||||
|
||||
//#when
|
||||
const result = AthenaConfigSchema.safeParse(config)
|
||||
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects Athena config with unknown model field", () => {
|
||||
//#given
|
||||
const config = {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
council: {
|
||||
members: [
|
||||
{ model: "openai/gpt-5.3-codex", name: "member-a" },
|
||||
{ model: "xai/grok-code-fast-1", name: "member-b" },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
//#when
|
||||
const result = AthenaConfigSchema.safeParse(config)
|
||||
|
||||
@@ -13,18 +13,25 @@ const ModelStringSchema = z
|
||||
export const CouncilMemberSchema = z.object({
|
||||
model: ModelStringSchema,
|
||||
variant: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
name: z.string().min(1).trim().regex(/^[a-zA-Z0-9][a-zA-Z0-9 .\-]*$/, {
|
||||
message: "Council member name must contain only letters, numbers, spaces, hyphens, and dots",
|
||||
}),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
}).strict()
|
||||
|
||||
export const CouncilConfigSchema = z.object({
|
||||
members: z.array(CouncilMemberSchema).min(2),
|
||||
members: z.array(CouncilMemberSchema).min(2).refine(
|
||||
(members) => {
|
||||
const names = members.map(m => m.name.toLowerCase())
|
||||
return new Set(names).size === names.length
|
||||
},
|
||||
{ message: "Council member names must be unique (case-insensitive)" },
|
||||
),
|
||||
}).strict()
|
||||
|
||||
export type CouncilMemberConfig = z.infer<typeof CouncilMemberSchema>
|
||||
export type CouncilConfig = z.infer<typeof CouncilConfigSchema>
|
||||
|
||||
export const AthenaConfigSchema = z.object({
|
||||
model: z.string().optional(),
|
||||
council: CouncilConfigSchema,
|
||||
}).strict()
|
||||
|
||||
Reference in New Issue
Block a user