Files
fusion/packages/engine/src/cli-agent/__tests__/runtime-postgres.test.ts
gsxdsm ba1e82381e fix(FN-7952): cut runtime services over to PostgreSQL (#2109)
## 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 -->
2026-07-14 22:16:16 -07:00

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();
}
});
});