agentic-workflow-manager 6.1.2 → 6.2.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/dist/src/core/diagnostics/provider-checks.js +54 -9
- package/dist/src/core/install-transaction.js +10 -28
- package/dist/src/core/profile.js +15 -1
- package/dist/src/core/renderers/registry.js +64 -0
- package/dist/src/core/update-check.js +5 -1
- package/dist/tests/core/diagnostics/provider-tier.test.js +38 -0
- package/dist/tests/core/profile.test.js +9 -2
- package/dist/tests/core/update-check.test.js +34 -3
- package/package.json +1 -1
|
@@ -67,7 +67,7 @@ function binaryVersionCheck(agent) {
|
|
|
67
67
|
* verificacion que no ocurria. Ahora tambien comprueba contenido, con el marcador que
|
|
68
68
|
* declara el renderer (`rendererIntegrityMarker`): un archivo con la extension correcta
|
|
69
69
|
* y el cuerpo vacio o truncado ya no pasa como sano. */
|
|
70
|
-
function skillsGlobalCheck(dir, owners, integrity, renderer) {
|
|
70
|
+
function skillsGlobalCheck(dir, owners, integrity, renderer, artifacts) {
|
|
71
71
|
if (dir === null)
|
|
72
72
|
return null;
|
|
73
73
|
if (renderer !== 'link') {
|
|
@@ -107,18 +107,41 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
|
|
|
107
107
|
// Comprobar presencia y extension dejaba pasar un archivo correcto por fuera y
|
|
108
108
|
// vacio o truncado por dentro — el agente cargaba nada y doctor decia que si.
|
|
109
109
|
const corrupt = corruptRendered(dir, rendered, renderer);
|
|
110
|
+
if (corrupt.length > 0) {
|
|
111
|
+
return {
|
|
112
|
+
id: 'skills.global', state: 'broken', target: dir,
|
|
113
|
+
owners: owners.length > 1 ? owners : undefined,
|
|
114
|
+
detail: `${corrupt.length} rendered file(s) missing their expected content (${corrupt.slice(0, 3).join(', ')})`,
|
|
115
|
+
// Codigo propio: no es una usurpacion (nadie lo reemplazo por otra cosa) ni
|
|
116
|
+
// un symlink colgante. Es nuestro archivo, con el contenido incompleto — lo
|
|
117
|
+
// arregla re-instalar el bundle, que lo reescribe.
|
|
118
|
+
remediationCode: 'reinstall-rendered-artifacts',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Frescura, DESPUES de integridad: un archivo truncado tambien difiere de su
|
|
122
|
+
// fuente, y "incompleto" es mas util que "viejo". Solo aplica a renderizados — un
|
|
123
|
+
// symlink apunta al registry, asi que `awm update` lo actualiza solo. Los
|
|
124
|
+
// generados NO: quedan con el contenido de la version anterior hasta que alguien
|
|
125
|
+
// corra `awm sync`, y hasta ahora nada lo decia.
|
|
126
|
+
const stale = staleRendered(dir, artifacts);
|
|
127
|
+
const names = stale.map((r) => path_1.default.basename(r.targetPath));
|
|
110
128
|
return {
|
|
111
129
|
id: 'skills.global',
|
|
112
|
-
state:
|
|
130
|
+
state: stale.length > 0 ? 'stale' : 'supported',
|
|
113
131
|
target: dir,
|
|
114
132
|
owners: owners.length > 1 ? owners : undefined,
|
|
115
|
-
detail:
|
|
116
|
-
? `${
|
|
133
|
+
detail: stale.length > 0
|
|
134
|
+
? `${stale.length} rendered file(s) no longer match the installed registry (${names.slice(0, 3).join(', ')})`
|
|
135
|
+
: undefined,
|
|
136
|
+
// El remedio depende del ALCANCE, y se midio cual funciona en cada uno:
|
|
137
|
+
// `awm update` reconcilia los artefactos de maquina, `awm sync` los que el
|
|
138
|
+
// profile del proyecto declara. Ofrecer el equivocado seria mandar al usuario
|
|
139
|
+
// a un comando que corre limpio sin cambiar nada — el defecto que ya tuvo
|
|
140
|
+
// `open-hooks-trust` (D-010), y que la primera version de ESTE chequeo
|
|
141
|
+
// repitio: ofrecia `reinstall-bundle` porque dos mediciones mias estaban mal.
|
|
142
|
+
remediationCode: stale.length > 0
|
|
143
|
+
? (stale.some((r) => r.scope === 'local') ? 'awm-sync' : 'awm-update')
|
|
117
144
|
: undefined,
|
|
118
|
-
// Codigo propio: no es una usurpacion (nadie lo reemplazo por otra cosa) ni un
|
|
119
|
-
// symlink colgante. Es nuestro archivo, con el contenido incompleto — lo
|
|
120
|
-
// arregla re-instalar el bundle, que lo reescribe.
|
|
121
|
-
remediationCode: corrupt.length > 0 ? 'reinstall-rendered-artifacts' : undefined,
|
|
122
145
|
};
|
|
123
146
|
}
|
|
124
147
|
const shared = owners.length > 1;
|
|
@@ -178,6 +201,24 @@ function skillsGlobalCheck(dir, owners, integrity, renderer) {
|
|
|
178
201
|
* Un archivo ilegible cuenta como corrupto: no poder leerlo no es evidencia de que este
|
|
179
202
|
* bien. Es la misma disciplina que el resto de los checks — nunca verde por no mirar.
|
|
180
203
|
*/
|
|
204
|
+
function staleRendered(dir, records) {
|
|
205
|
+
return records
|
|
206
|
+
.filter((r) => r.renderer !== 'link' && path_1.default.dirname(r.targetPath) === dir)
|
|
207
|
+
.filter((r) => {
|
|
208
|
+
try {
|
|
209
|
+
const expected = (0, registry_1.renderArtifact)(r.renderer, r.sourcePath);
|
|
210
|
+
if (expected === null)
|
|
211
|
+
return false;
|
|
212
|
+
return fs_1.default.readFileSync(r.targetPath, 'utf8') !== expected;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Fuente ausente (el registry ya no la trae) o target ilegible: no es
|
|
216
|
+
// desactualizacion, es otra cosa, y la reportan los checks que
|
|
217
|
+
// corresponden. Aca "no puedo comparar" no se convierte en "esta viejo".
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
181
222
|
function corruptRendered(dir, files, renderer) {
|
|
182
223
|
const marker = (0, registry_1.rendererIntegrityMarker)(renderer);
|
|
183
224
|
if (marker === null)
|
|
@@ -411,6 +452,10 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
|
|
|
411
452
|
for (const dir of ownersByDir.keys()) {
|
|
412
453
|
scansByDir.set(dir, scanSkills(dir));
|
|
413
454
|
}
|
|
455
|
+
// El ledger se lee UNA vez por corrida y se comparte: es la unica forma de saber
|
|
456
|
+
// que fuente produjo cada artefacto renderizado, y por lo tanto de preguntar si
|
|
457
|
+
// sigue coincidiendo con ella.
|
|
458
|
+
const artifacts = safeArtifactState();
|
|
414
459
|
return agents.map((agent) => {
|
|
415
460
|
const provider = (0, providers_1.providerFor)(agent);
|
|
416
461
|
const dir = provider.skill.global;
|
|
@@ -419,7 +464,7 @@ function gatherProviderChecks(agents, scanSkills, projectRoot) {
|
|
|
419
464
|
?? { valid: [], repairable: [], dead: [], usurped: [] };
|
|
420
465
|
const checks = [
|
|
421
466
|
binaryVersionCheck(agent),
|
|
422
|
-
skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer),
|
|
467
|
+
skillsGlobalCheck(dir, owners, integrity, provider.skill.renderer, artifacts),
|
|
423
468
|
agentsNativeCheck(agent),
|
|
424
469
|
workflowsGlobalCheck(agent),
|
|
425
470
|
hookTrustCheck(agent),
|
|
@@ -30,9 +30,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
30
30
|
const artifact_state_1 = require("./artifact-state");
|
|
31
31
|
const paths_1 = require("./paths");
|
|
32
32
|
const atomic_file_1 = require("./atomic-file");
|
|
33
|
-
const
|
|
34
|
-
const cursor_mdc_1 = require("./renderers/cursor-mdc");
|
|
35
|
-
const copilot_instructions_1 = require("./renderers/copilot-instructions");
|
|
33
|
+
const registry_1 = require("./renderers/registry");
|
|
36
34
|
const executor_1 = require("./executor");
|
|
37
35
|
/**
|
|
38
36
|
* The single timestamp-sanitization rule for transaction IDs, shared by
|
|
@@ -249,9 +247,6 @@ function stageRenderedFile(content, targetPath) {
|
|
|
249
247
|
* are sourced from that directory's SKILL.md, so every call site needs this
|
|
250
248
|
* same one-line join instead of reading `op.sourcePath` directly.
|
|
251
249
|
*/
|
|
252
|
-
function readSkillMdSource(op) {
|
|
253
|
-
return fs_1.default.readFileSync(path_1.default.join(op.sourcePath, 'SKILL.md'), 'utf8');
|
|
254
|
-
}
|
|
255
250
|
/**
|
|
256
251
|
* The real, filesystem-touching TransactionDeps used by applyInstallPlan by
|
|
257
252
|
* default. Renders `codex-agent-toml`/`cursor-mdc`/`copilot-instructions`
|
|
@@ -270,15 +265,10 @@ function defaultTransactionDeps() {
|
|
|
270
265
|
}
|
|
271
266
|
// Renders without writing anything, purely to surface parse errors
|
|
272
267
|
// before any backup/replace happens.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
(0, cursor_mdc_1.renderCursorMdc)(readSkillMdSource(op));
|
|
278
|
-
}
|
|
279
|
-
else if (op.renderer === 'copilot-instructions') {
|
|
280
|
-
(0, copilot_instructions_1.renderCopilotInstructions)(readSkillMdSource(op));
|
|
281
|
-
}
|
|
268
|
+
// Renderiza y descarta: el objetivo es que un error de parseo salte ANTES
|
|
269
|
+
// de tocar nada. El dispatch sale de la tabla (`renderArtifact`), no de una
|
|
270
|
+
// copia local — esta era una de las dos que habia en este archivo.
|
|
271
|
+
(0, registry_1.renderArtifact)(op.renderer, op.sourcePath);
|
|
282
272
|
},
|
|
283
273
|
backup(op, backupDir) {
|
|
284
274
|
if (createdAt === null)
|
|
@@ -295,19 +285,11 @@ function defaultTransactionDeps() {
|
|
|
295
285
|
return entry.existed ? path_1.default.join(backupDir, entry.backupRelPath) : null;
|
|
296
286
|
},
|
|
297
287
|
stage(op) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const rendered = (0, cursor_mdc_1.renderCursorMdc)(readSkillMdSource(op));
|
|
304
|
-
return stageRenderedFile(rendered, op.targetPath);
|
|
305
|
-
}
|
|
306
|
-
if (op.renderer === 'copilot-instructions') {
|
|
307
|
-
const rendered = (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMdSource(op));
|
|
308
|
-
return stageRenderedFile(rendered, op.targetPath);
|
|
309
|
-
}
|
|
310
|
-
return (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method);
|
|
288
|
+
const rendered = (0, registry_1.renderArtifact)(op.renderer, op.sourcePath);
|
|
289
|
+
// `null` = renderer `link`: no genera contenido, se instala el artefacto tal cual.
|
|
290
|
+
return rendered === null
|
|
291
|
+
? (0, executor_1.stageArtifact)(op.sourcePath, op.targetPath, op.method)
|
|
292
|
+
: stageRenderedFile(rendered, op.targetPath);
|
|
311
293
|
},
|
|
312
294
|
replace(op, staged) {
|
|
313
295
|
(0, executor_1.replaceArtifact)(staged, op.targetPath);
|
package/dist/src/core/profile.js
CHANGED
|
@@ -154,5 +154,19 @@ function ensureSkillsGitignored(root, agents) {
|
|
|
154
154
|
* project extensions and stay out of `.awm/profile.json`.
|
|
155
155
|
*/
|
|
156
156
|
function shouldRecordExtension(bundleScope, effective) {
|
|
157
|
-
|
|
157
|
+
// El criterio es el ALCANCE EFECTIVO, no el del bundle.
|
|
158
|
+
//
|
|
159
|
+
// Antes exigia ademas `bundleScope === 'project'`, asi que un bundle baseline
|
|
160
|
+
// instalado explicitamente con `--scope local` no quedaba registrado en ningun lado:
|
|
161
|
+
// `awm sync` reconcilia lo que declara el profile, y esos artefactos no estaban ahi.
|
|
162
|
+
// Resultado: nadie los refrescaba nunca. `update` cubre lo global y `sync` lo del
|
|
163
|
+
// profile; eso caia en el medio.
|
|
164
|
+
//
|
|
165
|
+
// Y no es un caso raro: `awm add dev --scope local` es exactamente lo que el playbook
|
|
166
|
+
// de aceptacion (AG-03) le pide correr a cualquiera que verifique un proveedor.
|
|
167
|
+
//
|
|
168
|
+
// Si alguien pidio artefactos de proyecto, el profile lo dice — que es lo que hace
|
|
169
|
+
// que un companero que clona el repo y corre `awm sync` obtenga lo mismo.
|
|
170
|
+
void bundleScope;
|
|
171
|
+
return effective === 'local';
|
|
158
172
|
}
|
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.renderedFilename = renderedFilename;
|
|
4
7
|
exports.rendererExtension = rendererExtension;
|
|
5
8
|
exports.rendererIntegrityMarker = rendererIntegrityMarker;
|
|
9
|
+
exports.renderArtifact = renderArtifact;
|
|
10
|
+
// src/core/renderers/registry.ts
|
|
11
|
+
//
|
|
12
|
+
// Modulo HOJA: la UNICA fuente de verdad sobre "que archivo produce cada
|
|
13
|
+
// renderer en disco".
|
|
14
|
+
//
|
|
15
|
+
// Por que existe. Ese hecho estaba escrito a mano en TRES lugares divergentes
|
|
16
|
+
// — `physicalTarget` (install-planner.ts, el escritor), `RENDERED_SKILL_EXTENSIONS`
|
|
17
|
+
// (diagnostics/provider-checks.ts, que ademas omitia `codex-agent-toml`), y un
|
|
18
|
+
// ternario suelto para agentes en diagnostics/context.ts — y estaba AUSENTE en
|
|
19
|
+
// el cuarto, `classifyLinks`, que es justamente el que decide si un artefacto
|
|
20
|
+
// "esta instalado".
|
|
21
|
+
//
|
|
22
|
+
// La consecuencia no fue teorica: el escritor dejaba `using-awm.mdc` en disco y
|
|
23
|
+
// el lector buscaba `using-awm`, asi que para Cursor y Copilot
|
|
24
|
+
// - `awm init` NUNCA era idempotente (reinstalaba el baseline entero en cada
|
|
25
|
+
// corrida, dejando 2 directorios de backup sin podar por corrida), y
|
|
26
|
+
// - `awm doctor` mostraba un rojo permanente cuyo remedio no podia satisfacerlo.
|
|
27
|
+
// Lo mismo para los artefactos `agent` de claude-code, el provider por defecto.
|
|
28
|
+
//
|
|
29
|
+
// Regla: NINGUN modulo vuelve a derivar una extension a mano. Si aparece un
|
|
30
|
+
// renderer nuevo, se agrega aca y todos los consumidores quedan correctos por
|
|
31
|
+
// construccion — que es lo que convierte "agregar un provider" en un cambio
|
|
32
|
+
// localizado en vez de una caceria por seis archivos.
|
|
33
|
+
const fs_1 = __importDefault(require("fs"));
|
|
34
|
+
const path_1 = __importDefault(require("path"));
|
|
35
|
+
const codex_agent_1 = require("./codex-agent");
|
|
36
|
+
const cursor_mdc_1 = require("./cursor-mdc");
|
|
37
|
+
const copilot_instructions_1 = require("./copilot-instructions");
|
|
6
38
|
/** Extension que el renderer estampa sobre el nombre base, o `null` cuando el
|
|
7
39
|
* artefacto se instala con el nombre tal cual (renderer `link`). */
|
|
8
40
|
const RENDERER_EXTENSION = {
|
|
@@ -61,3 +93,35 @@ function rendererExtension(renderer) {
|
|
|
61
93
|
function rendererIntegrityMarker(renderer) {
|
|
62
94
|
return RENDERER_INTEGRITY_MARKER[renderer];
|
|
63
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Contenido que `renderer` produce AHORA a partir de `sourcePath`, o `null` para `link`
|
|
98
|
+
* (no genera nada: instala el artefacto tal cual).
|
|
99
|
+
*
|
|
100
|
+
* Es la tercera cosa que un renderer tiene que declarar, junto con su extension y su
|
|
101
|
+
* marcador — y por eso vive en esta tabla. El dispatch `renderer id → funcion` estaba
|
|
102
|
+
* escrito DOS veces dentro de `install-transaction.ts` (una en `validate`, otra en
|
|
103
|
+
* `stage`), y agregar un consumidor mas —el chequeo de frescura del diagnostico— habria
|
|
104
|
+
* hecho tres. Es exactamente la duplicacion que el comentario de arriba de este modulo
|
|
105
|
+
* describe como el bug que costo una release.
|
|
106
|
+
*
|
|
107
|
+
* Puro: lee la fuente y devuelve texto. No escribe nada, asi que sirve igual para
|
|
108
|
+
* instalar, para validar antes de instalar, y para preguntar "¿lo instalado sigue
|
|
109
|
+
* coincidiendo con la fuente?".
|
|
110
|
+
*/
|
|
111
|
+
function renderArtifact(renderer, sourcePath) {
|
|
112
|
+
switch (renderer) {
|
|
113
|
+
case 'link':
|
|
114
|
+
return null;
|
|
115
|
+
case 'codex-agent-toml':
|
|
116
|
+
return (0, codex_agent_1.renderCodexAgent)(fs_1.default.readFileSync(sourcePath, 'utf8'));
|
|
117
|
+
case 'cursor-mdc':
|
|
118
|
+
return (0, cursor_mdc_1.renderCursorMdc)(readSkillMd(sourcePath));
|
|
119
|
+
case 'copilot-instructions':
|
|
120
|
+
return (0, copilot_instructions_1.renderCopilotInstructions)(readSkillMd(sourcePath));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Las skills se rinden desde el `SKILL.md` de su directorio; los agentes de Codex,
|
|
124
|
+
* desde el archivo directo. La diferencia la sabe cada rama de `renderArtifact`. */
|
|
125
|
+
function readSkillMd(sourceDir) {
|
|
126
|
+
return fs_1.default.readFileSync(path_1.default.join(sourceDir, 'SKILL.md'), 'utf8');
|
|
127
|
+
}
|
|
@@ -83,7 +83,11 @@ function maybeNotifyUpdate(opts) {
|
|
|
83
83
|
const spawnWorker = opts?.spawnWorker ?? spawnRefreshWorker;
|
|
84
84
|
const cache = readUpdateCache();
|
|
85
85
|
if (cache?.latest && isNewer(cache.latest, (0, cli_version_1.cliVersion)())) {
|
|
86
|
-
|
|
86
|
+
// stderr, NO stdout. Este aviso se imprime al final de CUALQUIER comando, asi
|
|
87
|
+
// que en stdout se mezclaba con la salida de `--json` y rompia a cualquiera que
|
|
88
|
+
// parsee: `awm doctor --json | jq` fallaba con un SyntaxError que no menciona la
|
|
89
|
+
// causa. stdout es la interfaz de maquina; los avisos al humano van por stderr.
|
|
90
|
+
process.stderr.write(picocolors_1.default.dim(`\n⬆ awm v${cache.latest} available → npm i -g ${cli_version_1.CLI_PACKAGE_NAME}\n`));
|
|
87
91
|
}
|
|
88
92
|
if (!cache || now - cache.lastCheck > TTL_MS)
|
|
89
93
|
spawnWorker();
|
|
@@ -244,6 +244,44 @@ describe('skillsGlobalCheck — renderer-aware (Task 4.4 / deferred Task 4.3 fin
|
|
|
244
244
|
expect(skillsCheck?.detail).toBeUndefined();
|
|
245
245
|
fs_1.default.rmSync(content, { recursive: true, force: true });
|
|
246
246
|
});
|
|
247
|
+
it('a rendered file that no longer matches its registry source is reported stale', () => {
|
|
248
|
+
// El caso que C4 realmente pedia. Un symlink apunta al registry, asi que
|
|
249
|
+
// `awm update` lo actualiza solo — por eso claude-code/codex/opencode nunca
|
|
250
|
+
// quedan viejos. Un `.mdc` es un archivo GENERADO: se queda con el contenido de
|
|
251
|
+
// la version anterior hasta que alguien corra `awm sync`, y nada lo decia.
|
|
252
|
+
const source = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-stale-src-'));
|
|
253
|
+
fs_1.default.mkdirSync(path_1.default.join(source, 'using-awm'), { recursive: true });
|
|
254
|
+
const skillMd = path_1.default.join(source, 'using-awm', 'SKILL.md');
|
|
255
|
+
fs_1.default.writeFileSync(skillMd, '---\nname: using-awm\ndescription: original\n---\n\nCuerpo v1.\n');
|
|
256
|
+
const rulesDir = path_1.default.join(tmpHome, '.cursor/rules');
|
|
257
|
+
fs_1.default.mkdirSync(rulesDir, { recursive: true });
|
|
258
|
+
const target = path_1.default.join(rulesDir, 'using-awm.mdc');
|
|
259
|
+
const { renderArtifact } = require('../../../src/core/renderers/registry');
|
|
260
|
+
fs_1.default.writeFileSync(target, renderArtifact('cursor-mdc', path_1.default.join(source, 'using-awm')));
|
|
261
|
+
const stateDir = path_1.default.join(tmpHome, '.awm', 'state');
|
|
262
|
+
fs_1.default.mkdirSync(stateDir, { recursive: true });
|
|
263
|
+
fs_1.default.writeFileSync(path_1.default.join(stateDir, 'artifacts.json'), JSON.stringify([{
|
|
264
|
+
name: 'using-awm', type: 'skill', scope: 'global',
|
|
265
|
+
targetPath: target, sourcePath: path_1.default.join(source, 'using-awm'),
|
|
266
|
+
renderer: 'cursor-mdc', owners: ['cursor'],
|
|
267
|
+
}]));
|
|
268
|
+
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
269
|
+
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
270
|
+
// Recien instalado: coincide con su fuente.
|
|
271
|
+
const fresh = gatherProviderChecks(['cursor'], scanSkills)[0]
|
|
272
|
+
.checks.find((c) => c.id === 'skills.global');
|
|
273
|
+
expect(fresh?.state).toBe('supported');
|
|
274
|
+
// `awm update` trae una version nueva de la skill upstream.
|
|
275
|
+
fs_1.default.writeFileSync(skillMd, '---\nname: using-awm\ndescription: original\n---\n\nCuerpo v2, cambiado upstream.\n');
|
|
276
|
+
const stale = gatherProviderChecks(['cursor'], scanSkills)[0]
|
|
277
|
+
.checks.find((c) => c.id === 'skills.global');
|
|
278
|
+
expect(stale?.state).toBe('stale');
|
|
279
|
+
expect(stale?.detail).toContain('using-awm.mdc');
|
|
280
|
+
// El remedio depende del alcance, y se midio: `awm update` reconcilia los
|
|
281
|
+
// artefactos de maquina; `awm sync`, los que declara el profile del proyecto.
|
|
282
|
+
expect(stale?.remediationCode).toBe('awm-update');
|
|
283
|
+
fs_1.default.rmSync(source, { recursive: true, force: true });
|
|
284
|
+
});
|
|
247
285
|
it('non-link renderer with an empty/missing dir reports absent, not a false healthy', () => {
|
|
248
286
|
const scanSkills = jest.fn(() => ({ valid: [], repairable: [], dead: [], usurped: [] }));
|
|
249
287
|
const { gatherProviderChecks } = require('../../../src/core/diagnostics/provider-checks');
|
|
@@ -100,11 +100,18 @@ describe('ensureSkillsGitignored', () => {
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
describe('shouldRecordExtension', () => {
|
|
103
|
-
it('
|
|
103
|
+
it('no registra nada instalado a nivel maquina, sea cual sea el bundle', () => {
|
|
104
104
|
expect((0, profile_1.shouldRecordExtension)('project', 'local')).toBe(true);
|
|
105
105
|
expect((0, profile_1.shouldRecordExtension)('project', 'global')).toBe(false);
|
|
106
106
|
expect((0, profile_1.shouldRecordExtension)('baseline', 'global')).toBe(false);
|
|
107
|
-
expect((0, profile_1.shouldRecordExtension)('baseline', 'local')).toBe(false);
|
|
108
107
|
expect((0, profile_1.shouldRecordExtension)('ambient', 'global')).toBe(false);
|
|
109
108
|
});
|
|
109
|
+
it('records a BASELINE bundle too when it was installed with --scope local', () => {
|
|
110
|
+
// Antes devolvia false, y esos artefactos quedaban fuera del profile: `awm sync`
|
|
111
|
+
// reconcilia lo que el profile declara, asi que nadie los refrescaba nunca —
|
|
112
|
+
// `update` cubre lo global y esto caia en el medio. No es un caso raro:
|
|
113
|
+
// `awm add dev --scope local` es lo que el playbook AG-03 pide correr.
|
|
114
|
+
expect((0, profile_1.shouldRecordExtension)('baseline', 'local')).toBe(true);
|
|
115
|
+
expect((0, profile_1.shouldRecordExtension)('ambient', 'local')).toBe(true);
|
|
116
|
+
});
|
|
110
117
|
});
|
|
@@ -59,12 +59,14 @@ describe('update-check', () => {
|
|
|
59
59
|
it('maybeNotifyUpdate avisa si el cache trae versión más nueva y NO refresca cache fresco', () => {
|
|
60
60
|
const m = require('../../src/core/update-check');
|
|
61
61
|
m.writeUpdateCache({ lastCheck: 1_000_000, latest: '99.0.0' });
|
|
62
|
-
|
|
62
|
+
// stderr, no stdout: el aviso sale al final de cualquier comando y en stdout
|
|
63
|
+
// rompia la salida de `--json`.
|
|
64
|
+
const err = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
63
65
|
const spawnWorker = jest.fn();
|
|
64
66
|
m.maybeNotifyUpdate({ now: 1_000_000 + 1000, spawnWorker });
|
|
65
|
-
expect(
|
|
67
|
+
expect(err.mock.calls.flat().join('\n')).toContain('99.0.0');
|
|
66
68
|
expect(spawnWorker).not.toHaveBeenCalled();
|
|
67
|
-
|
|
69
|
+
err.mockRestore();
|
|
68
70
|
});
|
|
69
71
|
it('cache viejo (>24h) dispara refresh en background', () => {
|
|
70
72
|
const m = require('../../src/core/update-check');
|
|
@@ -104,3 +106,32 @@ describe('update-check', () => {
|
|
|
104
106
|
expect(cache.lastCheck).toBeGreaterThan(0);
|
|
105
107
|
});
|
|
106
108
|
});
|
|
109
|
+
// El aviso de version nueva va por stderr, no por stdout.
|
|
110
|
+
//
|
|
111
|
+
// Se imprime al final de CUALQUIER comando, asi que en stdout se mezclaba con la salida
|
|
112
|
+
// de `--json` y rompia a cualquiera que parsee. Encontrado tropezando con el:
|
|
113
|
+
// `awm doctor --json | node -e 'JSON.parse(...)'` fallaba con un SyntaxError que no
|
|
114
|
+
// menciona la causa. stdout es la interfaz de maquina.
|
|
115
|
+
describe('the update banner never contaminates stdout', () => {
|
|
116
|
+
it('writes to stderr so `--json` output stays parseable', () => {
|
|
117
|
+
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
118
|
+
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
119
|
+
const log = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
120
|
+
try {
|
|
121
|
+
const { maybeNotifyUpdate } = require('../../src/core/update-check');
|
|
122
|
+
maybeNotifyUpdate({ spawnWorker: () => { } });
|
|
123
|
+
const onStdout = stdout.mock.calls.map((c) => String(c[0])).join('')
|
|
124
|
+
+ log.mock.calls.map((c) => String(c[0])).join('');
|
|
125
|
+
expect(onStdout).not.toContain('available');
|
|
126
|
+
// Y si habia aviso, salio por stderr — no se perdio, se movio.
|
|
127
|
+
const emitted = stderr.mock.calls.map((c) => String(c[0])).join('');
|
|
128
|
+
if (emitted.length > 0)
|
|
129
|
+
expect(emitted).toContain('available');
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
stdout.mockRestore();
|
|
133
|
+
stderr.mockRestore();
|
|
134
|
+
log.mockRestore();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|