feat(FN-3293): add stabilization docs to test audit report

Documentation for test stabilization was finalized by updating the test audit report with 2 additional lines.

Fusion-Task-Id: FN-3293
This commit is contained in:
Fusion
2026-05-04 11:38:40 -07:00
committed by gsxdsm
parent 73430354e1
commit df253a8eac
15 changed files with 1314 additions and 51 deletions

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { Database, createDatabase, toJson, toJsonNullable, fromJson, normalizeTaskComments } from "../db.js";
import { DEFAULT_PROJECT_SETTINGS } from "../types.js";
import { TaskStore } from "../store.js";
import { mkdtempSync, existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
@@ -109,6 +110,8 @@ describe("Database", () => {
expect(tableNames).toContain("roadmaps");
expect(tableNames).toContain("roadmap_milestones");
expect(tableNames).toContain("roadmap_features");
// Verification cache (migration 61)
expect(tableNames).toContain("verification_cache");
});
it("creates all expected indexes", () => {
@@ -152,10 +155,12 @@ describe("Database", () => {
// Roadmap indexes
expect(indexNames).toContain("idxRoadmapMilestonesRoadmapOrder");
expect(indexNames).toContain("idxRoadmapFeaturesMilestoneOrder");
// Verification cache index (migration 61)
expect(indexNames).toContain("idxVerificationCacheRecordedAt");
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
});
it("seeds lastModified", () => {
@@ -178,7 +183,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
});
it("does not overwrite existing config on re-init", () => {
@@ -899,7 +904,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -924,11 +929,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
db.close();
});
@@ -963,7 +968,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority");
@@ -1004,7 +1009,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -1073,7 +1078,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -1176,7 +1181,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1250,7 +1255,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1274,7 +1279,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1378,7 +1383,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1847,7 +1852,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();
@@ -1881,3 +1886,82 @@ describe("createDatabase factory", () => {
db2.close();
});
});
// ── TaskStore — verification cache methods ────────────────────────────────
describe("TaskStore — verification cache", () => {
let rootDir: string;
let globalDir: string;
let store: TaskStore;
beforeEach(async () => {
rootDir = mkdtempSync(join(tmpdir(), "kb-vc-test-"));
globalDir = mkdtempSync(join(tmpdir(), "kb-vc-global-"));
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
});
afterEach(async () => {
store.close();
await rm(rootDir, { recursive: true, force: true });
await rm(globalDir, { recursive: true, force: true });
});
it("returns null when no cache entry exists", () => {
const hit = store.getVerificationCacheHit("abc1234", "pnpm test", "pnpm build");
expect(hit).toBeNull();
});
it("records a pass and retrieves it as a cache hit", () => {
const treeSha = "deadbeef1234567890";
store.recordVerificationCachePass(treeSha, "pnpm test", "pnpm build", "FN-001");
const hit = store.getVerificationCacheHit(treeSha, "pnpm test", "pnpm build");
expect(hit).not.toBeNull();
expect(hit!.taskId).toBe("FN-001");
expect(new Date(hit!.recordedAt).toISOString()).toBe(hit!.recordedAt);
});
it("returns null for a different tree sha", () => {
store.recordVerificationCachePass("sha-a", "pnpm test", "", "FN-001");
const hit = store.getVerificationCacheHit("sha-b", "pnpm test", "");
expect(hit).toBeNull();
});
it("distinguishes entries by testCommand", () => {
const treeSha = "aabbccdd";
store.recordVerificationCachePass(treeSha, "pnpm test", "", "FN-001");
expect(store.getVerificationCacheHit(treeSha, "pnpm test", "")).not.toBeNull();
expect(store.getVerificationCacheHit(treeSha, "vitest run", "")).toBeNull();
});
it("distinguishes entries by buildCommand", () => {
const treeSha = "11223344";
store.recordVerificationCachePass(treeSha, "", "pnpm build", "FN-002");
expect(store.getVerificationCacheHit(treeSha, "", "pnpm build")).not.toBeNull();
expect(store.getVerificationCacheHit(treeSha, "", "tsc --noEmit")).toBeNull();
});
it("normalizes undefined to empty string for stable primary key", () => {
const treeSha = "normtest";
// Pass undefined-ish values (coerced via nullish fallback in impl)
store.recordVerificationCachePass(treeSha, "", "", "FN-003");
const hit = store.getVerificationCacheHit(treeSha, "", "");
expect(hit).not.toBeNull();
expect(hit!.taskId).toBe("FN-003");
});
it("overwrites an existing entry on re-record (INSERT OR REPLACE)", () => {
const treeSha = "upserttest";
store.recordVerificationCachePass(treeSha, "pnpm test", "", "FN-010");
store.recordVerificationCachePass(treeSha, "pnpm test", "", "FN-020");
const hit = store.getVerificationCacheHit(treeSha, "pnpm test", "");
expect(hit).not.toBeNull();
expect(hit!.taskId).toBe("FN-020");
});
});

View File

@@ -869,7 +869,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(60);
expect(db1.getSchemaVersion()).toBe(61);
db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables
@@ -904,7 +904,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration
db3.init();
expect(db3.getSchemaVersion()).toBe(60);
expect(db3.getSchemaVersion()).toBe(61);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -935,12 +935,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(60);
expect(db1.getSchemaVersion()).toBe(61);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(60);
expect(db2.getSchemaVersion()).toBe(61);
db2.close();
} finally {
rmSync(testDir, { recursive: true, force: true });

View File

@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
});
it("mission_features table has loop state columns", () => {

View File

@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
describe("schema version", () => {
it("schema version is 40 after init", () => {
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
});
});

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
});
});
});

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(60);
expect(db.getSchemaVersion()).toBe(61);
const index = db
.prepare(

View File

@@ -88,7 +88,7 @@ export function probeFts5(db: DatabaseSync): boolean {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 60;
const SCHEMA_VERSION = 61;
function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined,
@@ -2358,6 +2358,22 @@ export class Database {
});
}
if (version < 61) {
this.applyMigration(61, () => {
this.db.exec(`
CREATE TABLE IF NOT EXISTS verification_cache (
treeSha TEXT NOT NULL,
testCommand TEXT NOT NULL DEFAULT '',
buildCommand TEXT NOT NULL DEFAULT '',
recordedAt TEXT NOT NULL,
taskId TEXT,
PRIMARY KEY (treeSha, testCommand, buildCommand)
)
`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxVerificationCacheRecordedAt ON verification_cache(recordedAt)`);
});
}
}
/**

View File

@@ -6374,6 +6374,60 @@ ${notificationsSection}`;
return this.todoStore;
}
// ── Verification Cache ────────────────────────────────────────────────────
/**
* Look up a previously recorded verification cache pass for a given tree sha
* and command pair. Returns null when no cached pass exists.
*
* @param treeSha - The git tree SHA of the merged commit.
* @param testCommand - The test command string (normalized to empty string when absent).
* @param buildCommand - The build command string (normalized to empty string when absent).
*/
getVerificationCacheHit(
treeSha: string,
testCommand: string,
buildCommand: string,
): { recordedAt: string; taskId: string | null } | null {
const normalizedTest = testCommand ?? "";
const normalizedBuild = buildCommand ?? "";
const row = this.db
.prepare(
`SELECT recordedAt, taskId FROM verification_cache
WHERE treeSha = ? AND testCommand = ? AND buildCommand = ?`,
)
.get(treeSha, normalizedTest, normalizedBuild) as
| { recordedAt: string; taskId: string | null }
| undefined;
return row ?? null;
}
/**
* Record a successful verification pass for the given tree sha and commands.
* Uses INSERT OR REPLACE so a re-run of the same tree updates the timestamp.
*
* @param treeSha - The git tree SHA of the merged commit.
* @param testCommand - The test command string (normalized to empty string when absent).
* @param buildCommand - The build command string (normalized to empty string when absent).
* @param taskId - The task ID that triggered the pass (for telemetry).
*/
recordVerificationCachePass(
treeSha: string,
testCommand: string,
buildCommand: string,
taskId: string,
): void {
const normalizedTest = testCommand ?? "";
const normalizedBuild = buildCommand ?? "";
const recordedAt = new Date().toISOString();
this.db
.prepare(
`INSERT OR REPLACE INTO verification_cache (treeSha, testCommand, buildCommand, recordedAt, taskId)
VALUES (?, ?, ?, ?, ?)`,
)
.run(treeSha, normalizedTest, normalizedBuild, recordedAt, taskId);
}
// ── Backward Compatibility (Multi-Project Support) ────────────────────────
}