/** * lib/detector.resolveWebRoot — THE web root of a generated project, resolved * from disk and never guessed. A generated project has ONE web app named after * the PROJECT (`web/-web`), hosting every business application — the * audit router used to guess `web/-web` and failed on every project whose * name differs from its application code (client NOTES/MEMOS, 2026-10-03). * Cascade pinned here: config override → init-state nameLower → unique * candidate → @atlashub/smartstack dependents → ambiguous (said, never picked) * → legacy layouts → none. Listing is sorted, so the answer is host-independent. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { resolveWebRoot, SMARTSTACK_WEB_PACKAGE } from '../detector.js' let root: string beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'ss-web-root-')) }) afterEach(() => rmSync(root, { recursive: true, force: true })) function pkg(dir: string, content: Record = {}): string { mkdirSync(dir, { recursive: true }) writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'x', ...content }), 'utf8') return dir } function smartstackDir(dir: string): string { mkdirSync(join(root, '.smartstack'), { recursive: true }) return dir } describe('resolveWebRoot', () => { it('no web app at all → none, with an empty candidate list', () => { expect(resolveWebRoot(root)).toEqual({ status: 'none', candidates: [] }) }) it('a single web//package.json → found via unique, whatever the name', () => { const web = pkg(join(root, 'web', 'demo-web')) expect(resolveWebRoot(root)).toEqual({ status: 'found', path: web, via: 'unique', candidates: [web] }) }) it('a folder without package.json (a stray) is not a candidate', () => { mkdirSync(join(root, 'web', 'app-web'), { recursive: true }) const web = pkg(join(root, 'web', 'demo-web')) expect(resolveWebRoot(root)).toMatchObject({ status: 'found', path: web, via: 'unique' }) }) it('dot-folders and node_modules under web/ are skipped', () => { pkg(join(root, 'web', '.cache')) pkg(join(root, 'web', 'node_modules')) const web = pkg(join(root, 'web', 'demo-web')) expect(resolveWebRoot(root)).toMatchObject({ status: 'found', path: web, via: 'unique' }) }) it('several candidates, none a SmartStack client → ambiguous, candidates sorted by name', () => { const b = pkg(join(root, 'web', 'b-web')) const a = pkg(join(root, 'web', 'a-web')) expect(resolveWebRoot(root)).toEqual({ status: 'ambiguous', candidates: [a, b] }) }) it('several candidates → the ONE depending on @atlashub/smartstack wins (dependencies or devDependencies)', () => { pkg(join(root, 'web', 'a-web')) const client = pkg(join(root, 'web', 'b-web'), { devDependencies: { [SMARTSTACK_WEB_PACKAGE]: '3.70.0' } }) expect(resolveWebRoot(root)).toMatchObject({ status: 'found', path: client, via: 'smartstack-dep' }) }) it('several SmartStack clients → still ambiguous (never the first one)', () => { pkg(join(root, 'web', 'a-web'), { dependencies: { [SMARTSTACK_WEB_PACKAGE]: '3.70.0' } }) pkg(join(root, 'web', 'b-web'), { dependencies: { [SMARTSTACK_WEB_PACKAGE]: '3.70.0' } }) expect(resolveWebRoot(root).status).toBe('ambiguous') }) it('.smartstack/init-state.json projectConfig.nameLower → web/-web, before any listing', () => { pkg(join(root, 'web', 'a-web'), { dependencies: { [SMARTSTACK_WEB_PACKAGE]: '3.70.0' } }) const named = pkg(join(root, 'web', 'demo-web'), { dependencies: { [SMARTSTACK_WEB_PACKAGE]: '3.70.0' } }) smartstackDir(root) writeFileSync(join(root, '.smartstack', 'init-state.json'), JSON.stringify({ projectConfig: { nameLower: 'demo' } }), 'utf8') expect(resolveWebRoot(root)).toEqual({ status: 'found', path: named, via: 'init-state', candidates: [named] }) }) it('an init-state naming a folder that does not exist falls through to the listing', () => { const web = pkg(join(root, 'web', 'demo-web')) smartstackDir(root) writeFileSync(join(root, '.smartstack', 'init-state.json'), JSON.stringify({ projectConfig: { nameLower: 'gone' } }), 'utf8') expect(resolveWebRoot(root)).toMatchObject({ status: 'found', path: web, via: 'unique' }) }) it('.smartstack/config.json paths.web wins over everything, resolved against the project root', () => { pkg(join(root, 'web', 'demo-web')) const declared = join(root, 'frontend-custom') mkdirSync(declared, { recursive: true }) smartstackDir(root) writeFileSync(join(root, '.smartstack', 'config.json'), JSON.stringify({ paths: { web: 'frontend-custom' } }), 'utf8') expect(resolveWebRoot(root)).toEqual({ status: 'found', path: declared, via: 'config', candidates: [declared] }) }) it('a config paths.web pointing at a missing folder is ignored, not trusted', () => { const web = pkg(join(root, 'web', 'demo-web')) smartstackDir(root) writeFileSync(join(root, '.smartstack', 'config.json'), JSON.stringify({ paths: { web: 'nowhere' } }), 'utf8') expect(resolveWebRoot(root)).toMatchObject({ status: 'found', path: web, via: 'unique' }) }) it('an unreadable config.json falls through silently', () => { const web = pkg(join(root, 'web', 'demo-web')) smartstackDir(root) writeFileSync(join(root, '.smartstack', 'config.json'), '{ not json', 'utf8') expect(resolveWebRoot(root)).toMatchObject({ status: 'found', path: web, via: 'unique' }) }) it('legacy layouts (web/package.json, client/, frontend/, ui/, src/web) answer only when web/ has no candidate', () => { const legacy = pkg(join(root, 'frontend')) expect(resolveWebRoot(root)).toEqual({ status: 'found', path: legacy, via: 'fallback-layout', candidates: [legacy] }) }) })