67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fc from "fast-check";
|
|
import { configureProperties } from "../../helpers/propertyConfig.ts";
|
|
import { validateComboDAG, resolveNestedComboModels } from "../../../open-sse/services/combo.ts";
|
|
|
|
configureProperties();
|
|
|
|
// ComboLike shape: { name: string, models: unknown[] }
|
|
// normalizeModelEntry extracts .model (string) from each entry — plain strings work directly.
|
|
// "combo:X" notation is NOT used; the resolver checks if models[i] matches combo.name in allCombos.
|
|
|
|
test("validateComboDAG throws on self-cycle", () => {
|
|
// c0 references itself directly — must throw
|
|
const combos = [{ name: "c0", models: ["c0"] }];
|
|
assert.throws(() => validateComboDAG("c0", combos));
|
|
});
|
|
|
|
test("validateComboDAG throws on indirect cycle (a->b->a)", () => {
|
|
const combos = [
|
|
{ name: "a", models: ["b"] },
|
|
{ name: "b", models: ["a"] },
|
|
];
|
|
assert.throws(() => validateComboDAG("a", combos));
|
|
});
|
|
|
|
test("validateComboDAG does not throw on acyclic graph", () => {
|
|
const combos = [
|
|
{ name: "a", models: ["b", "openai/gpt-4o"] },
|
|
{ name: "b", models: ["anthropic/claude-3-5-sonnet"] },
|
|
];
|
|
assert.doesNotThrow(() => validateComboDAG("a", combos));
|
|
});
|
|
|
|
test("resolveNestedComboModels never throws and is cycle-safe", () => {
|
|
// Arbitrary combo graph: names c0..cN where each combo's models reference
|
|
// other combo names by their exact name string (no "combo:" prefix needed).
|
|
const graphArb = fc.integer({ min: 1, max: 5 }).chain((n) => {
|
|
const names = Array.from({ length: n }, (_, i) => `c${i}`);
|
|
const comboArr = names.map((name, i) => {
|
|
// Each combo references up to 2 other combos by name plus one real model
|
|
const refs = names.filter((_, j) => j !== i).slice(0, 2);
|
|
return { name, models: [...refs, "openai/gpt-4o"] };
|
|
});
|
|
return fc.constant(comboArr);
|
|
});
|
|
|
|
fc.assert(
|
|
fc.property(graphArb, (combos) => {
|
|
// resolveNestedComboModels must always return an array (never throw, never loop)
|
|
const out = resolveNestedComboModels(combos[0], combos);
|
|
assert.ok(Array.isArray(out), "must return array");
|
|
})
|
|
);
|
|
});
|
|
|
|
test("resolveNestedComboModels returns [] on direct cycle (cycle-safety)", () => {
|
|
// Cycle: c0->c1->c0
|
|
const combos = [
|
|
{ name: "c0", models: ["c1"] },
|
|
{ name: "c1", models: ["c0"] },
|
|
];
|
|
const out = resolveNestedComboModels(combos[0], combos);
|
|
// When a cycle is detected, the visited guard returns [] — no infinite loop
|
|
assert.ok(Array.isArray(out));
|
|
});
|