Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/gittensory-miner/lib/status.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export function collectStatus(env?: Record<string, string | undefined>, cwd?: st

export function runStatus(args?: string[], env?: Record<string, string | undefined>, cwd?: string): number;

export function runDoctorChecks(env?: Record<string, string | undefined>): DoctorCheck[];
export function checkConfigContent(cwd: string, readImpl?: (path: string, encoding: "utf8") => string): DoctorCheck;

export function runDoctor(args?: string[], env?: Record<string, string | undefined>): number;
export function runDoctorChecks(env?: Record<string, string | undefined>, cwd?: string): DoctorCheck[];

export function runDoctor(args?: string[], env?: Record<string, string | undefined>, cwd?: string): number;

export function readInstalledEnginePackageVersionFromPaths(
resolvedEntry: string,
Expand Down
31 changes: 26 additions & 5 deletions packages/gittensory-miner/lib/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { join } from "node:path";
import { CODING_AGENT_DRIVER_CONFIG_ENV, resolveFirstConfiguredCodingAgentDriverName } from "@jsonbored/gittensory-engine";
import { CODING_AGENT_DRIVER_CONFIG_ENV, parseMinerGoalSpecContent, resolveFirstConfiguredCodingAgentDriverName } from "@jsonbored/gittensory-engine";
import {
checkClaudeCliPresent,
checkCodexCliPresent,
Expand All @@ -11,7 +11,7 @@ import {
findExecutableOnPath,
} from "./laptop-init.js";
import { resolveMinerVersion } from "./version.js";
import { checkStoreIntegrity } from "./store-maintenance.js";
import { checkStoreIntegrity, describeError } from "./store-maintenance.js";
import { resolveEventLedgerDbPath } from "./event-ledger.js";
import { resolveGovernorLedgerDbPath } from "./governor-ledger.js";
import { resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
Expand Down Expand Up @@ -294,9 +294,29 @@ function storeIntegrityChecks(env) {
return stores.map(([name, dbPath]) => checkStoreIntegrity(`store-integrity:${name}`, dbPath));
}

/** Validate the discovered `.gittensory-miner` config's CONTENT (#4873), not just its path: parse it with the
* tolerant goal-spec parser and surface its warnings, so a malformed config is flagged by `doctor` rather than
* silently degrading to defaults. No config file is fine (defaults apply); a read failure is reported. `readImpl`
* is injectable for tests. */
export function checkConfigContent(cwd, readImpl = readFileSync) {
const configPath = discoverConfigFile(cwd);
if (!configPath) {
return { name: "config-content", ok: true, detail: "no .gittensory-miner config found (using defaults)" };
}
let warnings;
try {
warnings = parseMinerGoalSpecContent(readImpl(configPath, "utf8")).warnings;
} catch (error) {
return { name: "config-content", ok: false, detail: `${configPath}: ${describeError(error)}` };
}
return warnings.length === 0
? { name: "config-content", ok: true, detail: `${configPath}: valid` }
: { name: "config-content", ok: false, detail: `${configPath}: ${warnings.join("; ")}` };
}

/** Run the doctor checks. Returns an array of { name, ok, detail }; only writes a transient probe in the state dir,
* never touches the network. */
export function runDoctorChecks(env = process.env) {
export function runDoctorChecks(env = process.env, cwd = process.cwd()) {
const nodeMajor = Number(process.versions.node.split(".")[0]);
const requiredMajor = requiredNodeMajor();
const engineVersion = readEngineVersion();
Expand All @@ -317,12 +337,13 @@ export function runDoctorChecks(env = process.env) {
checkDockerPresent(),
checkClaudeCliPresent({ env }),
checkCodexCliPresent({ env }),
checkConfigContent(cwd),
...storeIntegrityChecks(env),
];
}

export function runDoctor(args = [], env = process.env) {
const checks = runDoctorChecks(env);
export function runDoctor(args = [], env = process.env, cwd = process.cwd()) {
const checks = runDoctorChecks(env, cwd);
const failed = checks.filter((check) => !check.ok);
if (args.includes("--json")) {
console.log(JSON.stringify({ ok: failed.length === 0, checks }, null, 2));
Expand Down
51 changes: 49 additions & 2 deletions test/unit/miner-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolveEventLedgerDbPath } from "../../packages/gittensory-miner/lib/ev
import { afterEach, describe, expect, it, vi } from "vitest";
import {
buildEngineVersionSkewCheck,
checkConfigContent,
collectStatus,
compareInstalledEngineVersion,
readExpectedEnginePackageVersion,
Expand Down Expand Up @@ -82,7 +83,8 @@ describe("gittensory-miner status/doctor (#2288)", () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
const env = { GITTENSORY_MINER_CONFIG_DIR: join(tempRoot(), "state") };
initLaptopState(env);
const checks = runDoctorChecks(env);
const cwd = tempRoot(); // a config-less working dir ⇒ config-content check is a clean pass
const checks = runDoctorChecks(env, cwd);
expect(checks.every((check) => check.ok)).toBe(true);
expect(checks.map((check) => check.name)).toEqual([
"node-version",
Expand All @@ -93,6 +95,7 @@ describe("gittensory-miner status/doctor (#2288)", () => {
"docker-present",
"claude-cli-present",
"codex-cli-present",
"config-content",
"store-integrity:event-ledger",
"store-integrity:governor-ledger",
"store-integrity:prediction-ledger",
Expand All @@ -101,7 +104,7 @@ describe("gittensory-miner status/doctor (#2288)", () => {
"store-integrity:run-state",
"store-integrity:plan-store",
]);
expect(runDoctor([], env)).toBe(0);
expect(runDoctor([], env, cwd)).toBe(0);
expect(log).toHaveBeenCalled();
});

Expand All @@ -115,6 +118,50 @@ describe("gittensory-miner status/doctor (#2288)", () => {
expect(runDoctor([], env)).toBe(1); // a failed check makes doctor exit non-zero
});

describe("checkConfigContent (#4873)", () => {
it("is a clean pass when no config file is present (defaults apply)", () => {
const result = checkConfigContent(tempRoot());
expect(result).toMatchObject({ name: "config-content", ok: true });
expect(result.detail).toContain("no .gittensory-miner config");
});

it("passes a well-formed config", () => {
const cwd = tempRoot();
writeFileSync(join(cwd, ".gittensory-miner.yml"), "wantedPaths:\n - src\n");
const result = checkConfigContent(cwd);
expect(result.ok).toBe(true);
expect(result.detail).toContain("valid");
});

it("flags a malformed config with the parser's specific warnings", () => {
const cwd = tempRoot();
// wantedPaths must be a list; a scalar triggers a parser warning rather than a silent default.
writeFileSync(join(cwd, ".gittensory-miner.yml"), "wantedPaths: not-a-list\n");
const result = checkConfigContent(cwd);
expect(result.ok).toBe(false);
expect(result.detail).toMatch(/wantedPaths/);
});

it("reports a read failure (config discovered but unreadable)", () => {
const cwd = tempRoot();
writeFileSync(join(cwd, ".gittensory-miner.yml"), "wantedPaths:\n - src\n");
const throwingRead = () => {
throw new Error("EACCES: permission denied");
};
const result = checkConfigContent(cwd, throwingRead);
expect(result.ok).toBe(false);
expect(result.detail).toContain("permission denied");
});
});

it("doctor flags a malformed config file (#4873)", () => {
const env = { GITTENSORY_MINER_CONFIG_DIR: join(tempRoot(), "state") };
const cwd = tempRoot();
writeFileSync(join(cwd, ".gittensory-miner.yml"), "wantedPaths: not-a-list\n");
expect(runDoctorChecks(env, cwd).find((check) => check.name === "config-content")?.ok).toBe(false);
expect(runDoctor([], env, cwd)).toBe(1);
});

it("engine version skew helpers compare installed vs expected semver", () => {
expect(compareInstalledEngineVersion("0.2.0", "0.2.0")).toBe(0);
expect(compareInstalledEngineVersion("0.1.0", "0.2.0")).toBe(-1);
Expand Down