FN-8219: remove expired CLI quarantined tests
Remove expired CLI test quarantines and preserve evidence of their resolution. - Delete five expired quarantined CLI test files - Remove their Vitest exclusions - Add a lockstep regression guard for the retired quarantine scope Files changed: .../src/__tests__/extension-fn-secret-get.test.ts | 164 - .../src/__tests__/lockstep-cli-quarantine.test.ts | 64 + packages/cli/src/__tests__/skill-sync.test.ts | 562 --- packages/cli/src/__tests__/version.test.ts | 62 - .../cli/src/commands/__tests__/dashboard.test.ts | 3736 -------------------- .../__tests__/bundled-plugin-freshness.test.ts | 75 - packages/cli/vitest.config.ts | 21 +- 7 files changed, 78 insertions(+), 4606 deletions(-) Fusion-Task-Id: FN-8219 Fusion-Task-Lineage: cac0b4c5-3255-4733-ba24-2156979d60f3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -1,164 +0,0 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { workflowAuthoringEngineMock } from "./helpers/engine-workflow-authoring-mock.js";
|
||||
|
||||
const resolveSecretAccessPolicyMock = vi.hoisted(() => vi.fn());
|
||||
const revealSecretMock = vi.hoisted(() => vi.fn());
|
||||
const listSecretsMock = vi.hoisted(() => vi.fn());
|
||||
const approvalCreateMock = vi.hoisted(() => vi.fn());
|
||||
const approvalFindLatestByDedupeKeyMock = vi.hoisted(() => vi.fn());
|
||||
const recordRunAuditEventMock = vi.hoisted(() => vi.fn());
|
||||
const assertNoSecretPlaintextMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@fusion/dashboard", () => ({
|
||||
registerGithubTrackingHook: vi.fn(),
|
||||
// FNXC:CliTests 2026-07-13-09:40: Missing dashboard barrel exports added for mock completeness (scripts/check-mock-completeness.mjs gate).
|
||||
GitLabClient: vi.fn(),
|
||||
resolveGitlabAuth: vi.fn(() => ({})),
|
||||
buildGitLabTaskProvenance: vi.fn(() => ({})),
|
||||
isGitLabAlreadyImported: vi.fn(),
|
||||
buildGitLabTaskDescription: vi.fn(),
|
||||
}));
|
||||
vi.mock("@fusion/engine", () => ({
|
||||
installBaselineArchiveWorktreeDisposer: vi.fn(),
|
||||
...workflowAuthoringEngineMock,
|
||||
createFnAgent: vi.fn(),
|
||||
fetchWebContent: vi.fn(),
|
||||
assertNoSecretPlaintext: assertNoSecretPlaintextMock,
|
||||
emitGoalRetrievalAudit: vi.fn(),
|
||||
createWorkflowAuthoringTools: vi.fn(() => ({})),
|
||||
workflowListParams: {},
|
||||
workflowGetParams: {},
|
||||
workflowValidateParams: {}, // FNXC:Round10 FN-7911 added this export to @fusion/engine barrel
|
||||
workflowSelectParams: {},
|
||||
workflowCreateParams: {},
|
||||
workflowUpdateParams: {},
|
||||
workflowDeleteParams: {},
|
||||
workflowSettingsParams: {},
|
||||
traitListParams: {},
|
||||
normalizeAgentLogPaging: vi.fn(() => ({ limit: 100, offset: 0 })),
|
||||
renderAgentLogEntries: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", async () => {
|
||||
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
|
||||
class MockTaskStore {
|
||||
async init() {}
|
||||
async getSecretsStore() {
|
||||
return { listSecrets: listSecretsMock, revealSecret: revealSecretMock };
|
||||
}
|
||||
getGlobalSettingsStore() {
|
||||
return { getSettings: async () => ({ secretsAccessPolicy: "prompt" }) };
|
||||
}
|
||||
recordRunAuditEvent = recordRunAuditEventMock;
|
||||
getDatabase() {
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
class MockApprovalRequestStore {
|
||||
constructor(_db: unknown) {}
|
||||
findLatestByDedupeKey = approvalFindLatestByDedupeKeyMock;
|
||||
create = approvalCreateMock;
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
TaskStore: MockTaskStore,
|
||||
ApprovalRequestStore: MockApprovalRequestStore,
|
||||
resolveSecretAccessPolicy: resolveSecretAccessPolicyMock,
|
||||
};
|
||||
});
|
||||
|
||||
import kbExtension from "../extension.js";
|
||||
|
||||
describe("extension fn_secret_get", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
listSecretsMock.mockImplementation((scope?: "project" | "global") => {
|
||||
if (scope === "project") return [{ id: "s1", key: "API_KEY", accessPolicy: "auto" }];
|
||||
return [];
|
||||
});
|
||||
revealSecretMock.mockResolvedValue({ key: "API_KEY", plaintextValue: "secret-value" });
|
||||
resolveSecretAccessPolicyMock.mockReturnValue({ policy: "auto", source: "secret" });
|
||||
approvalFindLatestByDedupeKeyMock.mockReturnValue(null);
|
||||
approvalCreateMock.mockReturnValue({ id: "apr-1", status: "pending" });
|
||||
assertNoSecretPlaintextMock.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it("returns value for auto policy", async () => {
|
||||
const tools = new Map<string, any>();
|
||||
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
|
||||
const tool = tools.get("fn_secret_get");
|
||||
const result = await tool.execute("id", { key: "API_KEY" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1", runId: "run-1" });
|
||||
expect(result.details.value).toBe("secret-value");
|
||||
expect(approvalCreateMock).not.toHaveBeenCalled();
|
||||
const event = recordRunAuditEventMock.mock.calls[0][0];
|
||||
expect(event.mutationType).toBe("secret:read");
|
||||
expect(event.metadata).toEqual({ key: "API_KEY", scope: "project" });
|
||||
expect(event.metadata).not.toHaveProperty("plaintextValue");
|
||||
expect(event.metadata).not.toHaveProperty("value");
|
||||
expect(event.metadata).not.toHaveProperty("ciphertext");
|
||||
expect(event.metadata).not.toHaveProperty("nonce");
|
||||
expect(event.metadata).not.toHaveProperty("decrypted");
|
||||
expect(JSON.stringify(event)).not.toContain("secret-value");
|
||||
});
|
||||
|
||||
it("returns pending_approval for prompt policy", async () => {
|
||||
resolveSecretAccessPolicyMock.mockReturnValue({ policy: "prompt", source: "secret" });
|
||||
const tools = new Map<string, any>();
|
||||
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
|
||||
const tool = tools.get("fn_secret_get");
|
||||
const result = await tool.execute("id", { key: "API_KEY" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1" });
|
||||
expect(result.details.outcome).toBe("pending_approval");
|
||||
expect(approvalCreateMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips audit emission if metadata guard throws", async () => {
|
||||
assertNoSecretPlaintextMock.mockImplementation(() => {
|
||||
throw new Error("secret audit metadata may not include plaintext fields");
|
||||
});
|
||||
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
const tools = new Map<string, any>();
|
||||
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
|
||||
const tool = tools.get("fn_secret_get");
|
||||
|
||||
const result = await tool.execute("id", { key: "API_KEY" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1", runId: "run-1" });
|
||||
expect(result.details.value).toBe("secret-value");
|
||||
expect(recordRunAuditEventMock).not.toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns denied for deny policy and not found when missing", async () => {
|
||||
const tools = new Map<string, any>();
|
||||
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
|
||||
const tool = tools.get("fn_secret_get");
|
||||
|
||||
resolveSecretAccessPolicyMock.mockReturnValue({ policy: "deny", source: "secret" });
|
||||
const denied = await tool.execute("id", { key: "API_KEY" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1", runId: "run-1" });
|
||||
expect(denied.details.error).toBe("denied");
|
||||
expect(revealSecretMock).not.toHaveBeenCalled();
|
||||
expect(approvalCreateMock).not.toHaveBeenCalled();
|
||||
|
||||
listSecretsMock.mockReturnValue([]);
|
||||
const missing = await tool.execute("id", { key: "NOPE" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1" });
|
||||
expect(missing.details.error).toBe("not-found");
|
||||
});
|
||||
|
||||
it("uses explicit scope when provided, otherwise falls back project then global", async () => {
|
||||
listSecretsMock.mockImplementation((scope?: "project" | "global") => {
|
||||
if (scope === "project") return [{ id: "p1", key: "SHARED", accessPolicy: "auto" }];
|
||||
if (scope === "global") return [{ id: "g1", key: "SHARED", accessPolicy: "auto" }];
|
||||
return [];
|
||||
});
|
||||
|
||||
const tools = new Map<string, any>();
|
||||
kbExtension({ registerTool: (d: any) => tools.set(d.name, d), registerCommand: vi.fn(), registerShortcut: vi.fn(), registerFlag: vi.fn(), on: vi.fn() } as any);
|
||||
const tool = tools.get("fn_secret_get");
|
||||
|
||||
await tool.execute("id", { key: "SHARED", scope: "global" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1" });
|
||||
expect(revealSecretMock).toHaveBeenLastCalledWith("g1", "global", { agentId: "agent-1" });
|
||||
|
||||
await tool.execute("id", { key: "SHARED" }, undefined, undefined, { cwd: process.cwd(), agentId: "agent-1" });
|
||||
expect(revealSecretMock).toHaveBeenLastCalledWith("p1", "project", { agentId: "agent-1" });
|
||||
});
|
||||
});
|
||||
64
packages/cli/src/__tests__/lockstep-cli-quarantine.test.ts
Normal file
64
packages/cli/src/__tests__/lockstep-cli-quarantine.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type QuarantineEntry = {
|
||||
file: string;
|
||||
reason: string;
|
||||
quarantinedAt: string;
|
||||
};
|
||||
|
||||
const repoRoot = resolve(import.meta.dirname!, "../../../..");
|
||||
const configPath = resolve(repoRoot, "packages/cli/vitest.config.ts");
|
||||
const ledgerPath = resolve(repoRoot, "scripts/lib/test-quarantine.json");
|
||||
const fn8210BoundaryPath = "src/__tests__/package-config.test.ts";
|
||||
const expiredFn8219Paths = new Set([
|
||||
"src/__tests__/extension-fn-secret-get.test.ts",
|
||||
"src/__tests__/skill-sync.test.ts",
|
||||
"src/__tests__/version.test.ts",
|
||||
"src/commands/__tests__/dashboard.test.ts",
|
||||
"src/plugins/__tests__/bundled-plugin-freshness.test.ts",
|
||||
]);
|
||||
|
||||
function parseQuarantinedCliTests(configSource: string): string[] {
|
||||
const declaration = configSource.match(/const quarantinedCliTests: string\[\] = \[([\s\S]*?)\n\];/);
|
||||
expect(declaration, "quarantinedCliTests declaration must remain statically parseable").not.toBeNull();
|
||||
|
||||
return [...declaration![1].matchAll(/"([^"\n]+)"/g)].map((match) => match[1]);
|
||||
}
|
||||
|
||||
function countByPath(paths: string[]): Map<string, number> {
|
||||
return paths.reduce((counts, path) => counts.set(path, (counts.get(path) ?? 0) + 1), new Map<string, number>());
|
||||
}
|
||||
|
||||
describe("CLI quarantine ledger lockstep", () => {
|
||||
/*
|
||||
FNXC:CliTests 2026-07-17-10:00:
|
||||
FN-8219 deletes five expired 2026-06-25 quarantines instead of rescuing or
|
||||
re-recording them. This source-level guard prevents those five paths from
|
||||
returning as config-only or ledger-only entries. FN-8210 must remove the
|
||||
package-config boundary and widen this to full CLI coverage if it ever adds
|
||||
or resolves that separate quarantine.
|
||||
*/
|
||||
it("keeps the expired FN-8219 scope in bidirectional config-to-ledger lockstep", () => {
|
||||
const configPaths = parseQuarantinedCliTests(readFileSync(configPath, "utf8"))
|
||||
.filter((path) => expiredFn8219Paths.has(path));
|
||||
const ledger = JSON.parse(readFileSync(ledgerPath, "utf8")) as { entries: QuarantineEntry[] };
|
||||
const ledgerEntries = ledger.entries.filter((entry) =>
|
||||
expiredFn8219Paths.has(entry.file.replace(/^packages\/cli\//, "")),
|
||||
);
|
||||
const ledgerPaths = ledgerEntries.map((entry) => entry.file.replace(/^packages\/cli\//, ""));
|
||||
|
||||
expect(configPaths).not.toContain(fn8210BoundaryPath);
|
||||
expect(countByPath(configPaths)).toEqual(countByPath(ledgerPaths));
|
||||
|
||||
for (const entry of ledgerEntries) {
|
||||
expect(entry.reason.trim()).not.toBe("");
|
||||
expect(entry.quarantinedAt).toMatch(/^\d{4}-\d{2}-\d{2}(?:T.*Z)?$/);
|
||||
expect(Number.isNaN(Date.parse(entry.quarantinedAt))).toBe(false);
|
||||
}
|
||||
|
||||
expect(configPaths).toEqual([]);
|
||||
expect(ledgerPaths).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,562 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
||||
import { resolve, dirname, relative, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const cliRoot = resolve(__dirname, "../..");
|
||||
const repoRoot = resolve(cliRoot, "../..");
|
||||
const skillDir = resolve(cliRoot, "skill/fusion");
|
||||
const extensionPath = resolve(cliRoot, "src/extension.ts");
|
||||
const engineAgentToolsPath = resolve(repoRoot, "packages/engine/src/agent-tools.ts");
|
||||
|
||||
const EXT_TOOLS_BEGIN =
|
||||
"<!-- BEGIN: extension-tools (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const EXT_TOOLS_END = "<!-- END: extension-tools -->";
|
||||
const CAP_TABLE_BEGIN =
|
||||
"<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->";
|
||||
const CAP_TABLE_END = "<!-- END: fusion-capabilities-tool-table -->";
|
||||
|
||||
const engineToolSourceFiles = [
|
||||
resolve(repoRoot, "packages/engine/src/agent-tools.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/triage.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/executor.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/merger.ts"),
|
||||
resolve(repoRoot, "packages/engine/src/agent-heartbeat.ts"),
|
||||
];
|
||||
|
||||
function findMatchingBrace(source: string, openIndex: number): number {
|
||||
let depth = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = openIndex; i < source.length; i++) {
|
||||
const char = source[i];
|
||||
const next = source[i + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (char === "\n") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (char === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && char === "'") inSingle = false;
|
||||
else if (inDouble && char === '"') inDouble = false;
|
||||
else if (inTemplate && char === "`") inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (char === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (char === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "{") depth++;
|
||||
if (char === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unbalanced braces");
|
||||
}
|
||||
|
||||
function splitTopLevelProperties(objectBody: string): string[] {
|
||||
const props: string[] = [];
|
||||
let start = 0;
|
||||
let depthParen = 0;
|
||||
let depthBrace = 0;
|
||||
let depthBracket = 0;
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inTemplate = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = 0; i < objectBody.length; i++) {
|
||||
const ch = objectBody[i];
|
||||
const next = objectBody[i + 1];
|
||||
|
||||
if (inLineComment) {
|
||||
if (ch === "\n") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (ch === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble || inTemplate) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle && ch === "'") inSingle = false;
|
||||
else if (inDouble && ch === '"') inDouble = false;
|
||||
else if (inTemplate && ch === "`") inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "/" && next === "/") {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "(") depthParen++;
|
||||
else if (ch === ")") depthParen--;
|
||||
else if (ch === "{") depthBrace++;
|
||||
else if (ch === "}") depthBrace--;
|
||||
else if (ch === "[") depthBracket++;
|
||||
else if (ch === "]") depthBracket--;
|
||||
|
||||
if (ch === "," && depthParen === 0 && depthBrace === 0 && depthBracket === 0) {
|
||||
const prop = objectBody.slice(start, i).trim();
|
||||
if (prop) props.push(prop);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const tail = objectBody.slice(start).trim();
|
||||
if (tail) props.push(tail);
|
||||
return props;
|
||||
}
|
||||
|
||||
function getWorkflowExtensionSpecBlocks(): Array<{ name: string; paramsRef: string }> {
|
||||
const src = readFileSync(extensionPath, "utf-8");
|
||||
const specStart = src.indexOf("const workflowExtensionToolSpecs");
|
||||
if (specStart === -1) return [];
|
||||
const specEnd = src.indexOf("// ── Extension entry point", specStart);
|
||||
const body = src.slice(specStart, specEnd === -1 ? undefined : specEnd);
|
||||
const blocks: Array<{ name: string; paramsRef: string }> = [];
|
||||
|
||||
for (const match of body.matchAll(/\{[\s\S]*?name:\s*"(fn_[a-z_]+)"[\s\S]*?parameters:\s*([A-Za-z_][A-Za-z0-9_]*)[\s\S]*?\}/g)) {
|
||||
blocks.push({ name: match[1], paramsRef: match[2] });
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function getExportedTypeObjectParamNames(exportName: string): string[] {
|
||||
const src = readFileSync(engineAgentToolsPath, "utf-8");
|
||||
const exportStart = src.indexOf(`export const ${exportName} = Type.Object(`);
|
||||
if (exportStart === -1) return [];
|
||||
const braceStart = src.indexOf("{", exportStart);
|
||||
if (braceStart === -1) return [];
|
||||
const braceEnd = findMatchingBrace(src, braceStart);
|
||||
const body = src.slice(braceStart + 1, braceEnd);
|
||||
|
||||
return splitTopLevelProperties(body)
|
||||
.map((prop) => prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:/)?.[1])
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function getRegisterToolBlocks(): Array<{ name: string; block: string }> {
|
||||
const src = readFileSync(extensionPath, "utf-8");
|
||||
const blocks: Array<{ name: string; block: string }> = [];
|
||||
const token = "pi.registerTool(";
|
||||
let from = 0;
|
||||
|
||||
while (true) {
|
||||
const start = src.indexOf(token, from);
|
||||
if (start === -1) break;
|
||||
const braceStart = src.indexOf("{", start);
|
||||
const braceEnd = findMatchingBrace(src, braceStart);
|
||||
const block = src.slice(braceStart, braceEnd + 1);
|
||||
const nameMatch = block.match(/name:\s*"(fn_[a-z_]+)"/);
|
||||
if (nameMatch) {
|
||||
blocks.push({ name: nameMatch[1], block });
|
||||
}
|
||||
from = braceEnd + 1;
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all tool names registered via pi.registerTool({ name: "..." })
|
||||
* from the extension source code.
|
||||
*/
|
||||
function getExtensionToolNames(): string[] {
|
||||
return [
|
||||
...getWorkflowExtensionSpecBlocks().map((entry) => entry.name),
|
||||
...getRegisterToolBlocks().map((entry) => entry.name),
|
||||
].sort();
|
||||
}
|
||||
|
||||
function getExtensionToolParamNames(): Map<string, string[]> {
|
||||
const result = new Map<string, string[]>();
|
||||
for (const { name, paramsRef } of getWorkflowExtensionSpecBlocks()) {
|
||||
result.set(name, getExportedTypeObjectParamNames(paramsRef));
|
||||
}
|
||||
for (const { name, block } of getRegisterToolBlocks()) {
|
||||
const paramsStart = block.indexOf("parameters:");
|
||||
if (paramsStart === -1) {
|
||||
result.set(name, []);
|
||||
continue;
|
||||
}
|
||||
const objectStart = block.indexOf("Type.Object(", paramsStart);
|
||||
if (objectStart === -1) {
|
||||
result.set(name, []);
|
||||
continue;
|
||||
}
|
||||
const braceStart = block.indexOf("{", objectStart);
|
||||
const braceEnd = findMatchingBrace(block, braceStart);
|
||||
const body = block.slice(braceStart + 1, braceEnd);
|
||||
|
||||
const params = splitTopLevelProperties(body)
|
||||
.map((prop) => prop.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:/)?.[1])
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.sort();
|
||||
|
||||
result.set(name, params);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool names documented in extension-tools.md (### fn_* headings).
|
||||
*/
|
||||
function getDocumentedToolNames(): string[] {
|
||||
const doc = readFileSync(
|
||||
resolve(skillDir, "references/extension-tools.md"),
|
||||
"utf-8",
|
||||
);
|
||||
const matches = [...doc.matchAll(/^### (fn_[a-z_]+)/gm)];
|
||||
return matches.map((m) => m[1]).sort();
|
||||
}
|
||||
|
||||
function getDocumentedToolParamNames(): Map<string, string[]> {
|
||||
const doc = readFileSync(resolve(skillDir, "references/extension-tools.md"), "utf-8");
|
||||
const map = new Map<string, string[]>();
|
||||
|
||||
const toolRegex = /^### (fn_[a-z_]+)\n([\s\S]*?)(?=^### fn_|^## [A-Za-z]|^<!-- END: extension-tools -->)/gm;
|
||||
for (const match of doc.matchAll(toolRegex)) {
|
||||
const [, toolName, section] = match;
|
||||
const params = [...section.matchAll(/\| `([A-Za-z_][A-Za-z0-9_]*)` \|/g)]
|
||||
.map((m) => m[1])
|
||||
.sort();
|
||||
map.set(toolName, params);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool names listed in SKILL.md under the tool categories.
|
||||
*/
|
||||
function getSkillMdToolNames(): string[] {
|
||||
const doc = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
||||
const matches = [...doc.matchAll(/`(fn_[a-z_]+)`/g)];
|
||||
return [...new Set(matches.map((m) => m[1]))].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool names from the capabilities catalog table.
|
||||
*/
|
||||
function getCapabilitiesToolNames(): string[] {
|
||||
const doc = readFileSync(
|
||||
resolve(skillDir, "references/fusion-capabilities.md"),
|
||||
"utf-8",
|
||||
);
|
||||
const matches = [...doc.matchAll(/\| `(fn_[a-z_]+)` \|/g)];
|
||||
return matches.map((m) => m[1]).sort();
|
||||
}
|
||||
|
||||
function getEngineSessionToolNames(): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const path of engineToolSourceFiles) {
|
||||
const src = readFileSync(path, "utf-8");
|
||||
for (const match of src.matchAll(/name:\s*"(fn_[a-z_]+)"/g)) {
|
||||
names.add(match[1]);
|
||||
}
|
||||
}
|
||||
return [...names].sort();
|
||||
}
|
||||
|
||||
function getDocumentedEngineToolNames(): string[] {
|
||||
const doc = readFileSync(resolve(skillDir, "references/engine-tools.md"), "utf-8");
|
||||
return [...new Set([...doc.matchAll(/`(fn_[a-z_]+)`/g)].map((m) => m[1]))].sort();
|
||||
}
|
||||
|
||||
function collectMarkdownFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectMarkdownFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
describe("Skill-Extension Sync", () => {
|
||||
it("skill directory structure exists", () => {
|
||||
expect(existsSync(resolve(skillDir, "SKILL.md"))).toBe(true);
|
||||
expect(existsSync(resolve(skillDir, "references"))).toBe(true);
|
||||
expect(existsSync(resolve(skillDir, "workflows"))).toBe(true);
|
||||
expect(
|
||||
existsSync(resolve(skillDir, "references/extension-tools.md")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("SKILL.md has valid frontmatter with name and description", () => {
|
||||
const content = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
||||
expect(content).toMatch(/^---\n/);
|
||||
expect(content).toMatch(/\nname:\s*fusion\n/);
|
||||
expect(content).toMatch(/\ndescription:\s*.+\n/);
|
||||
});
|
||||
|
||||
it("extension-tools.md documents exactly the same tools as extension.ts", () => {
|
||||
const extensionTools = getExtensionToolNames();
|
||||
const documentedTools = getDocumentedToolNames();
|
||||
|
||||
const missingFromDocs = extensionTools.filter(
|
||||
(t) => !documentedTools.includes(t),
|
||||
);
|
||||
const extraInDocs = documentedTools.filter(
|
||||
(t) => !extensionTools.includes(t),
|
||||
);
|
||||
|
||||
expect(missingFromDocs).toEqual([]);
|
||||
expect(extraInDocs).toEqual([]);
|
||||
expect(documentedTools).toEqual(extensionTools);
|
||||
});
|
||||
|
||||
it("extension-tools.md tool parameter tables match extension.ts registrations", () => {
|
||||
const extensionTools = getExtensionToolParamNames();
|
||||
const documentedTools = getDocumentedToolParamNames();
|
||||
|
||||
for (const [toolName, extensionParams] of extensionTools.entries()) {
|
||||
expect(documentedTools.has(toolName), `missing documented section for ${toolName}`).toBe(true);
|
||||
const documentedParams = documentedTools.get(toolName) ?? [];
|
||||
expect(documentedParams).toEqual(extensionParams);
|
||||
}
|
||||
});
|
||||
|
||||
it("SKILL.md tool listing includes all registered tools", () => {
|
||||
const extensionTools = getExtensionToolNames();
|
||||
const skillTools = getSkillMdToolNames();
|
||||
|
||||
const missingFromSkill = extensionTools.filter(
|
||||
(t) => !skillTools.includes(t),
|
||||
);
|
||||
expect(missingFromSkill).toEqual([]);
|
||||
});
|
||||
|
||||
it("SKILL.md documents workflow authoring tools in a dedicated category", () => {
|
||||
const skillMd = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
||||
const workflowCategory = skillMd.match(/- \*\*Workflow tools\*\* — ([^\n]+)/)?.[1] ?? "";
|
||||
|
||||
expect(workflowCategory).toContain("`fn_workflow_create`");
|
||||
expect(workflowCategory).toContain("`fn_workflow_update`");
|
||||
expect(workflowCategory).toContain("`fn_trait_list`");
|
||||
});
|
||||
|
||||
it("fusion-capabilities.md tool table includes all registered tools", () => {
|
||||
const extensionTools = getExtensionToolNames();
|
||||
const capTools = getCapabilitiesToolNames();
|
||||
|
||||
const missingFromCaps = extensionTools.filter(
|
||||
(t) => !capTools.includes(t),
|
||||
);
|
||||
expect(missingFromCaps).toEqual([]);
|
||||
expect(capTools).toEqual(extensionTools);
|
||||
});
|
||||
|
||||
it("fusion-capabilities.md tool table is auto-generated with markers", () => {
|
||||
const doc = readFileSync(resolve(skillDir, "references/fusion-capabilities.md"), "utf-8");
|
||||
expect(doc).toContain(CAP_TABLE_BEGIN);
|
||||
expect(doc).toContain(CAP_TABLE_END);
|
||||
});
|
||||
|
||||
it("extension-tools.md has auto-generated markers", () => {
|
||||
const doc = readFileSync(resolve(skillDir, "references/extension-tools.md"), "utf-8");
|
||||
expect(doc).toContain(EXT_TOOLS_BEGIN);
|
||||
expect(doc).toContain(EXT_TOOLS_END);
|
||||
});
|
||||
|
||||
it("extension-tools.md includes workflow tool reference sections", () => {
|
||||
const doc = readFileSync(resolve(skillDir, "references/extension-tools.md"), "utf-8");
|
||||
const workflowSection = doc.match(/## Workflow Tools\n([\s\S]*?)\n## GitHub Tools/)?.[1] ?? "";
|
||||
|
||||
expect(workflowSection).toContain("### fn_workflow_create");
|
||||
expect(workflowSection).toContain("### fn_workflow_settings");
|
||||
expect(workflowSection).toContain("### fn_trait_list");
|
||||
expect(workflowSection).toContain("| `ir` | unknown | ✓ |");
|
||||
});
|
||||
|
||||
it("engine-tools.md documents all engine session-scoped tools", () => {
|
||||
const engineTools = getEngineSessionToolNames();
|
||||
const documented = getDocumentedEngineToolNames();
|
||||
const missing = engineTools.filter((name) => !documented.includes(name));
|
||||
// FNXC:SkillSync 2026-06-17-23:06: This test enforces the invariant that every engine session-scoped `fn_*` registration across the engine source set must be mirrored in `engine-tools.md`, so failures must print the exact undocumented names instead of hiding drift behind a generic deep-equality diff.
|
||||
expect(
|
||||
missing,
|
||||
`undocumented engine tools in engine-tools.md: ${missing.join(", ") || "none"}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("covers the full Fusion skill markdown surface", () => {
|
||||
const markdownFiles = collectMarkdownFiles(skillDir).map((filePath) =>
|
||||
relative(skillDir, filePath),
|
||||
);
|
||||
|
||||
expect(markdownFiles).toEqual([
|
||||
"SKILL.md",
|
||||
"references/best-practices.md",
|
||||
"references/cli-commands.md",
|
||||
"references/engine-tools.md",
|
||||
"references/extension-tools.md",
|
||||
"references/fusion-capabilities.md",
|
||||
"references/skill-patterns.md",
|
||||
"references/task-structure.md",
|
||||
"workflows/dashboard-cli.md",
|
||||
"workflows/specifications.md",
|
||||
"workflows/task-lifecycle.md",
|
||||
"workflows/task-management.md",
|
||||
]);
|
||||
});
|
||||
|
||||
it("enforces fn_* naming across extension + all skill markdown for public tools", () => {
|
||||
const extensionTools = getExtensionToolNames();
|
||||
const publicSuffixes = extensionTools.map((toolName) =>
|
||||
toolName.replace(/^fn_/, ""),
|
||||
);
|
||||
|
||||
const allowedUnprefixedInternalTools = new Set([
|
||||
"task_create",
|
||||
"task_update",
|
||||
"task_log",
|
||||
"task_done",
|
||||
"review_step",
|
||||
"spawn_agent",
|
||||
]);
|
||||
|
||||
const forbiddenSuffixes = publicSuffixes.filter(
|
||||
(suffix) => !allowedUnprefixedInternalTools.has(suffix),
|
||||
);
|
||||
|
||||
const filesToScan = [extensionPath, ...collectMarkdownFiles(skillDir)];
|
||||
const violations: string[] = [];
|
||||
|
||||
for (const filePath of filesToScan) {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
const relativePath = relative(cliRoot, filePath);
|
||||
|
||||
for (const suffix of forbiddenSuffixes) {
|
||||
const regex = new RegExp(`(?<!fn_)\\b${escapeRegex(suffix)}\\b`, "g");
|
||||
if (regex.test(content)) {
|
||||
violations.push(`${relativePath}: ${suffix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("/fn command is documented in the skill", () => {
|
||||
const skillMd = readFileSync(resolve(skillDir, "SKILL.md"), "utf-8");
|
||||
expect(skillMd).toContain("/fn");
|
||||
|
||||
const dashboardCli = readFileSync(
|
||||
resolve(skillDir, "workflows/dashboard-cli.md"),
|
||||
"utf-8",
|
||||
);
|
||||
expect(dashboardCli).toContain("/fn");
|
||||
});
|
||||
|
||||
it("SKILL.md and reference generated blocks match sync script output (no drift)", () => {
|
||||
const script = resolve(repoRoot, "scripts/sync-fusion-skill-tools.mjs");
|
||||
const result = spawnSync("node", [script, "--check"], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`sync-fusion-skill-tools --check failed:\n${result.stderr || result.stdout}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("package.json includes skills in pi config and files array", () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(resolve(cliRoot, "package.json"), "utf-8"),
|
||||
);
|
||||
expect(pkg.pi.skills).toContain("./skill");
|
||||
expect(pkg.files).toContain("skill/**");
|
||||
});
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const repoRoot = join(import.meta.dirname!, "..", "..", "..", "..");
|
||||
|
||||
describe("Changeset configuration", () => {
|
||||
// Release guardrail: These tests protect the @runfusion/fusion release pipeline
|
||||
// by ensuring changeset configuration required for npm publishing remains intact.
|
||||
// If these tests fail, the automated release workflow will break.
|
||||
it("should have a valid .changeset/config.json", () => {
|
||||
const configPath = join(repoRoot, ".changeset", "config.json");
|
||||
expect(existsSync(configPath)).toBe(true);
|
||||
|
||||
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
expect(config).toBeDefined();
|
||||
expect(typeof config).toBe("object");
|
||||
});
|
||||
|
||||
it("should have baseBranch set to 'main' for the default branch", () => {
|
||||
const configPath = join(repoRoot, ".changeset", "config.json");
|
||||
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
expect(config.baseBranch).toBe("main");
|
||||
});
|
||||
|
||||
it("should have changeset scripts in root package.json", () => {
|
||||
// These scripts drive the changesets CLI workflow: changeset (add), version (bump), release:version (apply + sync workspace version)
|
||||
const pkgPath = join(repoRoot, "package.json");
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
||||
|
||||
expect(pkg.scripts.changeset).toBe("changeset");
|
||||
expect(pkg.scripts.version).toBe("changeset version");
|
||||
// FNXC:ReleasePipeline 2026-06-24-23:50: release:version now includes run-ci-distill.mjs to distill changelog entries after version bump.
|
||||
expect(pkg.scripts["release:version"]).toBe("changeset version && node scripts/sync-workspace-version.mjs && node scripts/run-ci-distill.mjs");
|
||||
});
|
||||
|
||||
it("should keep the workspace package.json version aligned with the published CLI package", () => {
|
||||
const workspacePkgPath = join(repoRoot, "package.json");
|
||||
const cliPkgPath = join(repoRoot, "packages", "cli", "package.json");
|
||||
const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, "utf-8"));
|
||||
const cliPkg = JSON.parse(readFileSync(cliPkgPath, "utf-8"));
|
||||
|
||||
expect(workspacePkg.version).toBe(cliPkg.version);
|
||||
});
|
||||
|
||||
it("should have .github/workflows/version.yml configured for manual releases", () => {
|
||||
const workflowPath = join(
|
||||
repoRoot,
|
||||
".github",
|
||||
"workflows",
|
||||
"version.yml",
|
||||
);
|
||||
expect(existsSync(workflowPath)).toBe(true);
|
||||
|
||||
const content = readFileSync(workflowPath, "utf-8");
|
||||
// Guardrail: workflow must use changesets/action for npm publishing
|
||||
expect(content).toContain("changesets/action");
|
||||
// Guardrail: workflow must be manually triggered (auto-trigger disabled for safety)
|
||||
expect(content).toContain("workflow_dispatch");
|
||||
expect(content).toContain("Auto-trigger disabled");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { BUNDLED_PLUGIN_IDS } from "../bundled-plugin-install.js";
|
||||
import { findStaleBundledPlugins } from "../bundled-plugin-freshness.js";
|
||||
import { ALL_STAGED_BUNDLED_IDS } from "../staged-bundled-plugin-ids.js";
|
||||
|
||||
const older = new Date("2026-01-01T00:00:00.000Z");
|
||||
const newer = new Date("2026-01-01T00:01:00.000Z");
|
||||
|
||||
describe("bundled plugin build freshness", () => {
|
||||
let tempRoot: string | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
if (tempRoot) {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
tempRoot = null;
|
||||
}
|
||||
});
|
||||
|
||||
function makeTempPluginsRoot(): string {
|
||||
tempRoot = mkdtempSync(join(tmpdir(), "bundled-plugin-freshness-"));
|
||||
return tempRoot;
|
||||
}
|
||||
|
||||
function writePluginFile(pluginsRoot: string, pluginId: string, relativePath: string, content = "// test fixture\n") {
|
||||
const fullPath = join(pluginsRoot, pluginId, relativePath);
|
||||
mkdirSync(dirname(fullPath), { recursive: true });
|
||||
writeFileSync(fullPath, content);
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
it("reports stale compiled dist while allowing fresh and dist-absent plugins", () => {
|
||||
const pluginsRoot = makeTempPluginsRoot();
|
||||
|
||||
const staleSrc = writePluginFile(pluginsRoot, "fixture-stale", "src/index.ts");
|
||||
const staleDist = writePluginFile(pluginsRoot, "fixture-stale", "dist/index.js");
|
||||
utimesSync(staleDist, older, older);
|
||||
utimesSync(staleSrc, newer, newer);
|
||||
|
||||
const freshSrc = writePluginFile(pluginsRoot, "fixture-fresh", "src/index.ts");
|
||||
const freshDist = writePluginFile(pluginsRoot, "fixture-fresh", "dist/index.js");
|
||||
utimesSync(freshSrc, older, older);
|
||||
utimesSync(freshDist, newer, newer);
|
||||
|
||||
writePluginFile(pluginsRoot, "fixture-dist-absent", "src/index.ts");
|
||||
|
||||
const stale = findStaleBundledPlugins(["fixture-stale", "fixture-fresh", "fixture-dist-absent"], {
|
||||
pluginsRoot,
|
||||
});
|
||||
|
||||
expect(stale).toHaveLength(1);
|
||||
expect(stale[0]).toMatchObject({ id: "fixture-stale" });
|
||||
expect(stale[0]?.reason).toContain("run pnpm build");
|
||||
});
|
||||
|
||||
it("keeps the live staged bundled-plugin set fresh after build", () => {
|
||||
expect(findStaleBundledPlugins(ALL_STAGED_BUNDLED_IDS)).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the auto-install list covered by the staged bundled-plugin set", () => {
|
||||
const staged = new Set<string>(ALL_STAGED_BUNDLED_IDS);
|
||||
const missingFromStagedSet = BUNDLED_PLUGIN_IDS.filter((id) => !staged.has(id));
|
||||
|
||||
expect(missingFromStagedSet).toEqual([]);
|
||||
|
||||
/*
|
||||
* FNXC:BundledPlugins 2026-06-17-22:06:
|
||||
* The staged set intentionally remains a superset today: droid/acp runtimes are shipped for explicit runtime selection but are not part of the default auto-install list. Use subset coverage, not equality, until product requirements say those runtimes should auto-install.
|
||||
*/
|
||||
expect(new Set(BUNDLED_PLUGIN_IDS)).not.toEqual(staged);
|
||||
expect(ALL_STAGED_BUNDLED_IDS).toEqual(expect.arrayContaining(["fusion-plugin-droid-runtime", "fusion-plugin-acp-runtime"]));
|
||||
});
|
||||
});
|
||||
@@ -44,23 +44,30 @@ const quarantinedCliTests: string[] = [
|
||||
/*
|
||||
FNXC:CliTests 2026-06-25-14:00:
|
||||
The SQLite-to-PostgreSQL cutover (feature quarantine-sqlite-internals-tests, retry session)
|
||||
quarantines pre-existing CLI test failures observed during verify:workspace. Root causes vary:
|
||||
recorded pre-existing CLI test failures observed during verify:workspace. Root causes varied:
|
||||
- extension-fn-secret-get.test.ts: store.getAsyncLayer mock drift (async-satellite dual-path).
|
||||
- chat.test.ts: MessageStore.getInbox returns non-array under Node 26 node:sqlite (SQLite-path).
|
||||
- skill-sync.test.ts: undocumented engine tools (fn_acquire_repo_worktree, fn_artifact_*).
|
||||
- version.test.ts: changeset script assertion drift (project now uses scripts/release.mjs).
|
||||
- dashboard.test.ts: mesh lifecycle mock assertion drift.
|
||||
- bundled-plugin-freshness.test.ts: bundled plugin build freshness drift.
|
||||
Quarantined on sight per AGENTS.md flaky-test rule so verify:workspace goes green.
|
||||
The entries were quarantined on sight per AGENTS.md; FN-8219 deleted the five
|
||||
in-scope expired entries on 2026-07-17 rather than rescuing or re-recording them.
|
||||
|
||||
FNXC:CliTests 2026-07-17-09:45:
|
||||
FN-8210 restores package-config.test.ts to the package lane after the direct green run proved its failures were stale tsup plugin-external and verify:workspace expectations, not flaky behavior. Its old exclusion had no matching ledger entry; do not re-quarantine without new root-cause evidence and a lockstep ledger entry.
|
||||
*/
|
||||
"src/__tests__/extension-fn-secret-get.test.ts",
|
||||
"src/__tests__/skill-sync.test.ts",
|
||||
"src/__tests__/version.test.ts",
|
||||
"src/commands/__tests__/dashboard.test.ts",
|
||||
"src/plugins/__tests__/bundled-plugin-freshness.test.ts",
|
||||
/*
|
||||
FNXC:CliTests 2026-07-17-10:00:
|
||||
FN-8219 reconciled the five 2026-06-25 config-only quarantines after their
|
||||
2026-07-09 deletion deadline. Per docs/testing.md, expired quarantines are
|
||||
deleted rather than rescued or re-recorded: extension-fn-secret-get
|
||||
(async-layer mock drift), skill-sync (undocumented engine tools), version
|
||||
(release-script assertion drift), dashboard (mesh lifecycle mock drift), and
|
||||
bundled-plugin-freshness (build freshness drift) are now git-history-only.
|
||||
The quarantine ledger has no packages/cli rows. FN-8210 owns the separate
|
||||
package-config.test.ts resolution; future lockstep coverage must include it.
|
||||
*/
|
||||
/*
|
||||
FNXC:CliTests 2026-06-25-16:30:
|
||||
The SQLite-to-PostgreSQL cutover (feature delete-sqlite-runtime-final, PHASE A)
|
||||
|
||||
Reference in New Issue
Block a user