weather-namespace.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
  2. import { tmpdir } from "node:os";
  3. import { join } from "node:path";
  4. import { describe, expect, it } from "vitest";
  5. import { createEngine, WEATHER_ENTITY_ID } from "@nengine/core";
  6. import { createEngineNamespaces, McpGateway } from "../src/index.js";
  7. const TIME_YAML = [
  8. "id: default",
  9. "dayLengthTicks: 480",
  10. "seasons:",
  11. " - { name: spring, dayCount: 28 }",
  12. " - { name: summer, dayCount: 28 }",
  13. "start: { day: 1, season: spring }",
  14. "",
  15. ].join("\n");
  16. const FARM_YAML = [
  17. "id: farm",
  18. "width: 2",
  19. "height: 2",
  20. 'tilesetRef: "tiles"',
  21. "spawn: { x: 0, y: 0 }",
  22. "layers:",
  23. " - name: ground",
  24. " data:",
  25. " - [a, b]",
  26. " - [b, a]",
  27. "",
  28. ].join("\n");
  29. const WEATHER_YAML = [
  30. "id: default",
  31. "defaults:",
  32. " sunny: { name: Sunny }",
  33. " rain: { name: Rain }",
  34. "seasons:",
  35. " - season: spring",
  36. " weathers:",
  37. " - { id: sunny, weight: 70 }",
  38. " - { id: rain, weight: 30 }",
  39. "",
  40. ].join("\n");
  41. interface Setup {
  42. engine: ReturnType<typeof createEngine>;
  43. gateway: McpGateway;
  44. tmp: string;
  45. close: () => Promise<void>;
  46. }
  47. async function setup(): Promise<Setup> {
  48. const tmp = await mkdtemp(join(tmpdir(), "farm-weather-ns-"));
  49. await mkdir(join(tmp, "maps"), { recursive: true });
  50. await writeFile(join(tmp, "time.yaml"), TIME_YAML);
  51. await writeFile(join(tmp, "maps", "farm.yaml"), FARM_YAML);
  52. await writeFile(join(tmp, "weather.yaml"), WEATHER_YAML);
  53. const engine = createEngine({ contentDir: tmp });
  54. const gateway = new McpGateway();
  55. for (const namespace of createEngineNamespaces(engine, { gateway })) {
  56. gateway.register(namespace);
  57. }
  58. await engine.start();
  59. return {
  60. engine,
  61. gateway,
  62. tmp,
  63. close: async () => {
  64. await engine.stop();
  65. await rm(tmp, { recursive: true, force: true });
  66. },
  67. };
  68. }
  69. async function callText(gateway: McpGateway, name: string, args: Record<string, unknown> = {}): Promise<{
  70. ok: boolean;
  71. text: string;
  72. }> {
  73. const result = await gateway.callTool(name, args);
  74. return { ok: result.isError !== true, text: result.content[0]?.text ?? "" };
  75. }
  76. describe("weather namespace", () => {
  77. it("weather.get returns the initial state (current season, display name)", async () => {
  78. const { engine, gateway, close } = await setup();
  79. try {
  80. expect(engine.world.get(WEATHER_ENTITY_ID)?.weatherId).toBe("sunny");
  81. const got = await callText(gateway, "weather.get");
  82. expect(got.ok).toBe(true);
  83. expect(JSON.parse(got.text)).toEqual({ weatherId: "sunny", name: "Sunny", season: "spring" });
  84. } finally {
  85. await close();
  86. }
  87. });
  88. it("weather.set forces a declared weather and returns the new state with from", async () => {
  89. const { engine, gateway, close } = await setup();
  90. try {
  91. const set = await callText(gateway, "weather.set", { weatherId: "rain" });
  92. expect(set.ok).toBe(true);
  93. expect(JSON.parse(set.text)).toEqual({ weatherId: "rain", name: "Rain", season: "spring", from: "sunny" });
  94. expect(engine.world.get(WEATHER_ENTITY_ID)?.weatherId).toBe("rain");
  95. const same = await callText(gateway, "weather.set", { weatherId: "rain" });
  96. expect(JSON.parse(same.text)).toEqual({ weatherId: "rain", name: "Rain", season: "spring" });
  97. } finally {
  98. await close();
  99. }
  100. });
  101. it("weather.set rejects unknown ids and blank ids", async () => {
  102. const { gateway, close } = await setup();
  103. try {
  104. const unknown = await callText(gateway, "weather.set", { weatherId: "blizzard" });
  105. expect(unknown.ok).toBe(false);
  106. expect(unknown.text).toMatch(/unknown weather id: blizzard/);
  107. const blank = await callText(gateway, "weather.set", { weatherId: "" });
  108. expect(blank.ok).toBe(false);
  109. expect(blank.text).toMatch(/non-empty string/);
  110. } finally {
  111. await close();
  112. }
  113. });
  114. it("weather://state resource reflects the current weather", async () => {
  115. const { gateway, close } = await setup();
  116. try {
  117. const state = await gateway.readResource("weather://state");
  118. expect(JSON.parse(state.contents[0]?.text ?? "{}")).toEqual({ weatherId: "sunny", name: "Sunny", season: "spring" });
  119. } finally {
  120. await close();
  121. }
  122. });
  123. it("weather.get reports an error result when no weather def is registered", async () => {
  124. const tmp = await mkdtemp(join(tmpdir(), "farm-weather-none-"));
  125. await mkdir(join(tmp, "maps"), { recursive: true });
  126. await writeFile(join(tmp, "time.yaml"), TIME_YAML);
  127. await writeFile(join(tmp, "maps", "farm.yaml"), FARM_YAML);
  128. const engine = createEngine({ contentDir: tmp });
  129. const gateway = new McpGateway();
  130. for (const namespace of createEngineNamespaces(engine, { gateway })) {
  131. gateway.register(namespace);
  132. }
  133. await engine.start();
  134. try {
  135. const got = await callText(gateway, "weather.get");
  136. expect(got.ok).toBe(false);
  137. expect(got.text).toMatch(/no weather def registered/);
  138. } finally {
  139. await engine.stop();
  140. await rm(tmp, { recursive: true, force: true });
  141. }
  142. });
  143. });