@roubo/shared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config-schema.ts +432 -0
- package/deep-merge.ts +38 -0
- package/gate-overrides-contract.ts +120 -0
- package/integration-types.ts +124 -0
- package/package.json +39 -0
- package/plugin-consent-schema.ts +80 -0
- package/plugin-enable-state-schema.ts +30 -0
- package/plugin-manifest-schema.ts +179 -0
- package/plugin-manifest.ts +55 -0
- package/plugin-runtime-types.ts +50 -0
- package/provision-descriptor-schema.ts +103 -0
- package/testbench-canonicalize.ts +129 -0
- package/testbench-contracts.ts +271 -0
- package/testbench-domain-types.ts +128 -0
- package/testbench-domain.ts +232 -0
- package/testbench-targeting-schema.ts +132 -0
- package/types.ts +1975 -0
- package/work-units-contract.ts +155 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Pure canonicalisation of a TestCasesPlan into a deterministic string
|
|
2
|
+
// (spike-407 AC1 rules 1-5). The server hashes this string with node:crypto to
|
|
3
|
+
// detect staleness (FR-016); this module does NOT hash.
|
|
4
|
+
//
|
|
5
|
+
// Platform-agnostic: no fs, no node:crypto, no React. Safe in the Vite client
|
|
6
|
+
// build. Canonical contract types land with testbench-contracts (#6); until
|
|
7
|
+
// then this consumes the local types in testbench-domain-types.ts.
|
|
8
|
+
//
|
|
9
|
+
// See .specifications/testbench/spikes/spike-407-staleness-hash-reconcile.md
|
|
10
|
+
// AC1 (the rules) and AC2 (the worked example this module is verified against).
|
|
11
|
+
|
|
12
|
+
import type { Case, Observation, Step, TestCasesPlan } from "./testbench-domain-types";
|
|
13
|
+
|
|
14
|
+
// Byte-wise (code-point) comparison, NOT a locale collator. A locale-aware
|
|
15
|
+
// collator is environment-dependent and would make the canonical string (and
|
|
16
|
+
// therefore the hash) non-deterministic across machines. Comparing the raw
|
|
17
|
+
// UTF-16 code units of two strings is a total, stable, environment-independent
|
|
18
|
+
// order, which is what spike-407 AC1 rule 2 requires.
|
|
19
|
+
function compareCodePoints(a: string, b: string): number {
|
|
20
|
+
if (a < b) {
|
|
21
|
+
return -1;
|
|
22
|
+
}
|
|
23
|
+
if (a > b) {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// String content-normalisation (spike-407 AC1 rule 3), applied to every
|
|
30
|
+
// included string value: NFC normalise, convert CRLF and lone CR to LF, trim
|
|
31
|
+
// leading/trailing whitespace, collapse each run of internal whitespace
|
|
32
|
+
// (spaces/tabs/newlines) to a single space.
|
|
33
|
+
function normalizeString(value: string): string {
|
|
34
|
+
return value.normalize("NFC").replace(/\r\n?/g, "\n").trim().replace(/\s+/g, " ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Per-observation projection in fixed canonical key order: id, expected.
|
|
38
|
+
function projectObservation(observation: Observation): { id: string; expected: string } {
|
|
39
|
+
return {
|
|
40
|
+
id: observation.id,
|
|
41
|
+
expected: normalizeString(observation.expected),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Per-step projection in fixed canonical key order: id, instruction,
|
|
46
|
+
// observations (sorted by Observation.id).
|
|
47
|
+
function projectStep(step: Step): {
|
|
48
|
+
id: string;
|
|
49
|
+
instruction: string;
|
|
50
|
+
observations: ReturnType<typeof projectObservation>[];
|
|
51
|
+
} {
|
|
52
|
+
const observations = [...step.observations]
|
|
53
|
+
.sort((a, b) => compareCodePoints(a.id, b.id))
|
|
54
|
+
.map(projectObservation);
|
|
55
|
+
return {
|
|
56
|
+
id: step.id,
|
|
57
|
+
instruction: normalizeString(step.instruction),
|
|
58
|
+
observations,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Per-case projection in fixed canonical key order: id, title, level, priority
|
|
63
|
+
// (when present), preconditions (when present and non-empty), steps (sorted by
|
|
64
|
+
// Step.id).
|
|
65
|
+
//
|
|
66
|
+
// `level` is an integer in the merged v1.1.0 shape; it is stringified before
|
|
67
|
+
// normalisation so the canonical form stays an all-strings projection. `priority`
|
|
68
|
+
// is optional in v1.1.0 (canonical authors omit it): an absent priority omits the
|
|
69
|
+
// key, exactly as preconditions does. The canonical product-dev metadata fields
|
|
70
|
+
// (area, type, tags, linked_*) are deliberately NOT projected: the staleness hash
|
|
71
|
+
// tracks the testable body (title, level, steps), so re-tagging or re-linking a
|
|
72
|
+
// case never marks it stale-for-retest. Like every TargetingField and unknown
|
|
73
|
+
// field, they are dropped.
|
|
74
|
+
//
|
|
75
|
+
// preconditions order is PRESERVED (not sorted). An absent and an empty
|
|
76
|
+
// preconditions list canonicalise identically: the key is omitted in both cases.
|
|
77
|
+
function projectCase(testCase: Case): Record<string, unknown> {
|
|
78
|
+
const projected: Record<string, unknown> = {
|
|
79
|
+
id: testCase.id,
|
|
80
|
+
title: normalizeString(testCase.title),
|
|
81
|
+
level: normalizeString(String(testCase.level)),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (testCase.priority !== undefined) {
|
|
85
|
+
projected.priority = normalizeString(testCase.priority);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (testCase.preconditions !== undefined && testCase.preconditions.length > 0) {
|
|
89
|
+
projected.preconditions = testCase.preconditions.map(normalizeString);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
projected.steps = [...testCase.steps]
|
|
93
|
+
.sort((a, b) => compareCodePoints(a.id, b.id))
|
|
94
|
+
.map(projectStep);
|
|
95
|
+
|
|
96
|
+
return projected;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Produce the deterministic canonical string for a SINGLE case (spike-407 AC1,
|
|
100
|
+
// the per-case projection). This is the per-case unit that `canonicalize(plan)`
|
|
101
|
+
// composes over the whole case set, exported so `testbench-domain.reconcile`
|
|
102
|
+
// can compare one plan case's canonical body against the snapshot stored on a
|
|
103
|
+
// recorded result, sharing one canonicalisation authority (no second copy of
|
|
104
|
+
// the projection rules). Applies the same projection + normalisation + fixed
|
|
105
|
+
// key order as the plan-level canonicalize: drops every TargetingField and
|
|
106
|
+
// unknown field, sorts steps/observations by id, preserves preconditions order.
|
|
107
|
+
export function canonicalizeCase(testCase: Case): string {
|
|
108
|
+
return JSON.stringify(projectCase(testCase));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Produce the deterministic canonical string for a plan (spike-407 AC1).
|
|
112
|
+
//
|
|
113
|
+
// Rules, in order:
|
|
114
|
+
// 1. Project to included fields only (drop $schema, schemaVersion, specSlug,
|
|
115
|
+
// every TargetingField, and any unknown field).
|
|
116
|
+
// 2. Stable-id sort cases/steps/observations by code-point comparison;
|
|
117
|
+
// preconditions order preserved.
|
|
118
|
+
// 3. Normalise every included string value.
|
|
119
|
+
// 4. Serialise with fixed canonical key order and no insignificant whitespace.
|
|
120
|
+
// 5. Return the string (no hashing).
|
|
121
|
+
//
|
|
122
|
+
// The empty case set canonicalises to a fixed, stable, non-empty string:
|
|
123
|
+
// {"cases":[]}
|
|
124
|
+
export function canonicalize(plan: TestCasesPlan): string {
|
|
125
|
+
const cases = [...plan.cases].sort((a, b) => compareCodePoints(a.id, b.id)).map(projectCase);
|
|
126
|
+
// JSON.stringify with no spacing emits keys in insertion order (the fixed
|
|
127
|
+
// canonical order built above) and no insignificant whitespace.
|
|
128
|
+
return JSON.stringify({ cases });
|
|
129
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// TestBench contracts: the single, compile-time source of truth (in `shared/`)
|
|
2
|
+
// for the two published, versioned TestBench files, `test-cases.json` and
|
|
3
|
+
// `test-results.json`. Both server and client validate against these schemas
|
|
4
|
+
// (FR-019, FR-020, FR-021). Shapes follow the architecture.md Data model table
|
|
5
|
+
// exactly; the runtime validators wrap `safeParse` (never throw) and return
|
|
6
|
+
// actionable, field-named errors.
|
|
7
|
+
//
|
|
8
|
+
// Scope note: this module authors the zod source schemas, the inferred types,
|
|
9
|
+
// the runtime validators, and the versioned `$id` constants. The roots carry
|
|
10
|
+
// `.meta({ $id })` so the generate script + CI drift guard (FR-023, #411) can
|
|
11
|
+
// emit versioned JSON Schema from them; the store IO (#11) is out of scope here.
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// ── Versioned schema identifiers (NFR-005) ──
|
|
16
|
+
//
|
|
17
|
+
// Each published file carries a `$schema` URI whose path embeds a semver. A
|
|
18
|
+
// breaking change ships a major bump plus a migration registry entry; additive
|
|
19
|
+
// optional fields (like the reserved targeting fields below) do not bump the
|
|
20
|
+
// version. `TEST_CASES_SCHEMA_VERSION` / `TEST_RESULTS_SCHEMA_VERSION` are the
|
|
21
|
+
// matching `schemaVersion` string values kept consistent with the `$id` semver.
|
|
22
|
+
|
|
23
|
+
export const TEST_CASES_SCHEMA_ID = "https://roubo.dev/schema/testbench/test-cases/v1.1.0.json";
|
|
24
|
+
export const TEST_CASES_SCHEMA_VERSION = "1.1.0";
|
|
25
|
+
|
|
26
|
+
export const TEST_RESULTS_SCHEMA_ID = "https://roubo.dev/schema/testbench/test-results/v2.0.0.json";
|
|
27
|
+
export const TEST_RESULTS_SCHEMA_VERSION = "2.0.0";
|
|
28
|
+
|
|
29
|
+
// ── Shared leaf schemas ──
|
|
30
|
+
|
|
31
|
+
// The fixed derived-status set (FR-009). Owned by `testbench-domain` as a
|
|
32
|
+
// concept, but the literal enum lives here so both files can reference it.
|
|
33
|
+
export const CaseStatusSchema = z.enum([
|
|
34
|
+
"not_started",
|
|
35
|
+
"in_progress",
|
|
36
|
+
"passed",
|
|
37
|
+
"failed",
|
|
38
|
+
"blocked",
|
|
39
|
+
]);
|
|
40
|
+
export type CaseStatus = z.infer<typeof CaseStatusSchema>;
|
|
41
|
+
|
|
42
|
+
// Author of a mark, override, or note (FR-012). `isSentinel` is set when git
|
|
43
|
+
// identity was unset at write time, so writes never fail on missing identity.
|
|
44
|
+
export const AuthorSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
name: z.string(),
|
|
47
|
+
email: z.string(),
|
|
48
|
+
isSentinel: z.literal(true).optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export type Author = z.infer<typeof AuthorSchema>;
|
|
52
|
+
|
|
53
|
+
// Reserved, optional, additive targeting field (FR-019, NFR-005). Flat and
|
|
54
|
+
// all-optional: ignored by the in-app UI today, reserved for the future
|
|
55
|
+
// guided-execution Chrome extension. Present or absent, both validate; adding
|
|
56
|
+
// it to a step/observation is non-breaking.
|
|
57
|
+
export const TargetingFieldSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
cssSelector: z.string().optional(),
|
|
60
|
+
ariaRole: z.string().optional(),
|
|
61
|
+
ariaName: z.string().optional(),
|
|
62
|
+
textAnchor: z.string().optional(),
|
|
63
|
+
routeContext: z.string().optional(),
|
|
64
|
+
region: z.string().optional(),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
export type TargetingField = z.infer<typeof TargetingFieldSchema>;
|
|
68
|
+
|
|
69
|
+
// ── test-cases.json (the source plan; never mutated) ──
|
|
70
|
+
|
|
71
|
+
export const ObservationSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
expected: z.string(),
|
|
75
|
+
// Reserved per-observation targeting field (FR-019).
|
|
76
|
+
observe: TargetingFieldSchema.optional(),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
export type Observation = z.infer<typeof ObservationSchema>;
|
|
80
|
+
|
|
81
|
+
export const StepSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
instruction: z.string(),
|
|
85
|
+
observations: z.array(ObservationSchema),
|
|
86
|
+
// Reserved per-step targeting field (FR-019).
|
|
87
|
+
target: TargetingFieldSchema.optional(),
|
|
88
|
+
})
|
|
89
|
+
.strict();
|
|
90
|
+
export type Step = z.infer<typeof StepSchema>;
|
|
91
|
+
|
|
92
|
+
// Recommended `type` vocabulary: the canonical product-dev set
|
|
93
|
+
// (functional, security, performance, accessibility, integration, negative,
|
|
94
|
+
// edge_case, e2e_flow) expanded with `reliability` and `structural`, both
|
|
95
|
+
// already in use across real specs. This is authoring guidance carried in the
|
|
96
|
+
// product-dev docs; the contract itself validates `type` as a permissive string
|
|
97
|
+
// (see CaseSchema), never a strict enum. The merged schema (v1.1.0) is strict on
|
|
98
|
+
// STRUCTURE (envelope, step ids, required fields) but lenient on open-ended
|
|
99
|
+
// metadata, so a newly-coined `type` can never silently make an entire spec
|
|
100
|
+
// undiscoverable (the failure mode v1.1.0 fixed).
|
|
101
|
+
export const RECOMMENDED_CASE_TYPES = [
|
|
102
|
+
"functional",
|
|
103
|
+
"security",
|
|
104
|
+
"performance",
|
|
105
|
+
"accessibility",
|
|
106
|
+
"integration",
|
|
107
|
+
"negative",
|
|
108
|
+
"edge_case",
|
|
109
|
+
"e2e_flow",
|
|
110
|
+
"reliability",
|
|
111
|
+
"structural",
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
export const CaseSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
title: z.string(),
|
|
118
|
+
// Canonical feature area (kebab-case), e.g. "checkout". Required.
|
|
119
|
+
area: z.string(),
|
|
120
|
+
// Canonical level: an integer 1-4 (L1 smoke .. L4 exploratory). The merge
|
|
121
|
+
// keeps level from the canonical product-dev format (an integer), replacing
|
|
122
|
+
// the earlier string level.
|
|
123
|
+
level: z.number().int().min(1).max(4),
|
|
124
|
+
// Test flavor (see RECOMMENDED_CASE_TYPES). Permissive string by design.
|
|
125
|
+
type: z.string().min(1),
|
|
126
|
+
// Optional priority label (carried from the TestBench shape). Canonical
|
|
127
|
+
// authors omit it; the UI rollup buckets cases with no priority gracefully.
|
|
128
|
+
priority: z.string().optional(),
|
|
129
|
+
preconditions: z.array(z.string()).optional(),
|
|
130
|
+
steps: z.array(StepSchema),
|
|
131
|
+
// Canonical metadata: free-form tags + requirement/story traceability.
|
|
132
|
+
// Every case links at least one requirement; user-story links may be empty.
|
|
133
|
+
tags: z.array(z.string()),
|
|
134
|
+
linked_requirement_ids: z.array(z.string()).min(1),
|
|
135
|
+
linked_user_story_ids: z.array(z.string()),
|
|
136
|
+
})
|
|
137
|
+
.strict();
|
|
138
|
+
export type Case = z.infer<typeof CaseSchema>;
|
|
139
|
+
|
|
140
|
+
export const TestCasesPlanSchema = z
|
|
141
|
+
.object({
|
|
142
|
+
$schema: z.string(),
|
|
143
|
+
schemaVersion: z.string(),
|
|
144
|
+
specSlug: z.string(),
|
|
145
|
+
cases: z.array(CaseSchema),
|
|
146
|
+
})
|
|
147
|
+
.strict()
|
|
148
|
+
// Pass the literal `$id` key through `.meta()` so `z.toJSONSchema()` emits a
|
|
149
|
+
// top-level `$id` carrying the versioned identifier (NFR-005). The generate
|
|
150
|
+
// script + CI drift guard (FR-023) consume this.
|
|
151
|
+
.meta({ $id: TEST_CASES_SCHEMA_ID });
|
|
152
|
+
export type TestCasesPlan = z.infer<typeof TestCasesPlanSchema>;
|
|
153
|
+
|
|
154
|
+
// ── test-results.json (the sidecar; references cases by stable id) ──
|
|
155
|
+
|
|
156
|
+
export const ObservationMarkSchema = z
|
|
157
|
+
.object({
|
|
158
|
+
result: z.enum(["pass", "fail"]),
|
|
159
|
+
author: AuthorSchema,
|
|
160
|
+
timestamp: z.string(),
|
|
161
|
+
})
|
|
162
|
+
.strict();
|
|
163
|
+
export type ObservationMark = z.infer<typeof ObservationMarkSchema>;
|
|
164
|
+
|
|
165
|
+
// Recorded distinctly from `derivedStatus` (FR-010).
|
|
166
|
+
export const StatusOverrideSchema = z
|
|
167
|
+
.object({
|
|
168
|
+
status: CaseStatusSchema,
|
|
169
|
+
author: AuthorSchema,
|
|
170
|
+
timestamp: z.string(),
|
|
171
|
+
})
|
|
172
|
+
.strict();
|
|
173
|
+
export type StatusOverride = z.infer<typeof StatusOverrideSchema>;
|
|
174
|
+
|
|
175
|
+
// Append-only: no edit/delete path exists (FR-011). `statusAtWrite` captures
|
|
176
|
+
// the effective status at the moment the note was written.
|
|
177
|
+
export const NoteSchema = z
|
|
178
|
+
.object({
|
|
179
|
+
id: z.string(),
|
|
180
|
+
text: z.string(),
|
|
181
|
+
author: AuthorSchema,
|
|
182
|
+
timestamp: z.string(),
|
|
183
|
+
statusAtWrite: CaseStatusSchema,
|
|
184
|
+
})
|
|
185
|
+
.strict();
|
|
186
|
+
export type Note = z.infer<typeof NoteSchema>;
|
|
187
|
+
|
|
188
|
+
export const CaseResultSchema = z
|
|
189
|
+
.object({
|
|
190
|
+
// Keyed by observation id; results reference cases by stable id and never
|
|
191
|
+
// require editing test-cases.json (FR-020).
|
|
192
|
+
observationMarks: z.record(z.string(), ObservationMarkSchema),
|
|
193
|
+
derivedStatus: CaseStatusSchema,
|
|
194
|
+
statusOverride: StatusOverrideSchema.optional(),
|
|
195
|
+
notes: z.array(NoteSchema),
|
|
196
|
+
// A removed case's result is marked orphaned and retained, never deleted,
|
|
197
|
+
// and excluded from the rollup (FR-013, FR-017).
|
|
198
|
+
orphaned: z.literal(true).optional(),
|
|
199
|
+
// The per-case canonical body snapshot reconcile compares against the live
|
|
200
|
+
// plan to classify changed vs unchanged (#413, spike-407 AC3). Optional so a
|
|
201
|
+
// result with no stored snapshot still parses and is conservatively
|
|
202
|
+
// classified changed; persisting it lets the signal survive a round-trip to
|
|
203
|
+
// disk (#447).
|
|
204
|
+
caseCanon: z.string().optional(),
|
|
205
|
+
})
|
|
206
|
+
.strict();
|
|
207
|
+
export type CaseResult = z.infer<typeof CaseResultSchema>;
|
|
208
|
+
|
|
209
|
+
// The recorded-results body for a single spec: case results keyed by case id,
|
|
210
|
+
// plus a write timestamp. As of the v2.0.0 flatten (#493), one results file
|
|
211
|
+
// lives per worktree (sibling of test-cases.json), so there is exactly one of
|
|
212
|
+
// these per file and it sits at the top level. This stays a named type because
|
|
213
|
+
// it is also the API result shape the client reads (the route projects the file
|
|
214
|
+
// body down to it), so keeping the name avoids client churn.
|
|
215
|
+
export const BenchResultsSchema = z
|
|
216
|
+
.object({
|
|
217
|
+
// Keyed by case id.
|
|
218
|
+
caseResults: z.record(z.string(), CaseResultSchema),
|
|
219
|
+
updatedAt: z.string(),
|
|
220
|
+
})
|
|
221
|
+
.strict();
|
|
222
|
+
export type BenchResults = z.infer<typeof BenchResultsSchema>;
|
|
223
|
+
|
|
224
|
+
export const TestResultsFileSchema = z
|
|
225
|
+
.object({
|
|
226
|
+
$schema: z.string(),
|
|
227
|
+
schemaVersion: z.string(),
|
|
228
|
+
planHash: z.string(),
|
|
229
|
+
// Flattened in v2.0.0 (#493): one results file per worktree means exactly
|
|
230
|
+
// one bench per file, so case results sit at the top level rather than
|
|
231
|
+
// nested under a per-bench `benches` map. Keyed by case id.
|
|
232
|
+
caseResults: z.record(z.string(), CaseResultSchema),
|
|
233
|
+
updatedAt: z.string(),
|
|
234
|
+
})
|
|
235
|
+
.strict()
|
|
236
|
+
// Pass the literal `$id` key through `.meta()` so `z.toJSONSchema()` emits a
|
|
237
|
+
// top-level `$id` carrying the versioned identifier (NFR-005). The generate
|
|
238
|
+
// script + CI drift guard (FR-023) consume this.
|
|
239
|
+
.meta({ $id: TEST_RESULTS_SCHEMA_ID });
|
|
240
|
+
export type TestResultsFile = z.infer<typeof TestResultsFileSchema>;
|
|
241
|
+
|
|
242
|
+
// ── Runtime validators ──
|
|
243
|
+
//
|
|
244
|
+
// Both wrap `safeParse` (never throw, FR-021) and return a discriminated result.
|
|
245
|
+
// On failure, each zod issue becomes a clear `path: message` string keyed by
|
|
246
|
+
// the field that failed.
|
|
247
|
+
|
|
248
|
+
export type ValidationResult<T> = { ok: true; data: T } | { ok: false; errors: string[] };
|
|
249
|
+
|
|
250
|
+
function zodIssuesToFieldErrors(issues: z.ZodIssue[]): string[] {
|
|
251
|
+
return issues.map((issue) => {
|
|
252
|
+
const path = issue.path.join(".");
|
|
253
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function validateTestCases(raw: unknown): ValidationResult<TestCasesPlan> {
|
|
258
|
+
const parsed = TestCasesPlanSchema.safeParse(raw);
|
|
259
|
+
if (!parsed.success) {
|
|
260
|
+
return { ok: false, errors: zodIssuesToFieldErrors(parsed.error.issues) };
|
|
261
|
+
}
|
|
262
|
+
return { ok: true, data: parsed.data };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function validateTestResults(raw: unknown): ValidationResult<TestResultsFile> {
|
|
266
|
+
const parsed = TestResultsFileSchema.safeParse(raw);
|
|
267
|
+
if (!parsed.success) {
|
|
268
|
+
return { ok: false, errors: zodIssuesToFieldErrors(parsed.error.issues) };
|
|
269
|
+
}
|
|
270
|
+
return { ok: true, data: parsed.data };
|
|
271
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Minimal, self-contained input types for the pure TestBench domain modules
|
|
2
|
+
// (testbench-domain, testbench-canonicalize).
|
|
3
|
+
//
|
|
4
|
+
// The canonical contract types are owned by testbench-contracts (issue #6),
|
|
5
|
+
// which is authored in parallel with this work (#412) and has NOT landed yet.
|
|
6
|
+
// To keep these pure modules self-contained, the shapes below are local copies
|
|
7
|
+
// that structurally match the architecture.md data model
|
|
8
|
+
// (.specifications/testbench/architecture.md, the Data model table). When #6
|
|
9
|
+
// lands, it owns the canonical zod-derived types and these local interfaces
|
|
10
|
+
// should be aligned with (or replaced by) the ones it exports.
|
|
11
|
+
//
|
|
12
|
+
// These modules are platform-agnostic: no fs, no node:crypto, no React. They
|
|
13
|
+
// must not break the Vite client build.
|
|
14
|
+
|
|
15
|
+
// The fixed status set (FR-009). "blocked" is NOT derivable from observation
|
|
16
|
+
// marks (marks are pass|fail only); it reaches a CaseResult only through an
|
|
17
|
+
// explicit override (FR-010), via effectiveStatus = override ?? derived.
|
|
18
|
+
export type CaseStatus = "not_started" | "in_progress" | "passed" | "failed" | "blocked";
|
|
19
|
+
|
|
20
|
+
// Author of a mark or note (FR-012). Included for structural fidelity with the
|
|
21
|
+
// architecture.md shape; the domain functions do not read it.
|
|
22
|
+
export interface Author {
|
|
23
|
+
name: string;
|
|
24
|
+
email: string;
|
|
25
|
+
isSentinel?: true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A single observation result keyed by observation id in a CaseResult's
|
|
29
|
+
// observationMarks map. Marks are pass|fail only.
|
|
30
|
+
export interface ObservationMark {
|
|
31
|
+
result: "pass" | "fail";
|
|
32
|
+
author: Author;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Reserved, all-optional guided-execution targeting field (FR-019). Ignored by
|
|
37
|
+
// canonicalisation (see testbench-canonicalize): present on the input shape so
|
|
38
|
+
// these local types match the architecture.md Step/Observation shapes.
|
|
39
|
+
export interface TargetingField {
|
|
40
|
+
cssSelector?: string;
|
|
41
|
+
ariaRole?: string;
|
|
42
|
+
ariaName?: string;
|
|
43
|
+
textAnchor?: string;
|
|
44
|
+
routeContext?: string;
|
|
45
|
+
region?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Observation {
|
|
49
|
+
id: string;
|
|
50
|
+
expected: string;
|
|
51
|
+
observe?: TargetingField;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Step {
|
|
55
|
+
id: string;
|
|
56
|
+
instruction: string;
|
|
57
|
+
observations: Observation[];
|
|
58
|
+
target?: TargetingField;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Mirrors the merged v1.1.0 CaseSchema in testbench-contracts.ts: the canonical
|
|
62
|
+
// product-dev fields (area, integer level, type, tags, linked_*) unioned with
|
|
63
|
+
// the TestBench shape (optional priority, TestBench step shape). The pure domain
|
|
64
|
+
// modules only read id/title/level/priority/preconditions/steps; the canonical
|
|
65
|
+
// metadata fields ride along for structural fidelity with the contract type.
|
|
66
|
+
export interface Case {
|
|
67
|
+
id: string;
|
|
68
|
+
title: string;
|
|
69
|
+
area: string;
|
|
70
|
+
level: number;
|
|
71
|
+
type: string;
|
|
72
|
+
priority?: string;
|
|
73
|
+
preconditions?: string[];
|
|
74
|
+
steps: Step[];
|
|
75
|
+
tags: string[];
|
|
76
|
+
linked_requirement_ids: string[];
|
|
77
|
+
linked_user_story_ids: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TestCasesPlan {
|
|
81
|
+
$schema: string;
|
|
82
|
+
schemaVersion: string;
|
|
83
|
+
specSlug: string;
|
|
84
|
+
cases: Case[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Result-side input shapes (consumed by testbench-domain.reconcile) ──
|
|
88
|
+
//
|
|
89
|
+
// These mirror the architecture.md data model and the canonical zod shapes in
|
|
90
|
+
// testbench-contracts.ts (NoteSchema, StatusOverrideSchema, CaseResultSchema,
|
|
91
|
+
// BenchResultsSchema). They are local copies for the same reason as the shapes
|
|
92
|
+
// above: testbench-contracts owns the canonical types, and these should be
|
|
93
|
+
// aligned with (or replaced by) its exports when this module consolidates.
|
|
94
|
+
|
|
95
|
+
// An explicit status override, recorded distinctly from derivedStatus (FR-010).
|
|
96
|
+
export interface StatusOverride {
|
|
97
|
+
status: CaseStatus;
|
|
98
|
+
author: Author;
|
|
99
|
+
timestamp: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// An append-only note (FR-011). statusAtWrite captures the effective status at
|
|
103
|
+
// the moment the note was written.
|
|
104
|
+
export interface Note {
|
|
105
|
+
id: string;
|
|
106
|
+
text: string;
|
|
107
|
+
author: Author;
|
|
108
|
+
timestamp: string;
|
|
109
|
+
statusAtWrite: CaseStatus;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A recorded result for one case, keyed by case id in BenchResults.caseResults.
|
|
113
|
+
//
|
|
114
|
+
// The shape is now derived from the published contract's CaseResultSchema
|
|
115
|
+
// (testbench-contracts), re-exported here so it is declared once. This is a
|
|
116
|
+
// type-only re-export: it is erased at compile time, so no zod runtime import
|
|
117
|
+
// is pulled into these pure, Vite-safe domain modules. caseCanon (the per-case
|
|
118
|
+
// canonical body snapshot reconcile compares against the live plan to classify
|
|
119
|
+
// changed vs unchanged) is one of the contract fields; a result with no stored
|
|
120
|
+
// snapshot is conservatively classified changed.
|
|
121
|
+
import type { CaseResult } from "./testbench-contracts";
|
|
122
|
+
export type { CaseResult };
|
|
123
|
+
|
|
124
|
+
// One bench's recorded results, keyed by case id.
|
|
125
|
+
export interface BenchResults {
|
|
126
|
+
caseResults: Record<string, CaseResult>;
|
|
127
|
+
updatedAt: string;
|
|
128
|
+
}
|