hub-create-similar-project.test.mjs
123 lines 4.0 KB
Raw
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor ⚠ breaking 16 days ago
1 /**
2 * Mirrors web/hub/hub.js project similarity for create-path guard.
3 */
4 import { describe, it } from 'node:test';
5 import assert from 'node:assert';
6
7 function normSlug(s) {
8 return String(s || '')
9 .toLowerCase()
10 .replace(/[^a-z0-9-]/g, '-')
11 .replace(/-+/g, '-')
12 .replace(/^-|-$/g, '');
13 }
14
15 function normalizeProjectKeyForSimilarity(s) {
16 return String(s || '')
17 .toLowerCase()
18 .trim()
19 .replace(/[\s_]+/g, '-')
20 .replace(/-+/g, '-')
21 .replace(/^-|-$/g, '');
22 }
23
24 function levenshteinHub(a, b) {
25 const m = a.length;
26 const n = b.length;
27 if (!m) return n;
28 if (!n) return m;
29 const row = new Array(n + 1);
30 for (let j = 0; j <= n; j++) row[j] = j;
31 for (let i = 1; i <= m; i++) {
32 let prev = row[0];
33 row[0] = i;
34 for (let j = 1; j <= n; j++) {
35 const cur = row[j];
36 const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
37 row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + cost);
38 prev = cur;
39 }
40 }
41 return row[n];
42 }
43
44 function findSimilarFacetProject(userSlug, projectsArr) {
45 if (!userSlug || !projectsArr || !projectsArr.length) return null;
46 const uNorm = normSlug(String(userSlug));
47 if (!uNorm) return null;
48 for (const p of projectsArr) {
49 if (normSlug(String(p)) === uNorm) return null;
50 }
51 const uCompact = normalizeProjectKeyForSimilarity(userSlug).replace(/-/g, '');
52 let best = null;
53 let bestScore = Infinity;
54 for (const p of projectsArr) {
55 const pv = String(p).trim();
56 if (!pv) continue;
57 const pNorm = normSlug(pv);
58 if (!pNorm) continue;
59 const pCompact = normalizeProjectKeyForSimilarity(pv).replace(/-/g, '');
60 let score = Infinity;
61 if (uCompact.length >= 3 && pCompact.length >= 3 && uCompact === pCompact) score = 0;
62 if (score > 0) {
63 const a = normalizeProjectKeyForSimilarity(userSlug);
64 const b = normalizeProjectKeyForSimilarity(pv);
65 const d = levenshteinHub(a, b);
66 if (d <= 2 && Math.abs(a.length - b.length) <= 3) score = Math.min(score, d + 0.1);
67 }
68 if (score > 0) {
69 const a = normalizeProjectKeyForSimilarity(userSlug);
70 const b = normalizeProjectKeyForSimilarity(pv);
71 const shorter = a.length <= b.length ? a : b;
72 const longer = a.length <= b.length ? b : a;
73 if (shorter.length >= 3 && longer.startsWith(shorter) && longer.length - shorter.length <= 2) {
74 score = Math.min(score, longer.length - shorter.length + 0.5);
75 }
76 }
77 if (score < bestScore) {
78 bestScore = score;
79 best = pv;
80 }
81 }
82 return bestScore < 10 ? best : null;
83 }
84
85 function collectProjectSubroots(slug, folderStrings) {
86 const prefix = 'projects/' + slug.replace(/^\/+|\/+$/g, '') + '/';
87 const subs = new Set();
88 for (const f of folderStrings || []) {
89 if (!f || typeof f !== 'string') continue;
90 const n = f.replace(/\\/g, '/').replace(/\/+$/, '');
91 if (!n.startsWith(prefix)) continue;
92 const rest = n.slice(prefix.length);
93 if (!rest) continue;
94 const first = rest.split('/')[0];
95 if (first) subs.add(first);
96 }
97 return [...subs].sort((a, b) => a.localeCompare(b));
98 }
99
100 describe('findSimilarFacetProject', () => {
101 const facets = ['born-free', 'store-free'];
102
103 it('returns null when slug matches a facet (normSlug)', () => {
104 assert.strictEqual(findSimilarFacetProject('born-free', facets), null);
105 assert.strictEqual(findSimilarFacetProject('Born-Free', facets), null);
106 });
107
108 it('maps hyphen-less typo to existing slug', () => {
109 assert.strictEqual(findSimilarFacetProject('bornfree', facets), 'born-free');
110 assert.strictEqual(findSimilarFacetProject('storefree', facets), 'store-free');
111 });
112
113 it('returns null when unrelated', () => {
114 assert.strictEqual(findSimilarFacetProject('completely-other', ['alpha', 'beta']), null);
115 });
116 });
117
118 describe('collectProjectSubroots', () => {
119 it('collects first path segment under projects/slug/', () => {
120 const folders = ['projects/acme/inbox', 'projects/acme/research/deep', 'inbox', 'projects/other/x'];
121 assert.deepStrictEqual(collectProjectSubroots('acme', folders), ['inbox', 'research']);
122 });
123 });
File History 2 commits
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor 16 days ago