@principles/pd-cli 1.129.0 → 1.131.0
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/commands/__tests__/intent-flag-wiring.test.d.ts +9 -0
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +1 -0
- package/dist/commands/__tests__/intent-flag-wiring.test.js +166 -0
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -0
- package/dist/commands/intent.d.ts +59 -0
- package/dist/commands/intent.d.ts.map +1 -0
- package/dist/commands/intent.js +350 -0
- package/dist/commands/intent.js.map +1 -0
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +10 -11
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-internalization-queue.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-queue.js +13 -16
- package/dist/commands/runtime-internalization-queue.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js +20 -12
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -3
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/intent-flag-wiring.test.ts +197 -0
- package/src/commands/intent.ts +388 -0
- package/src/commands/runtime-canary.ts +11 -13
- package/src/commands/runtime-internalization-queue.ts +14 -19
- package/src/index.ts +6 -0
- package/src/services/__tests__/runtime-adapter-resolver.test.ts +20 -12
- package/src/services/runtime-adapter-resolver.ts +3 -3
- package/tests/commands/intent.test.ts +321 -0
- package/tests/commands/runtime-canary.test.ts +46 -36
- package/tests/commands/runtime-internalization-queue.test.ts +11 -24
- package/tests/commands/runtime-internalization-run-once.test.ts +14 -6
- package/dist/services/feature-flag-loader.d.ts +0 -6
- package/dist/services/feature-flag-loader.d.ts.map +0 -1
- package/dist/services/feature-flag-loader.js +0 -53
- package/dist/services/feature-flag-loader.js.map +0 -1
- package/src/services/feature-flag-loader.ts +0 -73
- package/tests/services/feature-flag-loader.test.ts +0 -207
|
@@ -24,27 +24,14 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
24
24
|
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
-
vi.mock('../../src/services/feature-flag-loader.js', () => ({
|
|
28
|
-
loadEffectiveFeatureFlags: vi.fn().mockReturnValue({
|
|
29
|
-
flags: {
|
|
30
|
-
prompt: { id: 'prompt', enabled: true, category: 'core' },
|
|
31
|
-
code_tool_hook: { id: 'code_tool_hook', enabled: true, category: 'core' },
|
|
32
|
-
defer_archive: { id: 'defer_archive', enabled: true, category: 'core' },
|
|
33
|
-
},
|
|
34
|
-
source: 'defaults',
|
|
35
|
-
configPath: '/fake/workspace/.pd/feature-flags.yaml',
|
|
36
|
-
warnings: [],
|
|
37
|
-
}),
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
27
|
const { mockLoadPdConfig, mockComputeFlagsFromLoadResult } = vi.hoisted(() => ({
|
|
41
|
-
mockLoadPdConfig: vi.fn().mockReturnValue({
|
|
28
|
+
mockLoadPdConfig: vi.fn().mockReturnValue({ ok: true, effective: {}, source: 'defaults' }),
|
|
42
29
|
mockComputeFlagsFromLoadResult: vi.fn().mockReturnValue({
|
|
43
30
|
flags: {
|
|
44
31
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
|
|
45
32
|
},
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
34
|
+
warnings: [],
|
|
48
35
|
}),
|
|
49
36
|
}));
|
|
50
37
|
|
|
@@ -89,8 +76,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
89
76
|
flags: {
|
|
90
77
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
|
|
91
78
|
},
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
80
|
+
warnings: [],
|
|
94
81
|
});
|
|
95
82
|
});
|
|
96
83
|
|
|
@@ -308,8 +295,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
308
295
|
flags: {
|
|
309
296
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: true, category: 'quiet' },
|
|
310
297
|
},
|
|
311
|
-
|
|
312
|
-
|
|
298
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
299
|
+
warnings: [],
|
|
313
300
|
});
|
|
314
301
|
|
|
315
302
|
await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
|
|
@@ -332,8 +319,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
332
319
|
flags: {
|
|
333
320
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
|
|
334
321
|
},
|
|
335
|
-
|
|
336
|
-
|
|
322
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
323
|
+
warnings: [],
|
|
337
324
|
});
|
|
338
325
|
|
|
339
326
|
await handleRuntimeInternalizationQueue({ workspace: WS, json: true });
|
|
@@ -379,8 +366,8 @@ describe('handleRuntimeInternalizationQueue', () => {
|
|
|
379
366
|
flags: {
|
|
380
367
|
internalization_auto_consumer: { id: 'internalization_auto_consumer', enabled: false, category: 'quiet' },
|
|
381
368
|
},
|
|
382
|
-
|
|
383
|
-
|
|
369
|
+
enabledChannels: ['prompt', 'code_tool_hook', 'defer_archive'],
|
|
370
|
+
warnings: [],
|
|
384
371
|
});
|
|
385
372
|
|
|
386
373
|
await handleRuntimeInternalizationQueue({ workspace: WS, json: false });
|
|
@@ -41,14 +41,22 @@ vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
|
41
41
|
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
-
// PRI-
|
|
45
|
-
// doesn't call the real
|
|
46
|
-
|
|
47
|
-
vi.
|
|
48
|
-
|
|
44
|
+
// PRI-460: Mock pd-config-loader so the shared resolver's L2 dreamer sub-branch
|
|
45
|
+
// doesn't call the real loadPdConfig (which needs fs access).
|
|
46
|
+
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
47
|
+
loadPdConfig: vi.fn().mockReturnValue({
|
|
48
|
+
ok: true,
|
|
49
|
+
effective: {},
|
|
50
|
+
source: 'defaults',
|
|
51
|
+
configPath: '/fake/workspace/.pd/config.yaml',
|
|
52
|
+
warnings: [],
|
|
53
|
+
legacyFilesDetected: [],
|
|
54
|
+
legacyFileNextActions: [],
|
|
55
|
+
}),
|
|
56
|
+
computeFlagsFromLoadResult: vi.fn().mockReturnValue({
|
|
49
57
|
flags: {},
|
|
58
|
+
enabledChannels: [],
|
|
50
59
|
warnings: [],
|
|
51
|
-
configPath: '/fake/workspace/.pd/feature-flags.yaml',
|
|
52
60
|
}),
|
|
53
61
|
}));
|
|
54
62
|
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
|
|
2
|
-
export declare const FEATURE_FLAGS_CONFIG_FILENAME = "feature-flags.yaml";
|
|
3
|
-
export declare const FEATURE_FLAGS_CONFIG_DIR = ".pd";
|
|
4
|
-
export declare function getFeatureFlagsConfigPath(workspaceDir: string): string;
|
|
5
|
-
export declare function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags;
|
|
6
|
-
//# sourceMappingURL=feature-flag-loader.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag-loader.d.ts","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEzE,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAClE,eAAO,MAAM,wBAAwB,QAAQ,CAAC;AAQ9C,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,qBAAqB,CAkDrF"}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, } from '@principles/core/runtime-v2';
|
|
5
|
-
export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
|
|
6
|
-
export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
|
|
7
|
-
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
8
|
-
function isRecord(value) {
|
|
9
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
-
}
|
|
11
|
-
export function getFeatureFlagsConfigPath(workspaceDir) {
|
|
12
|
-
return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
|
|
13
|
-
}
|
|
14
|
-
export function loadEffectiveFeatureFlags(workspaceDir) {
|
|
15
|
-
const configPath = getFeatureFlagsConfigPath(workspaceDir);
|
|
16
|
-
if (!fs.existsSync(configPath)) {
|
|
17
|
-
return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
|
|
18
|
-
}
|
|
19
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
20
|
-
let parsed;
|
|
21
|
-
try {
|
|
22
|
-
parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return {
|
|
26
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
27
|
-
warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
31
|
-
return {
|
|
32
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
33
|
-
warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
const parsedRecord = Object.create(null);
|
|
37
|
-
const warnings = [];
|
|
38
|
-
for (const key of Object.keys(parsed)) {
|
|
39
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
40
|
-
warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
|
|
44
|
-
parsedRecord[key] = parsed[key];
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const result = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
|
|
48
|
-
if (warnings.length > 0) {
|
|
49
|
-
result.warnings = [...warnings, ...result.warnings];
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
//# sourceMappingURL=feature-flag-loader.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag-loader.js","sourceRoot":"","sources":["../../src/services/feature-flag-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import {
|
|
5
|
-
computeEffectiveFlags,
|
|
6
|
-
DEFAULT_FEATURE_FLAGS,
|
|
7
|
-
} from '@principles/core/runtime-v2';
|
|
8
|
-
import type { EffectiveFeatureFlags } from '@principles/core/runtime-v2';
|
|
9
|
-
|
|
10
|
-
export const FEATURE_FLAGS_CONFIG_FILENAME = 'feature-flags.yaml';
|
|
11
|
-
export const FEATURE_FLAGS_CONFIG_DIR = '.pd';
|
|
12
|
-
|
|
13
|
-
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
14
|
-
|
|
15
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getFeatureFlagsConfigPath(workspaceDir: string): string {
|
|
20
|
-
return path.join(workspaceDir, FEATURE_FLAGS_CONFIG_DIR, FEATURE_FLAGS_CONFIG_FILENAME);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function loadEffectiveFeatureFlags(workspaceDir: string): EffectiveFeatureFlags {
|
|
24
|
-
const configPath = getFeatureFlagsConfigPath(workspaceDir);
|
|
25
|
-
|
|
26
|
-
if (!fs.existsSync(configPath)) {
|
|
27
|
-
return computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const raw = fs.readFileSync(configPath, 'utf8');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let parsed: unknown;
|
|
34
|
-
try {
|
|
35
|
-
parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
|
|
36
|
-
} catch {
|
|
37
|
-
return {
|
|
38
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
39
|
-
warnings: ['feature-flags.yaml: YAML parse error, using defaults'],
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
44
|
-
return {
|
|
45
|
-
...computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath),
|
|
46
|
-
warnings: ['feature-flags.yaml: expected a mapping, using defaults'],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const parsedRecord: Record<string, unknown> = Object.create(null);
|
|
51
|
-
const warnings: string[] = [];
|
|
52
|
-
for (const key of Object.keys(parsed)) {
|
|
53
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
54
|
-
warnings.push(`feature-flags.yaml: dangerous key '${key}' rejected`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (Object.hasOwn(parsed, key) && isRecord(parsed)) {
|
|
58
|
-
parsedRecord[key] = parsed[key];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const result = computeEffectiveFlags(
|
|
63
|
-
parsedRecord,
|
|
64
|
-
DEFAULT_FEATURE_FLAGS,
|
|
65
|
-
configPath,
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
if (warnings.length > 0) {
|
|
69
|
-
result.warnings = [...warnings, ...result.warnings];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import {
|
|
6
|
-
loadEffectiveFeatureFlags,
|
|
7
|
-
getFeatureFlagsConfigPath,
|
|
8
|
-
FEATURE_FLAGS_CONFIG_FILENAME,
|
|
9
|
-
FEATURE_FLAGS_CONFIG_DIR,
|
|
10
|
-
} from '../../src/services/feature-flag-loader.js';
|
|
11
|
-
|
|
12
|
-
function createTempWorkspace(): string {
|
|
13
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'pd-ffl-test-'));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function writeConfig(tmpDir: string, content: string): void {
|
|
17
|
-
const configDir = path.join(tmpDir, FEATURE_FLAGS_CONFIG_DIR);
|
|
18
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
19
|
-
fs.writeFileSync(path.join(configDir, FEATURE_FLAGS_CONFIG_FILENAME), content, 'utf8');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function cleanup(tmpDir: string): void {
|
|
23
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('feature-flag-loader', () => {
|
|
27
|
-
describe('getFeatureFlagsConfigPath', () => {
|
|
28
|
-
it('should return path under .pd/feature-flags.yaml', () => {
|
|
29
|
-
const result = getFeatureFlagsConfigPath('/workspace/project');
|
|
30
|
-
expect(result).toBe(path.join('/workspace/project', '.pd', 'feature-flags.yaml'));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should use FEATURE_FLAGS_CONFIG_DIR and FEATURE_FLAGS_CONFIG_FILENAME constants', () => {
|
|
34
|
-
const result = getFeatureFlagsConfigPath('/root');
|
|
35
|
-
expect(result).toContain(FEATURE_FLAGS_CONFIG_DIR);
|
|
36
|
-
expect(result).toContain(FEATURE_FLAGS_CONFIG_FILENAME);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('loadEffectiveFeatureFlags', () => {
|
|
41
|
-
it('should return defaults when config file does not exist', () => {
|
|
42
|
-
const tmpDir = createTempWorkspace();
|
|
43
|
-
try {
|
|
44
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
45
|
-
expect(result.source).toBe('defaults');
|
|
46
|
-
expect(result.warnings).toEqual([]);
|
|
47
|
-
} finally {
|
|
48
|
-
cleanup(tmpDir);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should return defaults with YAML parse error warning for malformed YAML', () => {
|
|
53
|
-
const tmpDir = createTempWorkspace();
|
|
54
|
-
writeConfig(tmpDir, 'gfi: [unterminated');
|
|
55
|
-
try {
|
|
56
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
57
|
-
expect(result.warnings.some(w => w.includes('YAML parse error'))).toBe(true);
|
|
58
|
-
} finally {
|
|
59
|
-
cleanup(tmpDir);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should return defaults with mapping warning when YAML contains an array', () => {
|
|
64
|
-
const tmpDir = createTempWorkspace();
|
|
65
|
-
writeConfig(tmpDir, '- item1\n- item2\n');
|
|
66
|
-
try {
|
|
67
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
68
|
-
expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
|
|
69
|
-
} finally {
|
|
70
|
-
cleanup(tmpDir);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should return defaults with mapping warning when YAML contains null', () => {
|
|
75
|
-
const tmpDir = createTempWorkspace();
|
|
76
|
-
writeConfig(tmpDir, '---\nnull\n');
|
|
77
|
-
try {
|
|
78
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
79
|
-
expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
|
|
80
|
-
} finally {
|
|
81
|
-
cleanup(tmpDir);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should return defaults with mapping warning when YAML is undefined after parse', () => {
|
|
86
|
-
const tmpDir = createTempWorkspace();
|
|
87
|
-
writeConfig(tmpDir, '---\n');
|
|
88
|
-
try {
|
|
89
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
90
|
-
expect(result.source).toBe('defaults');
|
|
91
|
-
expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
|
|
92
|
-
} finally {
|
|
93
|
-
cleanup(tmpDir);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should return defaults with mapping warning when YAML contains a scalar string', () => {
|
|
98
|
-
const tmpDir = createTempWorkspace();
|
|
99
|
-
writeConfig(tmpDir, 'just a string\n');
|
|
100
|
-
try {
|
|
101
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
102
|
-
expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
|
|
103
|
-
} finally {
|
|
104
|
-
cleanup(tmpDir);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return defaults with mapping warning when YAML contains a number', () => {
|
|
109
|
-
const tmpDir = createTempWorkspace();
|
|
110
|
-
writeConfig(tmpDir, '42\n');
|
|
111
|
-
try {
|
|
112
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
113
|
-
expect(result.warnings.some(w => w.includes('expected a mapping'))).toBe(true);
|
|
114
|
-
} finally {
|
|
115
|
-
cleanup(tmpDir);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should reject __proto__ dangerous key', () => {
|
|
120
|
-
const tmpDir = createTempWorkspace();
|
|
121
|
-
writeConfig(tmpDir, '"__proto__":\n enabled: true\n');
|
|
122
|
-
try {
|
|
123
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
124
|
-
expect(result.warnings.some(w => w.includes("dangerous key '__proto__' rejected"))).toBe(true);
|
|
125
|
-
expect(Object.hasOwn(result.flags, '__proto__')).toBe(false);
|
|
126
|
-
} finally {
|
|
127
|
-
cleanup(tmpDir);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should reject constructor dangerous key', () => {
|
|
132
|
-
const tmpDir = createTempWorkspace();
|
|
133
|
-
writeConfig(tmpDir, 'constructor:\n enabled: true\n');
|
|
134
|
-
try {
|
|
135
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
136
|
-
expect(result.warnings.some(w => w.includes("dangerous key 'constructor' rejected"))).toBe(true);
|
|
137
|
-
expect(Object.hasOwn(result.flags, 'constructor')).toBe(false);
|
|
138
|
-
} finally {
|
|
139
|
-
cleanup(tmpDir);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should reject prototype dangerous key', () => {
|
|
144
|
-
const tmpDir = createTempWorkspace();
|
|
145
|
-
writeConfig(tmpDir, 'prototype:\n enabled: true\n');
|
|
146
|
-
try {
|
|
147
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
148
|
-
expect(result.warnings.some(w => w.includes("dangerous key 'prototype' rejected"))).toBe(true);
|
|
149
|
-
expect(Object.hasOwn(result.flags, 'prototype')).toBe(false);
|
|
150
|
-
} finally {
|
|
151
|
-
cleanup(tmpDir);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should allow valid keys while rejecting dangerous ones', () => {
|
|
156
|
-
const tmpDir = createTempWorkspace();
|
|
157
|
-
writeConfig(tmpDir, 'gfi:\n enabled: true\nprototype:\n enabled: true\n');
|
|
158
|
-
try {
|
|
159
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
160
|
-
expect(result.warnings.some(w => w.includes("dangerous key 'prototype' rejected"))).toBe(true);
|
|
161
|
-
expect(result.flags.gfi).toBeDefined();
|
|
162
|
-
if (result.flags.gfi) {
|
|
163
|
-
expect(result.flags.gfi.enabled).toBe(true);
|
|
164
|
-
}
|
|
165
|
-
} finally {
|
|
166
|
-
cleanup(tmpDir);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should parse valid YAML config and set source to workspace_file', () => {
|
|
171
|
-
const tmpDir = createTempWorkspace();
|
|
172
|
-
writeConfig(tmpDir, 'gfi:\n enabled: true\n');
|
|
173
|
-
try {
|
|
174
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
175
|
-
expect(result.source).toBe('workspace_file');
|
|
176
|
-
expect(result.flags.gfi).toBeDefined();
|
|
177
|
-
if (result.flags.gfi) {
|
|
178
|
-
expect(result.flags.gfi.enabled).toBe(true);
|
|
179
|
-
}
|
|
180
|
-
} finally {
|
|
181
|
-
cleanup(tmpDir);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should handle empty YAML mapping with defaults source', () => {
|
|
186
|
-
const tmpDir = createTempWorkspace();
|
|
187
|
-
writeConfig(tmpDir, '{}\n');
|
|
188
|
-
try {
|
|
189
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
190
|
-
expect(result.source).toBe('defaults');
|
|
191
|
-
expect(result.warnings).toEqual([]);
|
|
192
|
-
} finally {
|
|
193
|
-
cleanup(tmpDir);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should include configPath in result', () => {
|
|
198
|
-
const tmpDir = createTempWorkspace();
|
|
199
|
-
try {
|
|
200
|
-
const result = loadEffectiveFeatureFlags(tmpDir);
|
|
201
|
-
expect(result.configPath).toContain(FEATURE_FLAGS_CONFIG_FILENAME);
|
|
202
|
-
} finally {
|
|
203
|
-
cleanup(tmpDir);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|