* feat: Redesign git changes to split stage/unstaged files. Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * refactor: streamline git changes panel * fix: label staged and working diff tabs * fix: isolate staged and working diff files * fix: scope staged and working diff updates * fix: scope git row revert to working changes --------- Signed-off-by: Paolo Insogna <paolo@cowtech.it> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { createGitIndexMutationQueue, type GitIndexMutationDirection } from './gitIndexMutationQueue';
|
|
|
|
type Deferred<T> = {
|
|
promise: Promise<T>;
|
|
resolve: (value: T) => void;
|
|
reject: (error: unknown) => void;
|
|
};
|
|
|
|
const createDeferred = <T>(): Deferred<T> => {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (error: unknown) => void;
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return { promise, resolve, reject };
|
|
};
|
|
|
|
const waitMicrotask = async () => {
|
|
await Promise.resolve();
|
|
};
|
|
|
|
describe('createGitIndexMutationQueue', () => {
|
|
test('coalesces consecutive mutations with the same directory and direction', async () => {
|
|
const calls: Array<{ direction: GitIndexMutationDirection; paths: string[] }> = [];
|
|
const queue = createGitIndexMutationQueue({
|
|
runMutation: async ({ direction, paths }) => {
|
|
calls.push({ direction, paths });
|
|
},
|
|
onMutationComplete: () => {},
|
|
onMutationError: () => {},
|
|
onPathsComplete: () => {},
|
|
scheduleFlush: () => queue.flush(),
|
|
});
|
|
|
|
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['a.ts']) });
|
|
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['b.ts', 'a.ts']) });
|
|
queue.flush();
|
|
await waitMicrotask();
|
|
|
|
expect(calls).toEqual([{ direction: 'stage', paths: ['a.ts', 'b.ts'] }]);
|
|
});
|
|
|
|
test('serializes mutations and preserves alternating direction order', async () => {
|
|
const first = createDeferred<void>();
|
|
const calls: Array<{ direction: GitIndexMutationDirection; paths: string[] }> = [];
|
|
let callCount = 0;
|
|
|
|
const queue = createGitIndexMutationQueue({
|
|
runMutation: ({ direction, paths }) => {
|
|
calls.push({ direction, paths });
|
|
callCount += 1;
|
|
return callCount === 1 ? first.promise : Promise.resolve();
|
|
},
|
|
onMutationComplete: () => {},
|
|
onMutationError: () => {},
|
|
onPathsComplete: () => {},
|
|
scheduleFlush: () => queue.flush(),
|
|
});
|
|
|
|
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['a.ts']) });
|
|
queue.enqueue({ directory: '/repo', direction: 'unstage', paths: new Set(['a.ts']) });
|
|
queue.flush();
|
|
queue.flush();
|
|
await waitMicrotask();
|
|
|
|
expect(calls).toEqual([{ direction: 'stage', paths: ['a.ts'] }]);
|
|
expect(queue.isRunning()).toBe(true);
|
|
|
|
first.resolve();
|
|
await waitMicrotask();
|
|
await waitMicrotask();
|
|
|
|
expect(calls).toEqual([
|
|
{ direction: 'stage', paths: ['a.ts'] },
|
|
{ direction: 'unstage', paths: ['a.ts'] },
|
|
]);
|
|
});
|
|
|
|
test('reports errors, completes paths, and continues the queue', async () => {
|
|
const errors: unknown[] = [];
|
|
const completedPaths: string[][] = [];
|
|
const completedDirections: GitIndexMutationDirection[] = [];
|
|
let callCount = 0;
|
|
|
|
const queue = createGitIndexMutationQueue({
|
|
runMutation: async ({ direction }) => {
|
|
callCount += 1;
|
|
if (callCount === 1) {
|
|
throw new Error(`${direction} failed`);
|
|
}
|
|
},
|
|
onMutationComplete: ({ direction }) => {
|
|
completedDirections.push(direction);
|
|
},
|
|
onMutationError: (_mutation, error) => {
|
|
errors.push(error);
|
|
},
|
|
onPathsComplete: (paths) => {
|
|
completedPaths.push(paths);
|
|
},
|
|
scheduleFlush: () => queue.flush(),
|
|
});
|
|
|
|
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['a.ts']) });
|
|
queue.enqueue({ directory: '/repo', direction: 'unstage', paths: new Set(['b.ts']) });
|
|
queue.flush();
|
|
await waitMicrotask();
|
|
await waitMicrotask();
|
|
|
|
expect(errors).toHaveLength(1);
|
|
expect(completedDirections).toEqual(['unstage']);
|
|
expect(completedPaths).toEqual([['a.ts'], ['b.ts']]);
|
|
});
|
|
|
|
test('passes rollback callbacks to error handlers', async () => {
|
|
let rollbackCalled = false;
|
|
const queue = createGitIndexMutationQueue({
|
|
runMutation: async () => {
|
|
throw new Error('stage failed');
|
|
},
|
|
onMutationComplete: () => {},
|
|
onMutationError: (mutation) => {
|
|
mutation.rollback?.();
|
|
},
|
|
onPathsComplete: () => {},
|
|
scheduleFlush: () => queue.flush(),
|
|
});
|
|
|
|
queue.enqueue({
|
|
directory: '/repo',
|
|
direction: 'stage',
|
|
paths: new Set(['a.ts']),
|
|
rollback: () => {
|
|
rollbackCalled = true;
|
|
},
|
|
});
|
|
queue.flush();
|
|
await waitMicrotask();
|
|
|
|
expect(rollbackCalled).toBe(true);
|
|
});
|
|
});
|