supcoflow 0.7.0 → 0.8.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 +253 -176
- package/dist/alias.js +60 -0
- package/dist/api.js +41 -19
- package/dist/argumentos.js +92 -85
- package/dist/cli.js +40 -35
- package/dist/comandos/clave-vigente.js +2 -2
- package/dist/comandos/claves.js +25 -25
- package/dist/comandos/configurar.js +40 -40
- package/dist/comandos/contexto.js +102 -0
- package/dist/comandos/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +162 -152
- package/dist/comandos/guia.js +150 -62
- package/dist/comandos/instalacion.js +73 -41
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +177 -64
- package/dist/comandos/proyecto-del-repo.js +12 -2
- package/dist/comandos/proyecto.js +48 -34
- package/dist/comandos/quien-soy.js +8 -7
- package/dist/comandos/repo-conectar.js +9 -9
- package/dist/comandos/widget.js +9 -5
- package/dist/diagnostico.js +51 -0
- package/dist/entorno.js +57 -26
- package/dist/flags-por-comando.js +25 -21
- package/dist/guardado/archivo.js +2 -2
- package/dist/guardado/index.js +12 -8
- package/dist/guardado/llavero.js +5 -5
- package/dist/guardado/lugar.js +63 -0
- package/dist/guardado/protegido.js +38 -0
- package/dist/guardado/repo.js +98 -0
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +16 -28
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +40 -22
- package/dist/repo-local.js +95 -13
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
package/dist/comandos/probar.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { agruparPuntos, contarBuenos, lineaDeLoRevisado, nombreDelPunto, puntoDeSesion, puntoDelRepo, TOTAL_DE_INSTALACION } from "../diagnostico.js";
|
|
3
|
+
import { leerRepoLocal } from "../repo-local.js";
|
|
2
4
|
import { PLAZO_MINIMO_DE_SONDEO_MS, TRAMO_MAXIMO_MS } from "../sondeo.js";
|
|
3
5
|
import { fallo, rutaDe } from "./configurar.js";
|
|
4
6
|
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
@@ -6,6 +8,7 @@ import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
|
6
8
|
// gastar un pedido (ni guardar un intento que el motor va a rechazar); la regla de verdad es la del motor.
|
|
7
9
|
const MAX_MENSAJE = 20_000;
|
|
8
10
|
const ID_DE_PRUEBA = /^prb_[0-9a-f]{16}$/;
|
|
11
|
+
const TECHO = /^\d+(?:\.\d{1,4})?$/;
|
|
9
12
|
const INTERVALO_DE_SEGUIMIENTO_MS = 5_000;
|
|
10
13
|
const PLAZO_MAXIMO_DE_SONDEO_MS = 15_000;
|
|
11
14
|
// La tabla de la prueba (brief M7, «los estados»): qué código sale en cada estado. Lo que no está acá es
|
|
@@ -16,7 +19,18 @@ const PIDEN_UNA_PERSONA = new Set([
|
|
|
16
19
|
"issue_con_cierre_anomalo", "fallido", "cierre_sin_evidencia", "reclamado_sin_run", "arranque_incierto", "esperando_cupo", "sin_despacho",
|
|
17
20
|
]);
|
|
18
21
|
const usd = (n) => `US$ ${n.toFixed(2)}`;
|
|
19
|
-
const usdONull = (n) => (n === null ? "
|
|
22
|
+
const usdONull = (n) => (n === null ? "unmeasured" : usd(n));
|
|
23
|
+
// La política como se dice en inglés; a la API siempre viaja la suya (`relevar` / `guardar`).
|
|
24
|
+
const POLITICA_EN_INGLES = { relevar: "investigate", guardar: "save" };
|
|
25
|
+
const enInglesLaPolitica = (p) => (p === null ? "none" : (POLITICA_EN_INGLES[p] ?? p));
|
|
26
|
+
const aLaPoliticaDeLaApi = (p) => p === "investigate" || p === "relevar" ? "relevar" : p === "save" || p === "guardar" ? "guardar" : null;
|
|
27
|
+
// El comando exacto para retomar un pedido guardado: con lo que se autorizó, no con lo que haya ahora. Un pedido SIN
|
|
28
|
+
// política (lo guardó la 0.7.0, o se pidió con el alias viejo) sólo se retoma por el alias: el comando canónico exige
|
|
29
|
+
// `--expect-policy` (3 falta_politica) y, con una política, ya no es el mismo pedido (4 intento_pendiente). No se inventa
|
|
30
|
+
// una política para él: cambiaría su cuerpo y su clave.
|
|
31
|
+
const comandoDeReintento = (i) => i.politicaEsperada === null
|
|
32
|
+
? `supcoflow probar --autorizo-gasto --ceiling ${i.techoUsd}`
|
|
33
|
+
: `supcoflow test --authorize-spend --ceiling ${i.techoUsd} --expect-policy ${enInglesLaPolitica(i.politicaEsperada)}`;
|
|
20
34
|
function datosDelDiagnostico(g) {
|
|
21
35
|
return {
|
|
22
36
|
listo: g.listo,
|
|
@@ -29,7 +43,27 @@ function datosDelDiagnostico(g) {
|
|
|
29
43
|
prueba_abierta: g.pruebaAbierta,
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
|
-
const lineasDePuntos = (
|
|
46
|
+
const lineasDePuntos = (puntos, nombre = (p) => p) => puntos.map((p) => ` [${p.ok ? "ok" : "missing"}] ${nombre(p.punto)}: ${p.mensaje}`);
|
|
47
|
+
// Lo que esta máquina puede o no hacer con el pedido de la prueba, visto ANTES del OK de gasto y sin escribir nada. Un
|
|
48
|
+
// prechequeo que falla es «no ok», nunca un error: avisar tarde es justo lo que esto evita.
|
|
49
|
+
async function revisarLaMaquina(d, proyectoId) {
|
|
50
|
+
try {
|
|
51
|
+
return await d.pruebas.revisar(proyectoId);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { maquina: { ok: false, impedimentos: [{ codigo: "no_se_pudo_revisar", detalle: "I couldn't check whether this machine can save the test request." }] }, intentoPendiente: null };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const datosDeLaMaquina = (r) => ({ maquina: r.maquina });
|
|
58
|
+
const datosDelIntentoPendiente = (r) => ({
|
|
59
|
+
intento_pendiente: r.intentoPendiente === null ? null : { techo_usd: r.intentoPendiente.techoUsd, politica: r.intentoPendiente.politica },
|
|
60
|
+
});
|
|
61
|
+
const detalles = (r) => r.maquina.impedimentos.map((i) => i.detalle).join(" ");
|
|
62
|
+
function puntoDeLaMaquina(r) {
|
|
63
|
+
return r.maquina.ok
|
|
64
|
+
? { punto: "maquina", ok: true, mensaje: "No obstacle found to saving the test request on this machine.", grupo: "prueba" }
|
|
65
|
+
: { punto: "maquina", ok: false, mensaje: `This machine can't save the test request yet: ${detalles(r)}`, grupo: "prueba" };
|
|
66
|
+
}
|
|
33
67
|
export async function diagnosticar(d, flags) {
|
|
34
68
|
const p = await proyectoDelRepo(d, flags);
|
|
35
69
|
if (!p.ok)
|
|
@@ -41,33 +75,96 @@ export async function diagnosticar(d, flags) {
|
|
|
41
75
|
catch (e) {
|
|
42
76
|
return resultadoDeFallo(e);
|
|
43
77
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
// Los dos puntos que sólo la terminal puede ver: la llamada de arriba autenticó (si no, no se llega acá) y el
|
|
79
|
+
// `origin` de esta carpeta es el repo del proyecto. Sin git o sin origin no se adivina: el punto sale no ok.
|
|
80
|
+
let local;
|
|
81
|
+
try {
|
|
82
|
+
local = await leerRepoLocal(d.git, d.cwd);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
local = { repo: null, raiz: null, motivo: "sin_git" };
|
|
48
86
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
87
|
+
const agrupados = agruparPuntos(g, [puntoDeSesion(), puntoDelRepo(local, p.proyecto.repo)]);
|
|
88
|
+
const instalacion = agrupados.instalacion;
|
|
89
|
+
// El punto local de la prueba (1A.1): ¿esta máquina puede guardar el pedido? Si no, la prueba no entra, y se dice acá y
|
|
90
|
+
// no después del OK de gasto. No cuenta en el «N of 6» de la instalación.
|
|
91
|
+
const revision = await revisarLaMaquina(d, p.proyecto.id);
|
|
92
|
+
const prueba = [puntoDeLaMaquina(revision), ...agrupados.prueba];
|
|
93
|
+
const buenos = contarBuenos(instalacion);
|
|
94
|
+
const listo = g.listo && revision.maquina.ok && instalacion.every((x) => x.ok || !["sign-in", "repo"].includes(x.punto));
|
|
95
|
+
const datos = {
|
|
96
|
+
proyecto: { id: p.proyecto.id, repo: p.proyecto.repo },
|
|
97
|
+
...datosDelDiagnostico(g),
|
|
98
|
+
listo,
|
|
99
|
+
puntos: [...instalacion, ...prueba],
|
|
100
|
+
instalacion: { ok: buenos, total: TOTAL_DE_INSTALACION, puntos: instalacion },
|
|
101
|
+
prueba: { listo: g.listo && revision.maquina.ok, puntos: prueba },
|
|
102
|
+
...datosDeLaMaquina(revision),
|
|
103
|
+
...datosDelIntentoPendiente(revision),
|
|
104
|
+
};
|
|
105
|
+
const resumen = `Monthly balance ${usdONull(g.saldoUsd)} of ${usdONull(g.topeUsd)}; a test authorizes up to ${usd(g.techoUsd)}. Policy: ${enInglesLaPolitica(g.politica)}.`;
|
|
106
|
+
const motivos = [
|
|
107
|
+
...prueba.filter((x) => !x.ok).map((x) => x.mensaje),
|
|
108
|
+
...(buenos < TOTAL_DE_INSTALACION ? [`the installation isn't complete (${buenos} of ${TOTAL_DE_INSTALACION})`] : []),
|
|
109
|
+
];
|
|
110
|
+
const listaParaProbar = listo ? "Ready for a test: yes." : `Ready for a test: no, because ${motivos.join("; ") || "the engine says it isn't ready"}`;
|
|
111
|
+
const cuerpo = [
|
|
112
|
+
lineaDeLoRevisado(buenos),
|
|
113
|
+
...lineasDePuntos(instalacion, nombreDelPunto),
|
|
114
|
+
listaParaProbar,
|
|
115
|
+
...lineasDePuntos(prueba),
|
|
116
|
+
resumen,
|
|
117
|
+
];
|
|
118
|
+
if (listo)
|
|
119
|
+
return { codigo: SALIDA.ok, json: { ok: true, ...datos }, texto: cuerpo.join("\n") };
|
|
120
|
+
const message = `Something needs fixing before testing ${p.proyecto.repo}.`;
|
|
121
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "no_listo", message, ...datos }, texto: [message, ...cuerpo].join("\n") };
|
|
51
122
|
}
|
|
52
123
|
export async function probar(d, flags) {
|
|
53
124
|
// El reloj del tramo arranca con el comando: los 90 s son del comando entero, no sólo de los sondeos.
|
|
54
125
|
const inicio = d.ahora();
|
|
55
126
|
// Todo lo que se puede rechazar sin mirar nada, antes de tocar el motor o lo guardado.
|
|
56
|
-
if (flags.wait && (flags.
|
|
57
|
-
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "`
|
|
127
|
+
if (flags.wait && (flags.authorizeSpend || flags.message !== null || flags.ceiling !== null || flags.expectPolicy !== null)) {
|
|
128
|
+
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "`test --wait` only follows a test: no --authorize-spend, --ceiling, --expect-policy or --message. First `supcoflow test --authorize-spend --ceiling <usd> --expect-policy <policy>`, then `supcoflow test --wait`.");
|
|
129
|
+
}
|
|
130
|
+
if (!flags.wait && flags.testId !== null) {
|
|
131
|
+
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--test-id goes with --wait: `supcoflow test --wait --test-id prb_…`.");
|
|
58
132
|
}
|
|
59
|
-
if (!flags.wait && flags.
|
|
60
|
-
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--
|
|
133
|
+
if (!flags.authorizeSpend && !flags.wait && (flags.ceiling !== null || flags.expectPolicy !== null)) {
|
|
134
|
+
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--ceiling and --expect-policy go with --authorize-spend: they say what you authorize.");
|
|
61
135
|
}
|
|
62
|
-
if (flags.
|
|
63
|
-
return fallo(SALIDA.entradaInvalida, "prueba_invalida", "--
|
|
136
|
+
if (flags.testId !== null && !ID_DE_PRUEBA.test(flags.testId)) {
|
|
137
|
+
return fallo(SALIDA.entradaInvalida, "prueba_invalida", "--test-id is the id `supcoflow test` printed (prb_ and 16 characters).");
|
|
64
138
|
}
|
|
65
139
|
let mensaje = null;
|
|
66
|
-
if (flags.
|
|
67
|
-
mensaje = flags.
|
|
140
|
+
if (flags.message !== null) {
|
|
141
|
+
mensaje = flags.message.trim();
|
|
68
142
|
if (mensaje.length === 0 || mensaje.length > MAX_MENSAJE) {
|
|
69
|
-
return fallo(SALIDA.entradaInvalida, "mensaje_invalido", `--
|
|
143
|
+
return fallo(SALIDA.entradaInvalida, "mensaje_invalido", `--message goes with text, up to ${MAX_MENSAJE} characters.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Lo que la persona autorizó: el techo y la política que vio. Sin eso no hay pedido que valga (R5).
|
|
147
|
+
let autorizado = null;
|
|
148
|
+
if (flags.authorizeSpend) {
|
|
149
|
+
if (flags.ceiling === null) {
|
|
150
|
+
const comoSeEscribe = "`supcoflow test --authorize-spend --ceiling <usd> --expect-policy investigate|save`";
|
|
151
|
+
return fallo(SALIDA.entradaInvalida, "falta_techo", flags.viejo === true
|
|
152
|
+
? `\`probar --autorizo-gasto\` now needs --ceiling <usd>, the number you were shown: an alias can't widen what you authorized. Use ${comoSeEscribe}. (The old behaviour is in supcoflow 0.7.0.)`
|
|
153
|
+
: `--authorize-spend needs --ceiling <usd>, the number you were shown: ${comoSeEscribe}.`);
|
|
70
154
|
}
|
|
155
|
+
if (!TECHO.test(flags.ceiling) || !(Number(flags.ceiling) > 0)) {
|
|
156
|
+
return fallo(SALIDA.entradaInvalida, "techo_invalido", "--ceiling is a positive number of US dollars, for example 2 or 2.50.");
|
|
157
|
+
}
|
|
158
|
+
let politicaEsperada = null;
|
|
159
|
+
if (flags.expectPolicy !== null) {
|
|
160
|
+
politicaEsperada = aLaPoliticaDeLaApi(flags.expectPolicy);
|
|
161
|
+
if (politicaEsperada === null)
|
|
162
|
+
return fallo(SALIDA.entradaInvalida, "politica_invalida", "--expect-policy is `investigate` or `save`.");
|
|
163
|
+
}
|
|
164
|
+
else if (flags.viejo !== true) {
|
|
165
|
+
return fallo(SALIDA.entradaInvalida, "falta_politica", "--authorize-spend needs --expect-policy investigate|save: the policy you were offered, so a yes to a test that won't investigate can't turn into a paid one.");
|
|
166
|
+
}
|
|
167
|
+
autorizado = { techoUsd: Number(flags.ceiling), politicaEsperada };
|
|
71
168
|
}
|
|
72
169
|
const p = await proyectoDelRepo(d, flags);
|
|
73
170
|
if (!p.ok)
|
|
@@ -75,9 +172,9 @@ export async function probar(d, flags) {
|
|
|
75
172
|
const proyecto = p.proyecto;
|
|
76
173
|
const clave = p.clave;
|
|
77
174
|
if (flags.wait)
|
|
78
|
-
return seguir(d, clave, proyecto.id, flags.
|
|
175
|
+
return seguir(d, clave, proyecto.id, flags.testId, inicio);
|
|
79
176
|
// ── Sin el flag, o con --dry-run: sólo el diagnóstico (gratis). Nada que escriba, ni acá ni en el motor.
|
|
80
|
-
if (
|
|
177
|
+
if (autorizado === null || flags.dryRun) {
|
|
81
178
|
let g;
|
|
82
179
|
try {
|
|
83
180
|
g = await d.leerDiagnostico(d.api, clave, proyecto.id);
|
|
@@ -85,10 +182,12 @@ export async function probar(d, flags) {
|
|
|
85
182
|
catch (e) {
|
|
86
183
|
return resultadoDeFallo(e);
|
|
87
184
|
}
|
|
88
|
-
|
|
185
|
+
// Lo que esta máquina puede hacer con el pedido, visto ANTES del sí de gasto (fricción 3).
|
|
186
|
+
const revision = await revisarLaMaquina(d, proyecto.id);
|
|
187
|
+
const datos = { proyecto: { id: proyecto.id, repo: proyecto.repo }, ...datosDelDiagnostico(g), ...datosDeLaMaquina(revision) };
|
|
89
188
|
const gasto = g.politica === "guardar"
|
|
90
|
-
? `
|
|
91
|
-
: `
|
|
189
|
+
? `With the \`save\` policy the test goes as far as the saved case and spends nothing; the ceiling it authorizes is still ${usd(g.techoUsd)}.`
|
|
190
|
+
: `A test runs an investigation that spends up to ${usd(g.techoUsd)} (monthly balance ${usdONull(g.saldoUsd)} of ${usdONull(g.topeUsd)}).`;
|
|
92
191
|
if (flags.dryRun) {
|
|
93
192
|
// Lo guardado se mira sólo para decir qué haría; si no se puede leer, el dry-run igual contesta.
|
|
94
193
|
let pendiente = null;
|
|
@@ -99,37 +198,44 @@ export async function probar(d, flags) {
|
|
|
99
198
|
pendiente = null;
|
|
100
199
|
}
|
|
101
200
|
const ruta = rutaDe(proyecto, "/pruebas");
|
|
201
|
+
const cuerpo = autorizado === null ? `techo_usd: ${g.techoUsd}` : `techo_usd: ${autorizado.techoUsd}${autorizado.politicaEsperada !== null ? `, politica_esperada: ${autorizado.politicaEsperada}` : ""}`;
|
|
102
202
|
const haria = pendiente
|
|
103
|
-
? `
|
|
104
|
-
: `POST ${ruta} {
|
|
203
|
+
? `would resend the saved request that was never confirmed (POST ${ruta}, the same Idempotency-Key)`
|
|
204
|
+
: `would send POST ${ruta} {${cuerpo}${mensaje !== null ? ", mensaje" : ""}} with a new Idempotency-Key`;
|
|
105
205
|
return {
|
|
106
206
|
codigo: SALIDA.ok,
|
|
107
|
-
json: { ok: true, dry_run: true, autorizado: flags.
|
|
108
|
-
texto: `
|
|
207
|
+
json: { ok: true, dry_run: true, autorizado: flags.authorizeSpend, intento_pendiente: pendiente, haria, ...datos },
|
|
208
|
+
texto: `With --dry-run I do nothing. ${flags.authorizeSpend ? "Without --dry-run, I" : "With --authorize-spend and without --dry-run, I"}${haria.replace(/^would/, "")} (project ${proyecto.repo}, policy ${enInglesLaPolitica(g.politica)}). ${gasto}`,
|
|
109
209
|
};
|
|
110
210
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
211
|
+
const lanzar = `supcoflow test --authorize-spend --ceiling ${g.techoUsd} --expect-policy ${enInglesLaPolitica(g.politica)}`;
|
|
212
|
+
// Primero lo que impide, si algo: es lo que la persona tiene que saber antes de decir que sí.
|
|
213
|
+
const impide = revision.maquina.ok ? "" : `This machine can't save the test request yet: ${detalles(revision)} Fix that before authorizing. `;
|
|
214
|
+
const previo = revision.intentoPendiente;
|
|
215
|
+
const pendiente = previo === null ? ""
|
|
216
|
+
: ` There's a test request waiting for confirmation (ceiling ${usd(previo.techoUsd)}, policy ${enInglesLaPolitica(previo.politica)}): to recover that same request, repeat \`${comandoDeReintento({ clave: "", mensaje: null, techoUsd: previo.techoUsd, politicaEsperada: previo.politica })}\`.`;
|
|
217
|
+
const message = `${impide}${gasto} To launch it: \`${lanzar}\`. \`supcoflow diagnose\` first says whether everything is ready.${pendiente}`;
|
|
218
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "falta_autorizacion", message, ...datos, ...datosDelIntentoPendiente(revision) }, texto: message };
|
|
113
219
|
}
|
|
114
|
-
// ── Con --
|
|
220
|
+
// ── Con --authorize-spend: el ciclo de la clave.
|
|
115
221
|
let intento;
|
|
116
222
|
try {
|
|
117
223
|
const guardado = await d.pruebas.leer(proyecto.id);
|
|
118
224
|
if (guardado.intento !== null) {
|
|
119
|
-
|
|
120
|
-
|
|
225
|
+
const previo = guardado.intento;
|
|
226
|
+
const igual = Math.abs(previo.techoUsd - autorizado.techoUsd) < 1e-9 && previo.politicaEsperada === autorizado.politicaEsperada;
|
|
227
|
+
if (mensaje !== null && mensaje !== previo.mensaje) {
|
|
228
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", `There's a test request waiting for confirmation; repeat \`${comandoDeReintento(previo)}\` without --message to recover it.`);
|
|
121
229
|
}
|
|
122
|
-
|
|
230
|
+
if (!igual) {
|
|
231
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", `There's a test request waiting for confirmation, authorized with a ceiling of ${usd(previo.techoUsd)}; repeat it with exactly that to recover it: \`${comandoDeReintento(previo)}\`.`);
|
|
232
|
+
}
|
|
233
|
+
intento = previo;
|
|
123
234
|
}
|
|
124
235
|
else {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return enCurso(g.pruebaAbierta);
|
|
129
|
-
// No frena (el motor decide lo que acepta), pero quien autorizó tiene que ver lo que falta.
|
|
130
|
-
for (const punto of g.puntos.filter((x) => !x.ok))
|
|
131
|
-
d.avisar(`Aviso (${punto.punto}): ${punto.mensaje}`);
|
|
132
|
-
intento = { clave: d.generar(), mensaje, techoUsd: g.techoUsd };
|
|
236
|
+
// Lo que se manda es lo que la persona vio (R5): no se relee el diagnóstico para elegirlo. El motor compara
|
|
237
|
+
// el techo y la política con los de ahora y rechaza si cambiaron.
|
|
238
|
+
intento = { clave: d.generar(), mensaje, techoUsd: autorizado.techoUsd, politicaEsperada: autorizado.politicaEsperada };
|
|
133
239
|
await d.pruebas.abrir(proyecto.id, intento);
|
|
134
240
|
}
|
|
135
241
|
}
|
|
@@ -142,17 +248,17 @@ export async function probar(d, flags) {
|
|
|
142
248
|
await d.pruebas.confirmar(proyecto.id, r.pruebaId);
|
|
143
249
|
}
|
|
144
250
|
catch {
|
|
145
|
-
const message = `
|
|
251
|
+
const message = `The engine accepted test ${r.pruebaId}, but I couldn't save it on this machine. Repeat \`${comandoDeReintento(intento)}\` (it recovers the same one) or follow it with \`supcoflow test --wait --test-id ${r.pruebaId}\`.`;
|
|
146
252
|
return { codigo: SALIDA.inesperado, json: { ok: false, error: "prueba_no_guardada", message, prueba_id: r.pruebaId, intento_pendiente: true }, texto: message };
|
|
147
253
|
}
|
|
148
254
|
return {
|
|
149
255
|
codigo: SALIDA.esperandoAUnaPersona,
|
|
150
256
|
json: { ok: false, esperando: "relevamiento", prueba_id: r.pruebaId, reporte_id: r.reporteId, estado: r.estado },
|
|
151
|
-
texto: `
|
|
257
|
+
texto: `Test ${r.pruebaId} requested (${r.estado}). Follow it with \`supcoflow test --wait\`.`,
|
|
152
258
|
};
|
|
153
259
|
}
|
|
154
260
|
if (r.tipo === "ambigua") {
|
|
155
|
-
const message = `${r.message}
|
|
261
|
+
const message = `${r.message} The request was saved: repeat \`${comandoDeReintento(intento)}\` and I'll pick up the same test, without asking for another.`;
|
|
156
262
|
return { codigo: SALIDA.inesperado, json: { ok: false, error: r.error, message, intento_pendiente: true }, texto: message };
|
|
157
263
|
}
|
|
158
264
|
// Un rechazo definitivo: el motor no creó nada con esta clave, así que el intento se cierra.
|
|
@@ -160,22 +266,29 @@ export async function probar(d, flags) {
|
|
|
160
266
|
await d.pruebas.descartar(proyecto.id);
|
|
161
267
|
}
|
|
162
268
|
catch {
|
|
163
|
-
d.avisar("
|
|
269
|
+
d.avisar("I couldn't close the saved request; the next `test --authorize-spend` will resend it.");
|
|
164
270
|
}
|
|
165
|
-
return rechazo(r, d.avisar);
|
|
271
|
+
return rechazo(r, intento, d.avisar);
|
|
166
272
|
}
|
|
167
273
|
function enCurso(pruebaId) {
|
|
168
|
-
const message = `
|
|
274
|
+
const message = `This project already has a test with no outcome (${pruebaId}). Follow it with \`supcoflow test --wait --test-id ${pruebaId}\`.`;
|
|
169
275
|
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "prueba_en_curso", message, prueba_id: pruebaId }, texto: message };
|
|
170
276
|
}
|
|
171
|
-
function rechazo(r, avisar) {
|
|
277
|
+
function rechazo(r, intento, avisar) {
|
|
172
278
|
const c = r.cuerpo;
|
|
173
279
|
switch (r.error) {
|
|
174
280
|
case "prueba_en_curso":
|
|
175
281
|
return typeof c.prueba_id === "string" ? enCurso(c.prueba_id) : fallo(SALIDA.haceFaltaUnaPersona, r.error, r.message);
|
|
176
282
|
case "techo_distinto": {
|
|
177
|
-
|
|
178
|
-
|
|
283
|
+
// Sin reintentar: lo que se autorizó ya no es lo que hay, y decidir de nuevo es de la persona (con el número nuevo).
|
|
284
|
+
const nuevo = typeof c.techo_usd === "number" ? c.techo_usd : null;
|
|
285
|
+
const message = `The ceiling for a test changed${nuevo !== null ? `: it's now ${usd(nuevo)}` : ""}, and you authorized ${usd(intento.techoUsd)}. Nothing was started. Ask again with the new number.`;
|
|
286
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: r.error, message, techo_usd: nuevo, autorizado_usd: intento.techoUsd }, texto: message };
|
|
287
|
+
}
|
|
288
|
+
case "politica_distinta": {
|
|
289
|
+
const ahora = typeof c.politica === "string" ? c.politica : typeof c.politica_actual === "string" ? c.politica_actual : null;
|
|
290
|
+
const message = `The project's policy changed${ahora !== null ? `: it's now \`${enInglesLaPolitica(ahora)}\`` : ""}, and you authorized a test under \`${enInglesLaPolitica(intento.politicaEsperada)}\`. Nothing was started. Ask again with the current policy.`;
|
|
291
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: r.error, message, politica: ahora, politica_esperada: intento.politicaEsperada }, texto: message };
|
|
179
292
|
}
|
|
180
293
|
case "tope_mensual":
|
|
181
294
|
return {
|
|
@@ -184,13 +297,13 @@ function rechazo(r, avisar) {
|
|
|
184
297
|
texto: r.message,
|
|
185
298
|
};
|
|
186
299
|
case "requiere_api_key": {
|
|
187
|
-
const message = `
|
|
300
|
+
const message = `The test is requested with your personal API key (the one \`supcoflow login\` leaves), not with a read-only key or the console session. ${r.message}`;
|
|
188
301
|
return fallo(SALIDA.haceFaltaUnaPersona, r.error, message);
|
|
189
302
|
}
|
|
190
303
|
case "idempotency_key_reusada":
|
|
191
304
|
// No debería pasar: siempre se reenvía lo guardado con su clave. Si pasa, la clave ya no sirve.
|
|
192
|
-
avisar("
|
|
193
|
-
return fallo(SALIDA.inesperado, r.error, `${r.message}
|
|
305
|
+
avisar("The engine says this request's key was already used with other content; I discarded it.");
|
|
306
|
+
return fallo(SALIDA.inesperado, r.error, `${r.message} Run \`${comandoDeReintento(intento)}\` again to ask for a new test.`);
|
|
194
307
|
}
|
|
195
308
|
if (r.status === 401)
|
|
196
309
|
return fallo(SALIDA.faltaLogin, "clave_invalida", r.message);
|
|
@@ -201,14 +314,14 @@ function rechazo(r, avisar) {
|
|
|
201
314
|
function textoDelCobro(c) {
|
|
202
315
|
// Pendiente y perdido nunca se muestran como cero: el monto todavía no se sabe, la cota es el techo.
|
|
203
316
|
if (c.estado === "leido" && c.usd !== null)
|
|
204
|
-
return `
|
|
317
|
+
return `Cost: ${usd(c.usd)}.`;
|
|
205
318
|
if (c.estado === "pendiente")
|
|
206
|
-
return `
|
|
319
|
+
return `Charge pending: it's read a few minutes after the run closes (up to ${usd(c.techo_usd)}).`;
|
|
207
320
|
if (c.estado === "perdido")
|
|
208
|
-
return `
|
|
321
|
+
return `Charge lost: the spend couldn't be read (up to ${usd(c.techo_usd)}).`;
|
|
209
322
|
if (c.estado === null)
|
|
210
323
|
return null;
|
|
211
|
-
return `
|
|
324
|
+
return `Charge: ${c.estado} (up to ${usd(c.techo_usd)}).`;
|
|
212
325
|
}
|
|
213
326
|
function resultadoDelSeguimiento(s) {
|
|
214
327
|
const datos = {
|
|
@@ -216,24 +329,24 @@ function resultadoDelSeguimiento(s) {
|
|
|
216
329
|
reporte: s.reporte, run: s.run, issue: s.issue, cobro: s.cobro,
|
|
217
330
|
};
|
|
218
331
|
const lineas = [
|
|
219
|
-
`
|
|
332
|
+
`Test ${s.pruebaId}: ${s.estado}.${s.mensaje ? ` ${s.mensaje}` : ""}`,
|
|
220
333
|
s.issue ? `Issue #${s.issue.numero}: ${s.issue.url}` : null,
|
|
221
|
-
s.run?.motivo ? `
|
|
334
|
+
s.run?.motivo ? `Run reason: ${s.run.motivo}` : null,
|
|
222
335
|
textoDelCobro(s.cobro),
|
|
223
336
|
];
|
|
224
337
|
if (ABIERTOS.has(s.estado)) {
|
|
225
|
-
lineas.push("
|
|
338
|
+
lineas.push("Run `supcoflow test --wait` again.");
|
|
226
339
|
return { codigo: SALIDA.esperandoAUnaPersona, json: { ok: false, esperando: "relevamiento", ...datos }, texto: lineas.filter(Boolean).join("\n") };
|
|
227
340
|
}
|
|
228
341
|
if (CON_DESENLACE_OK.has(s.estado))
|
|
229
342
|
return { codigo: SALIDA.ok, json: { ok: true, ...datos }, texto: lineas.filter(Boolean).join("\n") };
|
|
230
343
|
if (PIDEN_UNA_PERSONA.has(s.estado)) {
|
|
231
344
|
if (s.reporte)
|
|
232
|
-
lineas.push(`
|
|
345
|
+
lineas.push(`The case in the console: ${s.reporte.url}`);
|
|
233
346
|
const texto = lineas.filter(Boolean).join("\n");
|
|
234
347
|
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: s.estado, message: s.mensaje ?? texto, ...datos }, texto };
|
|
235
348
|
}
|
|
236
|
-
const message = `
|
|
349
|
+
const message = `The engine says the test is in «${s.estado}», a state this version of the terminal doesn't know. Update supcoflow or look at it in the console.`;
|
|
237
350
|
return { codigo: SALIDA.inesperado, json: { ok: false, error: "estado_desconocido", message, ...datos }, texto: message };
|
|
238
351
|
}
|
|
239
352
|
async function seguir(d, clave, proyectoId, elegida, inicio) {
|
|
@@ -242,17 +355,17 @@ async function seguir(d, clave, proyectoId, elegida, inicio) {
|
|
|
242
355
|
try {
|
|
243
356
|
const g = await d.pruebas.leer(proyectoId);
|
|
244
357
|
// Un pedido sin confirmar gana sobre la última: seguir la anterior y salir 0 con SU issue se leería
|
|
245
|
-
// como el desenlace de la prueba recién pedida. Con `--
|
|
358
|
+
// como el desenlace de la prueba recién pedida. Con `--test-id` explícito no se llega acá.
|
|
246
359
|
pruebaId = g.ultima;
|
|
247
360
|
if (g.intento !== null) {
|
|
248
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente",
|
|
361
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", `There's a test request waiting for confirmation; repeat \`${comandoDeReintento(g.intento)}\` without --message to recover it, and then \`--wait\`.`);
|
|
249
362
|
}
|
|
250
363
|
}
|
|
251
364
|
catch (e) {
|
|
252
365
|
return resultadoDeFallo(e);
|
|
253
366
|
}
|
|
254
367
|
if (pruebaId === null) {
|
|
255
|
-
return fallo(SALIDA.entradaInvalida, "sin_prueba", "
|
|
368
|
+
return fallo(SALIDA.entradaInvalida, "sin_prueba", "There's no saved test on this machine for this project: request one with `supcoflow test --authorize-spend --ceiling <usd> --expect-policy <policy>`, or pass `--test-id prb_…`.");
|
|
256
369
|
}
|
|
257
370
|
}
|
|
258
371
|
// Se aparta lo que puede tardar un sondeo forzado (el primero siempre va, aunque quede poco): así el
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
2
|
import { repoPedido } from "../repo-local.js";
|
|
3
3
|
import { claveVigente } from "./clave-vigente.js";
|
|
4
|
+
import { coherenciaDelClon, leerElArchivoDelClon } from "./contexto.js";
|
|
4
5
|
export async function proyectoDelRepo(d, flags) {
|
|
5
6
|
let repo;
|
|
7
|
+
let raiz;
|
|
6
8
|
try {
|
|
7
|
-
repo =
|
|
9
|
+
({ repo, raiz } = await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd }));
|
|
8
10
|
}
|
|
9
11
|
catch (e) {
|
|
10
12
|
return { ok: false, resultado: resultadoDeFallo(e) };
|
|
@@ -12,10 +14,18 @@ export async function proyectoDelRepo(d, flags) {
|
|
|
12
14
|
const leida = await claveVigente(d.guardado, d.api.origin);
|
|
13
15
|
if (!leida.ok)
|
|
14
16
|
return { ok: false, resultado: leida.resultado };
|
|
17
|
+
// El archivo del clon se lee ANTES de actuar: un `proyecto.json` que no se entiende frena sin tocar nada, y uno que anota
|
|
18
|
+
// otro proyecto que el que la clave ve también (ningún pedido parcial actúa sobre un proyecto distinto del que anota el clon).
|
|
19
|
+
const archivo = leerElArchivoDelClon(d.archivos, raiz);
|
|
20
|
+
if (archivo.estado === "ilegible")
|
|
21
|
+
return { ok: false, resultado: coherenciaDelClon(archivo, null, repo) };
|
|
15
22
|
try {
|
|
16
23
|
const proyecto = await d.buscarProyecto(d.api, leida.vigente.clave, repo);
|
|
24
|
+
const incoherente = coherenciaDelClon(archivo, proyecto, repo);
|
|
25
|
+
if (incoherente !== null)
|
|
26
|
+
return { ok: false, resultado: incoherente };
|
|
17
27
|
if (proyecto === null) {
|
|
18
|
-
const message = `
|
|
28
|
+
const message = `The repo ${repo} doesn't have a project yet. Create the project: \`supcoflow project create\`.`;
|
|
19
29
|
return { ok: false, resultado: { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "sin_proyecto", message, repo }, texto: message } };
|
|
20
30
|
}
|
|
21
31
|
return { ok: true, clave: leida.vigente.clave, proyecto };
|