Fix doctor summary after repairs
This commit is contained in:
99
cli/src/__tests__/doctor.test.ts
Normal file
99
cli/src/__tests__/doctor.test.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { doctor } from "../commands/doctor.js";
|
||||||
|
import { writeConfig } from "../config/store.js";
|
||||||
|
import type { PaperclipConfig } from "../config/schema.js";
|
||||||
|
|
||||||
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
|
function createTempConfig(): string {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-doctor-"));
|
||||||
|
const configPath = path.join(root, ".paperclip", "config.json");
|
||||||
|
const runtimeRoot = path.join(root, "runtime");
|
||||||
|
|
||||||
|
const config: PaperclipConfig = {
|
||||||
|
$meta: {
|
||||||
|
version: 1,
|
||||||
|
updatedAt: "2026-03-10T00:00:00.000Z",
|
||||||
|
source: "configure",
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
mode: "embedded-postgres",
|
||||||
|
embeddedPostgresDataDir: path.join(runtimeRoot, "db"),
|
||||||
|
embeddedPostgresPort: 55432,
|
||||||
|
backup: {
|
||||||
|
enabled: true,
|
||||||
|
intervalMinutes: 60,
|
||||||
|
retentionDays: 30,
|
||||||
|
dir: path.join(runtimeRoot, "backups"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
mode: "file",
|
||||||
|
logDir: path.join(runtimeRoot, "logs"),
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
deploymentMode: "local_trusted",
|
||||||
|
exposure: "private",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 3199,
|
||||||
|
allowedHostnames: [],
|
||||||
|
serveUi: true,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
baseUrlMode: "auto",
|
||||||
|
disableSignUp: false,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
provider: "local_disk",
|
||||||
|
localDisk: {
|
||||||
|
baseDir: path.join(runtimeRoot, "storage"),
|
||||||
|
},
|
||||||
|
s3: {
|
||||||
|
bucket: "paperclip",
|
||||||
|
region: "us-east-1",
|
||||||
|
prefix: "",
|
||||||
|
forcePathStyle: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
provider: "local_encrypted",
|
||||||
|
strictMode: false,
|
||||||
|
localEncrypted: {
|
||||||
|
keyFilePath: path.join(runtimeRoot, "secrets", "master.key"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
writeConfig(config, configPath);
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("doctor", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
|
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-runs repairable checks so repaired failures do not remain blocking", async () => {
|
||||||
|
const configPath = createTempConfig();
|
||||||
|
|
||||||
|
const summary = await doctor({
|
||||||
|
config: configPath,
|
||||||
|
repair: true,
|
||||||
|
yes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(summary.failed).toBe(0);
|
||||||
|
expect(summary.warned).toBe(0);
|
||||||
|
expect(process.env.PAPERCLIP_AGENT_JWT_SECRET).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -66,28 +66,40 @@ export async function doctor(opts: {
|
|||||||
printResult(deploymentAuthResult);
|
printResult(deploymentAuthResult);
|
||||||
|
|
||||||
// 3. Agent JWT check
|
// 3. Agent JWT check
|
||||||
const jwtResult = agentJwtSecretCheck(opts.config);
|
results.push(
|
||||||
results.push(jwtResult);
|
await runRepairableCheck({
|
||||||
printResult(jwtResult);
|
run: () => agentJwtSecretCheck(opts.config),
|
||||||
await maybeRepair(jwtResult, opts);
|
configPath,
|
||||||
|
opts,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 4. Secrets adapter check
|
// 4. Secrets adapter check
|
||||||
const secretsResult = secretsCheck(config, configPath);
|
results.push(
|
||||||
results.push(secretsResult);
|
await runRepairableCheck({
|
||||||
printResult(secretsResult);
|
run: () => secretsCheck(config, configPath),
|
||||||
await maybeRepair(secretsResult, opts);
|
configPath,
|
||||||
|
opts,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 5. Storage check
|
// 5. Storage check
|
||||||
const storageResult = storageCheck(config, configPath);
|
results.push(
|
||||||
results.push(storageResult);
|
await runRepairableCheck({
|
||||||
printResult(storageResult);
|
run: () => storageCheck(config, configPath),
|
||||||
await maybeRepair(storageResult, opts);
|
configPath,
|
||||||
|
opts,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 6. Database check
|
// 6. Database check
|
||||||
const dbResult = await databaseCheck(config, configPath);
|
results.push(
|
||||||
results.push(dbResult);
|
await runRepairableCheck({
|
||||||
printResult(dbResult);
|
run: () => databaseCheck(config, configPath),
|
||||||
await maybeRepair(dbResult, opts);
|
configPath,
|
||||||
|
opts,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 7. LLM check
|
// 7. LLM check
|
||||||
const llmResult = await llmCheck(config);
|
const llmResult = await llmCheck(config);
|
||||||
@@ -95,10 +107,13 @@ export async function doctor(opts: {
|
|||||||
printResult(llmResult);
|
printResult(llmResult);
|
||||||
|
|
||||||
// 8. Log directory check
|
// 8. Log directory check
|
||||||
const logResult = logCheck(config, configPath);
|
results.push(
|
||||||
results.push(logResult);
|
await runRepairableCheck({
|
||||||
printResult(logResult);
|
run: () => logCheck(config, configPath),
|
||||||
await maybeRepair(logResult, opts);
|
configPath,
|
||||||
|
opts,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// 9. Port check
|
// 9. Port check
|
||||||
const portResult = await portCheck(config);
|
const portResult = await portCheck(config);
|
||||||
@@ -120,9 +135,9 @@ function printResult(result: CheckResult): void {
|
|||||||
async function maybeRepair(
|
async function maybeRepair(
|
||||||
result: CheckResult,
|
result: CheckResult,
|
||||||
opts: { repair?: boolean; yes?: boolean },
|
opts: { repair?: boolean; yes?: boolean },
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
if (result.status === "pass" || !result.canRepair || !result.repair) return;
|
if (result.status === "pass" || !result.canRepair || !result.repair) return false;
|
||||||
if (!opts.repair) return;
|
if (!opts.repair) return false;
|
||||||
|
|
||||||
let shouldRepair = opts.yes;
|
let shouldRepair = opts.yes;
|
||||||
if (!shouldRepair) {
|
if (!shouldRepair) {
|
||||||
@@ -130,7 +145,7 @@ async function maybeRepair(
|
|||||||
message: `Repair "${result.name}"?`,
|
message: `Repair "${result.name}"?`,
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
});
|
});
|
||||||
if (p.isCancel(answer)) return;
|
if (p.isCancel(answer)) return false;
|
||||||
shouldRepair = answer;
|
shouldRepair = answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +153,30 @@ async function maybeRepair(
|
|||||||
try {
|
try {
|
||||||
await result.repair();
|
await result.repair();
|
||||||
p.log.success(`Repaired: ${result.name}`);
|
p.log.success(`Repaired: ${result.name}`);
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
p.log.error(`Repair failed: ${err instanceof Error ? err.message : String(err)}`);
|
p.log.error(`Repair failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runRepairableCheck(input: {
|
||||||
|
run: () => CheckResult | Promise<CheckResult>;
|
||||||
|
configPath: string;
|
||||||
|
opts: { repair?: boolean; yes?: boolean };
|
||||||
|
}): Promise<CheckResult> {
|
||||||
|
let result = await input.run();
|
||||||
|
printResult(result);
|
||||||
|
|
||||||
|
const repaired = await maybeRepair(result, input.opts);
|
||||||
|
if (!repaired) return result;
|
||||||
|
|
||||||
|
// Repairs may create/update the adjacent .env file or other local resources.
|
||||||
|
loadPaperclipEnvFile(input.configPath);
|
||||||
|
result = await input.run();
|
||||||
|
printResult(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printSummary(results: CheckResult[]): { passed: number; warned: number; failed: number } {
|
function printSummary(results: CheckResult[]): { passed: number; warned: number; failed: number } {
|
||||||
|
|||||||
Reference in New Issue
Block a user