fix(athena): add schema validation for unique names and sanitization

This commit is contained in:
ismeth
2026-02-20 14:33:16 +01:00
committed by YeonGyu-Kim
parent 21202ee877
commit 734ef10fbb
2 changed files with 159 additions and 6 deletions

View File

@@ -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)

View File

@@ -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()