feat(p7): moteur git détaillé, API fichiers et watcher FS temps réel
Fondations de la refonte « vrai IDE IA worktree » (chantier P7→P12).
- git.ts : listChanges (status --porcelain=v2 -z + --numstat fusionnés),
fileDiff (diff unifié borné, refus binaire, untracked via --no-index),
stage/unstage/restore/clean, commitStaged, amendCommit (refus si poussé),
fetchRemote, pull (ff-only|rebase), lastCommit, resolveGitDir,
isSafeRelativePath ; worktreeStatus enrichi (compteurs staged/unstaged/
conflict + dernier commit), champs additifs WorktreeGitStatus.
- core/fs-watcher.ts : FsWatcherService (chokidar, refcount + pool LRU borné,
ignore .git sauf HEAD/index, débounce) → invalide factsCache, rediffuse
worktree_update et émet worktree_changes.
- WorktreeManager : getWorktreeChanges/getFileDiff/stage/unstage/discard/
fetch/pull/watch/unwatch/assertPathInWorktree ; commitWorktree { mode, amend }.
- routes/git.ts (changes/diff/stage/unstage/discard/fetch/pull) et
routes/files.ts (GET/PUT contenu fichier, bornage strict au worktree) ;
fs/list?includeFiles=1 (flag isFile).
- protocole ADDITIF (PROTOCOL_VERSION inchangé) : messages WS watch/unwatch
(validés dans parseClientMessage) + worktree_changes poussé ciblé par la
gateway ; front wsClient.watchWorktree + lib/git-api.ts + api.put.
- tests git/fs-watcher/protocole + acceptance-p7.mjs (ALL GREEN).
- dépendance : chokidar (serveur).
This commit is contained in:
@@ -128,6 +128,11 @@ describe('parseClientMessage — cas valides', () => {
|
||||
expect(parseClientMessage('{"type":"sub","topics":[]}')).toEqual({ type: 'sub', topics: [] });
|
||||
});
|
||||
|
||||
it('watch / unwatch (P7) avec repoId + path', () => {
|
||||
expect(parseClientMessage('{"type":"watch","repoId":"r1","path":"/home/u/wt"}')).toEqual({ type: 'watch', repoId: 'r1', path: '/home/u/wt' });
|
||||
expect(parseClientMessage('{"type":"unwatch","repoId":"r1","path":"/home/u/wt"}')).toEqual({ type: 'unwatch', repoId: 'r1', path: '/home/u/wt' });
|
||||
});
|
||||
|
||||
it('ping (champs superflus ignorés)', () => {
|
||||
expect(parseClientMessage('{"type":"ping","extra":42}')).toEqual({ type: 'ping' });
|
||||
});
|
||||
@@ -152,6 +157,13 @@ describe('parseClientMessage — cas malformés', () => {
|
||||
expect(parseClientMessage('{"type":"hello","protocol":"1"}')).toBeNull();
|
||||
});
|
||||
|
||||
it('watch / unwatch : repoId ou path manquant / mal typé', () => {
|
||||
expect(parseClientMessage('{"type":"watch","path":"/x"}')).toBeNull();
|
||||
expect(parseClientMessage('{"type":"watch","repoId":"r1"}')).toBeNull();
|
||||
expect(parseClientMessage('{"type":"watch","repoId":"","path":"/x"}')).toBeNull();
|
||||
expect(parseClientMessage('{"type":"unwatch","repoId":"r1","path":42}')).toBeNull();
|
||||
});
|
||||
|
||||
it('attach : dimensions hors bornes ou non entières', () => {
|
||||
const make = (over: Record<string, unknown>): string =>
|
||||
JSON.stringify({ type: 'attach', sessionId: 's1', mode: 'interactive', cols: 80, rows: 24, ...over });
|
||||
|
||||
Reference in New Issue
Block a user