feat(wp-c): migration colonnes token par poste (patch 1, inerte)
Ajoute token_hash + token_issued_at à enrolled_agents via ALTER TABLE idempotent (_init_db). Colonnes inertes : aucun branchement auth, runtime inchangé (tests WP-B verts). Base du token par poste (WP-C, cf DETTE-015). TDD: tests/unit/test_wpc_migration.py (présence, idempotence, préservation des données d'une base existante). 3 tests + non-régression WP-B = 9 passed. refs DETTE-015 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -111,6 +111,20 @@ class AgentRegistry:
|
||||
"CREATE INDEX IF NOT EXISTS idx_enrolled_agents_machine "
|
||||
"ON enrolled_agents(machine_id)"
|
||||
)
|
||||
# WP-C Patch 1 : colonnes « token par poste », migration additive
|
||||
# idempotente. Inertes tant que l'auth par poste n'est pas branchée
|
||||
# (patchs WP-C ultérieurs). Voir DETTE-015.
|
||||
existing_cols = {
|
||||
row[1]
|
||||
for row in conn.execute(
|
||||
"PRAGMA table_info(enrolled_agents)"
|
||||
).fetchall()
|
||||
}
|
||||
for col in ("token_hash", "token_issued_at"):
|
||||
if col not in existing_cols:
|
||||
conn.execute(
|
||||
f"ALTER TABLE enrolled_agents ADD COLUMN {col} TEXT"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Lecture
|
||||
|
||||
92
tests/unit/test_wpc_migration.py
Normal file
92
tests/unit/test_wpc_migration.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""WP-C Patch 1 — migration additive idempotente des colonnes « token par poste ».
|
||||
|
||||
Ajoute `token_hash` et `token_issued_at` à la table `enrolled_agents`, sans
|
||||
casser les bases existantes ni perdre de données. Comportement runtime inchangé :
|
||||
les colonnes restent inertes tant que l'auth par poste n'est pas branchée
|
||||
(patchs WP-C ultérieurs). Voir DETTE-015 / PLAN-WPC-TDD-EXECUTABLE.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from agent_v0.server_v1.agent_registry import AgentRegistry
|
||||
|
||||
|
||||
def _columns(db_path) -> set[str]:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
rows = conn.execute("PRAGMA table_info(enrolled_agents)").fetchall()
|
||||
return {r[1] for r in rows}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_token_columns_present_after_init(tmp_path):
|
||||
"""Une base neuve doit contenir les colonnes token dès l'init."""
|
||||
db = tmp_path / "fleet.db"
|
||||
AgentRegistry(db_path=db)
|
||||
cols = _columns(db)
|
||||
assert "token_hash" in cols
|
||||
assert "token_issued_at" in cols
|
||||
|
||||
|
||||
def test_token_columns_idempotent(tmp_path):
|
||||
"""Ré-initialiser plusieurs fois ne doit jamais lever (migration idempotente)."""
|
||||
db = tmp_path / "fleet.db"
|
||||
AgentRegistry(db_path=db)
|
||||
AgentRegistry(db_path=db)
|
||||
AgentRegistry(db_path=db)
|
||||
cols = _columns(db)
|
||||
assert "token_hash" in cols
|
||||
assert "token_issued_at" in cols
|
||||
|
||||
|
||||
def test_migration_preserves_existing_rows(tmp_path):
|
||||
"""Une base ancienne (sans les colonnes) est migrée sans perte de données."""
|
||||
db = tmp_path / "fleet.db"
|
||||
# Ancien schéma, sans les colonnes token, avec une ligne existante.
|
||||
conn = sqlite3.connect(str(db))
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE enrolled_agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
machine_id TEXT NOT NULL UNIQUE,
|
||||
user_name TEXT,
|
||||
user_email TEXT,
|
||||
user_id TEXT,
|
||||
hostname TEXT,
|
||||
os_info TEXT,
|
||||
version TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
enrolled_at TEXT NOT NULL,
|
||||
last_seen_at TEXT,
|
||||
uninstalled_at TEXT,
|
||||
uninstall_reason TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO enrolled_agents (machine_id, status, enrolled_at) "
|
||||
"VALUES ('PC-LEGACY', 'active', '2026-01-01T00:00:00+00:00')"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Le démarrage du registry doit migrer la base existante.
|
||||
AgentRegistry(db_path=db)
|
||||
|
||||
cols = _columns(db)
|
||||
assert "token_hash" in cols
|
||||
assert "token_issued_at" in cols
|
||||
|
||||
conn = sqlite3.connect(str(db))
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT machine_id, token_hash FROM enrolled_agents "
|
||||
"WHERE machine_id = 'PC-LEGACY'"
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
assert row is not None
|
||||
assert row[0] == "PC-LEGACY"
|
||||
assert row[1] is None # colonne ajoutée, NULL par défaut
|
||||
Reference in New Issue
Block a user