@openlife/cli 1.7.5 → 1.7.7
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/cli/InstallModules.js +37 -0
- package/dist/cli/InstallWizard.js +46 -8
- package/dist/cli/LogsCommand.js +181 -0
- package/dist/cli/StatusCommand.js +217 -0
- package/dist/index.js +55 -0
- package/dist/test_install_wizard.js +112 -21
- package/dist/test_logs_command.js +177 -0
- package/dist/test_status_command.js +218 -0
- package/docs/getting-started.md +137 -0
- package/package.json +5 -2
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
// Uses CannedAnswerProvider so no real stdin/tty is required.
|
|
4
4
|
//
|
|
5
5
|
// Question order in wizard.run() (when no pre-existing install):
|
|
6
|
-
// 1) profile (choice: 0=framework, 1=autonomous)
|
|
6
|
+
// 1) profile (choice: 0=framework, 1=autonomous, 2=both)
|
|
7
7
|
// 2) host (choice: 0=claude-code, 1=gemini-cli, 2=codex)
|
|
8
|
-
// 3)
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// 3) wantsApiKeys (yesNo)
|
|
9
|
+
// 3a-d) [if Y] OPENAI / ANTHROPIC / GEMINI / OPENROUTER pastes (text)
|
|
10
|
+
// 4) wantsOAuth (yesNo)
|
|
11
|
+
// 5) model order (text: blank or comma list)
|
|
12
|
+
// 6) telegram? (yesNo, autonomous only)
|
|
13
|
+
// 7) skip doctor? (yesNo)
|
|
14
|
+
// 8) confirm preview (yesNo)
|
|
12
15
|
//
|
|
13
16
|
// When pre-existing install detected, an extra choice prompt fires FIRST:
|
|
14
17
|
// 0) abort 1) reinstall 2) repair
|
|
@@ -76,8 +79,8 @@ function writeExistingInstall(root) {
|
|
|
76
79
|
async function scenario1HappyPathFrameworkClaudeCode() {
|
|
77
80
|
const root = tempRoot();
|
|
78
81
|
try {
|
|
79
|
-
// profile=framework(0), host=claude-code(0), models='', skipDoctor=false, confirm=true
|
|
80
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, '', false, true]);
|
|
82
|
+
// profile=framework(0), host=claude-code(0), apiKeys=N, oauth=N, models='', skipDoctor=false, confirm=true
|
|
83
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, false, false, '', false, true]);
|
|
81
84
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
82
85
|
const result = await wizard.run();
|
|
83
86
|
assert(result.ok === true, 'scenario1: expected ok=true');
|
|
@@ -88,7 +91,8 @@ async function scenario1HappyPathFrameworkClaudeCode() {
|
|
|
88
91
|
assert(result.options.skipDoctor === false, 'scenario1: skipDoctor should be false');
|
|
89
92
|
assert(result.options.modelOrder === undefined, 'scenario1: modelOrder should be undefined when blank');
|
|
90
93
|
assert(result.preExistingAction === undefined, 'scenario1: no pre-existing action expected');
|
|
91
|
-
|
|
94
|
+
// SKIPPED_API_KEYS warning is expected when wantsApiKeys=false
|
|
95
|
+
assert(Array.isArray(result.warnings) && result.warnings.some((w) => w.includes('SKIPPED_API_KEYS')), 'scenario1: should warn SKIPPED_API_KEYS when api-key prompt skipped');
|
|
92
96
|
console.log('✅ scenario 1: happy path framework + claude-code');
|
|
93
97
|
}
|
|
94
98
|
finally {
|
|
@@ -98,8 +102,8 @@ async function scenario1HappyPathFrameworkClaudeCode() {
|
|
|
98
102
|
async function scenario2HappyPathAutonomousClaudeCode() {
|
|
99
103
|
const root = tempRoot();
|
|
100
104
|
try {
|
|
101
|
-
// profile=autonomous(1), host=claude-code(0), models='', telegram=true, skipDoctor=false, confirm=true
|
|
102
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, '', true, false, true]);
|
|
105
|
+
// profile=autonomous(1), host=claude-code(0), apiKeys=N, oauth=N, models='', telegram=true, skipDoctor=false, confirm=true
|
|
106
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, false, false, '', true, false, true]);
|
|
103
107
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
104
108
|
const result = await wizard.run();
|
|
105
109
|
assert(result.ok === true, 'scenario2: expected ok=true');
|
|
@@ -107,7 +111,8 @@ async function scenario2HappyPathAutonomousClaudeCode() {
|
|
|
107
111
|
return;
|
|
108
112
|
assert(result.options.profile === 'autonomous', 'scenario2: profile should be autonomous');
|
|
109
113
|
assert(result.options.host === 'claude-code', 'scenario2: host should be claude-code');
|
|
110
|
-
|
|
114
|
+
// SKIPPED_API_KEYS warning is expected when wantsApiKeys=false
|
|
115
|
+
assert(Array.isArray(result.warnings) && result.warnings.some((w) => w.includes('SKIPPED_API_KEYS')), 'scenario2: should warn SKIPPED_API_KEYS when api-key prompt skipped');
|
|
111
116
|
console.log('✅ scenario 2: happy path autonomous + claude-code');
|
|
112
117
|
}
|
|
113
118
|
finally {
|
|
@@ -117,8 +122,8 @@ async function scenario2HappyPathAutonomousClaudeCode() {
|
|
|
117
122
|
async function scenario3UserAbortsOnConfirm() {
|
|
118
123
|
const root = tempRoot();
|
|
119
124
|
try {
|
|
120
|
-
// framework, claude-code, blank models, skipDoctor=false, confirm=false (abort)
|
|
121
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, '', false, false]);
|
|
125
|
+
// framework, claude-code, apiKeys=N, oauth=N, blank models, skipDoctor=false, confirm=false (abort)
|
|
126
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, false, false, '', false, false]);
|
|
122
127
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
123
128
|
const result = await wizard.run();
|
|
124
129
|
assert(result.ok === false, 'scenario3: expected ok=false');
|
|
@@ -156,8 +161,8 @@ async function scenario5PreExistingRepair() {
|
|
|
156
161
|
const root = tempRoot();
|
|
157
162
|
try {
|
|
158
163
|
writeExistingInstall(root);
|
|
159
|
-
// 2=repair, then full flow: framework, claude-code, '', skipDoctor=false, confirm=true
|
|
160
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([2, 0, 0, '', false, true]);
|
|
164
|
+
// 2=repair, then full flow: framework, claude-code, apiKeys=N, oauth=N, '', skipDoctor=false, confirm=true
|
|
165
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([2, 0, 0, false, false, '', false, true]);
|
|
161
166
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
162
167
|
const result = await wizard.run();
|
|
163
168
|
assert(result.ok === true, 'scenario5: expected ok=true');
|
|
@@ -175,8 +180,8 @@ async function scenario6PreExistingReinstall() {
|
|
|
175
180
|
const root = tempRoot();
|
|
176
181
|
try {
|
|
177
182
|
writeExistingInstall(root);
|
|
178
|
-
// 1=reinstall, framework, claude-code, '', skipDoctor=false, confirm=true
|
|
179
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, 0, '', false, true]);
|
|
183
|
+
// 1=reinstall, framework, claude-code, apiKeys=N, oauth=N, '', skipDoctor=false, confirm=true
|
|
184
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([1, 0, 0, false, false, '', false, true]);
|
|
180
185
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
181
186
|
const result = await wizard.run();
|
|
182
187
|
assert(result.ok === true, 'scenario6: expected ok=true');
|
|
@@ -192,8 +197,8 @@ async function scenario6PreExistingReinstall() {
|
|
|
192
197
|
async function scenario7UnsupportedHostGeminiCli() {
|
|
193
198
|
const root = tempRoot();
|
|
194
199
|
try {
|
|
195
|
-
// framework, host=1 (gemini-cli, not yet supported), blank models, skipDoctor=false, confirm=true
|
|
196
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 1, '', false, true]);
|
|
200
|
+
// framework, host=1 (gemini-cli, not yet supported), apiKeys=N, oauth=N, blank models, skipDoctor=false, confirm=true
|
|
201
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 1, false, false, '', false, true]);
|
|
197
202
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
198
203
|
const result = await wizard.run();
|
|
199
204
|
assert(result.ok === true, 'scenario7: expected ok=true even with unsupported host');
|
|
@@ -212,8 +217,8 @@ async function scenario8CustomModelChain() {
|
|
|
212
217
|
const root = tempRoot();
|
|
213
218
|
try {
|
|
214
219
|
const models = 'gemini-api/gemini-3.1-pro-preview,openai-api/gpt-5.4-mini';
|
|
215
|
-
// framework, claude-code, models=custom, skipDoctor=false, confirm=true
|
|
216
|
-
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, models, false, true]);
|
|
220
|
+
// framework, claude-code, apiKeys=N, oauth=N, models=custom, skipDoctor=false, confirm=true
|
|
221
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([0, 0, false, false, models, false, true]);
|
|
217
222
|
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
218
223
|
const result = await wizard.run();
|
|
219
224
|
assert(result.ok === true, 'scenario8: expected ok=true');
|
|
@@ -252,6 +257,89 @@ async function scenario9OutOfAnswersThrows() {
|
|
|
252
257
|
cleanup(root);
|
|
253
258
|
}
|
|
254
259
|
}
|
|
260
|
+
async function scenario10WithApiKeysPersists() {
|
|
261
|
+
const root = tempRoot();
|
|
262
|
+
try {
|
|
263
|
+
// framework, claude-code, apiKeys=Y, paste 4 keys (openai+anthropic, skip gemini+openrouter),
|
|
264
|
+
// oauth=N, blank models, skipDoctor=false, confirm=true
|
|
265
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([
|
|
266
|
+
0, 0,
|
|
267
|
+
true, // wantsApiKeys
|
|
268
|
+
'openai-test-key', // openai paste (non-secret test fixture)
|
|
269
|
+
'anthropic-test-key', // anthropic paste (non-secret test fixture)
|
|
270
|
+
'', // gemini skip
|
|
271
|
+
'', // openrouter skip
|
|
272
|
+
false, // wantsOAuth
|
|
273
|
+
'', // models
|
|
274
|
+
false, // skipDoctor
|
|
275
|
+
true, // confirm
|
|
276
|
+
]);
|
|
277
|
+
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
278
|
+
const result = await wizard.run();
|
|
279
|
+
assert(result.ok === true, 'scenario10: expected ok=true');
|
|
280
|
+
if (!result.ok)
|
|
281
|
+
return;
|
|
282
|
+
// .env should contain the pasted keys
|
|
283
|
+
const envPath = path.join(root, '.env');
|
|
284
|
+
assert(fs.existsSync(envPath), 'scenario10: .env should exist after wizard');
|
|
285
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
286
|
+
assert(envContent.includes('OPENAI_API_KEY=openai-test-key'), 'scenario10: .env should have OPENAI_API_KEY');
|
|
287
|
+
assert(envContent.includes('ANTHROPIC_API_KEY=anthropic-test-key'), 'scenario10: .env should have ANTHROPIC_API_KEY');
|
|
288
|
+
assert(!envContent.includes('GEMINI_API_KEY='), 'scenario10: .env should NOT have GEMINI_API_KEY (skipped)');
|
|
289
|
+
// SKIPPED_API_KEYS warning should NOT fire when at least one key was provided
|
|
290
|
+
if (result.warnings) {
|
|
291
|
+
assert(!result.warnings.some((w) => w.includes('SKIPPED_API_KEYS')), 'scenario10: should NOT warn SKIPPED_API_KEYS when keys were provided');
|
|
292
|
+
}
|
|
293
|
+
console.log('✅ scenario 10: API keys collected and persisted to .env');
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
cleanup(root);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function scenario11AbortDoesNotPersistApiKeys() {
|
|
300
|
+
const root = tempRoot();
|
|
301
|
+
try {
|
|
302
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([
|
|
303
|
+
0, 0,
|
|
304
|
+
true,
|
|
305
|
+
'openai-abort-test-key',
|
|
306
|
+
'',
|
|
307
|
+
'',
|
|
308
|
+
'',
|
|
309
|
+
false,
|
|
310
|
+
'',
|
|
311
|
+
false,
|
|
312
|
+
false,
|
|
313
|
+
]);
|
|
314
|
+
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
315
|
+
const result = await wizard.run();
|
|
316
|
+
assert(result.ok === false, 'scenario11: expected ok=false');
|
|
317
|
+
assert(!fs.existsSync(path.join(root, '.env')), 'scenario11: aborted wizard must not write .env');
|
|
318
|
+
console.log('✅ scenario 11: abort after API-key collection does not persist .env');
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
cleanup(root);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function scenario12ProfileBothMapsToAutonomous() {
|
|
325
|
+
const root = tempRoot();
|
|
326
|
+
try {
|
|
327
|
+
// profile=both(2), claude-code, apiKeys=N, oauth=N, models='', telegram=true (autonomous path),
|
|
328
|
+
// skipDoctor=false, confirm=true
|
|
329
|
+
const provider = new InstallWizard_1.CannedAnswerProvider([2, 0, false, false, '', true, false, true]);
|
|
330
|
+
const wizard = new InstallWizard_1.InstallWizard(root, provider);
|
|
331
|
+
const result = await wizard.run();
|
|
332
|
+
assert(result.ok === true, 'scenario11: expected ok=true');
|
|
333
|
+
if (!result.ok)
|
|
334
|
+
return;
|
|
335
|
+
assert(result.options.profile === 'autonomous', 'scenario11: profile=both should map internally to autonomous');
|
|
336
|
+
assert(Array.isArray(result.warnings) && result.warnings.some((w) => w.includes('INSTALLING_BOTH')), 'scenario11: should emit INSTALLING_BOTH warning so caller knows both layers were intended');
|
|
337
|
+
console.log('✅ scenario 11: profile=both maps to autonomous with INSTALLING_BOTH warning');
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
cleanup(root);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
255
343
|
async function main() {
|
|
256
344
|
console.log('🧪 test_install_wizard — Story 3.5 regression suite');
|
|
257
345
|
await scenario1HappyPathFrameworkClaudeCode();
|
|
@@ -263,6 +351,9 @@ async function main() {
|
|
|
263
351
|
await scenario7UnsupportedHostGeminiCli();
|
|
264
352
|
await scenario8CustomModelChain();
|
|
265
353
|
await scenario9OutOfAnswersThrows();
|
|
354
|
+
await scenario10WithApiKeysPersists();
|
|
355
|
+
await scenario11AbortDoesNotPersistApiKeys();
|
|
356
|
+
await scenario12ProfileBothMapsToAutonomous();
|
|
266
357
|
console.log('');
|
|
267
358
|
console.log('TEST_INSTALL_WIZARD_OK');
|
|
268
359
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// test_logs_command — verifies filter / tail / since semantics + duration
|
|
3
|
+
// parser of `openlife logs`.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const LogsCommand_1 = require("./cli/LogsCommand");
|
|
42
|
+
function assert(cond, msg) {
|
|
43
|
+
if (!cond) {
|
|
44
|
+
console.error(`❌ ASSERT FAILED: ${msg}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function tempRoot() {
|
|
49
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-logs-'));
|
|
50
|
+
fs.mkdirSync(path.join(root, '.openlife'), { recursive: true });
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
function cleanup(root) {
|
|
54
|
+
try {
|
|
55
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// best-effort
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeJsonl(root, filename, entries) {
|
|
62
|
+
fs.writeFileSync(path.join(root, '.openlife', filename), entries.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
function scenarioDurationParser() {
|
|
65
|
+
assert((0, LogsCommand_1.parseDurationToSeconds)('30s') === 30, '30s → 30');
|
|
66
|
+
assert((0, LogsCommand_1.parseDurationToSeconds)('5m') === 300, '5m → 300');
|
|
67
|
+
assert((0, LogsCommand_1.parseDurationToSeconds)('1h') === 3600, '1h → 3600');
|
|
68
|
+
assert((0, LogsCommand_1.parseDurationToSeconds)('2d') === 172800, '2d → 172800');
|
|
69
|
+
assert((0, LogsCommand_1.parseDurationToSeconds)('bogus') === null, 'bogus → null');
|
|
70
|
+
assert((0, LogsCommand_1.parseDurationToSeconds)('-1m') === null, 'negative → null');
|
|
71
|
+
console.log('✅ scenario 1: parseDurationToSeconds covers s/m/h/d + rejects garbage');
|
|
72
|
+
}
|
|
73
|
+
function scenarioTailDefault() {
|
|
74
|
+
const root = tempRoot();
|
|
75
|
+
try {
|
|
76
|
+
const entries = [];
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
for (let i = 0; i < 50; i++) {
|
|
79
|
+
entries.push({ ts: new Date(now - (50 - i) * 1000).toISOString(), summary: `entry ${i}` });
|
|
80
|
+
}
|
|
81
|
+
writeJsonl(root, 'governance-ledger.jsonl', entries);
|
|
82
|
+
const collected = (0, LogsCommand_1.collectLogs)({ root, tail: 5 });
|
|
83
|
+
assert(collected.length === 5, `expected 5 entries, got ${collected.length}`);
|
|
84
|
+
assert(collected[collected.length - 1].parsed.summary === 'entry 49', 'last entry should be the newest');
|
|
85
|
+
console.log('✅ scenario 2: tail=5 returns last 5 chronological entries');
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
cleanup(root);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function scenarioFilterBySubsystem() {
|
|
92
|
+
const root = tempRoot();
|
|
93
|
+
try {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const govEntries = [
|
|
96
|
+
{ ts: new Date(now).toISOString(), summary: 'gov-entry' },
|
|
97
|
+
];
|
|
98
|
+
const mediaEntries = [
|
|
99
|
+
{ ts: new Date(now).toISOString(), summary: 'media-entry' },
|
|
100
|
+
];
|
|
101
|
+
writeJsonl(root, 'governance-ledger.jsonl', govEntries);
|
|
102
|
+
writeJsonl(root, 'media-routing.log.jsonl', mediaEntries);
|
|
103
|
+
const govOnly = (0, LogsCommand_1.collectLogs)({ root, filter: 'governance' });
|
|
104
|
+
assert(govOnly.length === 1, `expected 1 governance entry, got ${govOnly.length}`);
|
|
105
|
+
assert(govOnly[0].subsystem === 'governance-ledger', 'subsystem should match');
|
|
106
|
+
const mediaOnly = (0, LogsCommand_1.collectLogs)({ root, filter: 'media' });
|
|
107
|
+
assert(mediaOnly.length === 1, `expected 1 media entry, got ${mediaOnly.length}`);
|
|
108
|
+
assert(mediaOnly[0].subsystem === 'media-routing', 'media subsystem name');
|
|
109
|
+
console.log('✅ scenario 3: --filter narrows by subsystem id');
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
cleanup(root);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function scenarioSinceFiltersOldEntries() {
|
|
116
|
+
const root = tempRoot();
|
|
117
|
+
try {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const entries = [
|
|
120
|
+
{ ts: new Date(now - 3600_000).toISOString(), summary: 'one-hour-ago' },
|
|
121
|
+
{ ts: new Date(now - 60_000).toISOString(), summary: 'one-minute-ago' },
|
|
122
|
+
{ ts: new Date(now - 5_000).toISOString(), summary: 'five-seconds-ago' },
|
|
123
|
+
];
|
|
124
|
+
writeJsonl(root, 'governance-ledger.jsonl', entries);
|
|
125
|
+
const recent = (0, LogsCommand_1.collectLogs)({ root, since: '5m' });
|
|
126
|
+
assert(recent.length === 2, `expected 2 entries within 5m, got ${recent.length}`);
|
|
127
|
+
assert(!recent.some((e) => e.parsed.summary === 'one-hour-ago'), 'one-hour-ago should be excluded');
|
|
128
|
+
console.log('✅ scenario 4: --since=5m excludes older entries');
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
cleanup(root);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function scenarioRenderHumanAndJson() {
|
|
135
|
+
const root = tempRoot();
|
|
136
|
+
try {
|
|
137
|
+
writeJsonl(root, 'governance-ledger.jsonl', [
|
|
138
|
+
{ ts: new Date().toISOString(), summary: 'test summary' },
|
|
139
|
+
]);
|
|
140
|
+
const entries = (0, LogsCommand_1.collectLogs)({ root });
|
|
141
|
+
const human = (0, LogsCommand_1.renderLogsHuman)(entries);
|
|
142
|
+
assert(human.includes('[governance-ledger]'), 'human render includes subsystem');
|
|
143
|
+
assert(human.includes('test summary'), 'human render includes summary');
|
|
144
|
+
const jsonLine = (0, LogsCommand_1.renderLogsJson)(entries);
|
|
145
|
+
const parsed = JSON.parse(jsonLine);
|
|
146
|
+
assert(parsed.subsystem === 'governance-ledger', 'json render preserves subsystem');
|
|
147
|
+
console.log('✅ scenario 5: human + JSON renderers both produce expected shape');
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
cleanup(root);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function scenarioNoEntriesGracefullyHandled() {
|
|
154
|
+
const root = tempRoot();
|
|
155
|
+
try {
|
|
156
|
+
const entries = (0, LogsCommand_1.collectLogs)({ root });
|
|
157
|
+
assert(entries.length === 0, 'no jsonl files → empty result');
|
|
158
|
+
const human = (0, LogsCommand_1.renderLogsHuman)(entries);
|
|
159
|
+
assert(human.includes('no entries'), 'human render reports empty');
|
|
160
|
+
console.log('✅ scenario 6: empty state renders helpful message');
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
cleanup(root);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function main() {
|
|
167
|
+
console.log('🧪 test_logs_command — jsonl viewer');
|
|
168
|
+
scenarioDurationParser();
|
|
169
|
+
scenarioTailDefault();
|
|
170
|
+
scenarioFilterBySubsystem();
|
|
171
|
+
scenarioSinceFiltersOldEntries();
|
|
172
|
+
scenarioRenderHumanAndJson();
|
|
173
|
+
scenarioNoEntriesGracefullyHandled();
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log('TEST_LOGS_COMMAND_OK');
|
|
176
|
+
}
|
|
177
|
+
main();
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// test_status_command — verifies the shape and overall-derivation logic of
|
|
3
|
+
// `openlife status`. Uses a temp .openlife/ scratch dir so we don't rely on
|
|
4
|
+
// (or pollute) the maintainer's local runtime state.
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const StatusCommand_1 = require("./cli/StatusCommand");
|
|
43
|
+
function assert(cond, msg) {
|
|
44
|
+
if (!cond) {
|
|
45
|
+
console.error(`❌ ASSERT FAILED: ${msg}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function tempRoot() {
|
|
50
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openlife-status-'));
|
|
51
|
+
fs.mkdirSync(path.join(root, '.openlife'), { recursive: true });
|
|
52
|
+
return root;
|
|
53
|
+
}
|
|
54
|
+
function cleanup(root) {
|
|
55
|
+
try {
|
|
56
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// best-effort
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function writeJson(root, filename, payload) {
|
|
63
|
+
fs.writeFileSync(path.join(root, '.openlife', filename), JSON.stringify(payload, null, 2), 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
function writeJsonl(root, filename, entries) {
|
|
66
|
+
fs.writeFileSync(path.join(root, '.openlife', filename), entries.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf-8');
|
|
67
|
+
}
|
|
68
|
+
function scenarioEmpty() {
|
|
69
|
+
const root = tempRoot();
|
|
70
|
+
try {
|
|
71
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
72
|
+
assert(report.overall === 'down', 'empty state should be down (no heartbeat, no executors)');
|
|
73
|
+
assert(report.heartbeat.present === false, 'no heartbeat in empty state');
|
|
74
|
+
assert(report.governance.ledgerPresent === false, 'no governance ledger in empty state');
|
|
75
|
+
assert(report.executors.length === 0, 'no executors in empty state');
|
|
76
|
+
assert(report.notes.includes('no_heartbeat'), 'should note no_heartbeat');
|
|
77
|
+
console.log('✅ scenario 1: empty state → overall=down with no_heartbeat note');
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
cleanup(root);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function scenarioHealthy() {
|
|
84
|
+
const root = tempRoot();
|
|
85
|
+
try {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
writeJson(root, 'heartbeat.json', { pid: 12345, host: 'test-host', ts: now, uptime_s: 100, startedAt: now - 100_000 });
|
|
88
|
+
writeJson(root, 'runtime-policy-status.json', {
|
|
89
|
+
executors: {
|
|
90
|
+
openai: { executor: 'openai', available: true, reason: 'ok', updatedAt: new Date(now).toISOString() },
|
|
91
|
+
anthropic: { executor: 'anthropic', available: true, reason: 'ok', updatedAt: new Date(now).toISOString() },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
writeJsonl(root, 'governance-ledger.jsonl', [
|
|
95
|
+
{ index: 1, ts: new Date(now).toISOString(), entryHash: 'aaa' },
|
|
96
|
+
{ index: 2, ts: new Date(now).toISOString(), entryHash: 'bbb' },
|
|
97
|
+
]);
|
|
98
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
99
|
+
assert(report.overall === 'healthy', `expected healthy, got ${report.overall} (notes: ${report.notes.join(',')})`);
|
|
100
|
+
assert(report.heartbeat.fresh === true, 'fresh heartbeat expected');
|
|
101
|
+
assert(report.executors.length === 2, 'two executors expected');
|
|
102
|
+
assert(report.executors.every((e) => e.available), 'all executors available');
|
|
103
|
+
assert(report.governance.entryCount === 2, 'two ledger entries expected');
|
|
104
|
+
assert(report.governance.lastEntryHash === 'bbb', 'last entry hash is bbb');
|
|
105
|
+
console.log('✅ scenario 2: healthy heartbeat + all executors + ledger → overall=healthy');
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
cleanup(root);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function scenarioDegradedOneExecutorDown() {
|
|
112
|
+
const root = tempRoot();
|
|
113
|
+
try {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
writeJson(root, 'heartbeat.json', { pid: 1, host: 'h', ts: now, uptime_s: 1, startedAt: now });
|
|
116
|
+
writeJson(root, 'runtime-policy-status.json', {
|
|
117
|
+
executors: {
|
|
118
|
+
openai: { executor: 'openai', available: true, reason: 'ok' },
|
|
119
|
+
anthropic: { executor: 'anthropic', available: false, reason: 'auth expired', category: 'auth' },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
123
|
+
assert(report.overall === 'degraded', `expected degraded, got ${report.overall}`);
|
|
124
|
+
assert(report.notes.some((n) => n.startsWith('executors_down:')), 'should note specific executors down');
|
|
125
|
+
console.log('✅ scenario 3: one executor down → overall=degraded');
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
cleanup(root);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function scenarioDownAllExecutorsDown() {
|
|
132
|
+
const root = tempRoot();
|
|
133
|
+
try {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
writeJson(root, 'heartbeat.json', { pid: 1, host: 'h', ts: now, uptime_s: 1, startedAt: now });
|
|
136
|
+
writeJson(root, 'runtime-policy-status.json', {
|
|
137
|
+
executors: {
|
|
138
|
+
openai: { executor: 'openai', available: false, reason: 'rate limit' },
|
|
139
|
+
anthropic: { executor: 'anthropic', available: false, reason: 'auth expired' },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
143
|
+
assert(report.overall === 'down', `expected down, got ${report.overall}`);
|
|
144
|
+
assert(report.notes.includes('all_executors_down'), 'should note all_executors_down');
|
|
145
|
+
console.log('✅ scenario 4: all executors down → overall=down');
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
cleanup(root);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function scenarioStaleHeartbeat() {
|
|
152
|
+
const root = tempRoot();
|
|
153
|
+
try {
|
|
154
|
+
const staleTs = Date.now() - (StatusCommand_1.HEARTBEAT_FRESH_S + 30) * 1000;
|
|
155
|
+
writeJson(root, 'heartbeat.json', { pid: 1, host: 'h', ts: staleTs, uptime_s: 9999, startedAt: staleTs - 1000 });
|
|
156
|
+
writeJson(root, 'runtime-policy-status.json', {
|
|
157
|
+
executors: { openai: { executor: 'openai', available: true, reason: 'ok' } },
|
|
158
|
+
});
|
|
159
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
160
|
+
assert(report.overall === 'degraded', `expected degraded for stale beat, got ${report.overall}`);
|
|
161
|
+
assert(report.heartbeat.fresh === false, 'stale heartbeat is not fresh');
|
|
162
|
+
assert(report.notes.some((n) => n.startsWith('heartbeat_warning_')), 'should warn about stale heartbeat');
|
|
163
|
+
console.log('✅ scenario 5: stale heartbeat → overall=degraded with heartbeat_warning note');
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
cleanup(root);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function scenarioDeadHeartbeat() {
|
|
170
|
+
const root = tempRoot();
|
|
171
|
+
try {
|
|
172
|
+
const deadTs = Date.now() - (StatusCommand_1.HEARTBEAT_STALE_S + 60) * 1000;
|
|
173
|
+
writeJson(root, 'heartbeat.json', { pid: 1, host: 'h', ts: deadTs, uptime_s: 9999, startedAt: deadTs - 1000 });
|
|
174
|
+
writeJson(root, 'runtime-policy-status.json', {
|
|
175
|
+
executors: { openai: { executor: 'openai', available: true, reason: 'ok' } },
|
|
176
|
+
});
|
|
177
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
178
|
+
assert(report.overall === 'down', `expected down for dead beat, got ${report.overall}`);
|
|
179
|
+
assert(report.notes.some((n) => n.startsWith('heartbeat_stale_')), 'should note heartbeat_stale_<age>s');
|
|
180
|
+
console.log('✅ scenario 6: dead heartbeat → overall=down');
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
cleanup(root);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function scenarioCorruptLedgerHandled() {
|
|
187
|
+
const root = tempRoot();
|
|
188
|
+
try {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
writeJson(root, 'heartbeat.json', { pid: 1, host: 'h', ts: now, uptime_s: 1, startedAt: now });
|
|
191
|
+
writeJson(root, 'runtime-policy-status.json', {
|
|
192
|
+
executors: { openai: { executor: 'openai', available: true, reason: 'ok' } },
|
|
193
|
+
});
|
|
194
|
+
// Write a corrupt last line — must not throw.
|
|
195
|
+
fs.writeFileSync(path.join(root, '.openlife', 'governance-ledger.jsonl'), '{"index":1,"entryHash":"good","ts":"' + new Date(now).toISOString() + '"}\n{not-valid-json\n', 'utf-8');
|
|
196
|
+
const report = (0, StatusCommand_1.buildStatusReport)(root);
|
|
197
|
+
assert(report.governance.entryCount === 2, 'should count both lines (parseability separate from count)');
|
|
198
|
+
// Last entry was corrupt → lastEntryHash undefined; gracefully degraded, doesn't throw.
|
|
199
|
+
assert(report.governance.lastEntryHash === undefined, 'corrupt last line yields no entryHash');
|
|
200
|
+
console.log('✅ scenario 7: corrupt ledger line handled gracefully');
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
cleanup(root);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function main() {
|
|
207
|
+
console.log('🧪 test_status_command — runtime state aggregator');
|
|
208
|
+
scenarioEmpty();
|
|
209
|
+
scenarioHealthy();
|
|
210
|
+
scenarioDegradedOneExecutorDown();
|
|
211
|
+
scenarioDownAllExecutorsDown();
|
|
212
|
+
scenarioStaleHeartbeat();
|
|
213
|
+
scenarioDeadHeartbeat();
|
|
214
|
+
scenarioCorruptLedgerHandled();
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log('TEST_STATUS_COMMAND_OK');
|
|
217
|
+
}
|
|
218
|
+
main();
|