mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-18 13:37:23 +02:00
fix(hosts): validate suppressed resolver names
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import { ALL_HOST_CONFIGS, getHostConfig, ALL_HOST_NAMES } from '../hosts/index';
|
import { ALL_HOST_CONFIGS, getHostConfig, ALL_HOST_NAMES } from '../hosts/index';
|
||||||
import { validateAllConfigs } from './host-config';
|
import { validateAllConfigs } from './host-config';
|
||||||
|
import { RESOLVERS } from './resolvers';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
const CLI_REGEX = /^[a-z][a-z0-9_-]*$/;
|
const CLI_REGEX = /^[a-z][a-z0-9_-]*$/;
|
||||||
@@ -82,7 +83,7 @@ switch (command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'validate': {
|
case 'validate': {
|
||||||
const errors = validateAllConfigs(ALL_HOST_CONFIGS);
|
const errors = validateAllConfigs(ALL_HOST_CONFIGS, new Set(Object.keys(RESOLVERS)));
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
for (const error of errors) {
|
for (const error of errors) {
|
||||||
console.error(`ERROR: ${error}`);
|
console.error(`ERROR: ${error}`);
|
||||||
|
|||||||
+15
-3
@@ -117,7 +117,7 @@ const NAME_REGEX = /^[a-z][a-z0-9-]*$/;
|
|||||||
const PATH_REGEX = /^[a-zA-Z0-9_.\/${}~-]+$/;
|
const PATH_REGEX = /^[a-zA-Z0-9_.\/${}~-]+$/;
|
||||||
const CLI_REGEX = /^[a-z][a-z0-9_-]*$/;
|
const CLI_REGEX = /^[a-z][a-z0-9_-]*$/;
|
||||||
|
|
||||||
export function validateHostConfig(config: HostConfig): string[] {
|
export function validateHostConfig(config: HostConfig, validResolverNames?: ReadonlySet<string>): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!NAME_REGEX.test(config.name)) {
|
if (!NAME_REGEX.test(config.name)) {
|
||||||
@@ -152,15 +152,27 @@ export function validateHostConfig(config: HostConfig): string[] {
|
|||||||
errors.push(`install.linkingStrategy must be 'real-dir-symlink' or 'symlink-generated'`);
|
errors.push(`install.linkingStrategy must be 'real-dir-symlink' or 'symlink-generated'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cross-check suppressedResolvers against the known resolver names (injected to avoid a
|
||||||
|
// circular import on the resolver registry). A typo would otherwise silently no-op: the
|
||||||
|
// generator short-circuits suppressed names before the "unknown placeholder" throw, so an
|
||||||
|
// unknown entry never surfaces at generation time either.
|
||||||
|
if (validResolverNames && config.suppressedResolvers) {
|
||||||
|
for (const name of config.suppressedResolvers) {
|
||||||
|
if (!validResolverNames.has(name)) {
|
||||||
|
errors.push(`suppressedResolvers entry '${name}' is not a known resolver`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAllConfigs(configs: HostConfig[]): string[] {
|
export function validateAllConfigs(configs: HostConfig[], validResolverNames?: ReadonlySet<string>): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Per-config validation
|
// Per-config validation
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
const configErrors = validateHostConfig(config);
|
const configErrors = validateHostConfig(config, validResolverNames);
|
||||||
errors.push(...configErrors.map(e => `[${config.name}] ${e}`));
|
errors.push(...configErrors.map(e => `[${config.name}] ${e}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import {
|
|||||||
openclaw,
|
openclaw,
|
||||||
} from '../hosts/index';
|
} from '../hosts/index';
|
||||||
import { HOST_PATHS } from '../scripts/resolvers/types';
|
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||||
|
import { RESOLVERS } from '../scripts/resolvers';
|
||||||
|
|
||||||
const ROOT = path.resolve(import.meta.dir, '..');
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
|
const RESOLVER_NAMES = new Set(Object.keys(RESOLVERS));
|
||||||
|
|
||||||
// ─── hosts/index.ts ─────────────────────────────────────────
|
// ─── hosts/index.ts ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -205,13 +207,32 @@ describe('validateHostConfig', () => {
|
|||||||
c.cliCommand = 'opencode;rm -rf /';
|
c.cliCommand = 'opencode;rm -rf /';
|
||||||
expect(validateHostConfig(c).some(e => e.includes('cliCommand'))).toBe(true);
|
expect(validateHostConfig(c).some(e => e.includes('cliCommand'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('valid suppressedResolvers pass when resolver names provided', () => {
|
||||||
|
const c = makeValid();
|
||||||
|
c.suppressedResolvers = ['DESIGN_OUTSIDE_VOICES', 'REVIEW_ARMY'];
|
||||||
|
expect(validateHostConfig(c, RESOLVER_NAMES)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown suppressedResolvers entry is caught', () => {
|
||||||
|
const c = makeValid();
|
||||||
|
c.suppressedResolvers = ['DESIGN_OUTSIDE_VOICES', 'NONEXISTENT_RESOLVER'];
|
||||||
|
const errors = validateHostConfig(c, RESOLVER_NAMES);
|
||||||
|
expect(errors.some(e => e.includes('NONEXISTENT_RESOLVER'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suppressedResolvers unchecked when resolver names omitted', () => {
|
||||||
|
const c = makeValid();
|
||||||
|
c.suppressedResolvers = ['TYPO_RESOLVER'];
|
||||||
|
expect(validateHostConfig(c)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── validateAllConfigs ─────────────────────────────────────
|
// ─── validateAllConfigs ─────────────────────────────────────
|
||||||
|
|
||||||
describe('validateAllConfigs', () => {
|
describe('validateAllConfigs', () => {
|
||||||
test('real configs all pass validation', () => {
|
test('real configs all pass validation', () => {
|
||||||
const errors = validateAllConfigs(ALL_HOST_CONFIGS);
|
const errors = validateAllConfigs(ALL_HOST_CONFIGS, RESOLVER_NAMES);
|
||||||
expect(errors).toEqual([]);
|
expect(errors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,6 +254,12 @@ describe('validateAllConfigs', () => {
|
|||||||
expect(errors.some(e => e.includes('Duplicate globalRoot'))).toBe(true);
|
expect(errors.some(e => e.includes('Duplicate globalRoot'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('unknown suppressedResolvers entry surfaces with host-name prefix', () => {
|
||||||
|
const bad = { ...codex, name: 'bad-host', hostSubdir: '.bad', globalRoot: '.bad/skills/gstack', suppressedResolvers: ['BOGUS_RESOLVER'] } as HostConfig;
|
||||||
|
const errors = validateAllConfigs([bad], RESOLVER_NAMES);
|
||||||
|
expect(errors.some(e => e.startsWith('[bad-host]') && e.includes('BOGUS_RESOLVER'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test('per-config validation errors are prefixed with host name', () => {
|
test('per-config validation errors are prefixed with host name', () => {
|
||||||
const bad = { ...codex, name: 'BAD', cliCommand: 'also bad' } as HostConfig;
|
const bad = { ...codex, name: 'BAD', cliCommand: 'also bad' } as HostConfig;
|
||||||
const errors = validateAllConfigs([bad]);
|
const errors = validateAllConfigs([bad]);
|
||||||
|
|||||||
Reference in New Issue
Block a user