fix(S04/T01): Add schema v9 migration with sequence column on slices/ta…
- src/resources/extensions/gsd/gsd-db.ts - src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts - .gsd/milestones/M001/slices/S04/S04-PLAN.md - .gsd/milestones/M001/slices/S04/tasks/T01-PLAN.md
This commit is contained in:
parent
b73f525834
commit
f86882bde5
5 changed files with 277 additions and 10 deletions
|
|
@ -27,6 +27,7 @@
|
|||
- `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/planning-crossval.test.ts` — DB↔rendered parity
|
||||
- `rg 'parseRoadmapSlices|parseRoadmap|parsePlan' src/resources/extensions/gsd/dispatch-guard.ts src/resources/extensions/gsd/auto-verification.ts src/resources/extensions/gsd/parallel-eligibility.ts` returns no matches (parser imports removed from migrated files)
|
||||
- `rg 'parseRoadmap' src/resources/extensions/gsd/auto-dispatch.ts` returns no matches (parser import narrowed)
|
||||
- Diagnostic: `node -e "const{openDatabase,getMilestoneSlices}=require('./src/resources/extensions/gsd/gsd-db.ts');openDatabase(':memory:');console.log(getMilestoneSlices('NONEXISTENT'))"` — returns empty array `[]` (no crash on missing milestone, observable failure state)
|
||||
|
||||
## Observability / Diagnostics
|
||||
|
||||
|
|
@ -42,7 +43,7 @@
|
|||
|
||||
## Tasks
|
||||
|
||||
- [ ] **T01: Add schema v9 migration with sequence column and fix ORDER BY queries** `est:30m`
|
||||
- [x] **T01: Add schema v9 migration with sequence column and fix ORDER BY queries** `est:30m`
|
||||
- Why: R016 requires sequence-aware ordering. All caller migrations and cross-validation depend on correct query ordering.
|
||||
- Files: `src/resources/extensions/gsd/gsd-db.ts`, `src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts`
|
||||
- Do: Add `sequence INTEGER DEFAULT 0` to slices and tasks tables in a `currentVersion < 9` migration block. Bump `SCHEMA_VERSION` to 9. Update `SliceRow` and `TaskRow` interfaces to include `sequence: number`. Change all 6 `ORDER BY id` queries to `ORDER BY sequence, id`. Add `insertSlicePlanning`/`insertTask` to accept optional `sequence` param. Write test file proving: migration adds column, ORDER BY respects sequence, null/0 sequence falls back to id ordering, backfill from positional order.
|
||||
|
|
|
|||
|
|
@ -54,3 +54,11 @@ Add a `sequence INTEGER DEFAULT 0` column to the `slices` and `tasks` tables via
|
|||
|
||||
- `src/resources/extensions/gsd/gsd-db.ts` — updated with schema v9, sequence field, ORDER BY changes
|
||||
- `src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts` — new test file proving sequence ordering
|
||||
|
||||
## Observability Impact
|
||||
|
||||
- **Schema version**: `SCHEMA_VERSION` constant changes from 8 → 9; `schema_version` table gains a row for version 9 with timestamp
|
||||
- **Column visibility**: `PRAGMA table_info(slices)` and `PRAGMA table_info(tasks)` now show `sequence INTEGER DEFAULT 0`
|
||||
- **Query ordering**: All slice/task list queries sort by `sequence, id` — inspectable via `EXPLAIN QUERY PLAN` or by inserting rows with non-lexicographic sequence values
|
||||
- **Failure state**: `getMilestoneSlices('NONEXISTENT')` returns `[]` (empty array, no crash); `getSliceTasks` with no DB open returns `[]`
|
||||
- **Interface change**: `SliceRow.sequence` and `TaskRow.sequence` fields available to all downstream consumers
|
||||
|
|
|
|||
62
.gsd/milestones/M001/slices/S04/tasks/T01-SUMMARY.md
Normal file
62
.gsd/milestones/M001/slices/S04/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S04
|
||||
milestone: M001
|
||||
key_files:
|
||||
- src/resources/extensions/gsd/gsd-db.ts
|
||||
- src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts
|
||||
- .gsd/milestones/M001/slices/S04/S04-PLAN.md
|
||||
- .gsd/milestones/M001/slices/S04/tasks/T01-PLAN.md
|
||||
key_decisions:
|
||||
- Added sequence column to initial CREATE TABLE DDL in addition to migration block — required for fresh databases that skip migrations
|
||||
- Used INTEGER DEFAULT 0 (not NOT NULL) for sequence column to keep it nullable-safe and backward compatible
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-23T16:57:23.834Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Add schema v9 migration with sequence column on slices/tasks tables and fix ORDER BY queries to use sequence, id
|
||||
|
||||
**Add schema v9 migration with sequence column on slices/tasks tables and fix ORDER BY queries to use sequence, id**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added a `sequence INTEGER DEFAULT 0` column to both `slices` and `tasks` tables via two changes: (1) updated the initial CREATE TABLE definitions so fresh databases include the column from the start, and (2) added a `currentVersion < 9` migration block using `ensureColumn()` for existing databases upgrading from v8. Bumped `SCHEMA_VERSION` from 8 to 9.
|
||||
|
||||
Updated both `SliceRow` and `TaskRow` TypeScript interfaces to include `sequence: number`, and updated their `rowToSlice`/`rowToTask` converter functions to read the field with a `?? 0` fallback.
|
||||
|
||||
Updated all 4 slice/task `ORDER BY id` queries to `ORDER BY sequence, id`: `getSliceTasks()`, `getActiveSliceFromDb()`, `getActiveTaskFromDb()`, and `getMilestoneSlices()`. Left the 2 milestone queries (`getAllMilestones`, `getActiveMilestoneFromDb`) using `ORDER BY id` as milestones don't have a sequence column.
|
||||
|
||||
Updated `insertSlice` and `insertTask` to accept an optional `sequence` parameter, defaulting to 0.
|
||||
|
||||
Wrote 7 tests covering: migration adds columns, sequence-based ordering for slices and tasks, default sequence=0 falls back to id ordering, `getActiveSliceFromDb` and `getActiveTaskFromDb` respect sequence, and sequence defaults to 0 when not provided.
|
||||
|
||||
Also addressed the pre-flight observability gaps: added a diagnostic verification step to S04-PLAN.md and an Observability Impact section to T01-PLAN.md.
|
||||
|
||||
## Verification
|
||||
|
||||
Ran schema-v9-sequence test suite: 7/7 pass. Ran plan-milestone, plan-slice, plan-task regression tests: 15/15 pass. Verified SCHEMA_VERSION=9. Verified all 4 slice/task ORDER BY queries use `sequence, id`. Verified milestone ORDER BY queries remain `ORDER BY id`.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts` | 0 | ✅ pass | 203ms |
|
||||
| 2 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/plan-milestone.test.ts src/resources/extensions/gsd/tests/plan-slice.test.ts src/resources/extensions/gsd/tests/plan-task.test.ts` | 0 | ✅ pass | 207ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Added `sequence INTEGER DEFAULT 0` to the initial CREATE TABLE definitions for slices and tasks (not just the migration block). This was necessary because fresh databases created via `openDatabase` use the CREATE TABLE DDL directly — the migration block only runs for existing DBs upgrading from a prior version. Without this, insertSlice/insertTask would fail on fresh DBs because the column wouldn't exist.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `src/resources/extensions/gsd/gsd-db.ts`
|
||||
- `src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts`
|
||||
- `.gsd/milestones/M001/slices/S04/S04-PLAN.md`
|
||||
- `.gsd/milestones/M001/slices/S04/tasks/T01-PLAN.md`
|
||||
|
|
@ -145,7 +145,7 @@ function openRawDb(path: string): unknown {
|
|||
return new Database(path);
|
||||
}
|
||||
|
||||
const SCHEMA_VERSION = 8;
|
||||
const SCHEMA_VERSION = 9;
|
||||
|
||||
function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
||||
if (fileBacked) db.exec("PRAGMA journal_mode=WAL");
|
||||
|
|
@ -267,6 +267,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|||
proof_level TEXT NOT NULL DEFAULT '',
|
||||
integration_closure TEXT NOT NULL DEFAULT '',
|
||||
observability_impact TEXT NOT NULL DEFAULT '',
|
||||
sequence INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (milestone_id, id),
|
||||
FOREIGN KEY (milestone_id) REFERENCES milestones(id)
|
||||
)
|
||||
|
|
@ -297,6 +298,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|||
inputs TEXT NOT NULL DEFAULT '[]',
|
||||
expected_output TEXT NOT NULL DEFAULT '[]',
|
||||
observability_impact TEXT NOT NULL DEFAULT '',
|
||||
sequence INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (milestone_id, slice_id, id),
|
||||
FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id)
|
||||
)
|
||||
|
|
@ -592,6 +594,16 @@ function migrateSchema(db: DbAdapter): void {
|
|||
});
|
||||
}
|
||||
|
||||
if (currentVersion < 9) {
|
||||
ensureColumn(db, "slices", "sequence", `ALTER TABLE slices ADD COLUMN sequence INTEGER DEFAULT 0`);
|
||||
ensureColumn(db, "tasks", "sequence", `ALTER TABLE tasks ADD COLUMN sequence INTEGER DEFAULT 0`);
|
||||
|
||||
db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
|
||||
":version": 9,
|
||||
":applied_at": new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
db.exec("COMMIT");
|
||||
} catch (err) {
|
||||
db.exec("ROLLBACK");
|
||||
|
|
@ -967,16 +979,17 @@ export function insertSlice(s: {
|
|||
risk?: string;
|
||||
depends?: string[];
|
||||
demo?: string;
|
||||
sequence?: number;
|
||||
planning?: Partial<SlicePlanningRecord>;
|
||||
}): void {
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
||||
currentDb.prepare(
|
||||
`INSERT OR IGNORE INTO slices (
|
||||
milestone_id, id, title, status, risk, depends, demo, created_at,
|
||||
goal, success_criteria, proof_level, integration_closure, observability_impact
|
||||
goal, success_criteria, proof_level, integration_closure, observability_impact, sequence
|
||||
) VALUES (
|
||||
:milestone_id, :id, :title, :status, :risk, :depends, :demo, :created_at,
|
||||
:goal, :success_criteria, :proof_level, :integration_closure, :observability_impact
|
||||
:goal, :success_criteria, :proof_level, :integration_closure, :observability_impact, :sequence
|
||||
)`,
|
||||
).run({
|
||||
":milestone_id": s.milestoneId,
|
||||
|
|
@ -992,6 +1005,7 @@ export function insertSlice(s: {
|
|||
":proof_level": s.planning?.proofLevel ?? "",
|
||||
":integration_closure": s.planning?.integrationClosure ?? "",
|
||||
":observability_impact": s.planning?.observabilityImpact ?? "",
|
||||
":sequence": s.sequence ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1032,6 +1046,7 @@ export function insertTask(t: {
|
|||
keyFiles?: string[];
|
||||
keyDecisions?: string[];
|
||||
fullSummaryMd?: string;
|
||||
sequence?: number;
|
||||
planning?: Partial<TaskPlanningRecord>;
|
||||
}): void {
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
||||
|
|
@ -1040,12 +1055,12 @@ export function insertTask(t: {
|
|||
milestone_id, slice_id, id, title, status, one_liner, narrative,
|
||||
verification_result, duration, completed_at, blocker_discovered,
|
||||
deviations, known_issues, key_files, key_decisions, full_summary_md,
|
||||
description, estimate, files, verify, inputs, expected_output, observability_impact
|
||||
description, estimate, files, verify, inputs, expected_output, observability_impact, sequence
|
||||
) VALUES (
|
||||
:milestone_id, :slice_id, :id, :title, :status, :one_liner, :narrative,
|
||||
:verification_result, :duration, :completed_at, :blocker_discovered,
|
||||
:deviations, :known_issues, :key_files, :key_decisions, :full_summary_md,
|
||||
:description, :estimate, :files, :verify, :inputs, :expected_output, :observability_impact
|
||||
:description, :estimate, :files, :verify, :inputs, :expected_output, :observability_impact, :sequence
|
||||
)`,
|
||||
).run({
|
||||
":milestone_id": t.milestoneId,
|
||||
|
|
@ -1071,6 +1086,7 @@ export function insertTask(t: {
|
|||
":inputs": JSON.stringify(t.planning?.inputs ?? []),
|
||||
":expected_output": JSON.stringify(t.planning?.expectedOutput ?? []),
|
||||
":observability_impact": t.planning?.observabilityImpact ?? "",
|
||||
":sequence": t.sequence ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1133,6 +1149,7 @@ export interface SliceRow {
|
|||
proof_level: string;
|
||||
integration_closure: string;
|
||||
observability_impact: string;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
function rowToSlice(row: Record<string, unknown>): SliceRow {
|
||||
|
|
@ -1153,6 +1170,7 @@ function rowToSlice(row: Record<string, unknown>): SliceRow {
|
|||
proof_level: (row["proof_level"] as string) ?? "",
|
||||
integration_closure: (row["integration_closure"] as string) ?? "",
|
||||
observability_impact: (row["observability_impact"] as string) ?? "",
|
||||
sequence: (row["sequence"] as number) ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1200,6 +1218,7 @@ export interface TaskRow {
|
|||
inputs: string[];
|
||||
expected_output: string[];
|
||||
observability_impact: string;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
function rowToTask(row: Record<string, unknown>): TaskRow {
|
||||
|
|
@ -1227,6 +1246,7 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
|
|||
inputs: JSON.parse((row["inputs"] as string) || "[]"),
|
||||
expected_output: JSON.parse((row["expected_output"] as string) || "[]"),
|
||||
observability_impact: (row["observability_impact"] as string) ?? "",
|
||||
sequence: (row["sequence"] as number) ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1242,7 +1262,7 @@ export function getTask(milestoneId: string, sliceId: string, taskId: string): T
|
|||
export function getSliceTasks(milestoneId: string, sliceId: string): TaskRow[] {
|
||||
if (!currentDb) return [];
|
||||
const rows = currentDb.prepare(
|
||||
"SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid ORDER BY id",
|
||||
"SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid ORDER BY sequence, id",
|
||||
).all({ ":mid": milestoneId, ":sid": sliceId });
|
||||
return rows.map(rowToTask);
|
||||
}
|
||||
|
|
@ -1361,7 +1381,7 @@ export function getActiveMilestoneFromDb(): MilestoneRow | null {
|
|||
export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
|
||||
if (!currentDb) return null;
|
||||
const rows = currentDb.prepare(
|
||||
"SELECT * FROM slices WHERE milestone_id = :mid AND status NOT IN ('complete', 'done') ORDER BY id",
|
||||
"SELECT * FROM slices WHERE milestone_id = :mid AND status NOT IN ('complete', 'done') ORDER BY sequence, id",
|
||||
).all({ ":mid": milestoneId });
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
|
|
@ -1382,7 +1402,7 @@ export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
|
|||
export function getActiveTaskFromDb(milestoneId: string, sliceId: string): TaskRow | null {
|
||||
if (!currentDb) return null;
|
||||
const row = currentDb.prepare(
|
||||
"SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY id LIMIT 1",
|
||||
"SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
|
||||
).get({ ":mid": milestoneId, ":sid": sliceId });
|
||||
if (!row) return null;
|
||||
return rowToTask(row);
|
||||
|
|
@ -1390,7 +1410,7 @@ export function getActiveTaskFromDb(milestoneId: string, sliceId: string): TaskR
|
|||
|
||||
export function getMilestoneSlices(milestoneId: string): SliceRow[] {
|
||||
if (!currentDb) return [];
|
||||
const rows = currentDb.prepare("SELECT * FROM slices WHERE milestone_id = :mid ORDER BY id").all({ ":mid": milestoneId });
|
||||
const rows = currentDb.prepare("SELECT * FROM slices WHERE milestone_id = :mid ORDER BY sequence, id").all({ ":mid": milestoneId });
|
||||
return rows.map(rowToSlice);
|
||||
}
|
||||
|
||||
|
|
|
|||
176
src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts
Normal file
176
src/resources/extensions/gsd/tests/schema-v9-sequence.test.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
import {
|
||||
openDatabase,
|
||||
closeDatabase,
|
||||
insertMilestone,
|
||||
insertSlice,
|
||||
insertTask,
|
||||
getMilestoneSlices,
|
||||
getSliceTasks,
|
||||
getActiveSliceFromDb,
|
||||
getActiveTaskFromDb,
|
||||
} from '../gsd-db.ts';
|
||||
|
||||
function makeTmp(): string {
|
||||
return mkdtempSync(join(tmpdir(), 'gsd-v9-'));
|
||||
}
|
||||
|
||||
function cleanup(base: string): void {
|
||||
try { closeDatabase(); } catch { /* noop */ }
|
||||
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
||||
}
|
||||
|
||||
test('schema v9: migration adds sequence column to slices and tasks', () => {
|
||||
const base = makeTmp();
|
||||
const dbPath = join(base, 'gsd.db');
|
||||
openDatabase(dbPath);
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
// If sequence column doesn't exist, these would throw
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice 1', sequence: 5 });
|
||||
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Task 1', sequence: 3 });
|
||||
|
||||
const slices = getMilestoneSlices('M001');
|
||||
assert.equal(slices.length, 1);
|
||||
assert.equal(slices[0]!.sequence, 5);
|
||||
|
||||
const tasks = getSliceTasks('M001', 'S01');
|
||||
assert.equal(tasks.length, 1);
|
||||
assert.equal(tasks[0]!.sequence, 3);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test('schema v9: getMilestoneSlices returns slices ordered by sequence then id', () => {
|
||||
const base = makeTmp();
|
||||
openDatabase(join(base, 'gsd.db'));
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
|
||||
// Insert in reverse lexicographic order with sequence overriding id order
|
||||
insertSlice({ id: 'S03', milestoneId: 'M001', title: 'Third by id, first by seq', sequence: 1 });
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First by id, third by seq', sequence: 3 });
|
||||
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second by id, second by seq', sequence: 2 });
|
||||
|
||||
const slices = getMilestoneSlices('M001');
|
||||
assert.equal(slices.length, 3);
|
||||
assert.equal(slices[0]!.id, 'S03', 'sequence=1 should be first');
|
||||
assert.equal(slices[1]!.id, 'S02', 'sequence=2 should be second');
|
||||
assert.equal(slices[2]!.id, 'S01', 'sequence=3 should be third');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test('schema v9: getSliceTasks returns tasks ordered by sequence then id', () => {
|
||||
const base = makeTmp();
|
||||
openDatabase(join(base, 'gsd.db'));
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice' });
|
||||
|
||||
// Insert tasks with sequence overriding id order
|
||||
insertTask({ id: 'T03', sliceId: 'S01', milestoneId: 'M001', title: 'Third by id', sequence: 1 });
|
||||
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First by id', sequence: 3 });
|
||||
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Second by id', sequence: 2 });
|
||||
|
||||
const tasks = getSliceTasks('M001', 'S01');
|
||||
assert.equal(tasks.length, 3);
|
||||
assert.equal(tasks[0]!.id, 'T03', 'sequence=1 should be first');
|
||||
assert.equal(tasks[1]!.id, 'T02', 'sequence=2 should be second');
|
||||
assert.equal(tasks[2]!.id, 'T01', 'sequence=3 should be third');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test('schema v9: default sequence (0) falls back to id-based ordering', () => {
|
||||
const base = makeTmp();
|
||||
openDatabase(join(base, 'gsd.db'));
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
|
||||
// All slices with default sequence=0 should sort by id
|
||||
insertSlice({ id: 'S03', milestoneId: 'M001', title: 'Third' });
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First' });
|
||||
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second' });
|
||||
|
||||
const slices = getMilestoneSlices('M001');
|
||||
assert.equal(slices[0]!.id, 'S01', 'default seq=0: should sort by id');
|
||||
assert.equal(slices[1]!.id, 'S02');
|
||||
assert.equal(slices[2]!.id, 'S03');
|
||||
|
||||
// Same for tasks
|
||||
insertSlice({ id: 'S04', milestoneId: 'M001', title: 'Container' });
|
||||
insertTask({ id: 'T02', sliceId: 'S04', milestoneId: 'M001', title: 'B' });
|
||||
insertTask({ id: 'T01', sliceId: 'S04', milestoneId: 'M001', title: 'A' });
|
||||
insertTask({ id: 'T03', sliceId: 'S04', milestoneId: 'M001', title: 'C' });
|
||||
|
||||
const tasks = getSliceTasks('M001', 'S04');
|
||||
assert.equal(tasks[0]!.id, 'T01');
|
||||
assert.equal(tasks[1]!.id, 'T02');
|
||||
assert.equal(tasks[2]!.id, 'T03');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test('schema v9: getActiveSliceFromDb respects sequence ordering', () => {
|
||||
const base = makeTmp();
|
||||
openDatabase(join(base, 'gsd.db'));
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
|
||||
// S02 has lower sequence so should be active first despite higher id than S01
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Higher seq', status: 'pending', sequence: 5 });
|
||||
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Lower seq', status: 'pending', sequence: 2 });
|
||||
|
||||
const active = getActiveSliceFromDb('M001');
|
||||
assert.ok(active);
|
||||
assert.equal(active!.id, 'S02', 'lower sequence should be active first');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test('schema v9: getActiveTaskFromDb respects sequence ordering', () => {
|
||||
const base = makeTmp();
|
||||
openDatabase(join(base, 'gsd.db'));
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice' });
|
||||
|
||||
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Higher seq', status: 'pending', sequence: 10 });
|
||||
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Lower seq', status: 'pending', sequence: 1 });
|
||||
|
||||
const active = getActiveTaskFromDb('M001', 'S01');
|
||||
assert.ok(active);
|
||||
assert.equal(active!.id, 'T02', 'lower sequence should be active first');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test('schema v9: sequence field defaults to 0 when not provided', () => {
|
||||
const base = makeTmp();
|
||||
openDatabase(join(base, 'gsd.db'));
|
||||
try {
|
||||
insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
|
||||
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'No seq' });
|
||||
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'No seq' });
|
||||
|
||||
const slices = getMilestoneSlices('M001');
|
||||
assert.equal(slices[0]!.sequence, 0, 'slice sequence defaults to 0');
|
||||
|
||||
const tasks = getSliceTasks('M001', 'S01');
|
||||
assert.equal(tasks[0]!.sequence, 0, 'task sequence defaults to 0');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue