Files
xinmi/scripts/verify_asset_etl.py
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

265 lines
16 KiB
Python

#!/usr/bin/env python3
"""Run asset migration regressions in acceptance PostgreSQL and roll them back.
Source rules, skill bodies, and generated SQL stay in memory. Reports contain
only check names, booleans, counts, and the fixed acceptance scope.
"""
from __future__ import annotations
import argparse
import datetime
import json
from pathlib import Path
from apply_weknora_assets import build_plan, target_snapshot
from apply_weknora_identities import sql_run
from verify_identity_etl import rejected_case
from weknora_asset_etl import (BINDING_LEDGER, LEDGER, SOURCE, TARGET_DATABASE,
TARGET_TENANT, asset_sql, safe_asset_summary)
from weknora_category_etl import literal, save_json
from weknora_etl import consistent_snapshot
from weknora_identity_etl import IDENTITY_LEDGER
def supporting_snapshot():
tables = (LEDGER, BINDING_LEDGER, IDENTITY_LEDGER, "users", "tenant_members",
"tenants", "knowledge_bases")
fields = []
for table in tables:
fields.extend((literal(table),
f"(SELECT coalesce(json_agg(fingerprint ORDER BY fingerprint),'[]') "
f"FROM (SELECT md5(to_jsonb(r)::text) AS fingerprint FROM {table} r) s)"))
return json.loads(sql_run("SELECT json_build_object(" + ",".join(fields) + ");"))
def unchanged_check(name, pairs):
conditions = []
for table, baseline in pairs:
conditions.extend((f"EXISTS (SELECT * FROM {table} EXCEPT SELECT * FROM {baseline})",
f"EXISTS (SELECT * FROM {baseline} EXCEPT SELECT * FROM {table})"))
return f"""
DO $asset_unchanged$
BEGIN
IF {' OR '.join(conditions)} THEN
RAISE EXCEPTION 'Asset regression changed rows unexpectedly';
END IF;
END $asset_unchanged$;
SELECT json_build_object('check',{literal(name)},'passed',true);
"""
def regression_sql(plan, query):
agent, skill = plan["agents"][0]["target"], plan["skills"][0]["target"]
aid, sid = literal(agent["id"]), literal(skill["id"])
other_owner = literal(next(uid for uid in plan["identity_mapping"].values()
if uid != agent["created_by"]))
other_skill_owner = literal(next(uid for uid in plan["identity_mapping"].values()
if uid != skill["created_by"]))
asset_scope = f"source_system={literal(SOURCE)} AND source_organization_id=1"
agent_ledger = f"{asset_scope} AND source_kind='agent' AND target_id={aid}"
skill_ledger = f"{asset_scope} AND source_kind='skill' AND target_id={sid}"
binding = plan["bindings"][0]
binding_where = f"{asset_scope} AND source_binding_id={binding['source']['id']}"
kb = literal(next(iter(plan["kb_mapping"].values())))
model = literal(agent["config"]["model_id"])
owner = literal(agent["created_by"])
foreign_tenant = TARGET_TENANT + 1
cases = [
("mapped_agent_wrong_tenant_rejected", f"UPDATE custom_agents SET tenant_id={foreign_tenant} WHERE id={aid};",
"Mapped native asset removed; refusing recreation"),
("mapped_skill_wrong_tenant_rejected", f"UPDATE tenant_skill_catalog SET tenant_id={foreign_tenant} WHERE id={sid};",
"Mapped native asset removed; refusing recreation"),
("mapped_agent_wrong_owner_rejected", f"UPDATE custom_agents SET created_by={other_owner} WHERE id={aid};",
"Mapped native asset removed; refusing recreation"),
("mapped_skill_wrong_owner_rejected", f"UPDATE tenant_skill_catalog SET created_by={other_skill_owner} WHERE id={sid};",
"Mapped native asset removed; refusing recreation"),
("mapped_skill_kind_change_rejected", f"UPDATE tenant_skill_catalog SET instruction_only=false WHERE id={sid};",
"Mapped native asset removed; refusing recreation"),
("asset_ledger_wrong_tenant_rejected", f"UPDATE {LEDGER} SET target_tenant_id={foreign_tenant} WHERE {agent_ledger};",
"Asset durable mapping conflict"),
("asset_ledger_wrong_id_rejected", f"UPDATE {LEDGER} SET target_id='rollback-only-id' WHERE {agent_ledger};",
"Asset durable mapping conflict"),
("asset_ledger_source_change_rejected", f"UPDATE {LEDGER} SET source_snapshot='{{}}'::jsonb WHERE {skill_ledger};",
"Asset durable mapping conflict"),
("asset_ledger_native_plan_change_rejected", f"UPDATE {LEDGER} SET native_snapshot='{{}}'::jsonb WHERE {agent_ledger};",
"Asset durable mapping conflict"),
("asset_ledger_changed_duty_rejected", f"UPDATE {LEDGER} SET native_snapshot=jsonb_set(native_snapshot,'{{config,role}}','\"invalid-role\"'::jsonb) WHERE {agent_ledger};",
"Asset durable mapping conflict"),
("hard_deleted_agent_not_recreated", f"DELETE FROM custom_agents WHERE id={aid};",
"Mapped native asset removed; refusing recreation"),
("hard_deleted_skill_not_recreated", f"DELETE FROM tenant_skill_catalog WHERE id={sid};",
"Mapped native asset removed; refusing recreation"),
("agent_id_without_ledger_rejected", f"DELETE FROM {LEDGER} WHERE {agent_ledger};",
"Native Agent ID or owner/name collision"),
("skill_id_without_ledger_rejected", f"DELETE FROM {LEDGER} WHERE {skill_ledger};",
"Native skill ID or owner/name collision"),
("agent_owner_name_without_ledger_rejected", f"DELETE FROM {LEDGER} WHERE {agent_ledger}; "
f"UPDATE custom_agents SET id='rollback-only-agent-id' WHERE id={aid};",
"Native Agent ID or owner/name collision"),
("skill_owner_name_without_ledger_rejected", f"DELETE FROM {LEDGER} WHERE {skill_ledger}; "
f"UPDATE tenant_skill_catalog SET id='rollback-only-skill-id' WHERE id={sid};",
"Native skill ID or owner/name collision"),
("inactive_target_tenant_rejected", f"UPDATE tenants SET status='suspended' WHERE id={TARGET_TENANT};",
"Asset migration requires active target tenant"),
("owner_identity_mapping_change_rejected", f"UPDATE {IDENTITY_LEDGER} SET target_user_id='rollback-only-owner' "
f"WHERE {asset_scope} AND target_user_id={owner};",
"Asset owner durable identity or membership mismatch"),
("owner_membership_deletion_rejected", f"UPDATE tenant_members SET deleted_at=now() WHERE tenant_id={TARGET_TENANT} AND user_id={owner};",
"Asset owner durable identity or membership mismatch"),
("knowledge_base_wrong_tenant_rejected", f"UPDATE knowledge_bases SET tenant_id={foreign_tenant} WHERE id={kb};",
"Asset knowledge base mapping missing or outside tenant"),
("deleted_model_rejected", f"UPDATE models SET deleted_at=now() WHERE id={model};",
"Asset model mapping missing or not usable"),
("binding_wrong_tenant_rejected", f"UPDATE {BINDING_LEDGER} SET target_tenant_id={foreign_tenant} WHERE {binding_where};",
"Legacy skill binding provenance conflict"),
("binding_agent_change_rejected", f"UPDATE {BINDING_LEDGER} SET target_agent_id='rollback-only-agent' WHERE {binding_where};",
"Legacy skill binding provenance conflict"),
("binding_source_change_rejected", f"UPDATE {BINDING_LEDGER} SET source_snapshot='{{}}'::jsonb WHERE {binding_where};",
"Legacy skill binding provenance conflict"),
("binding_access_review_change_rejected", f"UPDATE {BINDING_LEDGER} SET access_review='[\"rollback-only-review\"]'::jsonb WHERE {binding_where};",
"Legacy skill binding provenance conflict"),
("binding_effective_flag_change_rejected", f"UPDATE {BINDING_LEDGER} SET effective=NOT effective WHERE {binding_where};",
"Legacy skill binding provenance conflict"),
]
if plan["prompt_templates"]:
template_id = plan["prompt_templates"][0]["id"]
cases.append(("template_source_change_rejected",
f"UPDATE {LEDGER} SET source_snapshot='{{}}'::jsonb WHERE {asset_scope} "
f"AND source_kind='template' AND source_id={template_id};",
"Legacy prompt template provenance conflict"))
tables = ("custom_agents", "tenant_skill_catalog", LEDGER, BINDING_LEDGER, "models")
pairs = [(table, f"_asset_before_{index}") for index, table in enumerate(tables)]
sql = """BEGIN ISOLATION LEVEL SERIALIZABLE;
SET LOCAL statement_timeout='30s';
SET LOCAL lock_timeout='5s';
SET LOCAL log_statement='none';
SET LOCAL log_min_error_statement='panic';
"""
for table, baseline in pairs:
sql += f"CREATE TEMP TABLE {baseline} ON COMMIT DROP AS SELECT * FROM {table};\n"
sql += query + unchanged_check("exact_repeat_and_existing_models_unchanged", pairs)
sql += f"""
SELECT json_build_object('check','all_39_source_bindings_retained_only_30_effective','passed',
(SELECT count(*)=39 AND count(*) FILTER (WHERE effective)=30 FROM {BINDING_LEDGER}
WHERE {asset_scope} AND target_tenant_id={TARGET_TENANT})
AND (SELECT count(*)=15 FROM {LEDGER} WHERE {asset_scope} AND source_kind='agent')
AND (SELECT count(*)=9 FROM {LEDGER} WHERE {asset_scope} AND source_kind='skill')
AND NOT EXISTS (
(SELECT b.target_agent_id,b.target_skill_id FROM {BINDING_LEDGER} b
WHERE b.source_system={literal(SOURCE)} AND b.source_organization_id=1 AND b.effective
EXCEPT
SELECT a.id,s.value FROM custom_agents a JOIN {LEDGER} l
ON l.source_kind='agent' AND l.target_id=a.id AND l.target_tenant_id=a.tenant_id
CROSS JOIN LATERAL jsonb_array_elements_text(a.config->'instruction_skill_ids') s(value)
WHERE l.source_system={literal(SOURCE)} AND l.source_organization_id=1)
UNION ALL
(SELECT a.id,s.value FROM custom_agents a JOIN {LEDGER} l
ON l.source_kind='agent' AND l.target_id=a.id AND l.target_tenant_id=a.tenant_id
CROSS JOIN LATERAL jsonb_array_elements_text(a.config->'instruction_skill_ids') s(value)
WHERE l.source_system={literal(SOURCE)} AND l.source_organization_id=1
EXCEPT
SELECT b.target_agent_id,b.target_skill_id FROM {BINDING_LEDGER} b
WHERE b.source_system={literal(SOURCE)} AND b.source_organization_id=1 AND b.effective)));
SELECT json_build_object('check','cross_owner_private_skills_blocked','passed',
(SELECT count(*)=9 FROM {BINDING_LEDGER} WHERE {asset_scope} AND NOT effective)
AND NOT EXISTS (SELECT 1 FROM {BINDING_LEDGER} b
JOIN custom_agents a ON a.id=b.target_agent_id AND a.tenant_id=b.target_tenant_id
JOIN tenant_skill_catalog s ON s.id=b.target_skill_id AND s.tenant_id=b.target_tenant_id
WHERE b.source_system={literal(SOURCE)} AND b.source_organization_id=1
AND b.effective IS DISTINCT FROM (s.visibility='global' OR s.created_by=a.created_by)));
"""
for name, mutation, expected in cases:
sql += rejected_case(name, mutation, expected, query)
sql += f"""
SAVEPOINT pre_duty_replay;
UPDATE {LEDGER} SET native_snapshot=native_snapshot #- '{{config,role}}' #- '{{config,content_tags}}' #- '{{config,fusion_agent_ids}}'
WHERE {asset_scope} AND source_kind='agent';
CREATE TEMP TABLE _pre_duty_ledger ON COMMIT DROP AS SELECT * FROM {LEDGER};
"""
sql += query + unchanged_check("pre_duty_ledger_replay_preserves_native_edits_and_audit",
[("custom_agents", pairs[0][1]), (LEDGER, "_pre_duty_ledger")])
sql += "ROLLBACK TO SAVEPOINT pre_duty_replay;\n"
duty_migration = (Path(__file__).resolve().parents[1] / "migrations/versioned/000205_agent_duties.up.sql").read_text()
sql += f"""
SAVEPOINT duty_backfill;
UPDATE custom_agents SET config=config - 'role' - 'content_tags' WHERE id={aid};
{duty_migration}
SELECT json_build_object('check','missing_duties_backfilled_without_other_changes','passed',
(SELECT a.config->>'role'=l.source_snapshot->>'role'
AND a.config->'content_tags'=l.source_snapshot->'content_tags'
AND (to_jsonb(a) #- '{{config,role}}' #- '{{config,content_tags}}') =
(to_jsonb(b) #- '{{config,role}}' #- '{{config,content_tags}}')
FROM custom_agents a JOIN {LEDGER} l ON l.target_id=a.id AND l.source_kind='agent'
JOIN {pairs[0][1]} b ON b.id=a.id WHERE a.id={aid}));
UPDATE custom_agents SET config=config || '{{"role":"general","content_tags":[]}}'::jsonb WHERE id={aid};
CREATE TEMP TABLE _duty_edited_agents ON COMMIT DROP AS SELECT * FROM custom_agents;
{duty_migration}
"""
sql += unchanged_check("duty_backfill_preserves_explicit_native_edits", [("custom_agents", "_duty_edited_agents")])
sql += "ROLLBACK TO SAVEPOINT duty_backfill;\n"
sql += f"""
UPDATE custom_agents SET name='Rollback-only native Agent edit',description='local edit',
enabled=false,visibility='private',
config=jsonb_set(jsonb_set(config,'{{system_prompt}}','\"Rollback-only native prompt\"'::jsonb),
'{{instruction_skill_ids}}','[]'::jsonb) || '{{"role":"general","content_tags":[]}}'::jsonb WHERE id={aid} AND tenant_id={TARGET_TENANT};
UPDATE tenant_skill_catalog SET name='Rollback-only native skill edit',description='local edit',
instructions='Rollback-only native instructions',version='77',enabled=false,visibility='private'
WHERE id={sid} AND tenant_id={TARGET_TENANT};
CREATE TEMP TABLE _edited_agents ON COMMIT DROP AS SELECT * FROM custom_agents;
CREATE TEMP TABLE _edited_skills ON COMMIT DROP AS SELECT * FROM tenant_skill_catalog;
"""
sql += query + unchanged_check("native_edits_disabling_and_binding_removal_preserved",
[("custom_agents", "_edited_agents"), ("tenant_skill_catalog", "_edited_skills"), *pairs[2:]])
sql += f"""
UPDATE custom_agents SET deleted_at=now() WHERE id={aid} AND tenant_id={TARGET_TENANT};
UPDATE tenant_skill_catalog SET deleted_at=now() WHERE id={sid} AND tenant_id={TARGET_TENANT};
CREATE TEMP TABLE _deleted_agents ON COMMIT DROP AS SELECT * FROM custom_agents;
CREATE TEMP TABLE _deleted_skills ON COMMIT DROP AS SELECT * FROM tenant_skill_catalog;
"""
sql += query + unchanged_check("soft_deleted_assets_not_revived",
[("custom_agents", "_deleted_agents"), ("tenant_skill_catalog", "_deleted_skills"), *pairs[2:]])
sql += "ROLLBACK;\n"
return sql, len(cases) + 8
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sqlite", type=Path, required=True)
parser.add_argument("--kb-mapping", type=Path, required=True)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
if args.out.exists():
parser.error("Output already exists")
manifest = json.loads(args.manifest.read_text())
with consistent_snapshot(args.sqlite) as (db, info):
plan = build_plan(db, args.kb_mapping)
expected = {k: v for k, v in manifest.items() if k != "source_snapshot_sha256"}
if safe_asset_summary(plan) != expected or info["source_mutated"]:
raise RuntimeError("Asset source differs from the reviewed manifest")
if (len(plan["agents"]), len(plan["skills"]), len(plan["bindings"]),
sum(b["effective"] for b in plan["bindings"])) != (15, 9, 39, 30):
raise RuntimeError("Asset counts differ from the reviewed acceptance batch")
plan["source_snapshot_sha256"] = info["sha256"]
query, expected_checks = regression_sql(plan, asset_sql(plan))
before, supporting_before = target_snapshot(), supporting_snapshot()
output = sql_run(query)
checks = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
after, supporting_after = target_snapshot(), supporting_snapshot()
checks.extend((
{"check": "all_regressions_executed", "passed": len(checks) == expected_checks},
{"check": "all_test_changes_rolled_back", "passed": before == after and supporting_before == supporting_after},
{"check": "acceptance_scope", "passed": before["database"] == after["database"] == TARGET_DATABASE},
))
report = {"checked_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"database": TARGET_DATABASE, "tenant_id": TARGET_TENANT,
"committed": False, "source_mutated": info["source_mutated"],
"checks": checks, "passed": all(c["passed"] for c in checks)}
save_json(args.out, report)
print(json.dumps(report))
return 0 if report["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())