feat: découverte automatique des dépôts git (scan + montrer/cacher)
Scan borné du système de fichiers (racines configurables, défaut home ; profondeur/nombre/timeout bornés ; symlinks non suivis ; exclusions node_modules/dotdirs) qui auto-enregistre les nouveaux dépôts. Insertion atomique ON CONFLICT DO NOTHING (idempotence + anti-résurrection d'un dépôt masqué + anti-course). Scan au démarrage + bouton manuel + re-scan périodique (RepoDiscoveryService, démarré dans runDaemon). Colonne repos.hidden : masquer = conservé en DB mais exclu du dashboard et jamais ré-ajouté ; supprimer = re-découvrable. UI : bouton œil par dépôt + bascule afficher-les-masqués sur le dashboard, section Découverte dans les Réglages (racines + intervalle, allow-list stricte). Robustesse : listAllWorktrees tolère l'échec git par dépôt ; flag --no-discover (escape hatch + hermétisme des acceptations). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,10 @@ describe('GET /api/v1/settings', () => {
|
||||
expect(body.server.allowedOrigins).toEqual(['https://host.tailnet.ts.net']);
|
||||
expect(body.server.vapidPublicKey).toBeTruthy(); // clé publique = sûre à exposer
|
||||
expect(body.settings.giteaUrl).toBeNull();
|
||||
// défauts de découverte : home (1 racine) + intervalle 5 min
|
||||
expect(Array.isArray(body.settings.scanRoots)).toBe(true);
|
||||
expect(body.settings.scanRoots).toHaveLength(1);
|
||||
expect(body.settings.scanIntervalMin).toBe(5);
|
||||
});
|
||||
|
||||
it('n’expose AUCUN secret (server_secret, clé privée VAPID)', async () => {
|
||||
@@ -124,3 +128,36 @@ describe('PATCH /api/v1/settings', () => {
|
||||
expect(res.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/v1/settings — découverte des dépôts', () => {
|
||||
it('enregistre des scanRoots et un intervalle valides et les renvoie', async () => {
|
||||
const res = await bundle.app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/api/v1/settings',
|
||||
headers: auth(),
|
||||
payload: { scanRoots: ['/home/u/work', '/srv/code'], scanIntervalMin: 10 },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json() as SettingsResponse;
|
||||
expect(body.settings.scanRoots).toEqual(['/home/u/work', '/srv/code']);
|
||||
expect(body.settings.scanIntervalMin).toBe(10);
|
||||
// persisté
|
||||
const get = await bundle.app.inject({ method: 'GET', url: '/api/v1/settings', headers: auth() });
|
||||
expect((get.json() as SettingsResponse).settings.scanRoots).toEqual(['/home/u/work', '/srv/code']);
|
||||
});
|
||||
|
||||
it('rejette des racines non absolues, "/", avec ".." ou en surnombre (400)', async () => {
|
||||
const bads: unknown[] = [['relative/path'], ['/'], ['/a/../b'], Array.from({ length: 17 }, (_, i) => `/r${i}`)];
|
||||
for (const scanRoots of bads) {
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { scanRoots } });
|
||||
expect(res.statusCode).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejette un intervalle hors borne (400)', async () => {
|
||||
const res = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { scanIntervalMin: 5000 } });
|
||||
expect(res.statusCode).toBe(400);
|
||||
const neg = await bundle.app.inject({ method: 'PATCH', url: '/api/v1/settings', headers: auth(), payload: { scanIntervalMin: -1 } });
|
||||
expect(neg.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user