fix(coding-agent): coalesce models config resource probe
The models config resource regression started two cold child processes inside one five-second test. Under parallel CI chunk load, the second child could still be waiting for pipe drain or process exit after the validator had completed. Keep the process boundary as the owner of the lifecycle measurement and run the missing and custom phases in one child. The missing phase closes its storage and registry before the baseline snapshot, while the custom phase proves schema identity and retention without changing config loading or validator cleanup semantics. Signed-off-by: Christian Stewart <christian@aperture.us> (cherry picked from commit 81fa98491544c7fbcbf075922889e0a9e1a0a3b4)
This commit is contained in:
committed by
can1357
parent
44907cef75
commit
a43c7a4d36
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Coalesced the models configuration resource probe into one child process to avoid startup contention while preserving retained-resource coverage.
|
||||
|
||||
## [17.1.8] - 2026-07-28
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
+76
-34
@@ -9,14 +9,52 @@ interface HeapSnapshot {
|
||||
snapshot: { meta: { node_fields: string[] } };
|
||||
}
|
||||
|
||||
const root = process.argv[2];
|
||||
const mode = process.argv[3];
|
||||
if (!root || (mode !== "missing" && mode !== "custom")) {
|
||||
throw new Error("Expected an isolated config root and missing|custom mode");
|
||||
interface ProbeResult {
|
||||
retainedHeapNodes: number;
|
||||
schemaIdentityStable?: boolean;
|
||||
model?: {
|
||||
provider: string;
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
api: string;
|
||||
thinking?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
const configPath = path.join(root, mode, "models.yml");
|
||||
if (mode === "custom") {
|
||||
const root = process.argv[2];
|
||||
if (!root) {
|
||||
throw new Error("Expected an isolated config root");
|
||||
}
|
||||
|
||||
function retainedHeapNodes(): number {
|
||||
Bun.gc(true);
|
||||
const snapshot = JSON.parse(Bun.generateHeapSnapshot("v8")) as HeapSnapshot;
|
||||
return snapshot.nodes.length / snapshot.snapshot.meta.node_fields.length;
|
||||
}
|
||||
|
||||
async function measureMissing(): Promise<ProbeResult> {
|
||||
let model: ProbeResult["model"];
|
||||
{
|
||||
const authStorage = await AuthStorage.create(":memory:");
|
||||
try {
|
||||
const registry = new ModelRegistry(authStorage, path.join(root, "missing", "models.yml"));
|
||||
const found = registry.find("anthropic", "claude-sonnet-4-5");
|
||||
model = found && {
|
||||
provider: found.provider,
|
||||
id: found.id,
|
||||
baseUrl: found.baseUrl,
|
||||
api: found.api,
|
||||
thinking: found.thinking,
|
||||
};
|
||||
} finally {
|
||||
authStorage.close();
|
||||
}
|
||||
}
|
||||
return { retainedHeapNodes: retainedHeapNodes(), model };
|
||||
}
|
||||
|
||||
async function writeCustomConfig(): Promise<string> {
|
||||
const configPath = path.join(root, "custom", "models.yml");
|
||||
await Bun.write(
|
||||
configPath,
|
||||
YAML.stringify(
|
||||
@@ -45,34 +83,38 @@ if (mode === "custom") {
|
||||
2,
|
||||
),
|
||||
);
|
||||
return configPath;
|
||||
}
|
||||
|
||||
const authStorage = await AuthStorage.create(":memory:");
|
||||
try {
|
||||
const registry = new ModelRegistry(authStorage, configPath);
|
||||
const model =
|
||||
mode === "custom" ? registry.find("lazy-models", "lazy-model") : registry.find("anthropic", "claude-sonnet-4-5");
|
||||
const firstSchema = mode === "custom" ? ModelsConfigFile.relocate(configPath).schema : undefined;
|
||||
const secondSchema =
|
||||
mode === "custom" ? ModelsConfigFile.relocate(path.join(root, "second", "models.yml")).schema : undefined;
|
||||
|
||||
Bun.gc(true);
|
||||
const snapshot = JSON.parse(Bun.generateHeapSnapshot("v8")) as HeapSnapshot;
|
||||
const nodeWidth = snapshot.snapshot.meta.node_fields.length;
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
retainedHeapNodes: snapshot.nodes.length / nodeWidth,
|
||||
schemaIdentityStable: mode === "custom" ? firstSchema === secondSchema : undefined,
|
||||
model: model && {
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
baseUrl: model.baseUrl,
|
||||
api: model.api,
|
||||
thinking: model.thinking,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
authStorage.close();
|
||||
async function measureCustom(configPath: string): Promise<ProbeResult> {
|
||||
let model: ProbeResult["model"];
|
||||
let schemaIdentityStable = false;
|
||||
{
|
||||
const authStorage = await AuthStorage.create(":memory:");
|
||||
try {
|
||||
const registry = new ModelRegistry(authStorage, configPath);
|
||||
const found = registry.find("lazy-models", "lazy-model");
|
||||
model = found && {
|
||||
provider: found.provider,
|
||||
id: found.id,
|
||||
baseUrl: found.baseUrl,
|
||||
api: found.api,
|
||||
thinking: found.thinking,
|
||||
};
|
||||
const firstSchema = ModelsConfigFile.relocate(configPath).schema;
|
||||
const secondSchema = ModelsConfigFile.relocate(path.join(root, "second", "models.yml")).schema;
|
||||
schemaIdentityStable = firstSchema === secondSchema;
|
||||
} finally {
|
||||
authStorage.close();
|
||||
}
|
||||
}
|
||||
return {
|
||||
retainedHeapNodes: retainedHeapNodes(),
|
||||
schemaIdentityStable,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
const missing = await measureMissing();
|
||||
const custom = await measureCustom(await writeCustomConfig());
|
||||
process.stdout.write(JSON.stringify({ missing, custom }));
|
||||
|
||||
@@ -14,10 +14,15 @@ interface ProbeResult {
|
||||
};
|
||||
}
|
||||
|
||||
interface ProbeResults {
|
||||
missing: ProbeResult;
|
||||
custom: ProbeResult;
|
||||
}
|
||||
|
||||
const probePath = path.join(import.meta.dir, "fixtures", "models-config-validator-construction-probe.ts");
|
||||
|
||||
async function runProbe(root: string, mode: "missing" | "custom"): Promise<ProbeResult> {
|
||||
const proc = Bun.spawn([process.execPath, probePath, root, mode], {
|
||||
async function runProbe(root: string): Promise<ProbeResults> {
|
||||
const proc = Bun.spawn([process.execPath, probePath, root], {
|
||||
cwd: path.join(import.meta.dir, "../../.."),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -28,14 +33,13 @@ async function runProbe(root: string, mode: "missing" | "custom"): Promise<Probe
|
||||
proc.exited,
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
return JSON.parse(stdout) as ProbeResult;
|
||||
return JSON.parse(stdout) as ProbeResults;
|
||||
}
|
||||
|
||||
test("models config validation resources are retained only for a custom config", async () => {
|
||||
const tempDir = TempDir.createSync("@models-config-validator-");
|
||||
try {
|
||||
const missing = await runProbe(tempDir.path(), "missing");
|
||||
const custom = await runProbe(tempDir.path(), "custom");
|
||||
const { missing, custom } = await runProbe(tempDir.path());
|
||||
|
||||
expect(missing.model).toMatchObject({
|
||||
provider: "anthropic",
|
||||
@@ -55,6 +59,7 @@ test("models config validation resources are retained only for a custom config",
|
||||
},
|
||||
});
|
||||
expect(custom.schemaIdentityStable).toBe(true);
|
||||
expect(missing.retainedHeapNodes).toBeLessThan(custom.retainedHeapNodes);
|
||||
expect(
|
||||
custom.retainedHeapNodes - missing.retainedHeapNodes,
|
||||
"custom config validation should retain its schema bundle",
|
||||
|
||||
Reference in New Issue
Block a user