Skip to content

Commit dbae2c6

Browse files
committed
fix(core): deep merge workflow metadata
1 parent c1a9ff6 commit dbae2c6

3 files changed

Lines changed: 87 additions & 4 deletions

File tree

.aiox-core/core/orchestration/context-manager.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,31 @@
1616

1717
const fs = require('fs-extra');
1818
const path = require('path');
19+
const { isPlainObject } = require('../config/merge-utils');
20+
21+
function mergeMetadata(target, source) {
22+
const base = isPlainObject(target) ? target : {};
23+
24+
if (!isPlainObject(source)) {
25+
return { ...base };
26+
}
27+
28+
const result = { ...base };
29+
30+
for (const [key, value] of Object.entries(source)) {
31+
result[key] = isPlainObject(value) && isPlainObject(result[key])
32+
? mergeMetadata(result[key], value)
33+
: value;
34+
}
35+
36+
return result;
37+
}
38+
39+
function describeMetadataInput(value) {
40+
if (value === null) return 'null';
41+
if (Array.isArray(value)) return 'array';
42+
return typeof value;
43+
}
1944

2045
/**
2146
* Manages workflow execution context and state persistence
@@ -224,8 +249,14 @@ class ContextManager {
224249
* @param {Object} metadata - Metadata to merge
225250
*/
226251
async updateMetadata(metadata) {
252+
if (!isPlainObject(metadata)) {
253+
throw new TypeError(
254+
`updateMetadata expects a plain object, received: ${describeMetadataInput(metadata)}`,
255+
);
256+
}
257+
227258
const state = await this.loadState();
228-
state.metadata = { ...state.metadata, ...metadata };
259+
state.metadata = mergeMetadata(state.metadata, metadata);
229260
this._stateCache = state;
230261
await this._saveState();
231262
}

.aiox-core/install-manifest.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# - File types for categorization
99
#
1010
version: 5.1.15
11-
generated_at: "2026-05-08T04:49:28.006Z"
11+
generated_at: "2026-05-08T05:14:31.637Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1111
1414
files:
@@ -845,9 +845,9 @@ files:
845845
type: core
846846
size: 12722
847847
- path: core/orchestration/context-manager.js
848-
hash: sha256:7bf273723a2c08cb84e670b9d4c55aacec51819b1fbd5f3b0c46c4ccfa2ec192
848+
hash: sha256:b6ed0e4eb069558998f95ab0c68fa040b1b8c2aab74ab44e26ab682fe9e247b4
849849
type: core
850-
size: 16842
850+
size: 17620
851851
- path: core/orchestration/dashboard-integration.js
852852
hash: sha256:8f2dd7d3885a9eaf58957505d312923e149f2771ae3ca0cda879631683f826c9
853853
type: core

tests/core/orchestration/context-manager.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,58 @@ describe('ContextManager', () => {
567567
expect(manager._stateCache.metadata.projectRoot).toBe('/outro/caminho');
568568
});
569569

570+
test('preserva metadados aninhados ao aplicar merge profundo', async () => {
571+
await manager.updateMetadata({
572+
observability: {
573+
attempts: {
574+
total: 1,
575+
lastPhase: 'planning',
576+
},
577+
tags: ['initial'],
578+
nullable: 'keep',
579+
},
580+
});
581+
582+
await manager.updateMetadata({
583+
observability: {
584+
attempts: {
585+
failed: 1,
586+
},
587+
lastError: {
588+
code: 'EACCES',
589+
},
590+
tags: ['latest'],
591+
nullable: null,
592+
},
593+
});
594+
595+
expect(manager._stateCache.metadata.observability).toEqual({
596+
attempts: {
597+
total: 1,
598+
lastPhase: 'planning',
599+
failed: 1,
600+
},
601+
lastError: {
602+
code: 'EACCES',
603+
},
604+
tags: ['latest'],
605+
nullable: null,
606+
});
607+
});
608+
609+
test.each([
610+
['null', null],
611+
['undefined', undefined],
612+
['array', []],
613+
['string', 'metadata'],
614+
])('rejeita metadata inválido: %s', async (_label, invalidMetadata) => {
615+
await expect(manager.updateMetadata(invalidMetadata)).rejects.toThrow(
616+
'updateMetadata expects a plain object',
617+
);
618+
619+
expect(fs.writeJson).not.toHaveBeenCalled();
620+
});
621+
570622
test('persiste estado no disco', async () => {
571623
await manager.updateMetadata({ foo: 'bar' });
572624

0 commit comments

Comments
 (0)