fix(2137): skip worktree isolation when .gitmodules detected (#2144)
* feat(sdk): add typed query foundation and gsd-sdk query (Phase 1) Add sdk/src/query registry and handlers with tests, GSDQueryError, CLI query wiring, and supporting type/tool-scoping hooks. Update CHANGELOG. Vitest 4 constructor mock fixes in milestone-runner tests. Made-with: Cursor * fix(2137): skip worktree isolation when .gitmodules detected When a project contains git submodules, worktree isolation cannot correctly handle submodule commits — three separate gaps exist in worktree setup, executor commit protocol, and merge-back. Rather than patch each gap individually, detect .gitmodules at phase start and fall back to sequential execution, which handles submodules transparently (Option B). Affected workflows: execute-phase.md, quick.md --------- Co-authored-by: David Sienkowski <dave@sienkowski.com>
This commit is contained in:
119
sdk/src/query/registry.test.ts
Normal file
119
sdk/src/query/registry.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Unit tests for QueryRegistry, extractField, and createRegistry factory.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { QueryRegistry, extractField } from './registry.js';
|
||||
import { createRegistry } from './index.js';
|
||||
import type { QueryResult } from './utils.js';
|
||||
|
||||
// ─── extractField ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('extractField', () => {
|
||||
it('extracts nested value with dot notation', () => {
|
||||
expect(extractField({ a: { b: 1 } }, 'a.b')).toBe(1);
|
||||
});
|
||||
|
||||
it('extracts top-level value', () => {
|
||||
expect(extractField({ slug: 'my-phase' }, 'slug')).toBe('my-phase');
|
||||
});
|
||||
|
||||
it('extracts array element with bracket notation', () => {
|
||||
expect(extractField({ items: [10, 20, 30] }, 'items[1]')).toBe(20);
|
||||
});
|
||||
|
||||
it('extracts array element with negative index', () => {
|
||||
expect(extractField({ items: [10, 20, 30] }, 'items[-1]')).toBe(30);
|
||||
});
|
||||
|
||||
it('returns undefined for null input', () => {
|
||||
expect(extractField(null, 'a')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for undefined input', () => {
|
||||
expect(extractField(undefined, 'a')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for missing nested path', () => {
|
||||
expect(extractField({ a: 1 }, 'b.c')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined when bracket access targets non-array', () => {
|
||||
expect(extractField({ items: 'not-array' }, 'items[0]')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles deeply nested paths', () => {
|
||||
expect(extractField({ a: { b: { c: { d: 42 } } } }, 'a.b.c.d')).toBe(42);
|
||||
});
|
||||
|
||||
it('handles mixed dot and bracket notation', () => {
|
||||
expect(extractField({ data: { items: [{ name: 'x' }] } }, 'data.items[0].name')).toBe('x');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── QueryRegistry ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('QueryRegistry', () => {
|
||||
it('register makes has() return true', () => {
|
||||
const registry = new QueryRegistry();
|
||||
const handler = async () => ({ data: 'test' });
|
||||
registry.register('test-cmd', handler);
|
||||
|
||||
expect(registry.has('test-cmd')).toBe(true);
|
||||
});
|
||||
|
||||
it('has() returns false for unregistered command', () => {
|
||||
const registry = new QueryRegistry();
|
||||
|
||||
expect(registry.has('nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
it('dispatch calls registered handler', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
const handler = vi.fn(async (args: string[], _projectDir: string): Promise<QueryResult> => {
|
||||
return { data: { value: args[0] } };
|
||||
});
|
||||
registry.register('test-cmd', handler);
|
||||
|
||||
const result = await registry.dispatch('test-cmd', ['arg1'], '/tmp');
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(['arg1'], '/tmp');
|
||||
expect(result).toEqual({ data: { value: 'arg1' } });
|
||||
});
|
||||
|
||||
it('dispatch throws GSDError for unregistered command', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
// Bridge removed in v3.0 — unknown commands throw, not fallback
|
||||
await expect(registry.dispatch('unknown-cmd', ['arg1'], '/tmp/project'))
|
||||
.rejects.toThrow('Unknown command: "unknown-cmd"');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── createRegistry ────────────────────────────────────────────────────────
|
||||
|
||||
describe('createRegistry', () => {
|
||||
it('returns a QueryRegistry instance', () => {
|
||||
const registry = createRegistry();
|
||||
|
||||
expect(registry).toBeInstanceOf(QueryRegistry);
|
||||
});
|
||||
|
||||
it('has generate-slug registered', () => {
|
||||
const registry = createRegistry();
|
||||
|
||||
expect(registry.has('generate-slug')).toBe(true);
|
||||
});
|
||||
|
||||
it('has current-timestamp registered', () => {
|
||||
const registry = createRegistry();
|
||||
|
||||
expect(registry.has('current-timestamp')).toBe(true);
|
||||
});
|
||||
|
||||
it('can dispatch generate-slug', async () => {
|
||||
const registry = createRegistry();
|
||||
const result = await registry.dispatch('generate-slug', ['My Phase'], '/tmp');
|
||||
|
||||
expect(result).toEqual({ data: { slug: 'my-phase' } });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user