forked from wangbo/easyai
feat(catalog): 将官方模型目录迁入公开Git仓库
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
assertNoMissingModels,
|
||||
buildRelease,
|
||||
checkArtifacts,
|
||||
prepareArtifacts,
|
||||
publicBaseUrl,
|
||||
validateSource,
|
||||
} from "./catalog.mjs";
|
||||
|
||||
const directory = path.dirname(new URL(import.meta.url).pathname);
|
||||
const source = JSON.parse(
|
||||
fs.readFileSync(path.join(directory, "catalog.source.json"), "utf8"),
|
||||
);
|
||||
|
||||
function fixture(t) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "easyai-git-catalog-"));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
const data = structuredClone(source);
|
||||
data.baseModels = data.baseModels.slice(0, 2);
|
||||
const save = () =>
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "catalog.source.json"),
|
||||
JSON.stringify(data, null, 2) + "\n",
|
||||
);
|
||||
save();
|
||||
return { dir, data, save };
|
||||
}
|
||||
|
||||
test("唯一维护源通过完整 Schema 和规范格式校验", () => {
|
||||
const data = validateSource();
|
||||
assert.ok(data.baseModels.length >= 220);
|
||||
assert.equal(data.providers.length, 27);
|
||||
});
|
||||
|
||||
test("Nano Banana 2.1 保留唯一原身份和图像能力", () => {
|
||||
const rows = source.baseModels.filter(
|
||||
(model) => model.defaultRealModelName === "gemini-nano-banana-2.1",
|
||||
);
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(
|
||||
rows[0].catalogKey,
|
||||
"base:runtime:google-gemini:Nano Banana 2.1:gemini-nano-banana-2.1",
|
||||
);
|
||||
assert.equal(rows[0].alias, "Nano Banana 2.1");
|
||||
assert.equal(rows[0].status, "active");
|
||||
assert.deepEqual(rows[0].modelTypes, ["image_generate", "image_edit"]);
|
||||
assert.deepEqual(rows[0].capabilities.image_generate.output_resolutions, [
|
||||
"1K",
|
||||
"2K",
|
||||
"4K",
|
||||
]);
|
||||
assert.equal(rows[0].capabilities.image_edit.input_max_images_count, 14);
|
||||
});
|
||||
|
||||
test("重复模型身份拒绝保存", (t) => {
|
||||
const { dir, data, save } = fixture(t);
|
||||
data.baseModels.push(structuredClone(data.baseModels[0]));
|
||||
save();
|
||||
assert.throws(
|
||||
() => validateSource(path.join(dir, "catalog.source.json")),
|
||||
/重复/,
|
||||
);
|
||||
});
|
||||
|
||||
test("能力字段拼写和类型受 Schema 约束", (t) => {
|
||||
const { dir, data, save } = fixture(t);
|
||||
const type = data.baseModels[0].modelTypes[0];
|
||||
data.baseModels[0].capabilities[type].misspelledCapability = true;
|
||||
save();
|
||||
assert.throws(
|
||||
() => validateSource(path.join(dir, "catalog.source.json")),
|
||||
/additional properties/,
|
||||
);
|
||||
});
|
||||
|
||||
test("准备版本时同步生成公开指针和校验和匹配的快照", (t) => {
|
||||
const { dir } = fixture(t);
|
||||
const { pointer } = prepareArtifacts(dir, { version: "test-v1" });
|
||||
assert.equal(
|
||||
pointer.catalogUrl,
|
||||
publicBaseUrl + "/releases/test-v1/catalog.json",
|
||||
);
|
||||
assert.equal(checkArtifacts(dir).version, "test-v1");
|
||||
});
|
||||
|
||||
test("只改维护源而未重新生成快照会被检查拒绝", (t) => {
|
||||
const { dir, data, save } = fixture(t);
|
||||
prepareArtifacts(dir, { version: "test-v1" });
|
||||
data.baseModels[0].displayName = "修改后的名称";
|
||||
save();
|
||||
assert.throws(() => checkArtifacts(dir), /不一致/);
|
||||
});
|
||||
|
||||
test("篡改版本快照会被拒绝", (t) => {
|
||||
const { dir } = fixture(t);
|
||||
prepareArtifacts(dir, { version: "test-v1" });
|
||||
const file = path.join(dir, "releases/test-v1/catalog.json");
|
||||
const release = JSON.parse(fs.readFileSync(file));
|
||||
release.baseModels[0].displayName = "篡改";
|
||||
fs.writeFileSync(file, JSON.stringify(release));
|
||||
assert.throws(() => checkArtifacts(dir), /不一致/);
|
||||
});
|
||||
|
||||
test("遗漏已发布模型默认拒绝,正常下线保留原身份", (t) => {
|
||||
const { data } = fixture(t);
|
||||
const previous = buildRelease(data, { version: "test-v1" });
|
||||
const reduced = structuredClone(data);
|
||||
reduced.baseModels.pop();
|
||||
assert.throws(
|
||||
() => assertNoMissingModels(previous, reduced),
|
||||
/遗漏已发布模型/,
|
||||
);
|
||||
data.baseModels[0].status = "offline";
|
||||
assert.deepEqual(assertNoMissingModels(previous, data), []);
|
||||
});
|
||||
|
||||
test("物理移除必须同时声明允许和原因,并记录在新版本中", (t) => {
|
||||
const { dir, data, save } = fixture(t);
|
||||
const { release: previous } = prepareArtifacts(dir, { version: "test-v1" });
|
||||
data.baseModels.pop();
|
||||
save();
|
||||
assert.throws(
|
||||
() => prepareArtifacts(dir, { previous, allowRemovals: true }),
|
||||
/遗漏/,
|
||||
);
|
||||
assert.throws(
|
||||
() => prepareArtifacts(dir, { previous, removalReason: "迁移" }),
|
||||
/遗漏/,
|
||||
);
|
||||
const { release } = prepareArtifacts(dir, {
|
||||
previous,
|
||||
version: "test-v2",
|
||||
allowRemovals: true,
|
||||
removalReason: "明确移除",
|
||||
});
|
||||
assert.equal(release.removals.models.length, 1);
|
||||
assert.equal(release.removals.reason, "明确移除");
|
||||
assert.equal(checkArtifacts(dir).version, "test-v2");
|
||||
});
|
||||
|
||||
test("历史快照不能覆盖且版本不能包含目录穿越", (t) => {
|
||||
const { dir } = fixture(t);
|
||||
prepareArtifacts(dir, { version: "test-v1" });
|
||||
assert.throws(
|
||||
() => prepareArtifacts(dir, { version: "test-v1" }),
|
||||
/不能覆盖/,
|
||||
);
|
||||
assert.throws(
|
||||
() => prepareArtifacts(dir, { version: "../outside" }),
|
||||
/版本格式/,
|
||||
);
|
||||
});
|
||||
|
||||
test("公开指针不能重新指向原 CDN 或其他仓库", (t) => {
|
||||
const { dir } = fixture(t);
|
||||
prepareArtifacts(dir, { version: "test-v1" });
|
||||
const file = path.join(dir, "latest.json");
|
||||
const pointer = JSON.parse(fs.readFileSync(file));
|
||||
pointer.catalogUrl =
|
||||
"https://aliyuncdn.51easyai.com/model-catalog/official/v1/catalog.json";
|
||||
fs.writeFileSync(file, JSON.stringify(pointer));
|
||||
assert.throws(() => checkArtifacts(dir), /easyai 仓库公开地址/);
|
||||
});
|
||||
|
||||
test("恢复 Git 历史定义后生成新回退版本,保留全部快照", (t) => {
|
||||
const { dir, data, save } = fixture(t);
|
||||
const original = structuredClone(data);
|
||||
const first = prepareArtifacts(dir, { version: "test-v1" });
|
||||
data.baseModels[0].displayName = "新版";
|
||||
save();
|
||||
const second = prepareArtifacts(dir, {
|
||||
previous: first.release,
|
||||
version: "test-v2",
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "catalog.source.json"),
|
||||
JSON.stringify(original, null, 2) + "\n",
|
||||
);
|
||||
const third = prepareArtifacts(dir, {
|
||||
previous: second.release,
|
||||
version: "test-v3",
|
||||
note: "恢复 test-v1 的定义",
|
||||
});
|
||||
assert.equal(third.release.previousVersion, "test-v2");
|
||||
assert.equal(third.release.checksum, first.release.checksum);
|
||||
assert.equal(checkArtifacts(dir).version, "test-v3");
|
||||
assert.deepEqual(fs.readdirSync(path.join(dir, "releases")).sort(), [
|
||||
"test-v1",
|
||||
"test-v2",
|
||||
"test-v3",
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user