test: bind a project partition in the PG harness for built-in agent provisioning
FN-8764 made AgentStore.init() unconditionally provision the four durable built-in workflow-owner agents, and that provisioning needs a bound asyncLayer.projectId — backendProjectId rejects the empty/unbound partition so a shared cluster cannot mix ownership. Every AgentStore-backed PG test therefore threw in init(); without this change all 10 cases in agent-instructions.pg.test fail at agent-store.ts:526. createTaskStoreForTest / createSharedPgTaskStoreTestHarness gain an OPT-IN projectId. Undefined keeps the historical project-agnostic harness (RLS bypass, empty-string partition) that the rest of the core suite relies on. When set, the connection GUC `fusion.project_id`, the AsyncDataLayer, and the seeded config row all share one partition — so agents (explicit project_id) and their config revisions (GUC-default project_id) land together and the (project_id, agent_id) FK on agent_config_revisions holds. Also folds in two already-merged consequences: serve.test expects the consumerId: "engine" that serve.ts:326 already passes (FN-8685), and the auto-generated Fusion skill docs pick up fn_workflow_step_resume, the roles / max_workflow_sessions agent fields, and the deprecated singular role. Uncommitted in the working tree; reviewed, verified against the real database, and committed as-is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ Mission → Milestone → Slice → Feature → Task
|
||||
**Tool categories:**
|
||||
<!-- BEGIN: tool-categories (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
|
||||
- **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_logs_read`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_bypass_review`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_browse_gitlab_project_issues`, `fn_task_import_gitlab_project_issues`, `fn_task_browse_gitlab_group_issues`, `fn_task_import_gitlab_group_issues`, `fn_task_browse_gitlab_merge_requests`, `fn_task_import_gitlab_merge_requests`, `fn_task_plan`
|
||||
- **Workflow tools** — `fn_workflow_list`, `fn_workflow_get`, `fn_workflow_validate`, `fn_workflow_create`, `fn_workflow_update`, `fn_workflow_delete`, `fn_workflow_settings`, `fn_trait_list`, `fn_workflow_select`
|
||||
- **Workflow tools** — `fn_workflow_list`, `fn_workflow_get`, `fn_workflow_validate`, `fn_workflow_create`, `fn_workflow_update`, `fn_workflow_delete`, `fn_workflow_settings`, `fn_trait_list`, `fn_workflow_select`, `fn_workflow_step_resume`
|
||||
- **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
|
||||
- **Mission tools** — `fn_mission_create`, `fn_mission_list`, `fn_mission_show`, `fn_mission_list_goals`, `fn_mission_link_goal`, `fn_mission_unlink_goal`, `fn_mission_backfill_assertions`, `fn_mission_delete`, `fn_mission_update`, `fn_milestone_add`, `fn_slice_add`, `fn_feature_add`, `fn_feature_delete`, `fn_slice_delete`, `fn_milestone_delete`, `fn_slice_activate`, `fn_feature_link_task`, `fn_feature_update`, `fn_milestone_update`
|
||||
- **Goal tools** — `fn_goal_list`, `fn_goal_create`, `fn_goal_archive`, `fn_goal_show`
|
||||
|
||||
@@ -305,6 +305,16 @@ Assign a workflow definition to a task by workflow ID.
|
||||
| `workflow_id` | string | ✓ | The workflow definition ID to select (e.g. 'WF-003', or a 'builtin:*' id). Use fn_workflow_list to discover available IDs. |
|
||||
| `task_id` | string | — | Task to assign the workflow to. Defaults to the current task. |
|
||||
|
||||
### fn_workflow_step_resume
|
||||
|
||||
Resume a stuck pending workflow step on an in-review or in-progress Fusion task (operator-only, mandatory reason, audit-logged). When a prompt node (like code-review) is dispatched but never receives a verdict callback (Runfusion/Fusion#1946), the step stays in 'pending' status indefinitely. This tool transitions it to 'failed', enabling the existing fn_task_bypass_review escape hatch to clear the merge blocker. Requires a mandatory reason and step ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | string | ✓ | Task ID (e.g. FN-001) |
|
||||
| `stepId` | string | ✓ | Workflow step ID to resume (e.g. 'code-review', 'plan-review') |
|
||||
| `reason` | string | ✓ | Mandatory justification for resuming the step (audit-logged) |
|
||||
|
||||
## GitHub Tools
|
||||
|
||||
### fn_task_import_github
|
||||
@@ -577,7 +587,8 @@ Create a new non-ephemeral agent.
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | ✓ | Agent name |
|
||||
| `role` | union | ✓ | Agent role/capability |
|
||||
| `role` | union | — | Deprecated singular role; use roles for multi-role agents. |
|
||||
| `roles` | array | — | Canonical permanent-agent role tags. |
|
||||
| `soul` | string | — | Agent personality/identity text |
|
||||
| `instructions_text` | string | — | Inline custom instructions |
|
||||
| `instructions_path` | string | — | Path to instructions markdown |
|
||||
@@ -585,6 +596,7 @@ Create a new non-ephemeral agent.
|
||||
| `heartbeat_interval_ms` | number | — | |
|
||||
| `heartbeat_timeout_ms` | number | — | |
|
||||
| `max_concurrent_runs` | number | — | |
|
||||
| `max_workflow_sessions` | number | — | Max concurrent workflow sessions, independent of heartbeat runs |
|
||||
| `message_response_mode` | union | — | |
|
||||
|
||||
### fn_agent_update
|
||||
@@ -595,7 +607,8 @@ Update editable configuration for an existing non-ephemeral agent. Agent callers
|
||||
|-----------|------|----------|-------------|
|
||||
| `agent_id` | string | ✓ | Target agent ID or name to update |
|
||||
| `name` | string | — | New display name |
|
||||
| `role` | union | — | Agent role/capability |
|
||||
| `role` | union | — | Deprecated singular role; replaces roles for compatibility. |
|
||||
| `roles` | array | — | Canonical permanent-agent role tags. |
|
||||
| `title` | string | — | Optional title shown for the agent |
|
||||
| `icon` | string | — | Optional compact icon/emoji |
|
||||
| `soul` | string | — | Agent personality/identity text |
|
||||
@@ -606,6 +619,7 @@ Update editable configuration for an existing non-ephemeral agent. Agent callers
|
||||
| `heartbeat_interval_ms` | number | — | Heartbeat polling interval in ms |
|
||||
| `heartbeat_timeout_ms` | number | — | Heartbeat timeout in ms |
|
||||
| `max_concurrent_runs` | number | — | Max concurrent heartbeat runs |
|
||||
| `max_workflow_sessions` | number | — | Max concurrent workflow sessions, independent of heartbeat runs |
|
||||
| `message_response_mode` | union | — | How agent responds to messages |
|
||||
|
||||
### fn_agent_set_instructions
|
||||
|
||||
@@ -31,6 +31,7 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
|
||||
| `fn_task_unpause` | Unpause a task — resumes automated agent and scheduler interaction. |
|
||||
| `fn_task_retry` | Retry a failed task — clears the error state. Non-review failures move to todo; in-review execution failures move to todo preserving progress; in-review merge failures stay in-place for auto-merge retry. |
|
||||
| `fn_task_bypass_review` | Policy-gated escape hatch for an in-review task stranded solely by a failed pre-merge review lane (leading real-world cause: the Runfusion/Fusion#1946 '(no feedback captured)' no-verdict dispatch defect), not a real REVISE. Rewrites the latest failed pre-merge WorkflowStepResult to a terminal non-blocking status with explicit bypass audit metadata (who/when/why/prior status) — it never fabricates a reviewer verdict. Requires a mandatory reason and is audit-logged. Clears ONLY the failed-pre-merge-step merge blocker; paused, incomplete-step, blocking-status, and still-pending conditions still block, and an autoMerge:false task is not force-merged. |
|
||||
| `fn_workflow_step_resume` | Resume a stuck pending workflow step on an in-review or in-progress Fusion task (operator-only, mandatory reason, audit-logged). When a prompt node (like code-review) is dispatched but never receives a verdict callback (Runfusion/Fusion#1946), the step stays in 'pending' status indefinitely. This tool transitions it to 'failed', enabling the existing fn_task_bypass_review escape hatch to clear the merge blocker. Requires a mandatory reason and step ID. |
|
||||
| `fn_task_duplicate` | Duplicate an existing task, creating a fresh copy in planning. Copies the title and description but resets all execution state. The AI planning agent will replan the new task. |
|
||||
| `fn_task_refine` | Request a refinement of a completed or in-review task. Creates a new follow-up task in planning that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes. |
|
||||
| `fn_task_archive` | Archive a task from any live column (move to archived). Archived tasks are preserved for historical reference but moved out of the main board view. If the task is still referenced as a lineage parent by another task, archiving is rejected unless removeLineageReferences:true is passed. |
|
||||
|
||||
@@ -120,7 +120,19 @@ export interface PgExtensionHarness {
|
||||
* entries never leak across tests.
|
||||
*/
|
||||
export function createPgExtensionHarness(prefix: string): PgExtensionHarness {
|
||||
const pg = createSharedPgTaskStoreTestHarness({ prefix });
|
||||
/*
|
||||
FNXC:WorkflowAgentRouting 2026-08-07-18:40:
|
||||
FN-8764 made AgentStore.init() unconditionally provision the four durable built-in
|
||||
workflow-owner agents, and that provisioning requires a bound asyncLayer.projectId
|
||||
(backendProjectId rejects the empty/unbound partition to avoid mixing ownership on a
|
||||
shared PG cluster). Bind this CLI-extension harness to a real projectId end-to-end so the
|
||||
connection GUC `fusion.project_id`, the layer's projectId, and the seeded config row all
|
||||
agree: agents (explicit project_id) and their config revisions (GUC-default project_id)
|
||||
land in the SAME partition, so the (project_id, agent_id) FK on agent_config_revisions holds.
|
||||
A project-agnostic bind (projectId "") would split those writes across partitions and
|
||||
reintroduce the FN-8764 provisioning throw.
|
||||
*/
|
||||
const pg = createSharedPgTaskStoreTestHarness({ prefix, projectId: `ext_${prefix}` });
|
||||
return {
|
||||
rootDir: pg.rootDir,
|
||||
store: pg.store,
|
||||
|
||||
@@ -983,8 +983,12 @@ describe("runServe", () => {
|
||||
|
||||
// FN-8399: serve now passes an onMigrationProgress callback so the holding
|
||||
// server can expose incomplete migration status on the dashboard.
|
||||
// FN-8685: serve also tags the shared store with the durable engine consumer
|
||||
// identity (buildConsumerId("engine") === "engine") so its deletion cursor
|
||||
// survives restart and cross-process deletes reach runtime observers.
|
||||
expect(mocks.createTaskStoreForBackendMock).toHaveBeenCalledWith({
|
||||
rootDir: "/repo",
|
||||
consumerId: "engine",
|
||||
onMigrationProgress: expect.any(Function),
|
||||
});
|
||||
expect(mocks.taskStoreCtor).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -693,9 +693,20 @@ export async function createTaskStoreForTest(options?: {
|
||||
readonly poolMax?: number;
|
||||
readonly prefix?: string;
|
||||
readonly copyFromGolden?: boolean;
|
||||
/*
|
||||
FNXC:WorkflowAgentRouting 2026-08-07-18:40:
|
||||
Opt-in project binding. Default (undefined) preserves the historical project-agnostic
|
||||
harness that runs with RLS bypass and writes/reads the empty-string partition. When set,
|
||||
the runtime connection is created with `fusion.project_id` (enforced RLS, no bypass) and
|
||||
the AsyncDataLayer carries the same projectId, so explicit-project writes, GUC-default
|
||||
writes, and reads all agree on one partition — required by FN-8764 built-in workflow-owner
|
||||
provisioning during AgentStore.init().
|
||||
*/
|
||||
readonly projectId?: string;
|
||||
}): Promise<PgTestHarness> {
|
||||
const poolMax = options?.poolMax ?? 5;
|
||||
const prefix = options?.prefix ?? "fusion_test";
|
||||
const projectId = options?.projectId;
|
||||
|
||||
const dbName = uniqueDbName(prefix);
|
||||
|
||||
@@ -772,8 +783,9 @@ export async function createTaskStoreForTest(options?: {
|
||||
const connections = await createConnectionSetFromUrl(schemaBackend, {
|
||||
poolMax,
|
||||
connectTimeoutSeconds: 5,
|
||||
projectId,
|
||||
});
|
||||
const layer = createAsyncDataLayer(connections);
|
||||
const layer = createAsyncDataLayer(connections, projectId ? { projectId } : undefined);
|
||||
|
||||
// Admin connection for direct row inspection/seeding in tests.
|
||||
const adminSql = postgres(testUrl, {
|
||||
@@ -936,7 +948,16 @@ const TRUNCATE_ALL_SQL = `TRUNCATE TABLE ${ALL_APPLICATION_TABLES.join(", ")} RE
|
||||
export function createSharedPgTaskStoreTestHarness(options?: {
|
||||
readonly poolMax?: number;
|
||||
readonly prefix?: string;
|
||||
/*
|
||||
FNXC:WorkflowAgentRouting 2026-08-07-18:40:
|
||||
Opt-in project binding threaded to createTaskStoreForTest and the config re-seed below.
|
||||
Default undefined keeps the project-agnostic (projectId "") harness every existing core
|
||||
test relies on; the CLI extension harness sets it so FN-8764 built-in workflow-owner
|
||||
provisioning during AgentStore.init() has a bound projectId.
|
||||
*/
|
||||
readonly projectId?: string;
|
||||
}): SharedPgTaskStoreHarness {
|
||||
const boundProjectId = options?.projectId ?? "";
|
||||
let harness: PgTestHarness | null = null;
|
||||
let store: TaskStore | null = null;
|
||||
// Lazily import DEFAULT_PROJECT_SETTINGS to avoid pulling the full types
|
||||
@@ -1008,6 +1029,7 @@ export function createSharedPgTaskStoreTestHarness(options?: {
|
||||
...options,
|
||||
prefix: options?.prefix ?? "fusion_shared",
|
||||
copyFromGolden: true,
|
||||
projectId: options?.projectId,
|
||||
});
|
||||
store = harness.store;
|
||||
},
|
||||
@@ -1027,9 +1049,11 @@ export function createSharedPgTaskStoreTestHarness(options?: {
|
||||
sql.raw(
|
||||
// FNXC:MultiProjectIsolation 2026-07-11: config is keyed per-project on
|
||||
// project_id (the PK) — id is no longer unique, so the upsert arbiter
|
||||
// must be project_id. Harness stores run project-agnostic (projectId '').
|
||||
// must be project_id. Harness stores run project-agnostic (projectId '')
|
||||
// unless a bound projectId was requested (FNXC:WorkflowAgentRouting 2026-08-07-18:40),
|
||||
// in which case the config row and all other writes share that partition.
|
||||
`INSERT INTO ${PROJECT_SCHEMA}.config (id, project_id, next_id, next_workflow_step_id, settings, workflow_steps, updated_at)
|
||||
VALUES (1, '', 1, 1, '${defaultsJson.replace(/'/g, "''")}'::jsonb, '[]'::jsonb, now())
|
||||
VALUES (1, '${boundProjectId.replace(/'/g, "''")}', 1, 1, '${defaultsJson.replace(/'/g, "''")}'::jsonb, '[]'::jsonb, now())
|
||||
ON CONFLICT (project_id) DO UPDATE SET next_id = 1, next_workflow_step_id = 1, settings = EXCLUDED.settings, workflow_steps = '[]'::jsonb, updated_at = now()`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,8 +7,11 @@ import {
|
||||
} from "../__test-utils__/pg-test-harness.js";
|
||||
|
||||
pgDescribe("AgentStore pause marker resume cleanup (FN-8569)", () => {
|
||||
// FNXC:WorkflowAgentRouting 2026-08-07-18:40: bind a real projectId so FN-8764 built-in
|
||||
// workflow-owner provisioning in AgentStore.init() has a partition.
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_pause_marker_clear",
|
||||
projectId: "proj_pause_marker_clear",
|
||||
});
|
||||
let agentStore: AgentStore;
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@ import {
|
||||
const pgTest = pgDescribe;
|
||||
|
||||
pgTest("task→agent routing policy (issue #2015)", () => {
|
||||
// FNXC:WorkflowAgentRouting 2026-08-07-18:40: bind a real projectId so FN-8764 built-in
|
||||
// workflow-owner provisioning in AgentStore.init() has a partition. The cross-project
|
||||
// isolation case below uses a DISTINCT projectId so the two stores are genuinely separate.
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_agent_routing",
|
||||
projectId: "proj_agent_routing",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
@@ -298,7 +302,7 @@ pgTest("task→agent routing policy (issue #2015)", () => {
|
||||
|
||||
describe("project isolation", () => {
|
||||
it("an agent registered in another project's store can never be bound to this project's tasks", async () => {
|
||||
const otherHarness = await createTaskStoreForTest({ prefix: "fusion_agent_routing_other" });
|
||||
const otherHarness = await createTaskStoreForTest({ prefix: "fusion_agent_routing_other", projectId: "proj_agent_routing_other" });
|
||||
const otherAgentStore = new AgentStore({ rootDir: otherHarness.rootDir, asyncLayer: otherHarness.layer, taskStore: otherHarness.store });
|
||||
await otherAgentStore.init();
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@ import { AgentStore } from "../../agents/agent-store.js";
|
||||
const pgTest = pgDescribe;
|
||||
|
||||
pgTest("AgentStore instructions fields (PostgreSQL)", () => {
|
||||
// FNXC:WorkflowAgentRouting 2026-08-07-18:40: bind a real projectId so FN-8764 built-in
|
||||
// workflow-owner provisioning in AgentStore.init() has a partition and agent config-revision
|
||||
// writes (GUC-default project_id) share it with the explicit agent writes.
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_agent_instr",
|
||||
projectId: "proj_agent_instr",
|
||||
});
|
||||
|
||||
let agentStore: AgentStore;
|
||||
|
||||
@@ -22,8 +22,11 @@ import { AgentStore } from "../../agents/agent-store.js";
|
||||
const pgTest = pgDescribe;
|
||||
|
||||
pgTest("AgentStore.getAgent backs the async wake hook (PostgreSQL)", () => {
|
||||
// FNXC:WorkflowAgentRouting 2026-08-07-18:40: bind a real projectId so FN-8764 built-in
|
||||
// workflow-owner provisioning in AgentStore.init() has a partition.
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_agent_wake_getagent",
|
||||
projectId: "proj_agent_wake",
|
||||
});
|
||||
|
||||
let agentStore: AgentStore;
|
||||
|
||||
Reference in New Issue
Block a user