fix(cli): accept renumbered migration journal order (#11684)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Worktree provisioning clones a source database into an isolated workspace > - Source validation must accept a migration journal that matches a prefix of the checkout journal > - Long-lived instances can apply migrations in a different order after migration files are renumbered > - The validator compared application order with filename order and rejected a valid source > - This pull request compares the resolved migration names as a set and records the checkout-prefix revision > - The benefit is that valid renumbered migration histories can pass source validation without allowing divergent histories ## Linked Issues or Issue Description **What happened?** Worktree seed source validation compared applied migrations in database application order with available migration files in filename order. A current source with the same migration set failed with `Migration journal is not a prefix` after migration files were renumbered. **Expected behavior** Source validation must accept a source when its resolved applied migration set equals a prefix of the checkout migration files. It must still reject a source that contains a resolved migration outside that prefix. **Steps to reproduce** 1. Apply migrations before a migration-file renumber operation. 2. Update the checkout so the same migration files have a different filename order. 3. Run worktree seed source validation against the long-lived source. 4. Observe that positional comparison rejects the source even though the sets are equal. **Paperclip version or commit** The bug reproduces on the master-equivalent worktree-seeding implementation before this commit. **Deployment mode** Local dev with embedded PostgreSQL. ## What Changed - Compare resolved applied migration names with the expected checkout prefix as an order-independent set. - Derive the reported source revision from the checkout prefix instead of database application order. - Add unit and embedded-PostgreSQL regressions for shuffled application order, stale unresolved rows, lagging sources, and true divergence. ## Verification - `pnpm exec vitest run cli/src/__tests__/worktree.test.ts` — 54 tests passed. - `pnpm --filter paperclipai typecheck` — passed. - The focused suite includes the real embedded-PostgreSQL seed path. ## Risks - Low risk. The change is limited to source migration-prefix validation. - The validator still rejects missing or unknown resolved migrations. - Duplicate resolved names remain set-equivalent by design. Raw stale journal rows remain tolerated. - Existing documentation already specifies order-independent checkout-prefix behavior, so no documentation change is required. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, exact model ID `gpt-5.6-sol`. The Codex runtime manages the context window. The model used reasoning, shell tools, code execution, and test execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
536d5880c5
commit
51a843e135
|
|
@ -488,14 +488,34 @@ describe("worktree helpers", () => {
|
|||
expect(full.nullifyColumns).toEqual({});
|
||||
});
|
||||
|
||||
it("rejects a source migration journal that is ahead of the code journal", () => {
|
||||
it("rejects a source migration journal that diverges from the code journal", () => {
|
||||
expect(() => resolveWorktreeSeedMigrationRevision({
|
||||
status: "upToDate",
|
||||
tableCount: 1,
|
||||
availableMigrations: ["0001_initial.sql", "0002_current.sql"],
|
||||
appliedMigrations: ["0001_initial.sql", "0002_current.sql"],
|
||||
appliedMigrations: ["0001_initial.sql", "0003_unknown.sql"],
|
||||
journalEntryCount: 3,
|
||||
}, "sourcePrefix")).toThrow("Migration journal is ahead of this Paperclip checkout");
|
||||
}, "sourcePrefix")).toThrow("Migration journal is not a prefix of this Paperclip checkout");
|
||||
});
|
||||
|
||||
it("accepts a current source whose migration application order differs from filename order", () => {
|
||||
expect(resolveWorktreeSeedMigrationRevision({
|
||||
status: "upToDate",
|
||||
tableCount: 1,
|
||||
availableMigrations: [
|
||||
"0001_initial.sql",
|
||||
"0002_renumbered.sql",
|
||||
"0003_applied_earlier.sql",
|
||||
"0004_current.sql",
|
||||
],
|
||||
appliedMigrations: [
|
||||
"0001_initial.sql",
|
||||
"0003_applied_earlier.sql",
|
||||
"0002_renumbered.sql",
|
||||
"0004_current.sql",
|
||||
],
|
||||
journalEntryCount: 6,
|
||||
}, "upToDate")).toBe("0004_current.sql");
|
||||
});
|
||||
|
||||
it("accepts a source migration journal that is multiple revisions behind", () => {
|
||||
|
|
@ -508,9 +528,9 @@ describe("worktree helpers", () => {
|
|||
"0003_pending.sql",
|
||||
"0004_pending.sql",
|
||||
],
|
||||
appliedMigrations: ["0001_initial.sql", "0002_applied.sql"],
|
||||
appliedMigrations: ["0002_applied.sql", "0001_initial.sql"],
|
||||
pendingMigrations: ["0003_pending.sql", "0004_pending.sql"],
|
||||
journalEntryCount: 2,
|
||||
journalEntryCount: 3,
|
||||
reason: "pending-migrations",
|
||||
}, "sourcePrefix")).toBe("0002_applied.sql");
|
||||
});
|
||||
|
|
@ -1244,7 +1264,7 @@ describe("worktree helpers", () => {
|
|||
});
|
||||
|
||||
itEmbeddedPostgres(
|
||||
"seeds a source whose migration journal is behind the code journal",
|
||||
"seeds a lagging source whose migration application order differs from filename order",
|
||||
async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-"));
|
||||
const worktreeRoot = path.join(tempRoot, "PAP-999-auth-seed");
|
||||
|
|
@ -1264,7 +1284,30 @@ describe("worktree helpers", () => {
|
|||
DELETE FROM "drizzle"."__drizzle_migrations"
|
||||
WHERE "id" = (
|
||||
SELECT max("id") FROM "drizzle"."__drizzle_migrations"
|
||||
);
|
||||
|
||||
WITH pair AS (
|
||||
SELECT
|
||||
array_agg("id" ORDER BY "id" DESC) AS ids,
|
||||
array_agg("hash" ORDER BY "id" DESC) AS hashes
|
||||
FROM (
|
||||
SELECT "id", "hash"
|
||||
FROM "drizzle"."__drizzle_migrations"
|
||||
ORDER BY "id" DESC
|
||||
LIMIT 2
|
||||
) latest
|
||||
)
|
||||
UPDATE "drizzle"."__drizzle_migrations" migrations
|
||||
SET "hash" = CASE
|
||||
WHEN migrations."id" = pair.ids[1] THEN pair.hashes[2]
|
||||
WHEN migrations."id" = pair.ids[2] THEN pair.hashes[1]
|
||||
ELSE migrations."hash"
|
||||
END
|
||||
FROM pair
|
||||
WHERE migrations."id" IN (pair.ids[1], pair.ids[2]);
|
||||
|
||||
INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
|
||||
VALUES ('stale-unresolvable-migration-hash', 0)
|
||||
`);
|
||||
await sourceDbClient.$client.end({ timeout: 5 });
|
||||
const laggingMigrationState = await inspectMigrations(sourceDb.connectionString);
|
||||
|
|
@ -1273,7 +1316,18 @@ describe("worktree helpers", () => {
|
|||
throw new Error("Expected the source migration journal to lag the code journal");
|
||||
}
|
||||
expect(laggingMigrationState.pendingMigrations).toHaveLength(1);
|
||||
const sourceMigrationRevision = laggingMigrationState.appliedMigrations.at(-1);
|
||||
const expectedAppliedPrefix = laggingMigrationState.availableMigrations.slice(
|
||||
0,
|
||||
laggingMigrationState.appliedMigrations.length,
|
||||
);
|
||||
expect(laggingMigrationState.appliedMigrations).not.toEqual(expectedAppliedPrefix);
|
||||
expect([...laggingMigrationState.appliedMigrations].sort()).toEqual(
|
||||
[...expectedAppliedPrefix].sort(),
|
||||
);
|
||||
expect(laggingMigrationState.journalEntryCount).toBeGreaterThan(
|
||||
laggingMigrationState.appliedMigrations.length,
|
||||
);
|
||||
const sourceMigrationRevision = expectedAppliedPrefix.at(-1);
|
||||
expect(sourceMigrationRevision).toBeTruthy();
|
||||
|
||||
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
|
||||
|
|
|
|||
|
|
@ -1416,20 +1416,14 @@ export function resolveWorktreeSeedMigrationRevision(
|
|||
migrationState: Awaited<ReturnType<typeof inspectMigrations>>,
|
||||
requirement: "sourcePrefix" | "upToDate",
|
||||
): string {
|
||||
if (migrationState.journalEntryCount > migrationState.availableMigrations.length) {
|
||||
throw new Error(
|
||||
`Migration journal is ahead of this Paperclip checkout (${migrationState.journalEntryCount} applied migration(s), ${migrationState.availableMigrations.length} available).`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectedAppliedPrefix = migrationState.availableMigrations.slice(
|
||||
0,
|
||||
migrationState.appliedMigrations.length,
|
||||
);
|
||||
const appliedMigrationNames = new Set(migrationState.appliedMigrations);
|
||||
if (
|
||||
migrationState.appliedMigrations.some(
|
||||
(migration, index) => migration !== expectedAppliedPrefix[index],
|
||||
)
|
||||
appliedMigrationNames.size !== expectedAppliedPrefix.length ||
|
||||
expectedAppliedPrefix.some((migration) => !appliedMigrationNames.has(migration))
|
||||
) {
|
||||
throw new Error("Migration journal is not a prefix of this Paperclip checkout's migration journal.");
|
||||
}
|
||||
|
|
@ -1440,7 +1434,7 @@ export function resolveWorktreeSeedMigrationRevision(
|
|||
);
|
||||
}
|
||||
|
||||
const migrationRevision = migrationState.appliedMigrations.at(-1);
|
||||
const migrationRevision = expectedAppliedPrefix.at(-1);
|
||||
if (!migrationRevision) {
|
||||
throw new Error("Migration journal has no applied revision.");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue