Skip to content

Commit b932d40

Browse files
committed
🧪 Reproduce widened dynamic extension types
1 parent ba05db5 commit b932d40

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

‎test/dynamic.test.ts‎

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { option } from "../lib/option.ts";
1515
import { schema } from "../lib/param.ts";
1616
import { parse } from "../lib/parse.ts";
1717
import { printHelp } from "../lib/print.ts";
18+
import { cli } from "../lib/read.ts";
1819
import { route, routes, version } from "../lib/route.ts";
1920
import type {
2021
AnyRoute,
@@ -147,6 +148,190 @@ describe("dynamic()", () => {
147148
>(true);
148149
});
149150

151+
it("preserves a concrete command supplied through dynamic input", () => {
152+
let plugin = command(
153+
name("serve"),
154+
option(name("port"), schema(type("number"))),
155+
);
156+
type Plugin = typeof plugin;
157+
let app = command(
158+
name("simulacrum"),
159+
dynamic((plugin: Plugin) => extend(routes(plugin))),
160+
);
161+
type Next = ContinuationOf<typeof app>;
162+
163+
expectType<Equal<RequirementOf<typeof app>, Plugin>>(true);
164+
expectType<Equal<ChildrenOf<Next>[0]["name"], "serve">>(true);
165+
expectType<Equal<ModelOf<ChildrenOf<Next>[0]>, { port: number }>>(true);
166+
167+
let first = parse(app, {
168+
argv: ["serve", "--port", "4100"],
169+
});
170+
assertIncrement(first, {});
171+
172+
let result = first.resume({ ok: true, value: plugin });
173+
expect(result).toMatchObject({
174+
ok: true,
175+
method: "execute",
176+
route: "/serve",
177+
model: { port: 4100 },
178+
});
179+
});
180+
181+
it("preserves the resume boundary for runtime-sized command extensions", () => {
182+
let app = command(
183+
name("simulacrum"),
184+
dynamic((plugins: PluginSet) => extend(routes(...plugins.commands))),
185+
);
186+
187+
expectType<Equal<RequirementOf<typeof app>, PluginSet>>(true);
188+
expectType<Equal<ParseIncrement<typeof app>["model"], {}>>(true);
189+
190+
let first = parse(app, { argv: ["serve"] });
191+
assertIncrement(first, {});
192+
193+
let result = first.resume({
194+
ok: true,
195+
value: { commands: [command(name("serve"))] },
196+
});
197+
expect(result).toMatchObject({
198+
ok: true,
199+
method: "execute",
200+
route: "/serve",
201+
});
202+
});
203+
204+
it("preserves the resume boundary for runtime-sized extensions", () => {
205+
let app = command(
206+
name("xmd"),
207+
option(name("path"), schema(type("string"))),
208+
dynamic((declared: readonly string[]) =>
209+
extend(
210+
...declared.map((property) =>
211+
option(name(property), schema(type("string")))
212+
),
213+
)
214+
),
215+
option(name("raw"), schema(type("string | undefined"))),
216+
);
217+
218+
expectType<Equal<RequirementOf<typeof app>, readonly string[]>>(true);
219+
expectType<
220+
Equal<ParseIncrement<typeof app>["model"], {
221+
path: string;
222+
}>
223+
>(true);
224+
225+
type Continued = ModelOf<ContinuationOf<typeof app>>;
226+
expectType<Equal<Continued["path"], string>>(true);
227+
expectType<Equal<Continued["raw"], string | undefined>>(true);
228+
229+
let first = parse(app, {
230+
argv: ["--path", "doc.md", "--author", "Ada", "--raw", "true"],
231+
});
232+
assertIncrement(first, { path: "doc.md" });
233+
234+
let result = first.resume({ ok: true, value: ["author"] });
235+
expect(result).toMatchObject({
236+
ok: true,
237+
model: { path: "doc.md", author: "Ada", raw: "true" },
238+
});
239+
if (!result.ok || result.method !== "execute") {
240+
throw new Error("expected execute result");
241+
}
242+
expectType<
243+
Equal<typeof result.model, {
244+
path: string;
245+
raw: string | undefined;
246+
}>
247+
>(true);
248+
});
249+
250+
it("preserves the boundary for generated options with explicit CLI readers", () => {
251+
let generated = (property: string) =>
252+
option(
253+
name(property),
254+
cli([`--${property}`]),
255+
schema(type("string | undefined")),
256+
);
257+
let app = command(
258+
name("xmd"),
259+
option(
260+
name("file"),
261+
cli(["--file"]),
262+
schema(type("string")),
263+
),
264+
dynamic((frontmatter: Frontmatter) =>
265+
extend(...frontmatter.properties.map(generated))
266+
),
267+
option(
268+
name("raw"),
269+
cli(["--raw"], { switch: true }),
270+
schema(type("boolean | undefined")),
271+
),
272+
);
273+
274+
expectType<Equal<RequirementOf<typeof app>, Frontmatter>>(true);
275+
276+
let step = parse(app, {
277+
argv: ["--file", "doc.md", "--props-name", "Ada"],
278+
});
279+
if (!step.ok || !("resume" in step)) throw new Error("expected increment");
280+
expectType<Equal<typeof step.model, { file: string }>>(true);
281+
282+
let result = step.resume({
283+
ok: true,
284+
value: { properties: ["props-name"] },
285+
});
286+
if (!result.ok || result.method !== "execute") {
287+
throw new Error("expected execute result");
288+
}
289+
expect(result).toMatchObject({
290+
model: { file: "doc.md", "props-name": "Ada" },
291+
});
292+
expectType<
293+
Equal<typeof result.model, {
294+
file: string;
295+
raw: boolean | undefined;
296+
}>
297+
>(true);
298+
});
299+
300+
it("preserves the resume boundary for unknown requirements", () => {
301+
let app = command(
302+
name("xmd"),
303+
option(name("path"), schema(type("string"))),
304+
dynamic((input: unknown) => {
305+
let declared = Array.isArray(input)
306+
? input.filter((value): value is string => typeof value === "string")
307+
: [];
308+
return extend(
309+
...declared.map((property) =>
310+
option(name(property), schema(type("string")))
311+
),
312+
);
313+
}),
314+
);
315+
316+
expectType<Equal<RequirementOf<typeof app>, unknown>>(true);
317+
expectType<
318+
Equal<ParseIncrement<typeof app>["model"], {
319+
path: string;
320+
}>
321+
>(true);
322+
323+
let first = parse(app, {
324+
argv: ["--path", "doc.md", "--author", "Ada"],
325+
});
326+
assertIncrement(first, { path: "doc.md" });
327+
328+
let result = first.resume({ ok: true, value: ["author"] });
329+
expect(result).toMatchObject({
330+
ok: true,
331+
model: { path: "doc.md", author: "Ada" },
332+
});
333+
});
334+
150335
it("preserves route-level controls across phases", () => {
151336
let app = command(
152337
name("simulacrum"),
@@ -797,6 +982,14 @@ interface Plugins {
797982
readonly names: readonly string[];
798983
}
799984

985+
interface PluginSet {
986+
readonly commands: readonly AnyRoute[];
987+
}
988+
989+
interface Frontmatter {
990+
readonly properties: readonly string[];
991+
}
992+
800993
interface Services {
801994
readonly count: number;
802995
}

0 commit comments

Comments
 (0)