Files
fusion/scripts/__tests__/skill-sync-cache.test.mjs
gsxdsm 94ddfe1064 FN-7274: document workflow tools in Fusion skill
Document the workflow-authoring tool surface in the packaged Fusion skill references.

- Add workflow tools to the generated Fusion skill category and capability tables.
- Teach the skill sync script to read workflow tool specs from extension and engine agent tool sources.
- Cover workflow tool documentation and cache inputs with sync tests.
- Add a changeset for the published CLI skill update.

Files changed:
 .changeset/fn-7274-workflow-skill-tools.md         |   7 +
 packages/cli/skill/fusion/SKILL.md                 |   1 +
 .../cli/skill/fusion/references/engine-tools.md    |   4 +-
 .../cli/skill/fusion/references/extension-tools.md |  83 +++++++++-
 .../skill/fusion/references/fusion-capabilities.md |   8 +
 packages/cli/src/__tests__/skill-sync.test.ts      |  60 ++++++-
 scripts/__tests__/skill-sync-cache.test.mjs        |   5 +
 scripts/sync-fusion-skill-tools.mjs                | 172 ++++++++++++++++++---
 8 files changed, 312 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-7274

Fusion-Task-Lineage: e1cd6bd8-aecc-49ca-96ed-a937a15a18a4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-30 08:04:12 -07:00

118 lines
4.4 KiB
JavaScript

/**
* Unit tests for the U3 skill-sync skip cache in scripts/sync-fusion-skill-tools.mjs.
*
* Runner: node --test scripts/__tests__/skill-sync-cache.test.mjs
*
* Uses an isolated temp rootDir with a fabricated node_modules/.cache/fusion so
* the real cache is never touched.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import {
SKILL_SYNC_INPUT_PATHS,
computeSkillSyncHash,
isSkillSyncCheckCached,
recordSkillSyncCheckPass,
} from "../sync-fusion-skill-tools.mjs";
function withRoot(fn) {
const root = mkdtempSync(path.join(tmpdir(), "skill-sync-cache-"));
try {
fn(root);
} finally {
rmSync(root, { recursive: true, force: true });
}
}
/**
* A git stub that reports each input path as a tracked file with a stable blob
* sha derived from a content map, and reports clean status. `contents` maps a
* repo-relative path to a sha string.
*/
function fakeGit(shaByPath, { dirty = [] } = {}) {
return (args) => {
if (args[0] === "ls-files") {
return SKILL_SYNC_INPUT_PATHS.map((p) => `100644 ${shaByPath[p] ?? "0000"} 0\t${p}`).join("\n");
}
if (args[0] === "status") {
return dirty.map((p) => ` M ${p}`).join("\n");
}
return null;
};
}
const baseShas = Object.fromEntries(SKILL_SYNC_INPUT_PATHS.map((p, i) => [p, `sha${i}`]));
test("skill sync cache watches extension and engine workflow tool sources", () => {
assert.ok(SKILL_SYNC_INPUT_PATHS.includes("packages/cli/src/extension.ts"));
assert.ok(SKILL_SYNC_INPUT_PATHS.includes("packages/engine/src/agent-tools.ts"));
});
test("recordSkillSyncCheckPass then isSkillSyncCheckCached returns true on unchanged inputs", () => {
withRoot((root) => {
const deps = { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("") };
assert.equal(isSkillSyncCheckCached(root, deps), false, "no cache yet");
recordSkillSyncCheckPass(root, deps);
assert.equal(isSkillSyncCheckCached(root, deps), true, "cache hit after recording");
});
});
test("isSkillSyncCheckCached returns false after an input blob sha changes", () => {
withRoot((root) => {
const deps = { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("") };
recordSkillSyncCheckPass(root, deps);
assert.equal(isSkillSyncCheckCached(root, deps), true);
// Change one input's blob sha (a skill tool / extension edit landed).
const changed = { ...baseShas, [SKILL_SYNC_INPUT_PATHS[0]]: "DIFFERENT" };
const changedDeps = { gitFn: fakeGit(changed), readFn: () => Buffer.from("") };
assert.equal(isSkillSyncCheckCached(root, changedDeps), false, "changed input must bust cache");
});
});
test("isSkillSyncCheckCached returns false on a stale cache-format version", () => {
withRoot((root) => {
const cacheDir = path.join(root, "node_modules", ".cache", "fusion");
mkdirSync(cacheDir, { recursive: true });
writeFileSync(
path.join(cacheDir, "skill-sync-cache.json"),
JSON.stringify({ version: 999, hash: "x" }),
);
const deps = { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("") };
assert.equal(isSkillSyncCheckCached(root, deps), false);
});
});
test("computeSkillSyncHash is stable for identical inputs and busts when dirty content changes", () => {
withRoot((root) => {
const clean = computeSkillSyncHash(root, { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("X") });
const same = computeSkillSyncHash(root, { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("X") });
assert.equal(clean, same);
// Same blob shas but a dirty worktree edit on one file → hash differs.
const dirtyDeps = {
gitFn: fakeGit(baseShas, { dirty: [SKILL_SYNC_INPUT_PATHS[0]] }),
readFn: () => Buffer.from("EDITED"),
};
assert.notEqual(clean, computeSkillSyncHash(root, dirtyDeps));
});
});
test("recordSkillSyncCheckPass writes a versioned payload with a passedAt timestamp", () => {
withRoot((root) => {
const deps = { gitFn: fakeGit(baseShas), readFn: () => Buffer.from("") };
recordSkillSyncCheckPass(root, deps);
const raw = JSON.parse(
readFileSync(path.join(root, "node_modules", ".cache", "fusion", "skill-sync-cache.json"), "utf8"),
);
assert.equal(raw.version, 1);
assert.equal(typeof raw.hash, "string");
assert.equal(raw.hash.length, 64);
assert.ok(!Number.isNaN(Date.parse(raw.passedAt)));
});
});