fix: align v1→v2 commands with upstream types, remove engine-dependent slice mutations
- Fix ProjectTotals field names (totalCost→cost, totalDuration→duration, etc.) - Fix UnitMetrics field names (status→finishedAt, unitId→id) - Fix ModelAggregate field name (count→units) - Fix ActiveRef usage (no phase/slices fields) - Remove commands-slice-mutation.ts (depends on single-writer engine) - Remove slice mutation routes from catalog, do command, and workflow handler - Update backlog promote to work without engine slice commands
This commit is contained in:
parent
0516a611e3
commit
d8a472d4b4
10 changed files with 36 additions and 399 deletions
|
|
@ -14,12 +14,22 @@ import { deriveState } from "./state.js";
|
|||
import { gsdRoot } from "./paths.js";
|
||||
import { loadPrompt } from "./prompt-loader.js";
|
||||
|
||||
function findLastCompletedSlice(state: { activeMilestone?: { slices?: Array<{ id: string; status: string }> } }): string | null {
|
||||
const slices = state.activeMilestone?.slices ?? [];
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
if (slices[i].status === "done" || slices[i].status === "completed") {
|
||||
return slices[i].id;
|
||||
function findLastCompletedSlice(basePath: string, milestoneId: string): string | null {
|
||||
// Scan disk for slices that have a SUMMARY.md (indicating completion)
|
||||
const slicesDir = join(gsdRoot(basePath), "milestones", milestoneId, "slices");
|
||||
if (!existsSync(slicesDir)) return null;
|
||||
|
||||
try {
|
||||
const entries = readdirSync(slicesDir, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory() && /^S\d+$/.test(e.name))
|
||||
.sort((a, b) => b.name.localeCompare(a.name)); // reverse order — latest first
|
||||
|
||||
for (const entry of entries) {
|
||||
const summaryPath = join(slicesDir, entry.name, `${entry.name}-SUMMARY.md`);
|
||||
if (existsSync(summaryPath)) return entry.name;
|
||||
}
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -92,7 +102,7 @@ export async function handleAddTests(
|
|||
const milestoneId = state.activeMilestone.id;
|
||||
|
||||
// Determine target
|
||||
const targetId = args.trim() || findLastCompletedSlice(state);
|
||||
const targetId = args.trim() || findLastCompletedSlice(basePath, milestoneId);
|
||||
if (!targetId) {
|
||||
ctx.ui.notify(
|
||||
"No completed slices found. Specify a slice ID: /gsd add-tests S03",
|
||||
|
|
|
|||
|
|
@ -129,14 +129,12 @@ async function promoteBacklogItem(
|
|||
return;
|
||||
}
|
||||
|
||||
// Promote via add-slice
|
||||
const { handleAddSlice } = await import("./commands-slice-mutation.js");
|
||||
await handleAddSlice(item.title, ctx, pi);
|
||||
|
||||
// Mark as done
|
||||
// Promote — currently requires single-writer engine (not yet available)
|
||||
// Mark as promoted in backlog for now; slice creation will be available with the engine.
|
||||
item.done = true;
|
||||
item.note = `promoted ${new Date().toISOString().slice(0, 10)}`;
|
||||
writeBacklog(basePath, items);
|
||||
ctx.ui.notify(`Promoted ${itemId}: "${item.title}" — add it to the roadmap manually or wait for engine slice commands.`, "info");
|
||||
}
|
||||
|
||||
async function removeBacklogItem(basePath: string, itemId: string, ctx: ExtensionCommandContext): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ const ROUTES: Route[] = [
|
|||
{ keywords: ["export", "report", "share results"], command: "export" },
|
||||
{ keywords: ["ship", "pull request", "create pr", "open pr", "merge"], command: "ship" },
|
||||
{ keywords: ["discuss", "talk about", "architecture", "design"], command: "discuss" },
|
||||
{ keywords: ["add slice", "new slice", "add scope", "expand scope"], command: "add-slice" },
|
||||
{ keywords: ["remove slice", "delete slice", "drop slice"], command: "remove-slice" },
|
||||
{ keywords: ["undo", "revert", "rollback", "take back"], command: "undo" },
|
||||
{ keywords: ["skip", "skip task", "skip this"], command: "skip" },
|
||||
{ keywords: ["queue", "reorder", "milestone order", "order milestones"], command: "queue" },
|
||||
|
|
|
|||
|
|
@ -22,20 +22,21 @@ function formatSessionReport(units: UnitMetrics[]): string {
|
|||
const lines: string[] = [];
|
||||
lines.push("╭─ Session Report ──────────────────────────────────────╮");
|
||||
|
||||
if (totals.totalDuration > 0) {
|
||||
lines.push(`│ Duration: ${formatDuration(totals.totalDuration).padEnd(40)}│`);
|
||||
if (totals.duration > 0) {
|
||||
lines.push(`│ Duration: ${formatDuration(totals.duration).padEnd(40)}│`);
|
||||
}
|
||||
lines.push(`│ Units: ${String(units.length).padEnd(40)}│`);
|
||||
lines.push(`│ Cost: ${formatCost(totals.totalCost).padEnd(40)}│`);
|
||||
lines.push(`│ Tokens: ${`${formatTokenCount(totals.totalInput)} in / ${formatTokenCount(totals.totalOutput)} out`.padEnd(40)}│`);
|
||||
lines.push(`│ Cost: ${formatCost(totals.cost).padEnd(40)}│`);
|
||||
lines.push(`│ Tokens: ${`${formatTokenCount(totals.tokens.input)} in / ${formatTokenCount(totals.tokens.output)} out`.padEnd(40)}│`);
|
||||
lines.push("│ │");
|
||||
|
||||
// Work completed
|
||||
if (units.length > 0) {
|
||||
lines.push("│ Work Completed: │");
|
||||
for (const unit of units) {
|
||||
const status = unit.status === "completed" ? "✓" : unit.status === "skipped" ? "⊘" : "•";
|
||||
const label = ` ${status} ${unit.unitId ?? "unknown"}`;
|
||||
const finished = unit.finishedAt > 0;
|
||||
const status = finished ? "✓" : "•";
|
||||
const label = ` ${status} ${unit.id ?? "unknown"}`;
|
||||
lines.push(`│ ${label.padEnd(53)}│`);
|
||||
}
|
||||
lines.push("│ │");
|
||||
|
|
@ -45,7 +46,7 @@ function formatSessionReport(units: UnitMetrics[]): string {
|
|||
if (byModel.length > 0) {
|
||||
lines.push("│ Model Usage: │");
|
||||
for (const m of byModel) {
|
||||
const label = ` ${m.model}: ${m.count} units (${formatCost(m.cost)})`;
|
||||
const label = ` ${m.model}: ${m.units} units (${formatCost(m.cost)})`;
|
||||
lines.push(`│ ${label.padEnd(53)}│`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,13 +85,13 @@ function generatePRContent(basePath: string, milestoneId: string, milestoneTitle
|
|||
const byModel = aggregateByModel(units);
|
||||
sections.push("## Metrics\n");
|
||||
sections.push(`- **Units executed:** ${units.length}`);
|
||||
sections.push(`- **Total cost:** ${formatCost(totals.totalCost)}`);
|
||||
sections.push(`- **Tokens:** ${formatTokenCount(totals.totalInput)} input / ${formatTokenCount(totals.totalOutput)} output`);
|
||||
if (totals.totalDuration > 0) {
|
||||
sections.push(`- **Duration:** ${formatDuration(totals.totalDuration)}`);
|
||||
sections.push(`- **Total cost:** ${formatCost(totals.cost)}`);
|
||||
sections.push(`- **Tokens:** ${formatTokenCount(totals.tokens.input)} input / ${formatTokenCount(totals.tokens.output)} output`);
|
||||
if (totals.duration > 0) {
|
||||
sections.push(`- **Duration:** ${formatDuration(totals.duration)}`);
|
||||
}
|
||||
if (byModel.length > 0) {
|
||||
sections.push(`- **Models:** ${byModel.map((m) => `${m.model} (${m.count} units)`).join(", ")}`);
|
||||
sections.push(`- **Models:** ${byModel.map((m) => `${m.model} (${m.units} units)`).join(", ")}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
|
@ -134,10 +134,10 @@ export async function handleShip(
|
|||
const milestoneId = state.activeMilestone.id;
|
||||
const milestoneTitle = state.activeMilestone.title ?? "";
|
||||
|
||||
// 2. Check for incomplete work
|
||||
if (state.activeMilestone.phase !== "complete" && !force) {
|
||||
// 2. Check for incomplete work (use GSD phase as proxy — no phase field on ActiveRef)
|
||||
if (state.phase !== "complete" && !force) {
|
||||
ctx.ui.notify(
|
||||
`Milestone ${milestoneId} is not complete (phase: ${state.activeMilestone.phase}). Use --force to ship anyway.`,
|
||||
`Milestone ${milestoneId} may not be complete (phase: ${state.phase}). Use --force to ship anyway.`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,226 +0,0 @@
|
|||
/**
|
||||
* GSD Commands — /gsd add-slice, /gsd insert-slice, /gsd remove-slice
|
||||
*
|
||||
* Thin CLI wrappers around the engine's updateRoadmap() command.
|
||||
* All mutations go through the single-writer WorkflowEngine.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
||||
|
||||
import { deriveState } from "./state.js";
|
||||
import { _getAdapter, isDbAvailable } from "./gsd-db.js";
|
||||
import { updateRoadmap } from "./workflow-commands.js";
|
||||
import { renderAllProjections } from "./workflow-projections.js";
|
||||
|
||||
function parseFlag(args: string, flag: string): string | undefined {
|
||||
const regex = new RegExp(`${flag}\\s+(\\S+)`);
|
||||
const match = args.match(regex);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
function stripFlags(args: string): string {
|
||||
return args
|
||||
.replace(/--\w+\s+\S+/g, "")
|
||||
.replace(/--\w+/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function generateNextSliceId(existingIds: string[]): string {
|
||||
let maxNum = 0;
|
||||
for (const id of existingIds) {
|
||||
const match = id.match(/^S(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxNum) maxNum = num;
|
||||
}
|
||||
}
|
||||
return `S${String(maxNum + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export async function handleAddSlice(
|
||||
args: string,
|
||||
ctx: ExtensionCommandContext,
|
||||
_pi: ExtensionAPI,
|
||||
): Promise<void> {
|
||||
const basePath = process.cwd();
|
||||
const state = await deriveState(basePath);
|
||||
|
||||
if (!state.activeMilestone) {
|
||||
ctx.ui.notify("No active milestone. Create one with /gsd new-milestone first.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseFlag(args, "--id");
|
||||
const risk = parseFlag(args, "--risk") ?? "medium";
|
||||
const dependsStr = parseFlag(args, "--depends");
|
||||
const depends = dependsStr ? dependsStr.split(",").map((d) => d.trim()) : [];
|
||||
const title = stripFlags(args).replace(/^['"]|['"]$/g, "");
|
||||
|
||||
if (!title) {
|
||||
ctx.ui.notify(
|
||||
"Usage: /gsd add-slice [--id S99] [--risk high] [--depends S01,S02] <title>",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const milestoneId = state.activeMilestone.id;
|
||||
|
||||
// Determine slice ID
|
||||
const existingSliceIds = (state.activeMilestone.slices ?? []).map((s: { id: string }) => s.id);
|
||||
const sliceId = id ?? generateNextSliceId(existingSliceIds);
|
||||
|
||||
if (!isDbAvailable()) {
|
||||
ctx.ui.notify("Engine database not available. Run /gsd init first.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = _getAdapter();
|
||||
const result = updateRoadmap(db, {
|
||||
milestoneId,
|
||||
addSlices: [{ id: sliceId, title, risk, depends, demo: "" }],
|
||||
});
|
||||
renderAllProjections(db, basePath, milestoneId);
|
||||
ctx.ui.notify(
|
||||
`Added ${sliceId}: "${title}" (${result.totalSlices} total slices)`,
|
||||
"success",
|
||||
);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
ctx.ui.notify(`Failed to add slice: ${msg}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleInsertSlice(
|
||||
args: string,
|
||||
ctx: ExtensionCommandContext,
|
||||
_pi: ExtensionAPI,
|
||||
): Promise<void> {
|
||||
const basePath = process.cwd();
|
||||
const state = await deriveState(basePath);
|
||||
|
||||
if (!state.activeMilestone) {
|
||||
ctx.ui.notify("No active milestone. Create one with /gsd new-milestone first.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = args.trim().split(/\s+/);
|
||||
const afterId = parts[0];
|
||||
const title = parts.slice(1).join(" ").replace(/^['"]|['"]$/g, "");
|
||||
|
||||
if (!afterId || !title) {
|
||||
ctx.ui.notify(
|
||||
'Usage: /gsd insert-slice <after-slice-id> <title>\nExample: /gsd insert-slice S03 "Auth middleware"',
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const milestoneId = state.activeMilestone.id;
|
||||
const existingSliceIds = (state.activeMilestone.slices ?? []).map((s: { id: string }) => s.id);
|
||||
|
||||
if (!existingSliceIds.includes(afterId)) {
|
||||
ctx.ui.notify(
|
||||
`Slice ${afterId} not found. Available: ${existingSliceIds.join(", ")}`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sliceId = generateNextSliceId(existingSliceIds);
|
||||
|
||||
if (!isDbAvailable()) {
|
||||
ctx.ui.notify("Engine database not available. Run /gsd init first.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = _getAdapter();
|
||||
|
||||
// Add the new slice
|
||||
updateRoadmap(db, {
|
||||
milestoneId,
|
||||
addSlices: [{ id: sliceId, title, risk: "medium", depends: [], demo: "" }],
|
||||
});
|
||||
|
||||
// Reorder: insert after the specified slice
|
||||
const reorder = [...existingSliceIds];
|
||||
const insertIdx = reorder.indexOf(afterId);
|
||||
reorder.splice(insertIdx + 1, 0, sliceId);
|
||||
|
||||
const result = updateRoadmap(db, {
|
||||
milestoneId,
|
||||
reorderSliceIds: reorder,
|
||||
});
|
||||
|
||||
renderAllProjections(db, basePath, milestoneId);
|
||||
ctx.ui.notify(
|
||||
`Inserted ${sliceId}: "${title}" after ${afterId} (${result.totalSlices} total slices)`,
|
||||
"success",
|
||||
);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
ctx.ui.notify(`Failed to insert slice: ${msg}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleRemoveSlice(
|
||||
args: string,
|
||||
ctx: ExtensionCommandContext,
|
||||
_pi: ExtensionAPI,
|
||||
): Promise<void> {
|
||||
const basePath = process.cwd();
|
||||
const state = await deriveState(basePath);
|
||||
|
||||
if (!state.activeMilestone) {
|
||||
ctx.ui.notify("No active milestone.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const force = args.includes("--force");
|
||||
const sliceId = args.replace(/--force/g, "").trim();
|
||||
|
||||
if (!sliceId) {
|
||||
ctx.ui.notify("Usage: /gsd remove-slice <slice-id> [--force]", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const milestoneId = state.activeMilestone.id;
|
||||
|
||||
if (!isDbAvailable()) {
|
||||
ctx.ui.notify("Engine database not available. Run /gsd init first.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = _getAdapter();
|
||||
|
||||
// If force, delete tasks first
|
||||
if (force) {
|
||||
db.prepare("DELETE FROM tasks WHERE milestone_id = ? AND slice_id = ?").run(milestoneId, sliceId);
|
||||
}
|
||||
|
||||
const result = updateRoadmap(db, {
|
||||
milestoneId,
|
||||
removeSliceIds: [sliceId],
|
||||
});
|
||||
|
||||
if (result.removed === 0) {
|
||||
ctx.ui.notify(
|
||||
`Could not remove ${sliceId} — only pending slices can be removed. Use --force to remove slices with tasks.`,
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
renderAllProjections(db, basePath, milestoneId);
|
||||
ctx.ui.notify(
|
||||
`Removed ${sliceId} (${result.totalSlices} slices remaining)`,
|
||||
"success",
|
||||
);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
ctx.ui.notify(`Failed to remove slice: ${msg}`, "error");
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export interface GsdCommandDefinition {
|
|||
type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
|
||||
|
||||
export const GSD_COMMAND_DESCRIPTION =
|
||||
"GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase|notifications|ship|add-slice|insert-slice|remove-slice|do|session-report|backlog|pr-branch|add-tests";
|
||||
"GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|model|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests";
|
||||
|
||||
export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
|
||||
{ cmd: "help", desc: "Categorized command reference with descriptions" },
|
||||
|
|
@ -75,9 +75,6 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
|
|||
{ cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
|
||||
{ cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
|
||||
{ cmd: "ship", desc: "Create PR from milestone artifacts and open for review" },
|
||||
{ cmd: "add-slice", desc: "Append a new slice to the active milestone's roadmap" },
|
||||
{ cmd: "insert-slice", desc: "Insert a slice after a specific position in the roadmap" },
|
||||
{ cmd: "remove-slice", desc: "Remove a pending slice from the roadmap" },
|
||||
{ cmd: "do", desc: "Route freeform text to the right GSD command" },
|
||||
{ cmd: "session-report", desc: "Session cost, tokens, and work summary" },
|
||||
{ cmd: "backlog", desc: "Manage backlog items (add, promote, remove, list)" },
|
||||
|
|
@ -259,14 +256,6 @@ const NESTED_COMPLETIONS: CompletionMap = {
|
|||
{ cmd: "--base", desc: "Override target branch (default: main)" },
|
||||
{ cmd: "--force", desc: "Ship even with pending tasks" },
|
||||
],
|
||||
"add-slice": [
|
||||
{ cmd: "--id", desc: "Explicit slice ID (default: auto-generated)" },
|
||||
{ cmd: "--risk", desc: "Risk level: low, medium, high (default: medium)" },
|
||||
{ cmd: "--depends", desc: "Comma-separated dependency slice IDs" },
|
||||
],
|
||||
"remove-slice": [
|
||||
{ cmd: "--force", desc: "Remove even if slice has tasks (deletes them)" },
|
||||
],
|
||||
"session-report": [
|
||||
{ cmd: "--json", desc: "Machine-readable JSON output" },
|
||||
{ cmd: "--save", desc: "Save report to .gsd/reports/" },
|
||||
|
|
|
|||
|
|
@ -227,22 +227,6 @@ export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionComma
|
|||
await handleDo(trimmed.replace(/^do\s*/, "").trim(), ctx, pi);
|
||||
return true;
|
||||
}
|
||||
// ── Slice mutation commands ──
|
||||
if (trimmed === "add-slice" || trimmed.startsWith("add-slice ")) {
|
||||
const { handleAddSlice } = await import("../../commands-slice-mutation.js");
|
||||
await handleAddSlice(trimmed.replace(/^add-slice\s*/, "").trim(), ctx, pi);
|
||||
return true;
|
||||
}
|
||||
if (trimmed === "insert-slice" || trimmed.startsWith("insert-slice ")) {
|
||||
const { handleInsertSlice } = await import("../../commands-slice-mutation.js");
|
||||
await handleInsertSlice(trimmed.replace(/^insert-slice\s*/, "").trim(), ctx, pi);
|
||||
return true;
|
||||
}
|
||||
if (trimmed === "remove-slice" || trimmed.startsWith("remove-slice ")) {
|
||||
const { handleRemoveSlice } = await import("../../commands-slice-mutation.js");
|
||||
await handleRemoveSlice(trimmed.replace(/^remove-slice\s*/, "").trim(), ctx, pi);
|
||||
return true;
|
||||
}
|
||||
// ── Backlog management ──
|
||||
if (trimmed === "backlog" || trimmed.startsWith("backlog ")) {
|
||||
const { handleBacklog } = await import("../../commands-backlog.js");
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ const ROUTES: Route[] = [
|
|||
{ keywords: ["clean up", "cleanup", "remove old", "prune", "tidy"], command: "cleanup" },
|
||||
{ keywords: ["ship", "pull request", "create pr", "open pr", "merge"], command: "ship" },
|
||||
{ keywords: ["discuss", "talk about", "architecture", "design"], command: "discuss" },
|
||||
{ keywords: ["add slice", "new slice", "add scope", "expand scope"], command: "add-slice" },
|
||||
{ keywords: ["undo", "revert", "rollback", "take back"], command: "undo" },
|
||||
{ keywords: ["skip", "skip task", "skip this"], command: "skip" },
|
||||
{ keywords: ["visualize", "viz", "graph", "chart", "show graph"], command: "visualize" },
|
||||
|
|
@ -126,9 +125,3 @@ test("/gsd do: routes 'session report' to session-report", () => {
|
|||
assert.ok(match);
|
||||
assert.equal(match.command, "session-report");
|
||||
});
|
||||
|
||||
test("/gsd do: routes 'add new slice' to add-slice", () => {
|
||||
const match = matchRoute("add new slice for authentication");
|
||||
assert.ok(match);
|
||||
assert.equal(match.command, "add-slice");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Test the argument parsing logic used by slice mutation commands.
|
||||
// Full integration tests require DB + engine runtime, so we test
|
||||
// the parsing and ID generation utilities directly.
|
||||
|
||||
// ─── Utilities from commands-slice-mutation.ts ──────────────────────────
|
||||
|
||||
function parseFlag(args: string, flag: string): string | undefined {
|
||||
const regex = new RegExp(`${flag}\\s+(\\S+)`);
|
||||
const match = args.match(regex);
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
function stripFlags(args: string): string {
|
||||
return args
|
||||
.replace(/--\w+\s+\S+/g, "")
|
||||
.replace(/--\w+/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function generateNextSliceId(existingIds: string[]): string {
|
||||
let maxNum = 0;
|
||||
for (const id of existingIds) {
|
||||
const match = id.match(/^S(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > maxNum) maxNum = num;
|
||||
}
|
||||
}
|
||||
return `S${String(maxNum + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
test("add-slice: parse --id flag", () => {
|
||||
assert.equal(parseFlag("--id S99 My title", "--id"), "S99");
|
||||
assert.equal(parseFlag("My title --id S05", "--id"), "S05");
|
||||
assert.equal(parseFlag("My title", "--id"), undefined);
|
||||
});
|
||||
|
||||
test("add-slice: parse --risk flag", () => {
|
||||
assert.equal(parseFlag("--risk high My title", "--risk"), "high");
|
||||
assert.equal(parseFlag("My title", "--risk"), undefined);
|
||||
});
|
||||
|
||||
test("add-slice: parse --depends flag", () => {
|
||||
assert.equal(parseFlag("--depends S01,S02 My title", "--depends"), "S01,S02");
|
||||
const deps = parseFlag("--depends S01,S02 My title", "--depends")?.split(",");
|
||||
assert.deepEqual(deps, ["S01", "S02"]);
|
||||
});
|
||||
|
||||
test("add-slice: strip flags leaves title", () => {
|
||||
assert.equal(stripFlags("--id S99 --risk high My new slice"), "My new slice");
|
||||
assert.equal(stripFlags("Simple title"), "Simple title");
|
||||
assert.equal(stripFlags("--depends S01,S02 --risk low Auth middleware"), "Auth middleware");
|
||||
});
|
||||
|
||||
test("add-slice: empty after stripping flags", () => {
|
||||
assert.equal(stripFlags("--id S99 --risk high"), "");
|
||||
});
|
||||
|
||||
test("add-slice: generate next slice ID from empty", () => {
|
||||
assert.equal(generateNextSliceId([]), "S01");
|
||||
});
|
||||
|
||||
test("add-slice: generate next slice ID increments", () => {
|
||||
assert.equal(generateNextSliceId(["S01", "S02", "S03"]), "S04");
|
||||
});
|
||||
|
||||
test("add-slice: generate next slice ID handles gaps", () => {
|
||||
assert.equal(generateNextSliceId(["S01", "S05", "S03"]), "S06");
|
||||
});
|
||||
|
||||
test("add-slice: generate next slice ID pads to 2 digits", () => {
|
||||
assert.equal(generateNextSliceId(["S09"]), "S10");
|
||||
assert.equal(generateNextSliceId(["S01"]), "S02");
|
||||
});
|
||||
|
||||
test("remove-slice: parse --force flag", () => {
|
||||
const args1 = "S05 --force";
|
||||
const args2 = "S05";
|
||||
|
||||
assert.ok(args1.includes("--force"));
|
||||
assert.ok(!args2.includes("--force"));
|
||||
|
||||
assert.equal(args1.replace(/--force/g, "").trim(), "S05");
|
||||
assert.equal(args2.replace(/--force/g, "").trim(), "S05");
|
||||
});
|
||||
|
||||
test("insert-slice: parse after-id and title", () => {
|
||||
const args = "S03 Auth middleware";
|
||||
const parts = args.trim().split(/\s+/);
|
||||
const afterId = parts[0];
|
||||
const title = parts.slice(1).join(" ");
|
||||
|
||||
assert.equal(afterId, "S03");
|
||||
assert.equal(title, "Auth middleware");
|
||||
});
|
||||
|
||||
test("insert-slice: quoted title", () => {
|
||||
const args = 'S03 "Auth middleware with OAuth"';
|
||||
const parts = args.trim().split(/\s+/);
|
||||
const afterId = parts[0];
|
||||
const title = parts.slice(1).join(" ").replace(/^['"]|['"]$/g, "");
|
||||
|
||||
assert.equal(afterId, "S03");
|
||||
assert.equal(title, "Auth middleware with OAuth");
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue