## Summary Engine and dashboard traffic now stays on the authoritative PostgreSQL layer across execution, recovery, project discovery, planning sessions, analytics, and shutdown. The dashboard no longer presents a migration notice for a cutover that is already mandatory. ## Design decisions - Runtime composition requires an async data layer instead of constructing a hidden SQLite fallback. - Engine workflow, mission, claim, and self-healing reads await their PostgreSQL-backed store contracts. - Project-scoped dashboard stores retain and close their backend owner exactly once. - The dashboard test quarantine entry remains paired with its Vitest exclusion, preserving the repository’s deletion-ratchet policy. ## Validation - Core, Engine, Dashboard, CLI, and Desktop typechecks pass on the stacked branch. - `pnpm test:gate` passes all 478 gate tests. - This PR changes 62 files. ## Stack - Depends on #2108. - CLI/desktop/ops, plugins, and docs/release follow in later PRs. Related: #2105 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Project discovery now recognizes projects using the `.fusion/project.json` marker. * Knowledge indexing and search are more reliable across project-scoped storage. * **Bug Fixes** * Improved session, audit timeline, approval, monitoring, and analytics data consistency. * Prevented stale planning-session updates and project-store shutdown races. * Ensured chat usage and CLI session status are saved before continuing. * **UI Changes** * Removed the storage migration notice banner now that the PostgreSQL transition is complete. * **Reliability** * Improved shutdown handling, workflow execution, and worktree behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
/**
|
|
* FNXC:CliAgentPostgres 2026-07-14-12:00:
|
|
* Enabling the experimental CLI Agent Executor must bootstrap from the shared
|
|
* PostgreSQL data layer and flush its durable session queue on shutdown.
|
|
*/
|
|
import { EventEmitter } from "node:events";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { CliSessionStore, type AsyncDataLayer } from "@fusion/core";
|
|
import type { IPty } from "node-pty";
|
|
import { CliAdapterRegistry, type CliAgentAdapter } from "../adapter.js";
|
|
import { createCliAgentRuntime } from "../runtime.js";
|
|
import { CliSessionManager } from "../session-manager.js";
|
|
|
|
describe("createCliAgentRuntime PostgreSQL wiring", () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it("hydrates from AsyncDataLayer and flushes persistence during disposal", async () => {
|
|
const flush = vi.fn(async () => {});
|
|
const fakeStore = Object.assign(new EventEmitter(), {
|
|
flush,
|
|
listSessions: () => [],
|
|
listByTask: () => [],
|
|
getSession: () => undefined,
|
|
createSession: vi.fn(),
|
|
updateSession: vi.fn(),
|
|
deleteSession: vi.fn(),
|
|
}) as unknown as CliSessionStore;
|
|
const layer = { db: {} } as AsyncDataLayer;
|
|
const create = vi.spyOn(CliSessionStore, "create").mockResolvedValue(fakeStore);
|
|
|
|
const runtime = await createCliAgentRuntime({
|
|
fusionDir: "/tmp/fusion-cli-agent-test",
|
|
asyncLayer: layer,
|
|
projectId: "project-a",
|
|
hookEndpointUrl: "http://127.0.0.1:4545/api/cli-agent/hooks",
|
|
});
|
|
|
|
expect(create).toHaveBeenCalledWith(layer, "project-a");
|
|
expect(runtime.bundle.store).toBe(fakeStore);
|
|
await runtime.dispose();
|
|
expect(flush).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("preserves a PTY spawn failure when dead-state persistence also fails", async () => {
|
|
/*
|
|
FNXC:CliAgentPostgres 2026-07-14-21:33:
|
|
Spawn-failure persistence is diagnostic cleanup; callers must receive the original PTY error even when the queued PostgreSQL flush rejects.
|
|
*/
|
|
const spawnError = new Error("pty spawn failed");
|
|
const flushError = new Error("postgres flush failed");
|
|
const flush = vi.fn()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockRejectedValueOnce(flushError);
|
|
const record = {
|
|
id: "session-1",
|
|
adapterId: "test",
|
|
projectId: "project-a",
|
|
purpose: "execute",
|
|
taskId: null,
|
|
chatSessionId: null,
|
|
worktreePath: null,
|
|
autonomyPosture: null,
|
|
agentState: "starting",
|
|
};
|
|
const updateSession = vi.fn(() => record);
|
|
const fakeStore = Object.assign(new EventEmitter(), {
|
|
flush,
|
|
createSession: vi.fn(() => record),
|
|
updateSession,
|
|
getSession: vi.fn(),
|
|
}) as unknown as CliSessionStore;
|
|
const registry = new CliAdapterRegistry();
|
|
registry.register({
|
|
id: "test",
|
|
name: "Test",
|
|
capabilities: {
|
|
nativeDone: false,
|
|
nativeWaiting: false,
|
|
transcriptSource: "none",
|
|
supportsResume: false,
|
|
},
|
|
buildLaunch: () => ({ command: "test-agent", args: [] }),
|
|
buildEnvAllowlist: () => [],
|
|
createReadinessDetector: () => ({ observe: () => true }),
|
|
formatInjection: (text) => ({ payload: text }),
|
|
} satisfies CliAgentAdapter);
|
|
const manager = new CliSessionManager({
|
|
registry,
|
|
store: fakeStore,
|
|
loadPty: vi.fn(async () => ({
|
|
spawn: () => {
|
|
throw spawnError;
|
|
},
|
|
})) as unknown as () => Promise<{ spawn: () => IPty }>,
|
|
});
|
|
|
|
try {
|
|
await expect(manager.spawn({
|
|
adapterId: "test",
|
|
projectId: "project-a",
|
|
purpose: "execute",
|
|
})).rejects.toBe(spawnError);
|
|
expect(flush).toHaveBeenCalledTimes(2);
|
|
expect(updateSession).toHaveBeenCalledWith("session-1", {
|
|
agentState: "dead",
|
|
terminationReason: "crashed",
|
|
});
|
|
} finally {
|
|
manager.dispose();
|
|
}
|
|
});
|
|
});
|