fix(housekeeping): isolate executable command schemas
This commit is contained in:
1 parent
00618fb2a3
commit
7895188b56
5 files changed
+431
-67
No files matched your search
@@ -1,10 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { anyCapability, ok } from "../contracts";
|
||||
import { housekeepingCommandRegistryReady } from "./bootstrap";
|
||||
import { registerHousekeepingCommand } from "./registry";
|
||||
import {
|
||||
defineHousekeepingCommands,
|
||||
housekeepingCommandRegistryReady,
|
||||
registerHousekeepingCommands,
|
||||
} from "./bootstrap";
|
||||
import {
|
||||
type HousekeepingCommand,
|
||||
registerHousekeepingCommand,
|
||||
} from "./registry";
|
||||
|
||||
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", () => {
|
||||
expect(housekeepingCommandRegistryReady).toBe(true);
|
||||
expect(() =>
|
||||
|
||||
@@ -6,12 +6,37 @@ import {
|
||||
sealHousekeepingCommandRegistry,
|
||||
} from "./registry";
|
||||
|
||||
const currentHousekeepingCommands =
|
||||
[] as const satisfies readonly HousekeepingCommand<unknown, unknown>[];
|
||||
type PreserveHousekeepingCommand<Definition> =
|
||||
Definition extends HousekeepingCommand<infer _Input, infer _Output>
|
||||
? Definition
|
||||
: never;
|
||||
|
||||
for (const command of currentHousekeepingCommands) {
|
||||
registerHousekeepingCommand(command);
|
||||
type HousekeepingCommandTuple<Commands extends readonly unknown[]> =
|
||||
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();
|
||||
|
||||
export const housekeepingCommandRegistryReady = true;
|
||||
@@ -385,7 +385,7 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
]);
|
||||
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 audit = auditRecorder(events);
|
||||
const tracedContext = {
|
||||
@@ -399,9 +399,7 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
baseCommand("system.dispatch.preflight-order", {
|
||||
risk: "sensitive",
|
||||
requiresReason: true,
|
||||
input: z.object({ enabled: z.boolean() }).superRefine(() => {
|
||||
events.push("input");
|
||||
}),
|
||||
input: z.object({ enabled: z.boolean() }),
|
||||
execute: async (context) => {
|
||||
events.push("execute");
|
||||
return ok(null, context.correlationId);
|
||||
@@ -427,7 +425,6 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
|
||||
expect(events).toEqual([
|
||||
"capability",
|
||||
"input",
|
||||
"rate",
|
||||
"audit:intent",
|
||||
"execute",
|
||||
@@ -566,15 +563,19 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
const audit = auditRecorder();
|
||||
register(
|
||||
baseCommand("system.dispatch.schema-exception", {
|
||||
input: z.preprocess(() => {
|
||||
throw new Error("schema secret exposed");
|
||||
}, z.object({})),
|
||||
input: z.object({ value: z.string() }),
|
||||
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(
|
||||
{ commandId: "system.dispatch.schema-exception", input: {} },
|
||||
{ commandId: "system.dispatch.schema-exception", input: throwingInput },
|
||||
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