-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtext-classifier.test.js
More file actions
331 lines (287 loc) · 15.1 KB
/
Copy pathtext-classifier.test.js
File metadata and controls
331 lines (287 loc) · 15.1 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
/**
* tests/text-classifier.test.js
*
* Locks the regex pre-filter behavior in place so future expansions don't
* regress. Covers:
* - 12 trades × multiple input forms (Arabic / French / English / Darija
* romanized / Arabizi numeric)
* - 15 cities + key neighborhoods (Casa: maarif/anfa, Marrakech: gueliz, ...)
* - Arabizi conventions: 3=ع, 7=ح, 9=ق, 8=غ, 5=خ, 6=ط
* - Multi-trade renovation phrase detection
* - Disambiguation edge cases (meuble alone vs meuble bois;
* tnaqil meubles → نقل not نجارة)
* - Mixed-script queries (English + French + Arabic in one sentence)
*
* Run: `node --test tests/text-classifier.test.js`
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const {
KEYWORD_TO_CAT,
KEYWORD_TO_CITY,
detectFromText,
detectMultiTrade,
VALID_CATS,
} = require('../lib/text-classifier');
// ─── Helpers ────────────────────────────────────────────────────────────────
function classify(query) {
return {
trade: detectFromText(query, KEYWORD_TO_CAT),
city: detectFromText(query, KEYWORD_TO_CITY),
multi_trade: detectMultiTrade(query),
};
}
// ─── 12 Trades × 4 input forms each ─────────────────────────────────────────
test('بلومبي (plumbing) — Arabic + Fr + En + Darija romanized', () => {
assert.equal(classify('بغيت بلومبي').trade, 'بلومبي');
assert.equal(classify('bghit plombier f tanja').trade, 'بلومبي');
assert.equal(classify('plumber needed').trade, 'بلومبي');
assert.equal(classify('bloumbi mzyan').trade, 'بلومبي');
assert.equal(classify('chauffao kharbat').trade, 'بلومبي');
assert.equal(classify('lavabo kasar').trade, 'بلومبي');
assert.equal(classify('fuite f hammam').trade, 'بلومبي');
assert.equal(classify('canalisation mssddoda').trade, 'بلومبي');
});
test('طريسيان (electrician) — incl. Arabizi 9=ق, 3=ع', () => {
assert.equal(classify('بغيت طريسيان').trade, 'طريسيان');
assert.equal(classify('électricien urgent').trade, 'طريسيان');
assert.equal(classify('electrician needed').trade, 'طريسيان');
assert.equal(classify('9hraba 6ay7a').trade, 'طريسيان'); // Arabizi 9=ق, 6=ط
assert.equal(classify('3hraba m9to3a').trade, 'طريسيان'); // Arabizi 3=ع
assert.equal(classify('panne courant').trade, 'طريسيان');
assert.equal(classify('short circuit f chi9a').trade, 'طريسيان');
assert.equal(classify('compteur kayt9tar').trade, 'طريسيان');
assert.equal(classify('kahrabaji jdid').trade, 'طريسيان');
});
test('صباغة (painter) — incl. Darija sba8/sbgha', () => {
assert.equal(classify('بغيت صباغ').trade, 'صباغة');
assert.equal(classify('peintre f maarif').trade, 'صباغة');
assert.equal(classify('painter for walls').trade, 'صباغة');
assert.equal(classify('sba8 f casa').trade, 'صباغة');
assert.equal(classify('lsba8a tay7a').trade, 'صباغة');
assert.equal(classify('peinture mat brillant').trade, 'صباغة');
});
test('نجارة (carpenter) — incl. khachab/menuisier', () => {
assert.equal(classify('بغيت نجار').trade, 'نجارة');
assert.equal(classify('menuisier bois').trade, 'نجارة');
assert.equal(classify('carpenter needed').trade, 'نجارة');
assert.equal(classify('najjar f mraks').trade, 'نجارة');
assert.equal(classify('khachab placard').trade, 'نجارة');
assert.equal(classify('placard bois f rbat').trade, 'نجارة');
assert.equal(classify('escalier bois').trade, 'نجارة');
});
test('بناء (mason / construction)', () => {
assert.equal(classify('بغيت بناء').trade, 'بناء');
assert.equal(classify('maçon urgent').trade, 'بناء');
assert.equal(classify('builder for new house').trade, 'بناء');
assert.equal(classify('bnay f lcasa').trade, 'بناء');
assert.equal(classify('lbnyya f gueliz').trade, 'بناء');
assert.equal(classify('fissure f mur').trade, 'بناء');
assert.equal(classify('briques mortier').trade, 'بناء');
});
test('نقاوة (cleaning) — incl. ndafa/n9aoua', () => {
assert.equal(classify('بغيت نقاوة').trade, 'نقاوة');
assert.equal(classify('femme de menage f anfa').trade, 'نقاوة');
assert.equal(classify('cleaner needed').trade, 'نقاوة');
assert.equal(classify('ndafa f sla').trade, 'نقاوة');
assert.equal(classify('n9aoua mzyana').trade, 'نقاوة');
assert.equal(classify('vitres a nettoyer').trade, 'نقاوة');
});
test('حدادة (ironwork) — Arabizi 7=ح variants', () => {
assert.equal(classify('بغيت حداد').trade, 'حدادة');
assert.equal(classify('ferronnier urgent').trade, 'حدادة');
assert.equal(classify('ironworker needed').trade, 'حدادة');
assert.equal(classify('hddad mzyan').trade, 'حدادة');
assert.equal(classify('7eddad f marrakech').trade, 'حدادة'); // Arabizi 7=ح
assert.equal(classify('l7eddad f gueliz').trade, 'حدادة'); // Arabizi 7=ح with definite article
assert.equal(classify('portail aluminium').trade, 'حدادة');
assert.equal(classify('moustiquaire f shabek').trade, 'حدادة');
});
test('ديكور (decor / interior design)', () => {
assert.equal(classify('بغيت ديكور').trade, 'ديكور');
assert.equal(classify('décorateur intérieur').trade, 'ديكور');
assert.equal(classify('interior designer').trade, 'ديكور');
assert.equal(classify('dekor f hay riyad').trade, 'ديكور');
assert.equal(classify('faux plafond jbes').trade, 'ديكور');
assert.equal(classify('placo platre').trade, 'ديكور');
});
test('نقل (moving / transport) — disambiguates from نجارة', () => {
assert.equal(classify('بغيت نقل عفش').trade, 'نقل');
assert.equal(classify('déménagement f casa').trade, 'نقل');
assert.equal(classify('mover f tanja').trade, 'نقل');
assert.equal(classify('tnaqil meubles').trade, 'نقل'); // disambiguation fix
assert.equal(classify('tnaqil 3afsh').trade, 'نقل');
assert.equal(classify('camion déménagement').trade, 'نقل');
});
test('كلامبيستري (tiling)', () => {
assert.equal(classify('بغيت كلامبيستري').trade, 'كلامبيستري');
assert.equal(classify('carreleur f mraks').trade, 'كلامبيستري');
assert.equal(classify('tile installer').trade, 'كلامبيستري');
assert.equal(classify('carro f tnja').trade, 'كلامبيستري');
assert.equal(classify('zlij marocain').trade, 'كلامبيستري');
assert.equal(classify('joint coulis bali').trade, 'كلامبيستري');
assert.equal(classify('mosaique fayance').trade, 'كلامبيستري');
});
test('خياطة (tailoring) — incl. Arabizi 5=خ', () => {
assert.equal(classify('بغيت خياط').trade, 'خياطة');
assert.equal(classify('couturier f maarif').trade, 'خياطة');
assert.equal(classify('tailor f ojda').trade, 'خياطة');
assert.equal(classify('khayyat lblanto').trade, 'خياطة');
assert.equal(classify('5ayyat f bouskoura').trade, 'خياطة'); // Arabizi 5=خ
assert.equal(classify('broderie marocaine').trade, 'خياطة');
});
test('حراسة (security / guard)', () => {
assert.equal(classify('بغيت حارس').trade, 'حراسة');
assert.equal(classify('gardien f mohammedia').trade, 'حراسة');
assert.equal(classify('watchman needed').trade, 'حراسة');
assert.equal(classify('haris kher').trade, 'حراسة');
assert.equal(classify('7arass f atlas fes').trade, 'حراسة'); // Arabizi 7=ح
assert.equal(classify('cctv camera surveillance').trade, 'حراسة');
assert.equal(classify('vigile night shift').trade, 'حراسة');
});
// ─── 15 Cities × multiple input forms ───────────────────────────────────────
test('الدار البيضاء (Casablanca) — incl. all major neighborhoods', () => {
for (const q of [
'casa', 'casablanca', 'lcasa', 'dar lbida', 'lbeida',
'maarif', 'ma3arif', 'anfa', 'ain chock', 'sidi moumen',
'mohammedia', 'bouskoura', 'hay hassani',
]) {
assert.equal(classify(q).city, 'الدار البيضاء', `query="${q}"`);
}
});
test('الرباط (Rabat) — incl. agdal/hay riyad', () => {
for (const q of ['rabat', 'rbat', 'lrbat', 'agdal', 'hay riyad', 'hassan', 'capital']) {
assert.equal(classify(q).city, 'الرباط', `query="${q}"`);
}
});
test('طنجة (Tangier) — incl. Arabizi 6=ط and neighborhoods', () => {
for (const q of ['tanger', 'tangier', 'tanja', 'tnja', '6anja', '6nja', 'malabata', 'charf']) {
assert.equal(classify(q).city, 'طنجة', `query="${q}"`);
}
});
test('مراكش (Marrakech) — incl. gueliz/menara', () => {
for (const q of ['marrakech', 'marrakesh', 'mraks', 'lmraks', 'gueliz', 'menara', 'red city']) {
assert.equal(classify(q).city, 'مراكش', `query="${q}"`);
}
});
test('أكادير (Agadir) — incl. inezgane/ait melloul', () => {
for (const q of ['agadir', 'lgadir', 'inezgane', 'ait melloul', 'founty', 'talborjt']) {
assert.equal(classify(q).city, 'أكادير', `query="${q}"`);
}
});
test('فاس (Fes)', () => {
for (const q of ['fes', 'fez', 'fas', 'lfas', 'fes el bali', 'fes el jdid', 'atlas fes']) {
assert.equal(classify(q).city, 'فاس', `query="${q}"`);
}
});
test('سلا (Salé)', () => {
for (const q of ['salé', 'sla', 'lsla', 'sallé', 'tabriquet']) {
assert.equal(classify(q).city, 'سلا', `query="${q}"`);
}
});
test('مكناس (Meknes) — incl. hamria', () => {
for (const q of ['meknes', 'meknès', 'lmeknas', 'mknas', 'lmeknassa', 'hamria']) {
assert.equal(classify(q).city, 'مكناس', `query="${q}"`);
}
});
test('وجدة (Oujda)', () => {
for (const q of ['oujda', 'wajda', 'wjda', 'lojda', 'ojda']) {
assert.equal(classify(q).city, 'وجدة', `query="${q}"`);
}
});
test('القنيطرة (Kenitra) — incl. Arabizi 9=ق', () => {
for (const q of ['kenitra', 'kénitra', 'qnitra', 'knitra', '9nitra', 'l9nitra']) {
assert.equal(classify(q).city, 'القنيطرة', `query="${q}"`);
}
});
test('تطوان (Tetouan) — incl. Spanish/Arabizi forms', () => {
for (const q of ['tetouan', 'tétouan', 'titwan', 'tetwan', 'tetuan', '6twan']) {
assert.equal(classify(q).city, 'تطوان', `query="${q}"`);
}
});
test('الجديدة (El Jadida) — incl. mazagan historical', () => {
for (const q of ['jadida', 'el jadida', 'jdida', 'ljdida', 'mazagan', 'lmazagan']) {
assert.equal(classify(q).city, 'الجديدة', `query="${q}"`);
}
});
test('بني ملال (Beni Mellal)', () => {
for (const q of ['beni mellal', 'bani mellal', 'bni mlal', 'bani melal']) {
assert.equal(classify(q).city, 'بني ملال', `query="${q}"`);
}
});
test('خريبكة (Khouribga) — incl. Arabizi 5=خ', () => {
for (const q of ['khouribga', 'khribga', '5ribga', 'lkhribga']) {
assert.equal(classify(q).city, 'خريبكة', `query="${q}"`);
}
});
test('سطات (Settat)', () => {
for (const q of ['settat', 'stat', 'sttat', 'seta']) {
assert.equal(classify(q).city, 'سطات', `query="${q}"`);
}
});
// ─── Multi-trade renovation phrases ─────────────────────────────────────────
test('Multi-trade: bathroom renovation (4 trades)', () => {
const expected = ['بلومبي', 'كلامبيستري', 'طريسيان', 'صباغة'];
for (const q of ['تجديد الحمام', 'jded l7mam', 'nrm l7mam', 'sallat l7mam', 'renovation salle de bain']) {
assert.deepEqual(classify(q).multi_trade, expected, `query="${q}"`);
}
});
test('Multi-trade: apartment / home renovation', () => {
const expected = ['بناء', 'طريسيان', 'صباغة', 'ديكور'];
for (const q of ['تجديد الشقة', 'jded ddar', 'nrm chi9a', 'renovation appartement']) {
assert.deepEqual(classify(q).multi_trade, expected, `query="${q}"`);
}
});
test('Multi-trade: house construction', () => {
const expected = ['بناء', 'طريسيان', 'بلومبي', 'كلامبيستري'];
for (const q of ['بناء دار', 'nbni dar', 'bnay dar', 'construction maison']) {
assert.deepEqual(classify(q).multi_trade, expected, `query="${q}"`);
}
});
test('Multi-trade: kitchen renovation', () => {
const expected = ['نجارة', 'طريسيان', 'بلومبي', 'كلامبيستري'];
for (const q of ['مطبخ جديد', 'jded kuzina', 'nbeddel kuzina', 'kitchen renovation']) {
assert.deepEqual(classify(q).multi_trade, expected, `query="${q}"`);
}
});
test('Multi-trade: roof / terrace waterproofing', () => {
const expected = ['بناء', 'كلامبيستري', 'صباغة'];
for (const q of ['تجديد السطح', 'jded ssath', 'fuite stah', 'fuite toiture']) {
assert.deepEqual(classify(q).multi_trade, expected, `query="${q}"`);
}
});
// ─── Mixed-script + complex queries ─────────────────────────────────────────
test('Mixed scripts: English + French + Darija in one query', () => {
// "carpenter bois f gueliz" — English + French + neighborhood
assert.equal(classify('carpenter bois f gueliz').trade, 'نجارة');
assert.equal(classify('carpenter bois f gueliz').city, 'مراكش');
// "tnaqil 3afsh f mohammedia" — Darija + Arabizi + Casa suburb
assert.equal(classify('tnaqil 3afsh f mohammedia').trade, 'نقل');
assert.equal(classify('tnaqil 3afsh f mohammedia').city, 'الدار البيضاء');
// "9nitra panne courant" — Arabizi city + French problem
assert.equal(classify('9nitra panne courant').trade, 'طريسيان');
assert.equal(classify('9nitra panne courant').city, 'القنيطرة');
// "f 6anja bghit 7eddad" — two Arabizi conventions in one query
assert.equal(classify('f 6anja bghit 7eddad').trade, 'حدادة');
assert.equal(classify('f 6anja bghit 7eddad').city, 'طنجة');
});
test('Disambiguation: meuble alone is NOT carpentry; meuble bois IS', () => {
// After Phase 10 fix: bare "meuble" no longer matches نجارة (collides with نقل)
assert.equal(classify('tnaqil meubles').trade, 'نقل');
assert.equal(classify('meuble bois sur mesure').trade, 'نجارة');
assert.equal(classify('placard bois').trade, 'نجارة');
});
test('Off-topic / non-services queries return null', () => {
for (const q of ['salam', 'merci', 'okay', '', 'بغيت طاجين', 'تاجين هاسي', 'مرحبا']) {
const r = classify(q);
assert.equal(r.trade, null, `query="${q}" — should be null`);
}
});
test('All 12 valid trade categories are reachable', () => {
// Sanity check: every category in VALID_CATS must be reachable from some
// keyword. Pick the canonical Arabic name itself as the proof.
for (const cat of VALID_CATS) {
const result = classify(cat);
assert.equal(result.trade, cat, `canonical "${cat}" must map to itself`);
}
});