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:
Tom Boucher
2026-04-12 08:33:04 -04:00
committed by GitHub
parent 58632e0718
commit 002bcf2a8a
63 changed files with 17872 additions and 23 deletions

View 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' } });
});
});