@agentmessier/openclaw-agent-messier 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/watcher.ts +6 -4
- package/src/generate.test.ts +0 -193
- package/src/spec.test.ts +0 -107
- package/src/tools.test.ts +0 -57
- package/src/venues.test.ts +0 -71
- package/src/watcher.test.ts +0 -137
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentmessier/openclaw-agent-messier",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Agent Messier multi-venue client for OpenClaw \u2014 play games and work tasks on the AgentNet platform (soccer today; venues discovered from the marketplace registry)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/watcher.ts
CHANGED
|
@@ -105,8 +105,9 @@ export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advan
|
|
|
105
105
|
stratBlock +
|
|
106
106
|
`${v.summary}\n\n` +
|
|
107
107
|
`${ins.play}\n\n` +
|
|
108
|
-
`Decide and act
|
|
109
|
-
`
|
|
108
|
+
`Decide and act NOW: make ONE ${actTool} call with a move for every player you control — ` +
|
|
109
|
+
`every time you are prompted, even if the plan is unchanged. The order holds until you change it, ` +
|
|
110
|
+
`so if you go quiet your team freezes on stale orders. Never reply without acting.`
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
113
|
// Fallback (pre-envelope server or handshake not yet arrived). describeTeam is
|
|
@@ -116,8 +117,9 @@ export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advan
|
|
|
116
117
|
return (
|
|
117
118
|
stratBlock +
|
|
118
119
|
`${rendered}\n\n` +
|
|
119
|
-
`Decide and act
|
|
120
|
-
`
|
|
120
|
+
`Decide and act NOW: make ONE ${actTool} call with a move for every player you control — ` +
|
|
121
|
+
`every time you are prompted, even if the plan is unchanged. The order holds until you change it, ` +
|
|
122
|
+
`so if you go quiet your team freezes on stale orders. Never reply without acting.`
|
|
121
123
|
);
|
|
122
124
|
}
|
|
123
125
|
|
package/src/generate.test.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import { generateVenueTools, defaultVenueTools, joinVenue, seatOf, defaultRealtimeVenue, isRealtimeVenue, _resetSeats } from "./generate.js";
|
|
3
|
-
import { session } from "./state.js";
|
|
4
|
-
import type { GameSpec, PluginCfg } from "./tools.js";
|
|
5
|
-
|
|
6
|
-
function cfg(extra: Partial<PluginCfg> = {}): PluginCfg {
|
|
7
|
-
return { serverUrl: "http://pitch.test", sessionKey: "did:wba:me", ...extra };
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// A venue the plugin has NEVER heard of — proves tools come from the spec, not code.
|
|
11
|
-
const GOLF_VENUE = { id: "agent-golf", origin: "http://golf.test", specUrl: "/spec" };
|
|
12
|
-
const GOLF_SPEC: GameSpec = {
|
|
13
|
-
game: "agent-golf", specVersion: 1, rulesVersion: 1,
|
|
14
|
-
actions: { type: "string", enum: ["drive", "chip", "putt"], descriptions: { drive: "tee shot", putt: "on the green" } },
|
|
15
|
-
observe: { mode: "stream", suggestedIntervalMs: 3000 },
|
|
16
|
-
routes: { observe: "/rounds/{matchId}/agents/{did}/observe", act: "/rounds/{matchId}/players/{playerId}/swing" },
|
|
17
|
-
client: {
|
|
18
|
-
prefix: "golf", noun: "round",
|
|
19
|
-
lobby: { tool: "golf_rounds", route: "/rounds", params: {}, summary: "List rounds." },
|
|
20
|
-
join: { tool: "golf_join", route: "/quickround", seatRoute: "/rounds/{matchId}/join", params: { holes: { type: "integer" }, matchId: { type: "string" } }, seat: { id: "matchId", token: "token", controls: "playerIds" }, summary: "Join a round." },
|
|
21
|
-
autoplay: { tool: "golf_autoplay", summary: "Hands-free." },
|
|
22
|
-
leave: { tool: "golf_leave", route: "/rounds/{matchId}/leave", summary: "Leave the round (forfeit if live)." },
|
|
23
|
-
observe: { tool: "golf_observe", params: {}, summary: "See the course." },
|
|
24
|
-
act: { tool: "golf_play", params: { club: { type: "string" } }, summary: "Swing." },
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
describe("generated tool surface comes entirely from the spec", () => {
|
|
29
|
-
it("baked soccer + taskmarket specs generate their canonical named tools", () => {
|
|
30
|
-
const names = defaultVenueTools(cfg()).map(t => t.name);
|
|
31
|
-
// soccer (a game, seated): lobby + join + observe + batch play
|
|
32
|
-
expect(names).toEqual(expect.arrayContaining(["soccer_matches", "soccer_join", "soccer_observe", "soccer_play"]));
|
|
33
|
-
// taskmarket (seatless work): observe + act, NO lobby/join
|
|
34
|
-
expect(names).toEqual(expect.arrayContaining(["work_observe", "work_act"]));
|
|
35
|
-
expect(names).not.toContain("work_join");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("golf thesis: a brand-new venue's spec yields golf_* tools with ZERO plugin code", () => {
|
|
39
|
-
_resetSeats();
|
|
40
|
-
const tools = generateVenueTools(GOLF_VENUE, GOLF_SPEC, cfg());
|
|
41
|
-
const names = tools.map(t => t.name);
|
|
42
|
-
expect(names).toEqual(["golf_rounds", "golf_join", "golf_observe", "golf_play", "golf_leave"]);
|
|
43
|
-
|
|
44
|
-
// The act tool is BATCH (golf seats players) and carries the spec's action enum.
|
|
45
|
-
const play = tools.find(t => t.name === "golf_play")!;
|
|
46
|
-
const enumList = (play.parameters as any).properties.moves.items.properties.type.enum;
|
|
47
|
-
expect(enumList).toEqual(["drive", "chip", "putt"]);
|
|
48
|
-
// Per-action descriptions from the spec flow into the tool description.
|
|
49
|
-
expect(play.description).toContain("drive: tee shot");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("the act enum tracks the spec — a server-added action surfaces with no plugin edit", () => {
|
|
53
|
-
_resetSeats();
|
|
54
|
-
const evolved: GameSpec = { ...GOLF_SPEC, actions: { ...GOLF_SPEC.actions!, enum: [...GOLF_SPEC.actions!.enum, "flop"] } };
|
|
55
|
-
const play = generateVenueTools(GOLF_VENUE, evolved, cfg()).find(t => t.name === "golf_play")!;
|
|
56
|
-
expect((play.parameters as any).properties.moves.items.properties.type.enum).toContain("flop");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("joinVenue — the one spec-driven seating path (tool + service share it)", () => {
|
|
61
|
-
afterEach(() => { vi.unstubAllGlobals(); _resetSeats(); });
|
|
62
|
-
|
|
63
|
-
it("quickmatch (no matchId) posts to client.join.route and reads the seat block", async () => {
|
|
64
|
-
let url = "", body: any = null;
|
|
65
|
-
vi.stubGlobal("fetch", vi.fn(async (u: any, init?: any) => {
|
|
66
|
-
url = String(u); body = JSON.parse(init.body);
|
|
67
|
-
return { ok: true, json: async () => ({ matchId: "r7", token: "tok", playerIds: ["p1", "p2"], did: "did:wba:me", started: false }) } as any;
|
|
68
|
-
}));
|
|
69
|
-
const seat = await joinVenue(GOLF_VENUE, GOLF_SPEC, cfg({ sessionKey: "did:wba:me" }), { params: { holes: 9 } });
|
|
70
|
-
expect(url).toBe("http://golf.test/quickround");
|
|
71
|
-
expect(body.agentId).toBe("did:wba:me");
|
|
72
|
-
expect(body.holes).toBe(9); // whitelisted join param
|
|
73
|
-
expect(seat.id).toBe("r7"); // seat.id field from spec
|
|
74
|
-
expect(seat.controls).toEqual(["p1", "p2"]); // seat.controls field
|
|
75
|
-
expect(seat.token).toBe("tok");
|
|
76
|
-
expect(seatOf("agent-golf")?.id).toBe("r7");
|
|
77
|
-
expect(session.matchId).toBe("r7"); // mirrored for the watcher
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("rejoining a KNOWN room uses seatRoute with {matchId} substituted", async () => {
|
|
81
|
-
let url = "";
|
|
82
|
-
vi.stubGlobal("fetch", vi.fn(async (u: any) => {
|
|
83
|
-
url = String(u);
|
|
84
|
-
return { ok: true, json: async () => ({ matchId: "r7", token: "t", playerIds: ["p1"] }) } as any;
|
|
85
|
-
}));
|
|
86
|
-
await joinVenue(GOLF_VENUE, GOLF_SPEC, cfg(), { matchId: "r7" });
|
|
87
|
-
expect(url).toBe("http://golf.test/rounds/r7/join");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("rejoin response without the seat-id field falls back to the matchId joined with", async () => {
|
|
91
|
-
_resetSeats();
|
|
92
|
-
// per-room join responses omit matchId (you already know it from the URL) —
|
|
93
|
-
// seat.id must still resolve, or the observe loop 404s and reclaim-loops.
|
|
94
|
-
vi.stubGlobal("fetch", vi.fn(async () =>
|
|
95
|
-
({ ok: true, json: async () => ({ token: "t", playerIds: ["p1", "p2", "p3"], started: true }) }) as any));
|
|
96
|
-
const seat = await joinVenue(GOLF_VENUE, GOLF_SPEC, cfg(), { matchId: "r7" });
|
|
97
|
-
expect(seat.id).toBe("r7");
|
|
98
|
-
expect(session.matchId).toBe("r7");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("service extras (teamSize/identity) ride along but tool calls omit them", async () => {
|
|
102
|
-
let body: any = null;
|
|
103
|
-
vi.stubGlobal("fetch", vi.fn(async (_u: any, init?: any) => {
|
|
104
|
-
body = JSON.parse(init.body);
|
|
105
|
-
return { ok: true, json: async () => ({ matchId: "r1", token: "t", playerIds: [] }) } as any;
|
|
106
|
-
}));
|
|
107
|
-
await joinVenue(GOLF_VENUE, GOLF_SPEC, cfg(), { params: { holes: 18 }, extra: { teamSize: 5, identity: { name: "Eagles" } } });
|
|
108
|
-
expect(body).toMatchObject({ holes: 18, teamSize: 5, identity: { name: "Eagles" } });
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("realtime-venue detection (which venue the autoplay watcher drives)", () => {
|
|
113
|
-
it("a streamed + seated + autoplay venue is realtime; seatless/poll is not", () => {
|
|
114
|
-
expect(isRealtimeVenue(GOLF_SPEC)).toBe(true);
|
|
115
|
-
expect(isRealtimeVenue({ ...GOLF_SPEC, observe: { mode: "poll", suggestedIntervalMs: 1000 } })).toBe(false);
|
|
116
|
-
expect(isRealtimeVenue({ ...GOLF_SPEC, client: { ...GOLF_SPEC.client!, join: null } } as GameSpec)).toBe(false);
|
|
117
|
-
expect(isRealtimeVenue(null)).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("the baked defaults pick soccer (realtime), never the seatless taskmarket", () => {
|
|
121
|
-
const rt = defaultRealtimeVenue();
|
|
122
|
-
expect(rt?.venue.id).toBe("agent-soccer");
|
|
123
|
-
expect(rt?.spec.client?.act.tool).toBe("soccer_play");
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("join-by-id and leave (the lifecycle gap closed in VA-6)", () => {
|
|
128
|
-
afterEach(() => { vi.unstubAllGlobals(); _resetSeats(); });
|
|
129
|
-
|
|
130
|
-
async function exec(name: string, params: unknown) {
|
|
131
|
-
const t = generateVenueTools(GOLF_VENUE, GOLF_SPEC, cfg()).find(x => x.name === name)!;
|
|
132
|
-
const r = await t.execute("id", params as any) as { content: { text: string }[] };
|
|
133
|
-
return JSON.parse(r.content[0]!.text);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
it("the join tool routes a matchId param through seatRoute (not the body)", async () => {
|
|
137
|
-
let url = "", body: any = null;
|
|
138
|
-
vi.stubGlobal("fetch", vi.fn(async (u: any, init?: any) => {
|
|
139
|
-
url = String(u); body = JSON.parse(init.body);
|
|
140
|
-
return { ok: true, json: async () => ({ token: "t", playerIds: ["p1"] }) } as any;
|
|
141
|
-
}));
|
|
142
|
-
const out = await exec("golf_join", { matchId: "r9", holes: 9 });
|
|
143
|
-
expect(url).toBe("http://golf.test/rounds/r9/join"); // seatRoute, not /quickround
|
|
144
|
-
expect(body.holes).toBe(9);
|
|
145
|
-
expect(body.matchId).toBeUndefined(); // matchId is routing, never a body field
|
|
146
|
-
expect(out.joined).toBe("r9");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("attaches x-agent-model (effective model) to venue requests once observed", async () => {
|
|
150
|
-
session.lastModel = "anthropic/claude-sonnet-4-6";
|
|
151
|
-
let hdrs: any = null;
|
|
152
|
-
vi.stubGlobal("fetch", vi.fn(async (_u: any, init?: any) => {
|
|
153
|
-
hdrs = init?.headers;
|
|
154
|
-
return { ok: true, json: async () => ({ matchId: "r1", token: "t", playerIds: [] }) } as any;
|
|
155
|
-
}));
|
|
156
|
-
await exec("golf_join", { holes: 9 });
|
|
157
|
-
expect(hdrs["x-agent-model"]).toBe("anthropic/claude-sonnet-4-6");
|
|
158
|
-
session.lastModel = null; // no model observed → header is simply omitted
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("omitting matchId quickmatches (find-or-create) via the join route", async () => {
|
|
162
|
-
let url = "";
|
|
163
|
-
vi.stubGlobal("fetch", vi.fn(async (u: any) => {
|
|
164
|
-
url = String(u);
|
|
165
|
-
return { ok: true, json: async () => ({ matchId: "r1", token: "t", playerIds: ["p1"] }) } as any;
|
|
166
|
-
}));
|
|
167
|
-
await exec("golf_join", { holes: 9 });
|
|
168
|
-
expect(url).toBe("http://golf.test/quickround");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("the leave tool posts to the leave route and frees the seat locally", async () => {
|
|
172
|
-
// seat first so there's a match to leave
|
|
173
|
-
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({ matchId: "r5", token: "t", playerIds: ["p1"] }) }) as any));
|
|
174
|
-
await joinVenue(GOLF_VENUE, GOLF_SPEC, cfg(), { matchId: "r5" });
|
|
175
|
-
expect(session.matchId).toBe("r5");
|
|
176
|
-
|
|
177
|
-
let url = "", method = "";
|
|
178
|
-
vi.stubGlobal("fetch", vi.fn(async (u: any, init?: any) => {
|
|
179
|
-
url = String(u); method = init?.method;
|
|
180
|
-
return { ok: true, json: async () => ({ left: "r5", forfeit: true, winner: "away" }) } as any;
|
|
181
|
-
}));
|
|
182
|
-
const out = await exec("golf_leave", {});
|
|
183
|
-
expect(method).toBe("POST");
|
|
184
|
-
expect(url).toBe("http://golf.test/rounds/r5/leave");
|
|
185
|
-
expect(out.left).toBe("r5");
|
|
186
|
-
expect(session.matchId).toBeNull(); // freed → ready to join elsewhere
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("leaving when not in a match is a no-op error, not a crash", async () => {
|
|
190
|
-
const out = await exec("golf_leave", {});
|
|
191
|
-
expect(out.error).toMatch(/not in a match/i);
|
|
192
|
-
});
|
|
193
|
-
});
|
package/src/spec.test.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import { fetchSpec, playActionTypes, type GameSpec, type PluginCfg } from "./tools.js";
|
|
3
|
-
import { generateVenueTools } from "./generate.js";
|
|
4
|
-
|
|
5
|
-
const SOCCER_VENUE = { id: "agent-soccer", origin: "pitch", specUrl: "/spec" };
|
|
6
|
-
function withClient(spec: GameSpec): GameSpec {
|
|
7
|
-
return { ...spec, routes: { act: "/matches/{matchId}/players/{playerId}/action", observe: "/matches/{matchId}/agents/{did}/observe" },
|
|
8
|
-
client: { prefix: "soccer", noun: "match",
|
|
9
|
-
join: { tool: "soccer_join", route: "/quickmatch", params: {}, seat: { id: "matchId", token: "token", controls: "playerIds" }, summary: "join" },
|
|
10
|
-
observe: { tool: "soccer_observe", params: {}, summary: "see" },
|
|
11
|
-
act: { tool: "soccer_play", params: {}, summary: "order" } } };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// A FIXTURE manifest with a FAKE action the static list never had. Adding it
|
|
15
|
-
// here must surface it in the generated tool with zero further code change.
|
|
16
|
-
const FIXTURE: GameSpec = {
|
|
17
|
-
game: "agent-soccer",
|
|
18
|
-
specVersion: 1,
|
|
19
|
-
rulesVersion: 1,
|
|
20
|
-
actions: {
|
|
21
|
-
type: "string",
|
|
22
|
-
enum: ["run", "kick", "chase", "shoot", "teleport", "stop"],
|
|
23
|
-
descriptions: { teleport: "blink to the ball (test-only fake action)" },
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function cfg(extra: Partial<PluginCfg> = {}): PluginCfg {
|
|
28
|
-
return { serverUrl: "http://pitch.test", sessionKey: `s-${Math.random().toString(36).slice(2)}`, ...extra };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("Phase 4 — soccer tools generate from /spec (static fallback when absent)", () => {
|
|
32
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
33
|
-
|
|
34
|
-
it("playActionTypes derives the easy-tier enum from the manifest (fake action included)", () => {
|
|
35
|
-
const acts = playActionTypes(FIXTURE, "easy");
|
|
36
|
-
expect(acts).toContain("teleport"); // the fake action surfaced
|
|
37
|
-
expect(acts).toContain("shoot");
|
|
38
|
-
expect(acts).not.toContain("run"); // run/kick are the advanced tier, excluded from easy
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("playActionTypes falls back to the static vocabulary when spec is null", () => {
|
|
42
|
-
const acts = playActionTypes(null, "easy");
|
|
43
|
-
expect(acts).toContain("chase");
|
|
44
|
-
expect(acts).toContain("shoot");
|
|
45
|
-
expect(acts).not.toContain("teleport"); // nothing invented offline
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("fetchSpec returns the manifest from GET /spec", async () => {
|
|
49
|
-
vi.stubGlobal("fetch", vi.fn(async (url: any) => {
|
|
50
|
-
expect(String(url)).toContain("/spec");
|
|
51
|
-
return { ok: true, json: async () => FIXTURE } as any;
|
|
52
|
-
}));
|
|
53
|
-
expect(await fetchSpec(cfg())).toEqual(FIXTURE);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("fetchSpec returns null when /spec is unreachable (offline-safe)", async () => {
|
|
57
|
-
vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("ECONNREFUSED"); }));
|
|
58
|
-
expect(await fetchSpec(cfg())).toBeNull();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("fetchSpec returns null on a non-ok response", async () => {
|
|
62
|
-
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: false, status: 404, text: async () => "nope" } as any)));
|
|
63
|
-
expect(await fetchSpec(cfg())).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("the generated soccer_play (batch) carries the spec's action enum (fake action included)", () => {
|
|
67
|
-
const tools = generateVenueTools(SOCCER_VENUE, withClient(FIXTURE), cfg());
|
|
68
|
-
const play = tools.find(t => t.name === "soccer_play")!;
|
|
69
|
-
const enumList: string[] = (play.parameters as any).properties.moves.items.properties.type.enum;
|
|
70
|
-
expect(enumList).toContain("teleport"); // a server-added action surfaces with zero plugin edit
|
|
71
|
-
expect(enumList).toContain("shoot");
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// ── per-match spec snapshot + protection ladder ──────────────────────────────
|
|
76
|
-
import { fetchMatchSpec } from "./tools.js";
|
|
77
|
-
|
|
78
|
-
describe("fetchMatchSpec — per-game snapshot with the /spec fallback ladder", () => {
|
|
79
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
80
|
-
|
|
81
|
-
it("fetches the match's snapshot first", async () => {
|
|
82
|
-
const seen: string[] = [];
|
|
83
|
-
vi.stubGlobal("fetch", vi.fn(async (url: any) => {
|
|
84
|
-
seen.push(String(url));
|
|
85
|
-
return { ok: true, json: async () => FIXTURE } as any;
|
|
86
|
-
}));
|
|
87
|
-
expect(await fetchMatchSpec(cfg(), "m7")).toEqual(FIXTURE);
|
|
88
|
-
expect(seen[0]).toContain("/matches/m7/spec");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("falls back to server-current /spec when the match route fails (old server)", async () => {
|
|
92
|
-
const seen: string[] = [];
|
|
93
|
-
vi.stubGlobal("fetch", vi.fn(async (url: any) => {
|
|
94
|
-
seen.push(String(url));
|
|
95
|
-
if (String(url).includes("/matches/")) return { ok: false, status: 404 } as any;
|
|
96
|
-
return { ok: true, json: async () => FIXTURE } as any;
|
|
97
|
-
}));
|
|
98
|
-
expect(await fetchMatchSpec(cfg(), "m7")).toEqual(FIXTURE);
|
|
99
|
-
expect(seen.length).toBe(2);
|
|
100
|
-
expect(seen[1]).toMatch(/\/spec$/);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("returns null when the whole ladder fails (caller stays on static fallback and retries later)", async () => {
|
|
104
|
-
vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("ECONNREFUSED"); }));
|
|
105
|
-
expect(await fetchMatchSpec(cfg(), "m7")).toBeNull();
|
|
106
|
-
});
|
|
107
|
-
});
|
package/src/tools.test.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
-
import { pitchClient, agentIdOf, apiKeyOf, type PluginCfg } from "./tools.js";
|
|
3
|
-
import { session } from "./state.js";
|
|
4
|
-
|
|
5
|
-
// A config whose sessionKey is unique per test so the cross-process tmp caches
|
|
6
|
-
// (token/did) never collide between cases.
|
|
7
|
-
function cfg(extra: Partial<PluginCfg> = {}): PluginCfg {
|
|
8
|
-
return { serverUrl: "http://pitch.test", sessionKey: `t-${Math.random().toString(36).slice(2)}`, ...extra };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe("soccer extension join auth", () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
session.did = null;
|
|
14
|
-
session.token = null;
|
|
15
|
-
session.matchId = null;
|
|
16
|
-
});
|
|
17
|
-
afterEach(() => vi.unstubAllGlobals());
|
|
18
|
-
|
|
19
|
-
it("apiKeyOf comes from config only — never from the environment", () => {
|
|
20
|
-
expect(apiKeyOf(cfg({ apiKey: "from-cfg" }))).toBe("from-cfg");
|
|
21
|
-
// a stray env var must NOT be picked up (config-only; avoids the env+network
|
|
22
|
-
// 'credential harvesting' scanner flag and keeps config the single channel).
|
|
23
|
-
vi.stubEnv("AGENTNET_API_KEY", "from-env");
|
|
24
|
-
expect(apiKeyOf(cfg())).toBeUndefined();
|
|
25
|
-
vi.unstubAllEnvs();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("quickMatch sends the Bearer key and learns the returned DID", async () => {
|
|
29
|
-
const seen: { auth?: string } = {};
|
|
30
|
-
vi.stubGlobal("fetch", vi.fn(async (_url: any, init: any) => {
|
|
31
|
-
seen.auth = init?.headers?.Authorization;
|
|
32
|
-
return { ok: true, json: async () => ({ matchId: "m1", team: "home", playerIds: ["h1"], started: false, did: "did:wba:tester", token: "seat-tok" }) } as any;
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
const c = cfg({ apiKey: "good-key" });
|
|
36
|
-
const data = await pitchClient(c).quickMatch(agentIdOf(c), { teamSize: 5 });
|
|
37
|
-
|
|
38
|
-
expect(seen.auth).toBe("Bearer good-key");
|
|
39
|
-
expect(data.did).toBe("did:wba:tester");
|
|
40
|
-
// DID is now this agent's identity for subsequent seat lookups + calls.
|
|
41
|
-
expect(session.did).toBe("did:wba:tester");
|
|
42
|
-
expect(agentIdOf(c)).toBe("did:wba:tester");
|
|
43
|
-
expect(session.token).toBe("seat-tok");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("omits the Authorization header when no key is configured (dev mode)", async () => {
|
|
47
|
-
const seen: { auth?: string } = {};
|
|
48
|
-
vi.stubGlobal("fetch", vi.fn(async (_url: any, init: any) => {
|
|
49
|
-
seen.auth = init?.headers?.Authorization;
|
|
50
|
-
return { ok: true, json: async () => ({ matchId: "m1", team: "home", playerIds: ["h1"], started: false, token: "seat-tok" }) } as any;
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
|
-
const c = cfg(); // no apiKey, no env
|
|
54
|
-
await pitchClient(c).quickMatch(agentIdOf(c), {});
|
|
55
|
-
expect(seen.auth).toBeUndefined();
|
|
56
|
-
});
|
|
57
|
-
});
|
package/src/venues.test.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import { venuesTool, type GameSpec, type PluginCfg } from "./tools.js";
|
|
3
|
-
import { generateVenueTools, _resetSeats } from "./generate.js";
|
|
4
|
-
|
|
5
|
-
const REGISTRY = { marketplaces: [
|
|
6
|
-
{ id: "agent-soccer", name: "Agent Soccer", origin: "pitch", specUrl: "/spec", feeBps: 2000, status: "live", kind: "game" },
|
|
7
|
-
{ id: "taskmarket", name: "Agent Task Market", origin: "taskmarket", specUrl: "/spec", feeBps: 1000, status: "live", kind: "work" },
|
|
8
|
-
] };
|
|
9
|
-
|
|
10
|
-
const WORK_VENUE = { id: "taskmarket", origin: "taskmarket", specUrl: "/spec" };
|
|
11
|
-
const WORK_SPEC: GameSpec = {
|
|
12
|
-
game: "taskmarket", specVersion: 1, rulesVersion: 1,
|
|
13
|
-
actions: { type: "string", enum: ["post", "bid", "deliver"], descriptions: {} },
|
|
14
|
-
observe: { mode: "poll", suggestedIntervalMs: 30000 },
|
|
15
|
-
routes: { observe: "/agents/{did}/observe", act: "/agents/{did}/action" },
|
|
16
|
-
client: {
|
|
17
|
-
prefix: "taskmarket", noun: "task", lobby: null, join: null,
|
|
18
|
-
observe: { tool: "work_observe", params: { cursor: { type: "number" } }, summary: "See the task market." },
|
|
19
|
-
act: { tool: "work_act", params: { title: { type: "string" }, description: { type: "string" }, budget: { type: "number" } }, summary: "Act in the task market." },
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function cfg(extra: Partial<PluginCfg> = {}): PluginCfg {
|
|
24
|
-
return { serverUrl: "http://pitch.test", sessionKey: "did:wba:me", ...extra };
|
|
25
|
-
}
|
|
26
|
-
function workTool(name: string) {
|
|
27
|
-
const tools = generateVenueTools(WORK_VENUE, WORK_SPEC, cfg());
|
|
28
|
-
return tools.find(t => t.name === name)!;
|
|
29
|
-
}
|
|
30
|
-
async function runTool(t: any, params: unknown): Promise<any> {
|
|
31
|
-
const r = await t.execute("id", params as any) as { content: { text: string }[] };
|
|
32
|
-
return JSON.parse(r.content[0]!.text);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe("multi-venue tools (marketplace registry → generated work tools)", () => {
|
|
36
|
-
afterEach(() => { vi.unstubAllGlobals(); _resetSeats(); });
|
|
37
|
-
|
|
38
|
-
it("venues lists the registry with kinds", async () => {
|
|
39
|
-
vi.stubGlobal("fetch", vi.fn(async (url: any) => {
|
|
40
|
-
expect(String(url)).toContain("/platform/marketplaces");
|
|
41
|
-
return { ok: true, json: async () => REGISTRY } as any;
|
|
42
|
-
}));
|
|
43
|
-
const out = await runTool(venuesTool(cfg()), {});
|
|
44
|
-
expect(out.venues.map((v: any) => v.kind)).toEqual(["game", "work"]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("work_observe substitutes the DID into the venue's route template", async () => {
|
|
48
|
-
const seen: string[] = [];
|
|
49
|
-
vi.stubGlobal("fetch", vi.fn(async (url: any, init?: any) => {
|
|
50
|
-
seen.push(String(url));
|
|
51
|
-
expect(init?.headers?.["x-caller-did"]).toBe("did:wba:me");
|
|
52
|
-
return { ok: true, json: async () => ({ summary: "quiet", events: [], cursor: 0 }) } as any;
|
|
53
|
-
}));
|
|
54
|
-
const out = await runTool(workTool("work_observe"), {});
|
|
55
|
-
expect(out.view.summary).toBe("quiet");
|
|
56
|
-
expect(seen.some(u => u.includes("/agents/did%3Awba%3Ame/observe") || u.includes("/agents/did:wba:me/observe"))).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("work_act validates against the venue enum and posts the uniform body", async () => {
|
|
60
|
-
let posted: any = null;
|
|
61
|
-
vi.stubGlobal("fetch", vi.fn(async (_url: any, init?: any) => {
|
|
62
|
-
posted = JSON.parse(init.body);
|
|
63
|
-
return { ok: true, json: async () => ({ id: "task-1", status: "open" }) } as any;
|
|
64
|
-
}));
|
|
65
|
-
const bad = await runTool(workTool("work_act"), { type: "fly" });
|
|
66
|
-
expect(bad.error).toContain("post");
|
|
67
|
-
const ok2 = await runTool(workTool("work_act"), { type: "post", title: "t", description: "d", budget: 5 });
|
|
68
|
-
expect(ok2.result.id).toBe("task-1");
|
|
69
|
-
expect(posted).toEqual({ type: "post", title: "t", description: "d", budget: 5 });
|
|
70
|
-
});
|
|
71
|
-
});
|
package/src/watcher.test.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, writeFileSync, rmSync, utimesSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { strategyText, _clearStrategyCache, prompt, parseSseBlock } from "./watcher.js";
|
|
6
|
-
import type { TeamView } from "./format.js";
|
|
7
|
-
import type { GameSpec } from "./tools.js";
|
|
8
|
-
|
|
9
|
-
let dir: string;
|
|
10
|
-
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "soccer-strat-")); _clearStrategyCache(); });
|
|
11
|
-
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
|
12
|
-
|
|
13
|
-
const VIEW = {
|
|
14
|
-
tick: 1, clock: 0, phase: "live", team: "home",
|
|
15
|
-
score: { home: 0, away: 0 },
|
|
16
|
-
field: { length: 105, width: 68, attackGoal: { x: 52.5, y: 0 }, ownGoal: { x: -52.5, y: 0 }, goalHalfWidth: 3.66, tickHz: 10 },
|
|
17
|
-
mine: [{ id: "home-9", number: 9, pos: { x: 0, y: 0 }, vel: { x: 0, y: 0 }, hasBall: true }],
|
|
18
|
-
ball: { pos: { x: 0, y: 0 }, vel: { x: 0, y: 0 }, owner: "home-9" },
|
|
19
|
-
teammates: [], opponents: [],
|
|
20
|
-
} as unknown as TeamView;
|
|
21
|
-
|
|
22
|
-
describe("Phase 5 — strategyText (mtime-cached, capped) + prompt injection", () => {
|
|
23
|
-
it("returns '' when the file is absent", () => {
|
|
24
|
-
expect(strategyText(join(dir, "nope.md"))).toBe("");
|
|
25
|
-
expect(strategyText(undefined)).toBe("");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("reads a present file", () => {
|
|
29
|
-
const f = join(dir, "strategy.md");
|
|
30
|
-
writeFileSync(f, "Press high. Keep the ball.");
|
|
31
|
-
expect(strategyText(f)).toContain("Press high");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("caps the injected text at ~1k chars", () => {
|
|
35
|
-
const f = join(dir, "strategy.md");
|
|
36
|
-
writeFileSync(f, "x".repeat(5000));
|
|
37
|
-
expect(strategyText(f).length).toBeLessThanOrEqual(1000);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("refreshes when the file mtime changes", () => {
|
|
41
|
-
const f = join(dir, "strategy.md");
|
|
42
|
-
writeFileSync(f, "first plan");
|
|
43
|
-
expect(strategyText(f)).toContain("first plan");
|
|
44
|
-
writeFileSync(f, "second plan");
|
|
45
|
-
const future = new Date(Date.now() + 5000);
|
|
46
|
-
utimesSync(f, future, future);
|
|
47
|
-
const out = strategyText(f);
|
|
48
|
-
expect(out).toContain("second plan");
|
|
49
|
-
expect(out).not.toContain("first plan");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("prompt injects the strategy block when a strategyFile is set", () => {
|
|
53
|
-
const f = join(dir, "strategy.md");
|
|
54
|
-
writeFileSync(f, "Park the bus. Counter fast.");
|
|
55
|
-
const p = prompt(VIEW, "easy", f);
|
|
56
|
-
expect(p.toLowerCase()).toContain("standing instructions");
|
|
57
|
-
expect(p).toContain("Park the bus");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("prompt has no strategy block when the file is absent", () => {
|
|
61
|
-
const p = prompt(VIEW, "easy", join(dir, "missing.md"));
|
|
62
|
-
expect(p.toLowerCase()).not.toContain("standing instructions");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// ── the self-instructable envelope (generic GSP client) ─────────────────────
|
|
67
|
-
const SPEC = {
|
|
68
|
-
game: "agent-soccer", specVersion: 1, rulesVersion: 2,
|
|
69
|
-
actions: { type: "string", enum: ["chase", "shoot", "lob"], descriptions: { lob: "chip it" } },
|
|
70
|
-
instructions: {
|
|
71
|
-
system: "You are a decisive tactician.",
|
|
72
|
-
play: "SERVER PLAY GUIDANCE: lob when the keeper is off his line.",
|
|
73
|
-
output: 'Reply with ONLY JSON {"moves":{...}}',
|
|
74
|
-
},
|
|
75
|
-
} as unknown as GameSpec;
|
|
76
|
-
|
|
77
|
-
const VIEW_WITH_SUMMARY = { ...VIEW, summary: "⚽ SERVER-RENDERED SITUATION." } as TeamView & { summary: string };
|
|
78
|
-
|
|
79
|
-
describe("generic prompt — instructions + summary come from the server", () => {
|
|
80
|
-
it("uses the server's instructions and summary when a spec is cached", () => {
|
|
81
|
-
const p = prompt(VIEW_WITH_SUMMARY, "easy", undefined, SPEC, "golf_play");
|
|
82
|
-
expect(p).toContain("SERVER-RENDERED SITUATION");
|
|
83
|
-
expect(p).toContain("SERVER PLAY GUIDANCE");
|
|
84
|
-
// tool-calling host: act via the venue's act tool, named from the spec
|
|
85
|
-
expect(p).toContain("golf_play");
|
|
86
|
-
// the plugin's hardcoded soccer prose is gone on this path
|
|
87
|
-
expect(p).not.toContain("YOU ATTACK +x: opponent goal at x=+52.5");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("keeps the strategy block on the generic path", () => {
|
|
91
|
-
const f = join(dir, "strategy.md");
|
|
92
|
-
writeFileSync(f, "Park the bus tonight.");
|
|
93
|
-
const p = prompt(VIEW_WITH_SUMMARY, "easy", f, SPEC);
|
|
94
|
-
expect(p).toContain("Park the bus tonight");
|
|
95
|
-
expect(p).toContain("SERVER PLAY GUIDANCE");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("falls back to local rendering when there is no spec (old server)", () => {
|
|
99
|
-
const p = prompt(VIEW, "easy", undefined, null);
|
|
100
|
-
expect(p).toContain("soccer_play"); // the legacy describeTeam path
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("parseSseBlock — named events for the handshake", () => {
|
|
105
|
-
it("parses a named event block", () => {
|
|
106
|
-
const b = parseSseBlock('event: spec\ndata: {"game":"agent-soccer"}');
|
|
107
|
-
expect(b.event).toBe("spec");
|
|
108
|
-
expect(JSON.parse(b.data!)).toEqual({ game: "agent-soccer" });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("parses a plain data block (no event name)", () => {
|
|
112
|
-
const b = parseSseBlock('data: {"tick":1}');
|
|
113
|
-
expect(b.event).toBeUndefined();
|
|
114
|
-
expect(JSON.parse(b.data!)).toEqual({ tick: 1 });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("ignores comments and empty payloads", () => {
|
|
118
|
-
expect(parseSseBlock(": connected").data).toBeUndefined();
|
|
119
|
-
expect(parseSseBlock("data: ").data).toBeUndefined();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
import { observeUrl } from "./watcher.js";
|
|
124
|
-
|
|
125
|
-
describe("observeUrl — venue-agnostic observe endpoint (VA-4)", () => {
|
|
126
|
-
it("substitutes spec.routes.observe with matchId + did", () => {
|
|
127
|
-
const spec = { routes: { observe: "/matches/{matchId}/agents/{did}/observe" } } as any;
|
|
128
|
-
expect(observeUrl(spec, "m7", "did:wba:me")).toBe("/matches/m7/agents/did%3Awba%3Ame/observe");
|
|
129
|
-
});
|
|
130
|
-
it("uses a non-soccer venue's route shape unchanged", () => {
|
|
131
|
-
const spec = { routes: { observe: "/golf/{matchId}/player/{did}/look" } } as any;
|
|
132
|
-
expect(observeUrl(spec, "g3", "alice")).toBe("/golf/g3/player/alice/look");
|
|
133
|
-
});
|
|
134
|
-
it("falls back to the soccer-literal route when no spec", () => {
|
|
135
|
-
expect(observeUrl(null, "m9", "bob")).toBe("/matches/m9/agents/bob/observe");
|
|
136
|
-
});
|
|
137
|
-
});
|