Skip to content

Commit 912adcf

Browse files
committed
fix(export): apply the version gate to advanced options too
Picking an older Kitty is a promise the exported file will load in it, and Advanced was writing its options whatever was selected. Twelve of them, watcher and menu_map and action_alias among others, went out as directives to a Kitty that has never heard of them, which it refuses on the first unknown name. The gate is the one the other sections already use, so they arrive commented with the version that introduced them, like everything else. An option that writes a line per entry was also losing all but the first when held back, because only the first was being commented and the rest were dropped without a word. The suite runs every option through every supported version, which is how both turned up.
1 parent a5c4d38 commit 912adcf

2 files changed

Lines changed: 141 additions & 4 deletions

File tree

src/services/kitty-generator.service.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ export class KittyGeneratorService {
195195
continue;
196196
}
197197

198+
// Advanced was writing these whatever Kitty was selected, so picking an
199+
// older one still produced a file it refuses on the first unknown name.
200+
if (!this.versionService.isOptionAvailable(key)) {
201+
this.collectUnavailableOption(key, advanced, defaults, lines);
202+
continue;
203+
}
204+
198205
this.collectConfigLine(key, advanced[key], defaults[key], lines);
199206
}
200207

@@ -308,10 +315,12 @@ export class KittyGeneratorService {
308315
if (!requirement || !this.isDifferent(current[key], defaults[key])) return;
309316

310317
const formatted = this.formatValueForKey(key, current[key]);
311-
if (formatted) {
312-
lines.push(
313-
`# ${formatted[0]} # Requires Kitty >= ${requirement.minVersion}`,
314-
);
318+
if (!formatted) return;
319+
320+
// Every line, not just the first: an option like watcher writes one per
321+
// entry, and commenting only the first dropped the rest without saying so.
322+
for (const line of formatted) {
323+
lines.push(`# ${line} # Requires Kitty >= ${requirement.minVersion}`);
315324
}
316325
}
317326

src/version-gating.spec.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { TestBed } from "@angular/core/testing";
2+
import { beforeEach, describe, expect, it } from "vitest";
3+
import { DEFAULT_KITTY_CONFIG } from "./models/kitty-defaults";
4+
import type { KittyConfigAST } from "./models/kitty-types";
5+
import { KittyGeneratorService } from "./services/kitty-generator.service";
6+
import { KittyVersionService } from "./services/kitty-version.service";
7+
8+
/**
9+
* Picking an older Kitty is a promise that the exported file will load in it.
10+
* An option that release does not have has to arrive commented, carrying the
11+
* version that introduced it, rather than as a directive that release will
12+
* reject.
13+
*/
14+
describe("version gating", () => {
15+
let generator: KittyGeneratorService;
16+
let versions: KittyVersionService;
17+
18+
beforeEach(() => {
19+
TestBed.resetTestingModule();
20+
generator = TestBed.inject(KittyGeneratorService);
21+
versions = TestBed.inject(KittyVersionService);
22+
});
23+
24+
/** Every option in a section, paired with the section holding it. */
25+
const OPTIONS = Object.entries(
26+
DEFAULT_KITTY_CONFIG as unknown as Record<string, unknown>,
27+
).flatMap(([section, values]) =>
28+
values && typeof values === "object" && !Array.isArray(values)
29+
? Object.keys(values as Record<string, unknown>).map(
30+
(key) => [section, key] as const,
31+
)
32+
: [],
33+
);
34+
35+
/** A different value of the same shape, so the option is written at all. */
36+
function moved(value: unknown): unknown {
37+
if (typeof value === "boolean") return !value;
38+
if (typeof value === "number") return value + 1;
39+
if (typeof value === "string") return value === "yes" ? "no" : `${value}x`;
40+
if (Array.isArray(value)) return [...value, "extra"];
41+
return value;
42+
}
43+
44+
function everythingChanged(): KittyConfigAST {
45+
const config = structuredClone(DEFAULT_KITTY_CONFIG) as unknown as Record<
46+
string,
47+
Record<string, unknown>
48+
>;
49+
for (const [section, key] of OPTIONS) {
50+
const slice = config[section];
51+
if (slice) slice[key] = moved(slice[key]);
52+
}
53+
return config as unknown as KittyConfigAST;
54+
}
55+
56+
/** Directives only: a commented line is the thing being checked for. */
57+
function liveDirectives(text: string): Set<string> {
58+
const keys = new Set<string>();
59+
for (const line of text.split("\n")) {
60+
const trimmed = line.trim();
61+
if (!trimmed || trimmed.startsWith("#")) continue;
62+
const key = trimmed.split(/\s+/)[0];
63+
if (key) keys.add(key);
64+
}
65+
return keys;
66+
}
67+
68+
it("has something to gate on, or the rest of this proves nothing", () => {
69+
const gated = OPTIONS.filter(([, key]) =>
70+
versions.getVersionRequirement(key),
71+
);
72+
expect(gated.length).toBeGreaterThan(5);
73+
});
74+
75+
it.each(TestBed.inject(KittyVersionService).versions.map((v) => v.version))(
76+
"writes nothing Kitty %s would reject",
77+
(version) => {
78+
versions.setVersion(version);
79+
const text = generator.generateConfig(everythingChanged());
80+
const live = liveDirectives(text);
81+
82+
const tooNew = OPTIONS.filter(([, key]) => {
83+
const requirement = versions.getVersionRequirement(key);
84+
return requirement && !versions.isOptionAvailable(key) && live.has(key);
85+
}).map(([, key]) => key);
86+
87+
expect(tooNew).toEqual([]);
88+
},
89+
);
90+
91+
/**
92+
* Modelled so the editor can offer a control, but written as part of another
93+
* directive rather than one of their own: `env read_from_shell` and
94+
* `confirm_os_window_close N count-background`.
95+
*/
96+
const FOLDED_INTO_ANOTHER_LINE = new Set([
97+
"env_read_from_shell",
98+
"confirm_os_window_close_count_background",
99+
]);
100+
101+
it("still writes the options that release does have", () => {
102+
versions.setVersion("0.48.0");
103+
const live = liveDirectives(generator.generateConfig(everythingChanged()));
104+
105+
const missing = OPTIONS.filter(([, key]) => {
106+
if (FOLDED_INTO_ANOTHER_LINE.has(key)) return false;
107+
const requirement = versions.getVersionRequirement(key);
108+
return requirement && versions.isOptionAvailable(key) && !live.has(key);
109+
}).map(([, key]) => key);
110+
111+
expect(missing).toEqual([]);
112+
});
113+
114+
it("says which Kitty an option it held back needs", () => {
115+
versions.setVersion("0.15.0");
116+
const text = generator.generateConfig(everythingChanged());
117+
118+
const gatedLines = text
119+
.split("\n")
120+
.filter((line) => line.includes("Requires Kitty >="));
121+
122+
expect(gatedLines.length).toBeGreaterThan(0);
123+
for (const line of gatedLines) {
124+
expect(line.trim().startsWith("#")).toBe(true);
125+
expect(line).toMatch(/Requires Kitty >= \d+\.\d+/);
126+
}
127+
});
128+
});

0 commit comments

Comments
 (0)