@principles/pd-cli 1.106.0 → 1.108.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__/mvp-smoke.test.d.ts +15 -0
- package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +1 -0
- package/dist/commands/__tests__/mvp-smoke.test.js +245 -0
- package/dist/commands/__tests__/mvp-smoke.test.js.map +1 -0
- package/dist/commands/command-helpers.d.ts +19 -0
- package/dist/commands/command-helpers.d.ts.map +1 -0
- package/dist/commands/command-helpers.js +22 -0
- package/dist/commands/command-helpers.js.map +1 -0
- package/dist/commands/mvp-smoke.d.ts +30 -0
- package/dist/commands/mvp-smoke.d.ts.map +1 -0
- package/dist/commands/mvp-smoke.js +139 -0
- package/dist/commands/mvp-smoke.js.map +1 -0
- package/dist/commands/task.d.ts +11 -0
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +63 -2
- package/dist/commands/task.js.map +1 -1
- package/dist/index.js +5 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/mvp-smoke.test.ts +284 -0
- package/src/commands/command-helpers.ts +24 -0
- package/src/commands/mvp-smoke.ts +160 -0
- package/src/commands/task.ts +68 -3
- package/src/index.ts +7 -10
- package/tests/commands/pri-393-runtime-config-unification.test.ts +153 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pd mvp smoke — MVP mainline readiness check (PRI-397 / C5).
|
|
3
|
+
*
|
|
4
|
+
* Assembles a MainlineSnapshot from the shared reader, judges it via the
|
|
5
|
+
* pure assertMainlineContract, and outputs a single JSON verdict.
|
|
6
|
+
*
|
|
7
|
+
* Rules (EP-04, EP-02):
|
|
8
|
+
* - Read-only by default. NEVER mutates workspace state.
|
|
9
|
+
* - Uses the shared mainline-snapshot-assembler (no new chain logic).
|
|
10
|
+
* - --json mode: stdout = exactly one parseable JSON object — even on failure.
|
|
11
|
+
* Every degraded/refused path emits a structured `{ok, reason, nextAction}`
|
|
12
|
+
* JSON so CI/script consumers can branch on outcome (EP-04 Rule 6).
|
|
13
|
+
* - Human mode: formatted output to stdout.
|
|
14
|
+
* - Exit code: 0 on overall === 'ok', 1 on overall === 'violation' or failure.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import {
|
|
19
|
+
assembleMainlineSnapshot,
|
|
20
|
+
assertMainlineContract,
|
|
21
|
+
} from '../services/mainline-snapshot-assembler.js';
|
|
22
|
+
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
23
|
+
import { withWorkspaceAndJson } from './command-helpers.js';
|
|
24
|
+
|
|
25
|
+
export interface MvpSmokeOptions {
|
|
26
|
+
workspace?: string;
|
|
27
|
+
json?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Classify a thrown error so the failure JSON can name a real next action.
|
|
32
|
+
* EP-03: never silently swallow; emit a structured reason + actionable next step.
|
|
33
|
+
*/
|
|
34
|
+
function classifySmokeError(err: unknown): { reason: string; nextAction: string } {
|
|
35
|
+
if (err instanceof Error) {
|
|
36
|
+
const msg = err.message;
|
|
37
|
+
const lower = msg.toLowerCase();
|
|
38
|
+
// No .pd/state.db on disk (fresh / reset workspace) is the post-PRI-398
|
|
39
|
+
// expected first failure — name the recovery path explicitly.
|
|
40
|
+
if (
|
|
41
|
+
msg.includes('SQLITE_CANTOPEN') ||
|
|
42
|
+
lower.includes('no such file') ||
|
|
43
|
+
lower.includes('no such table') ||
|
|
44
|
+
lower.includes('cannot open database') ||
|
|
45
|
+
lower.includes('directory does not exist')
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
reason: `Workspace database is missing or unreadable: ${msg}`,
|
|
49
|
+
nextAction: 'Run "pd runtime internalization integrity-repair --confirm" or restore the workspace; this command requires a bootstrapped .pd/state.db.',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (lower.includes('workspace') && lower.includes('not configured')) {
|
|
53
|
+
return {
|
|
54
|
+
reason: msg,
|
|
55
|
+
nextAction: 'Set --workspace <path>, PD_WORKSPACE_DIR, or add workspace.default to .pd/config.yaml.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
reason: msg,
|
|
60
|
+
nextAction: 'Inspect the workspace state and retry. Run "pd config doctor --json" to validate the workspace.',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
reason: `Unknown failure: ${String(err)}`,
|
|
65
|
+
nextAction: 'Inspect the workspace state and retry. Run "pd config doctor --json" to validate the workspace.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function handleMvpSmoke(opts: MvpSmokeOptions): Promise<void> {
|
|
70
|
+
// Pass through resolveWorkspaceDir to honor the consistency warning for
|
|
71
|
+
// --workspace flags that disagree with the config default. The resolver
|
|
72
|
+
// returns an absolute, normalized path either way.
|
|
73
|
+
const workspaceDir = resolveWorkspaceDir(opts.workspace);
|
|
74
|
+
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await assembleMainlineSnapshot({ workspaceDir });
|
|
78
|
+
} catch (err: unknown) {
|
|
79
|
+
const { reason, nextAction } = classifySmokeError(err);
|
|
80
|
+
if (opts.json) {
|
|
81
|
+
// EP-04 Rule 1 + 6: stdout = exactly one parseable JSON object carrying
|
|
82
|
+
// a structured reason and nextAction, even on the failure path.
|
|
83
|
+
console.log(JSON.stringify({
|
|
84
|
+
ok: false,
|
|
85
|
+
reason,
|
|
86
|
+
nextAction,
|
|
87
|
+
workspace: workspaceDir,
|
|
88
|
+
}, null, 2));
|
|
89
|
+
} else {
|
|
90
|
+
console.error(`MVP smoke failed: ${reason}`);
|
|
91
|
+
console.error(`nextAction: ${nextAction}`);
|
|
92
|
+
}
|
|
93
|
+
process.exit(1);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const verdict = assertMainlineContract(result.snapshot);
|
|
98
|
+
|
|
99
|
+
if (opts.json) {
|
|
100
|
+
console.log(JSON.stringify({
|
|
101
|
+
ok: verdict.overall === 'ok',
|
|
102
|
+
verdict,
|
|
103
|
+
warnings: result.warnings,
|
|
104
|
+
resolvedPainId: result.resolvedPainId,
|
|
105
|
+
}, null, 2));
|
|
106
|
+
} else {
|
|
107
|
+
console.log(`\nMVP Smoke — ${workspaceDir}\n`);
|
|
108
|
+
console.log(` Overall: ${verdict.overall}`);
|
|
109
|
+
console.log(` Pain ID: ${verdict.painId ?? '(none)'}`);
|
|
110
|
+
console.log(` Generated At: ${verdict.generatedAt}\n`);
|
|
111
|
+
|
|
112
|
+
console.log(' Stages:');
|
|
113
|
+
for (const s of verdict.stages) {
|
|
114
|
+
const icon = s.status === 'ok' ? ' OK' : s.status === 'violation' ? ' VIOLATION' : ' SKIP';
|
|
115
|
+
console.log(` [${icon}] ${s.stage}`);
|
|
116
|
+
console.log(` ${s.reason}`);
|
|
117
|
+
if (s.nextAction) {
|
|
118
|
+
console.log(` nextAction: ${s.nextAction}`);
|
|
119
|
+
}
|
|
120
|
+
console.log('');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (result.warnings.length > 0) {
|
|
124
|
+
console.log(' Warnings:');
|
|
125
|
+
for (const w of result.warnings) {
|
|
126
|
+
console.log(` - ${w}`);
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (verdict.overall === 'violation') {
|
|
133
|
+
process.exit(1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register the `pd mvp` parent command and its `smoke` subcommand.
|
|
140
|
+
*
|
|
141
|
+
* Single source of truth for both production (`index.ts`) and parser tests
|
|
142
|
+
* (`mvp-smoke.test.ts`). If a test passes against this function, the same
|
|
143
|
+
* registration runs in production — flag typos surface at parseAsync time.
|
|
144
|
+
*/
|
|
145
|
+
export function registerMvpCommands(program: Command): Command {
|
|
146
|
+
const mvpCmd = program
|
|
147
|
+
.command('mvp')
|
|
148
|
+
.description('MVP readiness commands');
|
|
149
|
+
|
|
150
|
+
withWorkspaceAndJson(
|
|
151
|
+
mvpCmd
|
|
152
|
+
.command('smoke')
|
|
153
|
+
.description('Check MVP mainline readiness: assemble snapshot → assert contract → structured verdict')
|
|
154
|
+
.action(async (opts: { workspace?: string; json?: boolean }) => {
|
|
155
|
+
await handleMvpSmoke({ workspace: opts.workspace, json: opts.json });
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return mvpCmd;
|
|
160
|
+
}
|
package/src/commands/task.ts
CHANGED
|
@@ -6,29 +6,55 @@
|
|
|
6
6
|
* pd task show <taskId>
|
|
7
7
|
*/
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import type { Command } from 'commander';
|
|
9
10
|
import { RuntimeStateManager, MalformedRunError, type RunRecord } from '@principles/core';
|
|
10
11
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
12
|
+
import { withWorkspaceAndJson } from './command-helpers.js';
|
|
11
13
|
|
|
12
14
|
interface TaskListOptions {
|
|
13
15
|
status?: string;
|
|
14
16
|
kind?: string;
|
|
15
17
|
limit?: number;
|
|
18
|
+
workspace?: string;
|
|
19
|
+
json?: boolean;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export async function handleTaskList(opts: TaskListOptions): Promise<void> {
|
|
19
|
-
|
|
23
|
+
// Pass through resolveWorkspaceDir to honor its consistency warning when
|
|
24
|
+
// --workspace disagrees with workspace.default in .pd/config.yaml.
|
|
25
|
+
const workspaceDir = resolveWorkspaceDir(opts.workspace);
|
|
20
26
|
const stateManager = new RuntimeStateManager({ workspaceDir });
|
|
21
|
-
await stateManager.initialize();
|
|
22
27
|
|
|
23
28
|
try {
|
|
29
|
+
await stateManager.initialize();
|
|
30
|
+
|
|
24
31
|
const filter: Record<string, string | number> = {};
|
|
25
32
|
if (opts.status) filter.status = opts.status;
|
|
26
33
|
if (opts.kind) filter.taskKind = opts.kind;
|
|
27
34
|
if (opts.limit) filter.limit = opts.limit;
|
|
28
35
|
|
|
29
|
-
|
|
30
36
|
const tasks = await stateManager.listTasks(Object.keys(filter).length > 0 ? filter : undefined);
|
|
31
37
|
|
|
38
|
+
if (opts.json) {
|
|
39
|
+
console.log(JSON.stringify({
|
|
40
|
+
ok: true,
|
|
41
|
+
count: tasks.length,
|
|
42
|
+
workspace: workspaceDir,
|
|
43
|
+
tasks: tasks.map((t) => ({
|
|
44
|
+
taskId: t.taskId,
|
|
45
|
+
taskKind: t.taskKind,
|
|
46
|
+
status: t.status,
|
|
47
|
+
attemptCount: t.attemptCount,
|
|
48
|
+
maxAttempts: t.maxAttempts,
|
|
49
|
+
leaseOwner: t.leaseOwner ?? null,
|
|
50
|
+
leaseExpiresAt: t.leaseExpiresAt ?? null,
|
|
51
|
+
createdAt: t.createdAt,
|
|
52
|
+
updatedAt: t.updatedAt,
|
|
53
|
+
})),
|
|
54
|
+
}, null, 2));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
if (tasks.length === 0) {
|
|
33
59
|
console.log('No tasks found.');
|
|
34
60
|
return;
|
|
@@ -57,6 +83,23 @@ export async function handleTaskList(opts: TaskListOptions): Promise<void> {
|
|
|
57
83
|
);
|
|
58
84
|
}
|
|
59
85
|
console.log('');
|
|
86
|
+
} catch (err: unknown) {
|
|
87
|
+
// EP-04: failure paths emit a single parseable JSON object with
|
|
88
|
+
// structured reason + nextAction (mirror handleTaskShow).
|
|
89
|
+
if (opts.json) {
|
|
90
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
ok: false,
|
|
93
|
+
count: 0,
|
|
94
|
+
workspace: workspaceDir,
|
|
95
|
+
reason,
|
|
96
|
+
nextAction: 'Check workspace path and database accessibility. The workspace may need bootstrap (run "pd runtime internalization integrity-repair --confirm") or the workspace dir may be wrong.',
|
|
97
|
+
}, null, 2));
|
|
98
|
+
} else {
|
|
99
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
return;
|
|
60
103
|
} finally {
|
|
61
104
|
await stateManager.close();
|
|
62
105
|
}
|
|
@@ -204,3 +247,25 @@ export async function handleTaskShow(opts: TaskShowOptions): Promise<void> {
|
|
|
204
247
|
await stateManager.close();
|
|
205
248
|
}
|
|
206
249
|
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Register the `pd task list` subcommand.
|
|
253
|
+
*
|
|
254
|
+
* Single source of truth for both production (`index.ts`) and parser tests
|
|
255
|
+
* (mvp-smoke.test.ts). Reuses the `withWorkspaceAndJson` helper so adding a
|
|
256
|
+
* new --workspace/--json pair elsewhere is one line, not five.
|
|
257
|
+
*/
|
|
258
|
+
export function registerTaskListCommand(taskCmd: Command): Command {
|
|
259
|
+
const listCmd = taskCmd
|
|
260
|
+
.command('list')
|
|
261
|
+
.description('List runtime tasks')
|
|
262
|
+
.option('-s, --status <status>', 'Filter by status (pending, leased, retry_wait, succeeded, failed)')
|
|
263
|
+
.option('-k, --kind <kind>', 'Filter by task kind')
|
|
264
|
+
.option('-l, --limit <number>', 'Limit number of results', parseInt, 50)
|
|
265
|
+
.action(async (opts: { status?: string; kind?: string; limit?: number; workspace?: string; json?: boolean }) => {
|
|
266
|
+
await handleTaskList({ status: opts.status, kind: opts.kind, limit: opts.limit, workspace: opts.workspace, json: opts.json });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
withWorkspaceAndJson(listCmd);
|
|
270
|
+
return listCmd;
|
|
271
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { handleEvolutionTasksList } from './commands/evolution-tasks-list.js';
|
|
|
16
16
|
import { handleEvolutionTasksShow } from './commands/evolution-tasks-show.js';
|
|
17
17
|
import { handleHealth } from './commands/health.js';
|
|
18
18
|
import { handleCentralSync } from './commands/central-sync.js';
|
|
19
|
-
import {
|
|
19
|
+
import { handleTaskShow, registerTaskListCommand } from './commands/task.js';
|
|
20
20
|
import { handleRunList, handleRunShow } from './commands/run.js';
|
|
21
21
|
import { handleTrajectoryLocate } from './commands/trajectory.js';
|
|
22
22
|
import { handleHistoryQuery } from './commands/history.js';
|
|
@@ -50,6 +50,7 @@ import { handleProvenChannelBaseline } from './commands/proven-channel-baseline.
|
|
|
50
50
|
import { handleDemoStoryA } from './commands/demo-story-a.js';
|
|
51
51
|
import { handleRuntimeFeaturesStatus } from './commands/runtime-features.js';
|
|
52
52
|
import { handleConfigDoctor } from './commands/config-doctor.js';
|
|
53
|
+
import { registerMvpCommands } from './commands/mvp-smoke.js';
|
|
53
54
|
|
|
54
55
|
import { createRequire } from 'module';
|
|
55
56
|
const require = createRequire(import.meta.url);
|
|
@@ -191,15 +192,7 @@ const rtTaskCmd = program
|
|
|
191
192
|
.command('task')
|
|
192
193
|
.description('Runtime v2 task inspection');
|
|
193
194
|
|
|
194
|
-
rtTaskCmd
|
|
195
|
-
.command('list')
|
|
196
|
-
.description('List runtime tasks')
|
|
197
|
-
.option('-s, --status <status>', 'Filter by status (pending, leased, retry_wait, succeeded, failed)')
|
|
198
|
-
.option('-k, --kind <kind>', 'Filter by task kind')
|
|
199
|
-
.option('-l, --limit <number>', 'Limit number of results', parseInt, 50)
|
|
200
|
-
.action(async (opts) => {
|
|
201
|
-
await handleTaskList(opts);
|
|
202
|
-
});
|
|
195
|
+
registerTaskListCommand(rtTaskCmd);
|
|
203
196
|
|
|
204
197
|
rtTaskCmd
|
|
205
198
|
.command('show <taskId>')
|
|
@@ -885,6 +878,10 @@ const _legacyCleanupCmd = legacyCmd
|
|
|
885
878
|
await handleLegacyCleanup(opts.workspace, apply);
|
|
886
879
|
});
|
|
887
880
|
|
|
881
|
+
// ─── MVP Smoke (PRI-397) ────────────────────────────────────────────────────
|
|
882
|
+
|
|
883
|
+
registerMvpCommands(program);
|
|
884
|
+
|
|
888
885
|
const consoleCmd = program
|
|
889
886
|
.command('console')
|
|
890
887
|
.description('Start the pd-console web UI for principle review (default: legacy launcher)')
|
|
@@ -281,4 +281,157 @@ funnels:
|
|
|
281
281
|
expect(typeof parsed).toBe('object');
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
|
+
|
|
285
|
+
// ── Boundary Condition Tests ────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
describe('resolveRuntimeWithOverrides', () => {
|
|
288
|
+
it('CLI overrides take precedence over config values', async () => {
|
|
289
|
+
writeConfigYaml(tmpDir, makeValidConfigYaml({ provider: 'lmstudio', model: 'local-model' }));
|
|
290
|
+
|
|
291
|
+
const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
292
|
+
const resolved = resolveRuntimeWithOverrides(tmpDir, {
|
|
293
|
+
provider: 'override-provider',
|
|
294
|
+
model: 'override-model',
|
|
295
|
+
}, () => 'test-key');
|
|
296
|
+
|
|
297
|
+
expect(resolved.mergedConfig).not.toBeNull();
|
|
298
|
+
if (resolved.mergedConfig) {
|
|
299
|
+
expect(resolved.mergedConfig.provider).toBe('override-provider');
|
|
300
|
+
expect(resolved.mergedConfig.model).toBe('override-model');
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('returns mergedConfig=null when base config is malformed', async () => {
|
|
305
|
+
const pdDir = path.join(tmpDir, '.pd');
|
|
306
|
+
fs.mkdirSync(pdDir, { recursive: true });
|
|
307
|
+
fs.writeFileSync(path.join(pdDir, 'config.yaml'), 'version: [invalid', 'utf8');
|
|
308
|
+
|
|
309
|
+
const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
310
|
+
const resolved = resolveRuntimeWithOverrides(tmpDir, {
|
|
311
|
+
provider: 'override',
|
|
312
|
+
}, () => 'test-key');
|
|
313
|
+
|
|
314
|
+
expect(resolved.mergedConfig).toBeNull();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('preserves config values when overrides are undefined', async () => {
|
|
318
|
+
writeConfigYaml(tmpDir, makeValidConfigYaml({ provider: 'lmstudio', model: 'local-model' }));
|
|
319
|
+
|
|
320
|
+
const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
321
|
+
const resolved = resolveRuntimeWithOverrides(tmpDir, {
|
|
322
|
+
provider: undefined,
|
|
323
|
+
model: undefined,
|
|
324
|
+
}, () => 'test-key');
|
|
325
|
+
|
|
326
|
+
expect(resolved.mergedConfig).not.toBeNull();
|
|
327
|
+
if (resolved.mergedConfig) {
|
|
328
|
+
expect(resolved.mergedConfig.provider).toBe('lmstudio');
|
|
329
|
+
expect(resolved.mergedConfig.model).toBe('local-model');
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('merges numeric overrides correctly', async () => {
|
|
334
|
+
writeConfigYaml(tmpDir, makeValidConfigYaml());
|
|
335
|
+
|
|
336
|
+
const { resolveRuntimeWithOverrides } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
337
|
+
const resolved = resolveRuntimeWithOverrides(tmpDir, {
|
|
338
|
+
maxRetries: 5,
|
|
339
|
+
timeoutMs: 60000,
|
|
340
|
+
}, () => 'test-key');
|
|
341
|
+
|
|
342
|
+
expect(resolved.mergedConfig).not.toBeNull();
|
|
343
|
+
if (resolved.mergedConfig) {
|
|
344
|
+
expect(resolved.mergedConfig.maxRetries).toBe(5);
|
|
345
|
+
expect(resolved.mergedConfig.timeoutMs).toBe(60000);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('edge cases and error handling', () => {
|
|
351
|
+
it('handles empty workspace directory gracefully', async () => {
|
|
352
|
+
// No .pd directory at all
|
|
353
|
+
const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
354
|
+
const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
|
|
355
|
+
|
|
356
|
+
// Missing config should return defaults (ok=true with source='defaults')
|
|
357
|
+
expect(resolved.configLoadResult.ok).toBe(true);
|
|
358
|
+
expect(resolved.configSource).toBe('.pd/config.yaml');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('handles config with missing runtimeProfiles section', async () => {
|
|
362
|
+
writeConfigYaml(tmpDir, {
|
|
363
|
+
version: 1,
|
|
364
|
+
features: {},
|
|
365
|
+
// Missing runtimeProfiles
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
369
|
+
const { isRuntimeConfigError } = await import('@principles/core/runtime-v2');
|
|
370
|
+
const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
|
|
371
|
+
|
|
372
|
+
// Missing runtimeProfiles should either load with defaults or produce
|
|
373
|
+
// a RuntimeConfigError — never silently succeed with wrong config.
|
|
374
|
+
if (isRuntimeConfigError(resolved.result)) {
|
|
375
|
+
expect(resolved.result.reason).toBeTruthy();
|
|
376
|
+
expect(resolved.result.nextAction).toBeTruthy();
|
|
377
|
+
} else {
|
|
378
|
+
// If defaults were returned, configLoadResult.ok should be true
|
|
379
|
+
expect(resolved.configLoadResult.ok).toBe(true);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('handles config with invalid runtime profile type', async () => {
|
|
384
|
+
writeConfigYaml(tmpDir, {
|
|
385
|
+
version: 1,
|
|
386
|
+
runtimeProfiles: {
|
|
387
|
+
'bad-profile': {
|
|
388
|
+
type: 'invalid-type', // Invalid type
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
internalAgents: {
|
|
392
|
+
defaultRuntime: 'bad-profile',
|
|
393
|
+
agents: { diagnostician: { enabled: true, runtimeProfile: 'bad-profile' } },
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
398
|
+
const { isRuntimeConfigError } = await import('@principles/core/runtime-v2');
|
|
399
|
+
const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
|
|
400
|
+
|
|
401
|
+
// Invalid type should be caught — verify external contract, not internal state
|
|
402
|
+
expect(isRuntimeConfigError(resolved.result)).toBe(true);
|
|
403
|
+
if (isRuntimeConfigError(resolved.result)) {
|
|
404
|
+
expect(resolved.result.reason).toBeTruthy();
|
|
405
|
+
expect(resolved.result.nextAction).toBeTruthy();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('handles env var returning undefined for apiKeyEnv', async () => {
|
|
410
|
+
writeConfigYaml(tmpDir, makeValidConfigYaml());
|
|
411
|
+
|
|
412
|
+
const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
413
|
+
const resolved = resolveRuntimeFromPdConfig(tmpDir, () => undefined);
|
|
414
|
+
|
|
415
|
+
// Should handle undefined env var gracefully
|
|
416
|
+
expect(resolved.result).toBeDefined();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('handles multiple legacy files detected', async () => {
|
|
420
|
+
writeConfigYaml(tmpDir, makeValidConfigYaml());
|
|
421
|
+
|
|
422
|
+
// Create multiple legacy files
|
|
423
|
+
const stateDir = path.join(tmpDir, '.state');
|
|
424
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
425
|
+
fs.writeFileSync(path.join(stateDir, 'workflows.yaml'), 'version: 1', 'utf8');
|
|
426
|
+
|
|
427
|
+
const ffDir = path.join(tmpDir, '.pd');
|
|
428
|
+
fs.writeFileSync(path.join(ffDir, 'feature-flags.yaml'), 'flags: []', 'utf8');
|
|
429
|
+
|
|
430
|
+
const { resolveRuntimeFromPdConfig } = await import('../../src/services/resolve-runtime-from-pd-config.js');
|
|
431
|
+
const resolved = resolveRuntimeFromPdConfig(tmpDir, () => 'test-key');
|
|
432
|
+
|
|
433
|
+
expect(resolved.legacyWarnings.length).toBeGreaterThan(0);
|
|
434
|
+
expect(resolved.configLoadResult.legacyFilesDetected.length).toBeGreaterThanOrEqual(2);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
284
437
|
});
|