fix(core): restore title-redirect planning hold and stamp workflow-create partition
Full-suite repair, core cluster. Two real product regressions caught by the red tests: (1) the wave-18 refactor dropped the task.title argument FN-8840 added to isDuplicateRedirectOnlyPrompt inside isTaskAwaitingPlanning, so a title-only DUPLICATE:<ID> redirect read as executable Ready instead of awaiting planning; (2) FN-8998 scoped every workflow-definition read by layer.projectId but left the INSERT on the session-GUC default, so a JS-bound layer created workflow rows it could never read back — the insert now stamps the bound projectId like the FN-8997 workflowSteps insert. Test-lag fixes: analytics renamed-lanes harnesses now bind projectId 'p1' to match FN-8957/FN-8998 scoping, project-ownership-runtime-scope replaces a hardcoded expired approval date, schema-applier learns migrations 0059/0060 (115 project tables, baseline 0060), and builtin-workflow-settings-triage learns FN-8932's memoryConsolidationEnabled with position-independent lookups. Verified: 10 files / 128 tests green, core typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix title-only duplicate redirects showing as Ready and workflows created invisible to their own project.
|
||||
category: fix
|
||||
dev: Restores the `task.title` argument to `isDuplicateRedirectOnlyPrompt` in `isTaskAwaitingPlanning` (dropped by a refactor after FN-8840) and stamps `layer.projectId` on the `project.workflows` INSERT so FN-8998's project-scoped reads see a bound layer's own create.
|
||||
@@ -142,7 +142,14 @@ describe("workflow-native built-in workflow settings", () => {
|
||||
const movedIds = new Set(BUILTIN_MOVED_WORKFLOW_SETTINGS.map((setting) => setting.id));
|
||||
const movedKeyIds = new Set(MOVED_SETTINGS_KEYS);
|
||||
|
||||
/*
|
||||
FNXC:MemoryAgent 2026-08-15-22:10:
|
||||
FN-8932 declares memory consolidation workflow-native (it resolves through the default workflow
|
||||
on a no-task heartbeat, like patrol), so it belongs in this catalog and stays out of
|
||||
moved/project settings like every other key asserted below.
|
||||
*/
|
||||
expect(BUILTIN_OVERSIGHT_SETTINGS.map((setting) => setting.id)).toEqual([
|
||||
"memoryConsolidationEnabled",
|
||||
"plannerOversightLevel",
|
||||
"plannerOversightNotificationLevel",
|
||||
"plannerOverseerExecutorStuckAfterMs",
|
||||
@@ -156,7 +163,13 @@ describe("workflow-native built-in workflow settings", () => {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
});
|
||||
const oversight = BUILTIN_OVERSIGHT_SETTINGS[0];
|
||||
/* FNXC:MemoryAgent 2026-08-15-22:10: resolve by id, not position — FN-8932 prepended
|
||||
memoryConsolidationEnabled to this catalog and positional reads silently drifted. */
|
||||
const memoryConsolidation = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "memoryConsolidationEnabled");
|
||||
expect(memoryConsolidation).toMatchObject({ type: "boolean", default: true });
|
||||
expect(movedIds.has("memoryConsolidationEnabled")).toBe(false);
|
||||
expect(movedKeyIds.has("memoryConsolidationEnabled")).toBe(false);
|
||||
const oversight = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOversightLevel")!;
|
||||
expect(oversight).toMatchObject({
|
||||
type: "enum",
|
||||
default: "autonomous",
|
||||
@@ -180,7 +193,7 @@ describe("workflow-native built-in workflow settings", () => {
|
||||
"plannerOversightLevel should not be in MOVED_SETTINGS_KEYS",
|
||||
).toBe(false);
|
||||
|
||||
const notificationLevel = BUILTIN_OVERSIGHT_SETTINGS[1];
|
||||
const notificationLevel = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOversightNotificationLevel")!;
|
||||
expect(notificationLevel).toMatchObject({
|
||||
id: "plannerOversightNotificationLevel",
|
||||
type: "enum",
|
||||
@@ -213,7 +226,7 @@ describe("workflow-native built-in workflow settings", () => {
|
||||
|
||||
// FN-7743: executor-stall recovery threshold, declared alongside the other
|
||||
// workflow-native oversight settings.
|
||||
const executorStuckAfterMs = BUILTIN_OVERSIGHT_SETTINGS[2];
|
||||
const executorStuckAfterMs = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOverseerExecutorStuckAfterMs")!;
|
||||
expect(executorStuckAfterMs).toMatchObject({
|
||||
id: "plannerOverseerExecutorStuckAfterMs",
|
||||
type: "number",
|
||||
@@ -232,7 +245,7 @@ describe("workflow-native built-in workflow settings", () => {
|
||||
"plannerOverseerExecutorStuckAfterMs should not be in MOVED_SETTINGS_KEYS",
|
||||
).toBe(false);
|
||||
|
||||
const heartbeatPatrol = BUILTIN_OVERSIGHT_SETTINGS[6];
|
||||
const heartbeatPatrol = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerHeartbeatPatrolEnabled")!;
|
||||
expect(heartbeatPatrol).toMatchObject({
|
||||
id: "plannerHeartbeatPatrolEnabled",
|
||||
type: "boolean",
|
||||
|
||||
@@ -30,8 +30,16 @@ const IN_RANGE = "2026-06-15T12:00:00.000Z";
|
||||
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
|
||||
|
||||
pgDescribe("issue analytics under a renamed board vocabulary", () => {
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
FN-8957 scoped analytics task reads to `layer.projectId`. This file aggregates under projectId
|
||||
"p1", so the harness binds the same project: an unbound harness writes store rows under the GUC
|
||||
default partition and the scoped aggregate reads zero rows — an isolation false-negative, not the
|
||||
lane-vocabulary defect this file pins.
|
||||
*/
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_issue_analytics_lanes",
|
||||
projectId: "p1",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
|
||||
@@ -28,8 +28,16 @@ const IN_RANGE = "2026-06-15T12:00:00.000Z";
|
||||
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
|
||||
|
||||
pgDescribe("productivity analytics under a renamed board vocabulary", () => {
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
FN-8957 scoped analytics task reads to `layer.projectId`. This file aggregates under projectId
|
||||
"p1", so the harness binds the same project: an unbound harness writes store rows under the GUC
|
||||
default partition and the scoped aggregate reads zero rows — an isolation false-negative, not the
|
||||
lane-vocabulary defect this file pins.
|
||||
*/
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_productivity_lanes",
|
||||
projectId: "p1",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
|
||||
@@ -24,7 +24,13 @@ pgDescribe("project ownership runtime scope", () => {
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_project_ownership_runtime_scope",
|
||||
});
|
||||
const now = "2026-08-12T14:15:00.000Z";
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
Real wall-clock, not a literal: `decideApprovalRequest` rejects a pending request older than
|
||||
APPROVAL_REQUEST_PENDING_TTL_MS against Date.now(), so a hardcoded seed date expired the day
|
||||
after this file was written and turned the whole isolation case into a time bomb.
|
||||
*/
|
||||
const now = new Date().toISOString();
|
||||
const bind = (projectId: string): AsyncDataLayer => ({ ...h.layer(), projectId });
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
|
||||
@@ -101,6 +101,8 @@ import {
|
||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||
} from "../../postgres/schema-applier.js";
|
||||
import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
||||
import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.js";
|
||||
@@ -135,7 +137,11 @@ describe("schema-applier: immutable migration identities", () => {
|
||||
expect(PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION).toBe("0056");
|
||||
expect(PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION).toBe("0057");
|
||||
expect(MESSAGE_ARCHIVE_SCHEMA_VERSION).toBe("0058");
|
||||
expect(SCHEMA_BASELINE_VERSION).toBe("0058");
|
||||
/* FNXC:PgSchemaApplier 2026-08-15-22:10: 0059 (FN-9037 recommendation source-agent index) and
|
||||
0060 (FN-9059 workspace coordination leases/intents) advance the baseline to 0060. */
|
||||
expect(TASK_SOURCE_AGENT_INDEX_VERSION).toBe("0059");
|
||||
expect(WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION).toBe("0060");
|
||||
expect(SCHEMA_BASELINE_VERSION).toBe("0060");
|
||||
});
|
||||
|
||||
it("keeps monitor and approval isolation assigned to version 0003", () => {
|
||||
@@ -771,10 +777,11 @@ pgDescribe("schema-applier: VAL-SCHEMA-001 final-schema parity (table counts)",
|
||||
outbox tables; 0041 adds 4 lifecycle consumer tables; 0043 adds the durable unplanned-dispatch
|
||||
refusal marker (100 → 105); later baseline additions bring the count to 106; and 0048 adds
|
||||
GitHub check state (106 → 107); 0049 adds the agent-activity outbox and counter (→ 109);
|
||||
0050 adds immutable lock, evidence, and report history (109 → 112); 0052 adds recall records (→ 113). Plugin tables are added separately
|
||||
0050 adds immutable lock, evidence, and report history (109 → 112); 0052 adds recall records (→ 113);
|
||||
0060 adds workspace coordination leases and land intents (→ 115). Plugin tables are added separately
|
||||
by the schema-init hook and are excluded here.
|
||||
*/
|
||||
expect(bySchema.project).toBe(113);
|
||||
expect(bySchema.project).toBe(115);
|
||||
/*
|
||||
FNXC:CapacityModel 2026-07-29-08:10 (drop the cross-project cap — table half):
|
||||
17, not 18: `central.global_concurrency` is dropped by migration 0037. A fresh
|
||||
@@ -1644,7 +1651,13 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
|
||||
updated_at text NOT NULL
|
||||
);
|
||||
/* FNXC:GitHubImportTranslate 2026-07-16-23:30: Later durable-task migrations run after this historical 0000 fixture, so retain their required task table surface. */
|
||||
CREATE TABLE project.tasks (id text PRIMARY KEY);
|
||||
/*
|
||||
FNXC:PgSchemaApplier 2026-08-15-22:10:
|
||||
Migration 0059 (FN-9037) builds a partial index on tasks(project_id, source_agent_id).
|
||||
Real 0000 databases have source_agent_id (baseline since the PG cutover), so this
|
||||
historical fixture must retain it; project_id arrives via the 0006 ownership migration.
|
||||
*/
|
||||
CREATE TABLE project.tasks (id text PRIMARY KEY, source_agent_id text);
|
||||
/*
|
||||
FNXC:Ideation 2026-07-18-13:25:
|
||||
FN-8295 migration 0022 FKs ideation rows to missions/mission_features on (project_id, id).
|
||||
@@ -1809,6 +1822,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
|
||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||
]);
|
||||
expect((await applySchemaBaseline(ctx.db, { pluginHooks: [] })).applied).toBe(false);
|
||||
});
|
||||
@@ -1893,6 +1908,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
|
||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2110,6 +2127,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
|
||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2208,6 +2227,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
|
||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -2306,6 +2327,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
|
||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,8 +32,18 @@ const IN_RANGE = "2026-06-15T12:00:00.000Z";
|
||||
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z", now: Date.parse(IN_RANGE) };
|
||||
|
||||
pgDescribe("team analytics under a renamed board vocabulary", () => {
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
FN-8957 scoped every agent/task analytics read to `layer.projectId`. This file aggregates under
|
||||
projectId "p1", so the harness must BIND that same project: an unbound harness writes store rows
|
||||
under the GUC default partition ('__legacy_unscoped__' / '') and the scoped aggregate then reads
|
||||
zero rows — an isolation false-negative, not the lane-vocabulary defect this file pins.
|
||||
Raw adminDb seeds below carry an explicit project_id = 'p1' for the same reason (adminDb bypasses
|
||||
the bound GUC, so column defaults would land the row in the legacy partition).
|
||||
*/
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_team_analytics_lanes",
|
||||
projectId: "p1",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
@@ -71,8 +81,8 @@ pgDescribe("team analytics under a renamed board vocabulary", () => {
|
||||
const store = h.store();
|
||||
const adminDb = h.adminDb();
|
||||
await adminDb.execute(sql`
|
||||
INSERT INTO project.agents (id, name, role, state, created_at, updated_at)
|
||||
VALUES ('agent-1', 'Agent One', 'executor', 'idle', ${IN_RANGE}, ${IN_RANGE})`);
|
||||
INSERT INTO project.agents (id, project_id, name, role, state, created_at, updated_at)
|
||||
VALUES ('agent-1', 'p1', 'Agent One', 'executor', 'idle', ${IN_RANGE}, ${IN_RANGE})`);
|
||||
|
||||
for (const [id, lane] of [["KB-DONE", completeLane], ["KB-WIP", wipLane]] as const) {
|
||||
await store.createTaskWithReservedId(
|
||||
|
||||
@@ -52,8 +52,16 @@ const LEGACY_FLAGS = new Map<string, { countsTowardWip?: boolean; humanReview?:
|
||||
]);
|
||||
|
||||
pgDescribe("workflow analytics under a renamed board vocabulary", () => {
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
FN-8957 scoped analytics task reads to `layer.projectId`. This file aggregates under projectId
|
||||
"p1", so the harness binds the same project: an unbound harness writes store rows under the GUC
|
||||
default partition and the scoped aggregate reads zero rows — an isolation false-negative, not the
|
||||
lane-vocabulary defect this file pins.
|
||||
*/
|
||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||
prefix: "fusion_workflow_analytics_lanes",
|
||||
projectId: "p1",
|
||||
});
|
||||
|
||||
beforeAll(h.beforeAll);
|
||||
|
||||
@@ -107,7 +107,15 @@ pgTest("workflow definition create (PostgreSQL backend mode)", () => {
|
||||
const projectA = "proj_workflow_allocator_a";
|
||||
const projectB = "proj_workflow_allocator_b";
|
||||
const now = new Date().toISOString();
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
FN-8998 gave workflows composite (project_id, id) identity and project-scoped reads. The
|
||||
occupied row must therefore be stamped into project A's partition explicitly — adminDb has no
|
||||
bound GUC, so omitting project_id would strand the row in '__legacy_unscoped__' where the bound
|
||||
project-A read below could never prove it survived.
|
||||
*/
|
||||
await h.adminDb().insert(schema.project.workflows).values({
|
||||
projectId: projectA,
|
||||
id: "WF-002",
|
||||
name: "project A occupied workflow",
|
||||
description: "",
|
||||
|
||||
@@ -108,7 +108,13 @@ export function isTaskAwaitingPlanning(
|
||||
FNXC:DuplicateIntake 2026-08-01-19:24:
|
||||
A duplicate-only PROMPT is unplanned for execution — badge and triage must agree with
|
||||
scheduler filesystem validation so the card shows "Queued to plan", not Ready.
|
||||
|
||||
FNXC:DuplicateIntake 2026-08-15-22:10:
|
||||
`task.title` is a REQUIRED argument here, not an optional nicety: FN-8840 recognizes an exact
|
||||
`DUPLICATE: <ID>` redirect declared in the task TITLE even when PROMPT.md carries a real plan.
|
||||
A refactor once dropped the argument and title-only redirects silently read as executable
|
||||
("Ready") on the badge/triage surface — do not remove it again.
|
||||
*/
|
||||
if (isDuplicateRedirectOnlyPrompt(promptContent)) return true;
|
||||
if (isDuplicateRedirectOnlyPrompt(promptContent, task.title)) return true;
|
||||
return isUnplannedSeedPrompt(promptContent, task.id, task.title, task.description);
|
||||
}
|
||||
|
||||
@@ -828,7 +828,16 @@ export async function createWorkflowDefinitionImpl(store: TaskStore, input: Work
|
||||
|
||||
try {
|
||||
await workflowDefinitionBeforeInsertForTesting?.(id, store.backendMode);
|
||||
/*
|
||||
FNXC:MultiProjectIsolation 2026-08-15-22:10:
|
||||
Stamp the bound layer's project explicitly, like the FN-8997 workflowSteps insert does.
|
||||
FN-8998 scopes every workflow-definition READ by `layer.projectId`; leaving the INSERT to
|
||||
the session GUC default splits the write/read authority, so a layer bound in JS over a
|
||||
bypass connection creates a row it can never read back ('' normalizes to the GUC/legacy
|
||||
partition via the fusion_assign_project_id trigger, preserving unbound behavior).
|
||||
*/
|
||||
await store.asyncLayer!.db.insert(schema.project.workflows).values({
|
||||
projectId: store.asyncLayer!.projectId?.trim() ?? "",
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
|
||||
Reference in New Issue
Block a user