feat(0036d): promote kind-aware (Group → clear group_id)
NodeGroups window kind=Group ahora expone un boton SmallButton(TI_ARROW_UP) por fila que saca la entidad del grupo (group_id = NULL) y dispara reload del grafo. kind=Table mantiene el comportamiento de issue 0011. - entity_ops: nueva op `entity_clear_group_id(db, id)` idempotente. Si la columna group_id no existe (BD pre-0035a) retorna true como no-op. Falla solo si la entidad no existe o SQLite revienta. - views.cpp: extra columna "promote" en kind=Group, tooltip header diferenciado por kind, boton conectado a app.want_clear_group_id_entity. - main.cpp: handler que ejecuta entity_clear_group_id, marca windows como dirty, llama reload_after_mutation y loguea `[node_groups] promoted X out of group`. - gx-cli: flag `node update --clear-group-id` (booleano) y exposicion MCP en inputSchema + MCP_DISPATCH defaults para que el agente Echo pueda promover via tool calls. - tests: 3 nuevos CLI (clear, idempotente, combinable con --name) y 4 MCP (defaults, schema, dispatch end-to-end, idempotente). WSL: 102 passed (95 base + 7). Windows: 91 passed, 11 skipped (84 base + 7). Refs: issues/0036d-promote-kind-aware.md
This commit is contained in:
@@ -255,6 +255,57 @@ class TestCliNodeUpdate:
|
||||
"--name", "x", expect_ok=False)
|
||||
assert out.get("ok") is False
|
||||
|
||||
def test_node_update_clear_group_id(self, env_dirs):
|
||||
"""Issue 0036d: --clear-group-id saca la entidad del grupo."""
|
||||
nid = self._make(env_dirs, name="grouped")
|
||||
# Asignar group_id directamente via SQL (simula entity en grupo).
|
||||
cn = sqlite3.connect(env_dirs["ops"])
|
||||
cn.execute("UPDATE entities SET group_id = ? WHERE id = ?",
|
||||
("group_xyz", nid))
|
||||
cn.commit()
|
||||
cn.close()
|
||||
# Sanity: precondicion.
|
||||
row = fetchone(env_dirs["ops"],
|
||||
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||
assert row[0] == "group_xyz"
|
||||
# Run clear.
|
||||
run_gx(env_dirs, "node", "update", nid, "--clear-group-id")
|
||||
row = fetchone(env_dirs["ops"],
|
||||
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||
assert row[0] is None
|
||||
|
||||
def test_node_update_clear_group_id_idempotent(self, env_dirs):
|
||||
"""Issue 0036d: clear sobre entidad ya sin grupo no falla."""
|
||||
nid = self._make(env_dirs, name="solo")
|
||||
# Precondicion: group_id NULL desde el start.
|
||||
row = fetchone(env_dirs["ops"],
|
||||
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||
assert row[0] is None
|
||||
# Clear es no-op.
|
||||
run_gx(env_dirs, "node", "update", nid, "--clear-group-id")
|
||||
row = fetchone(env_dirs["ops"],
|
||||
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||
assert row[0] is None
|
||||
# Y la entidad sigue existiendo.
|
||||
row = fetchone(env_dirs["ops"],
|
||||
"SELECT id FROM entities WHERE id=?", nid)
|
||||
assert row is not None and row[0] == nid
|
||||
|
||||
def test_node_update_clear_group_id_combinable(self, env_dirs):
|
||||
"""--clear-group-id se combina con otros sets en un mismo update."""
|
||||
nid = self._make(env_dirs, name="combo")
|
||||
cn = sqlite3.connect(env_dirs["ops"])
|
||||
cn.execute("UPDATE entities SET group_id = ? WHERE id = ?",
|
||||
("group_abc", nid))
|
||||
cn.commit()
|
||||
cn.close()
|
||||
run_gx(env_dirs, "node", "update", nid,
|
||||
"--clear-group-id", "--name", "renamed")
|
||||
row = fetchone(env_dirs["ops"],
|
||||
"SELECT group_id, name FROM entities WHERE id=?", nid)
|
||||
assert row[0] is None
|
||||
assert row[1] == "renamed"
|
||||
|
||||
|
||||
class TestCliNodeListShow:
|
||||
def test_node_show_returns_notes(self, env_dirs):
|
||||
@@ -600,3 +651,49 @@ class TestMcpRegression0035d:
|
||||
props = tool["inputSchema"]["properties"]
|
||||
assert "notes" in props
|
||||
assert "append_notes" in props
|
||||
|
||||
|
||||
class TestMcpRegression0036d:
|
||||
"""Issue 0036d: clear_group_id como flag MCP del node_update.
|
||||
Bloquea cualquier futura regresion donde el dispatcher pierda el
|
||||
default o el inputSchema deje de anunciar el campo."""
|
||||
|
||||
def test_node_update_dispatch_includes_clear_group_id_default(self, gx_module):
|
||||
_, defaults = gx_module.MCP_DISPATCH["node_update"]
|
||||
assert "clear_group_id" in defaults
|
||||
assert defaults["clear_group_id"] is False
|
||||
|
||||
def test_node_update_inputschema_advertises_clear_group_id(self, gx_module):
|
||||
tool = next(t for t in gx_module.MCP_TOOLS
|
||||
if t["name"] == "node_update")
|
||||
props = tool["inputSchema"]["properties"]
|
||||
assert "clear_group_id" in props
|
||||
assert props["clear_group_id"]["type"] == "boolean"
|
||||
assert props["clear_group_id"]["default"] is False
|
||||
|
||||
def test_mcp_dispatch_clear_group_id_clears_membership(self, gx_module,
|
||||
mcp_env):
|
||||
"""End-to-end via dispatcher: crear nodo, asignar grupo via SQL,
|
||||
dispatch node_update con clear_group_id=True, verificar NULL."""
|
||||
out = gx_module._mcp_dispatch("node_create",
|
||||
{"name": "grp_member", "type": "text"})
|
||||
nid = out["id"]
|
||||
cn = sqlite3.connect(mcp_env["ops"])
|
||||
cn.execute("UPDATE entities SET group_id = ? WHERE id = ?",
|
||||
("group_mcp", nid))
|
||||
cn.commit()
|
||||
cn.close()
|
||||
out = gx_module._mcp_dispatch("node_update",
|
||||
{"id": nid, "clear_group_id": True})
|
||||
assert out["ok"] is True
|
||||
row = fetchone(mcp_env["ops"],
|
||||
"SELECT group_id FROM entities WHERE id=?", nid)
|
||||
assert row[0] is None
|
||||
|
||||
def test_mcp_dispatch_clear_group_id_idempotent(self, gx_module, mcp_env):
|
||||
out = gx_module._mcp_dispatch("node_create",
|
||||
{"name": "grp_solo", "type": "text"})
|
||||
nid = out["id"]
|
||||
out = gx_module._mcp_dispatch("node_update",
|
||||
{"id": nid, "clear_group_id": True})
|
||||
assert out["ok"] is True
|
||||
|
||||
Reference in New Issue
Block a user