oficina-opencode 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -208
- package/commands/oficina.md +7 -6
- package/dashboard/.dom-check.html +1257 -0
- package/dashboard/README-DASHBOARD.md +64 -0
- package/dashboard/assets/factory.css +1543 -0
- package/dashboard/assets/tokens.css +208 -0
- package/dashboard/build-kanban-data.mjs +279 -0
- package/dashboard/dashboard.css +753 -0
- package/dashboard/dashboard.html +121 -0
- package/dashboard/dashboard.js +610 -0
- package/dashboard/data/snapshot.json +883 -0
- package/dashboard/debate.html +134 -0
- package/dashboard/factory/build-snapshot.mjs +89 -0
- package/dashboard/factory/factory-app.js +1185 -0
- package/dashboard/factory/factory-core.js +541 -0
- package/dashboard/factory/snapshot.js +850 -0
- package/dashboard/factory.html +350 -0
- package/dashboard/kanban-data.json +671 -0
- package/dashboard/kanban-source.js +643 -0
- package/dashboard/kanban.check.mjs +454 -0
- package/dashboard/kanban.html +1257 -0
- package/dashboard/leadership.html +1008 -0
- package/dashboard/lib/debate-app.js +1149 -0
- package/dashboard/lib/debate-core.js +698 -0
- package/dashboard/lib/debate-data.js +163 -0
- package/dashboard/lib/debate-demo.js +110 -0
- package/dashboard/lib/debate.css +1155 -0
- package/dashboard/lib/leadership-model.js +946 -0
- package/dashboard/probe.mjs +35 -0
- package/dashboard/scripts/serve.mjs +122 -0
- package/dashboard/scripts/snapshot-data.mjs +93 -0
- package/dashboard/scripts/snapshot.mjs +28 -0
- package/dashboard/server.mjs +242 -0
- package/dashboard/shot.mjs +54 -0
- package/dashboard/shots/desktop-gates.png +0 -0
- package/dashboard/shots/desktop-light.png +0 -0
- package/dashboard/shots/desktop-real.png +0 -0
- package/dashboard/shots/mobile.png +0 -0
- package/dashboard/test/factory-core.test.mjs +244 -0
- package/dashboard/tests/debate-core.test.mjs +336 -0
- package/dashboard/tests/leadership-model.test.mjs +404 -0
- package/dist/cli.js +148 -26
- package/dist/cli.js.map +1 -1
- package/dist/core/bus.js +3 -3
- package/dist/core/bus.js.map +1 -1
- package/dist/core/catalog.d.ts +9 -1
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +77 -2
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/dashboard.d.ts +33 -0
- package/dist/core/dashboard.d.ts.map +1 -0
- package/dist/core/dashboard.js +115 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/dispatch.d.ts +1 -1
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +84 -12
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/docsync.d.ts +0 -9
- package/dist/core/docsync.d.ts.map +1 -1
- package/dist/core/docsync.js +12 -15
- package/dist/core/docsync.js.map +1 -1
- package/dist/core/environment.d.ts +28 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +80 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/factory.d.ts +16 -0
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +143 -0
- package/dist/core/factory.js.map +1 -1
- package/dist/core/fsx.d.ts +20 -2
- package/dist/core/fsx.d.ts.map +1 -1
- package/dist/core/fsx.js +10 -6
- package/dist/core/fsx.js.map +1 -1
- package/dist/core/gates.d.ts +23 -1
- package/dist/core/gates.d.ts.map +1 -1
- package/dist/core/gates.js +83 -8
- package/dist/core/gates.js.map +1 -1
- package/dist/core/leases.d.ts.map +1 -1
- package/dist/core/leases.js +4 -1
- package/dist/core/leases.js.map +1 -1
- package/dist/core/mesa.d.ts +1 -6
- package/dist/core/mesa.d.ts.map +1 -1
- package/dist/core/mesa.js +1 -6
- package/dist/core/mesa.js.map +1 -1
- package/dist/core/office.d.ts +3 -29
- package/dist/core/office.d.ts.map +1 -1
- package/dist/core/office.js +77 -90
- package/dist/core/office.js.map +1 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +6 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/retry.d.ts +2 -8
- package/dist/core/retry.d.ts.map +1 -1
- package/dist/core/retry.js +3 -34
- package/dist/core/retry.js.map +1 -1
- package/dist/core/roster.js +3 -3
- package/dist/core/roster.js.map +1 -1
- package/dist/core/sandbox.d.ts +38 -1
- package/dist/core/sandbox.d.ts.map +1 -1
- package/dist/core/sandbox.js +119 -15
- package/dist/core/sandbox.js.map +1 -1
- package/dist/core/state.d.ts +2 -2
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +5 -33
- package/dist/core/state.js.map +1 -1
- package/dist/core/stores.d.ts +3 -13
- package/dist/core/stores.d.ts.map +1 -1
- package/dist/core/stores.js +8 -32
- package/dist/core/stores.js.map +1 -1
- package/dist/core/systemPrompt.d.ts.map +1 -1
- package/dist/core/systemPrompt.js +4 -1
- package/dist/core/systemPrompt.js.map +1 -1
- package/dist/core/tripwire.d.ts +22 -8
- package/dist/core/tripwire.d.ts.map +1 -1
- package/dist/core/tripwire.js +71 -26
- package/dist/core/tripwire.js.map +1 -1
- package/dist/core/types.d.ts +6 -14
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +11 -3
- package/dist/core/types.js.map +1 -1
- package/dist/hooks/scopeGuard.d.ts +15 -1
- package/dist/hooks/scopeGuard.d.ts.map +1 -1
- package/dist/hooks/scopeGuard.js +88 -73
- package/dist/hooks/scopeGuard.js.map +1 -1
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +29 -81
- package/dist/tools/index.js.bak +972 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/v1.d.ts.map +1 -1
- package/dist/v1.js +30 -36
- package/dist/v1.js.map +1 -1
- package/dist/v2.d.ts +5 -0
- package/dist/v2.d.ts.map +1 -1
- package/dist/v2.js +79 -46
- package/dist/v2.js.bak +517 -0
- package/dist/v2.js.map +1 -1
- package/docs/GARANTIAS.md +2 -2
- package/docs/PRUEBA-E2E.md +4 -4
- package/docs/QUE-ES.md +4 -5
- package/docs/SEGURIDAD-REPO.md +83 -0
- package/docs/assets/banner.svg +42 -0
- package/docs/catalog-facts.json +3 -3
- package/docs/superpowers/specs/2026-10-29-factory-skill-creator-and-remove-budget.md +182 -0
- package/package.json +14 -6
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tests del modelo del panel del jefe (T-06).
|
|
3
|
+
*
|
|
4
|
+
* Correr: node --test dashboard/tests/
|
|
5
|
+
* El modelo es un script clasico con side-effect: se lo importa y queda en
|
|
6
|
+
* globalThis.OficinaLeadership (mismo objeto que usa leadership.html).
|
|
7
|
+
*/
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
|
|
11
|
+
import "../lib/leadership-model.js";
|
|
12
|
+
|
|
13
|
+
const L = globalThis.OficinaLeadership;
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Fixtures
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const NOW = Date.parse("2026-10-29T18:00:00.000Z");
|
|
20
|
+
|
|
21
|
+
function snapshot(overrides = {}) {
|
|
22
|
+
const base = {
|
|
23
|
+
generatedAt: new Date(NOW).toISOString(),
|
|
24
|
+
source: "test",
|
|
25
|
+
state: {
|
|
26
|
+
runId: "run-test",
|
|
27
|
+
status: "running",
|
|
28
|
+
cards: {
|
|
29
|
+
"T-06": {
|
|
30
|
+
id: "T-06",
|
|
31
|
+
title: "Panel de intervencion del jefe",
|
|
32
|
+
state: "in_progress",
|
|
33
|
+
owner: "e-xr-immersive-developer",
|
|
34
|
+
reviewer: "e-testing-api-tester",
|
|
35
|
+
acceptance: ["Tres secciones", "Filtros"],
|
|
36
|
+
gate: "leadership.html funcional",
|
|
37
|
+
},
|
|
38
|
+
"T-00": { id: "T-00", title: "Guard", state: "done", owner: "e-testing-api-tester" },
|
|
39
|
+
"T-07": { id: "T-07", title: "Kanban", state: "rework", owner: "e-xr-immersive-developer" },
|
|
40
|
+
},
|
|
41
|
+
decisions: [
|
|
42
|
+
{ at: "2026-10-29T17:39:09.101Z", cardId: "T-06", decision: "approved", reason: "routing: sin agente disponible", actor: "integrator" },
|
|
43
|
+
{ at: "2026-10-29T16:00:00.000Z", cardId: "T-07", decision: "rework", reason: "distancia de similitud 0.42, bajo el umbral", actor: "oficina-critico" },
|
|
44
|
+
{ at: "2026-10-29T15:00:00.000Z", cardId: "T-07", decision: "replan", reason: "el contrato de datos no cierra con el schema actual", actor: "oficina-director" },
|
|
45
|
+
{ at: "2026-10-29T14:00:00.000Z", cardId: "T-00", decision: "spawn_rejected", reason: "no hace falta agente nuevo: factory ya cubre", actor: "oficina-director" },
|
|
46
|
+
],
|
|
47
|
+
escalations: [
|
|
48
|
+
{
|
|
49
|
+
id: "esc-1",
|
|
50
|
+
cardId: "T-07",
|
|
51
|
+
at: "2026-10-29T13:00:00.000Z",
|
|
52
|
+
cause: "deadlock entre lease de T-07 y T-06",
|
|
53
|
+
options: [{ id: "o1", label: "Liberar lease", tradeoff: "pierde progreso" }],
|
|
54
|
+
recommendation: "liberar",
|
|
55
|
+
status: "open",
|
|
56
|
+
},
|
|
57
|
+
{ id: "esc-0", cardId: "T-06", at: "2026-10-29T12:00:00.000Z", cause: "ya resuelta", options: [], recommendation: "", status: "resolved" },
|
|
58
|
+
],
|
|
59
|
+
context: { decisions: ["HTML + CSS + JS vanilla sin build step", "T-06 consume snapshot unico"] },
|
|
60
|
+
},
|
|
61
|
+
roster: {
|
|
62
|
+
agents: {
|
|
63
|
+
"e-xr-immersive-developer": { id: "e-xr-immersive-developer", displayName: "XR Immersive Developer" },
|
|
64
|
+
"e-testing-api-tester": { id: "e-testing-api-tester", displayName: "API Tester" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
messages: [
|
|
68
|
+
{
|
|
69
|
+
channel: "coord-arch",
|
|
70
|
+
message: {
|
|
71
|
+
id: "m-arch-1", seq: 1, from: "architect", to: ["*"], kind: "NOTICE",
|
|
72
|
+
subject: "Contrato de snapshot",
|
|
73
|
+
body: { type: "contract", reason: "un endpoint evita lecturas rasgadas", text: "snapshot { state, roster, messages }" },
|
|
74
|
+
createdAt: "2026-10-29T17:00:00.000Z", priority: "high",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
channel: "coord-flow",
|
|
79
|
+
message: {
|
|
80
|
+
id: "m-flow-1", seq: 2, from: "integrator", to: ["e-testing-api-tester"], kind: "NOTICE",
|
|
81
|
+
subject: "T-06 despachada",
|
|
82
|
+
body: { card: "T-06", reason: "owner asignado, lease activo por 900s" },
|
|
83
|
+
createdAt: "2026-10-29T17:10:00.000Z", priority: "normal",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
channel: "peer",
|
|
88
|
+
message: {
|
|
89
|
+
id: "m-peer-1", seq: 3, from: "e-testing-api-tester", to: ["*"], kind: "STATUS",
|
|
90
|
+
subject: "T-00 entregada", body: { card: "T-00" },
|
|
91
|
+
createdAt: "2026-10-29T17:20:00.000Z", priority: "normal",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
artifacts: { "T-06": { dir: "../.oficina/artifacts/T-06/", files: ["handoff.md", "evidence.md"] } },
|
|
96
|
+
};
|
|
97
|
+
return { ...base, ...overrides };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// Clasificacion por coordinador
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
test("routing de state.json cae en Integrator", () => {
|
|
105
|
+
const cls = L.classifyDecision({ decision: "approved", reason: "routing: sin agente disponible", actor: "integrator" });
|
|
106
|
+
assert.equal(cls.coordinator, "integrator");
|
|
107
|
+
assert.equal(cls.type, "routing");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("escalated cae en Commander aunque el texto hable de gates", () => {
|
|
111
|
+
const cls = L.classifyDecision({ decision: "escalated", reason: "el gate de seguridad no cierra", actor: "e-testing-api-tester" });
|
|
112
|
+
assert.equal(cls.coordinator, "commander");
|
|
113
|
+
assert.equal(cls.type, "escalation");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("rework por rechazo de review cae en Integrator/gate", () => {
|
|
117
|
+
const cls = L.classifyDecision({ decision: "rework", reason: "1 de 3 rechazos", actor: "oficina-critico" });
|
|
118
|
+
assert.equal(cls.coordinator, "integrator");
|
|
119
|
+
assert.equal(cls.type, "gate");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("replan cae en Architect (cambia el enfoque tecnico)", () => {
|
|
123
|
+
const cls = L.classifyDecision({ decision: "replan", reason: "", actor: "oficina-director" });
|
|
124
|
+
assert.equal(cls.coordinator, "architect");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("spawn_approved / spawn_rejected caen en Integrator/spawn", () => {
|
|
128
|
+
for (const decision of ["spawn_approved", "spawn_rejected"]) {
|
|
129
|
+
const cls = L.classifyDecision({ decision, reason: "factory", actor: "oficina-director" });
|
|
130
|
+
assert.equal(cls.coordinator, "integrator", decision);
|
|
131
|
+
assert.equal(cls.type, "spawn", decision);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("deadlock se clasifica aunque el actor sea el director", () => {
|
|
136
|
+
const cls = L.classifyDecision({ decision: "approved", reason: "deadlock: dos leases sobre la misma card", actor: "oficina-director" });
|
|
137
|
+
assert.equal(cls.type, "deadlock");
|
|
138
|
+
assert.equal(cls.coordinator, "integrator");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("classifyActor reconoce los tres coordinadores", () => {
|
|
142
|
+
assert.equal(L.classifyActor("oficina-director"), "commander");
|
|
143
|
+
assert.equal(L.classifyActor("commander"), "commander");
|
|
144
|
+
assert.equal(L.classifyActor("integrator"), "integrator");
|
|
145
|
+
assert.equal(L.classifyActor("oficina-architect"), "architect");
|
|
146
|
+
assert.equal(L.classifyActor("e-testing-api-tester"), null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// Extraccion: las tres secciones
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
test("extractDecisions produce las tres secciones con contenido", () => {
|
|
154
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
155
|
+
assert.deepEqual(Object.keys(res.sections).sort(), ["architect", "commander", "integrator"]);
|
|
156
|
+
for (const id of ["commander", "integrator", "architect"]) {
|
|
157
|
+
assert.ok(res.sections[id].length > 0, `${id} deberia tener decisiones`);
|
|
158
|
+
for (const item of res.sections[id]) assert.equal(item.coordinator, id);
|
|
159
|
+
}
|
|
160
|
+
assert.equal(res.counts.total, res.items.length);
|
|
161
|
+
assert.equal(res.counts.commander + res.counts.integrator + res.counts.architect, res.counts.total);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("cada decision trae los seis campos exigidos y no quedan vacios", () => {
|
|
165
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
166
|
+
assert.ok(res.items.length >= 9);
|
|
167
|
+
for (const item of res.items) {
|
|
168
|
+
assert.ok(isFinite(Date.parse(item.at)), "timestamp valido: " + item.id);
|
|
169
|
+
assert.ok(item.actorId && item.actorId.length, "actor: " + item.id);
|
|
170
|
+
assert.ok(item.cardId === null || typeof item.cardId === "string", "cardId: " + item.id);
|
|
171
|
+
assert.ok(typeof item.decision === "string" && item.decision.length, "decision: " + item.id);
|
|
172
|
+
assert.ok(typeof item.reason === "string" && item.reason.length, "razon: " + item.id);
|
|
173
|
+
assert.ok(typeof item.nextAction === "string" && item.nextAction.length, "siguiente accion: " + item.id);
|
|
174
|
+
assert.ok(typeof item.typeLabel === "string" && item.typeLabel.length, "tipo: " + item.id);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("ordena de mas reciente a mas viejo", () => {
|
|
179
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
180
|
+
for (let i = 1; i < res.items.length; i++) {
|
|
181
|
+
assert.ok(Date.parse(res.items[i - 1].at) >= Date.parse(res.items[i].at));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("lee mensajes de los tres canales de coordinadores y descarta peer", () => {
|
|
186
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
187
|
+
const channels = new Set(res.items.filter((i) => i.source === "bus").map((i) => i.channel));
|
|
188
|
+
assert.deepEqual([...channels].sort(), ["coord-arch", "coord-flow"]);
|
|
189
|
+
assert.ok(!res.items.some((i) => i.id === "msg:m-peer-1"));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("incluye peer solo si se pide explicitamente", () => {
|
|
193
|
+
const res = L.extractDecisions(snapshot(), { now: NOW, includePeer: true });
|
|
194
|
+
assert.ok(res.items.some((i) => i.id === "msg:m-peer-1"));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("un mensaje de coord-arch sin tipo explicito cae en Architect", () => {
|
|
198
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
199
|
+
const archMsg = res.items.find((i) => i.id === "msg:m-arch-1");
|
|
200
|
+
assert.ok(archMsg);
|
|
201
|
+
assert.equal(archMsg.coordinator, "architect");
|
|
202
|
+
assert.equal(archMsg.type, "contract");
|
|
203
|
+
assert.equal(archMsg.cardId, null);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("infiere cardId desde el cuerpo y desde el asunto", () => {
|
|
207
|
+
assert.equal(L.inferCardId({ subject: "T-06 despachada", body: {} }), "T-06");
|
|
208
|
+
assert.equal(L.inferCardId({ subject: "nada", body: { card: "T-07" } }), "T-07");
|
|
209
|
+
assert.equal(L.inferCardId({ subject: "sin card", body: {} }), null);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("las escalations abiertas van a Commander y las resueltas se omiten", () => {
|
|
213
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
214
|
+
const escs = res.items.filter((i) => i.source === "escalation");
|
|
215
|
+
assert.equal(escs.length, 1);
|
|
216
|
+
assert.equal(escs[0].cardId, "T-07");
|
|
217
|
+
assert.equal(escs[0].coordinator, "commander");
|
|
218
|
+
assert.match(escs[0].nextAction, /usuario/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("el ledger context.decisions alimenta Architect como tech decisions", () => {
|
|
222
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
223
|
+
const ctxItems = res.items.filter((i) => i.source === "context");
|
|
224
|
+
assert.equal(ctxItems.length, 2);
|
|
225
|
+
for (const item of ctxItems) {
|
|
226
|
+
assert.equal(item.coordinator, "architect");
|
|
227
|
+
assert.equal(item.type, "tech_decision");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("no rompe con snapshot vacio", () => {
|
|
232
|
+
const res = L.extractDecisions({}, { now: NOW });
|
|
233
|
+
assert.equal(res.items.length, 0);
|
|
234
|
+
assert.deepEqual(res.counts, { total: 0, commander: 0, integrator: 0, architect: 0 });
|
|
235
|
+
assert.deepEqual(res.sections, { commander: [], integrator: [], architect: [] });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("no rompe con entradas malformadas", () => {
|
|
239
|
+
const res = L.extractDecisions({
|
|
240
|
+
state: { decisions: [null, 42, { at: "no-es-fecha", decision: "approved" }], escalations: [null], context: { decisions: [null] } },
|
|
241
|
+
messages: [null, { channel: "coord-flow" }, { channel: "coord-flow", message: { id: "x" } }],
|
|
242
|
+
}, { now: NOW });
|
|
243
|
+
assert.ok(Array.isArray(res.items));
|
|
244
|
+
for (const item of res.items) assert.ok(item.nextAction.length > 0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
248
|
+
// Siguiente accion
|
|
249
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
test("routing sobre card todo manda a esperar claim del owner", () => {
|
|
252
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
253
|
+
const routing = res.items.find((i) => i.type === "routing" && i.cardId === "T-06");
|
|
254
|
+
assert.ok(routing);
|
|
255
|
+
assert.match(routing.nextAction, /claim/i);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("escalation manda a decidir el usuario", () => {
|
|
259
|
+
assert.match(L.deriveNextAction({ type: "escalation", raw: "escalated" }, null), /usuario/i);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("gate sobre card en rework manda al owner a rehacer", () => {
|
|
263
|
+
const action = L.deriveNextAction({ type: "gate", raw: "rework" }, { state: "rework", owner: "e-x" });
|
|
264
|
+
assert.match(action, /rehace/i);
|
|
265
|
+
assert.match(action, /e-x/);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("deadlock manda a liberar lease y reasignar", () => {
|
|
269
|
+
assert.match(L.deriveNextAction({ type: "deadlock", raw: "approved" }, null), /lease/i);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("approved sobre card done no pide accion", () => {
|
|
273
|
+
assert.match(L.deriveNextAction({ type: "routing", raw: "approved" }, { state: "done" }), /Nada/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
277
|
+
// Filtros
|
|
278
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
test("filtra por coordinador", () => {
|
|
281
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
282
|
+
const only = L.filterDecisions(res.items, { coordinator: "architect" });
|
|
283
|
+
assert.ok(only.length > 0);
|
|
284
|
+
for (const item of only) assert.equal(item.coordinator, "architect");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("filtra por card", () => {
|
|
288
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
289
|
+
const only = L.filterDecisions(res.items, { card: "T-07" });
|
|
290
|
+
assert.ok(only.length >= 2);
|
|
291
|
+
for (const item of only) assert.equal(item.cardId, "T-07");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("filtra por tipo de decision", () => {
|
|
295
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
296
|
+
const only = L.filterDecisions(res.items, { type: "gate" });
|
|
297
|
+
assert.ok(only.length > 0);
|
|
298
|
+
for (const item of only) assert.equal(item.type, "gate");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("filtra por texto libre (razon y accion)", () => {
|
|
302
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
303
|
+
assert.ok(L.filterDecisions(res.items, { query: "lease" }).length > 0);
|
|
304
|
+
assert.ok(L.filterDecisions(res.items, { query: "routing" }).length > 0);
|
|
305
|
+
assert.equal(L.filterDecisions(res.items, { query: "zzz-no-existe" }).length, 0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("los filtros se combinan", () => {
|
|
309
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
310
|
+
const only = L.filterDecisions(res.items, { coordinator: "integrator", card: "T-07" });
|
|
311
|
+
for (const item of only) {
|
|
312
|
+
assert.equal(item.coordinator, "integrator");
|
|
313
|
+
assert.equal(item.cardId, "T-07");
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("filterOptions lista cards y tipos presentes", () => {
|
|
318
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
319
|
+
const opts = L.filterOptions(res.items);
|
|
320
|
+
assert.deepEqual(opts.cards.map((c) => c.id), ["T-00", "T-06", "T-07"]);
|
|
321
|
+
const typeIds = opts.types.map((t) => t.id);
|
|
322
|
+
assert.ok(typeIds.includes("routing"));
|
|
323
|
+
assert.ok(typeIds.includes("contract"));
|
|
324
|
+
assert.ok(typeIds.includes("escalation"));
|
|
325
|
+
for (const t of opts.types) assert.ok(L.COORDINATOR_IDS.includes(t.coordinator));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("groupByCoordinator siempre devuelve las tres claves", () => {
|
|
329
|
+
const g = L.groupByCoordinator(L.filterDecisions([], {}));
|
|
330
|
+
assert.deepEqual(Object.keys(g).sort(), ["architect", "commander", "integrator"]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
334
|
+
// Links a card y artifacts
|
|
335
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
test("cardLinks apunta a la card afectada y a sus artifacts", () => {
|
|
338
|
+
const links = L.cardLinks("T-06");
|
|
339
|
+
assert.equal(links.card, "#card-T-06");
|
|
340
|
+
assert.match(links.kanban, /kanban\.html#T-06$/);
|
|
341
|
+
assert.equal(links.artifacts, "../.oficina/artifacts/T-06/");
|
|
342
|
+
assert.equal(links.handoff, "../.oficina/artifacts/T-06/handoff.md");
|
|
343
|
+
assert.equal(links.evidence, "../.oficina/artifacts/T-06/evidence.md");
|
|
344
|
+
assert.equal(links.gates, "../.oficina/artifacts/T-06/gates.md");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("cardLinks devuelve nulls cuando no hay card", () => {
|
|
348
|
+
const links = L.cardLinks(null);
|
|
349
|
+
for (const k of Object.keys(links)) assert.equal(links[k], null, k);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("las decisiones sin card no inventan links", () => {
|
|
353
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
354
|
+
const archMsg = res.items.find((i) => i.id === "msg:m-arch-1");
|
|
355
|
+
assert.equal(archMsg.links.card, null);
|
|
356
|
+
assert.equal(archMsg.links.artifacts, null);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("las decisiones con card enlazan artifacts reales", () => {
|
|
360
|
+
const res = L.extractDecisions(snapshot(), { now: NOW });
|
|
361
|
+
const t06 = res.items.filter((i) => i.cardId === "T-06");
|
|
362
|
+
assert.ok(t06.length > 0);
|
|
363
|
+
for (const item of t06) assert.equal(item.links.artifacts, "../.oficina/artifacts/T-06/");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
367
|
+
// Tiempo
|
|
368
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
test("formatRelative cubre los rangos basicos", () => {
|
|
371
|
+
const base = "2026-10-29T18:00:00.000Z";
|
|
372
|
+
assert.equal(L.formatRelative(base, NOW), "ahora");
|
|
373
|
+
assert.equal(L.formatRelative("2026-10-29T17:59:30.000Z", NOW), "hace 30s");
|
|
374
|
+
assert.equal(L.formatRelative("2026-10-29T17:55:00.000Z", NOW), "hace 5m");
|
|
375
|
+
assert.equal(L.formatRelative("2026-10-29T15:00:00.000Z", NOW), "hace 3h");
|
|
376
|
+
assert.equal(L.formatRelative("2026-10-27T18:00:00.000Z", NOW), "hace 2d");
|
|
377
|
+
assert.equal(L.formatRelative("basura", NOW), "—");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("formatClock devuelve HH:MM:SS o —", () => {
|
|
381
|
+
assert.match(L.formatClock("2026-10-29T18:04:09.000Z"), /^\d{2}:\d{2}:\d{2}$/);
|
|
382
|
+
assert.equal(L.formatClock("nope"), "—");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
386
|
+
// Demo
|
|
387
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
test("el snapshot demo produce las tres secciones pobladas", () => {
|
|
390
|
+
const res = L.extractDecisions(L.demoSnapshot(NOW), { now: NOW });
|
|
391
|
+
for (const id of ["commander", "integrator", "architect"]) {
|
|
392
|
+
assert.ok(res.sections[id].length > 0, `demo sin decisiones en ${id}`);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("la API publica expone todo lo que el panel necesita", () => {
|
|
397
|
+
for (const key of [
|
|
398
|
+
"COORDINATORS", "classifyDecision", "deriveNextAction", "extractDecisions",
|
|
399
|
+
"filterDecisions", "filterOptions", "cardLinks", "formatRelative", "demoSnapshot",
|
|
400
|
+
]) {
|
|
401
|
+
assert.ok(L[key], "falta " + key);
|
|
402
|
+
}
|
|
403
|
+
assert.equal(L.COORDINATORS.length, 3);
|
|
404
|
+
});
|