fix(housekeeping): isolate executable command schemas
This commit is contained in:
1 parent
00618fb2a3
commit
7895188b56
5 files changed
+424
-60
No files matched your search
@@ -1,10 +1,58 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { anyCapability, ok } from "../contracts";
|
import { anyCapability, ok } from "../contracts";
|
||||||
import { housekeepingCommandRegistryReady } from "./bootstrap";
|
import {
|
||||||
import { registerHousekeepingCommand } from "./registry";
|
defineHousekeepingCommands,
|
||||||
|
housekeepingCommandRegistryReady,
|
||||||
|
registerHousekeepingCommands,
|
||||||
|
} from "./bootstrap";
|
||||||
|
import {
|
||||||
|
type HousekeepingCommand,
|
||||||
|
registerHousekeepingCommand,
|
||||||
|
} from "./registry";
|
||||||
|
|
||||||
describe("housekeeping command bootstrap", () => {
|
describe("housekeeping command bootstrap", () => {
|
||||||
|
it("preserves heterogeneous concrete input and output types", () => {
|
||||||
|
const stringCommand: HousekeepingCommand<
|
||||||
|
{ value: string },
|
||||||
|
{ length: number }
|
||||||
|
> = {
|
||||||
|
id: "system.bootstrap.string-shape",
|
||||||
|
owner: "system",
|
||||||
|
risk: "safe",
|
||||||
|
capability: anyCapability("admin.settings.view"),
|
||||||
|
input: z.object({ value: z.string() }),
|
||||||
|
requiresReason: false,
|
||||||
|
rateLimit: { attempts: 1, windowMs: 1_000 },
|
||||||
|
execute: async (context, input) =>
|
||||||
|
ok({ length: input.value.length }, context.correlationId),
|
||||||
|
};
|
||||||
|
const numberCommand: HousekeepingCommand<
|
||||||
|
{ count: number },
|
||||||
|
{ doubled: number }
|
||||||
|
> = {
|
||||||
|
id: "system.bootstrap.number-shape",
|
||||||
|
owner: "system",
|
||||||
|
risk: "safe",
|
||||||
|
capability: anyCapability("admin.settings.view"),
|
||||||
|
input: z.object({ count: z.number().int() }),
|
||||||
|
requiresReason: false,
|
||||||
|
rateLimit: { attempts: 2, windowMs: 2_000 },
|
||||||
|
execute: async (context, input) =>
|
||||||
|
ok({ doubled: input.count * 2 }, context.correlationId),
|
||||||
|
};
|
||||||
|
|
||||||
|
const commands = defineHousekeepingCommands(stringCommand, numberCommand);
|
||||||
|
const stringInput: z.input<(typeof commands)[0]["input"]> = {
|
||||||
|
value: "typed",
|
||||||
|
};
|
||||||
|
const numberInput: z.input<(typeof commands)[1]["input"]> = { count: 4 };
|
||||||
|
expect(commands[0].input.parse(stringInput)).toEqual({ value: "typed" });
|
||||||
|
expect(commands[1].input.parse(numberInput)).toEqual({ count: 4 });
|
||||||
|
expect(() => registerHousekeepingCommands(commands)).toThrow(
|
||||||
|
"command registry is sealed",
|
||||||
|
);
|
||||||
|
});
|
||||||
it("registers the complete current list and seals during module initialization", () => {
|
it("registers the complete current list and seals during module initialization", () => {
|
||||||
expect(housekeepingCommandRegistryReady).toBe(true);
|
expect(housekeepingCommandRegistryReady).toBe(true);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
|
|||||||
@@ -6,12 +6,37 @@ import {
|
|||||||
sealHousekeepingCommandRegistry,
|
sealHousekeepingCommandRegistry,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
|
|
||||||
const currentHousekeepingCommands =
|
type PreserveHousekeepingCommand<Definition> =
|
||||||
[] as const satisfies readonly HousekeepingCommand<unknown, unknown>[];
|
Definition extends HousekeepingCommand<infer _Input, infer _Output>
|
||||||
|
? Definition
|
||||||
|
: never;
|
||||||
|
|
||||||
for (const command of currentHousekeepingCommands) {
|
type HousekeepingCommandTuple<Commands extends readonly unknown[]> =
|
||||||
registerHousekeepingCommand(command);
|
Commands & {
|
||||||
|
readonly [Index in keyof Commands]: PreserveHousekeepingCommand<
|
||||||
|
Commands[Index]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function defineHousekeepingCommands<
|
||||||
|
const Commands extends readonly unknown[],
|
||||||
|
>(...commands: HousekeepingCommandTuple<Commands>): Commands {
|
||||||
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerHousekeepingCommands<
|
||||||
|
const Commands extends readonly unknown[],
|
||||||
|
>(commands: HousekeepingCommandTuple<Commands>): void {
|
||||||
|
for (const command of commands) {
|
||||||
|
registerHousekeepingCommand(
|
||||||
|
command as unknown as HousekeepingCommand<unknown, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentHousekeepingCommands = defineHousekeepingCommands();
|
||||||
|
|
||||||
|
registerHousekeepingCommands(currentHousekeepingCommands);
|
||||||
sealHousekeepingCommandRegistry();
|
sealHousekeepingCommandRegistry();
|
||||||
|
|
||||||
export const housekeepingCommandRegistryReady = true;
|
export const housekeepingCommandRegistryReady = true;
|
||||||
@@ -385,7 +385,7 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
]);
|
]);
|
||||||
expect(audit.entries[1]?.correlationId).toBe(result.correlationId);
|
expect(audit.entries[1]?.correlationId).toBe(result.correlationId);
|
||||||
});
|
});
|
||||||
it("runs sensitive validation and rate preflight before intent and execution", async () => {
|
it("runs sensitive capability and rate preflight before intent and execution", async () => {
|
||||||
const events: string[] = [];
|
const events: string[] = [];
|
||||||
const audit = auditRecorder(events);
|
const audit = auditRecorder(events);
|
||||||
const tracedContext = {
|
const tracedContext = {
|
||||||
@@ -399,9 +399,7 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
baseCommand("system.dispatch.preflight-order", {
|
baseCommand("system.dispatch.preflight-order", {
|
||||||
risk: "sensitive",
|
risk: "sensitive",
|
||||||
requiresReason: true,
|
requiresReason: true,
|
||||||
input: z.object({ enabled: z.boolean() }).superRefine(() => {
|
input: z.object({ enabled: z.boolean() }),
|
||||||
events.push("input");
|
|
||||||
}),
|
|
||||||
execute: async (context) => {
|
execute: async (context) => {
|
||||||
events.push("execute");
|
events.push("execute");
|
||||||
return ok(null, context.correlationId);
|
return ok(null, context.correlationId);
|
||||||
@@ -427,7 +425,6 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
|
|
||||||
expect(events).toEqual([
|
expect(events).toEqual([
|
||||||
"capability",
|
"capability",
|
||||||
"input",
|
|
||||||
"rate",
|
"rate",
|
||||||
"audit:intent",
|
"audit:intent",
|
||||||
"execute",
|
"execute",
|
||||||
@@ -566,15 +563,19 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
const audit = auditRecorder();
|
const audit = auditRecorder();
|
||||||
register(
|
register(
|
||||||
baseCommand("system.dispatch.schema-exception", {
|
baseCommand("system.dispatch.schema-exception", {
|
||||||
input: z.preprocess(() => {
|
input: z.object({ value: z.string() }),
|
||||||
throw new Error("schema secret exposed");
|
|
||||||
}, z.object({})),
|
|
||||||
execute: async (context) => ok(null, context.correlationId),
|
execute: async (context) => ok(null, context.correlationId),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const throwingInput = Object.defineProperty({}, "value", {
|
||||||
|
enumerable: true,
|
||||||
|
get: () => {
|
||||||
|
throw new Error("schema secret exposed");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const result = await dispatchHousekeepingCommand(
|
const result = await dispatchHousekeepingCommand(
|
||||||
{ commandId: "system.dispatch.schema-exception", input: {} },
|
{ commandId: "system.dispatch.schema-exception", input: throwingInput },
|
||||||
dependencies({ audit: audit.writer }),
|
dependencies({ audit: audit.writer }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user