1234567891011121314151617181920import { describe, expect, it } from "vitest";21import { predictBatch } from "../../background/ml";22import type { MLFieldFeatures, FieldPrediction, FieldType } from "../../shared/ml";2324252627282930function zeroFeatures(): MLFieldFeatures {31 return {32 type_text: 0, type_email: 0, type_password: 0, type_tel: 0,33 type_number: 0, type_search: 0, type_url: 0, type_other: 0,34 auto_username: 0, auto_email: 0, auto_current_password: 0,35 auto_new_password: 0, auto_one_time_code: 0, auto_off: 0, auto_other: 0,36 name_has_user: 0, name_has_login: 0, name_has_email: 0, name_has_pass: 0,37 name_length: 0,38 id_has_user: 0, id_has_login: 0, id_has_email: 0, id_has_pass: 0,39 id_length: 0,40 placeholder_has_user: 0, placeholder_has_email: 0, placeholder_has_pass: 0,41 placeholder_length: 0,42 aria_label_has_user: 0, aria_label_has_email: 0, aria_label_has_pass: 0,43 aria_label_length: 0,44 parent_is_form: 0, parent_is_div: 0, parent_is_section: 0,45 sibling_count: 0, has_password_sibling: 0, has_email_sibling: 0,46 form_has_submit: 0, form_action_has_login: 0,47 is_required: 0, has_placeholder: 0, has_aria_label: 0,48 inputmode_numeric: 0,49 };50}515253function passwordFeatures(): MLFieldFeatures {54 return {55 ...zeroFeatures(),56 type_password: 1,57 auto_current_password: 1,58 name_has_pass: 1,59 id_has_pass: 1,60 parent_is_form: 1,61 form_has_submit: 1,62 };63}6465const VALID_TYPES: FieldType[] = ["username", "password", "email", "totp", "none"];6667function assertValidPrediction(p: FieldPrediction): void {68 expect(VALID_TYPES).toContain(p.type);69 expect(p.confidence).toBeGreaterThanOrEqual(0);70 expect(p.confidence).toBeLessThanOrEqual(1);71 expect(Number.isFinite(p.confidence)).toBe(true);72 expect(Number.isNaN(p.confidence)).toBe(false);7374 let sumProb = 0;75 for (const t of VALID_TYPES) {76 const prob = p.probabilities[t];77 expect(Number.isFinite(prob)).toBe(true);78 expect(prob).toBeGreaterThanOrEqual(0);79 sumProb += prob;80 }81 82 expect(Math.abs(sumProb - 1.0)).toBeLessThan(1e-9);83}848586878889describe("T-030: predictBatch batch sizes", () => {90 91 it("T-030a: predictBatch([]) returns empty array", () => {92 const result = predictBatch([]);93 expect(Array.isArray(result)).toBe(true);94 expect(result.length).toBe(0);95 });9697 98 it("T-030b: predictBatch([single]) returns exactly 1 prediction", () => {99 const result = predictBatch([zeroFeatures()]);100 expect(result.length).toBe(1);101 assertValidPrediction(result[0]!);102 });103104 105 106 107 it("T-030c: predictBatch(100 vectors) returns 100 predictions", () => {108 const features = Array.from({ length: 100 }, () => zeroFeatures());109 const result = predictBatch(features);110 expect(result.length).toBe(100);111 for (const p of result) {112 assertValidPrediction(p);113 }114 });115116 117 118 119 it("T-030d: password-signal features → confidence for password is highest or meaningful", () => {120 const result = predictBatch([passwordFeatures()]);121 expect(result.length).toBe(1);122 assertValidPrediction(result[0]!);123 124 expect(result[0]!.probabilities.password).toBeGreaterThan(0);125 });126});127128describe("T-030: predictBatch softmax numerical stability", () => {129 130 131 132 133 134 it("T-030e: extreme positive feature values → no NaN or Infinity in output", () => {135 const extremeFeatures: MLFieldFeatures = {} as MLFieldFeatures;136 for (const key of Object.keys(zeroFeatures()) as (keyof MLFieldFeatures)[]) {137 (extremeFeatures as Record<string, number>)[key] = 1000;138 }139140 const result = predictBatch([extremeFeatures]);141 assertValidPrediction(result[0]!);142 });143144 145 146 it("T-030f: extreme negative feature values → no NaN or Infinity", () => {147 const extremeNeg: MLFieldFeatures = {} as MLFieldFeatures;148 for (const key of Object.keys(zeroFeatures()) as (keyof MLFieldFeatures)[]) {149 (extremeNeg as Record<string, number>)[key] = -1000;150 }151152 const result = predictBatch([extremeNeg]);153 assertValidPrediction(result[0]!);154 });155156 157 it("T-030g: mixed extreme positive and negative feature values → stable softmax", () => {158 const mixed: MLFieldFeatures = {} as MLFieldFeatures;159 let i = 0;160 for (const key of Object.keys(zeroFeatures()) as (keyof MLFieldFeatures)[]) {161 (mixed as Record<string, number>)[key] = i % 2 === 0 ? 1e6 : -1e6;162 i++;163 }164165 const result = predictBatch([mixed]);166 assertValidPrediction(result[0]!);167 });168169 170 171 172 it("T-030h: all predictions have a valid FieldType (not undefined)", () => {173 const cases = [174 zeroFeatures(),175 passwordFeatures(),176 { ...zeroFeatures(), type_email: 1, auto_email: 1, name_has_email: 1 },177 { ...zeroFeatures(), auto_one_time_code: 1, inputmode_numeric: 1 },178 ];179180 const results = predictBatch(cases);181 for (const r of results) {182 expect(VALID_TYPES).toContain(r.type);183 }184 });185});