feat(n8n): add scheduled quality-scan workflow

The original docs described two n8n workflows but the repository only ever
shipped one (return-processing); the sketched second workflow (knowledge
sync) depends on RAGcore, which isn't connected here, so it stays deferred.

Add POST /api/v1/integrations/n8n/scheduled-scan (X-Service-Token
protected, same pattern as the return callback), calling the same
run_scan() the manual "Run quality scan" UI action uses and recording a
service-actor data_quality_scan_run audit event. run_scan() already only
creates an issue for a condition without one open, so overlapping triggers
do no duplicate domain work.

n8n/mobilityops-scheduled-quality-scan.json (hourly schedule + manual test
trigger, both feeding the same HTTP call) ships "active": false so it can't
fire anywhere until deliberately published. Verified live against the
local n8n instance via the Manual test trigger: full green execution, and
the resulting data_quality_scan_run audit event (actor_type=service,
actor_label="n8n scheduled scan") confirms the real round trip, not just a
contract test. deploy/unraid/setup-scheduled-scan.sh mirrors the existing
return-workflow publish script for the shared Unraid n8n.
This commit is contained in:
NuklearRabbit
2026-08-02 06:59:17 +02:00
parent ec8f809497
commit e115031a57
9 changed files with 306 additions and 8 deletions
+40
View File
@@ -82,3 +82,43 @@ def test_callback_is_idempotent_by_event_id(client, ops_client):
).json()
matching = [e for e in audit_events if e["metadata"]["event_id"] == event_id]
assert len(matching) == 1
def test_scheduled_scan_rejects_wrong_service_token(client):
response = client.post(
"/api/v1/integrations/n8n/scheduled-scan",
headers={"X-Service-Token": "wrong-token"},
)
assert response.status_code == 401
def test_scheduled_scan_requires_service_token_header(client):
response = client.post("/api/v1/integrations/n8n/scheduled-scan")
assert response.status_code == 422
def test_scheduled_scan_runs_and_returns_counts_by_rule(client, ops_client):
settings = get_settings()
response = client.post(
"/api/v1/integrations/n8n/scheduled-scan",
headers={"X-Service-Token": settings.n8n_callback_token},
)
assert response.status_code == 200
assert response.json() == {"created": {}} # already-seeded conditions, nothing new
audit_events = ops_client.get(
"/api/v1/audit", params={"action": "data_quality_scan_run"}
).json()
service_triggered = [e for e in audit_events if e["actor_type"] == "service"]
assert len(service_triggered) >= 1
assert service_triggered[0]["actor_label"] == "n8n scheduled scan"
def test_scheduled_scan_is_idempotent_across_repeated_triggers(client):
settings = get_settings()
headers = {"X-Service-Token": settings.n8n_callback_token}
first = client.post("/api/v1/integrations/n8n/scheduled-scan", headers=headers)
second = client.post("/api/v1/integrations/n8n/scheduled-scan", headers=headers)
assert first.status_code == 200
assert second.status_code == 200
assert second.json()["created"] == {}