@synth-deploy/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import type { FastifyInstance } from "fastify";
|
|
4
|
+
import { DecisionDebrief, PartitionStore, EnvironmentStore, ArtifactStore, SettingsStore, TelemetryStore, LlmClient } from "@synth-deploy/core";
|
|
5
|
+
import type { LlmResult } from "@synth-deploy/core";
|
|
6
|
+
import { SynthAgent, InMemoryDeploymentStore } from "../src/agent/synth-agent.js";
|
|
7
|
+
import { registerDeploymentRoutes } from "../src/api/deployments.js";
|
|
8
|
+
import { registerPartitionRoutes } from "../src/api/partitions.js";
|
|
9
|
+
import { registerEnvironmentRoutes } from "../src/api/environments.js";
|
|
10
|
+
import { registerArtifactRoutes } from "../src/api/artifacts.js";
|
|
11
|
+
import { registerAgentRoutes, sanitizeUserInput, validateExtractedVersion, validateExtractedVariables } from "../src/api/agent.js";
|
|
12
|
+
import { registerSettingsRoutes } from "../src/api/settings.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Mock auth — inject a test user with all permissions on every request
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function addMockAuth(app: FastifyInstance) {
|
|
19
|
+
app.addHook("onRequest", async (request) => {
|
|
20
|
+
request.user = {
|
|
21
|
+
id: "test-user-id" as any,
|
|
22
|
+
email: "test@example.com",
|
|
23
|
+
name: "Test User",
|
|
24
|
+
permissions: [
|
|
25
|
+
"deployment.create", "deployment.approve", "deployment.reject", "deployment.view", "deployment.rollback",
|
|
26
|
+
"artifact.create", "artifact.update", "artifact.annotate", "artifact.delete", "artifact.view",
|
|
27
|
+
"environment.create", "environment.update", "environment.delete", "environment.view",
|
|
28
|
+
"partition.create", "partition.update", "partition.delete", "partition.view",
|
|
29
|
+
"envoy.register", "envoy.configure", "envoy.view",
|
|
30
|
+
"settings.manage", "users.manage", "roles.manage",
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Test server setup
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
let app: FastifyInstance;
|
|
41
|
+
let diary: DecisionDebrief;
|
|
42
|
+
let partitions: PartitionStore;
|
|
43
|
+
let environments: EnvironmentStore;
|
|
44
|
+
let deployments: InMemoryDeploymentStore;
|
|
45
|
+
let artifactStore: ArtifactStore;
|
|
46
|
+
let settings: SettingsStore;
|
|
47
|
+
let telemetry: TelemetryStore;
|
|
48
|
+
let agent: SynthAgent;
|
|
49
|
+
|
|
50
|
+
let artifactId: string;
|
|
51
|
+
let partitionId: string;
|
|
52
|
+
let productionEnvId: string;
|
|
53
|
+
let stagingEnvId: string;
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
diary = new DecisionDebrief();
|
|
57
|
+
partitions = new PartitionStore();
|
|
58
|
+
environments = new EnvironmentStore();
|
|
59
|
+
deployments = new InMemoryDeploymentStore();
|
|
60
|
+
artifactStore = new ArtifactStore();
|
|
61
|
+
settings = new SettingsStore();
|
|
62
|
+
telemetry = new TelemetryStore();
|
|
63
|
+
agent = new SynthAgent(
|
|
64
|
+
diary, deployments, artifactStore, environments, partitions,
|
|
65
|
+
undefined, { healthCheckBackoffMs: 1, executionDelayMs: 1 },
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
app = Fastify();
|
|
69
|
+
addMockAuth(app);
|
|
70
|
+
registerDeploymentRoutes(app, deployments, diary, partitions, environments, artifactStore, settings, telemetry);
|
|
71
|
+
registerPartitionRoutes(app, partitions, deployments, diary, telemetry);
|
|
72
|
+
registerEnvironmentRoutes(app, environments, deployments, telemetry);
|
|
73
|
+
registerArtifactRoutes(app, artifactStore, telemetry);
|
|
74
|
+
registerSettingsRoutes(app, settings, telemetry);
|
|
75
|
+
registerAgentRoutes(app, agent, partitions, environments, artifactStore, deployments, diary, settings);
|
|
76
|
+
|
|
77
|
+
await app.ready();
|
|
78
|
+
|
|
79
|
+
// Seed test data
|
|
80
|
+
const envRes = await app.inject({
|
|
81
|
+
method: "POST",
|
|
82
|
+
url: "/api/environments",
|
|
83
|
+
payload: { name: "production", variables: { APP_ENV: "production", LOG_LEVEL: "warn" } },
|
|
84
|
+
});
|
|
85
|
+
productionEnvId = JSON.parse(envRes.payload).environment.id;
|
|
86
|
+
|
|
87
|
+
const stagingRes = await app.inject({
|
|
88
|
+
method: "POST",
|
|
89
|
+
url: "/api/environments",
|
|
90
|
+
payload: { name: "staging", variables: { APP_ENV: "staging", LOG_LEVEL: "debug" } },
|
|
91
|
+
});
|
|
92
|
+
stagingEnvId = JSON.parse(stagingRes.payload).environment.id;
|
|
93
|
+
|
|
94
|
+
const artifactRes = await app.inject({
|
|
95
|
+
method: "POST",
|
|
96
|
+
url: "/api/artifacts",
|
|
97
|
+
payload: { name: "web-app", type: "nodejs" },
|
|
98
|
+
});
|
|
99
|
+
artifactId = JSON.parse(artifactRes.payload).artifact.id;
|
|
100
|
+
|
|
101
|
+
const partitionRes = await app.inject({
|
|
102
|
+
method: "POST",
|
|
103
|
+
url: "/api/partitions",
|
|
104
|
+
payload: { name: "Acme Corp" },
|
|
105
|
+
});
|
|
106
|
+
partitionId = JSON.parse(partitionRes.payload).partition.id;
|
|
107
|
+
|
|
108
|
+
await app.inject({
|
|
109
|
+
method: "PUT",
|
|
110
|
+
url: `/api/partitions/${partitionId}/variables`,
|
|
111
|
+
payload: { variables: { DB_HOST: "acme-db-1", APP_ENV: "production" } },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Helper: creates a deployment via the new artifact-based API.
|
|
117
|
+
*/
|
|
118
|
+
async function deployViaHttp(
|
|
119
|
+
server: FastifyInstance,
|
|
120
|
+
params: { artifactId: string; partitionId?: string; environmentId: string; version?: string; variables?: Record<string, string> },
|
|
121
|
+
) {
|
|
122
|
+
return server.inject({
|
|
123
|
+
method: "POST",
|
|
124
|
+
url: "/api/deployments",
|
|
125
|
+
payload: {
|
|
126
|
+
artifactId: params.artifactId,
|
|
127
|
+
environmentId: params.environmentId,
|
|
128
|
+
partitionId: params.partitionId,
|
|
129
|
+
version: params.version,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Deployment context tests
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
describe("Agent mode — deployment context", () => {
|
|
139
|
+
it("returns context with signals and environment summary", async () => {
|
|
140
|
+
// Trigger a deployment first to have some data
|
|
141
|
+
await deployViaHttp(app, { artifactId, partitionId, environmentId: productionEnvId, version: "1.0.0" });
|
|
142
|
+
|
|
143
|
+
const res = await app.inject({ method: "GET", url: "/api/agent/context" });
|
|
144
|
+
|
|
145
|
+
expect(res.statusCode).toBe(200);
|
|
146
|
+
const context = JSON.parse(res.payload);
|
|
147
|
+
|
|
148
|
+
expect(context.recentActivity).toBeDefined();
|
|
149
|
+
expect(context.recentActivity.deploymentsLast24h).toBeGreaterThanOrEqual(1);
|
|
150
|
+
expect(context.recentActivity.successRate).toBeDefined();
|
|
151
|
+
expect(context.environmentSummary).toBeDefined();
|
|
152
|
+
expect(context.environmentSummary.length).toBeGreaterThanOrEqual(2);
|
|
153
|
+
expect(context.signals).toBeDefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("environment summary reflects deployment status", async () => {
|
|
157
|
+
const res = await app.inject({ method: "GET", url: "/api/agent/context" });
|
|
158
|
+
const context = JSON.parse(res.payload);
|
|
159
|
+
|
|
160
|
+
const prodSummary = context.environmentSummary.find((e: any) => e.name === "production");
|
|
161
|
+
expect(prodSummary).toBeDefined();
|
|
162
|
+
expect(prodSummary.deployCount).toBeGreaterThanOrEqual(1);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Input sanitization tests
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
describe("Agent mode — input sanitization", () => {
|
|
172
|
+
it("strips control characters from intent", () => {
|
|
173
|
+
const input = "Deploy\x01\x02\x03\x07 web-app\x0B\x0C\x0E v1.0.0";
|
|
174
|
+
const result = sanitizeUserInput(input);
|
|
175
|
+
// Control characters should be removed
|
|
176
|
+
expect(result).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F]/);
|
|
177
|
+
// Printable content should remain
|
|
178
|
+
expect(result).toContain("Deploy");
|
|
179
|
+
expect(result).toContain("web-app");
|
|
180
|
+
expect(result).toContain("v1.0.0");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("truncates long inputs to 1000 characters", () => {
|
|
184
|
+
const longInput = "a".repeat(2000);
|
|
185
|
+
const result = sanitizeUserInput(longInput);
|
|
186
|
+
expect(result.length).toBe(1000);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("escapes XML tags in user input", () => {
|
|
190
|
+
const input = "<script>alert('xss')</script>";
|
|
191
|
+
const result = sanitizeUserInput(input);
|
|
192
|
+
expect(result).not.toContain("<script>");
|
|
193
|
+
expect(result).not.toContain("</script>");
|
|
194
|
+
expect(result).toContain("<script>");
|
|
195
|
+
expect(result).toContain("</script>");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("validates semver version format", () => {
|
|
199
|
+
// Valid formats
|
|
200
|
+
expect(validateExtractedVersion("1.2.3")).toBe(true);
|
|
201
|
+
expect(validateExtractedVersion("1.2.3-beta.1")).toBe(true);
|
|
202
|
+
expect(validateExtractedVersion("0.0.1")).toBe(true);
|
|
203
|
+
expect(validateExtractedVersion("10.20.30-alpha")).toBe(true);
|
|
204
|
+
|
|
205
|
+
// Invalid formats
|
|
206
|
+
expect(validateExtractedVersion("not-a-version")).toBe(false);
|
|
207
|
+
expect(validateExtractedVersion("1.2")).toBe(false);
|
|
208
|
+
expect(validateExtractedVersion("../../../etc/passwd")).toBe(false);
|
|
209
|
+
expect(validateExtractedVersion("v1.2.3")).toBe(false);
|
|
210
|
+
expect(validateExtractedVersion("")).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("validates variable key format", () => {
|
|
214
|
+
// Valid keys
|
|
215
|
+
const valid = validateExtractedVariables({ APP_ENV: "production", DB_HOST: "localhost" });
|
|
216
|
+
expect(valid).toHaveProperty("APP_ENV", "production");
|
|
217
|
+
expect(valid).toHaveProperty("DB_HOST", "localhost");
|
|
218
|
+
|
|
219
|
+
// Invalid keys should be excluded
|
|
220
|
+
const invalid = validateExtractedVariables({
|
|
221
|
+
"../../path": "value",
|
|
222
|
+
"key with spaces": "value",
|
|
223
|
+
"": "value",
|
|
224
|
+
"123invalid": "value",
|
|
225
|
+
});
|
|
226
|
+
expect(Object.keys(invalid)).toHaveLength(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("rejects variables with values exceeding 500 chars", () => {
|
|
230
|
+
const longValue = "x".repeat(600);
|
|
231
|
+
const result = validateExtractedVariables({ VALID_KEY: longValue, SHORT_KEY: "ok" });
|
|
232
|
+
expect(result).not.toHaveProperty("VALID_KEY");
|
|
233
|
+
expect(result).toHaveProperty("SHORT_KEY", "ok");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// LLM-powered query classification tests
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
describe("Agent mode — LLM query classification", () => {
|
|
243
|
+
let qApp: FastifyInstance;
|
|
244
|
+
let qDiary: DecisionDebrief;
|
|
245
|
+
let qPartitions: PartitionStore;
|
|
246
|
+
let qEnvironments: EnvironmentStore;
|
|
247
|
+
let qDeployments: InMemoryDeploymentStore;
|
|
248
|
+
let qArtifactStore: ArtifactStore;
|
|
249
|
+
let qSettings: SettingsStore;
|
|
250
|
+
let qTelemetry: TelemetryStore;
|
|
251
|
+
let qAgent: SynthAgent;
|
|
252
|
+
let qMockLlm: LlmClient;
|
|
253
|
+
|
|
254
|
+
let qArtifactId: string;
|
|
255
|
+
let qPartitionId: string;
|
|
256
|
+
let qProdEnvId: string;
|
|
257
|
+
let qStagingEnvId: string;
|
|
258
|
+
|
|
259
|
+
// Track what classify() should return for query classification
|
|
260
|
+
let qClassifyResponse: LlmResult;
|
|
261
|
+
|
|
262
|
+
beforeAll(async () => {
|
|
263
|
+
qDiary = new DecisionDebrief();
|
|
264
|
+
qPartitions = new PartitionStore();
|
|
265
|
+
qEnvironments = new EnvironmentStore();
|
|
266
|
+
qDeployments = new InMemoryDeploymentStore();
|
|
267
|
+
qArtifactStore = new ArtifactStore();
|
|
268
|
+
qSettings = new SettingsStore();
|
|
269
|
+
qTelemetry = new TelemetryStore();
|
|
270
|
+
qAgent = new SynthAgent(
|
|
271
|
+
qDiary, qDeployments, qArtifactStore, qEnvironments, qPartitions,
|
|
272
|
+
undefined, { healthCheckBackoffMs: 1, executionDelayMs: 1 },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
qMockLlm = new LlmClient(qDiary, "command", { apiKey: "test-key" });
|
|
276
|
+
qMockLlm.classify = async () => qClassifyResponse;
|
|
277
|
+
qMockLlm.isAvailable = () => true;
|
|
278
|
+
|
|
279
|
+
qApp = Fastify();
|
|
280
|
+
addMockAuth(qApp);
|
|
281
|
+
registerDeploymentRoutes(qApp, qDeployments, qDiary, qPartitions, qEnvironments, qArtifactStore, qSettings, qTelemetry);
|
|
282
|
+
registerPartitionRoutes(qApp, qPartitions, qDeployments, qDiary, qTelemetry);
|
|
283
|
+
registerEnvironmentRoutes(qApp, qEnvironments, qDeployments, qTelemetry);
|
|
284
|
+
registerArtifactRoutes(qApp, qArtifactStore, qTelemetry);
|
|
285
|
+
registerSettingsRoutes(qApp, qSettings, qTelemetry);
|
|
286
|
+
registerAgentRoutes(qApp, qAgent, qPartitions, qEnvironments, qArtifactStore, qDeployments, qDiary, qSettings, qMockLlm);
|
|
287
|
+
|
|
288
|
+
await qApp.ready();
|
|
289
|
+
|
|
290
|
+
// Seed test data
|
|
291
|
+
const envRes = await qApp.inject({
|
|
292
|
+
method: "POST",
|
|
293
|
+
url: "/api/environments",
|
|
294
|
+
payload: { name: "production", variables: { APP_ENV: "production" } },
|
|
295
|
+
});
|
|
296
|
+
qProdEnvId = JSON.parse(envRes.payload).environment.id;
|
|
297
|
+
|
|
298
|
+
const stagingRes = await qApp.inject({
|
|
299
|
+
method: "POST",
|
|
300
|
+
url: "/api/environments",
|
|
301
|
+
payload: { name: "staging", variables: { APP_ENV: "staging" } },
|
|
302
|
+
});
|
|
303
|
+
qStagingEnvId = JSON.parse(stagingRes.payload).environment.id;
|
|
304
|
+
|
|
305
|
+
const artifactRes = await qApp.inject({
|
|
306
|
+
method: "POST",
|
|
307
|
+
url: "/api/artifacts",
|
|
308
|
+
payload: { name: "web-app", type: "nodejs" },
|
|
309
|
+
});
|
|
310
|
+
qArtifactId = JSON.parse(artifactRes.payload).artifact.id;
|
|
311
|
+
|
|
312
|
+
const partRes = await qApp.inject({
|
|
313
|
+
method: "POST",
|
|
314
|
+
url: "/api/partitions",
|
|
315
|
+
payload: { name: "Acme Corp" },
|
|
316
|
+
});
|
|
317
|
+
qPartitionId = JSON.parse(partRes.payload).partition.id;
|
|
318
|
+
|
|
319
|
+
// Create a deployment so data queries have something to find
|
|
320
|
+
await deployViaHttp(qApp, { artifactId: qArtifactId, partitionId: qPartitionId, environmentId: qProdEnvId, version: "1.0.0" });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("navigate action: resolves 'show partition Acme Corp' to partition-detail", async () => {
|
|
324
|
+
qClassifyResponse = {
|
|
325
|
+
ok: true,
|
|
326
|
+
text: JSON.stringify({
|
|
327
|
+
action: "navigate",
|
|
328
|
+
view: "partition-detail",
|
|
329
|
+
params: { id: "Acme Corp" },
|
|
330
|
+
title: "Acme Corp",
|
|
331
|
+
}),
|
|
332
|
+
model: "claude-haiku-4-5-20251001",
|
|
333
|
+
responseTimeMs: 80,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const res = await qApp.inject({
|
|
337
|
+
method: "POST",
|
|
338
|
+
url: "/api/agent/query",
|
|
339
|
+
payload: { query: "show partition Acme Corp" },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(res.statusCode).toBe(200);
|
|
343
|
+
const result = JSON.parse(res.payload);
|
|
344
|
+
expect(result.action).toBe("navigate");
|
|
345
|
+
expect(result.view).toBe("partition-detail");
|
|
346
|
+
expect(result.params.id).toBe(qPartitionId);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("data action: resolves 'recent deployments' to deployment-list", async () => {
|
|
350
|
+
qClassifyResponse = {
|
|
351
|
+
ok: true,
|
|
352
|
+
text: JSON.stringify({
|
|
353
|
+
action: "data",
|
|
354
|
+
view: "deployment-list",
|
|
355
|
+
params: {},
|
|
356
|
+
title: "Recent Deployments",
|
|
357
|
+
}),
|
|
358
|
+
model: "claude-haiku-4-5-20251001",
|
|
359
|
+
responseTimeMs: 60,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const res = await qApp.inject({
|
|
363
|
+
method: "POST",
|
|
364
|
+
url: "/api/agent/query",
|
|
365
|
+
payload: { query: "recent deployments" },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(res.statusCode).toBe(200);
|
|
369
|
+
const result = JSON.parse(res.payload);
|
|
370
|
+
expect(result.action).toBe("data");
|
|
371
|
+
expect(result.view).toBe("deployment-list");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("create action: returns create intent for UI confirmation", async () => {
|
|
375
|
+
qClassifyResponse = {
|
|
376
|
+
ok: true,
|
|
377
|
+
text: JSON.stringify({
|
|
378
|
+
action: "create",
|
|
379
|
+
view: "partition-list",
|
|
380
|
+
params: { name: "New Corp" },
|
|
381
|
+
title: "Create Partition",
|
|
382
|
+
}),
|
|
383
|
+
model: "claude-haiku-4-5-20251001",
|
|
384
|
+
responseTimeMs: 90,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const res = await qApp.inject({
|
|
388
|
+
method: "POST",
|
|
389
|
+
url: "/api/agent/query",
|
|
390
|
+
payload: { query: "create partition New Corp" },
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
expect(res.statusCode).toBe(200);
|
|
394
|
+
const result = JSON.parse(res.payload);
|
|
395
|
+
// After #63, create actions are returned as-is for UI confirmation
|
|
396
|
+
expect(result.action).toBe("create");
|
|
397
|
+
expect(result.view).toBe("partition-list");
|
|
398
|
+
expect(result.params.name).toBe("New Corp");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("falls back to regex when LLM returns invalid JSON", async () => {
|
|
402
|
+
qClassifyResponse = {
|
|
403
|
+
ok: true,
|
|
404
|
+
text: "This is not valid JSON, just random text",
|
|
405
|
+
model: "claude-haiku-4-5-20251001",
|
|
406
|
+
responseTimeMs: 100,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const res = await qApp.inject({
|
|
410
|
+
method: "POST",
|
|
411
|
+
url: "/api/agent/query",
|
|
412
|
+
payload: { query: "show partition Acme Corp" },
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(res.statusCode).toBe(200);
|
|
416
|
+
const result = JSON.parse(res.payload);
|
|
417
|
+
// Regex fallback should still classify the query — it matches partition name + "show"
|
|
418
|
+
expect(result.action).toBe("navigate");
|
|
419
|
+
expect(result.view).toBe("partition-detail");
|
|
420
|
+
expect(result.params.id).toBe(qPartitionId);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("falls back to regex when LLM returns hallucinated entity names", async () => {
|
|
424
|
+
qClassifyResponse = {
|
|
425
|
+
ok: true,
|
|
426
|
+
text: JSON.stringify({
|
|
427
|
+
action: "navigate",
|
|
428
|
+
view: "partition-detail",
|
|
429
|
+
params: { id: "Nonexistent Partition" },
|
|
430
|
+
title: "Nonexistent Partition",
|
|
431
|
+
}),
|
|
432
|
+
model: "claude-haiku-4-5-20251001",
|
|
433
|
+
responseTimeMs: 90,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const res = await qApp.inject({
|
|
437
|
+
method: "POST",
|
|
438
|
+
url: "/api/agent/query",
|
|
439
|
+
payload: { query: "show partition Acme Corp" },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
expect(res.statusCode).toBe(200);
|
|
443
|
+
const result = JSON.parse(res.payload);
|
|
444
|
+
// classifyQueryWithLlm validates the partition name and returns null for unknown names,
|
|
445
|
+
// causing fallback to regex which finds "Acme Corp" in the query
|
|
446
|
+
expect(result.action).toBe("navigate");
|
|
447
|
+
expect(result.view).toBe("partition-detail");
|
|
448
|
+
expect(result.params.id).toBe(qPartitionId);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("falls back to regex when LLM call fails", async () => {
|
|
452
|
+
qClassifyResponse = {
|
|
453
|
+
ok: false,
|
|
454
|
+
fallback: true,
|
|
455
|
+
reason: "LLM rate limit exceeded (20 calls/min)",
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const res = await qApp.inject({
|
|
459
|
+
method: "POST",
|
|
460
|
+
url: "/api/agent/query",
|
|
461
|
+
payload: { query: "show all deployments" },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(res.statusCode).toBe(200);
|
|
465
|
+
const result = JSON.parse(res.payload);
|
|
466
|
+
// Regex fallback detects "deployments" keyword → returns inline markdown table
|
|
467
|
+
expect(result.action).toBe("answer");
|
|
468
|
+
expect(result.content).toBeDefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("records debrief entry for LLM-classified queries", async () => {
|
|
472
|
+
const existingIds = new Set(qDiary.getRecent(200).map((e) => e.id));
|
|
473
|
+
|
|
474
|
+
qClassifyResponse = {
|
|
475
|
+
ok: true,
|
|
476
|
+
text: JSON.stringify({
|
|
477
|
+
action: "data",
|
|
478
|
+
view: "deployment-list",
|
|
479
|
+
params: {},
|
|
480
|
+
title: "Deployments",
|
|
481
|
+
}),
|
|
482
|
+
model: "claude-haiku-4-5-20251001",
|
|
483
|
+
responseTimeMs: 50,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
await qApp.inject({
|
|
487
|
+
method: "POST",
|
|
488
|
+
url: "/api/agent/query",
|
|
489
|
+
payload: { query: "show all deployments" },
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const allEntries = qDiary.getRecent(200);
|
|
493
|
+
const newEntries = allEntries.filter((e) => !existingIds.has(e.id));
|
|
494
|
+
const queryEntry = newEntries.find(
|
|
495
|
+
(e) => e.decisionType === "system" && e.decision.includes("Canvas query"),
|
|
496
|
+
);
|
|
497
|
+
expect(queryEntry).toBeDefined();
|
|
498
|
+
expect(queryEntry!.decision).toContain("data");
|
|
499
|
+
expect(queryEntry!.decision).toContain("deployment-list");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("returns 400 for empty query", async () => {
|
|
503
|
+
const res = await qApp.inject({
|
|
504
|
+
method: "POST",
|
|
505
|
+
url: "/api/agent/query",
|
|
506
|
+
payload: { query: "" },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(res.statusCode).toBe(400);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("handles LLM response missing action field by falling back to regex", async () => {
|
|
513
|
+
qClassifyResponse = {
|
|
514
|
+
ok: true,
|
|
515
|
+
text: JSON.stringify({
|
|
516
|
+
view: "deployment-list",
|
|
517
|
+
params: {},
|
|
518
|
+
}),
|
|
519
|
+
model: "claude-haiku-4-5-20251001",
|
|
520
|
+
responseTimeMs: 100,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const res = await qApp.inject({
|
|
524
|
+
method: "POST",
|
|
525
|
+
url: "/api/agent/query",
|
|
526
|
+
payload: { query: "recent deployments" },
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(res.statusCode).toBe(200);
|
|
530
|
+
const result = JSON.parse(res.payload);
|
|
531
|
+
// Missing action field → classifyQueryWithLlm returns null → regex fallback
|
|
532
|
+
// "deployments" matches the deployment list pattern → returns inline markdown table
|
|
533
|
+
expect(result.action).toBe("answer");
|
|
534
|
+
expect(result.content).toBeDefined();
|
|
535
|
+
});
|
|
536
|
+
});
|