v1.0.63: autotest namespace and duplicate prevention

This commit is contained in:
2026-07-27 11:24:05 +04:00
parent 8531d5e15d
commit 0bcfa5a566
6 changed files with 62 additions and 4 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
# Архитектура app-autotest — полный документ
v1.0.62, 27.07.2026
v1.0.63, 27.07.2026
---
@@ -350,6 +350,7 @@ def detect_endpoint(token):
| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
| 20 | v1.0.63 | Дубли и чужие инстансы | Принадлежность определялась не по namespace | `autotest-` prefix + уникализация `displayName` при CREATE |
### ✅ Закрыто в v1.0.56
+13
View File
@@ -1,5 +1,18 @@
# История разработки app-autotest
## v1.0.63 (27.07.2026) — autotest namespace for instances
### Что изменилось
- В `site/templates/index.html` CREATE теперь генерирует `displayName` с префиксом `autotest-` и проверяет его на уникальность среди текущих инстансов.
- В `site/routes/api_test.py` backend тоже нормализует `displayName` с этим префиксом и, если нужно, добавляет суффикс для уникальности перед созданием в облаке.
- В `site/routes/main.py` список инстансов фильтруется по namespace `autotest-`, чтобы в UI попадали только тестовые инстансы приложения.
- В `DOCS/ARCHITECTURE.md` зафиксирован переход на namespace-based фильтрацию.
### Что это чинит
- Дубли `displayName` при CREATE.
- Чужие инстансы в списке.
- Зависимость от локального трекера как от основного признака принадлежности.
## v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
### Что изменилось
+1 -1
View File
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
from routes.api import bp as api_bp
from routes.api_test import bp as api_test_bp
VERSION = "1.0.62"
VERSION = "1.0.63"
app = Flask(__name__, template_folder="templates", static_folder="static")
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
+30 -1
View File
@@ -1,4 +1,5 @@
from flask import Blueprint, current_app, jsonify, request
import uuid
from api.http_client import HttpClient, detect_endpoint
from operations.get_services import get_services, get_service_detail
@@ -8,6 +9,33 @@ from operations.tracker import remove as tracker_remove
bp = Blueprint("api_test", __name__)
AUTOTEST_PREFIX = "autotest-"
def _with_prefix(name):
name = (name or "").strip() or "instance"
return name if name.startswith(AUTOTEST_PREFIX) else f"{AUTOTEST_PREFIX}{name}"
def _unique_display_name(client, requested_name):
base_name = _with_prefix(requested_name)
try:
existing = {
i.get("displayName", "")
for i in get_instances(client)
if str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX)
}
except Exception:
existing = set()
if base_name not in existing:
return base_name
while True:
candidate = f"{base_name}-{uuid.uuid4().hex[:6]}"
if candidate not in existing:
return candidate
def _client():
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
@@ -107,6 +135,7 @@ def api_test():
try:
if op_name == "create":
display_name = _unique_display_name(client, display_name)
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
resp = client.post("/instances", payload)
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
@@ -135,7 +164,7 @@ def api_test():
# фоном ждать завершения
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True), daemon=True).start()
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
else:
if not instance_uid:
+2
View File
@@ -7,6 +7,7 @@ from operations.tracker import list_all as tracker_list
from runner import load_config
bp = Blueprint("main", __name__)
AUTOTEST_PREFIX = "autotest-"
def _mask(s):
@@ -143,6 +144,7 @@ def api_operations(svc_id):
nubes_uids = {i["instanceUid"] for i in instances}
svc_instances = [i for i in instances
if i.get("instanceUid") in tracked_uids
and str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX)
and i.get("explainedStatus") not in ("deleted",)]
# Если Nubes уже не отдаёт tracked-инстанс после reload, добавляем его из трекера.
for uid, t in tracked_by_uid.items():
+14 -1
View File
@@ -99,6 +99,7 @@
<script>
let svcInstances=[], selectedInst=null, selectedOp=null, pollTimer=null;
const SVC_ID=1;
const AUTOTEST_PREFIX='autotest-';
selectService(SVC_ID);
@@ -151,6 +152,18 @@ function startCreate(){
showParams(18,'create');
}
function makeCreateDisplayName(){
const existing = new Set((svcInstances||[])
.map(i => i.displayName || '')
.filter(name => name.startsWith(AUTOTEST_PREFIX)));
const suffix = Date.now().toString(36);
let candidate = `${AUTOTEST_PREFIX}${suffix}`;
while(existing.has(candidate)){
candidate = `${AUTOTEST_PREFIX}${suffix}-${Math.random().toString(36).slice(2, 6)}`;
}
return candidate;
}
function runOp(opName,opId){
stopPoll();
selectedOp={opId,opName,svcId:SVC_ID};
@@ -176,7 +189,7 @@ function showParams(opId,opName){
fetch('/api/params/'+opId).then(r=>r.json()).then(params=>{
const form=document.getElementById('params-form');
const displayName=opName==='create'?('autotest-1-'+Date.now().toString(36)):'';
const displayName=opName==='create'?makeCreateDisplayName():'';
form.innerHTML=(opName==='create'?`<div class="param-row"><label class="req">displayName</label><input type="text" id="param-displayname" value="${displayName}"></div>`:'')
+ params.map(p=>{
const req=p.isRequired;