132 lines
3.5 KiB
TypeScript
132 lines
3.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { buildSkillRow } from "../../../core/skill/packager.js";
|
|
import { rootLogger } from "../../../core/logger/index.js";
|
|
import type { Embedder } from "../../../core/embedding/types.js";
|
|
import type { PolicyRow, SkillRow } from "../../../core/types.js";
|
|
import { makeDraft, makeSkillConfig, NOW, vec } from "./_helpers.js";
|
|
|
|
function mkPolicy(): PolicyRow {
|
|
return {
|
|
id: "po_pkg" as PolicyRow["id"],
|
|
title: "install",
|
|
trigger: "pip install errors on alpine",
|
|
procedure: "apk add, retry",
|
|
verification: "pip install succeeds",
|
|
boundary: "alpine",
|
|
support: 4,
|
|
gain: 0.4,
|
|
status: "active",
|
|
sourceEpisodeIds: ["ep_1" as PolicyRow["sourceEpisodeIds"][number]],
|
|
inducedBy: "l2.l2.induction.v1",
|
|
decisionGuidance: { preference: [], antiPattern: [] },
|
|
vec: vec([1, 0, 0]),
|
|
createdAt: NOW,
|
|
updatedAt: NOW,
|
|
};
|
|
}
|
|
|
|
function fakeEmbedder(): Embedder {
|
|
return {
|
|
dimensions: 3,
|
|
provider: "openai_compatible",
|
|
model: "fake",
|
|
async embedOne() {
|
|
return vec([0.1, 0.2, 0.3]);
|
|
},
|
|
async embedMany(inputs) {
|
|
return inputs.map(() => vec([0.1, 0.2, 0.3]));
|
|
},
|
|
stats() {
|
|
return {
|
|
hits: 0,
|
|
misses: 0,
|
|
requests: 0,
|
|
roundTrips: 0,
|
|
failures: 0,
|
|
lastOkAt: null,
|
|
lastError: null,
|
|
};
|
|
},
|
|
resetCache() {
|
|
/* noop */
|
|
},
|
|
async close() {
|
|
/* noop */
|
|
},
|
|
};
|
|
}
|
|
|
|
const log = rootLogger.child({ channel: "core.skill.packager" });
|
|
|
|
describe("skill/packager", () => {
|
|
it("builds a candidate skill row with embedding + invocation guide", async () => {
|
|
const r = await buildSkillRow(
|
|
{
|
|
draft: makeDraft(),
|
|
policy: mkPolicy(),
|
|
evidenceEpisodeIds: ["ep_1" as PolicyRow["sourceEpisodeIds"][number]],
|
|
},
|
|
{ embedder: fakeEmbedder(), log, config: makeSkillConfig({ minSupport: 3 }) },
|
|
);
|
|
expect(r.freshMint).toBe(true);
|
|
expect(r.row.status).toBe("candidate");
|
|
expect(r.row.invocationGuide).toContain("Alpine");
|
|
expect(r.row.vec).not.toBeNull();
|
|
expect(r.row.sourcePolicyIds).toContain("po_pkg");
|
|
expect(r.row.eta).toBeGreaterThanOrEqual(makeSkillConfig().minEtaForRetrieval);
|
|
});
|
|
|
|
it("preserves the existing skill id when rebuilding", async () => {
|
|
const existing = {
|
|
id: "sk_old" as SkillRow["id"],
|
|
name: "old",
|
|
status: "active",
|
|
invocationGuide: "",
|
|
procedureJson: null,
|
|
eta: 0.8,
|
|
support: 3,
|
|
gain: 0.3,
|
|
trialsAttempted: 5,
|
|
trialsPassed: 5,
|
|
sourcePolicyIds: ["po_pkg" as PolicyRow["id"]],
|
|
sourceWorldModelIds: [],
|
|
evidenceAnchors: [],
|
|
vec: null,
|
|
createdAt: NOW,
|
|
updatedAt: NOW,
|
|
version: 1,
|
|
} as SkillRow;
|
|
const r = await buildSkillRow(
|
|
{
|
|
draft: makeDraft(),
|
|
policy: mkPolicy(),
|
|
evidenceEpisodeIds: [],
|
|
existing,
|
|
},
|
|
{ embedder: null, log, config: makeSkillConfig() },
|
|
);
|
|
expect(r.row.id).toBe("sk_old");
|
|
expect(r.row.trialsAttempted).toBe(5);
|
|
expect(r.freshMint).toBe(false);
|
|
});
|
|
|
|
it("survives embedder failure", async () => {
|
|
const bad: Embedder = {
|
|
...fakeEmbedder(),
|
|
async embedOne() {
|
|
throw new Error("embed boom");
|
|
},
|
|
};
|
|
const r = await buildSkillRow(
|
|
{
|
|
draft: makeDraft(),
|
|
policy: mkPolicy(),
|
|
evidenceEpisodeIds: [],
|
|
},
|
|
{ embedder: bad, log, config: makeSkillConfig() },
|
|
);
|
|
expect(r.row.vec).toBeNull();
|
|
});
|
|
});
|