fix(knowledge-fs): remove provider mode from sync policies and update related constraints

This commit is contained in:
FFXN 2026-08-28 14:06:49 +08:00
parent 20fc6c130d
commit ae495d3f97
16 changed files with 67 additions and 38 deletions

View File

@ -190,7 +190,7 @@ class KnowledgeFSOnlineDriveWorkflowImportItemPayload(BaseModel):
class KnowledgeFSInitialSyncPolicyPayload(BaseModel):
custom_interval_seconds: int | None = Field(default=None, ge=3_600, le=2_592_000)
sync_policy: Literal["provider", "daily", "manual", "custom"] = "provider"
sync_policy: Literal["daily", "manual", "custom"] = "daily"
model_config = ConfigDict(extra="forbid")
@ -2113,7 +2113,7 @@ class KnowledgeFSSourceSyncPolicyResponse(ResponseModel):
)
id: str
knowledge_space_id: str = Field(validation_alias=AliasChoices("knowledge_space_id", "knowledgeSpaceId"))
mode: Literal["provider", "manual", "interval", "custom"]
mode: Literal["manual", "interval", "custom"]
next_run_at: datetime | None = Field(default=None, validation_alias=AliasChoices("next_run_at", "nextRunAt"))
revision: int = Field(ge=1)
source_id: str = Field(validation_alias=AliasChoices("source_id", "sourceId"))
@ -2350,7 +2350,7 @@ class KnowledgeFSSourceSyncPolicyPayload(BaseModel):
enabled: bool
expected_revision: int = Field(ge=0, alias="expectedRevision")
expected_source_version: int = Field(ge=1, alias="expectedSourceVersion")
mode: Literal["provider", "manual", "interval", "custom"]
mode: Literal["manual", "interval", "custom"]
model_config = ConfigDict(extra="forbid", validate_by_alias=True, validate_by_name=True)
@ -2390,7 +2390,7 @@ class KnowledgeFSCrawlPreviewSelectionPayload(BaseModel):
class KnowledgeFSDeferredSyncPolicyPayload(BaseModel):
custom_interval_seconds: int | None = Field(default=None, ge=3_600, le=2_592_000, alias="customIntervalSeconds")
enabled: bool
mode: Literal["provider", "manual", "interval", "custom"]
mode: Literal["manual", "interval", "custom"]
model_config = ConfigDict(extra="forbid", validate_by_alias=True, validate_by_name=True)

View File

@ -339,12 +339,7 @@ def _sync_policy_payload(
expectedRevision=expected_revision,
expectedSourceVersion=source_version,
)
return KnowledgeFSSourceSyncPolicyPayload(
enabled=True,
mode="provider",
expectedRevision=expected_revision,
expectedSourceVersion=source_version,
)
raise ValueError(f"Unsupported sync policy: {payload.sync_policy}")
def start_initial_source_import(

View File

@ -252,7 +252,7 @@ class RecordingRemote:
"expectedSourceVersion": 1,
"id": "policy-1",
"knowledgeSpaceId": "space-1",
"mode": "provider",
"mode": "interval",
"revision": 1,
"sourceId": "source-1",
"updatedAt": "2030-01-01T00:00:00Z",
@ -1089,7 +1089,7 @@ def test_basic_product_facade_resolves_control_space_then_uses_exact_kfs_routes(
assert sources.data[0].sync_workflow.state == "syncing"
assert sources.data[0].last_synced_at == datetime(2030, 1, 1, 1, tzinfo=UTC)
assert sources.data[0].sync_policy is not None
assert sources.data[0].sync_policy.mode == "provider"
assert sources.data[0].sync_policy.mode == "interval"
assert source.knowledge_space_id == "space-1"
assert tasks.data == []
assert traces.data == []

View File

@ -279,7 +279,7 @@ def test_product_application_create_schedules_selected_website_import() -> None:
"title": "Getting started",
}
],
"sync_policy": "provider",
"sync_policy": "daily",
},
)
@ -333,7 +333,7 @@ def test_product_application_create_schedules_selected_connector_import() -> Non
"workspace_id": "workspace-1",
}
],
"sync_policy": "provider",
"sync_policy": "daily",
}

View File

@ -79,7 +79,7 @@ def test_commit_online_document_import_uses_same_async_reconciliation() -> None:
"workspaceId": "workspace-1",
}
],
"syncPolicy": {"enabled": True, "mode": "provider"},
"syncPolicy": {"enabled": True, "mode": "interval"},
}
).root

View File

@ -63,7 +63,7 @@ export const SourceSyncPolicyResponseSchema = z
expectedSourceVersion: z.number().int().min(1),
id: z.string().uuid(),
knowledgeSpaceId: z.string().uuid(),
mode: z.enum(["provider", "manual", "interval", "custom"]),
mode: z.enum(["manual", "interval", "custom"]),
nextRunAt: z.string().optional(),
revision: z.number().int().min(1),
sourceId: z.string().uuid(),

View File

@ -1521,7 +1521,7 @@ describe("source handlers without optional collaborators", () => {
expectedSourceVersion: 1,
id: "00000000-0000-4000-8000-000000000111",
knowledgeSpaceId: spaceId,
mode: "provider",
mode: "interval",
permissionSnapshotId: "permission-1",
permissionSnapshotRevision: 1,
requestedBySubjectId: "u1",
@ -1577,7 +1577,7 @@ describe("source handlers without optional collaborators", () => {
status: "syncing",
syncPolicy: {
enabled: true,
mode: "provider",
mode: "interval",
sourceId,
},
},

View File

@ -511,7 +511,7 @@ describe("source-product handlers", () => {
`/knowledge-spaces/${spaceId}/sources/${sourceId}/sync-policy`,
);
expect(policy.status).toBe(200);
await expect(policy.json()).resolves.toMatchObject({ mode: "provider", revision: 1 });
await expect(policy.json()).resolves.toMatchObject({ mode: "interval", revision: 1 });
const missingPolicy = await app.request(
`/knowledge-spaces/${spaceId}/sources/${sourceId}/sync-policy`,
);
@ -891,7 +891,7 @@ describe("source-product handlers", () => {
enabled: true,
expectedRevision: 1,
expectedSourceVersion: 2,
mode: "provider",
mode: "interval",
}),
headers: { "content-type": "application/json" },
method: "PUT",
@ -968,7 +968,7 @@ describe("source-product handlers", () => {
enabled: true,
expectedRevision: 1,
expectedSourceVersion: 2,
mode: "provider",
mode: "interval",
}),
headers: { "content-type": "application/json" },
method: "PUT",
@ -1101,8 +1101,8 @@ function syncPolicy(patch: Record<string, unknown> = {}) {
expectedSourceVersion: 2,
id: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c44",
knowledgeSpaceId: spaceId,
mode: "provider" as const,
nextRunAt: "2026-07-14T13:00:00.000Z",
mode: "interval" as const,
nextRunAt: "2026-07-15T12:00:00.000Z",
permissionSnapshotId: "permission-policy",
permissionSnapshotRevision: 1,
requestedBySubjectId: "editor-a",

View File

@ -435,7 +435,7 @@ export const putSourceSyncPolicyRoute = createRoute({
enabled: z.boolean(),
expectedRevision: z.number().int().min(0),
expectedSourceVersion: z.number().int().min(1),
mode: z.enum(["provider", "manual", "interval", "custom"]),
mode: z.enum(["manual", "interval", "custom"]),
})
.strict(),
},

View File

@ -1365,7 +1365,7 @@ function policyRecord(
expectedSourceVersion: 1,
id,
knowledgeSpaceId,
mode: "provider",
mode: "interval",
nextRunAt: createdAt,
permissionSnapshotId: `permission-${id}`,
permissionSnapshotRevision: 1,

View File

@ -1036,7 +1036,7 @@ describe("source product workflow service boundaries", () => {
expectedRevision: 0,
expectedSourceVersion: 1,
knowledgeSpaceId,
mode: "provider",
mode: "interval",
sourceId: source.id,
subject: editor,
});
@ -1067,7 +1067,7 @@ describe("source product workflow service boundaries", () => {
enabled: true,
expectedRevision: 1,
expectedSourceVersion: 1,
mode: "provider",
mode: "interval",
}),
).rejects.toMatchObject({ code: "SOURCE_NOT_FOUND" });
});
@ -1100,7 +1100,7 @@ describe("source product workflow service boundaries", () => {
}),
).resolves.toBeNull();
await expect(
service.putSyncPolicy({ ...base, enabled: true, expectedSourceVersion: 2, mode: "provider" }),
service.putSyncPolicy({ ...base, enabled: true, expectedSourceVersion: 2, mode: "interval" }),
).rejects.toMatchObject({ code: "SOURCE_SYNC_POLICY_SOURCE_CONFLICT" });
for (const request of [
@ -1116,11 +1116,11 @@ describe("source product workflow service boundaries", () => {
});
}
const created = await service.putSyncPolicy({ ...base, enabled: true, mode: "provider" });
const created = await service.putSyncPolicy({ ...base, enabled: true, mode: "interval" });
expect(created).toMatchObject({
createdAt: "2026-02-03T00:00:00.000Z",
enabled: true,
nextRunAt: "2026-02-03T01:00:00.000Z",
nextRunAt: "2026-02-04T00:00:00.000Z",
revision: 1,
});
await expect(
@ -1437,9 +1437,6 @@ describe("source product workflow service boundaries", () => {
});
it("computes supported policy intervals and rejects invalid scheduling anchors", () => {
expect(nextSyncPolicyRunAt("provider", undefined, "2026-02-06T00:00:00.000Z")).toBe(
"2026-02-06T01:00:00.000Z",
);
expect(nextSyncPolicyRunAt("interval", undefined, "2026-02-06T00:00:00.000Z")).toBe(
"2026-02-07T00:00:00.000Z",
);
@ -1449,7 +1446,7 @@ describe("source product workflow service boundaries", () => {
expect(() => nextSyncPolicyRunAt("manual", undefined, "2026-02-06T00:00:00.000Z")).toThrow(
SourceWorkflowError,
);
expect(() => nextSyncPolicyRunAt("provider", undefined, "invalid")).toThrow(
expect(() => nextSyncPolicyRunAt("interval", undefined, "invalid")).toThrow(
SourceWorkflowError,
);
expect(() => nextSyncPolicyRunAt("custom", undefined, "2026-02-06T00:00:00.000Z")).toThrow(

View File

@ -213,7 +213,7 @@ interface SourceSyncPolicyBase {
readonly expectedSourceVersion: number;
readonly id: string;
readonly knowledgeSpaceId: string;
readonly mode: "provider" | "manual" | "interval" | "custom";
readonly mode: "manual" | "interval" | "custom";
readonly nextRunAt?: string | undefined;
readonly revision: number;
readonly sourceId: string;
@ -1329,7 +1329,7 @@ export function nextSyncPolicyRunAt(
if (!Number.isFinite(timestamp) || mode === "manual") {
throw new SourceWorkflowError("SOURCE_SYNC_POLICY_INVALID", "Sync policy anchor is invalid");
}
const seconds = mode === "custom" ? customIntervalSeconds : mode === "provider" ? 3_600 : 86_400;
const seconds = mode === "custom" ? customIntervalSeconds : 86_400;
if (!Number.isSafeInteger(seconds) || (seconds as number) < 1) {
throw new SourceWorkflowError("SOURCE_SYNC_POLICY_INVALID", "Sync policy interval is invalid");
}

View File

@ -0,0 +1,11 @@
-- Knowledge Platform schema migration
-- Migration id: 0046_remove_provider_sync_policy
-- Dialect: postgres
-- Historical provider policies must be converted to manual before this migration runs.
ALTER TABLE "source_sync_policies"
DROP CONSTRAINT IF EXISTS "source_sync_policies_mode_ck";
ALTER TABLE "source_sync_policies"
ADD CONSTRAINT "source_sync_policies_mode_ck"
CHECK ("mode" IN ('manual', 'interval', 'custom'));

View File

@ -0,0 +1,24 @@
-- Knowledge Platform schema migration
-- Migration id: 0046_remove_provider_sync_policy
-- Dialect: tidb
-- Historical provider policies must be converted to manual before this migration runs.
SET @kfs_0046_source_sync_policies_mode_ck_exists = (
SELECT COUNT(*) FROM information_schema.tidb_check_constraints
WHERE constraint_schema = DATABASE()
AND table_name = 'source_sync_policies'
AND constraint_name = 'source_sync_policies_mode_ck'
);
SET @kfs_0046_source_sync_policies_mode_ck_drop_sql = IF(
@kfs_0046_source_sync_policies_mode_ck_exists > 0,
'ALTER TABLE `source_sync_policies` DROP CONSTRAINT `source_sync_policies_mode_ck`',
'SELECT 1'
);
PREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt
FROM @kfs_0046_source_sync_policies_mode_ck_drop_sql;
EXECUTE kfs_0046_source_sync_policies_mode_ck_drop_stmt;
DEALLOCATE PREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt;
ALTER TABLE `source_sync_policies`
ADD CONSTRAINT `source_sync_policies_mode_ck`
CHECK (`mode` IN ('manual', 'interval', 'custom'));

View File

@ -93,4 +93,6 @@ export const migrationArtifacts = [
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0044_document_semantic_window_checkpoints\n-- Dialect: tidb\n-- Persists validated semantic-window model responses for generation-scoped retry recovery.\n\nCREATE TABLE IF NOT EXISTS `document_semantic_window_checkpoints` (\n `tenant_id` VARCHAR(255) NOT NULL,\n `knowledge_space_id` CHAR(36) NOT NULL,\n `document_asset_id` CHAR(36) NOT NULL,\n `document_version` INT NOT NULL,\n `publication_generation_id` CHAR(36) NOT NULL,\n `window_id` VARCHAR(128) NOT NULL,\n `input_fingerprint` VARCHAR(71) NOT NULL,\n `model_fingerprint` VARCHAR(71) NOT NULL,\n `response_text` MEDIUMTEXT NOT NULL,\n `completion` JSON NOT NULL,\n `created_at` DATETIME(3) NOT NULL,\n PRIMARY KEY (\n `tenant_id`, `knowledge_space_id`, `publication_generation_id`,\n `window_id`, `input_fingerprint`\n ),\n CONSTRAINT `document_semantic_window_checkpoints_scope_fk`\n FOREIGN KEY (`tenant_id`, `knowledge_space_id`)\n REFERENCES `knowledge_spaces` (`tenant_id`, `id`) ON DELETE CASCADE,\n CONSTRAINT `document_semantic_window_checkpoints_asset_fk`\n FOREIGN KEY (`knowledge_space_id`, `document_asset_id`, `document_version`)\n REFERENCES `document_assets` (`knowledge_space_id`, `id`, `version`) ON DELETE CASCADE,\n CONSTRAINT `document_semantic_window_checkpoints_fingerprint_ck`\n CHECK (\n `input_fingerprint` REGEXP '^sha256:[a-f0-9]{64}$'\n AND `model_fingerprint` REGEXP '^sha256:[a-f0-9]{64}$'\n ),\n CONSTRAINT `document_semantic_window_checkpoints_completion_ck`\n CHECK (JSON_TYPE(`completion`) = 'OBJECT'),\n CONSTRAINT `document_semantic_window_checkpoints_version_ck`\n CHECK (`document_version` >= 1)\n);\n\nCREATE INDEX IF NOT EXISTS `document_semantic_window_checkpoints_asset_idx`\n ON `document_semantic_window_checkpoints` (\n `knowledge_space_id`, `document_asset_id`, `document_version`\n );\n", path: "packages/database/migrations/0044_document_semantic_window_checkpoints.tidb.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0045_quality_replay_match_policy\n-- Dialect: postgres\n-- Freezes each golden question's evidence match policy into durable quality replay items.\n\nALTER TABLE \"quality_replay_items\"\n ADD COLUMN IF NOT EXISTS \"match_policy\" VARCHAR(8);\n\nUPDATE \"quality_replay_items\"\nSET \"match_policy\" = 'all'\nWHERE \"match_policy\" IS NULL;\n\nALTER TABLE \"quality_replay_items\"\n ALTER COLUMN \"match_policy\" SET NOT NULL;\n\nALTER TABLE \"quality_replay_items\"\n DROP CONSTRAINT IF EXISTS \"quality_replay_items_match_policy_ck\";\n\nALTER TABLE \"quality_replay_items\"\n ADD CONSTRAINT \"quality_replay_items_match_policy_ck\"\n CHECK (\"match_policy\" IN ('all', 'any'));\n", path: "packages/database/migrations/0045_quality_replay_match_policy.postgres.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0045_quality_replay_match_policy\n-- Dialect: tidb\n-- Freezes each golden question's evidence match policy into durable quality replay items.\n\nALTER TABLE `quality_replay_items`\n ADD COLUMN IF NOT EXISTS `match_policy` VARCHAR(8);\n\nUPDATE `quality_replay_items`\nSET `match_policy` = 'all'\nWHERE `match_policy` IS NULL;\n\nALTER TABLE `quality_replay_items`\n MODIFY COLUMN `match_policy` VARCHAR(8) NOT NULL;\n\nSET @qr_0045_match_policy_ck_exists = (\n SELECT COUNT(*) FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'quality_replay_items'\n AND constraint_name = 'quality_replay_items_match_policy_ck'\n);\nSET @qr_0045_match_policy_ck_drop_sql = IF(\n @qr_0045_match_policy_ck_exists > 0,\n 'ALTER TABLE `quality_replay_items` DROP CONSTRAINT `quality_replay_items_match_policy_ck`',\n 'SELECT 1'\n);\nPREPARE qr_0045_match_policy_ck_drop_stmt FROM @qr_0045_match_policy_ck_drop_sql;\nEXECUTE qr_0045_match_policy_ck_drop_stmt;\nDEALLOCATE PREPARE qr_0045_match_policy_ck_drop_stmt;\n\nALTER TABLE `quality_replay_items`\n ADD CONSTRAINT `quality_replay_items_match_policy_ck`\n CHECK (`match_policy` IN ('all', 'any'));\n", path: "packages/database/migrations/0045_quality_replay_match_policy.tidb.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0046_remove_provider_sync_policy\n-- Dialect: postgres\n-- Historical provider policies must be converted to manual before this migration runs.\n\nALTER TABLE \"source_sync_policies\"\n DROP CONSTRAINT IF EXISTS \"source_sync_policies_mode_ck\";\n\nALTER TABLE \"source_sync_policies\"\n ADD CONSTRAINT \"source_sync_policies_mode_ck\"\n CHECK (\"mode\" IN ('manual', 'interval', 'custom'));\n", path: "packages/database/migrations/0046_remove_provider_sync_policy.postgres.sql" },
{ content: "-- Knowledge Platform schema migration\n-- Migration id: 0046_remove_provider_sync_policy\n-- Dialect: tidb\n-- Historical provider policies must be converted to manual before this migration runs.\n\nSET @kfs_0046_source_sync_policies_mode_ck_exists = (\n SELECT COUNT(*) FROM information_schema.tidb_check_constraints\n WHERE constraint_schema = DATABASE()\n AND table_name = 'source_sync_policies'\n AND constraint_name = 'source_sync_policies_mode_ck'\n);\nSET @kfs_0046_source_sync_policies_mode_ck_drop_sql = IF(\n @kfs_0046_source_sync_policies_mode_ck_exists > 0,\n 'ALTER TABLE `source_sync_policies` DROP CONSTRAINT `source_sync_policies_mode_ck`',\n 'SELECT 1'\n);\nPREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt\n FROM @kfs_0046_source_sync_policies_mode_ck_drop_sql;\nEXECUTE kfs_0046_source_sync_policies_mode_ck_drop_stmt;\nDEALLOCATE PREPARE kfs_0046_source_sync_policies_mode_ck_drop_stmt;\n\nALTER TABLE `source_sync_policies`\n ADD CONSTRAINT `source_sync_policies_mode_ck`\n CHECK (`mode` IN ('manual', 'interval', 'custom'));\n", path: "packages/database/migrations/0046_remove_provider_sync_policy.tidb.sql" },
] as const satisfies readonly MigrationArtifact[];

View File

@ -1363,8 +1363,8 @@ const tables = [
checkConstraints: [
{
expression: {
postgres: "\"mode\" IN ('provider', 'manual', 'interval', 'custom')",
tidb: "`mode` IN ('provider', 'manual', 'interval', 'custom')",
postgres: "\"mode\" IN ('manual', 'interval', 'custom')",
tidb: "`mode` IN ('manual', 'interval', 'custom')",
},
name: "source_sync_policies_mode_ck",
},