Gitea - lien « Code source » hardcodé (REPO_SOURCE_URL) vers le dépôt, toujours visible - retrait complet du réglage configurable (api.ts, route+store settings, SettingsView, i18n, help, tests) Découverte des dépôts - aucune racine de scan par défaut → pas de scan au premier démarrage (clean install) - corrige la découverte des dépôts à l'INTÉRIEUR d'une racine qui est elle-même un repo (depth 0 = conteneur de scan, on descend ; depth > 0 = feuille) Sécurité « enterprise-deployable » - en-têtes HTTP durcis (CSP, X-Frame-Options, nosniff, Referrer-Policy, HSTS conditionnel, no-store API), header Server retiré - permissions DB 0o600 / dossier de données 0o700 ; error handler sanitisé ; garde Content-Type sur les mutations - chiffrement au repos AES-256-GCM des secrets (server_secret, clé privée VAPID) via SecretBox - journal d'audit (migration #7) + endpoint /audit-logs + RGPD export/effacement + UI Réglages - SECURITY.md, docs/ENTERPRISE_DEPLOYMENT.md, sections README EN/FR, SBOM CycloneDX en CI CI - pack-smoke packe depuis le contexte du package (cd packages/server) au lieu de « npm pack -w » : corrige l'embarquement de @arboretum/shared dans le tarball Purge des données personnelles du dépôt public - suppression de spikes/s4-discovery/result.json, reformulation du VERDICT - chemin de test générique, anonymisation des 5 fixtures de dialogues
94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { munge, parseLines, scanProjects } from '../src/core/jsonl-discovery.js';
|
|
|
|
function writeJsonl(projectsDir: string, cwd: string, sid: string, lines: object[]): void {
|
|
const dir = join(projectsDir, munge(cwd));
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, `${sid}.jsonl`), `${lines.map((l) => JSON.stringify(l)).join('\n')}\n`);
|
|
}
|
|
|
|
describe('munge', () => {
|
|
it('reproduit le nom de dossier ~/.claude/projects', () => {
|
|
expect(munge('/home/x/My Project!')).toBe('-home-x-My-Project-');
|
|
expect(munge('/home/user/projects/demo')).toBe('-home-user-projects-demo');
|
|
});
|
|
});
|
|
|
|
describe('parseLines (tolérance)', () => {
|
|
it('ignore les lignes vides, coupées ou malformées sans throw', () => {
|
|
const text = '{"a":1}\n\n{not json}\n{"b":2}\n{"c":';
|
|
expect(parseLines(text)).toEqual([{ a: 1 }, { b: 2 }]);
|
|
});
|
|
it('shift/pop les lignes partielles en bord de chunk', () => {
|
|
const text = 'partial}\n{"ok":1}\n{also partial';
|
|
expect(parseLines(text, true, true)).toEqual([{ ok: 1 }]);
|
|
});
|
|
});
|
|
|
|
describe('scanProjects', () => {
|
|
let dir: string;
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(join(tmpdir(), 'arb-jsonl-'));
|
|
});
|
|
afterEach(() => {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('dossier absent → tableau vide', async () => {
|
|
expect(await scanProjects(join(dir, 'nope'))).toEqual([]);
|
|
});
|
|
|
|
it('extrait claudeSessionId + cwd + branche + version', async () => {
|
|
writeJsonl(dir, '/home/u/proj', 'sid-1', [
|
|
{ type: 'user', sessionId: 'sid-1', cwd: '/home/u/proj', gitBranch: 'main', version: '2.1.170', message: { content: 'hello world' } },
|
|
]);
|
|
const res = await scanProjects(dir);
|
|
expect(res).toHaveLength(1);
|
|
expect(res[0]).toMatchObject({ claudeSessionId: 'sid-1', cwd: '/home/u/proj', gitBranch: 'main', version: '2.1.170' });
|
|
});
|
|
|
|
it('écarte un fichier sans cwd (queue-operation only)', async () => {
|
|
writeJsonl(dir, '/x', 'sid-q', [{ type: 'queue-operation', sessionId: 'sid-q' }]);
|
|
expect(await scanProjects(dir)).toEqual([]);
|
|
});
|
|
|
|
it('survit à une ligne malformée et reste exploitable', async () => {
|
|
const d = join(dir, munge('/home/u/p2'));
|
|
mkdirSync(d, { recursive: true });
|
|
writeFileSync(
|
|
join(d, 'sid-2.jsonl'),
|
|
`{bad json\n${JSON.stringify({ type: 'user', sessionId: 'sid-2', cwd: '/home/u/p2', message: { content: 'ok' } })}\n`,
|
|
);
|
|
const res = await scanProjects(dir);
|
|
expect(res).toHaveLength(1);
|
|
expect(res[0].claudeSessionId).toBe('sid-2');
|
|
});
|
|
|
|
describe('priorité de titre (ai-title > summary > last-prompt > 1er user)', () => {
|
|
it('ai-title gagne sur tout', async () => {
|
|
writeJsonl(dir, '/t/1', 's', [
|
|
{ type: 'user', sessionId: 's', cwd: '/t/1', message: { content: 'premier prompt' } },
|
|
{ type: 'summary', sessionId: 's', summary: 'un résumé' },
|
|
{ type: 'last-prompt', sessionId: 's', lastPrompt: 'dernier prompt' },
|
|
{ type: 'ai-title', sessionId: 's', aiTitle: 'Titre IA' },
|
|
]);
|
|
expect((await scanProjects(dir))[0].title).toBe('Titre IA');
|
|
});
|
|
it('summary gagne sur last-prompt et user', async () => {
|
|
writeJsonl(dir, '/t/2', 's', [
|
|
{ type: 'user', sessionId: 's', cwd: '/t/2', message: { content: 'premier prompt' } },
|
|
{ type: 'last-prompt', sessionId: 's', lastPrompt: 'dernier prompt' },
|
|
{ type: 'summary', sessionId: 's', summary: 'un résumé' },
|
|
]);
|
|
expect((await scanProjects(dir))[0].title).toBe('un résumé');
|
|
});
|
|
it('repli ultime sur le premier prompt user', async () => {
|
|
writeJsonl(dir, '/t/3', 's', [{ type: 'user', sessionId: 's', cwd: '/t/3', message: { content: 'premier prompt' } }]);
|
|
expect((await scanProjects(dir))[0].title).toBe('premier prompt');
|
|
});
|
|
});
|
|
});
|