Skip to content

Commit 90fffb1

Browse files
authored
fix(agentstack): assignTask refuses tasks in a terminal status (#47)
1 parent a4cc229 commit 90fffb1

2 files changed

Lines changed: 328 additions & 305 deletions

File tree

Lines changed: 112 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,126 @@
1-
import { describe, expect, it, vi } from "vitest";
2-
import { validateManifest } from "@logicsrc/plugin-core";
3-
import {
4-
AgentStack,
5-
agentDid,
6-
agentStackManifest,
7-
agentStackPlugin,
8-
makeDid,
9-
parseDid,
10-
userDid
11-
} from "./index.js";
12-
import type { AgentProfile } from "./types.js";
13-
14-
const owner = userDid("123");
15-
const agent: AgentProfile = {
16-
did: agentDid("abc"),
17-
name: "Build Agent",
18-
sourceApp: "commandboard.run",
19-
supportedProtocols: ["logicsrc/1"]
20-
};
21-
22-
describe("DID helpers", () => {
23-
it("builds and parses CoinPay DIDs", () => {
24-
expect(makeDid("user", "123")).toBe("did:coinpay:user:123");
25-
expect(parseDid(owner)).toEqual({ kind: "user", id: "123" });
26-
expect(parseDid(agentDid("abc"))).toEqual({ kind: "agent", id: "abc" });
27-
});
28-
29-
it("rejects non-CoinPay DIDs", () => {
30-
expect(parseDid("did:web:example.com")).toBeNull();
31-
expect(parseDid("did:coinpay:bot:abc")).toBeNull();
32-
});
33-
});
34-
35-
describe("AgentStack manifest", () => {
36-
it("is a valid LogicSRC plugin manifest", () => {
37-
expect(() => validateManifest(agentStackManifest)).not.toThrow();
38-
expect(agentStackPlugin.manifest.id).toBe("agentstack");
1+
import { describe, expect, it, vi } from "vitest";
2+
import { validateManifest } from "@logicsrc/plugin-core";
3+
import {
4+
AgentStack,
5+
agentDid,
6+
agentStackManifest,
7+
agentStackPlugin,
8+
makeDid,
9+
parseDid,
10+
userDid
11+
} from "./index.js";
12+
import type { AgentProfile } from "./types.js";
13+
14+
const owner = userDid("123");
15+
const agent: AgentProfile = {
16+
did: agentDid("abc"),
17+
name: "Build Agent",
18+
sourceApp: "commandboard.run",
19+
supportedProtocols: ["logicsrc/1"]
20+
};
21+
22+
describe("DID helpers", () => {
23+
it("builds and parses CoinPay DIDs", () => {
24+
expect(makeDid("user", "123")).toBe("did:coinpay:user:123");
25+
expect(parseDid(owner)).toEqual({ kind: "user", id: "123" });
26+
expect(parseDid(agentDid("abc"))).toEqual({ kind: "agent", id: "abc" });
27+
});
28+
29+
it("rejects non-CoinPay DIDs", () => {
30+
expect(parseDid("did:web:example.com")).toBeNull();
31+
expect(parseDid("did:coinpay:bot:abc")).toBeNull();
32+
});
33+
});
34+
35+
describe("AgentStack manifest", () => {
36+
it("is a valid LogicSRC plugin manifest", () => {
37+
expect(() => validateManifest(agentStackManifest)).not.toThrow();
38+
expect(agentStackPlugin.manifest.id).toBe("agentstack");
39+
});
40+
});
41+
42+
describe("AgentStack coordinator", () => {
43+
it("creates pending tasks and queues assigned ones", () => {
44+
const stack = new AgentStack();
45+
const pending = stack.createTask({ ownerDid: owner, sourceApp: "ugig.net", title: "Crawl site" });
46+
expect(pending.status).toBe("pending");
47+
48+
stack.registerAgent(agent);
49+
const assigned = stack.createTask({
50+
ownerDid: owner,
51+
sourceApp: "ugig.net",
52+
title: "Crawl site 2",
53+
assigneeDid: agent.did
54+
});
55+
expect(assigned.status).toBe("queued");
56+
});
57+
58+
it("moves a task through its lifecycle and binds reputation", () => {
59+
const stack = new AgentStack();
60+
stack.registerAgent(agent);
61+
const task = stack.createTask({ ownerDid: owner, sourceApp: "sh1pt.com", title: "Ship build" });
62+
63+
stack.assignTask(task.id, agent.did);
64+
stack.updateTaskStatus(task.id, "running");
65+
const done = stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
66+
67+
expect(done.status).toBe("complete");
68+
expect(done.assigneeDid).toBe(agent.did);
69+
expect(done.reputationEventId).toBe("rep_1");
70+
});
71+
72+
it("refuses to transition out of a terminal status", () => {
73+
const stack = new AgentStack();
74+
const task = stack.createTask({ ownerDid: owner, sourceApp: "qaaas.dev", title: "Test run" });
75+
stack.updateTaskStatus(task.id, "cancelled");
76+
expect(() => stack.updateTaskStatus(task.id, "running")).toThrow(/cancelled/);
3977
});
40-
});
4178

42-
describe("AgentStack coordinator", () => {
43-
it("creates pending tasks and queues assigned ones", () => {
79+
it("refuses to assign a task that is already in a terminal status", () => {
4480
const stack = new AgentStack();
45-
const pending = stack.createTask({ ownerDid: owner, sourceApp: "ugig.net", title: "Crawl site" });
46-
expect(pending.status).toBe("pending");
47-
4881
stack.registerAgent(agent);
49-
const assigned = stack.createTask({
82+
const other: AgentProfile = { ...agent, did: agentDid("xyz") };
83+
stack.registerAgent(other);
84+
const task = stack.createTask({
5085
ownerDid: owner,
5186
sourceApp: "ugig.net",
52-
title: "Crawl site 2",
53-
assigneeDid: agent.did
87+
title: "Paid task",
88+
paymentIntentId: "pi_1",
89+
escrowId: "esc_1"
5490
});
55-
expect(assigned.status).toBe("queued");
56-
});
57-
58-
it("moves a task through its lifecycle and binds reputation", () => {
59-
const stack = new AgentStack();
60-
stack.registerAgent(agent);
61-
const task = stack.createTask({ ownerDid: owner, sourceApp: "sh1pt.com", title: "Ship build" });
62-
6391
stack.assignTask(task.id, agent.did);
64-
stack.updateTaskStatus(task.id, "running");
65-
const done = stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
92+
stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
6693

67-
expect(done.status).toBe("complete");
68-
expect(done.assigneeDid).toBe(agent.did);
69-
expect(done.reputationEventId).toBe("rep_1");
70-
});
71-
72-
it("refuses to transition out of a terminal status", () => {
73-
const stack = new AgentStack();
74-
const task = stack.createTask({ ownerDid: owner, sourceApp: "qaaas.dev", title: "Test run" });
75-
stack.updateTaskStatus(task.id, "cancelled");
76-
expect(() => stack.updateTaskStatus(task.id, "running")).toThrow(/cancelled/);
94+
expect(() => stack.assignTask(task.id, other.did)).toThrow(/already complete and cannot be assigned/);
95+
expect(stack.getTask(task.id)?.assigneeDid).toBe(agent.did);
96+
expect(stack.getTask(task.id)?.status).toBe("complete");
7797
});
7898

7999
it("records delegation grants and emits events", () => {
80100
const stack = new AgentStack();
81101
const listener = vi.fn();
82-
stack.on(listener);
83-
stack.registerAgent(agent);
84-
const grant = stack.delegate(owner, agent.did, ["tasks:create"]);
85-
86-
expect(grant.ownerDid).toBe(owner);
87-
expect(grant.agentDid).toBe(agent.did);
88-
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "agent.registered" }));
89-
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "delegation.granted" }));
90-
});
91-
92-
it("rejects unknown agents and invalid DIDs", () => {
93-
const stack = new AgentStack();
94-
expect(() => stack.createTask({ ownerDid: "nope", sourceApp: "x", title: "t" })).toThrow();
95-
const task = stack.createTask({ ownerDid: owner, sourceApp: "x", title: "t" });
96-
expect(() => stack.assignTask(task.id, agentDid("ghost"))).toThrow(/Unknown agent/);
97-
});
98-
99-
it("filters tasks in snapshots", () => {
100-
const stack = new AgentStack();
101-
stack.createTask({ ownerDid: owner, sourceApp: "x", title: "a" });
102-
stack.createTask({ ownerDid: userDid("999"), sourceApp: "x", title: "b" });
103-
expect(stack.listTasks({ ownerDid: owner })).toHaveLength(1);
104-
expect(stack.snapshot().tasks).toHaveLength(2);
105-
});
106-
});
102+
stack.on(listener);
103+
stack.registerAgent(agent);
104+
const grant = stack.delegate(owner, agent.did, ["tasks:create"]);
105+
106+
expect(grant.ownerDid).toBe(owner);
107+
expect(grant.agentDid).toBe(agent.did);
108+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "agent.registered" }));
109+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "delegation.granted" }));
110+
});
111+
112+
it("rejects unknown agents and invalid DIDs", () => {
113+
const stack = new AgentStack();
114+
expect(() => stack.createTask({ ownerDid: "nope", sourceApp: "x", title: "t" })).toThrow();
115+
const task = stack.createTask({ ownerDid: owner, sourceApp: "x", title: "t" });
116+
expect(() => stack.assignTask(task.id, agentDid("ghost"))).toThrow(/Unknown agent/);
117+
});
118+
119+
it("filters tasks in snapshots", () => {
120+
const stack = new AgentStack();
121+
stack.createTask({ ownerDid: owner, sourceApp: "x", title: "a" });
122+
stack.createTask({ ownerDid: userDid("999"), sourceApp: "x", title: "b" });
123+
expect(stack.listTasks({ ownerDid: owner })).toHaveLength(1);
124+
expect(stack.snapshot().tasks).toHaveLength(2);
125+
});
126+
});

0 commit comments

Comments
 (0)