mirror of
https://github.com/langgenius/dify.git
synced 2026-09-02 22:36:15 +08:00
1880 lines
71 KiB
TypeScript
1880 lines
71 KiB
TypeScript
import { createSchemaDatabaseAdapter } from "@knowledge/adapters";
|
|
import type { DatabaseExecuteInput, DatabaseExecuteResult } from "@knowledge/core";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createDatabaseKnowledgeSpaceProfileMigrationRepository } from "./knowledge-space-profile-migration-database-repository";
|
|
|
|
const tenantId = "tenant-migration";
|
|
const spaceId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a01";
|
|
const runId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a02";
|
|
const outboxId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a03";
|
|
const permissionId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a04";
|
|
const freshPermissionId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a09";
|
|
const embeddingId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a05";
|
|
const retrievalId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a06";
|
|
const candidateId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a07";
|
|
const publicationId = "018f0d60-7a49-7cc2-9c1b-5b36f18f4a08";
|
|
const digestA = "a".repeat(64);
|
|
const digestB = "b".repeat(64);
|
|
const fingerprint = `projection-set-sha256:${"c".repeat(64)}`;
|
|
const now = "2026-07-14T12:00:00.000Z";
|
|
const capabilityGrantId = "capability-profile-migration";
|
|
|
|
describe.each(["postgres", "tidb"] as const)(
|
|
"database profile migration repository (%s)",
|
|
(dialect) => {
|
|
it("admits one exact frozen tuple and atomically inserts its outbox", async () => {
|
|
const calls: DatabaseExecuteInput[] = [];
|
|
let inserted = false;
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
calls.push(input);
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "insert") {
|
|
inserted = true;
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.sql.includes("idempotency_key") || input.sql.includes("active_slot")) {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
return inserted ? { rows: [runRow()], rowsAffected: 1 } : { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_heads") {
|
|
return { rows: [{ id: "head" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
generateOutboxId: () => outboxId,
|
|
generateRunId: () => runId,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(repository.start(startInput())).resolves.toMatchObject({
|
|
candidateProfile: { id: candidateId, revision: 2, snapshotDigest: digestB },
|
|
runState: "queued",
|
|
});
|
|
await expect(repository.start(startInput())).resolves.toMatchObject({
|
|
id: runId,
|
|
rowVersion: 1,
|
|
runState: "queued",
|
|
});
|
|
await expect(
|
|
repository.start({
|
|
...startInput(),
|
|
candidateProfile: { id: candidateId, revision: 3, snapshotDigest: digestB },
|
|
}),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_IDEMPOTENCY_CONFLICT" });
|
|
const insert = calls.find(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_migration_runs" &&
|
|
call.operation === "insert",
|
|
);
|
|
expect(insert?.params).toEqual(
|
|
expect.arrayContaining([
|
|
"embedding",
|
|
candidateId,
|
|
2,
|
|
digestB,
|
|
embeddingId,
|
|
retrievalId,
|
|
publicationId,
|
|
fingerprint,
|
|
permissionId,
|
|
1,
|
|
]),
|
|
);
|
|
expect(insert?.sql).toContain(
|
|
dialect === "postgres"
|
|
? '"candidate_profile_snapshot_digest"'
|
|
: "`candidate_profile_snapshot_digest`",
|
|
);
|
|
expect(insert?.sql).toContain(
|
|
dialect === "postgres" ? '"idempotency_digest"' : "`idempotency_digest`",
|
|
);
|
|
expect(
|
|
insert?.params.some((value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value)),
|
|
).toBe(true);
|
|
expect(
|
|
calls.some(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_migration_outbox" &&
|
|
call.operation === "insert",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("looks up the latest candidate migration independently of authorization provenance", async () => {
|
|
const calls: DatabaseExecuteInput[] = [];
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
calls.push(input);
|
|
return { rows: [runRow()], rowsAffected: 1 };
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(
|
|
repository.findLatestByCandidate({
|
|
candidateProfileId: candidateId,
|
|
knowledgeSpaceId: spaceId,
|
|
tenantId,
|
|
}),
|
|
).resolves.toMatchObject({ id: runId, runState: "queued" });
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]?.params).toEqual([tenantId, spaceId, candidateId]);
|
|
expect(calls[0]?.sql).toContain(
|
|
dialect === "postgres"
|
|
? '"candidate_profile_revision_id"'
|
|
: "`candidate_profile_revision_id`",
|
|
);
|
|
expect(calls[0]?.sql).toContain(
|
|
dialect === "postgres" ? '"active_slot" IS NOT NULL' : "`active_slot` IS NOT NULL",
|
|
);
|
|
});
|
|
|
|
it("rejects malformed admissions and unsafe claim leases before database access", async () => {
|
|
const execute = async (): Promise<DatabaseExecuteResult> => {
|
|
throw new Error("database should not be called");
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 2,
|
|
});
|
|
|
|
await expect(
|
|
repository.start({ ...startInput(), rebuildScope: "clone-publication" }),
|
|
).rejects.toThrow("Embedding migration requires full-vector-space rebuild");
|
|
await expect(
|
|
repository.start({
|
|
...startInput(),
|
|
changedKind: "retrieval",
|
|
rebuildScope: "full-vector-space",
|
|
}),
|
|
).rejects.toThrow("Retrieval migration rebuild scope is invalid");
|
|
await expect(
|
|
repository.start({ ...startInput(), permissionSnapshotId: undefined }),
|
|
).rejects.toThrow("Profile migration requires exactly one authorization binding");
|
|
await expect(repository.start({ ...startInput(), accessChannel: undefined })).rejects.toThrow(
|
|
"Profile migration requires exactly one authorization binding",
|
|
);
|
|
await expect(
|
|
repository.start({
|
|
...startInput(),
|
|
accessChannel: undefined,
|
|
permissionSnapshotId: undefined,
|
|
}),
|
|
).rejects.toThrow("Profile migration requires exactly one authorization binding");
|
|
await expect(
|
|
repository.start({
|
|
...startInput(),
|
|
accessChannel: undefined,
|
|
permissionSnapshotId: undefined,
|
|
permissionSnapshotRevision: undefined,
|
|
}),
|
|
).rejects.toThrow("Profile migration requires exactly one authorization binding");
|
|
await expect(
|
|
repository.start({
|
|
...startInput(),
|
|
capabilityGrantId: "capability-migration",
|
|
}),
|
|
).rejects.toThrow("Profile migration requires exactly one authorization binding");
|
|
await expect(repository.start({ ...startInput(), maxExecutionAttempts: 0 })).rejects.toThrow(
|
|
"maxExecutionAttempts must be positive",
|
|
);
|
|
await expect(
|
|
repository.start({ ...startInput(), permissionSnapshotRevision: -1 }),
|
|
).rejects.toThrow("permissionSnapshotRevision must be positive");
|
|
await expect(repository.start({ ...startInput(), createdAt: "not-a-date" })).rejects.toThrow(
|
|
"Profile migration createdAt must be an ISO date-time",
|
|
);
|
|
await expect(
|
|
repository.claim({
|
|
leaseExpiresAt: "2026-07-14T12:10:00.000Z",
|
|
limit: 0,
|
|
now,
|
|
workerId: "worker-a",
|
|
}),
|
|
).rejects.toThrow("claim.limit must be positive");
|
|
await expect(
|
|
repository.claim({
|
|
leaseExpiresAt: "2026-07-14T12:10:00.000Z",
|
|
limit: 3,
|
|
now,
|
|
workerId: "worker-a",
|
|
}),
|
|
).rejects.toThrow("Profile migration claim exceeds limit");
|
|
await expect(
|
|
repository.claim({
|
|
leaseExpiresAt: now,
|
|
limit: 1,
|
|
now,
|
|
workerId: "worker-a",
|
|
}),
|
|
).rejects.toThrow("Profile migration lease must expire after now");
|
|
await expect(
|
|
repository.claim({
|
|
leaseExpiresAt: "2026-07-14T12:10:00.000Z",
|
|
limit: 1,
|
|
now,
|
|
workerId: "",
|
|
}),
|
|
).rejects.toThrow("workerId must not be empty");
|
|
});
|
|
|
|
it("admits an embedding migration when no prior embedding head exists", async () => {
|
|
let inserted = false;
|
|
const withoutEmbedding = runRow({
|
|
base_embedding_profile_revision: null,
|
|
base_embedding_profile_revision_id: null,
|
|
base_embedding_profile_snapshot_digest: null,
|
|
});
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "insert") {
|
|
inserted = true;
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.sql.includes("idempotency_digest") || input.sql.includes("active_slot")) {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
return inserted
|
|
? { rows: [withoutEmbedding], rowsAffected: 1 }
|
|
: { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_heads") {
|
|
return input.params[2] === "embedding"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [{ id: "retrieval-head" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
generateOutboxId: () => outboxId,
|
|
generateRunId: () => runId,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
const admitted = await repository.start({
|
|
...startInput(),
|
|
baseEmbeddingProfile: undefined,
|
|
});
|
|
expect(admitted).toMatchObject({ id: runId, runState: "queued" });
|
|
expect("baseEmbeddingProfile" in admitted).toBe(false);
|
|
await expect(
|
|
repository.start({ ...startInput(), baseEmbeddingProfile: undefined }),
|
|
).resolves.toMatchObject({ id: runId, runState: "queued" });
|
|
});
|
|
|
|
it("fails closed when admission references drift under the deletion lock", async () => {
|
|
let mode: "active-run" | "base-missing" | "candidate-missing" | "space-missing" =
|
|
"space-missing";
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return mode === "space-missing"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.sql.includes("idempotency_digest")) return { rows: [], rowsAffected: 0 };
|
|
return mode === "active-run"
|
|
? { rows: [{ id: "other-run" }], rowsAffected: 1 }
|
|
: { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return mode === "candidate-missing"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return mode === "base-missing"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(repository.start(startInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_SPACE_NOT_WRITABLE",
|
|
});
|
|
mode = "active-run";
|
|
await expect(repository.start(startInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_ALREADY_ACTIVE",
|
|
});
|
|
mode = "candidate-missing";
|
|
await expect(repository.start(startInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_CANDIDATE_INVALID",
|
|
});
|
|
mode = "base-missing";
|
|
await expect(repository.start(startInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_BASE_PUBLICATION_CHANGED",
|
|
});
|
|
});
|
|
|
|
it("rejects a digest collision after comparing the original idempotency tuple", async () => {
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
expect(input.sql).toContain(
|
|
dialect === "postgres" ? '"idempotency_digest"' : "`idempotency_digest`",
|
|
);
|
|
return {
|
|
rows: [runRow({ idempotency_key: "a-different-original-key" })],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(repository.start(startInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_IDEMPOTENCY_CONFLICT",
|
|
});
|
|
});
|
|
|
|
it("atomically binds a fresh exact owner permission snapshot when retrying", async () => {
|
|
const calls: DatabaseExecuteInput[] = [];
|
|
let current = runRow({
|
|
completed_at: now,
|
|
last_error_code: "PROFILE_MIGRATION_PERMISSION_INVALID",
|
|
last_error_message: "permission expired",
|
|
row_version: 4,
|
|
run_state: "failed",
|
|
});
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
calls.push(input);
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "update") {
|
|
current = {
|
|
...current,
|
|
completed_at: null,
|
|
last_error_code: null,
|
|
last_error_message: null,
|
|
permission_snapshot_id: String(input.params[0]),
|
|
permission_snapshot_revision: Number(input.params[1]),
|
|
row_version: Number(input.params[3]),
|
|
run_state: "queued",
|
|
updated_at: String(input.params[2]),
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.sql.includes("active_slot") && input.sql.includes("<>")) {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(freshPermissionId)], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return input.operation === "select"
|
|
? { rows: [{ revision: 1 }], rowsAffected: 1 }
|
|
: { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
generateOutboxId: () => outboxId,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(
|
|
repository.retry({
|
|
expectedPermissionSnapshotId: permissionId,
|
|
expectedPermissionSnapshotRevision: 1,
|
|
now,
|
|
permissionSnapshotId: freshPermissionId,
|
|
permissionSnapshotRevision: 1,
|
|
requestedBySubjectId: "owner-1",
|
|
runId,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
permissionSnapshotId: freshPermissionId,
|
|
permissionSnapshotRevision: 1,
|
|
runState: "queued",
|
|
});
|
|
const update = calls.find(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_migration_runs" &&
|
|
call.operation === "update",
|
|
);
|
|
expect(update?.params.slice(0, 2)).toEqual([freshPermissionId, 1]);
|
|
expect(update?.sql).toContain(
|
|
dialect === "postgres" ? '"permission_snapshot_id"' : "`permission_snapshot_id`",
|
|
);
|
|
});
|
|
|
|
it("fails cancellation closed when the fresh transaction permission is no longer owner", async () => {
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
return { rows: [runRow({ run_state: "running" })], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(freshPermissionId, { role: "editor" })], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(
|
|
repository.cancel({
|
|
accessChannel: "interactive",
|
|
now,
|
|
permissionSnapshotId: freshPermissionId,
|
|
permissionSnapshotRevision: 1,
|
|
reason: "cancel",
|
|
requestedBySubjectId: "owner-1",
|
|
runId,
|
|
}),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_PERMISSION_INVALID" });
|
|
});
|
|
|
|
it("fences the complete leased lifecycle through evaluation and success", async () => {
|
|
const workerId = "migration-worker-a";
|
|
const leaseToken = "migration-lease-a";
|
|
const initialLeaseExpiry = "2026-07-14T12:10:00.000Z";
|
|
const extendedLeaseExpiry = "2026-07-14T12:20:00.000Z";
|
|
const candidateFingerprint = `projection-set-sha256:${"d".repeat(64)}`;
|
|
const evaluationSummary = { passed: true, score: 0.98, ignored: { nested: true } };
|
|
let current: Record<string, unknown> = runRow();
|
|
const calls: DatabaseExecuteInput[] = [];
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
calls.push(input);
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "select" && input.sql.includes("INNER JOIN")) {
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (input.operation === "select") return { rows: [current], rowsAffected: 1 };
|
|
if (input.sql.includes("run_state") && input.sql.includes("'running'")) {
|
|
current = {
|
|
...current,
|
|
execution_attempts: Number(input.params[4]),
|
|
heartbeat_at: String(input.params[3]),
|
|
lease_expires_at: String(input.params[2]),
|
|
lease_token: String(input.params[1]),
|
|
row_version: Number(input.params[5]),
|
|
run_state: "running",
|
|
updated_at: String(input.params[3]),
|
|
worker_id: String(input.params[0]),
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.sql.includes("candidate_publication_id")) {
|
|
current = {
|
|
...current,
|
|
candidate_publication_fingerprint: input.params[2],
|
|
candidate_publication_id: input.params[1],
|
|
checkpoint: String(input.params[0]),
|
|
evaluation_summary: input.params[3],
|
|
row_version: Number(input.params[5]),
|
|
updated_at: String(input.params[4]),
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.sql.includes("active_slot")) {
|
|
current = {
|
|
...current,
|
|
canceled_at: input.params[5],
|
|
checkpoint: String(input.params[1]),
|
|
completed_at: String(input.params[4]),
|
|
heartbeat_at: null,
|
|
last_error_code: input.params[2],
|
|
last_error_message: input.params[3],
|
|
lease_expires_at: null,
|
|
lease_token: null,
|
|
row_version: Number(input.params[6]),
|
|
run_state: String(input.params[0]),
|
|
updated_at: String(input.params[4]),
|
|
worker_id: null,
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
current = {
|
|
...current,
|
|
heartbeat_at: String(input.params[1]),
|
|
lease_expires_at: String(input.params[0]),
|
|
row_version: Number(input.params[2]),
|
|
updated_at: String(input.params[1]),
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
generateLeaseToken: () => leaseToken,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
const [claimed] = await repository.claim({
|
|
leaseExpiresAt: initialLeaseExpiry,
|
|
limit: 1,
|
|
now,
|
|
workerId,
|
|
});
|
|
expect(claimed).toMatchObject({
|
|
executionAttempts: 1,
|
|
leaseToken,
|
|
rowVersion: 2,
|
|
runState: "running",
|
|
workerId,
|
|
});
|
|
|
|
await expect(
|
|
repository.heartbeat({
|
|
expectedRowVersion: 2,
|
|
leaseExpiresAt: extendedLeaseExpiry,
|
|
leaseToken,
|
|
now: "2026-07-14T12:01:00.000Z",
|
|
runId,
|
|
workerId,
|
|
}),
|
|
).resolves.toMatchObject({ leaseExpiresAt: extendedLeaseExpiry, rowVersion: 3 });
|
|
|
|
await expect(
|
|
repository.checkpoint({
|
|
candidatePublicationFingerprint: candidateFingerprint,
|
|
candidatePublicationId: publicationId,
|
|
checkpoint: "candidate-built",
|
|
expectedRowVersion: 3,
|
|
leaseToken,
|
|
now: "2026-07-14T12:02:00.000Z",
|
|
runId,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
candidatePublicationFingerprint: candidateFingerprint,
|
|
candidatePublicationId: publicationId,
|
|
checkpoint: "candidate-built",
|
|
rowVersion: 4,
|
|
});
|
|
|
|
await expect(
|
|
repository.checkpoint({
|
|
checkpoint: "evaluated",
|
|
evaluationSummary,
|
|
expectedRowVersion: 4,
|
|
leaseToken,
|
|
now: "2026-07-14T12:03:00.000Z",
|
|
runId,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
checkpoint: "evaluated",
|
|
evaluationSummary: { passed: true, score: 0.98 },
|
|
rowVersion: 5,
|
|
});
|
|
|
|
await expect(
|
|
repository.succeed({
|
|
expectedRowVersion: 5,
|
|
leaseToken,
|
|
now: "2026-07-14T12:04:00.000Z",
|
|
runId,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
checkpoint: "activated",
|
|
completedAt: "2026-07-14T12:04:00.000Z",
|
|
rowVersion: 6,
|
|
runState: "succeeded",
|
|
});
|
|
expect(
|
|
calls.some(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_migration_outbox" &&
|
|
call.operation === "update" &&
|
|
call.params[0] === "completed",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects checkpoint regressions and incomplete persisted evaluation state", async () => {
|
|
const leaseToken = "migration-lease-validation";
|
|
let current: Record<string, unknown> = runRow({
|
|
checkpoint: "candidate-built",
|
|
lease_expires_at: "2026-07-14T12:10:00.000Z",
|
|
lease_token: leaseToken,
|
|
row_version: 4,
|
|
run_state: "running",
|
|
worker_id: "worker-a",
|
|
});
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
const fence = { expectedRowVersion: 4, leaseToken, now, runId };
|
|
|
|
await expect(repository.checkpoint({ ...fence, checkpoint: "queued" })).rejects.toMatchObject(
|
|
{
|
|
code: "PROFILE_MIGRATION_CHECKPOINT_CONFLICT",
|
|
},
|
|
);
|
|
current = runRow({
|
|
lease_expires_at: "2026-07-14T12:10:00.000Z",
|
|
lease_token: leaseToken,
|
|
row_version: 4,
|
|
run_state: "running",
|
|
worker_id: "worker-a",
|
|
});
|
|
await expect(
|
|
repository.checkpoint({ ...fence, checkpoint: "candidate-built" }),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_CANDIDATE_PUBLICATION_REQUIRED" });
|
|
await expect(
|
|
repository.checkpoint({
|
|
...fence,
|
|
candidatePublicationFingerprint: fingerprint,
|
|
candidatePublicationId: publicationId,
|
|
checkpoint: "queued",
|
|
}),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_CHECKPOINT_CONFLICT" });
|
|
await expect(
|
|
repository.checkpoint({
|
|
...fence,
|
|
candidatePublicationFingerprint: fingerprint,
|
|
candidatePublicationId: publicationId,
|
|
checkpoint: "candidate-built",
|
|
evaluationSummary: { passed: true },
|
|
}),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_CHECKPOINT_CONFLICT" });
|
|
await expect(
|
|
repository.checkpoint({
|
|
...fence,
|
|
candidatePublicationFingerprint: fingerprint,
|
|
candidatePublicationId: publicationId,
|
|
checkpoint: "evaluated",
|
|
}),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_EVALUATION_REQUIRED" });
|
|
});
|
|
|
|
it("terminally closes an exhausted queued run without leasing it", async () => {
|
|
let current: Record<string, unknown> = runRow({
|
|
execution_attempts: 3,
|
|
max_execution_attempts: 3,
|
|
});
|
|
const calls: DatabaseExecuteInput[] = [];
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
calls.push(input);
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "select" && input.sql.includes("INNER JOIN")) {
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (input.operation === "update") {
|
|
current = {
|
|
...current,
|
|
checkpoint: String(input.params[1]),
|
|
completed_at: String(input.params[4]),
|
|
last_error_code: input.params[2],
|
|
last_error_message: input.params[3],
|
|
row_version: Number(input.params[6]),
|
|
run_state: String(input.params[0]),
|
|
updated_at: String(input.params[4]),
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_profile_revisions" ||
|
|
input.tableName === "knowledge_space_profile_publication_bindings" ||
|
|
input.tableName === "projection_set_publications" ||
|
|
input.tableName === "knowledge_space_profile_migration_outbox"
|
|
) {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(
|
|
repository.claim({
|
|
leaseExpiresAt: "2026-07-14T12:10:00.000Z",
|
|
limit: 1,
|
|
now,
|
|
workerId: "worker-a",
|
|
}),
|
|
).resolves.toEqual([]);
|
|
await expect(repository.get(runId)).resolves.toMatchObject({
|
|
lastErrorCode: "PROFILE_MIGRATION_ATTEMPTS_EXHAUSTED",
|
|
rowVersion: 2,
|
|
runState: "failed",
|
|
});
|
|
expect(
|
|
calls.some(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_migration_runs" &&
|
|
call.operation === "update" &&
|
|
call.params[0] === "failed",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("cancels an authorized active run and clears its candidate publication", async () => {
|
|
let current: Record<string, unknown> = runRow({ run_state: "running" });
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "update") {
|
|
current = {
|
|
...current,
|
|
canceled_at: String(input.params[5]),
|
|
checkpoint: String(input.params[1]),
|
|
completed_at: String(input.params[4]),
|
|
last_error_code: input.params[2],
|
|
last_error_message: input.params[3],
|
|
row_version: Number(input.params[6]),
|
|
run_state: String(input.params[0]),
|
|
updated_at: String(input.params[4]),
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_profile_revisions" ||
|
|
input.tableName === "knowledge_space_profile_publication_bindings" ||
|
|
input.tableName === "projection_set_publications" ||
|
|
input.tableName === "knowledge_space_profile_migration_outbox"
|
|
) {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(
|
|
repository.cancel({
|
|
accessChannel: "interactive",
|
|
now,
|
|
permissionSnapshotId: permissionId,
|
|
permissionSnapshotRevision: 1,
|
|
reason: "operator canceled migration",
|
|
requestedBySubjectId: "owner-1",
|
|
runId,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
canceledAt: now,
|
|
lastErrorCode: "PROFILE_MIGRATION_CANCELED",
|
|
rowVersion: 2,
|
|
runState: "canceled",
|
|
});
|
|
});
|
|
|
|
it("terminally fails the candidate and removes its unactivated publication binding", async () => {
|
|
const lease = "lease-terminal";
|
|
let current: Record<string, unknown> = runRow({
|
|
candidate_publication_fingerprint: fingerprint,
|
|
candidate_publication_id: publicationId,
|
|
checkpoint: "candidate-built",
|
|
execution_attempts: 1,
|
|
heartbeat_at: now,
|
|
lease_expires_at: "2026-07-14T12:10:00.000Z",
|
|
lease_token: lease,
|
|
row_version: 4,
|
|
run_state: "running",
|
|
worker_id: "worker-a",
|
|
});
|
|
const calls: DatabaseExecuteInput[] = [];
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
calls.push(input);
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "update") {
|
|
current = {
|
|
...current,
|
|
checkpoint: String(input.params[1]),
|
|
completed_at: String(input.params[4]),
|
|
heartbeat_at: null,
|
|
last_error_code: String(input.params[2]),
|
|
last_error_message: String(input.params[3]),
|
|
lease_expires_at: null,
|
|
lease_token: null,
|
|
row_version: Number(input.params[6]),
|
|
run_state: String(input.params[0]),
|
|
updated_at: String(input.params[4]),
|
|
worker_id: null,
|
|
};
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_profile_revisions" ||
|
|
input.tableName === "projection_set_publications" ||
|
|
input.tableName === "knowledge_space_profile_migration_outbox"
|
|
) {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_publication_bindings") {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
const repository = createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
maxClaimBatchSize: 10,
|
|
});
|
|
|
|
await expect(
|
|
repository.fail({
|
|
errorCode: "PROFILE_MIGRATION_EVALUATION_FAILED",
|
|
errorMessage: "candidate did not pass evaluation",
|
|
expectedRowVersion: 4,
|
|
leaseToken: lease,
|
|
now,
|
|
runId,
|
|
terminal: true,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
lastErrorCode: "PROFILE_MIGRATION_EVALUATION_FAILED",
|
|
runState: "failed",
|
|
});
|
|
|
|
const candidateFailure = calls.find(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_revisions" && call.operation === "update",
|
|
);
|
|
expect(candidateFailure?.sql).toContain("'failed'");
|
|
expect(candidateFailure?.params).toEqual(
|
|
expect.arrayContaining(["PROFILE_MIGRATION_EVALUATION_FAILED", candidateId, 2, digestB]),
|
|
);
|
|
expect(
|
|
calls.some(
|
|
(call) =>
|
|
call.tableName === "knowledge_space_profile_publication_bindings" &&
|
|
call.operation === "delete" &&
|
|
call.params.includes(publicationId),
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
calls.some(
|
|
(call) =>
|
|
call.tableName === "projection_set_publications" &&
|
|
call.operation === "update" &&
|
|
call.params.includes(publicationId),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("admits and replays a capability-bound request without legacy authorization fields", async () => {
|
|
let inserted = false;
|
|
const durableRow = capabilityRunRow();
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "capability_grants") {
|
|
return { rows: [{ grant_id: capabilityGrantId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.operation === "insert") {
|
|
inserted = true;
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.params[0] === "missing-run") return { rows: [], rowsAffected: 0 };
|
|
if (input.sql.includes("idempotency_digest")) {
|
|
return inserted
|
|
? { rows: [durableRow], rowsAffected: 1 }
|
|
: { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.sql.includes("active_slot")) return { rows: [], rowsAffected: 0 };
|
|
return inserted ? { rows: [durableRow], rowsAffected: 1 } : { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_heads") {
|
|
return { rows: [{ id: "profile-head" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const repository = createTestRepository(dialect, execute, {
|
|
generateOutboxId: () => outboxId,
|
|
generateRunId: () => runId,
|
|
});
|
|
|
|
await expect(repository.start(capabilityStartInput())).resolves.toMatchObject({
|
|
capabilityGrantId,
|
|
id: runId,
|
|
});
|
|
await expect(repository.start(capabilityStartInput())).resolves.toMatchObject({
|
|
capabilityGrantId,
|
|
id: runId,
|
|
});
|
|
await expect(
|
|
repository.findByRequest({
|
|
capabilityGrantId,
|
|
idempotencyKey: "settings-embedding-request",
|
|
knowledgeSpaceId: spaceId,
|
|
tenantId,
|
|
}),
|
|
).resolves.toMatchObject({ capabilityGrantId, id: runId });
|
|
await expect(repository.get("missing-run")).resolves.toBeNull();
|
|
|
|
const fencedRepository = createTestRepository(dialect, async (input) => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs" || input.tableName === "capability_grants") {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
});
|
|
await expect(fencedRepository.start(capabilityStartInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_PERMISSION_INVALID",
|
|
});
|
|
|
|
const storageFailure = new Error("permission storage unavailable");
|
|
const storageFailureRepository = createTestRepository(dialect, async (input) => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
throw storageFailure;
|
|
});
|
|
await expect(storageFailureRepository.start(startInput())).rejects.toBe(storageFailure);
|
|
});
|
|
|
|
it("fences claim replay and every claim-side CAS loss", async () => {
|
|
type Scenario =
|
|
| "capability-fenced"
|
|
| "capability-valid"
|
|
| "empty-lease"
|
|
| "outbox-lost"
|
|
| "run-disappeared"
|
|
| "run-lost";
|
|
let scenario: Scenario = "run-lost";
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (
|
|
input.tableName === "knowledge_space_profile_migration_runs" &&
|
|
input.operation === "select" &&
|
|
input.sql.includes("INNER JOIN")
|
|
) {
|
|
const row = scenario.startsWith("capability") ? capabilityRunRow() : runRow();
|
|
return { rows: [row], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "capability_grants") {
|
|
return scenario === "capability-fenced"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [{ grant_id: capabilityGrantId }], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_profile_migration_runs" &&
|
|
input.operation === "update"
|
|
) {
|
|
return scenario === "run-lost" || scenario === "capability-valid"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return scenario === "outbox-lost"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
return scenario === "run-disappeared"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [runRow()], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const repository = createTestRepository(dialect, execute, {
|
|
generateLeaseToken: () => (scenario === "empty-lease" ? " " : "claim-lease"),
|
|
});
|
|
const claim = () =>
|
|
repository.claim({
|
|
leaseExpiresAt: "2026-07-14T12:10:00.000Z",
|
|
limit: 1,
|
|
now,
|
|
workerId: "worker-a",
|
|
});
|
|
|
|
scenario = "empty-lease";
|
|
await expect(claim()).rejects.toThrow("leaseToken must not be empty");
|
|
scenario = "run-lost";
|
|
await expect(claim()).resolves.toEqual([]);
|
|
scenario = "outbox-lost";
|
|
await expect(claim()).rejects.toThrow("Profile migration outbox claim lost");
|
|
scenario = "run-disappeared";
|
|
await expect(claim()).rejects.toThrow("Profile migration run disappeared");
|
|
scenario = "capability-valid";
|
|
await expect(claim()).resolves.toEqual([]);
|
|
scenario = "capability-fenced";
|
|
await expect(claim()).rejects.toMatchObject({ code: "PROFILE_MIGRATION_PERMISSION_INVALID" });
|
|
});
|
|
|
|
it("returns null for stale lease fences and fails closed on write CAS loss", async () => {
|
|
type Scenario =
|
|
| "expired"
|
|
| "fail-sanitized"
|
|
| "missing"
|
|
| "terminal-lost"
|
|
| "update-lost"
|
|
| "wrong-worker";
|
|
let scenario: Scenario = "missing";
|
|
let current: Record<string, unknown> = runRow();
|
|
const runningRow = (overrides: Record<string, unknown> = {}) =>
|
|
runRow({
|
|
lease_expires_at: "2026-07-14T13:00:00.000Z",
|
|
lease_token: "lease-fence",
|
|
run_state: "running",
|
|
worker_id: "worker-a",
|
|
...overrides,
|
|
});
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (
|
|
input.tableName === "knowledge_space_profile_migration_runs" &&
|
|
input.operation === "select"
|
|
) {
|
|
return scenario === "missing"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_profile_migration_runs" &&
|
|
input.operation === "update"
|
|
) {
|
|
if (scenario === "update-lost" || scenario === "terminal-lost") {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (scenario === "fail-sanitized") {
|
|
current = {
|
|
...current,
|
|
checkpoint: input.params[1],
|
|
completed_at: input.params[4],
|
|
last_error_code: input.params[2],
|
|
last_error_message: input.params[3],
|
|
row_version: input.params[6],
|
|
run_state: input.params[0],
|
|
updated_at: input.params[4],
|
|
};
|
|
}
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const repository = createTestRepository(dialect, execute);
|
|
const fence = {
|
|
expectedRowVersion: 1,
|
|
leaseToken: "lease-fence",
|
|
now,
|
|
runId,
|
|
};
|
|
|
|
scenario = "missing";
|
|
await expect(
|
|
repository.heartbeat({
|
|
...fence,
|
|
leaseExpiresAt: "2026-07-14T14:00:00.000Z",
|
|
workerId: "worker-a",
|
|
}),
|
|
).resolves.toBeNull();
|
|
await expect(repository.checkpoint({ ...fence, checkpoint: "queued" })).resolves.toBeNull();
|
|
await expect(
|
|
repository.fail({
|
|
...fence,
|
|
errorCode: "retryable",
|
|
errorMessage: "temporary",
|
|
terminal: false,
|
|
}),
|
|
).resolves.toBeNull();
|
|
await expect(repository.succeed(fence)).resolves.toBeNull();
|
|
|
|
scenario = "expired";
|
|
current = runningRow({ lease_expires_at: now });
|
|
await expect(
|
|
repository.heartbeat({
|
|
...fence,
|
|
leaseExpiresAt: "2026-07-14T14:00:00.000Z",
|
|
workerId: "worker-a",
|
|
}),
|
|
).resolves.toBeNull();
|
|
|
|
scenario = "wrong-worker";
|
|
current = runningRow();
|
|
await expect(
|
|
repository.heartbeat({
|
|
...fence,
|
|
leaseExpiresAt: "2026-07-14T14:00:00.000Z",
|
|
workerId: "worker-b",
|
|
}),
|
|
).resolves.toBeNull();
|
|
|
|
scenario = "update-lost";
|
|
current = runningRow();
|
|
await expect(
|
|
repository.heartbeat({
|
|
...fence,
|
|
leaseExpiresAt: "2026-07-14T14:00:00.000Z",
|
|
workerId: "worker-a",
|
|
}),
|
|
).resolves.toBeNull();
|
|
await expect(repository.checkpoint({ ...fence, checkpoint: "queued" })).resolves.toBeNull();
|
|
current = runningRow({ checkpoint: "candidate-built" });
|
|
await expect(repository.succeed(fence)).resolves.toBeNull();
|
|
|
|
scenario = "terminal-lost";
|
|
current = runningRow();
|
|
await expect(
|
|
repository.fail({
|
|
...fence,
|
|
errorCode: "retryable",
|
|
errorMessage: "temporary",
|
|
terminal: false,
|
|
}),
|
|
).rejects.toThrow("Profile migration terminal transition lost fence");
|
|
|
|
scenario = "fail-sanitized";
|
|
current = runningRow();
|
|
await expect(
|
|
repository.fail({
|
|
...fence,
|
|
errorCode: " \n\t ",
|
|
errorMessage: " \r\n ",
|
|
terminal: false,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
lastErrorCode: "Profile migration failed",
|
|
lastErrorMessage: "Profile migration failed",
|
|
runState: "failed",
|
|
});
|
|
|
|
expect(() => createTestRepository(dialect, execute, { maxClaimBatchSize: 0 })).toThrow(
|
|
"maxClaimBatchSize must be positive",
|
|
);
|
|
});
|
|
|
|
it("short-circuits cancel safely across missing, terminal, and capability-bound runs", async () => {
|
|
type Scenario =
|
|
| "capability-terminal"
|
|
| "current-missing"
|
|
| "incomplete-authorization"
|
|
| "snapshot-missing"
|
|
| "space-fenced"
|
|
| "terminal";
|
|
let scenario: Scenario = "snapshot-missing";
|
|
let runReads = 0;
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
runReads += 1;
|
|
if (scenario === "snapshot-missing") return { rows: [], rowsAffected: 0 };
|
|
if (scenario === "current-missing" && runReads > 1) {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (scenario === "capability-terminal") {
|
|
return {
|
|
rows: [capabilityRunRow({ canceled_at: now, run_state: "canceled" })],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
return {
|
|
rows: [runRow({ run_state: scenario === "terminal" ? "succeeded" : "running" })],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return scenario === "space-fenced"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "capability_grants") {
|
|
return { rows: [{ grant_id: capabilityGrantId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const repository = createTestRepository(dialect, execute);
|
|
const legacyCancel = {
|
|
accessChannel: "interactive" as const,
|
|
now,
|
|
permissionSnapshotId: permissionId,
|
|
permissionSnapshotRevision: 1,
|
|
reason: "cancel migration",
|
|
requestedBySubjectId: "owner-1",
|
|
runId,
|
|
};
|
|
|
|
scenario = "snapshot-missing";
|
|
runReads = 0;
|
|
await expect(repository.cancel(legacyCancel)).resolves.toBeNull();
|
|
scenario = "space-fenced";
|
|
runReads = 0;
|
|
await expect(repository.cancel(legacyCancel)).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_SPACE_NOT_WRITABLE",
|
|
});
|
|
scenario = "current-missing";
|
|
runReads = 0;
|
|
await expect(repository.cancel(legacyCancel)).resolves.toBeNull();
|
|
scenario = "terminal";
|
|
runReads = 0;
|
|
await expect(repository.cancel(legacyCancel)).resolves.toMatchObject({
|
|
runState: "succeeded",
|
|
});
|
|
scenario = "capability-terminal";
|
|
runReads = 0;
|
|
await expect(
|
|
repository.cancel({ capabilityGrantId, now, reason: "cancel migration", runId }),
|
|
).resolves.toMatchObject({ capabilityGrantId, runState: "canceled" });
|
|
scenario = "incomplete-authorization";
|
|
runReads = 0;
|
|
await expect(repository.cancel({ now, reason: "cancel migration", runId })).rejects.toThrow(
|
|
"Profile migration authorization binding is incomplete",
|
|
);
|
|
});
|
|
|
|
it("fences retry replay, authorization mode, terminal errors, and retry CAS", async () => {
|
|
type Scenario =
|
|
| "active"
|
|
| "capability-mismatch"
|
|
| "capability-success"
|
|
| "current-missing"
|
|
| "legacy-incomplete"
|
|
| "mode-change"
|
|
| "not-failed"
|
|
| "permission-mismatch"
|
|
| "snapshot-missing"
|
|
| "space-fenced"
|
|
| "terminal"
|
|
| "update-lost";
|
|
let scenario: Scenario = "snapshot-missing";
|
|
let runReads = 0;
|
|
let current: Record<string, unknown> = runRow();
|
|
const failedRow = (overrides: Record<string, unknown> = {}) =>
|
|
runRow({
|
|
completed_at: now,
|
|
last_error_code: "PROFILE_MIGRATION_PERMISSION_INVALID",
|
|
last_error_message: "permission expired",
|
|
row_version: 4,
|
|
run_state: "failed",
|
|
...overrides,
|
|
});
|
|
const reset = (
|
|
next: Scenario,
|
|
overrides: Record<string, unknown> = {},
|
|
capability = false,
|
|
) => {
|
|
scenario = next;
|
|
runReads = 0;
|
|
current = capability
|
|
? capabilityRunRow({
|
|
completed_at: now,
|
|
last_error_code: "PROFILE_MIGRATION_PERMISSION_INVALID",
|
|
last_error_message: "capability revoked",
|
|
row_version: 4,
|
|
run_state: "failed",
|
|
...overrides,
|
|
})
|
|
: failedRow(overrides);
|
|
};
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (input.sql.includes("active_slot") && input.sql.includes("<>")) {
|
|
return scenario === "active"
|
|
? { rows: [{ id: "another-active-run" }], rowsAffected: 1 }
|
|
: { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.operation === "update") {
|
|
if (scenario === "update-lost") return { rows: [], rowsAffected: 0 };
|
|
if (current.capability_grant_id) {
|
|
current = {
|
|
...current,
|
|
completed_at: null,
|
|
row_version: input.params[2],
|
|
run_state: "queued",
|
|
updated_at: input.params[1],
|
|
};
|
|
} else {
|
|
current = {
|
|
...current,
|
|
completed_at: null,
|
|
permission_snapshot_id: input.params[0],
|
|
permission_snapshot_revision: input.params[1],
|
|
row_version: input.params[3],
|
|
run_state: "queued",
|
|
updated_at: input.params[2],
|
|
};
|
|
}
|
|
return { rows: [], rowsAffected: 1 };
|
|
}
|
|
runReads += 1;
|
|
if (scenario === "snapshot-missing") return { rows: [], rowsAffected: 0 };
|
|
if (scenario === "current-missing" && runReads > 1) {
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
return { rows: [current], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return scenario === "space-fenced"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "capability_grants") {
|
|
return { rows: [{ grant_id: capabilityGrantId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(freshPermissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_outbox") {
|
|
return input.operation === "select"
|
|
? { rows: [], rowsAffected: 0 }
|
|
: { rows: [], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const repository = createTestRepository(dialect, execute, {
|
|
generateOutboxId: () => outboxId,
|
|
});
|
|
const legacyRetry = {
|
|
expectedPermissionSnapshotId: permissionId,
|
|
expectedPermissionSnapshotRevision: 1,
|
|
now,
|
|
permissionSnapshotId: freshPermissionId,
|
|
permissionSnapshotRevision: 1,
|
|
requestedBySubjectId: "owner-1",
|
|
runId,
|
|
};
|
|
|
|
reset("snapshot-missing");
|
|
await expect(repository.retry(legacyRetry)).resolves.toBeNull();
|
|
reset("space-fenced");
|
|
await expect(repository.retry(legacyRetry)).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_SPACE_NOT_WRITABLE",
|
|
});
|
|
reset("current-missing");
|
|
await expect(repository.retry(legacyRetry)).resolves.toBeNull();
|
|
reset("permission-mismatch");
|
|
await expect(
|
|
repository.retry({ ...legacyRetry, expectedPermissionSnapshotId: "stale-snapshot" }),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_PERMISSION_SNAPSHOT_CONFLICT" });
|
|
reset("not-failed", { run_state: "queued" });
|
|
await expect(repository.retry(legacyRetry)).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_NOT_RETRYABLE",
|
|
});
|
|
reset("terminal", { last_error_code: "PROFILE_MIGRATION_EVALUATION_FAILED" });
|
|
await expect(repository.retry(legacyRetry)).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_NOT_RETRYABLE",
|
|
});
|
|
reset("active");
|
|
await expect(repository.retry(legacyRetry)).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_ALREADY_ACTIVE",
|
|
});
|
|
reset("legacy-incomplete", {
|
|
permission_snapshot_id: null,
|
|
permission_snapshot_revision: null,
|
|
});
|
|
await expect(
|
|
repository.retry({
|
|
...legacyRetry,
|
|
expectedPermissionSnapshotId: undefined,
|
|
expectedPermissionSnapshotRevision: undefined,
|
|
}),
|
|
).rejects.toThrow("Profile migration legacy retry authorization binding is incomplete");
|
|
reset("mode-change");
|
|
await expect(repository.retry({ ...legacyRetry, capabilityGrantId })).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_PERMISSION_SNAPSHOT_CONFLICT",
|
|
});
|
|
reset("update-lost");
|
|
await expect(repository.retry(legacyRetry)).resolves.toBeNull();
|
|
reset("capability-mismatch", {}, true);
|
|
await expect(
|
|
repository.retry({
|
|
capabilityGrantId,
|
|
expectedCapabilityGrantId: "stale-capability-grant",
|
|
now,
|
|
runId,
|
|
}),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_PERMISSION_SNAPSHOT_CONFLICT" });
|
|
reset("capability-success", {}, true);
|
|
await expect(
|
|
repository.retry({
|
|
capabilityGrantId,
|
|
expectedCapabilityGrantId: capabilityGrantId,
|
|
now,
|
|
runId,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
capabilityGrantId,
|
|
rowVersion: 5,
|
|
runState: "queued",
|
|
});
|
|
});
|
|
|
|
it("rejects drifted profile heads and malformed durable run rows", async () => {
|
|
type Scenario = "bad-json" | "bad-number" | "base-profile-missing" | "embedding-appeared";
|
|
let scenario: Scenario = "base-profile-missing";
|
|
const execute = async (input: DatabaseExecuteInput): Promise<DatabaseExecuteResult> => {
|
|
if (input.tableName === "knowledge_spaces") {
|
|
return {
|
|
rows: [{ deletion_job_id: null, id: spaceId, lifecycle_state: "active" }],
|
|
rowsAffected: 1,
|
|
};
|
|
}
|
|
if (input.tableName === "deletion_jobs") return { rows: [], rowsAffected: 0 };
|
|
if (input.tableName === "knowledge_space_permission_snapshots") {
|
|
return { rows: [permissionRow(permissionId)], rowsAffected: 1 };
|
|
}
|
|
if (
|
|
input.tableName === "knowledge_space_members" ||
|
|
input.tableName === "knowledge_space_access_policies" ||
|
|
input.tableName === "knowledge_space_api_access"
|
|
) {
|
|
return { rows: [{ id: "permission-lock" }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_migration_runs") {
|
|
if (scenario === "bad-json") {
|
|
return { rows: [runRow({ evaluation_summary: "not-json" })], rowsAffected: 1 };
|
|
}
|
|
if (scenario === "bad-number") {
|
|
return { rows: [runRow({ row_version: "not-a-number" })], rowsAffected: 1 };
|
|
}
|
|
return { rows: [], rowsAffected: 0 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_revisions") {
|
|
return { rows: [{ id: candidateId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "projection_set_publication_heads") {
|
|
return { rows: [{ id: publicationId }], rowsAffected: 1 };
|
|
}
|
|
if (input.tableName === "knowledge_space_profile_heads") {
|
|
if (scenario === "base-profile-missing") return { rows: [], rowsAffected: 0 };
|
|
return input.sql.includes("INNER JOIN")
|
|
? { rows: [{ id: retrievalId }], rowsAffected: 1 }
|
|
: { rows: [{ id: "unexpected-embedding-head" }], rowsAffected: 1 };
|
|
}
|
|
throw new Error(`Unexpected SQL table=${input.tableName}`);
|
|
};
|
|
const repository = createTestRepository(dialect, execute);
|
|
|
|
scenario = "base-profile-missing";
|
|
await expect(repository.start(startInput())).rejects.toMatchObject({
|
|
code: "PROFILE_MIGRATION_BASE_PROFILE_CHANGED",
|
|
});
|
|
scenario = "embedding-appeared";
|
|
await expect(
|
|
repository.start({ ...startInput(), baseEmbeddingProfile: undefined }),
|
|
).rejects.toMatchObject({ code: "PROFILE_MIGRATION_BASE_PROFILE_CHANGED" });
|
|
scenario = "bad-json";
|
|
await expect(repository.get(runId)).rejects.toThrow();
|
|
scenario = "bad-number";
|
|
await expect(repository.get(runId)).rejects.toThrow();
|
|
});
|
|
},
|
|
);
|
|
|
|
function startInput() {
|
|
return {
|
|
accessChannel: "interactive" as const,
|
|
baseEmbeddingProfile: { id: embeddingId, revision: 1, snapshotDigest: digestA },
|
|
basePublication: { fingerprint, headRevision: 9, id: publicationId },
|
|
baseRetrievalProfile: { id: retrievalId, revision: 1, snapshotDigest: digestA },
|
|
candidateProfile: { id: candidateId, revision: 2, snapshotDigest: digestB },
|
|
changedKind: "embedding" as const,
|
|
createdAt: now,
|
|
idempotencyKey: "settings-embedding-request",
|
|
knowledgeSpaceId: spaceId,
|
|
maxExecutionAttempts: 3,
|
|
permissionSnapshotId: permissionId,
|
|
permissionSnapshotRevision: 1,
|
|
rebuildScope: "full-vector-space" as const,
|
|
requestedBySubjectId: "owner-1",
|
|
tenantId,
|
|
};
|
|
}
|
|
|
|
function runRow(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
access_channel: "interactive",
|
|
base_embedding_profile_revision: 1,
|
|
base_embedding_profile_revision_id: embeddingId,
|
|
base_embedding_profile_snapshot_digest: digestA,
|
|
base_publication_fingerprint: fingerprint,
|
|
base_publication_head_revision: 9,
|
|
base_publication_id: publicationId,
|
|
base_retrieval_profile_revision: 1,
|
|
base_retrieval_profile_revision_id: retrievalId,
|
|
base_retrieval_profile_snapshot_digest: digestA,
|
|
canceled_at: null,
|
|
candidate_profile_revision: 2,
|
|
candidate_profile_revision_id: candidateId,
|
|
candidate_profile_snapshot_digest: digestB,
|
|
candidate_publication_fingerprint: null,
|
|
candidate_publication_id: null,
|
|
changed_kind: "embedding",
|
|
checkpoint: "queued",
|
|
completed_at: null,
|
|
created_at: now,
|
|
evaluation_summary: null,
|
|
execution_attempts: 0,
|
|
heartbeat_at: null,
|
|
id: runId,
|
|
idempotency_key: "settings-embedding-request",
|
|
knowledge_space_id: spaceId,
|
|
last_error_code: null,
|
|
last_error_message: null,
|
|
lease_expires_at: null,
|
|
lease_token: null,
|
|
max_execution_attempts: 3,
|
|
permission_snapshot_id: permissionId,
|
|
permission_snapshot_revision: 1,
|
|
rebuild_scope: "full-vector-space",
|
|
requested_by_subject_id: "owner-1",
|
|
row_version: 1,
|
|
run_state: "queued",
|
|
tenant_id: tenantId,
|
|
updated_at: now,
|
|
worker_id: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function permissionRow(id: string, overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
access_channel: "interactive",
|
|
access_policy_revision: 1,
|
|
api_access_revision: 1,
|
|
api_key_expires_at: null,
|
|
api_key_id: null,
|
|
api_key_revision: null,
|
|
created_at: now,
|
|
expires_at: "2026-07-14T13:00:00.000Z",
|
|
id,
|
|
knowledge_space_id: spaceId,
|
|
member_revision: 1,
|
|
permission_scopes: JSON.stringify([]),
|
|
revision: 1,
|
|
revoked_at: null,
|
|
role: "owner",
|
|
status: "active",
|
|
subject_id: "owner-1",
|
|
tenant_id: tenantId,
|
|
updated_at: now,
|
|
visibility: "only_me",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
interface TestRepositoryOverrides {
|
|
readonly generateLeaseToken?: (() => string) | undefined;
|
|
readonly generateOutboxId?: (() => string) | undefined;
|
|
readonly generateRunId?: (() => string) | undefined;
|
|
readonly maxClaimBatchSize?: number | undefined;
|
|
}
|
|
|
|
function createTestRepository(
|
|
dialect: "postgres" | "tidb",
|
|
execute: (input: DatabaseExecuteInput) => Promise<DatabaseExecuteResult>,
|
|
overrides: TestRepositoryOverrides = {},
|
|
) {
|
|
const database = createSchemaDatabaseAdapter({
|
|
executor: execute,
|
|
kind: dialect,
|
|
transaction: async (callback) => callback({ execute }),
|
|
});
|
|
return createDatabaseKnowledgeSpaceProfileMigrationRepository({
|
|
database,
|
|
...(overrides.generateLeaseToken ? { generateLeaseToken: overrides.generateLeaseToken } : {}),
|
|
...(overrides.generateOutboxId ? { generateOutboxId: overrides.generateOutboxId } : {}),
|
|
...(overrides.generateRunId ? { generateRunId: overrides.generateRunId } : {}),
|
|
maxClaimBatchSize: overrides.maxClaimBatchSize ?? 10,
|
|
});
|
|
}
|
|
|
|
function capabilityStartInput() {
|
|
return {
|
|
baseEmbeddingProfile: { id: embeddingId, revision: 1, snapshotDigest: digestA },
|
|
basePublication: { fingerprint, headRevision: 9, id: publicationId },
|
|
baseRetrievalProfile: { id: retrievalId, revision: 1, snapshotDigest: digestA },
|
|
candidateProfile: { id: candidateId, revision: 2, snapshotDigest: digestB },
|
|
capabilityGrantId,
|
|
changedKind: "embedding" as const,
|
|
createdAt: now,
|
|
idempotencyKey: "settings-embedding-request",
|
|
knowledgeSpaceId: spaceId,
|
|
maxExecutionAttempts: 3,
|
|
rebuildScope: "full-vector-space" as const,
|
|
tenantId,
|
|
};
|
|
}
|
|
|
|
function capabilityRunRow(overrides: Record<string, unknown> = {}) {
|
|
return runRow({
|
|
access_channel: null,
|
|
capability_grant_id: capabilityGrantId,
|
|
permission_snapshot_id: null,
|
|
permission_snapshot_revision: null,
|
|
requested_by_subject_id: null,
|
|
...overrides,
|
|
});
|
|
}
|