Skip to content

Commit d6b3a56

Browse files
ondreianclaude
andcommitted
feat: add validate-files command for PR validation
Add new `validate-files` command to enable efficient validation of individual room files without requiring full mapdb reconstruction. This is essential for GitHub Actions PR validation workflows. Features: - Validate specific room.json files by path - Support both GitRoom format and direct room format - JSON output option for CI integration (--json flag) - Detailed error reporting with file paths and room info - Comprehensive test coverage Usage: bun run mapdb validate-files gs/rooms/123/room.json gs/rooms/456/room.json bun run mapdb validate-files --json changed-files.txt 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 870a512 commit d6b3a56

File tree

4 files changed

+245
-1
lines changed

4 files changed

+245
-1
lines changed

mapdb/mapdb.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,42 @@ program.command("validate")
7373
process.exit(0)
7474
})
7575

76+
program.command("validate-files")
77+
.alias("vf")
78+
.description("validate specific room files")
79+
.option("--dr", "run in dragonrealms mode", false)
80+
.option("--json", "output errors in JSON format", false)
81+
.argument("<files...>", "room.json file paths to validate")
82+
.action(async (files: string[], args: {dr: boolean, json: boolean}) => {
83+
const then = performance.now()
84+
const spinner = ora()
85+
86+
spinner.start(`validating ${files.length} room files...`)
87+
88+
try {
89+
const results = await Tasks.validateFiles({files, json: args.json})
90+
const runtime = Math.round(performance.now() - then)
91+
92+
if (results.errors.length === 0) {
93+
spinner.succeed(`[${runtime}ms] validated ${results.validFiles} files`)
94+
process.exit(0)
95+
} else {
96+
spinner.fail(`[${runtime}ms] found ${results.errors.length} validation errors`)
97+
98+
if (args.json) {
99+
console.log(JSON.stringify(results, null, 2))
100+
} else {
101+
console.table(results.errors)
102+
}
103+
process.exit(1)
104+
}
105+
} catch (error: any) {
106+
const runtime = Math.round(performance.now() - then)
107+
spinner.fail(`[${runtime}ms] ${error.message}`)
108+
process.exit(1)
109+
}
110+
})
111+
76112
program.command("reconstruct")
77113
.alias("r")
78114
.description("reconstruct a mapdb.json file from git directory structure")

mapdb/tasks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ import { download } from "./download"
22
import { validate } from "./validate"
33
import { git } from "./git"
44
import { reconstruct } from "./reconstruct"
5-
export { download, validate, git, reconstruct }
5+
import { validateFiles } from "./validate-files"
6+
export { download, validate, git, reconstruct, validateFiles }

mapdb/tasks/validate-files.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as fs from "node:fs/promises"
2+
import { Room } from "../room/room"
3+
import { fromError } from 'zod-validation-error'
4+
5+
export interface ValidateFilesConfig {
6+
files: string[];
7+
json: boolean;
8+
}
9+
10+
export interface FileValidationError {
11+
file: string;
12+
id?: number;
13+
title?: string;
14+
error: string;
15+
}
16+
17+
export interface ValidateFilesResult {
18+
validFiles: number;
19+
errors: FileValidationError[];
20+
files: string[];
21+
}
22+
23+
export async function validateFiles(config: ValidateFilesConfig): Promise<ValidateFilesResult> {
24+
const { files } = config
25+
const errors: FileValidationError[] = []
26+
let validFiles = 0
27+
28+
for (const file of files) {
29+
try {
30+
// Read the git room file
31+
const fileContent = await fs.readFile(file, 'utf-8')
32+
const gitRoom = JSON.parse(fileContent)
33+
34+
// Extract room data - handle both GitRoom format and direct room format
35+
const roomData = gitRoom.room || gitRoom
36+
37+
// Validate using Room class
38+
Room.validate(roomData)
39+
validFiles++
40+
41+
} catch (error: any) {
42+
let errorMessage = error.message
43+
let roomId: number | undefined
44+
let roomTitle: string | undefined
45+
46+
// Try to extract room info even if validation failed
47+
try {
48+
const fileContent = await fs.readFile(file, 'utf-8')
49+
const gitRoom = JSON.parse(fileContent)
50+
const roomData = gitRoom.room || gitRoom
51+
roomId = roomData.id
52+
roomTitle = roomData.title?.[0]
53+
} catch {
54+
// Ignore errors when trying to extract room info
55+
}
56+
57+
// If it's a Zod validation error, make it more readable
58+
if (error.name === 'ZodError') {
59+
const humanized = fromError(error)
60+
errorMessage = humanized.toString()
61+
}
62+
63+
errors.push({
64+
file,
65+
id: roomId,
66+
title: roomTitle,
67+
error: errorMessage
68+
})
69+
}
70+
}
71+
72+
return {
73+
validFiles,
74+
errors,
75+
files
76+
}
77+
}

test/validate-files.spec.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { expect, test, beforeAll, afterAll } from "bun:test"
2+
import * as Tasks from "../mapdb/tasks"
3+
import * as fs from "node:fs/promises"
4+
import * as path from "path"
5+
6+
test("validateFiles can validate valid room files", async () => {
7+
const testDir = "/tmp/test-validate-files"
8+
const validRoomFile = path.join(testDir, "valid-room.json")
9+
10+
try {
11+
// Clean up any existing test data
12+
await fs.rm(testDir, { recursive: true, force: true })
13+
await fs.mkdir(testDir, { recursive: true })
14+
15+
// Create a valid room file in GitRoom format
16+
const validRoom = {
17+
checksum: "abc123",
18+
room: {
19+
id: 123,
20+
title: ["Test Room"],
21+
description: ["A test room for validation."],
22+
terrain: "rough",
23+
wayto: {},
24+
timeto: {},
25+
location: "test"
26+
}
27+
}
28+
29+
await fs.writeFile(validRoomFile, JSON.stringify(validRoom, null, 2))
30+
31+
// Test validation
32+
const results = await Tasks.validateFiles({
33+
files: [validRoomFile],
34+
json: false
35+
})
36+
37+
expect(results.validFiles).toBe(1)
38+
expect(results.errors.length).toBe(0)
39+
expect(results.files).toEqual([validRoomFile])
40+
41+
} finally {
42+
// Clean up test files
43+
await fs.rm(testDir, { recursive: true, force: true })
44+
}
45+
})
46+
47+
test("validateFiles can detect invalid room files", async () => {
48+
const testDir = "/tmp/test-validate-files-invalid"
49+
const invalidRoomFile = path.join(testDir, "invalid-room.json")
50+
51+
try {
52+
// Clean up any existing test data
53+
await fs.rm(testDir, { recursive: true, force: true })
54+
await fs.mkdir(testDir, { recursive: true })
55+
56+
// Create an invalid room file (missing required fields)
57+
const invalidRoom = {
58+
checksum: "abc123",
59+
room: {
60+
id: "invalid", // Should be number
61+
title: ["Test Room"],
62+
// Missing wayto, timeto (required fields)
63+
}
64+
}
65+
66+
await fs.writeFile(invalidRoomFile, JSON.stringify(invalidRoom, null, 2))
67+
68+
// Test validation
69+
const results = await Tasks.validateFiles({
70+
files: [invalidRoomFile],
71+
json: true
72+
})
73+
74+
expect(results.validFiles).toBe(0)
75+
expect(results.errors.length).toBe(1)
76+
expect(results.errors[0].file).toBe(invalidRoomFile)
77+
expect(results.errors[0].id).toBe("invalid")
78+
expect(results.errors[0].error).toContain("Expected number, received string")
79+
80+
} finally {
81+
// Clean up test files
82+
await fs.rm(testDir, { recursive: true, force: true })
83+
}
84+
})
85+
86+
test("validateFiles can handle mixed valid and invalid files", async () => {
87+
const testDir = "/tmp/test-validate-files-mixed"
88+
const validFile = path.join(testDir, "valid.json")
89+
const invalidFile = path.join(testDir, "invalid.json")
90+
91+
try {
92+
await fs.rm(testDir, { recursive: true, force: true })
93+
await fs.mkdir(testDir, { recursive: true })
94+
95+
// Create valid room
96+
const validRoom = {
97+
checksum: "abc123",
98+
room: {
99+
id: 456,
100+
title: ["Valid Room"],
101+
description: ["A valid room."],
102+
wayto: {},
103+
timeto: {}
104+
}
105+
}
106+
107+
// Create invalid room
108+
const invalidRoom = {
109+
room: {
110+
id: 789,
111+
// Missing wayto, timeto
112+
}
113+
}
114+
115+
await fs.writeFile(validFile, JSON.stringify(validRoom, null, 2))
116+
await fs.writeFile(invalidFile, JSON.stringify(invalidRoom, null, 2))
117+
118+
const results = await Tasks.validateFiles({
119+
files: [validFile, invalidFile],
120+
json: false
121+
})
122+
123+
expect(results.validFiles).toBe(1)
124+
expect(results.errors.length).toBe(1)
125+
expect(results.files.length).toBe(2)
126+
127+
} finally {
128+
await fs.rm(testDir, { recursive: true, force: true })
129+
}
130+
})

0 commit comments

Comments
 (0)