hub-create-similar-project.test.mjs
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
sha256:6a102aafafdfe7e70a24f4e59740200f0ee713ce7915f1b53e9d4ba5ee8b4410
Initial Muse snapshot
Human
48 days ago