fix: resume selection under-reports task totals with multiple loops
The resume prompt counted total/completed from progress.tasks, which only records TOUCHED tasks (started/completed/failed) — never-started tasks were silently missing, and file-checkbox completions weren't counted unless markCompleted had run. With e.g. 10 tasks where 3 completed and 5 untouched, it showed 3/5 done instead of 3/10 done. - add countPRDResumeStats (src/utils.ts): total from parseTaskFile of the PRD source; completed = progress completions ∪ PRD checkbox completions, deduped by task id; failed from progress. Falls back to touched-task counts when the source file is missing/unparseable. - selectPRDToResume (index.ts) uses the helper instead of Object.keys(). - tests/resume-stats.test.ts: 4 regression tests. Includes pre-existing uncommitted changes: index.ts tab-reformat (matches src/ style) and package.json version bump 0.4.2 -> 0.4.3.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mikefreno/ralpi",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"description": "Execute tasks from task files/PRD's using DAG-based dependency resolution with persistent progress tracking",
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
|
||||
44
src/utils.ts
44
src/utils.ts
@@ -7,6 +7,7 @@ import type {
|
||||
ToolUsage,
|
||||
} from "./types";
|
||||
import { DEFAULT_CONFIG } from "./types";
|
||||
import { parseTaskFile } from "./parser";
|
||||
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
createAgentSession,
|
||||
@@ -198,6 +199,49 @@ export function listPRDsSorted(
|
||||
return entries;
|
||||
}
|
||||
|
||||
export interface PRDResumeSummary {
|
||||
total: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count tasks for the resume-selection display.
|
||||
*
|
||||
* The progress tracker only records tasks that were TOUCHED (started,
|
||||
* completed, or failed) — never-started tasks are absent from `prd.tasks`,
|
||||
* so a naive Object.keys(prd.tasks).length under-reports the real total.
|
||||
* The true total comes from parsing the PRD source file. Completed counts
|
||||
* both progress-marked completions and PRD checkbox completions (a task
|
||||
* checked off in the file is done even if the loop was interrupted before
|
||||
* markCompleted), deduped by task id. Falls back to touched-task counts
|
||||
* when the source file is missing or unparseable.
|
||||
*/
|
||||
export function countPRDResumeStats(
|
||||
prd: PRDProgress,
|
||||
sourcePath: string,
|
||||
): PRDResumeSummary {
|
||||
const touched = Object.entries(prd.tasks);
|
||||
const failed = touched.filter(([, t]) => t.status === "failed").length;
|
||||
const completedIds = new Set(
|
||||
touched.filter(([, t]) => t.status === "completed").map(([id]) => id),
|
||||
);
|
||||
|
||||
let total: number;
|
||||
try {
|
||||
const project = parseTaskFile(sourcePath);
|
||||
total = project.tasks.length;
|
||||
for (const task of project.tasks) {
|
||||
if (task.status === "completed") completedIds.add(task.id);
|
||||
}
|
||||
} catch {
|
||||
// PRD file missing/unparseable — fall back to touched-task counts
|
||||
total = touched.length;
|
||||
}
|
||||
|
||||
return { total, completed: completedIds.size, failed };
|
||||
}
|
||||
|
||||
// ─── Model Resolution ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
87
tests/resume-stats.test.ts
Normal file
87
tests/resume-stats.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { describe, it, expect, beforeEach } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { ProgressTracker } from "../src/progress";
|
||||
import { countPRDResumeStats } from "../src/utils";
|
||||
|
||||
/**
|
||||
* Regression test: the resume-selection prompt under-reported task totals
|
||||
* when multiple loop histories existed. The progress tracker only records
|
||||
* TOUCHED tasks (started/completed/failed) — never-started tasks are absent
|
||||
* from prd.tasks, so a naive Object.keys() count missed them entirely, and
|
||||
* file-checked completions were ignored unless markCompleted had run.
|
||||
* countPRDResumeStats derives the true total from the parsed PRD file and
|
||||
* counts checkbox completions too.
|
||||
*/
|
||||
let root: string;
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), "ralpi-stats-test-"));
|
||||
});
|
||||
|
||||
const PRD_CONTENT = `# Test PRD
|
||||
|
||||
## Tasks
|
||||
- [ ] Task one
|
||||
- [x] Task two
|
||||
- [ ] Task three
|
||||
- [ ] Task four
|
||||
`;
|
||||
|
||||
function writePRD(rel: string): string {
|
||||
const p = path.join(root, rel);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, PRD_CONTENT, "utf-8");
|
||||
return p;
|
||||
}
|
||||
|
||||
describe("countPRDResumeStats", () => {
|
||||
it("reports the full task total from the PRD file, not just touched tasks", () => {
|
||||
const sourcePath = writePRD("tasks/a/README.md");
|
||||
const progress = new ProgressTracker(root, sourcePath);
|
||||
|
||||
// Simple-checkbox format assigns sequential ids 00-03. Only 00
|
||||
// (completed) and 02 (failed) were touched by the loop; 01 is checked
|
||||
// off in the file; 03 was never started.
|
||||
progress.markCompleted("00", 1000);
|
||||
progress.markFailed("02", "boom");
|
||||
|
||||
const stats = countPRDResumeStats(progress.getState(), sourcePath);
|
||||
expect(stats.total).toBe(4); // old code reported 2
|
||||
expect(stats.completed).toBe(2); // 00 via progress + 01 via checkbox
|
||||
expect(stats.failed).toBe(1);
|
||||
});
|
||||
|
||||
it("does not double-count a task that is both progress-completed and file-checked", () => {
|
||||
const sourcePath = writePRD("tasks/b/README.md");
|
||||
const progress = new ProgressTracker(root, sourcePath);
|
||||
|
||||
progress.markCompleted("01", 500); // 01 already [x] in the file
|
||||
|
||||
const stats = countPRDResumeStats(progress.getState(), sourcePath);
|
||||
expect(stats.completed).toBe(1);
|
||||
});
|
||||
|
||||
it("falls back to touched-task counts when the PRD file is missing", () => {
|
||||
const missing = path.join(root, "tasks/gone/README.md");
|
||||
const progress = new ProgressTracker(root, missing);
|
||||
progress.markCompleted("01", 1000);
|
||||
progress.markFailed("02", "nope");
|
||||
|
||||
const stats = countPRDResumeStats(progress.getState(), missing);
|
||||
expect(stats.total).toBe(2);
|
||||
expect(stats.completed).toBe(1);
|
||||
expect(stats.failed).toBe(1);
|
||||
});
|
||||
|
||||
it("reports zero for a never-touched PRD with no file", () => {
|
||||
const missing = path.join(root, "tasks/none/README.md");
|
||||
const progress = new ProgressTracker(root, missing);
|
||||
const stats = countPRDResumeStats(progress.getState(), missing);
|
||||
expect(stats.total).toBe(0);
|
||||
expect(stats.completed).toBe(0);
|
||||
expect(stats.failed).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user