diff --git a/backend-tests/MigrationSnapshotTests.cs b/backend-tests/MigrationSnapshotTests.cs index a1fc677..cd66df1 100644 --- a/backend-tests/MigrationSnapshotTests.cs +++ b/backend-tests/MigrationSnapshotTests.cs @@ -1,3 +1,7 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Nexus.Api.Data; using Nexus.Api.Migrations; using Xunit; @@ -5,6 +9,21 @@ namespace Nexus.Api.Tests; public sealed class MigrationSnapshotTests { + [Fact] + public void Historical_task_migrations_are_discoverable() + { + var options = new DbContextOptionsBuilder() + .UseNpgsql( + "Host=127.0.0.1;Database=nexus_migration_contract;Username=nexus;Password=unused") + .Options; + using var db = new NexusDbContext(options); + var migrations = db.GetService().Migrations.Keys; + + Assert.Contains("20260618233000_AddTaskParentChild", migrations); + Assert.Contains("20260618233001_AddTaskDueDate", migrations); + Assert.Contains("20260618233002_AddActivityTaskReference", migrations); + } + [Fact] public void Snapshot_builds_agent_provisioning_relationships() { diff --git a/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs b/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs index b5ad504..f9001d0 100644 --- a/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs +++ b/backend-tests/ToxiproxyAgentProvisioningIntegrationTests.cs @@ -140,7 +140,14 @@ public sealed class ToxiproxyAgentProvisioningIntegrationTests var reconciled = await service.GetByIdAsync(created.Proposal.Id); Assert.NotNull(reconciled); - Assert.Equal(AgentProposalStates.Ready, reconciled!.Status); + Assert.True( + string.Equals( + AgentProposalStates.Ready, + reconciled!.Status, + StringComparison.Ordinal), + $"Expected ready after read-only reconciliation, got " + + $"'{reconciled.Status}' ({reconciled.Error?.Code}: " + + $"{reconciled.Error?.Message})."); Assert.Equal("release-analyst", reconciled.OpenClawAgentId); Assert.Equal(1, gateway.CreateCalls); } diff --git a/backend/Data/Migrations/20260618233000_AddTaskParentChild.cs b/backend/Data/Migrations/20260618233000_AddTaskParentChild.cs index 4c20f17..550aeb9 100644 --- a/backend/Data/Migrations/20260618233000_AddTaskParentChild.cs +++ b/backend/Data/Migrations/20260618233000_AddTaskParentChild.cs @@ -1,49 +1,59 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using Nexus.Api.Data; #nullable disable namespace Nexus.Api.Migrations { /// + [DbContext(typeof(NexusDbContext))] + [Migration("20260618233000_AddTaskParentChild")] public partial class AddTaskParentChild : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "ParentTaskId", - table: "Tasks", - type: "uuid", - nullable: true); + // This historical migration originally lacked EF discovery + // metadata. Keep the catch-up operations idempotent so existing + // installations and fresh PostgreSQL databases converge safely. + migrationBuilder.Sql( + """ + ALTER TABLE "Tasks" + ADD COLUMN IF NOT EXISTS "ParentTaskId" uuid; - migrationBuilder.CreateIndex( - name: "IX_Tasks_ParentTaskId", - table: "Tasks", - column: "ParentTaskId"); + CREATE INDEX IF NOT EXISTS "IX_Tasks_ParentTaskId" + ON "Tasks" ("ParentTaskId"); - migrationBuilder.AddForeignKey( - name: "FK_Tasks_Tasks_ParentTaskId", - table: "Tasks", - column: "ParentTaskId", - principalTable: "Tasks", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'FK_Tasks_Tasks_ParentTaskId' + AND conrelid = '"Tasks"'::regclass + ) THEN + ALTER TABLE "Tasks" + ADD CONSTRAINT "FK_Tasks_Tasks_ParentTaskId" + FOREIGN KEY ("ParentTaskId") + REFERENCES "Tasks" ("Id") + ON DELETE SET NULL; + END IF; + END $$; + """); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_Tasks_Tasks_ParentTaskId", - table: "Tasks"); - - migrationBuilder.DropIndex( - name: "IX_Tasks_ParentTaskId", - table: "Tasks"); - - migrationBuilder.DropColumn( - name: "ParentTaskId", - table: "Tasks"); + migrationBuilder.Sql( + """ + ALTER TABLE "Tasks" + DROP CONSTRAINT IF EXISTS "FK_Tasks_Tasks_ParentTaskId"; + DROP INDEX IF EXISTS "IX_Tasks_ParentTaskId"; + ALTER TABLE "Tasks" + DROP COLUMN IF EXISTS "ParentTaskId"; + """); } } } diff --git a/backend/Data/Migrations/20260618233001_AddTaskDueDate.cs b/backend/Data/Migrations/20260618233001_AddTaskDueDate.cs index fe37f4f..9d33f98 100644 --- a/backend/Data/Migrations/20260618233001_AddTaskDueDate.cs +++ b/backend/Data/Migrations/20260618233001_AddTaskDueDate.cs @@ -1,28 +1,34 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using Nexus.Api.Data; #nullable disable namespace Nexus.Api.Migrations { /// + [DbContext(typeof(NexusDbContext))] + [Migration("20260618233001_AddTaskDueDate")] public partial class AddTaskDueDate : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "DueDate", - table: "Tasks", - type: "timestamp with time zone", - nullable: true); + migrationBuilder.Sql( + """ + ALTER TABLE "Tasks" + ADD COLUMN IF NOT EXISTS "DueDate" timestamp with time zone; + """); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "DueDate", - table: "Tasks"); + migrationBuilder.Sql( + """ + ALTER TABLE "Tasks" + DROP COLUMN IF EXISTS "DueDate"; + """); } } } diff --git a/backend/Data/Migrations/20260618233002_AddActivityTaskReference.cs b/backend/Data/Migrations/20260618233002_AddActivityTaskReference.cs index 914cc68..e3849ef 100644 --- a/backend/Data/Migrations/20260618233002_AddActivityTaskReference.cs +++ b/backend/Data/Migrations/20260618233002_AddActivityTaskReference.cs @@ -1,37 +1,38 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using Nexus.Api.Data; #nullable disable namespace Nexus.Api.Migrations { /// + [DbContext(typeof(NexusDbContext))] + [Migration("20260618233002_AddActivityTaskReference")] public partial class AddActivityTaskReference : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AddColumn( - name: "TaskId", - table: "Activity", - type: "uuid", - nullable: true); + migrationBuilder.Sql( + """ + ALTER TABLE "Activity" + ADD COLUMN IF NOT EXISTS "TaskId" uuid; - migrationBuilder.CreateIndex( - name: "IX_Activity_TaskId", - table: "Activity", - column: "TaskId"); + CREATE INDEX IF NOT EXISTS "IX_Activity_TaskId" + ON "Activity" ("TaskId"); + """); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropIndex( - name: "IX_Activity_TaskId", - table: "Activity"); - - migrationBuilder.DropColumn( - name: "TaskId", - table: "Activity"); + migrationBuilder.Sql( + """ + DROP INDEX IF EXISTS "IX_Activity_TaskId"; + ALTER TABLE "Activity" + DROP COLUMN IF EXISTS "TaskId"; + """); } } }