#!/usr/bin/env node import assert from "node:assert/strict"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; const execFile = promisify(execFileCallback); const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const taskBaseCommit = "3307f7916220b74a8e7d0d8e8b2b12f888d0632a"; const sourceStateFile = path.join(rootDir, "data", "boss-state.json"); async function createSeededRuntime(root, runtimeName) { const runtimeDir = await fs.mkdtemp(path.join(os.tmpdir(), runtimeName)); const stateFile = path.join(runtimeDir, "data", "boss-state.json"); await fs.mkdir(path.join(runtimeDir, "data", "uploads"), { recursive: true }); await fs.mkdir(path.join(runtimeDir, "public", "downloads"), { recursive: true }); const state = JSON.parse(await fs.readFile(sourceStateFile, "utf8")); await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf8"); return { runtimeDir, stateFile }; } function parseCookieValue(setCookieHeader, cookieName) { assert.ok(setCookieHeader, "set-cookie header is missing"); const match = setCookieHeader.match(new RegExp(`${cookieName}=([^;]+)`)); assert.ok(match, `${cookieName} cookie is missing`); return match[1]; } async function waitForServer(baseUrl, child, getServerLogs) { for (let attempt = 0; attempt < 60; attempt += 1) { if (child.exitCode !== null) { throw new Error(`SERVER_EXITED_EARLY:${child.exitCode}:${getServerLogs()}`); } try { const response = await fetch(`${baseUrl}/api/health`); if (response.ok) { return; } } catch { // keep waiting } await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error(`SERVER_START_TIMEOUT:${getServerLogs()}`); } async function startStandaloneServer(appRoot, runtimeDir, port) { const baseUrl = `http://127.0.0.1:${port}`; let logs = ""; const child = spawn("node", [".next/standalone/server.js"], { cwd: appRoot, env: { ...process.env, PORT: String(port), HOSTNAME: "127.0.0.1", BOSS_PUBLIC_BASE_URL: baseUrl, BOSS_RUNTIME_ROOT: runtimeDir, BOSS_STATE_FILE: path.join(runtimeDir, "data", "boss-state.json"), BOSS_AUTH_AUTO_LOGIN: "0", }, stdio: ["ignore", "pipe", "pipe"], }); child.stdout.on("data", (chunk) => { logs += chunk.toString(); }); child.stderr.on("data", (chunk) => { logs += chunk.toString(); }); await waitForServer(baseUrl, child, () => logs); return { baseUrl, child, getLogs: () => logs, async stop() { if (child.exitCode === null) { child.kill("SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 500)); } }, }; } async function loginAsAdmin(baseUrl) { const response = await fetch(`${baseUrl}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ account: "17600003315", password: "boss123456", method: "password", }), }); assert.equal(response.status, 200, "login should succeed"); const payload = await response.json(); assert.equal(payload.ok, true, "login payload should be ok"); const cookie = parseCookieValue(response.headers.get("set-cookie"), "boss_session"); return { cookie, payload }; } async function uploadAttachment(baseUrl, cookie, projectId, fileName, type, bytes) { const form = new FormData(); form.set("file", new File([bytes], fileName, { type })); const response = await fetch(`${baseUrl}/api/v1/projects/${projectId}/attachments`, { method: "POST", headers: { cookie: `boss_session=${cookie}`, }, body: form, }); assert.equal(response.status, 200, `upload ${fileName} should succeed`); return response.json(); } async function analyzeAttachment(baseUrl, cookie, projectId, attachmentId) { const response = await fetch(`${baseUrl}/api/v1/projects/${projectId}/attachments/${attachmentId}/analyze`, { method: "POST", headers: { cookie: `boss_session=${cookie}`, }, }); assert.equal(response.status, 200, "manual analyze should succeed"); return response.json(); } async function verifyHistoricalPrecheck() { const worktreePath = await fs.mkdtemp(path.join(os.tmpdir(), "boss-attachment-precheck-")); let server; try { await execFile("git", ["worktree", "add", "--detach", worktreePath, taskBaseCommit], { cwd: rootDir, maxBuffer: 32 * 1024 * 1024, }); await execFile("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: worktreePath, env: process.env, maxBuffer: 32 * 1024 * 1024, }); await execFile("npm", ["run", "build"], { cwd: worktreePath, env: process.env, maxBuffer: 32 * 1024 * 1024, }); const { runtimeDir } = await createSeededRuntime(worktreePath, "boss-attachment-precheck-"); server = await startStandaloneServer(worktreePath, runtimeDir, 3115); const { cookie } = await loginAsAdmin(server.baseUrl); const response = await fetch(`${server.baseUrl}/api/v1/projects/master-agent/attachments/att-missing/analyze`, { method: "POST", headers: { cookie: `boss_session=${cookie}`, }, }); assert.notEqual(response.status, 200, "pre-implementation analyze route should not succeed"); } finally { await server?.stop(); await execFile("git", ["worktree", "remove", "--force", worktreePath], { cwd: rootDir, maxBuffer: 32 * 1024 * 1024, }).catch(() => undefined); await fs.rm(worktreePath, { recursive: true, force: true }); } } await verifyHistoricalPrecheck(); const { runtimeDir } = await createSeededRuntime(rootDir, "boss-attachment-current-"); const currentServer = await startStandaloneServer(rootDir, runtimeDir, 3116); try { const { cookie } = await loginAsAdmin(currentServer.baseUrl); const textUpload = await uploadAttachment( currentServer.baseUrl, cookie, "master-agent", "analysis-note.txt", "text/plain", Buffer.from("text attachment for automatic analysis"), ); assert.equal(textUpload.attachment.analysisState, "queued_auto", "text attachment should queue automatically"); assert.ok(textUpload.analysisTask, "queued auto attachment should create a master agent task"); assert.equal( textUpload.analysisTask.taskType, "attachment_analysis", "queued task type should be attachment_analysis", ); assert.equal( textUpload.analysisTask.attachmentFileName, "analysis-note.txt", "queued task should carry attachment file name", ); assert.ok(textUpload.analysisTask.attachmentDownloadUrl, "queued task should expose attachment download url"); const promptDownloadUrlMatch = textUpload.analysisTask.executionPrompt.match(/downloadUrl:\s+(http[^\s]+)/); assert.ok(promptDownloadUrlMatch, "execution prompt should include attachment download url"); const unauthDownloadResponse = await fetch(textUpload.analysisTask.attachmentDownloadUrl); assert.equal(unauthDownloadResponse.status, 200, "attachment download url should be readable with task token"); assert.equal( await unauthDownloadResponse.text(), "text attachment for automatic analysis", "downloaded attachment content should match the uploaded text", ); const manualUpload = await uploadAttachment( currentServer.baseUrl, cookie, "master-agent", "manual-binary.bin", "application/octet-stream", Buffer.from([0, 1, 2, 3]), ); assert.equal( manualUpload.attachment.analysisState, "ready_manual", "binary attachment should be manually analyzable", ); const analyzePayload = await analyzeAttachment( currentServer.baseUrl, cookie, "master-agent", manualUpload.attachment.attachmentId, ); assert.ok(analyzePayload.taskId, "manual analyze should return a taskId"); assert.ok(analyzePayload.task, "manual analyze should return a task payload"); assert.equal( analyzePayload.task.taskType, "attachment_analysis", "manual analyze task should be attachment_analysis", ); assert.equal( analyzePayload.task.attachmentId, manualUpload.attachment.attachmentId, "manual task should link the attachment", ); console.log("attachment analysis validation passed"); } finally { await currentServer.stop(); await fs.rm(runtimeDir, { recursive: true, force: true }); }