feat(FN-3324): add runtime guard regression sentinel

Adds a regression test sentinel in the engine's in-process runtime test suite to guard against runtime guard behavior regressions (FN-3324).

Fusion-Task-Id: FN-3324
This commit is contained in:
Fusion
2026-05-04 15:27:41 -07:00
committed by gsxdsm
parent 452c31a085
commit c48feb2f79
9 changed files with 253 additions and 24 deletions

View File

@@ -137,6 +137,64 @@ describe("AgentStore", () => {
// ── createAgent ───────────────────────────────────────────────────
describe("createAgent", () => {
describe("createAgent name uniqueness", () => {
it("rejects creating a non-ephemeral agent with a duplicate name", async () => {
const created = await store.createAgent({
name: "Alpha",
role: "executor",
});
await expect(
store.createAgent({
name: "Alpha",
role: "reviewer",
}),
).rejects.toThrow(`Agent with name "Alpha" already exists (agentId: ${created.id})`);
});
it("allows creating ephemeral agents with duplicate names", async () => {
const first = await store.createAgent({
name: "executor-FN-123",
role: "executor",
metadata: { agentKind: "task-worker", taskWorker: true },
});
const second = await store.createAgent({
name: "executor-FN-123",
role: "executor",
metadata: { agentKind: "task-worker", taskWorker: true },
});
expect(first.id).not.toBe(second.id);
expect(first.name).toBe(second.name);
});
it("findAgentByName returns the correct agent", async () => {
const created = await store.createAgent({
name: "Beta",
role: "executor",
});
const found = await store.findAgentByName("Beta");
const missing = await store.findAgentByName("Gamma");
expect(found?.id).toBe(created.id);
expect(found?.name).toBe("Beta");
expect(missing).toBeNull();
});
it("findAgentByName excludes ephemeral agents", async () => {
await store.createAgent({
name: "Ephemeral-X",
role: "executor",
metadata: { agentKind: "task-worker", taskWorker: true },
});
const found = await store.findAgentByName("Ephemeral-X");
expect(found).toBeNull();
});
});
it("returns an agent with correct fields", async () => {
const agent = await store.createAgent({
name: " Test Agent ",

View File

@@ -472,6 +472,40 @@ export class AgentStore extends EventEmitter {
this.db.bumpLastModified();
}
/**
* Find the first non-ephemeral agent by exact name.
*
* Ephemeral task-worker/spawned agents are excluded so callers can use this
* for durable identity checks without transient runtime workers conflicting.
*
* @param name - Agent name to match exactly
* @returns Matching non-ephemeral agent, or null when none exists
*/
async findAgentByName(name: string): Promise<Agent | null> {
const rows = this.db
.prepare("SELECT * FROM agents WHERE name = ? ORDER BY createdAt DESC")
.all(name) as unknown as AgentRow[];
for (const row of rows) {
const agent = this.mapAgentRow(row);
if (!isEphemeralAgent(agent)) {
return agent;
}
}
return null;
}
async hasNonEphemeralAgentWithName(name: string): Promise<boolean> {
const normalizedName = name.trim();
if (!normalizedName) {
return false;
}
const existing = await this.findAgentByName(normalizedName);
return existing !== null;
}
/**
* Create a new agent with "idle" state.
*
@@ -484,9 +518,12 @@ export class AgentStore extends EventEmitter {
* same default (1h) to both at runtime. Writing the default explicitly
* removes that divergence and keeps the persisted config truthful.
*
* Also enforces non-ephemeral name uniqueness: durable agents cannot share a
* name, while ephemeral task-worker agents are allowed to duplicate names.
*
* @param input - Creation parameters
* @returns The created agent
* @throws Error if input is invalid
* @throws Error if input is invalid or a duplicate non-ephemeral name exists
*/
async createAgent(input: AgentCreateInput): Promise<Agent> {
if (!input.name?.trim()) {
@@ -496,10 +533,20 @@ export class AgentStore extends EventEmitter {
throw new Error("Agent role is required");
}
const normalizedName = input.name.trim();
const metadata = input.metadata ?? {};
const ephemeral = isEphemeralAgent({ metadata, name: input.name, role: input.role, reportsTo: input.reportsTo });
if (!ephemeral) {
const existing = await this.findAgentByName(normalizedName);
if (existing) {
throw new Error(`Agent with name "${normalizedName}" already exists (agentId: ${existing.id})`);
}
}
const now = new Date().toISOString();
const agentId = `agent-${randomUUID().slice(0, 8)}`;
const metadata = input.metadata ?? {};
const runtimeConfig = resolveCreationRuntimeConfig(input.runtimeConfig, metadata);
// Default heartbeatProcedurePath for new non-ephemeral agents so operators
@@ -508,13 +555,12 @@ export class AgentStore extends EventEmitter {
// tweaks to one agent's procedure do not bleed into the rest of the
// team. Ephemeral task workers skip this — they're short-lived and
// don't need persistent procedure files.
const ephemeral = isEphemeralAgent({ metadata, name: input.name, role: input.role, reportsTo: input.reportsTo });
const resolvedHeartbeatProcedurePath = input.heartbeatProcedurePath
?? (ephemeral ? undefined : getDefaultHeartbeatProcedurePath(agentId, input.name));
const agent: Agent = {
id: agentId,
name: input.name.trim(),
name: normalizedName,
role: input.role,
state: "idle",
createdAt: now,