feat(FN-1153): add cross-tab locks for AI planning sessions

- Bump SQLite schema to v19 with ai_sessions lock columns and lock index, and align migration coverage in core DB tests
- Extend AiSessionStore with acquire/release/force lock APIs, stale lock cleanup, and lock metadata in ai_session update summaries
- Enforce lock checks on planning, subtask, and mission interview mutation routes with 409 conflict responses while keeping stream reads unaffected
- Add frontend tab identity + useSessionLock hook and wire Planning, Subtask, and Mission modals to pass tabId, show lock overlay, and support Take Control
- Expand dashboard route/e2e and modal tests to validate lock enforcement, lock handoff, and lock-aware session reentry behavior
This commit is contained in:
gsxdsm
2026-04-08 16:14:32 -07:00
parent 833c7eb15a
commit 4aa1cbdba8
23 changed files with 1540 additions and 58 deletions

View File

@@ -91,6 +91,7 @@ describe("Database", () => {
expect(indexNames).toContain("idxAgentHeartbeatsRunId");
expect(indexNames).toContain("idxAiSessionsStatus");
expect(indexNames).toContain("idxAiSessionsType");
expect(indexNames).toContain("idxAiSessionsLock");
expect(indexNames).toContain("idxMessagesCreatedAt");
expect(indexNames).toContain("idxMessagesFrom");
expect(indexNames).toContain("idxMessagesTo");
@@ -105,7 +106,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
});
it("seeds lastModified", () => {
@@ -128,7 +129,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
});
it("does not overwrite existing config on re-init", () => {
@@ -735,7 +736,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations)
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -760,11 +761,11 @@ describe("schema migrations", () => {
const db = new Database(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
db.close();
});
@@ -780,7 +781,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
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" }]);
@@ -804,7 +805,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
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" }]);
@@ -908,7 +909,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 5
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1118,7 +1119,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(18);
expect(db.getSchemaVersion()).toBe(19);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();