200 lines
5.7 KiB
TypeScript
200 lines
5.7 KiB
TypeScript
import { Pool } from "pg";
|
|
|
|
import { buildApp } from "../app.js";
|
|
import { loadConfig } from "../config.js";
|
|
import { runMigrations } from "../db/migrations.js";
|
|
|
|
const config = loadConfig();
|
|
const workspaceSlug = readRequiredEnv("SMOKE_WORKSPACE_SLUG");
|
|
const projectId = readRequiredEnv("SMOKE_PROJECT_ID");
|
|
|
|
if (!config.DATABASE_URL) {
|
|
throw new Error("DATABASE_URL is required for e2e smoke test.");
|
|
}
|
|
|
|
if (!config.NODEDC_INTERNAL_ACCESS_TOKEN) {
|
|
throw new Error("NODEDC_INTERNAL_ACCESS_TOKEN is required for e2e smoke test.");
|
|
}
|
|
|
|
const migrationPool = new Pool({ connectionString: config.DATABASE_URL });
|
|
await runMigrations(migrationPool);
|
|
await migrationPool.end();
|
|
|
|
const app = await buildApp({
|
|
...config,
|
|
LOG_LEVEL: process.env.LOG_LEVEL === "debug" ? "debug" : "silent",
|
|
});
|
|
|
|
try {
|
|
const suffix = Date.now().toString(36);
|
|
const agentId = await createAgent(suffix);
|
|
await upsertGrant(agentId);
|
|
const token = await createToken(agentId);
|
|
const authHeaders = { Authorization: `Bearer ${token}` };
|
|
|
|
const projects = await requestJson("GET", "/api/v1/tools/projects", authHeaders);
|
|
const context = await requestJson(
|
|
"GET",
|
|
`/api/v1/tools/projects/${projectId}/context?workspace_slug=${encodeURIComponent(workspaceSlug)}`,
|
|
authHeaders
|
|
);
|
|
const createIssuePayload = {
|
|
project_id: projectId,
|
|
workspace_slug: workspaceSlug,
|
|
title: `NODE.DC Codex API smoke ${suffix}`,
|
|
description: "Created by Agent Gateway e2e smoke test.",
|
|
priority: "medium",
|
|
structured_blocks: [
|
|
{
|
|
id: "current-architecture",
|
|
type: "text",
|
|
title: "Текущая архитектура",
|
|
body: "Gateway создал карточку через реальный Tasker internal adapter.",
|
|
},
|
|
{
|
|
id: "checker-1",
|
|
type: "checker",
|
|
title: "Чекер smoke",
|
|
items: [
|
|
{
|
|
id: "auth",
|
|
text: "Agent token accepted",
|
|
checked: true,
|
|
},
|
|
{
|
|
id: "tasker",
|
|
text: "Tasker adapter created issue",
|
|
checked: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const createHeaders = { ...authHeaders, "Idempotency-Key": `rest-create-${suffix}` };
|
|
const issue = await requestJson("POST", "/api/v1/tools/issues", createHeaders, createIssuePayload);
|
|
const replayedIssue = await requestJson("POST", "/api/v1/tools/issues", createHeaders, createIssuePayload);
|
|
|
|
const issueId = issue.issue.id as string;
|
|
assert(replayedIssue.issue.id === issueId, "idempotent REST create returns the original issue");
|
|
|
|
await requestJson("POST", `/api/v1/tools/issues/${issueId}/comments`, { ...authHeaders, "Idempotency-Key": `rest-comment-${suffix}` }, {
|
|
project_id: projectId,
|
|
workspace_slug: workspaceSlug,
|
|
body: "Smoke comment from Agent Gateway.",
|
|
});
|
|
|
|
const states = Array.isArray(context.states) ? context.states : [];
|
|
const targetState = states.find((state) => typeof state?.id === "string");
|
|
if (targetState) {
|
|
await requestJson(
|
|
"POST",
|
|
`/api/v1/tools/issues/${issueId}/move`,
|
|
{ ...authHeaders, "Idempotency-Key": `rest-move-${suffix}` },
|
|
{
|
|
project_id: projectId,
|
|
workspace_slug: workspaceSlug,
|
|
state_id: targetState.id,
|
|
}
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
tasker_url: config.NODEDC_TASKER_INTERNAL_URL,
|
|
workspace_slug: workspaceSlug,
|
|
project_id: projectId,
|
|
visible_projects: Array.isArray(projects.projects) ? projects.projects.length : null,
|
|
issue_id: issueId,
|
|
idempotent_replay: "passed",
|
|
moved: Boolean(targetState),
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
|
|
async function createAgent(suffix: string): Promise<string> {
|
|
const payload = await requestJson("POST", "/api/v1/agents", undefined, {
|
|
owner_user_id: `e2e-owner-${suffix}`,
|
|
owner_email: `e2e-${suffix}@example.test`,
|
|
display_name: `E2E Codex ${suffix}`,
|
|
});
|
|
|
|
return payload.agent.id as string;
|
|
}
|
|
|
|
async function upsertGrant(agentId: string): Promise<void> {
|
|
await requestJson("POST", `/api/v1/agents/${agentId}/grants`, undefined, {
|
|
workspace_slug: workspaceSlug,
|
|
project_id: projectId,
|
|
scopes: [
|
|
"workspace:read",
|
|
"project:read",
|
|
"issue:read",
|
|
"issue:create",
|
|
"issue:update",
|
|
"issue:move",
|
|
"issue:comment",
|
|
"issue:label",
|
|
"issue:assign",
|
|
"issue:structured_blocks:write",
|
|
],
|
|
mode: "voluntary",
|
|
created_by_user_id: "e2e-smoke-admin",
|
|
});
|
|
}
|
|
|
|
async function createToken(agentId: string): Promise<string> {
|
|
const payload = await requestJson("POST", `/api/v1/agents/${agentId}/tokens`, undefined, {
|
|
name: "E2E smoke token",
|
|
});
|
|
|
|
return payload.token as string;
|
|
}
|
|
|
|
async function requestJson(
|
|
method: "GET" | "POST",
|
|
url: string,
|
|
headers?: Record<string, string>,
|
|
payload?: unknown
|
|
): Promise<Record<string, any>> {
|
|
const injectOptions: any = {
|
|
method,
|
|
url,
|
|
headers,
|
|
};
|
|
|
|
if (payload !== undefined) {
|
|
injectOptions.payload = payload;
|
|
}
|
|
|
|
const response = await app.inject(injectOptions);
|
|
|
|
const body = response.body ? JSON.parse(response.body) : {};
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
throw new Error(`HTTP ${response.statusCode} for ${method} ${url}: ${JSON.stringify(body)}`);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
function readRequiredEnv(key: string): string {
|
|
const value = process.env[key]?.trim();
|
|
if (!value) {
|
|
throw new Error(`${key} is required for e2e smoke test.`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function assert(condition: unknown, message: string): void {
|
|
if (!condition) {
|
|
throw new Error(`E2E smoke assertion failed: ${message}`);
|
|
}
|
|
}
|