Files
fusion/plugins/fusion-plugin-quality/src/quality-schema.ts
gsxdsm 446c969968 fix(FN-2127): harden Quality Postgres store (project scope, races) (#2230)
## Summary

Follow-up after **#2164** and main’s **FN-8103 / FN-8104**
(Postgres-only data access / SQLite retirement).

Main already routes Quality through `AsyncQualityStore` +
`getAsyncLayer()`. This PR keeps the **review hardening** that was still
missing:

- **Project binding** — reject request `projectId` mismatches vs bound
AsyncDataLayer; all SQL uses bound project
- **`createRunIfNoActive`** — advisory lock so concurrent starts cannot
double-queue
- **Cancel-safe runner** — cancel slot registered before the running
write; pre-spawn cancel skips process
- **`finalizeRun`** — never overwrites a `cancelled` terminal status
- **Detached execute** — catch only execution failures; prune fail-soft
in `finally`
- Guardrail tests + Quality v2 plan doc

## Test plan

- [ ] Task QA loads under PostgreSQL (no SQLite/backend-mode error)
- [ ] Concurrent start for same task → 409 second start
- [ ] Cancel during start does not leave a live orphan process
- [ ] Cancelled run stays cancelled after process exit
- [ ] `pnpm --filter @fusion-plugin-examples/quality test` (37 tests)
2026-07-16 11:38:34 -07:00

126 lines
4.2 KiB
TypeScript

import type { Database, PluginPostgresSchemaDefinition } from "@fusion/core";
/*
FNXC:Quality 2026-07-14-21:45:
Plugin-owned Quality tables via onSchemaInit. projectId on every row for multi-project isolation.
FNXC:QualityPostgres 2026-07-16-09:03:
ensureQualitySchema is SQLite/unit-test only. Production QA routes use
qualityPostgresSchema + AsyncQualityStore; they never open TaskStore.db.
*/
export function ensureQualitySchema(db: Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS quality_test_runs (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
task_id TEXT,
plan_id TEXT,
source TEXT NOT NULL,
preset_id TEXT,
command TEXT NOT NULL,
cwd TEXT NOT NULL,
cwd_kind TEXT NOT NULL,
status TEXT NOT NULL,
exit_code INTEGER,
error_message TEXT,
timeout_ms INTEGER NOT NULL,
started_at TEXT,
finished_at TEXT,
duration_ms INTEGER,
stdout TEXT NOT NULL DEFAULT '',
stderr TEXT NOT NULL DEFAULT '',
triggered_by TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_quality_test_runs_project_created
ON quality_test_runs(project_id, created_at DESC, id);
CREATE INDEX IF NOT EXISTS idx_quality_test_runs_task_created
ON quality_test_runs(project_id, task_id, created_at DESC, id);
CREATE TABLE IF NOT EXISTS quality_test_plans (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT NOT NULL,
steps_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_quality_test_plans_project
ON quality_test_plans(project_id, status, updated_at DESC, id);
CREATE TABLE IF NOT EXISTS quality_suggested_cases (
project_id TEXT NOT NULL,
task_id TEXT NOT NULL,
cases_json TEXT NOT NULL DEFAULT '[]',
generated_at TEXT NOT NULL,
method TEXT NOT NULL,
PRIMARY KEY (project_id, task_id)
);
`);
}
/**
* FNXC:QualityPostgres 2026-07-15-14:55:
* Quality remains available when Fusion uses the PostgreSQL-backed task store.
* The legacy SQLite hook above serves local DatabaseSync tests, while this
* declarative contract lets the host create the same project-scoped tables
* through its privileged PostgreSQL schema executor before plugin routes run.
*/
export const qualityPostgresSchema: PluginPostgresSchemaDefinition = {
version: 1,
tablePrefix: "quality_",
statements: [
`CREATE TABLE IF NOT EXISTS project.quality_test_runs (
project_id text NOT NULL,
id text NOT NULL,
task_id text,
plan_id text,
source text NOT NULL,
preset_id text,
command text NOT NULL,
cwd text NOT NULL,
cwd_kind text NOT NULL,
status text NOT NULL,
exit_code integer,
error_message text,
timeout_ms integer NOT NULL,
started_at text,
finished_at text,
duration_ms integer,
stdout text NOT NULL DEFAULT '',
stderr text NOT NULL DEFAULT '',
triggered_by text NOT NULL,
created_at text NOT NULL,
updated_at text NOT NULL,
PRIMARY KEY (project_id, id)
)`,
"CREATE INDEX IF NOT EXISTS idx_quality_test_runs_project_created ON project.quality_test_runs(project_id, created_at DESC, id)",
"CREATE INDEX IF NOT EXISTS idx_quality_test_runs_task_created ON project.quality_test_runs(project_id, task_id, created_at DESC, id)",
`CREATE TABLE IF NOT EXISTS project.quality_test_plans (
project_id text NOT NULL,
id text NOT NULL,
name text NOT NULL,
status text NOT NULL,
steps_json text NOT NULL DEFAULT '[]',
created_at text NOT NULL,
updated_at text NOT NULL,
PRIMARY KEY (project_id, id)
)`,
"CREATE INDEX IF NOT EXISTS idx_quality_test_plans_project ON project.quality_test_plans(project_id, status, updated_at DESC, id)",
`CREATE TABLE IF NOT EXISTS project.quality_suggested_cases (
project_id text NOT NULL,
task_id text NOT NULL,
cases_json text NOT NULL DEFAULT '[]',
generated_at text NOT NULL,
method text NOT NULL,
PRIMARY KEY (project_id, task_id)
)`,
],
};