Files
xinmi/scripts/verify_model_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

172 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Exercise model migration conflicts in PostgreSQL, always rolling back.
An optional single Pro model debug request records only a status, finish reason,
token counts, and elapsed time. SQL, credentials, and response text never leave
memory or appear in the report.
"""
from __future__ import annotations
import argparse
import datetime
import json
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from apply_weknora_identities import sql_run
from apply_weknora_models import native_encryptor, resolve_source_credential, target_snapshot
from verify_identity_etl import rejected_case
from weknora_category_etl import literal, save_json
from weknora_etl import consistent_snapshot
from weknora_model_etl import (LEDGER, TARGET_DATABASE, TARGET_TENANT, model_id,
model_plan, model_sql, read_credentials)
def ledger_snapshot():
return json.loads(sql_run(f"SELECT coalesce(json_agg(to_jsonb(l) ORDER BY source_catalog_id),'[]') "
f"FROM {LEDGER} l;"))
def regression_sql(plan, query):
uid = literal(plan["models"][0]["target_id"])
cases = [
("mapped_model_wrong_tenant_rejected", f"UPDATE models SET tenant_id={TARGET_TENANT + 1} WHERE id={uid};",
"Previously migrated model is missing or incompatible"),
("ledger_wrong_tenant_rejected", f"UPDATE {LEDGER} SET target_tenant_id={TARGET_TENANT + 1} WHERE target_model_id={uid};",
"Model migration ledger mapping differs"),
("ledger_wrong_model_id_rejected", f"UPDATE {LEDGER} SET target_model_id='rollback-only-conflicting-id' WHERE target_model_id={uid};",
"Model migration ledger mapping differs"),
("ledger_wrong_provider_rejected", f"UPDATE {LEDGER} SET source_provider_id=999 WHERE target_model_id={uid};",
"Model migration ledger mapping differs"),
("ledger_changed_source_rejected", f"UPDATE {LEDGER} SET source_snapshot='{{}}'::jsonb WHERE target_model_id={uid};",
"Model migration ledger mapping differs"),
("existing_model_id_without_ledger_rejected", f"DELETE FROM {LEDGER} WHERE target_model_id={uid};",
"Model ID collision without migration provenance"),
("deleted_model_not_revived", f"UPDATE models SET deleted_at=now() WHERE id={uid};",
"Previously migrated model is missing or incompatible"),
("wrong_model_type_rejected", f"UPDATE models SET type='Embedding' WHERE id={uid};",
"Previously migrated model is missing or incompatible"),
("inactive_target_tenant_rejected", f"UPDATE tenants SET status='suspended' WHERE id={TARGET_TENANT};",
"Model migration requires the active acceptance tenant"),
]
sql = f"""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';
CREATE TEMP TABLE _models_before ON COMMIT DROP AS SELECT * FROM models;
CREATE TEMP TABLE _model_ledger_before ON COMMIT DROP AS SELECT * FROM {LEDGER};
{query}
DO $model_repeat$
BEGIN
IF EXISTS (SELECT * FROM models EXCEPT SELECT * FROM _models_before)
OR EXISTS (SELECT * FROM _models_before EXCEPT SELECT * FROM models)
OR EXISTS (SELECT * FROM {LEDGER} EXCEPT SELECT * FROM _model_ledger_before)
OR EXISTS (SELECT * FROM _model_ledger_before EXCEPT SELECT * FROM {LEDGER})
THEN RAISE EXCEPTION 'Exact model migration repeat changed stored rows'; END IF;
END $model_repeat$;
SELECT json_build_object('check','exact_repeat_is_noop','passed',true);
SELECT json_build_object('check','existing_embedding_and_credentials_unchanged','passed',
EXISTS (SELECT 1 FROM _models_before WHERE type='Embedding') AND NOT EXISTS (
SELECT 1 FROM _models_before b LEFT JOIN models m ON m.id=b.id
WHERE to_jsonb(m) IS DISTINCT FROM to_jsonb(b)));
"""
for name, mutation, expected in cases:
sql += rejected_case(name, mutation, expected, query)
sql += f"""
UPDATE models SET display_name='Rollback-only native display edit',
parameters=jsonb_set(parameters,'{{api_key}}','"enc:v1:rollback-only-rotation"'::jsonb)
WHERE id={uid};
CREATE TEMP TABLE _model_rotated_before ON COMMIT DROP AS SELECT * FROM models;
{query}
DO $model_rotation_preservation$
BEGIN
IF EXISTS (SELECT * FROM models EXCEPT SELECT * FROM _model_rotated_before)
OR EXISTS (SELECT * FROM _model_rotated_before EXCEPT SELECT * FROM models)
THEN RAISE EXCEPTION 'Model migration overwrote native edits or rotated credentials'; END IF;
END $model_rotation_preservation$;
SELECT json_build_object('check','native_display_and_rotated_credential_preserved','passed',true);
SELECT json_build_object('check','all_model_ledger_rows_secret_free','passed',
NOT EXISTS (SELECT 1 FROM {LEDGER} WHERE source_snapshot::text LIKE '%"api_key"%'
OR source_snapshot::text LIKE '%"stored_parameters"%'
OR source_snapshot::text LIKE '%enc:v1:%'));
ROLLBACK;
"""
return sql
def pro_api_check(credentials_path):
credentials = json.loads(credentials_path.read_text())
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
login = urllib.request.Request("http://127.0.0.1:18080/api/v1/auth/login",
data=json.dumps({key: credentials[key] for key in ("email", "password")}).encode(),
headers={"Content-Type": "application/json"})
try:
with opener.open(login, timeout=20) as response:
auth = json.load(response)
except (urllib.error.URLError, ValueError):
raise RuntimeError("Acceptance verification login failed") from None
if auth.get("active_tenant", {}).get("id") != TARGET_TENANT or not auth.get("token"):
raise RuntimeError("Verification login is outside the acceptance tenant")
body = urllib.parse.urlencode({"input": "\u4ec5\u56de\u590dOK", "options": json.dumps({"max_tokens": 256})}).encode()
request = urllib.request.Request(
f"http://127.0.0.1:18080/api/v1/models/{model_id(3, 'deepseek-v4-pro')}/debug",
data=body, headers={"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + auth["token"]})
try:
with opener.open(request, timeout=55) as response:
payload = json.load(response)
except (urllib.error.URLError, ValueError):
return {"ok": False, "finish_reason": "request_failed", "prompt_tokens": None,
"completion_tokens": None, "total_tokens": None, "elapsed_ms": None}
data = payload.get("data") or {}
raw = data.get("raw_response") or {}
usage = raw.get("usage") or {}
finish = raw.get("finish_reason")
# Whitelist the only model text field permitted in the saved result.
allowed_finish = {"stop", "length", "tool_calls", "content_filter", "function_call", None, ""}
return {"ok": bool(payload.get("success") and data.get("ok") and str(raw.get("content", "")).strip() == "OK"),
"finish_reason": finish if finish in allowed_finish else "unrecognized",
"prompt_tokens": usage.get("prompt_tokens"), "completion_tokens": usage.get("completion_tokens"),
"total_tokens": usage.get("total_tokens"), "elapsed_ms": data.get("elapsed_ms")}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--sqlite", type=Path, required=True)
parser.add_argument("--environment", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--credentials", type=Path)
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):
fresh = model_plan(db)
expected = {k: v for k, v in manifest.items() if k != "source_snapshot_sha256"}
if fresh != expected or info["source_mutated"]:
raise RuntimeError("Model source differs from reviewed manifest")
query = model_sql(manifest, read_credentials(db, manifest, resolve_source_credential),
native_encryptor(args.environment))
before, ledger_before = target_snapshot(), ledger_snapshot()
output = sql_run(regression_sql(manifest, query))
checks = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
after, ledger_after = target_snapshot(), ledger_snapshot()
checks.append({"check": "all_test_changes_rolled_back", "passed": before == after and ledger_before == ledger_after})
checks.append({"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, "checks": checks, "passed": all(c["passed"] for c in checks)}
if args.credentials:
report["pro_debug_max_tokens_256"] = pro_api_check(args.credentials)
save_json(args.out, report)
print(json.dumps(report))
return 0 if report["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())