-
Notifications
You must be signed in to change notification settings - Fork 1
0.1b — validate lab.toml on extension load with field-precise errors (closes #16) #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import * as fs from "node:fs"; | ||
| import * as path from "node:path"; | ||
| import * as os from "node:os"; | ||
| import { validateFile, type Validation } from "@amicode/schema"; | ||
|
|
||
| // ============================================================================ | ||
| // lab.toml load-time validation (0.1b / S17). A partner lab's lab.toml is the | ||
| // only place hardware params enter a solve; validating it on extension load | ||
| // turns a malformed/mistyped config into a field-precise error (offending key + | ||
| // path) instead of a silent solve against the wrong hardware or an opaque | ||
| // mid-solve failure. The schema itself lives in the shared @amicode/schema | ||
| // package (0.1a) — this is only the resolve-path + load + surface seam. | ||
| // ============================================================================ | ||
|
|
||
| /** Resolve the lab.toml the extension validates on load. Empty config → | ||
| * ~/.amico/lab.toml (where install.sh writes the starter). A leading ~ is | ||
| * expanded, mirroring resolveRunsRoot / resolveJuliaProject. */ | ||
| export function resolveLabTomlPath(configValue: string): string { | ||
| const v = (configValue ?? "").trim(); | ||
| if (v === "") return path.join(os.homedir(), ".amico", "lab.toml"); | ||
| if (v === "~") return os.homedir(); | ||
| if (v.startsWith("~/")) return path.join(os.homedir(), v.slice(2)); | ||
| return v; | ||
| } | ||
|
|
||
| export type LabCheck = | ||
| | { state: "absent"; path: string } | ||
| | { state: "valid"; path: string } | ||
| | { state: "invalid"; path: string; errors: string[] }; | ||
|
|
||
| /** Validate the lab.toml at `labPath` against the shared lab schema. A missing | ||
| * file is `absent` (not an error — a lab may be provisioned later); a present | ||
| * file is validated field-precise via the single @amicode/schema validator. */ | ||
| export function checkLabToml(labPath: string): LabCheck { | ||
| if (!fs.existsSync(labPath)) return { state: "absent", path: labPath }; | ||
| const v: Validation = validateFile(labPath, "lab"); | ||
| return v.ok ? { state: "valid", path: labPath } : { state: "invalid", path: labPath, errors: v.errors }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { mkdtempSync, writeFileSync } from "node:fs"; | ||
| import { tmpdir, homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { validateFile } from "@amicode/schema"; | ||
| import { resolveLabTomlPath, checkLabToml } from "../src/lab_config"; | ||
|
|
||
| const VALID = | ||
| 'schema_version = "1"\n[lab]\nname = "demo-lab"\n' + | ||
| "[transmon]\nomega_GHz = 5.0\ndelta_GHz = 0.2\nlevels = 3\ndrive_max_GHz = 0.2\n"; | ||
|
|
||
| function writeLab(content: string): string { | ||
| const p = join(mkdtempSync(join(tmpdir(), "lab-")), "lab.toml"); | ||
| writeFileSync(p, content); | ||
| return p; | ||
| } | ||
| function errs(content: string): string[] { | ||
| const c = checkLabToml(writeLab(content)); | ||
| return c.state === "invalid" ? c.errors : []; | ||
| } | ||
| const has = (es: string[], needle: string) => es.some((e) => e.includes(needle)); | ||
|
|
||
| describe("resolveLabTomlPath", () => { | ||
| it("defaults to ~/.amico/lab.toml", () => | ||
| expect(resolveLabTomlPath("")).toBe(join(homedir(), ".amico", "lab.toml"))); | ||
| it("expands a leading ~", () => { | ||
| expect(resolveLabTomlPath("~")).toBe(homedir()); | ||
| expect(resolveLabTomlPath("~/x/lab.toml")).toBe(join(homedir(), "x", "lab.toml")); | ||
| }); | ||
| it("uses an explicit path, trimmed", () => | ||
| expect(resolveLabTomlPath(" /a/lab.toml ")).toBe("/a/lab.toml")); | ||
| }); | ||
|
|
||
| describe("checkLabToml", () => { | ||
| it("a missing file is `absent`, not an error (a lab may be provisioned later)", () => | ||
| expect(checkLabToml(join(tmpdir(), "definitely-absent-lab-dir", "lab.toml")).state).toBe("absent")); | ||
| it("a conforming lab.toml is `valid`", () => | ||
| expect(checkLabToml(writeLab(VALID)).state).toBe("valid")); | ||
|
|
||
| // field-precise negative matrix (#16 ACs / S17) | ||
| it("missing required key → names the absent key + path", () => | ||
| expect(has(errs(VALID.replace("drive_max_GHz = 0.2\n", "")), 'missing required key "drive_max_GHz"')).toBe(true)); | ||
| it("wrong type → names the offending key", () => | ||
| expect(has(errs(VALID.replace("levels = 3", 'levels = "three"')), "/transmon/levels: must be integer")).toBe(true)); | ||
| it("out-of-range → names the offending key (distinct from wrong-type)", () => | ||
| expect(has(errs(VALID.replace("levels = 3", "levels = 99")), "/transmon/levels: must be <= 10")).toBe(true)); | ||
| it("unknown / misspelled key → names that key", () => | ||
| expect(has(errs(VALID + "rogue = 1\n"), 'unknown key "rogue"')).toBe(true)); | ||
| it("absent schema_version → field-precise required error", () => | ||
| expect(has(errs(VALID.replace('schema_version = "1"\n', "")), 'missing required key "schema_version"')).toBe(true)); | ||
| it("unrecognized schema_version → version-specific error", () => | ||
| expect(has(errs(VALID.replace('schema_version = "1"', 'schema_version = "9"')), "/schema_version: unrecognized version")).toBe(true)); | ||
|
|
||
| it("hardware range bounds are field-precise (#29: omega/drive_max/delta) + name minLength", () => { | ||
| expect(has(errs(VALID.replace("omega_GHz = 5.0", "omega_GHz = 999")), "/transmon/omega_GHz: must be <= 100")).toBe(true); | ||
| expect(has(errs(VALID.replace("drive_max_GHz = 0.2", "drive_max_GHz = 50")), "/transmon/drive_max_GHz: must be <= 10")).toBe(true); | ||
| expect(has(errs(VALID.replace("delta_GHz = 0.2", "delta_GHz = 25")), "/transmon/delta_GHz: must be <= 2")).toBe(true); // garbage anharmonicity | ||
| expect(has(errs(VALID.replace('name = "demo-lab"', 'name = ""')), "/lab/name")).toBe(true); // minLength | ||
| }); | ||
|
|
||
| it("parity over a corpus: checkLabToml === @amicode/schema.validateFile on every input (no second path) [#16]", () => { | ||
| const corpus = [ | ||
| VALID, // valid | ||
| VALID.replace("drive_max_GHz = 0.2\n", ""), // missing required | ||
| VALID.replace("levels = 3", 'levels = "three"'), // wrong type | ||
| VALID.replace("levels = 3", "levels = 99"), // out of range | ||
| VALID.replace("delta_GHz = 0.2", "delta_GHz = 25"), // out of range (delta) | ||
| VALID + "rogue = 1\n", // unknown key | ||
| VALID.replace('schema_version = "1"\n', ""), // absent version | ||
| VALID.replace('schema_version = "1"', 'schema_version = "9"'), // unrecognized version | ||
| ]; | ||
| for (const content of corpus) { | ||
| const p = writeLab(content); | ||
| const c = checkLabToml(p); | ||
| const direct = validateFile(p, "lab"); | ||
| expect(c.state === "valid" ? [] : (c as { errors: string[] }).errors).toEqual(direct.errors); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("valid lab profiles conform (demo + Schuster)", () => { | ||
| it("scripts/lab.toml.example (the shipped starter) validates clean", () => | ||
| expect(validateFile(join(__dirname, "..", "scripts", "lab.toml.example"), "lab").errors).toEqual([])); | ||
| it("a Schuster-profile lab (negative-convention δ, 4 levels) validates clean", () => { | ||
| // Distinct from demo-lab: negative anharmonicity convention + a 4-level model, | ||
| // exercising the schema's range tolerance on a second real-shaped profile. | ||
| const schuster = 'schema_version = "1"\n[lab]\nname = "schuster-transmon"\n' + | ||
| "[transmon]\nomega_GHz = 4.8\ndelta_GHz = -0.33\nlevels = 4\ndrive_max_GHz = 0.1\n"; | ||
| expect(checkLabToml(writeLab(schuster)).state).toBe("valid"); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] this
showErrorMessage+ output-channel path is the one surface the unit tests can't reach — no VS Code host in CI, socheckLabTomlis covered but the toast/channel wiring isn't. I'll verify it by hand on the packaged VSIX (bad~/.amico/lab.toml→ reload → toast + "Amicode — runs" list). Non-blocking — but it's the seam Phase 1's UI work compounds on, so it wants a host-level test approach soon.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged — the toast/channel wiring stays unit-untestable (no VS Code host in CI). Noted as a Phase-1 prerequisite (a host-level/integration-test approach before the inspector/catalog UI compounds on this seam); not adding host-test infra in this slice.
checkLabTomlitself is covered.