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 movedIds = new Set(BUILTIN_MOVED_WORKFLOW_SETTINGS.map((setting) => setting.id));
|
||||||
const movedKeyIds = new Set(MOVED_SETTINGS_KEYS);
|
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([
|
expect(BUILTIN_OVERSIGHT_SETTINGS.map((setting) => setting.id)).toEqual([
|
||||||
|
"memoryConsolidationEnabled",
|
||||||
"plannerOversightLevel",
|
"plannerOversightLevel",
|
||||||
"plannerOversightNotificationLevel",
|
"plannerOversightNotificationLevel",
|
||||||
"plannerOverseerExecutorStuckAfterMs",
|
"plannerOverseerExecutorStuckAfterMs",
|
||||||
@@ -156,7 +163,13 @@ describe("workflow-native built-in workflow settings", () => {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false,
|
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({
|
expect(oversight).toMatchObject({
|
||||||
type: "enum",
|
type: "enum",
|
||||||
default: "autonomous",
|
default: "autonomous",
|
||||||
@@ -180,7 +193,7 @@ describe("workflow-native built-in workflow settings", () => {
|
|||||||
"plannerOversightLevel should not be in MOVED_SETTINGS_KEYS",
|
"plannerOversightLevel should not be in MOVED_SETTINGS_KEYS",
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
const notificationLevel = BUILTIN_OVERSIGHT_SETTINGS[1];
|
const notificationLevel = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOversightNotificationLevel")!;
|
||||||
expect(notificationLevel).toMatchObject({
|
expect(notificationLevel).toMatchObject({
|
||||||
id: "plannerOversightNotificationLevel",
|
id: "plannerOversightNotificationLevel",
|
||||||
type: "enum",
|
type: "enum",
|
||||||
@@ -213,7 +226,7 @@ describe("workflow-native built-in workflow settings", () => {
|
|||||||
|
|
||||||
// FN-7743: executor-stall recovery threshold, declared alongside the other
|
// FN-7743: executor-stall recovery threshold, declared alongside the other
|
||||||
// workflow-native oversight settings.
|
// workflow-native oversight settings.
|
||||||
const executorStuckAfterMs = BUILTIN_OVERSIGHT_SETTINGS[2];
|
const executorStuckAfterMs = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOverseerExecutorStuckAfterMs")!;
|
||||||
expect(executorStuckAfterMs).toMatchObject({
|
expect(executorStuckAfterMs).toMatchObject({
|
||||||
id: "plannerOverseerExecutorStuckAfterMs",
|
id: "plannerOverseerExecutorStuckAfterMs",
|
||||||
type: "number",
|
type: "number",
|
||||||
@@ -232,7 +245,7 @@ describe("workflow-native built-in workflow settings", () => {
|
|||||||
"plannerOverseerExecutorStuckAfterMs should not be in MOVED_SETTINGS_KEYS",
|
"plannerOverseerExecutorStuckAfterMs should not be in MOVED_SETTINGS_KEYS",
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
|
|
||||||
const heartbeatPatrol = BUILTIN_OVERSIGHT_SETTINGS[6];
|
const heartbeatPatrol = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerHeartbeatPatrolEnabled")!;
|
||||||
expect(heartbeatPatrol).toMatchObject({
|
expect(heartbeatPatrol).toMatchObject({
|
||||||
id: "plannerHeartbeatPatrolEnabled",
|
id: "plannerHeartbeatPatrolEnabled",
|
||||||
type: "boolean",
|
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" };
|
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
|
||||||
|
|
||||||
pgDescribe("issue analytics under a renamed board vocabulary", () => {
|
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({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
prefix: "fusion_issue_analytics_lanes",
|
prefix: "fusion_issue_analytics_lanes",
|
||||||
|
projectId: "p1",
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(h.beforeAll);
|
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" };
|
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
|
||||||
|
|
||||||
pgDescribe("productivity analytics under a renamed board vocabulary", () => {
|
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({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
prefix: "fusion_productivity_lanes",
|
prefix: "fusion_productivity_lanes",
|
||||||
|
projectId: "p1",
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(h.beforeAll);
|
beforeAll(h.beforeAll);
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ pgDescribe("project ownership runtime scope", () => {
|
|||||||
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
prefix: "fusion_project_ownership_runtime_scope",
|
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 });
|
const bind = (projectId: string): AsyncDataLayer => ({ ...h.layer(), projectId });
|
||||||
|
|
||||||
beforeAll(h.beforeAll);
|
beforeAll(h.beforeAll);
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ import {
|
|||||||
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
|
||||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||||
|
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||||
|
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||||
} from "../../postgres/schema-applier.js";
|
} from "../../postgres/schema-applier.js";
|
||||||
import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
||||||
import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.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_DECLARATION_DRIFT_VERSION).toBe("0056");
|
||||||
expect(PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION).toBe("0057");
|
expect(PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION).toBe("0057");
|
||||||
expect(MESSAGE_ARCHIVE_SCHEMA_VERSION).toBe("0058");
|
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", () => {
|
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
|
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
|
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);
|
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.
|
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):
|
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
|
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
|
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. */
|
/* 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:
|
FNXC:Ideation 2026-07-18-13:25:
|
||||||
FN-8295 migration 0022 FKs ideation rows to missions/mission_features on (project_id, id).
|
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_DECLARATION_DRIFT_VERSION,
|
||||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||||
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
MESSAGE_ARCHIVE_SCHEMA_VERSION,
|
||||||
|
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||||
|
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||||
]);
|
]);
|
||||||
expect((await applySchemaBaseline(ctx.db, { pluginHooks: [] })).applied).toBe(false);
|
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_DECLARATION_DRIFT_VERSION,
|
||||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||||
MESSAGE_ARCHIVE_SCHEMA_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_DECLARATION_DRIFT_VERSION,
|
||||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||||
MESSAGE_ARCHIVE_SCHEMA_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_DECLARATION_DRIFT_VERSION,
|
||||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||||
MESSAGE_ARCHIVE_SCHEMA_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_DECLARATION_DRIFT_VERSION,
|
||||||
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
|
||||||
MESSAGE_ARCHIVE_SCHEMA_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) };
|
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", () => {
|
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({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
prefix: "fusion_team_analytics_lanes",
|
prefix: "fusion_team_analytics_lanes",
|
||||||
|
projectId: "p1",
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(h.beforeAll);
|
beforeAll(h.beforeAll);
|
||||||
@@ -71,8 +81,8 @@ pgDescribe("team analytics under a renamed board vocabulary", () => {
|
|||||||
const store = h.store();
|
const store = h.store();
|
||||||
const adminDb = h.adminDb();
|
const adminDb = h.adminDb();
|
||||||
await adminDb.execute(sql`
|
await adminDb.execute(sql`
|
||||||
INSERT INTO project.agents (id, name, role, state, created_at, updated_at)
|
INSERT INTO project.agents (id, project_id, name, role, state, created_at, updated_at)
|
||||||
VALUES ('agent-1', 'Agent One', 'executor', 'idle', ${IN_RANGE}, ${IN_RANGE})`);
|
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) {
|
for (const [id, lane] of [["KB-DONE", completeLane], ["KB-WIP", wipLane]] as const) {
|
||||||
await store.createTaskWithReservedId(
|
await store.createTaskWithReservedId(
|
||||||
|
|||||||
@@ -52,8 +52,16 @@ const LEGACY_FLAGS = new Map<string, { countsTowardWip?: boolean; humanReview?:
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
pgDescribe("workflow analytics under a renamed board vocabulary", () => {
|
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({
|
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
|
||||||
prefix: "fusion_workflow_analytics_lanes",
|
prefix: "fusion_workflow_analytics_lanes",
|
||||||
|
projectId: "p1",
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(h.beforeAll);
|
beforeAll(h.beforeAll);
|
||||||
|
|||||||
@@ -107,7 +107,15 @@ pgTest("workflow definition create (PostgreSQL backend mode)", () => {
|
|||||||
const projectA = "proj_workflow_allocator_a";
|
const projectA = "proj_workflow_allocator_a";
|
||||||
const projectB = "proj_workflow_allocator_b";
|
const projectB = "proj_workflow_allocator_b";
|
||||||
const now = new Date().toISOString();
|
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({
|
await h.adminDb().insert(schema.project.workflows).values({
|
||||||
|
projectId: projectA,
|
||||||
id: "WF-002",
|
id: "WF-002",
|
||||||
name: "project A occupied workflow",
|
name: "project A occupied workflow",
|
||||||
description: "",
|
description: "",
|
||||||
|
|||||||
@@ -108,7 +108,13 @@ export function isTaskAwaitingPlanning(
|
|||||||
FNXC:DuplicateIntake 2026-08-01-19:24:
|
FNXC:DuplicateIntake 2026-08-01-19:24:
|
||||||
A duplicate-only PROMPT is unplanned for execution — badge and triage must agree with
|
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.
|
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);
|
return isUnplannedSeedPrompt(promptContent, task.id, task.title, task.description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -828,7 +828,16 @@ export async function createWorkflowDefinitionImpl(store: TaskStore, input: Work
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await workflowDefinitionBeforeInsertForTesting?.(id, store.backendMode);
|
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({
|
await store.asyncLayer!.db.insert(schema.project.workflows).values({
|
||||||
|
projectId: store.asyncLayer!.projectId?.trim() ?? "",
|
||||||
id: definition.id,
|
id: definition.id,
|
||||||
name: definition.name,
|
name: definition.name,
|
||||||
description: definition.description,
|
description: definition.description,
|
||||||
|
|||||||
Reference in New Issue
Block a user