300 lines
13 KiB
JavaScript
300 lines
13 KiB
JavaScript
#!/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}`);
|