#!/usr/bin/env node import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { AGENT_MGR_SERVICE_ID, AGENT_SESSION_STATUSES, AGENT_SKILLS_SERVICE_ID, AGENT_WORKER_SERVICE_ID, WORKER_SESSION_STATUSES, createAgentRunPlan, createSkillsManifest, finalizeAgentRun } from "../internal/agent/index.mjs"; import { createLocalAgentSession, getLocalAgentEvidence, getLocalAgentStatus, getLocalAgentTrace, runLocalWorkerDryRun } from "../internal/agent/runtime.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const fixedNow = () => "2026-05-21T00:00:00.000Z"; async function readJSON(relativePath) { const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); return JSON.parse(raw); } async function readText(relativePath) { return readFile(path.join(repoRoot, relativePath), "utf8"); } function sha256Hex(value) { return createHash("sha256").update(String(value)).digest("hex"); } function expectLifecycleConstants() { assert.deepEqual(AGENT_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]); assert.deepEqual(WORKER_SESSION_STATUSES, ["create", "start", "trace", "finish", "cleanup"]); } function expectRunPlan(plan, fixture) { const workerSessionId = `wkr_${fixture.agentSessionId}`; const traceId = `trc_${fixture.agentSessionId}`; const operationId = `op_${fixture.agentSessionId}`; const evidenceId = `evi_${fixture.agentSessionId}`; const workspaceVolumeId = `vol_${fixture.agentSessionId}`; const mountPath = `/workspace/${fixture.projectId}/${fixture.agentSessionId}`; const evidenceUri = `file://${path.posix.join(mountPath, "evidence.txt")}`; assert.equal(plan.skillCommitId, fixture.skillCommitId); assert.equal(plan.skillVersion, fixture.skillVersion); assert.equal(plan.agentSession.serviceId, AGENT_MGR_SERVICE_ID); assert.equal(plan.workerSession.serviceId, AGENT_WORKER_SERVICE_ID); assert.equal(plan.agentSession.agentSessionId, fixture.agentSessionId); assert.equal(plan.agentSession.projectId, fixture.projectId); assert.equal(plan.agentSession.environment, fixture.environment); assert.equal(plan.agentSession.status, "start"); assert.equal(plan.agentSession.startedAt, fixedNow()); assert.equal(plan.agentSession.updatedAt, fixedNow()); assert.equal(plan.agentSession.metadata.currentState, "start"); assert.equal(plan.agentSession.metadata.workerSessionId, workerSessionId); assert.equal(plan.agentSession.metadata.workspaceVolumeId, workspaceVolumeId); assert.equal(plan.workerSession.workerSessionId, workerSessionId); assert.equal(plan.workerSession.agentSessionId, fixture.agentSessionId); assert.equal(plan.workerSession.projectId, fixture.projectId); assert.equal(plan.workerSession.environment, fixture.environment); assert.equal(plan.workerSession.status, "start"); assert.equal(plan.workerSession.startedAt, fixedNow()); assert.equal(plan.workerSession.updatedAt, fixedNow()); assert.equal(plan.workerSession.metadata.currentState, "start"); assert.equal(plan.workerSession.metadata.workspaceVolumeId, workspaceVolumeId); assert.equal(plan.workspaceVolume.workspaceVolumeId, workspaceVolumeId); assert.equal(plan.workspaceVolume.mountPath, mountPath); assert.equal(plan.workspaceVolume.serviceId, AGENT_WORKER_SERVICE_ID); assert.equal(plan.workspaceVolume.lifecycle, "create"); assert.equal(plan.workspaceVolume.createdAt, fixedNow()); assert.equal(plan.workspaceVolume.updatedAt, fixedNow()); assert.deepEqual( plan.events.map((event) => event.state), ["create", "start", "trace", "finish", "cleanup"] ); assert.equal(plan.events[0].actor, AGENT_MGR_SERVICE_ID); assert.equal(plan.events[0].agentSessionId, fixture.agentSessionId); assert.equal(plan.events[1].actor, AGENT_WORKER_SERVICE_ID); assert.equal(plan.events[1].workerSessionId, workerSessionId); assert.equal(plan.events[2].actor, AGENT_MGR_SERVICE_ID); assert.equal(plan.events[2].traceEvent.traceEventId, `tev_${fixture.agentSessionId}`); assert.equal(plan.events[2].traceEvent.traceId, traceId); assert.equal(plan.events[2].traceEvent.projectId, fixture.projectId); assert.equal(plan.events[2].traceEvent.serviceId, AGENT_MGR_SERVICE_ID); assert.equal(plan.events[2].traceEvent.level, "info"); assert.equal(plan.events[2].traceEvent.message, fixture.prompt); assert.equal(plan.events[2].traceEvent.environment, fixture.environment); assert.equal(plan.events[2].traceEvent.agentSessionId, fixture.agentSessionId); assert.equal(plan.events[2].traceEvent.workerSessionId, workerSessionId); assert.equal(plan.events[2].traceEvent.operationId, operationId); assert.deepEqual(plan.events[2].traceEvent.metadata, {}); assert.equal(plan.traceEvent.traceId, traceId); assert.equal(plan.traceEvent.message, fixture.prompt); assert.equal(plan.traceEvent.occurredAt, fixedNow()); assert.equal(plan.events[3].actor, AGENT_WORKER_SERVICE_ID); assert.equal(plan.events[3].evidenceRecord.evidenceId, evidenceId); assert.equal(plan.events[4].actor, AGENT_WORKER_SERVICE_ID); assert.equal(plan.events[4].workspaceVolumeId, workspaceVolumeId); assert.equal(plan.evidenceRecord.evidenceId, evidenceId); assert.equal(plan.evidenceRecord.projectId, fixture.projectId); assert.equal(plan.evidenceRecord.operationId, operationId); assert.equal(plan.evidenceRecord.agentSessionId, fixture.agentSessionId); assert.equal(plan.evidenceRecord.workerSessionId, workerSessionId); assert.equal(plan.evidenceRecord.kind, "artifact"); assert.equal(plan.evidenceRecord.uri, evidenceUri); assert.equal(plan.evidenceRecord.mimeType, "text/plain"); assert.equal(plan.evidenceRecord.sha256, sha256Hex(evidenceUri)); assert.equal(plan.evidenceRecord.sizeBytes, 0); assert.equal(plan.evidenceRecord.serviceId, AGENT_WORKER_SERVICE_ID); assert.equal(plan.evidenceRecord.environment, fixture.environment); assert.equal(plan.evidenceRecord.createdAt, fixedNow()); assert.equal(plan.evidenceRecord.metadata.skillCommitId, fixture.skillCommitId); assert.equal(plan.evidenceRecord.metadata.skillVersion, fixture.skillVersion); assert.equal(plan.evidenceRecord.metadata.prompt, fixture.prompt); assert.equal(plan.evidenceRecord.metadata.goal, fixture.goal); assert.equal(plan.evidenceRecord.metadata.workspaceVolumeId, workspaceVolumeId); } function expectFinishedRun(finished, originalPlan) { assert.equal(finished.agentSession.status, "cleanup"); assert.equal(finished.workerSession.status, "cleanup"); assert.equal(finished.agentSession.completedAt, fixedNow()); assert.equal(finished.workerSession.completedAt, fixedNow()); assert.equal(finished.agentSession.metadata.cleanedUp, true); assert.equal(finished.workerSession.metadata.cleanedUp, true); assert.equal(finished.cleanup.workspaceVolumeId, originalPlan.workspaceVolume.workspaceVolumeId); assert.equal(finished.cleanup.removed, true); assert.equal(finished.evidenceRecord.sha256, originalPlan.evidenceRecord.sha256); assert.equal(originalPlan.agentSession.status, "start"); assert.equal(originalPlan.workerSession.status, "start"); } function expectSkillsManifest(manifest, fixture) { assert.equal(manifest.manifestVersion, "v1"); assert.equal(manifest.environment, fixture.environment); assert.equal(manifest.commitId, fixture.skillCommitId); assert.equal(manifest.version, fixture.skillVersion); assert.equal(manifest.namespace, "hwlab"); assert.equal(manifest.endpoint, "http://74.48.78.17:16667"); assert.equal(manifest.profiles.dev.enabled, true); assert.equal(manifest.profiles.dev.namespace, "hwlab-dev"); assert.equal(manifest.services[0].serviceId, AGENT_SKILLS_SERVICE_ID); assert.equal(manifest.services[0].image, `hwlab-agent-runtime:${fixture.skillCommitId.slice(0, 7)}`); assert.equal(manifest.services[0].env.HWLAB_SKILLS_COMMIT_ID, fixture.skillCommitId); assert.equal(manifest.services[0].env.HWLAB_SKILLS_VERSION, fixture.skillVersion); assert.deepEqual(manifest.skills, []); } function expectWorkspaceIsolation(primaryPlan, secondaryPlan, fixture) { const primaryVolumeId = primaryPlan.workspaceVolume.workspaceVolumeId; const secondaryVolumeId = secondaryPlan.workspaceVolume.workspaceVolumeId; const secondaryMountPath = `/workspace/${fixture.workspaceIsolation.projectId}/${fixture.workspaceIsolation.agentSessionId}`; const secondaryEvidenceUri = `file://${path.posix.join(secondaryMountPath, "evidence.txt")}`; assert.notEqual(primaryVolumeId, secondaryVolumeId); assert.notEqual(primaryPlan.workspaceVolume.mountPath, secondaryPlan.workspaceVolume.mountPath); assert.equal(secondaryPlan.workspaceVolume.mountPath, secondaryMountPath); assert.equal(secondaryPlan.evidenceRecord.uri, secondaryEvidenceUri); assert.equal(secondaryPlan.evidenceRecord.metadata.workspaceVolumeId, secondaryVolumeId); assert.equal(secondaryPlan.traceEvent.traceId, `trc_${fixture.workspaceIsolation.agentSessionId}`); } function expectEvidenceFixture(text, fixture) { assert.match(text, /^# M4 Agent Automation Loop Evidence Plan/m); assert.ok(text.includes(`- Scenario: ${fixture.scenario}`)); assert.ok(text.includes(`- Environment: ${fixture.environment}`)); assert.ok(text.includes(`- Project: ${fixture.projectId}`)); assert.ok(text.includes(`- Skill commitId: ${fixture.skillCommitId}`)); assert.ok(text.includes(`- Skill version: ${fixture.skillVersion}`)); assert.ok(text.includes("no secrets")); assert.ok(text.includes("no live DEV agent run")); } async function expectLocalRuntimeClosedLoop(fixture) { const stateDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-m4-agent-loop-")); try { const created = await createLocalAgentSession({ stateDir, agentSessionId: fixture.agentSessionId, projectId: fixture.projectId, goal: fixture.goal, prompt: fixture.prompt, skillCommitId: fixture.skillCommitId, skillVersion: fixture.skillVersion, now: fixedNow }); assert.equal(created.health.status, "ok"); assert.equal(created.task.status, "queued"); assert.equal(created.traceCount, 1); const worker = await runLocalWorkerDryRun({ stateDir, agentSessionId: fixture.agentSessionId, skillCommitId: fixture.skillCommitId, skillVersion: fixture.skillVersion, now: fixedNow }); assert.equal(worker.health.status, "ok"); assert.equal(worker.task.status, "completed"); assert.equal(worker.agentSession.status, "cleanup"); assert.equal(worker.workerSession.status, "cleanup"); assert.equal(worker.cleanup.removed, true); assert.equal(worker.workspace.existsAfterCleanup, false); assert.equal(worker.evidenceRecord.metadata.health.status, "ok"); assert.equal(worker.evidenceRecord.metadata.skills.manager.commitId, fixture.skillCommitId); assert.equal(worker.evidenceRecord.metadata.skills.worker.version, fixture.skillVersion); const status = await getLocalAgentStatus({ stateDir, agentSessionId: fixture.agentSessionId }); assert.equal(status.evidenceCount, 1); assert.equal(status.cleanup.removed, true); const trace = await getLocalAgentTrace({ stateDir, agentSessionId: fixture.agentSessionId }); assert.deepEqual( trace.traceEvents.map((event) => event.metadata.state), ["create", "start", "trace", "finish", "cleanup"] ); const evidence = await getLocalAgentEvidence({ stateDir, agentSessionId: fixture.agentSessionId }); assert.equal(evidence.status, "ok"); assert.equal(evidence.evidenceRecords.length, 1); assert.equal(evidence.evidenceRecords[0].metadata.traceId, `trc_${fixture.agentSessionId}`); } finally { await rm(stateDir, { recursive: true, force: true }); } } const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json"); const evidenceFixture = await readText(fixture.evidencePath); assert.equal(fixture.environment, "dev"); assert.equal(fixture.projectId.startsWith("prj_"), true); assert.equal(fixture.agentSessionId.startsWith("agt_"), true); assert.equal(fixture.skillCommitId.length >= 7, true); assert.equal(fixture.skillVersion.length > 0, true); assert.equal(fixture.workspaceIsolation.projectId.startsWith("prj_"), true); assert.equal(fixture.workspaceIsolation.agentSessionId.startsWith("agt_"), true); expectLifecycleConstants(); expectEvidenceFixture(evidenceFixture, fixture); const plan = createAgentRunPlan({ agentSessionId: fixture.agentSessionId, projectId: fixture.projectId, goal: fixture.goal, prompt: fixture.prompt, skillCommitId: fixture.skillCommitId, skillVersion: fixture.skillVersion, now: fixedNow }); expectRunPlan(plan, fixture); const finished = finalizeAgentRun(plan, { now: fixedNow }); expectFinishedRun(finished, plan); const manifest = createSkillsManifest({ commitId: fixture.skillCommitId, version: fixture.skillVersion }); expectSkillsManifest(manifest, fixture); const isolationPlan = createAgentRunPlan({ agentSessionId: fixture.workspaceIsolation.agentSessionId, projectId: fixture.workspaceIsolation.projectId, goal: fixture.goal, prompt: fixture.prompt, skillCommitId: fixture.skillCommitId, skillVersion: fixture.skillVersion, now: fixedNow }); expectWorkspaceIsolation(plan, isolationPlan, fixture); await expectLocalRuntimeClosedLoop(fixture); console.log(`[m4-smoke] ok ${fixture.scenario}`);