171 lines
8.2 KiB
TypeScript
171 lines
8.2 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { getEnvVariableDefinition } from "@toolctrl/config";
|
|
import { loadFeatureManifests } from "@toolctrl/plugins";
|
|
import { queueNames } from "@toolctrl/queue";
|
|
|
|
const manifestLoad = await loadFeatureManifests();
|
|
const manifests = manifestLoad.manifests;
|
|
|
|
const requiredPlaybooks = [
|
|
"docs/agent-entrypoints.md",
|
|
"docs/agent-playbooks/feature-integration.md",
|
|
"docs/agent-playbooks/mvp-intake.md",
|
|
"docs/agent-playbooks/admin-managed-features.md",
|
|
"docs/agent-playbooks/worker-backed-features.md",
|
|
];
|
|
|
|
function expectStringArray(value: unknown) {
|
|
expect(Array.isArray(value)).toBe(true);
|
|
expect((value as unknown[]).every((item) => typeof item === "string")).toBe(true);
|
|
}
|
|
|
|
describe("agent feature planning docs", () => {
|
|
it("provides the required agent entrypoint and playbooks", () => {
|
|
requiredPlaybooks.forEach((file) => {
|
|
expect(fs.existsSync(path.join(process.cwd(), file)), file).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("documents the key concepts agents need before implementation", () => {
|
|
const entrypoint = fs.readFileSync(path.join(process.cwd(), "docs/agent-entrypoints.md"), "utf8");
|
|
const featureIntegration = fs.readFileSync(path.join(process.cwd(), "docs/agent-playbooks/feature-integration.md"), "utf8");
|
|
const adminManaged = fs.readFileSync(path.join(process.cwd(), "docs/agent-playbooks/admin-managed-features.md"), "utf8");
|
|
const workerBacked = fs.readFileSync(path.join(process.cwd(), "docs/agent-playbooks/worker-backed-features.md"), "utf8");
|
|
|
|
expect(entrypoint).toContain("MVP import");
|
|
expect(featureIntegration).toContain("Feature Integration Playbook");
|
|
expect(featureIntegration).toContain("Do not expose provider secrets");
|
|
expect(adminManaged).toContain("admin-managed");
|
|
expect(adminManaged).toContain("Provider keys must not be sent to tenant loaders");
|
|
expect(workerBacked).toContain("Browser-backed features require extra constraints");
|
|
});
|
|
});
|
|
|
|
describe("feature manifests", () => {
|
|
it("keeps every manifest on the integration-aware schema", () => {
|
|
manifests.forEach((manifest) => {
|
|
expect(manifest.schemaVersion).toBe("1.0");
|
|
expect(manifest.id).toMatch(/^[a-z0-9][a-z0-9-]*$/);
|
|
expect(manifest.title).toEqual(expect.any(String));
|
|
expect(["core", "plugin-candidate", "remove-candidate", "mixed"]).toContain(manifest.classification);
|
|
expect(["active", "proposed", "review-needed"]).toContain(manifest.status);
|
|
expectStringArray(manifest.rationale);
|
|
expectStringArray(manifest.codeAreas);
|
|
expectStringArray(manifest.dependencies);
|
|
expectStringArray(manifest.consumers);
|
|
expectStringArray(manifest.boundaries.mustKeep);
|
|
expectStringArray(manifest.boundaries.mayMoveToPlugin);
|
|
expectStringArray(manifest.boundaries.mustNotTouch);
|
|
expectStringArray(manifest.pruning.notes);
|
|
expectStringArray(manifest.validation.source);
|
|
expect(manifest.validation.lastReviewed).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
});
|
|
});
|
|
|
|
it("requires explicit ownership, surfaces, configuration, workers and UAT", () => {
|
|
manifests.forEach((manifest) => {
|
|
expect(["tenant-owned", "admin-managed", "hybrid", "system-only"]).toContain(manifest.integration.ownership);
|
|
expectStringArray(manifest.integration.surfaces.adminRoutes);
|
|
expectStringArray(manifest.integration.surfaces.tenantRoutes);
|
|
expectStringArray(manifest.integration.surfaces.publicRoutes);
|
|
expectStringArray(manifest.integration.surfaces.apiRoutes);
|
|
expectStringArray(manifest.integration.configuration.admin);
|
|
expectStringArray(manifest.integration.configuration.tenant);
|
|
expectStringArray(manifest.integration.configuration.environment);
|
|
expectStringArray(manifest.integration.configuration.secrets);
|
|
expect(typeof manifest.integration.workers.required).toBe("boolean");
|
|
expectStringArray(manifest.integration.workers.queues);
|
|
expectStringArray(manifest.integration.workers.jobs);
|
|
expectStringArray(manifest.integration.workers.notes);
|
|
expectStringArray(manifest.integration.externalProviders);
|
|
expectStringArray(manifest.integration.permissions);
|
|
expectStringArray(manifest.integration.auditEvents);
|
|
expectStringArray(manifest.integration.billingImpacts);
|
|
expectStringArray(manifest.integration.uat);
|
|
expect(manifest.integration.uat.length, manifest.id).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("loads real docs manifests without loader diagnostics", () => {
|
|
expect(manifestLoad.ok).toBe(true);
|
|
expect(manifestLoad.diagnostics).toEqual([]);
|
|
});
|
|
|
|
it("guards the Feature Manifest manifest decision contract in docs", () => {
|
|
const inventory = fs.readFileSync(path.join(process.cwd(), "docs/feature-inventory.md"), "utf8");
|
|
const manifestReadme = fs.readFileSync(path.join(process.cwd(), "docs/feature-manifests/README.md"), "utf8");
|
|
|
|
["Manifest-Decision", "Pruned Core", "Full-Boilerplate", "plugin-candidate", "remove-candidate"].forEach((anchor) => {
|
|
expect(inventory, `inventory missing ${anchor}`).toContain(anchor);
|
|
expect(manifestReadme, `manifest README missing ${anchor}`).toContain(anchor);
|
|
});
|
|
|
|
["core", "plugin-candidates", "remove-candidates"].forEach((manifestIdAnchor) => {
|
|
expect(manifestReadme, `manifest README missing ${manifestIdAnchor}`).toContain(manifestIdAnchor);
|
|
});
|
|
});
|
|
|
|
it("guards admin-managed secret ownership and worker-backed declarations", () => {
|
|
manifests
|
|
.filter((manifest) => manifest.integration.ownership === "admin-managed")
|
|
.forEach((manifest) => {
|
|
expect(manifest.integration.configuration.admin.length, manifest.id).toBeGreaterThan(0);
|
|
expect(manifest.integration.configuration.secrets.join(" ").toLowerCase(), manifest.id).toMatch(/admin|platform/);
|
|
expect(manifest.integration.permissions.some((permission) => permission.startsWith("admin.")), manifest.id).toBe(true);
|
|
expect(manifest.integration.auditEvents.length, manifest.id).toBeGreaterThan(0);
|
|
});
|
|
|
|
manifests
|
|
.filter((manifest) => manifest.integration.workers.required)
|
|
.forEach((manifest) => {
|
|
expect(manifest.integration.workers.queues.length, manifest.id).toBeGreaterThan(0);
|
|
expect(manifest.integration.workers.jobs.length, manifest.id).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
it("includes the AI chatbot reference pattern for admin-managed worker-backed features", () => {
|
|
const chatbot = manifests.find((manifest) => manifest.id === "ai-chatbot-admin-managed");
|
|
|
|
expect(chatbot).toBeDefined();
|
|
expect(chatbot?.integration.ownership).toBe("admin-managed");
|
|
expect(chatbot?.integration.externalProviders).toContain("deployment-region-ai-provider");
|
|
expect(chatbot?.integration.workers.required).toBe(true);
|
|
expect(chatbot?.integration.workers.queues).toContain(queueNames.aiChatbot);
|
|
expect(chatbot?.integration.workers.queues).toContain(queueNames.browserAutomation);
|
|
expect(chatbot?.integration.workers.jobs).toEqual(expect.arrayContaining(["chat-completion", "browser-session-run", "browser-session-cleanup", "usage-flush"]));
|
|
expect(chatbot?.integration.configuration.environment).toEqual(
|
|
expect.arrayContaining([
|
|
"MISTRAL_API_KEY",
|
|
"MISTRAL_MODEL",
|
|
"SCALEWAY_API_KEY",
|
|
"SCALEWAY_MODEL",
|
|
"SCALEWAY_BASE_URL",
|
|
"ALEPH_ALPHA_API_KEY",
|
|
"ALEPH_ALPHA_MODEL",
|
|
"ALEPH_ALPHA_BASE_URL",
|
|
"BULLMQ_REDIS_URL",
|
|
"BROWSER_RUNTIME_URL",
|
|
])
|
|
);
|
|
expect(chatbot?.integration.configuration.secrets.join(" ")).toContain("never be exposed");
|
|
});
|
|
|
|
it("keeps admin-managed AI/browser feature contracts backed by config and queue packages", () => {
|
|
[
|
|
"BULLMQ_REDIS_URL",
|
|
"BROWSER_RUNTIME_URL",
|
|
"BROWSER_RUNTIME_API_KEY",
|
|
"BROWSER_SESSION_TIMEOUT_MS",
|
|
].forEach((name) => {
|
|
const definition = getEnvVariableDefinition(name);
|
|
expect(definition?.classification, name).toBe("optional-provider");
|
|
expect(definition?.targets.some((target) => target === "api" || target === "worker"), name).toBe(true);
|
|
});
|
|
|
|
expect(queueNames.aiChatbot).toBe("toolctrl.ai-chatbot");
|
|
expect(queueNames.browserAutomation).toBe("toolctrl.browser-automation");
|
|
});
|
|
});
|