-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathschema.ts
More file actions
114 lines (100 loc) · 3.39 KB
/
Copy pathschema.ts
File metadata and controls
114 lines (100 loc) · 3.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { z } from "zod";
const matchExactSchema = z.object({ kind: z.literal("exact") });
const matchMinRowsSchema = z.object({
kind: z.literal("minRows"),
min: z.number().int().nonnegative(),
});
const matchEveryRowContainsSchema = z.object({
kind: z.literal("everyRowContains"),
field: z.string(),
includes: z.string(),
});
const matchEveryRowFieldEqualsSchema = z.object({
kind: z.literal("everyRowFieldEquals"),
field: z.string(),
value: z.union([z.string(), z.number(), z.boolean()]),
});
export const matchSchema = z.union([
matchExactSchema,
matchMinRowsSchema,
matchEveryRowContainsSchema,
matchEveryRowFieldEqualsSchema,
]);
export type GoldenMatch = z.infer<typeof matchSchema>;
/**
* One-time or per-scenario setup step. Extend the union as more one-shot
* ingest / reset verbs land.
*/
export const setupStepSchema = z.discriminatedUnion("kind", [
z.object({
kind: z.literal("ingest-coverage"),
/** Path relative to the fixture root (e.g. `coverage/coverage-final.json`). */
path: z.string().min(1),
}),
z.object({
kind: z.literal("clear-coverage"),
}),
z.object({
kind: z.literal("seed-file-churn"),
/** Path relative to fixture root (JSON array of FileChurnRow). */
path: z.string().min(1),
}),
]);
export type GoldenSetupStep = z.infer<typeof setupStepSchema>;
export const scenarioSchema = z
.object({
id: z.string().min(1),
prompt: z.string().optional(),
sql: z.string().optional(),
recipe: z.string().optional(),
params: z
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
.optional(),
match: matchSchema.optional(),
/** Runs after global `setup` and before this scenario's query (e.g. clear coverage). */
preSetup: z.array(setupStepSchema).optional(),
budgetMs: z.number().positive().optional(),
})
.refine(
(s) => {
const hasSql = typeof s.sql === "string" && s.sql.length > 0;
const hasRecipe = typeof s.recipe === "string" && s.recipe.length > 0;
return hasSql !== hasRecipe;
},
{ message: "Scenario must have exactly one of sql or recipe" },
)
.refine(
(s) => {
// Raw-SQL scenarios cannot declare params; recipe-param validation
// (`resolveRecipeParams`) only runs on the recipe path. The runtime
// check in `query-golden.ts` enforces the same invariant; this
// schema-level refine fails at parse time with a clearer message.
const hasParams = s.params !== undefined;
const hasSql = typeof s.sql === "string" && s.sql.length > 0;
return !hasParams || !hasSql;
},
{
message:
"Scenario params are only allowed alongside `recipe`, not raw `sql`",
},
);
export type GoldenScenario = z.infer<typeof scenarioSchema>;
const legacyArraySchema = z.array(scenarioSchema);
const objectShapeSchema = z.object({
setup: z.array(setupStepSchema).optional(),
scenarios: z.array(scenarioSchema),
});
export const scenariosFileSchema = z.union([
legacyArraySchema,
objectShapeSchema,
]);
export interface ParsedScenariosFile {
setup: GoldenSetupStep[];
scenarios: GoldenScenario[];
}
export function parseScenariosJson(raw: string): ParsedScenariosFile {
const data: unknown = JSON.parse(raw);
const parsed = scenariosFileSchema.parse(data);
if (Array.isArray(parsed)) return { setup: [], scenarios: parsed };
return { setup: parsed.setup ?? [], scenarios: parsed.scenarios };
}