diff --git a/src/resources/extensions/gsd/commands-add-tests.ts b/src/resources/extensions/gsd/commands-add-tests.ts index a88a9b069..c69d9c176 100644 --- a/src/resources/extensions/gsd/commands-add-tests.ts +++ b/src/resources/extensions/gsd/commands-add-tests.ts @@ -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", diff --git a/src/resources/extensions/gsd/commands-backlog.ts b/src/resources/extensions/gsd/commands-backlog.ts index a72bee638..4241724eb 100644 --- a/src/resources/extensions/gsd/commands-backlog.ts +++ b/src/resources/extensions/gsd/commands-backlog.ts @@ -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 { diff --git a/src/resources/extensions/gsd/commands-do.ts b/src/resources/extensions/gsd/commands-do.ts index cdc04243b..af2a20f38 100644 --- a/src/resources/extensions/gsd/commands-do.ts +++ b/src/resources/extensions/gsd/commands-do.ts @@ -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" }, diff --git a/src/resources/extensions/gsd/commands-session-report.ts b/src/resources/extensions/gsd/commands-session-report.ts index 9994d40f4..211315057 100644 --- a/src/resources/extensions/gsd/commands-session-report.ts +++ b/src/resources/extensions/gsd/commands-session-report.ts @@ -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)}│`); } } diff --git a/src/resources/extensions/gsd/commands-ship.ts b/src/resources/extensions/gsd/commands-ship.ts index a289484ec..fe2fd0f1f 100644 --- a/src/resources/extensions/gsd/commands-ship.ts +++ b/src/resources/extensions/gsd/commands-ship.ts @@ -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; diff --git a/src/resources/extensions/gsd/commands-slice-mutation.ts b/src/resources/extensions/gsd/commands-slice-mutation.ts deleted file mode 100644 index 28b8b0f28..000000000 --- a/src/resources/extensions/gsd/commands-slice-mutation.ts +++ /dev/null @@ -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 { - 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] ", - "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"); - } -} diff --git a/src/resources/extensions/gsd/commands/catalog.ts b/src/resources/extensions/gsd/commands/catalog.ts index 08e75370b..8a7095170 100644 --- a/src/resources/extensions/gsd/commands/catalog.ts +++ b/src/resources/extensions/gsd/commands/catalog.ts @@ -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/" }, diff --git a/src/resources/extensions/gsd/commands/handlers/workflow.ts b/src/resources/extensions/gsd/commands/handlers/workflow.ts index 10eb14d28..b5aa3fc1c 100644 --- a/src/resources/extensions/gsd/commands/handlers/workflow.ts +++ b/src/resources/extensions/gsd/commands/handlers/workflow.ts @@ -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"); diff --git a/src/resources/extensions/gsd/tests/commands-do.test.ts b/src/resources/extensions/gsd/tests/commands-do.test.ts index fea754c34..be8ec0df4 100644 --- a/src/resources/extensions/gsd/tests/commands-do.test.ts +++ b/src/resources/extensions/gsd/tests/commands-do.test.ts @@ -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"); -}); diff --git a/src/resources/extensions/gsd/tests/commands-slice-mutation.test.ts b/src/resources/extensions/gsd/tests/commands-slice-mutation.test.ts deleted file mode 100644 index d8af6fae1..000000000 --- a/src/resources/extensions/gsd/tests/commands-slice-mutation.test.ts +++ /dev/null @@ -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"); -});