feat: enhance workflow and testing infrastructure

- Introduced ExecutionMode type for project execution modes (parallel, sequential).
- Updated SetupOpts to use ExecutionMode instead of string literals.
- Enhanced workflow states to include a new "In Review" state with appropriate transitions.
- Implemented TestHarness for end-to-end testing, including command interception and workspace setup.
- Created TestProvider for in-memory issue tracking during tests.
- Refactored project registration and setup tools to utilize ExecutionMode.
- Updated various tools to ensure compatibility with new workflow and execution modes.
- Added new dependencies: cockatiel for resilience and zod for schema validation.
This commit is contained in:
Lauren ten Hoor
2026-02-16 13:27:14 +08:00
parent a359ffed34
commit 371e760d94
37 changed files with 2444 additions and 263 deletions

View File

@@ -6,8 +6,11 @@ import {
type Issue,
type StateLabel,
type IssueComment,
type PrStatus,
PrState,
} from "./provider.js";
import { runCommand } from "../run-command.js";
import { withResilience } from "./resilience.js";
import {
DEFAULT_WORKFLOW,
getStateLabels,
@@ -25,8 +28,10 @@ export class GitLabProvider implements IssueProvider {
}
private async glab(args: string[]): Promise<string> {
const result = await runCommand(["glab", ...args], { timeoutMs: 30_000, cwd: this.repoPath });
return result.stdout.trim();
return withResilience(async () => {
const result = await runCommand(["glab", ...args], { timeoutMs: 30_000, cwd: this.repoPath });
return result.stdout.trim();
});
}
async ensureLabel(name: string, color: string): Promise<void> {
@@ -122,6 +127,28 @@ export class GitLabProvider implements IssueProvider {
} catch { return null; }
}
async getPrStatus(issueId: number): Promise<PrStatus> {
const pat = `#${issueId}`;
// Check open MRs first
try {
const raw = await this.glab(["mr", "list", "--output", "json", "--state", "opened"]);
const mrs = JSON.parse(raw) as Array<{ title: string; description: string; web_url: string; approved_by?: Array<unknown> }>;
const mr = mrs.find((m) => m.title.includes(pat) || (m.description ?? "").includes(pat));
if (mr) {
const state = mr.approved_by && mr.approved_by.length > 0 ? PrState.APPROVED : PrState.OPEN;
return { state, url: mr.web_url };
}
} catch { /* continue to merged check */ }
// Check merged MRs
try {
const raw = await this.glab(["mr", "list", "--output", "json", "--state", "merged"]);
const mrs = JSON.parse(raw) as Array<{ title: string; description: string; web_url: string }>;
const mr = mrs.find((m) => m.title.includes(pat) || (m.description ?? "").includes(pat));
if (mr) return { state: PrState.MERGED, url: mr.web_url };
} catch { /* ignore */ }
return { state: PrState.CLOSED, url: null };
}
async addComment(issueId: number, body: string): Promise<void> {
// Pass message directly as argv — no shell escaping needed with spawn
await this.glab(["issue", "note", String(issueId), "--message", body]);