enhanced-schema.test.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. /**
  2. * Enhanced Schema Validation Tests
  3. *
  4. * Tests validation of enhanced task.json and subtask_NN.json schema fields:
  5. * - Line-number precision for context/reference files
  6. * - Domain modeling fields (bounded_context, module, vertical_slice)
  7. * - Contract tracking
  8. * - Design artifacts
  9. * - ADR references
  10. * - Prioritization scores (RICE, WSJF)
  11. * - Release planning
  12. * - Backward compatibility
  13. */
  14. import { describe, it, expect } from "vitest";
  15. // Type definitions from enhanced schema
  16. interface ContextFileReference {
  17. path: string;
  18. lines?: string;
  19. reason?: string;
  20. }
  21. interface Contract {
  22. type: 'api' | 'interface' | 'event' | 'schema';
  23. name: string;
  24. path?: string;
  25. status: 'draft' | 'defined' | 'implemented' | 'verified';
  26. description?: string;
  27. }
  28. interface DesignComponent {
  29. type: 'figma' | 'wireframe' | 'mockup' | 'prototype' | 'sketch';
  30. url?: string;
  31. path?: string;
  32. description?: string;
  33. }
  34. interface ADRReference {
  35. id: string;
  36. path?: string;
  37. title?: string;
  38. decision?: string;
  39. }
  40. interface RICEScore {
  41. reach: number;
  42. impact: number;
  43. confidence: number;
  44. effort: number;
  45. score?: number;
  46. }
  47. interface WSJFScore {
  48. business_value: number;
  49. time_criticality: number;
  50. risk_reduction: number;
  51. job_size: number;
  52. score?: number;
  53. }
  54. interface EnhancedTask {
  55. id: string;
  56. name: string;
  57. status: 'active' | 'completed' | 'blocked' | 'archived';
  58. objective: string;
  59. context_files?: (string | ContextFileReference)[];
  60. reference_files?: (string | ContextFileReference)[];
  61. exit_criteria?: string[];
  62. subtask_count?: number;
  63. completed_count?: number;
  64. created_at: string;
  65. completed_at?: string;
  66. // Enhanced fields
  67. bounded_context?: string;
  68. module?: string;
  69. vertical_slice?: string;
  70. contracts?: Contract[];
  71. design_components?: DesignComponent[];
  72. related_adrs?: ADRReference[];
  73. rice_score?: RICEScore;
  74. wsjf_score?: WSJFScore;
  75. release_slice?: string;
  76. }
  77. interface EnhancedSubtask {
  78. id: string;
  79. seq: string;
  80. title: string;
  81. status: 'pending' | 'in_progress' | 'completed' | 'blocked';
  82. depends_on?: string[];
  83. parallel?: boolean;
  84. context_files?: (string | ContextFileReference)[];
  85. reference_files?: (string | ContextFileReference)[];
  86. suggested_agent?: string;
  87. acceptance_criteria?: string[];
  88. deliverables?: string[];
  89. agent_id?: string;
  90. started_at?: string;
  91. completed_at?: string;
  92. completion_summary?: string;
  93. // Enhanced fields
  94. bounded_context?: string;
  95. module?: string;
  96. vertical_slice?: string;
  97. contracts?: Contract[];
  98. design_components?: DesignComponent[];
  99. related_adrs?: ADRReference[];
  100. }
  101. // Validation functions
  102. function validateContextFileReference(ref: string | ContextFileReference): { valid: boolean; errors: string[] } {
  103. const errors: string[] = [];
  104. if (typeof ref === 'string') {
  105. // Legacy format - always valid
  106. return { valid: true, errors: [] };
  107. }
  108. // New format validation
  109. if (!ref.path || typeof ref.path !== 'string') {
  110. errors.push('ContextFileReference must have a valid path');
  111. }
  112. if (ref.lines !== undefined) {
  113. // Validate line range format: "10-50" or "1-20,45-60"
  114. const lineRangePattern = /^\d+-\d+(,\d+-\d+)*$/;
  115. if (!lineRangePattern.test(ref.lines)) {
  116. errors.push(`Invalid line range format: "${ref.lines}". Expected format: "10-50" or "1-20,45-60"`);
  117. }
  118. }
  119. if (ref.reason !== undefined && typeof ref.reason !== 'string') {
  120. errors.push('ContextFileReference reason must be a string');
  121. }
  122. if (ref.reason && ref.reason.length > 200) {
  123. errors.push('ContextFileReference reason must be max 200 characters');
  124. }
  125. return { valid: errors.length === 0, errors };
  126. }
  127. function validateContract(contract: Contract): { valid: boolean; errors: string[] } {
  128. const errors: string[] = [];
  129. const validTypes = ['api', 'interface', 'event', 'schema'];
  130. if (!validTypes.includes(contract.type)) {
  131. errors.push(`Invalid contract type: "${contract.type}". Must be one of: ${validTypes.join(', ')}`);
  132. }
  133. if (!contract.name || typeof contract.name !== 'string') {
  134. errors.push('Contract must have a valid name');
  135. }
  136. const validStatuses = ['draft', 'defined', 'implemented', 'verified'];
  137. if (!validStatuses.includes(contract.status)) {
  138. errors.push(`Invalid contract status: "${contract.status}". Must be one of: ${validStatuses.join(', ')}`);
  139. }
  140. if (contract.description && contract.description.length > 200) {
  141. errors.push('Contract description must be max 200 characters');
  142. }
  143. return { valid: errors.length === 0, errors };
  144. }
  145. function validateDesignComponent(component: DesignComponent): { valid: boolean; errors: string[] } {
  146. const errors: string[] = [];
  147. const validTypes = ['figma', 'wireframe', 'mockup', 'prototype', 'sketch'];
  148. if (!validTypes.includes(component.type)) {
  149. errors.push(`Invalid design component type: "${component.type}". Must be one of: ${validTypes.join(', ')}`);
  150. }
  151. if (!component.url && !component.path) {
  152. errors.push('DesignComponent must have either url or path');
  153. }
  154. if (component.description && component.description.length > 200) {
  155. errors.push('DesignComponent description must be max 200 characters');
  156. }
  157. return { valid: errors.length === 0, errors };
  158. }
  159. function validateADRReference(adr: ADRReference): { valid: boolean; errors: string[] } {
  160. const errors: string[] = [];
  161. if (!adr.id || typeof adr.id !== 'string') {
  162. errors.push('ADRReference must have a valid id');
  163. }
  164. if (adr.decision && adr.decision.length > 200) {
  165. errors.push('ADRReference decision must be max 200 characters');
  166. }
  167. return { valid: errors.length === 0, errors };
  168. }
  169. function validateRICEScore(rice: RICEScore): { valid: boolean; errors: string[] } {
  170. const errors: string[] = [];
  171. if (rice.reach <= 0) {
  172. errors.push('RICE reach must be > 0');
  173. }
  174. if (rice.impact < 0.25 || rice.impact > 3) {
  175. errors.push('RICE impact must be between 0.25 and 3');
  176. }
  177. if (rice.confidence < 0 || rice.confidence > 100) {
  178. errors.push('RICE confidence must be between 0 and 100');
  179. }
  180. if (rice.effort <= 0) {
  181. errors.push('RICE effort must be > 0');
  182. }
  183. // Validate calculated score if provided
  184. if (rice.score !== undefined) {
  185. const expectedScore = (rice.reach * rice.impact * (rice.confidence / 100)) / rice.effort;
  186. const tolerance = 0.01; // Allow small floating point differences
  187. if (Math.abs(rice.score - expectedScore) > tolerance) {
  188. errors.push(`RICE score mismatch: expected ${expectedScore.toFixed(2)}, got ${rice.score}`);
  189. }
  190. }
  191. return { valid: errors.length === 0, errors };
  192. }
  193. function validateWSJFScore(wsjf: WSJFScore): { valid: boolean; errors: string[] } {
  194. const errors: string[] = [];
  195. if (wsjf.business_value < 1 || wsjf.business_value > 10) {
  196. errors.push('WSJF business_value must be between 1 and 10');
  197. }
  198. if (wsjf.time_criticality < 1 || wsjf.time_criticality > 10) {
  199. errors.push('WSJF time_criticality must be between 1 and 10');
  200. }
  201. if (wsjf.risk_reduction < 1 || wsjf.risk_reduction > 10) {
  202. errors.push('WSJF risk_reduction must be between 1 and 10');
  203. }
  204. if (wsjf.job_size < 1 || wsjf.job_size > 10) {
  205. errors.push('WSJF job_size must be between 1 and 10');
  206. }
  207. // Validate calculated score if provided
  208. if (wsjf.score !== undefined) {
  209. const expectedScore = (wsjf.business_value + wsjf.time_criticality + wsjf.risk_reduction) / wsjf.job_size;
  210. const tolerance = 0.01; // Allow small floating point differences
  211. if (Math.abs(wsjf.score - expectedScore) > tolerance) {
  212. errors.push(`WSJF score mismatch: expected ${expectedScore.toFixed(2)}, got ${wsjf.score}`);
  213. }
  214. }
  215. return { valid: errors.length === 0, errors };
  216. }
  217. function fileExists(filePath: string): boolean {
  218. try {
  219. // Use dynamic import to avoid TypeScript errors
  220. const fs = eval('require')('fs');
  221. return fs.existsSync(filePath);
  222. } catch {
  223. return false;
  224. }
  225. }
  226. describe("Enhanced Schema Validation", () => {
  227. // ==========================================================================
  228. // Line-Number Precision Format Tests
  229. // ==========================================================================
  230. describe("ContextFileReference validation", () => {
  231. it("validates legacy string format", () => {
  232. const result = validateContextFileReference(".opencode/context/core/standards/code-quality.md");
  233. expect(result.valid).toBe(true);
  234. expect(result.errors).toHaveLength(0);
  235. });
  236. it("validates new format with path only", () => {
  237. const ref: ContextFileReference = {
  238. path: ".opencode/context/core/standards/code-quality.md"
  239. };
  240. const result = validateContextFileReference(ref);
  241. expect(result.valid).toBe(true);
  242. expect(result.errors).toHaveLength(0);
  243. });
  244. it("validates new format with single line range", () => {
  245. const ref: ContextFileReference = {
  246. path: ".opencode/context/core/standards/code-quality.md",
  247. lines: "10-50",
  248. reason: "Pure function patterns"
  249. };
  250. const result = validateContextFileReference(ref);
  251. expect(result.valid).toBe(true);
  252. expect(result.errors).toHaveLength(0);
  253. });
  254. it("validates new format with multiple line ranges", () => {
  255. const ref: ContextFileReference = {
  256. path: ".opencode/context/core/standards/security-patterns.md",
  257. lines: "1-25,120-145,200-220",
  258. reason: "JWT validation and token refresh patterns"
  259. };
  260. const result = validateContextFileReference(ref);
  261. expect(result.valid).toBe(true);
  262. expect(result.errors).toHaveLength(0);
  263. });
  264. it("rejects invalid line range format", () => {
  265. const ref: ContextFileReference = {
  266. path: ".opencode/context/core/standards/code-quality.md",
  267. lines: "invalid-range"
  268. };
  269. const result = validateContextFileReference(ref);
  270. expect(result.valid).toBe(false);
  271. expect(result.errors[0]).toContain("Invalid line range format");
  272. });
  273. it("rejects reason longer than 200 characters", () => {
  274. const ref: ContextFileReference = {
  275. path: ".opencode/context/core/standards/code-quality.md",
  276. lines: "10-50",
  277. reason: "a".repeat(201)
  278. };
  279. const result = validateContextFileReference(ref);
  280. expect(result.valid).toBe(false);
  281. expect(result.errors[0]).toContain("max 200 characters");
  282. });
  283. it("rejects missing path", () => {
  284. const ref = {
  285. lines: "10-50"
  286. } as ContextFileReference;
  287. const result = validateContextFileReference(ref);
  288. expect(result.valid).toBe(false);
  289. expect(result.errors[0]).toContain("must have a valid path");
  290. });
  291. });
  292. // ==========================================================================
  293. // Contract Validation Tests
  294. // ==========================================================================
  295. describe("Contract validation", () => {
  296. it("validates valid API contract", () => {
  297. const contract: Contract = {
  298. type: 'api',
  299. name: 'UserAPI',
  300. path: 'src/api/user.contract.ts',
  301. status: 'defined',
  302. description: 'REST API for user CRUD operations'
  303. };
  304. const result = validateContract(contract);
  305. expect(result.valid).toBe(true);
  306. expect(result.errors).toHaveLength(0);
  307. });
  308. it("validates valid interface contract", () => {
  309. const contract: Contract = {
  310. type: 'interface',
  311. name: 'JWTService',
  312. status: 'implemented'
  313. };
  314. const result = validateContract(contract);
  315. expect(result.valid).toBe(true);
  316. expect(result.errors).toHaveLength(0);
  317. });
  318. it("validates valid event contract", () => {
  319. const contract: Contract = {
  320. type: 'event',
  321. name: 'UserCreatedEvent',
  322. status: 'draft',
  323. description: 'Event emitted when new user is created'
  324. };
  325. const result = validateContract(contract);
  326. expect(result.valid).toBe(true);
  327. expect(result.errors).toHaveLength(0);
  328. });
  329. it("validates valid schema contract", () => {
  330. const contract: Contract = {
  331. type: 'schema',
  332. name: 'UserSchema',
  333. path: 'src/schemas/user.schema.ts',
  334. status: 'verified'
  335. };
  336. const result = validateContract(contract);
  337. expect(result.valid).toBe(true);
  338. expect(result.errors).toHaveLength(0);
  339. });
  340. it("rejects invalid contract type", () => {
  341. const contract = {
  342. type: 'invalid',
  343. name: 'TestContract',
  344. status: 'draft'
  345. } as unknown as Contract;
  346. const result = validateContract(contract);
  347. expect(result.valid).toBe(false);
  348. expect(result.errors[0]).toContain("Invalid contract type");
  349. });
  350. it("rejects invalid contract status", () => {
  351. const contract = {
  352. type: 'api',
  353. name: 'TestAPI',
  354. status: 'invalid'
  355. } as unknown as Contract;
  356. const result = validateContract(contract);
  357. expect(result.valid).toBe(false);
  358. expect(result.errors[0]).toContain("Invalid contract status");
  359. });
  360. it("rejects description longer than 200 characters", () => {
  361. const contract: Contract = {
  362. type: 'api',
  363. name: 'TestAPI',
  364. status: 'draft',
  365. description: "a".repeat(201)
  366. };
  367. const result = validateContract(contract);
  368. expect(result.valid).toBe(false);
  369. expect(result.errors[0]).toContain("max 200 characters");
  370. });
  371. it("rejects missing name", () => {
  372. const contract = {
  373. type: 'api',
  374. status: 'draft'
  375. } as Contract;
  376. const result = validateContract(contract);
  377. expect(result.valid).toBe(false);
  378. expect(result.errors[0]).toContain("must have a valid name");
  379. });
  380. });
  381. // ==========================================================================
  382. // Design Component Validation Tests
  383. // ==========================================================================
  384. describe("DesignComponent validation", () => {
  385. it("validates Figma component with URL", () => {
  386. const component: DesignComponent = {
  387. type: 'figma',
  388. url: 'https://figma.com/file/abc123/Login-Flow',
  389. description: 'Login page mockups'
  390. };
  391. const result = validateDesignComponent(component);
  392. expect(result.valid).toBe(true);
  393. expect(result.errors).toHaveLength(0);
  394. });
  395. it("validates wireframe component with local path", () => {
  396. const component: DesignComponent = {
  397. type: 'wireframe',
  398. path: 'docs/design/checkout-wireframe.png',
  399. description: 'Checkout flow wireframe'
  400. };
  401. const result = validateDesignComponent(component);
  402. expect(result.valid).toBe(true);
  403. expect(result.errors).toHaveLength(0);
  404. });
  405. it("validates all design component types", () => {
  406. const types: Array<DesignComponent['type']> = ['figma', 'wireframe', 'mockup', 'prototype', 'sketch'];
  407. types.forEach(type => {
  408. const component: DesignComponent = {
  409. type,
  410. url: 'https://example.com/design'
  411. };
  412. const result = validateDesignComponent(component);
  413. expect(result.valid).toBe(true);
  414. });
  415. });
  416. it("rejects invalid design component type", () => {
  417. const component = {
  418. type: 'invalid',
  419. url: 'https://example.com'
  420. } as unknown as DesignComponent;
  421. const result = validateDesignComponent(component);
  422. expect(result.valid).toBe(false);
  423. expect(result.errors[0]).toContain("Invalid design component type");
  424. });
  425. it("rejects component without url or path", () => {
  426. const component = {
  427. type: 'figma',
  428. description: 'Test design'
  429. } as DesignComponent;
  430. const result = validateDesignComponent(component);
  431. expect(result.valid).toBe(false);
  432. expect(result.errors[0]).toContain("must have either url or path");
  433. });
  434. it("rejects description longer than 200 characters", () => {
  435. const component: DesignComponent = {
  436. type: 'figma',
  437. url: 'https://example.com',
  438. description: "a".repeat(201)
  439. };
  440. const result = validateDesignComponent(component);
  441. expect(result.valid).toBe(false);
  442. expect(result.errors[0]).toContain("max 200 characters");
  443. });
  444. });
  445. // ==========================================================================
  446. // ADR Reference Validation Tests
  447. // ==========================================================================
  448. describe("ADRReference validation", () => {
  449. it("validates ADR with all fields", () => {
  450. const adr: ADRReference = {
  451. id: 'ADR-003',
  452. path: 'docs/adr/003-jwt-authentication.md',
  453. title: 'Use JWT for stateless authentication',
  454. decision: 'JWT with RS256, 15-min access tokens'
  455. };
  456. const result = validateADRReference(adr);
  457. expect(result.valid).toBe(true);
  458. expect(result.errors).toHaveLength(0);
  459. });
  460. it("validates ADR with minimal fields", () => {
  461. const adr: ADRReference = {
  462. id: 'ADR-007',
  463. path: 'docs/adr/007-database-choice.md'
  464. };
  465. const result = validateADRReference(adr);
  466. expect(result.valid).toBe(true);
  467. expect(result.errors).toHaveLength(0);
  468. });
  469. it("rejects ADR without id", () => {
  470. const adr = {
  471. path: 'docs/adr/001-test.md'
  472. } as ADRReference;
  473. const result = validateADRReference(adr);
  474. expect(result.valid).toBe(false);
  475. expect(result.errors[0]).toContain("must have a valid id");
  476. });
  477. it("rejects decision longer than 200 characters", () => {
  478. const adr: ADRReference = {
  479. id: 'ADR-001',
  480. decision: "a".repeat(201)
  481. };
  482. const result = validateADRReference(adr);
  483. expect(result.valid).toBe(false);
  484. expect(result.errors[0]).toContain("max 200 characters");
  485. });
  486. });
  487. // ==========================================================================
  488. // RICE Score Validation Tests
  489. // ==========================================================================
  490. describe("RICEScore validation", () => {
  491. it("validates valid RICE score", () => {
  492. const rice: RICEScore = {
  493. reach: 5000,
  494. impact: 2,
  495. confidence: 80,
  496. effort: 3
  497. };
  498. const result = validateRICEScore(rice);
  499. expect(result.valid).toBe(true);
  500. expect(result.errors).toHaveLength(0);
  501. });
  502. it("validates RICE score with calculated score", () => {
  503. const rice: RICEScore = {
  504. reach: 5000,
  505. impact: 2,
  506. confidence: 80,
  507. effort: 3,
  508. score: 2666.67
  509. };
  510. const result = validateRICEScore(rice);
  511. expect(result.valid).toBe(true);
  512. expect(result.errors).toHaveLength(0);
  513. });
  514. it("validates all valid impact values", () => {
  515. const validImpacts = [0.25, 0.5, 1, 2, 3];
  516. validImpacts.forEach(impact => {
  517. const rice: RICEScore = {
  518. reach: 1000,
  519. impact,
  520. confidence: 50,
  521. effort: 1
  522. };
  523. const result = validateRICEScore(rice);
  524. expect(result.valid).toBe(true);
  525. });
  526. });
  527. it("rejects reach <= 0", () => {
  528. const rice: RICEScore = {
  529. reach: 0,
  530. impact: 2,
  531. confidence: 80,
  532. effort: 3
  533. };
  534. const result = validateRICEScore(rice);
  535. expect(result.valid).toBe(false);
  536. expect(result.errors[0]).toContain("reach must be > 0");
  537. });
  538. it("rejects impact < 0.25", () => {
  539. const rice: RICEScore = {
  540. reach: 1000,
  541. impact: 0.1,
  542. confidence: 80,
  543. effort: 3
  544. };
  545. const result = validateRICEScore(rice);
  546. expect(result.valid).toBe(false);
  547. expect(result.errors[0]).toContain("impact must be between 0.25 and 3");
  548. });
  549. it("rejects impact > 3", () => {
  550. const rice: RICEScore = {
  551. reach: 1000,
  552. impact: 5,
  553. confidence: 80,
  554. effort: 3
  555. };
  556. const result = validateRICEScore(rice);
  557. expect(result.valid).toBe(false);
  558. expect(result.errors[0]).toContain("impact must be between 0.25 and 3");
  559. });
  560. it("rejects confidence < 0", () => {
  561. const rice: RICEScore = {
  562. reach: 1000,
  563. impact: 2,
  564. confidence: -10,
  565. effort: 3
  566. };
  567. const result = validateRICEScore(rice);
  568. expect(result.valid).toBe(false);
  569. expect(result.errors[0]).toContain("confidence must be between 0 and 100");
  570. });
  571. it("rejects confidence > 100", () => {
  572. const rice: RICEScore = {
  573. reach: 1000,
  574. impact: 2,
  575. confidence: 150,
  576. effort: 3
  577. };
  578. const result = validateRICEScore(rice);
  579. expect(result.valid).toBe(false);
  580. expect(result.errors[0]).toContain("confidence must be between 0 and 100");
  581. });
  582. it("rejects effort <= 0", () => {
  583. const rice: RICEScore = {
  584. reach: 1000,
  585. impact: 2,
  586. confidence: 80,
  587. effort: 0
  588. };
  589. const result = validateRICEScore(rice);
  590. expect(result.valid).toBe(false);
  591. expect(result.errors[0]).toContain("effort must be > 0");
  592. });
  593. it("rejects incorrect calculated score", () => {
  594. const rice: RICEScore = {
  595. reach: 5000,
  596. impact: 2,
  597. confidence: 80,
  598. effort: 3,
  599. score: 9999 // Wrong calculation
  600. };
  601. const result = validateRICEScore(rice);
  602. expect(result.valid).toBe(false);
  603. expect(result.errors[0]).toContain("RICE score mismatch");
  604. });
  605. });
  606. // ==========================================================================
  607. // WSJF Score Validation Tests
  608. // ==========================================================================
  609. describe("WSJFScore validation", () => {
  610. it("validates valid WSJF score", () => {
  611. const wsjf: WSJFScore = {
  612. business_value: 8,
  613. time_criticality: 6,
  614. risk_reduction: 5,
  615. job_size: 3
  616. };
  617. const result = validateWSJFScore(wsjf);
  618. expect(result.valid).toBe(true);
  619. expect(result.errors).toHaveLength(0);
  620. });
  621. it("validates WSJF score with calculated score", () => {
  622. const wsjf: WSJFScore = {
  623. business_value: 8,
  624. time_criticality: 6,
  625. risk_reduction: 5,
  626. job_size: 3,
  627. score: 6.33
  628. };
  629. const result = validateWSJFScore(wsjf);
  630. expect(result.valid).toBe(true);
  631. expect(result.errors).toHaveLength(0);
  632. });
  633. it("validates all fields at minimum value (1)", () => {
  634. const wsjf: WSJFScore = {
  635. business_value: 1,
  636. time_criticality: 1,
  637. risk_reduction: 1,
  638. job_size: 1
  639. };
  640. const result = validateWSJFScore(wsjf);
  641. expect(result.valid).toBe(true);
  642. expect(result.errors).toHaveLength(0);
  643. });
  644. it("validates all fields at maximum value (10)", () => {
  645. const wsjf: WSJFScore = {
  646. business_value: 10,
  647. time_criticality: 10,
  648. risk_reduction: 10,
  649. job_size: 10
  650. };
  651. const result = validateWSJFScore(wsjf);
  652. expect(result.valid).toBe(true);
  653. expect(result.errors).toHaveLength(0);
  654. });
  655. it("rejects business_value < 1", () => {
  656. const wsjf: WSJFScore = {
  657. business_value: 0,
  658. time_criticality: 5,
  659. risk_reduction: 5,
  660. job_size: 3
  661. };
  662. const result = validateWSJFScore(wsjf);
  663. expect(result.valid).toBe(false);
  664. expect(result.errors[0]).toContain("business_value must be between 1 and 10");
  665. });
  666. it("rejects business_value > 10", () => {
  667. const wsjf: WSJFScore = {
  668. business_value: 15,
  669. time_criticality: 5,
  670. risk_reduction: 5,
  671. job_size: 3
  672. };
  673. const result = validateWSJFScore(wsjf);
  674. expect(result.valid).toBe(false);
  675. expect(result.errors[0]).toContain("business_value must be between 1 and 10");
  676. });
  677. it("rejects time_criticality out of range", () => {
  678. const wsjf: WSJFScore = {
  679. business_value: 5,
  680. time_criticality: 0,
  681. risk_reduction: 5,
  682. job_size: 3
  683. };
  684. const result = validateWSJFScore(wsjf);
  685. expect(result.valid).toBe(false);
  686. expect(result.errors[0]).toContain("time_criticality must be between 1 and 10");
  687. });
  688. it("rejects risk_reduction out of range", () => {
  689. const wsjf: WSJFScore = {
  690. business_value: 5,
  691. time_criticality: 5,
  692. risk_reduction: 11,
  693. job_size: 3
  694. };
  695. const result = validateWSJFScore(wsjf);
  696. expect(result.valid).toBe(false);
  697. expect(result.errors[0]).toContain("risk_reduction must be between 1 and 10");
  698. });
  699. it("rejects job_size out of range", () => {
  700. const wsjf: WSJFScore = {
  701. business_value: 5,
  702. time_criticality: 5,
  703. risk_reduction: 5,
  704. job_size: 0
  705. };
  706. const result = validateWSJFScore(wsjf);
  707. expect(result.valid).toBe(false);
  708. expect(result.errors[0]).toContain("job_size must be between 1 and 10");
  709. });
  710. it("rejects incorrect calculated score", () => {
  711. const wsjf: WSJFScore = {
  712. business_value: 8,
  713. time_criticality: 6,
  714. risk_reduction: 5,
  715. job_size: 3,
  716. score: 9999 // Wrong calculation
  717. };
  718. const result = validateWSJFScore(wsjf);
  719. expect(result.valid).toBe(false);
  720. expect(result.errors[0]).toContain("WSJF score mismatch");
  721. });
  722. });
  723. // ==========================================================================
  724. // Domain Modeling Fields Tests
  725. // ==========================================================================
  726. describe("Domain modeling fields", () => {
  727. it("validates bounded_context field", () => {
  728. const task: Partial<EnhancedTask> = {
  729. id: 'test-task',
  730. name: 'Test Task',
  731. status: 'active',
  732. objective: 'Test objective',
  733. created_at: '2026-02-14T00:00:00Z',
  734. bounded_context: 'authentication'
  735. };
  736. expect(task.bounded_context).toBe('authentication');
  737. expect(typeof task.bounded_context).toBe('string');
  738. });
  739. it("validates module field", () => {
  740. const task: Partial<EnhancedTask> = {
  741. id: 'test-task',
  742. name: 'Test Task',
  743. status: 'active',
  744. objective: 'Test objective',
  745. created_at: '2026-02-14T00:00:00Z',
  746. module: '@app/auth'
  747. };
  748. expect(task.module).toBe('@app/auth');
  749. expect(typeof task.module).toBe('string');
  750. });
  751. it("validates vertical_slice field", () => {
  752. const task: Partial<EnhancedTask> = {
  753. id: 'test-task',
  754. name: 'Test Task',
  755. status: 'active',
  756. objective: 'Test objective',
  757. created_at: '2026-02-14T00:00:00Z',
  758. vertical_slice: 'user-registration'
  759. };
  760. expect(task.vertical_slice).toBe('user-registration');
  761. expect(typeof task.vertical_slice).toBe('string');
  762. });
  763. it("validates release_slice field", () => {
  764. const task: Partial<EnhancedTask> = {
  765. id: 'test-task',
  766. name: 'Test Task',
  767. status: 'active',
  768. objective: 'Test objective',
  769. created_at: '2026-02-14T00:00:00Z',
  770. release_slice: 'v1.0.0'
  771. };
  772. expect(task.release_slice).toBe('v1.0.0');
  773. expect(typeof task.release_slice).toBe('string');
  774. });
  775. });
  776. // ==========================================================================
  777. // Backward Compatibility Tests
  778. // ==========================================================================
  779. describe("Backward compatibility", () => {
  780. it("accepts task without any enhanced fields", () => {
  781. const task: EnhancedTask = {
  782. id: 'legacy-task',
  783. name: 'Legacy Task',
  784. status: 'active',
  785. objective: 'Test legacy task',
  786. created_at: '2026-02-14T00:00:00Z'
  787. };
  788. expect(task.id).toBe('legacy-task');
  789. expect(task.bounded_context).toBeUndefined();
  790. expect(task.contracts).toBeUndefined();
  791. expect(task.rice_score).toBeUndefined();
  792. });
  793. it("accepts subtask without any enhanced fields", () => {
  794. const subtask: EnhancedSubtask = {
  795. id: 'legacy-subtask-01',
  796. seq: '01',
  797. title: 'Legacy Subtask',
  798. status: 'pending'
  799. };
  800. expect(subtask.id).toBe('legacy-subtask-01');
  801. expect(subtask.bounded_context).toBeUndefined();
  802. expect(subtask.contracts).toBeUndefined();
  803. });
  804. it("accepts mixed legacy and new context file formats", () => {
  805. const task: Partial<EnhancedTask> = {
  806. id: 'mixed-task',
  807. name: 'Mixed Format Task',
  808. status: 'active',
  809. objective: 'Test mixed formats',
  810. created_at: '2026-02-14T00:00:00Z',
  811. context_files: [
  812. '.opencode/context/core/standards/code-quality.md',
  813. {
  814. path: '.opencode/context/core/standards/security-patterns.md',
  815. lines: '120-145',
  816. reason: 'JWT validation'
  817. }
  818. ]
  819. };
  820. expect(task.context_files).toHaveLength(2);
  821. expect(typeof task.context_files![0]).toBe('string');
  822. expect(typeof task.context_files![1]).toBe('object');
  823. // Validate both formats
  824. const result1 = validateContextFileReference(task.context_files![0]);
  825. const result2 = validateContextFileReference(task.context_files![1]);
  826. expect(result1.valid).toBe(true);
  827. expect(result2.valid).toBe(true);
  828. });
  829. it("accepts task with only some enhanced fields", () => {
  830. const task: Partial<EnhancedTask> = {
  831. id: 'partial-task',
  832. name: 'Partial Enhanced Task',
  833. status: 'active',
  834. objective: 'Test partial enhancement',
  835. created_at: '2026-02-14T00:00:00Z',
  836. bounded_context: 'authentication',
  837. // No contracts, no scores, etc.
  838. };
  839. expect(task.bounded_context).toBe('authentication');
  840. expect(task.contracts).toBeUndefined();
  841. expect(task.rice_score).toBeUndefined();
  842. expect(task.wsjf_score).toBeUndefined();
  843. });
  844. });
  845. // ==========================================================================
  846. // File Reference Validation Tests
  847. // ==========================================================================
  848. describe("File reference validation", () => {
  849. it("validates contract with path field", () => {
  850. const contract: Contract = {
  851. type: 'api',
  852. name: 'TestAPI',
  853. path: 'src/api/test.contract.ts',
  854. status: 'defined'
  855. };
  856. const result = validateContract(contract);
  857. expect(result.valid).toBe(true);
  858. expect(result.errors).toHaveLength(0);
  859. });
  860. it("validates ADR with path field", () => {
  861. const adr: ADRReference = {
  862. id: 'ADR-001',
  863. path: 'docs/adr/001-test.md',
  864. title: 'Test ADR'
  865. };
  866. const result = validateADRReference(adr);
  867. expect(result.valid).toBe(true);
  868. expect(result.errors).toHaveLength(0);
  869. });
  870. it("validates contract without path field", () => {
  871. const contract: Contract = {
  872. type: 'api',
  873. name: 'TestAPI',
  874. status: 'draft'
  875. };
  876. // Contract validation should pass (path is optional)
  877. const result = validateContract(contract);
  878. expect(result.valid).toBe(true);
  879. expect(result.errors).toHaveLength(0);
  880. });
  881. it("validates ADR without path field", () => {
  882. const adr: ADRReference = {
  883. id: 'ADR-001',
  884. title: 'Test ADR'
  885. };
  886. const result = validateADRReference(adr);
  887. expect(result.valid).toBe(true);
  888. expect(result.errors).toHaveLength(0);
  889. });
  890. });
  891. // ==========================================================================
  892. // Integration Tests
  893. // ==========================================================================
  894. describe("Complete enhanced task validation", () => {
  895. it("validates fully enhanced task with all fields", () => {
  896. const task: EnhancedTask = {
  897. id: 'user-authentication',
  898. name: 'User Authentication System',
  899. status: 'active',
  900. objective: 'Implement JWT-based authentication',
  901. context_files: [
  902. {
  903. path: '.opencode/context/core/standards/code-quality.md',
  904. lines: '53-95',
  905. reason: 'Pure function patterns'
  906. },
  907. '.opencode/context/core/standards/security-patterns.md'
  908. ],
  909. reference_files: [
  910. {
  911. path: 'src/middleware/auth.middleware.ts',
  912. lines: '1-50',
  913. reason: 'Existing auth middleware'
  914. }
  915. ],
  916. exit_criteria: [
  917. 'All tests passing',
  918. 'JWT tokens signed with RS256'
  919. ],
  920. subtask_count: 5,
  921. completed_count: 0,
  922. created_at: '2026-02-14T10:00:00Z',
  923. bounded_context: 'authentication',
  924. module: '@app/auth',
  925. vertical_slice: 'user-login',
  926. contracts: [
  927. {
  928. type: 'api',
  929. name: 'AuthAPI',
  930. path: 'src/api/auth.contract.ts',
  931. status: 'defined',
  932. description: 'REST endpoints for auth'
  933. }
  934. ],
  935. design_components: [
  936. {
  937. type: 'figma',
  938. url: 'https://figma.com/file/xyz789/Auth-Flows',
  939. description: 'Login UI mockups'
  940. }
  941. ],
  942. related_adrs: [
  943. {
  944. id: 'ADR-003',
  945. path: 'docs/adr/003-jwt-authentication.md',
  946. title: 'Use JWT for authentication'
  947. }
  948. ],
  949. rice_score: {
  950. reach: 10000,
  951. impact: 3,
  952. confidence: 90,
  953. effort: 4,
  954. score: 6750
  955. },
  956. wsjf_score: {
  957. business_value: 9,
  958. time_criticality: 8,
  959. risk_reduction: 7,
  960. job_size: 4,
  961. score: 6
  962. },
  963. release_slice: 'v1.0.0'
  964. };
  965. // Validate all context files
  966. task.context_files?.forEach(ref => {
  967. const result = validateContextFileReference(ref);
  968. expect(result.valid).toBe(true);
  969. });
  970. // Validate all contracts
  971. task.contracts?.forEach(contract => {
  972. const result = validateContract(contract);
  973. expect(result.valid).toBe(true);
  974. });
  975. // Validate all design components
  976. task.design_components?.forEach(component => {
  977. const result = validateDesignComponent(component);
  978. expect(result.valid).toBe(true);
  979. });
  980. // Validate all ADRs
  981. task.related_adrs?.forEach(adr => {
  982. const result = validateADRReference(adr);
  983. expect(result.valid).toBe(true);
  984. });
  985. // Validate RICE score
  986. if (task.rice_score) {
  987. const result = validateRICEScore(task.rice_score);
  988. expect(result.valid).toBe(true);
  989. }
  990. // Validate WSJF score
  991. if (task.wsjf_score) {
  992. const result = validateWSJFScore(task.wsjf_score);
  993. expect(result.valid).toBe(true);
  994. }
  995. // Validate domain fields
  996. expect(task.bounded_context).toBe('authentication');
  997. expect(task.module).toBe('@app/auth');
  998. expect(task.vertical_slice).toBe('user-login');
  999. expect(task.release_slice).toBe('v1.0.0');
  1000. });
  1001. it("validates fully enhanced subtask with all fields", () => {
  1002. const subtask: EnhancedSubtask = {
  1003. id: 'user-authentication-02',
  1004. seq: '02',
  1005. title: 'Implement JWT service',
  1006. status: 'pending',
  1007. depends_on: ['01'],
  1008. parallel: false,
  1009. context_files: [
  1010. {
  1011. path: '.opencode/context/core/standards/code-quality.md',
  1012. lines: '53-72',
  1013. reason: 'Pure function patterns'
  1014. }
  1015. ],
  1016. suggested_agent: 'CoderAgent',
  1017. acceptance_criteria: [
  1018. 'JWT tokens signed with RS256',
  1019. 'Unit tests cover all operations'
  1020. ],
  1021. deliverables: [
  1022. 'src/auth/jwt.service.ts',
  1023. 'src/auth/jwt.service.test.ts'
  1024. ],
  1025. bounded_context: 'authentication',
  1026. module: '@app/auth',
  1027. contracts: [
  1028. {
  1029. type: 'interface',
  1030. name: 'JWTService',
  1031. path: 'src/auth/jwt.service.ts',
  1032. status: 'implemented'
  1033. }
  1034. ],
  1035. related_adrs: [
  1036. {
  1037. id: 'ADR-003',
  1038. path: 'docs/adr/003-jwt-authentication.md'
  1039. }
  1040. ]
  1041. };
  1042. // Validate all enhanced fields
  1043. subtask.context_files?.forEach(ref => {
  1044. const result = validateContextFileReference(ref);
  1045. expect(result.valid).toBe(true);
  1046. });
  1047. subtask.contracts?.forEach(contract => {
  1048. const result = validateContract(contract);
  1049. expect(result.valid).toBe(true);
  1050. });
  1051. subtask.related_adrs?.forEach(adr => {
  1052. const result = validateADRReference(adr);
  1053. expect(result.valid).toBe(true);
  1054. });
  1055. expect(subtask.bounded_context).toBe('authentication');
  1056. expect(subtask.module).toBe('@app/auth');
  1057. });
  1058. });
  1059. });