From 8697631c8bfde10448ef23c026d01983b22b82e0 Mon Sep 17 00:00:00 2001 From: Pratik Dhanave Date: Sun, 1 Nov 2026 07:30:21 +0530 Subject: [PATCH] fix(cli): clear error for empty/comment-only scenario, persona, traits YAML loadScenario/loadPersona/loadTraits in config-loader.ts dereference the parsed YAML immediately (data.task, data.personality, ...). The yaml parser returns null for an empty or comment-only file, so 'scope run' with such a scenario/persona/traits file crashed with an opaque 'TypeError: Cannot read properties of null (reading "task")' instead of the clear validation error each loader is written to raise. Guard for a null/non-object parse result in all three loaders and raise the loader's own validation message. Add config-loader.test.ts covering the empty and comment-only cases plus a valid-scenario sanity check. --- apps/cli/src/config-loader.test.ts | 52 ++++++++++++++++++++++++++++++ apps/cli/src/config-loader.ts | 14 ++++++++ 2 files changed, 66 insertions(+) create mode 100644 apps/cli/src/config-loader.test.ts diff --git a/apps/cli/src/config-loader.test.ts b/apps/cli/src/config-loader.test.ts new file mode 100644 index 000000000..497b4db19 --- /dev/null +++ b/apps/cli/src/config-loader.test.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect, afterAll } from "vitest"; +import { writeFileSync, mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { resolveScenarioAndPersona } from "./config-loader.js"; + +const dir = mkdtempSync(join(tmpdir(), "scope-config-loader-")); +afterAll(() => rmSync(dir, { recursive: true, force: true })); + +function write(name: string, content: string): string { + const path = join(dir, name); + writeFileSync(path, content, "utf-8"); + return path; +} + +const validScenario = write( + "scenario.yaml", + 'task: "summarize the file"\ncriteria:\n - correctness\n' +); + +describe("resolveScenarioAndPersona config validation", () => { + it("reports a clear error for an empty scenario file (not a TypeError)", () => { + const scenario = write("empty-scenario.yaml", ""); + expect(() => resolveScenarioAndPersona(scenario)).toThrowError( + /must have a "task" string field/ + ); + }); + + it("reports a clear error for a comment-only scenario file", () => { + const scenario = write("comment-scenario.yaml", "# nothing here\n"); + expect(() => resolveScenarioAndPersona(scenario)).toThrowError( + /must have a "task" string field/ + ); + }); + + it("reports a clear error for a comment-only persona file (not a TypeError)", () => { + const persona = write("comment-persona.yaml", "# just a comment\n"); + expect(() => resolveScenarioAndPersona(validScenario, persona)).toThrowError( + /is empty or not a YAML mapping/ + ); + }); + + it("still loads a valid scenario without a persona", () => { + const result = resolveScenarioAndPersona(validScenario); + expect(result.task).toBe("summarize the file"); + expect(result.criteria).toEqual(["correctness"]); + }); +}); diff --git a/apps/cli/src/config-loader.ts b/apps/cli/src/config-loader.ts index 425cb0cd8..0db371662 100644 --- a/apps/cli/src/config-loader.ts +++ b/apps/cli/src/config-loader.ts @@ -37,6 +37,10 @@ function loadPersona(filePath: string): Persona { const raw = readFileSync(filePath, "utf-8"); const data = parseYaml(raw); + if (!data || typeof data !== "object") { + throw new Error(`Persona file ${filePath} is empty or not a YAML mapping`); + } + const validPersonality: Personality[] = ["demanding", "friendly"]; const validExperience: Experience[] = ["junior", "senior"]; const validVerbosity: Verbosity[] = ["brief", "moderate"]; @@ -75,6 +79,10 @@ function loadScenario(filePath: string): Scenario { const raw = readFileSync(filePath, "utf-8"); const data = parseYaml(raw); + if (!data || typeof data !== "object") { + throw new Error(`Scenario file ${filePath} must have a "task" string field`); + } + if (!data.task || typeof data.task !== "string") { throw new Error(`Scenario file ${filePath} must have a "task" string field`); } @@ -108,6 +116,12 @@ function loadTraits(filePath: string): TraitDescriptions { const raw = readFileSync(filePath, "utf-8"); const data = parseYaml(raw); + if (!data || typeof data !== "object") { + throw new Error( + `Traits file ${filePath} must have personality, experience, verbosity, and type sections` + ); + } + if (!data.personality || !data.experience || !data.verbosity || !data.type) { throw new Error( `Traits file ${filePath} must have personality, experience, verbosity, and type sections`