@intentius/chant-lexicon-fly 0.16.0 → 0.18.0

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.
Files changed (65) hide show
  1. package/README.md +2 -2
  2. package/dist/composites/fly-deploy.d.ts +1 -1
  3. package/dist/composites/fly-deploy.d.ts.map +1 -1
  4. package/dist/coverage.d.ts +15 -0
  5. package/dist/coverage.d.ts.map +1 -0
  6. package/dist/emulator-freshness-cli.d.ts +11 -0
  7. package/dist/emulator-freshness-cli.d.ts.map +1 -0
  8. package/dist/emulator-freshness.d.ts +38 -0
  9. package/dist/emulator-freshness.d.ts.map +1 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/mcp/context-tools.d.ts +15 -0
  13. package/dist/mcp/context-tools.d.ts.map +1 -0
  14. package/dist/op/activities/emulator-images.d.ts +20 -0
  15. package/dist/op/activities/emulator-images.d.ts.map +1 -0
  16. package/dist/op/activities/flaps.d.ts +1 -1
  17. package/dist/op/activities/flaps.d.ts.map +1 -1
  18. package/dist/op/activities/index.d.ts +4 -0
  19. package/dist/op/activities/index.d.ts.map +1 -1
  20. package/dist/op/activities/machines-contract.d.ts +36 -0
  21. package/dist/op/activities/machines-contract.d.ts.map +1 -0
  22. package/dist/op/activities/sprites-contract.d.ts +45 -0
  23. package/dist/op/activities/sprites-contract.d.ts.map +1 -0
  24. package/dist/op/activities/sprites-emulator.d.ts +29 -0
  25. package/dist/op/activities/sprites-emulator.d.ts.map +1 -0
  26. package/dist/op/activities/sprites-fake.d.ts +62 -0
  27. package/dist/op/activities/sprites-fake.d.ts.map +1 -0
  28. package/dist/op/activities/sprites.d.ts +195 -0
  29. package/dist/op/activities/sprites.d.ts.map +1 -0
  30. package/dist/plugin.d.ts.map +1 -1
  31. package/dist/reference-catalog.d.ts +21 -0
  32. package/dist/reference-catalog.d.ts.map +1 -0
  33. package/package.json +6 -2
  34. package/src/composites/fly-deploy.ts +1 -1
  35. package/src/coverage.ts +49 -0
  36. package/src/emulator-freshness-cli.ts +49 -0
  37. package/src/emulator-freshness.test.ts +86 -0
  38. package/src/emulator-freshness.ts +87 -0
  39. package/src/index.ts +15 -0
  40. package/src/mcp/context-tools.test.ts +27 -0
  41. package/src/mcp/context-tools.ts +120 -0
  42. package/src/op/activities/emulator-images.ts +21 -0
  43. package/src/op/activities/flaps.test.ts +2 -1
  44. package/src/op/activities/flaps.ts +3 -2
  45. package/src/op/activities/index.ts +53 -0
  46. package/src/op/activities/machines-contract.docker.integration.test.ts +72 -0
  47. package/src/op/activities/machines-contract.test.ts +49 -0
  48. package/src/op/activities/machines-contract.ts +73 -0
  49. package/src/op/activities/sprites-contract.docker.integration.test.ts +74 -0
  50. package/src/op/activities/sprites-contract.test.ts +60 -0
  51. package/src/op/activities/sprites-contract.ts +61 -0
  52. package/src/op/activities/sprites-emulator.ts +46 -0
  53. package/src/op/activities/sprites-fake.ts +314 -0
  54. package/src/op/activities/sprites.docker.integration.test.ts +99 -0
  55. package/src/op/activities/sprites.integration.test.ts +158 -0
  56. package/src/op/activities/sprites.real.test.ts +56 -0
  57. package/src/op/activities/sprites.test.ts +296 -0
  58. package/src/op/activities/sprites.ts +527 -0
  59. package/src/plugin.ts +28 -5
  60. package/src/reference-catalog.test.ts +50 -0
  61. package/src/reference-catalog.ts +39 -0
  62. package/src/skills/chant-fly-patterns.md +2 -2
  63. package/src/skills/chant-fly-sprites.md +104 -0
  64. package/src/skills/chant-fly.md +2 -2
  65. package/src/generated/.gitkeep +0 -0
@@ -0,0 +1,296 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ resolveSpritesEndpoint,
4
+ DEFAULT_SPRITES_BASE_URL,
5
+ accumulateExecFrames,
6
+ parseCheckpointNdjson,
7
+ pickCheckpointByComment,
8
+ splitCommand,
9
+ spriteExecWsUrl,
10
+ defaultSpritesHttp,
11
+ spriteCreate,
12
+ spriteCheckpoint,
13
+ spriteRestore,
14
+ listCheckpoints,
15
+ spriteDestroy,
16
+ type SpritesHttp,
17
+ type Checkpoint,
18
+ } from "./sprites";
19
+
20
+ const enc = new TextEncoder();
21
+
22
+ /** Build one exec frame `[stream][payload]`. */
23
+ function frame(stream: number, payload?: string | number[]): Uint8Array {
24
+ const body = typeof payload === "string" ? enc.encode(payload) : Uint8Array.from(payload ?? []);
25
+ return Uint8Array.from([stream, ...body]);
26
+ }
27
+
28
+ // A recording HTTP stub: captures every call and answers from a callback.
29
+ function recorder(answer: (method: string, url: string, body?: unknown) => { status: number; text: string }) {
30
+ const calls: Array<{ method: string; url: string; body?: unknown; headers?: Record<string, string> }> = [];
31
+ const http: SpritesHttp = async (method, url, body, headers) => {
32
+ calls.push({ method, url, body, headers });
33
+ return answer(method, url, body);
34
+ };
35
+ return { http, calls };
36
+ }
37
+
38
+ // ── Exec frame accumulator (the WS framing, socket-free) ──────────────────────
39
+
40
+ describe("accumulateExecFrames", () => {
41
+ test("stdout then exit 0", () => {
42
+ expect(accumulateExecFrames([frame(1, "hi\n"), frame(3, [0])])).toEqual({
43
+ stdout: "hi\n",
44
+ stderr: "",
45
+ exitCode: 0,
46
+ });
47
+ });
48
+
49
+ test("stderr then exit 1", () => {
50
+ expect(accumulateExecFrames([frame(2, "err"), frame(3, [1])])).toEqual({
51
+ stdout: "",
52
+ stderr: "err",
53
+ exitCode: 1,
54
+ });
55
+ });
56
+
57
+ test("concatenates multiple stdout frames and ignores stdin/eof frames", () => {
58
+ const frames = [frame(1, "foo"), frame(0), frame(1, "bar"), frame(2, "warn"), frame(4), frame(3, [7])];
59
+ expect(accumulateExecFrames(frames)).toEqual({ stdout: "foobar", stderr: "warn", exitCode: 7 });
60
+ });
61
+
62
+ test("no exit frame defaults exitCode to 0; empty frames are skipped", () => {
63
+ expect(accumulateExecFrames([new Uint8Array(0), frame(1, "x")])).toEqual({
64
+ stdout: "x",
65
+ stderr: "",
66
+ exitCode: 0,
67
+ });
68
+ });
69
+
70
+ test("a multi-byte character split across two stdout frames decodes intact", () => {
71
+ // "€" is E2 82 AC; split after the first byte across two frames.
72
+ expect(accumulateExecFrames([frame(1, [0xe2]), frame(1, [0x82, 0xac]), frame(3, [0])]).stdout).toBe("€");
73
+ });
74
+ });
75
+
76
+ // ── Checkpoint NDJSON parsing ─────────────────────────────────────────────────
77
+
78
+ describe("parseCheckpointNdjson", () => {
79
+ test("mines the version id from real Sprites' message text (type/data shape)", () => {
80
+ // Verbatim shape from api.sprites.dev: no structured id field — the version
81
+ // rides inside the " ID: v1" detail line and the completion message.
82
+ const body = [
83
+ '{"type":"info","data":"Creating checkpoint...","time":"2026-07-12T05:01:17Z"}',
84
+ '{"type":"info","data":"Checkpoint created successfully"}',
85
+ '{"type":"info","data":" ID: v1"}',
86
+ '{"type":"info","data":" Path: checkpoints/v1"}',
87
+ '{"type":"complete","data":"Checkpoint v1 created successfully"}',
88
+ ].join("\n");
89
+ expect(parseCheckpointNdjson(body)).toEqual({ checkpointId: "v1" });
90
+ });
91
+
92
+ test("honors a structured id from the older {event, id} shape", () => {
93
+ const body = '{"event":"info","message":"checkpointing"}\n{"event":"complete","id":"v3"}\n';
94
+ expect(parseCheckpointNdjson(body)).toEqual({ checkpointId: "v3" });
95
+ });
96
+
97
+ test("blank and unparseable lines are skipped; no id → empty", () => {
98
+ expect(parseCheckpointNdjson('\n{"event":"info"}\nnot-json\n')).toEqual({ checkpointId: "" });
99
+ });
100
+ });
101
+
102
+ // ── Comment picker (newest match) ─────────────────────────────────────────────
103
+
104
+ describe("pickCheckpointByComment", () => {
105
+ const list: Checkpoint[] = [
106
+ { id: "v1", comment: "pre-run", create_time: "2026-01-01T00:00:00.000Z", is_auto: false },
107
+ { id: "v2", comment: "other", create_time: "2026-01-01T00:00:01.000Z", is_auto: false },
108
+ { id: "v3", comment: "pre-run", create_time: "2026-01-01T00:00:02.000Z", is_auto: false },
109
+ ];
110
+
111
+ test("returns the newest checkpoint carrying the comment", () => {
112
+ expect(pickCheckpointByComment(list, "pre-run")?.id).toBe("v3");
113
+ });
114
+
115
+ test("returns undefined when nothing matches", () => {
116
+ expect(pickCheckpointByComment(list, "nope")).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ // ── Endpoint resolution (S3) ──────────────────────────────────────────────────
121
+
122
+ describe("resolveSpritesEndpoint (S3)", () => {
123
+ test("explicit arg wins and trailing slash is stripped", () => {
124
+ expect(resolveSpritesEndpoint({ endpoint: "http://localhost:9000/" }, {} as NodeJS.ProcessEnv)).toBe(
125
+ "http://localhost:9000",
126
+ );
127
+ });
128
+
129
+ test("SPRITES_BASE_URL env when no arg", () => {
130
+ expect(resolveSpritesEndpoint({}, { SPRITES_BASE_URL: "http://fake:9000" } as NodeJS.ProcessEnv)).toBe(
131
+ "http://fake:9000",
132
+ );
133
+ });
134
+
135
+ test("arg beats env", () => {
136
+ expect(
137
+ resolveSpritesEndpoint({ endpoint: "http://arg:1" }, { SPRITES_BASE_URL: "http://env:2" } as NodeJS.ProcessEnv),
138
+ ).toBe("http://arg:1");
139
+ });
140
+
141
+ test("default is real Sprites", () => {
142
+ expect(resolveSpritesEndpoint({}, {} as NodeJS.ProcessEnv)).toBe(DEFAULT_SPRITES_BASE_URL);
143
+ });
144
+ });
145
+
146
+ // ── Command tokenizing + exec WS url ──────────────────────────────────────────
147
+
148
+ describe("splitCommand", () => {
149
+ test("splits on whitespace and respects quotes", () => {
150
+ expect(splitCommand("echo hi")).toEqual(["echo", "hi"]);
151
+ expect(splitCommand('sh -c "exit 7"')).toEqual(["sh", "-c", "exit 7"]);
152
+ expect(splitCommand("echo bad > /state; false")).toEqual(["echo", "bad", ">", "/state;", "false"]);
153
+ });
154
+ });
155
+
156
+ describe("spriteExecWsUrl", () => {
157
+ test("http → ws with cmd/path/stdin/cc params", () => {
158
+ const url = new URL(spriteExecWsUrl("http://x:9000", "task-1", "echo hi"));
159
+ expect(url.protocol).toBe("ws:");
160
+ expect(url.pathname).toBe("/v1/sprites/task-1/exec");
161
+ expect(url.searchParams.getAll("cmd")).toEqual(["echo", "hi"]);
162
+ expect(url.searchParams.get("path")).toBe("echo");
163
+ expect(url.searchParams.get("stdin")).toBe("false");
164
+ expect(url.searchParams.get("cc")).toBe("true");
165
+ });
166
+
167
+ test("https → wss", () => {
168
+ expect(spriteExecWsUrl("https://api.sprites.dev", "s", "ls").startsWith("wss://api.sprites.dev/")).toBe(true);
169
+ });
170
+ });
171
+
172
+ // ── Activity request shapes (injected SpritesHttp; no real sockets) ───────────
173
+
174
+ describe("spriteCreate", () => {
175
+ test("POSTs /v1/sprites with the name and parses { id, url }", async () => {
176
+ const { http, calls } = recorder(() => ({
177
+ status: 201,
178
+ text: JSON.stringify({ id: "sprite-1", url: "http://h/s/task-1" }),
179
+ }));
180
+ const res = await spriteCreate({ name: "task-1", endpoint: "http://x:9000", image: "base:1" }, undefined, http);
181
+ expect(res).toEqual({ id: "sprite-1", url: "http://h/s/task-1" });
182
+ expect(calls[0].method).toBe("POST");
183
+ expect(calls[0].url).toBe("http://x:9000/v1/sprites");
184
+ expect(calls[0].body).toEqual({ name: "task-1", image: "base:1" });
185
+ });
186
+ });
187
+
188
+ describe("spriteCheckpoint", () => {
189
+ test("POSTs the singular /checkpoint with the comment and reads the NDJSON complete id", async () => {
190
+ const { http, calls } = recorder(() => ({
191
+ status: 200,
192
+ text: '{"event":"info"}\n{"event":"complete","id":"v5"}\n',
193
+ }));
194
+ const res = await spriteCheckpoint({ id: "task-1", comment: "pre-run", endpoint: "http://x" }, undefined, http);
195
+ expect(res).toEqual({ checkpointId: "v5" });
196
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1/checkpoint");
197
+ expect(calls[0].body).toEqual({ comment: "pre-run" });
198
+ });
199
+
200
+ test("omits the comment key when empty", async () => {
201
+ const { http, calls } = recorder(() => ({ status: 200, text: '{"event":"complete","id":"v1"}\n' }));
202
+ await spriteCheckpoint({ id: "task-1", endpoint: "http://x" }, undefined, http);
203
+ expect(calls[0].body).toBeUndefined();
204
+ });
205
+ });
206
+
207
+ describe("listCheckpoints", () => {
208
+ test("GETs the plural /checkpoints and returns the array", async () => {
209
+ const list = [{ id: "v1", comment: "pre-run", create_time: "2026-01-01T00:00:00.000Z", is_auto: false }];
210
+ const { http, calls } = recorder(() => ({ status: 200, text: JSON.stringify(list) }));
211
+ const res = await listCheckpoints({ id: "task-1", endpoint: "http://x" }, undefined, http);
212
+ expect(res).toEqual(list);
213
+ expect(calls[0].method).toBe("GET");
214
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1/checkpoints");
215
+ });
216
+ });
217
+
218
+ describe("spriteRestore", () => {
219
+ test("explicit checkpoint id → POST /checkpoints/{id}/restore", async () => {
220
+ const { http, calls } = recorder(() => ({ status: 200, text: '{"event":"complete","id":"v2"}\n' }));
221
+ const res = await spriteRestore({ id: "task-1", checkpoint: "v2", endpoint: "http://x" }, undefined, http);
222
+ expect(res).toEqual({});
223
+ expect(calls).toHaveLength(1);
224
+ expect(calls[0].method).toBe("POST");
225
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1/checkpoints/v2/restore");
226
+ });
227
+
228
+ test("comment → list, pick newest match, then restore that id", async () => {
229
+ const list: Checkpoint[] = [
230
+ { id: "v1", comment: "pre-run", create_time: "2026-01-01T00:00:00.000Z", is_auto: false },
231
+ { id: "v4", comment: "pre-run", create_time: "2026-01-01T00:00:09.000Z", is_auto: false },
232
+ ];
233
+ const { http, calls } = recorder((method) =>
234
+ method === "GET"
235
+ ? { status: 200, text: JSON.stringify(list) }
236
+ : { status: 200, text: '{"event":"complete","id":"v4"}\n' },
237
+ );
238
+ await spriteRestore({ id: "task-1", comment: "pre-run", endpoint: "http://x" }, undefined, http);
239
+ expect(calls[0].method).toBe("GET");
240
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1/checkpoints");
241
+ expect(calls[1].url).toBe("http://x/v1/sprites/task-1/checkpoints/v4/restore");
242
+ });
243
+
244
+ test("throws when no checkpoint carries the comment", async () => {
245
+ const { http } = recorder(() => ({ status: 200, text: "[]" }));
246
+ await expect(
247
+ spriteRestore({ id: "task-1", comment: "nope", endpoint: "http://x" }, undefined, http),
248
+ ).rejects.toThrow(/no checkpoint matching comment "nope"/);
249
+ });
250
+ });
251
+
252
+ describe("spriteDestroy", () => {
253
+ test("DELETEs /v1/sprites/{id}", async () => {
254
+ const { http, calls } = recorder(() => ({ status: 200, text: "{}" }));
255
+ await spriteDestroy({ id: "task-1", endpoint: "http://x" }, undefined, http);
256
+ expect(calls[0].method).toBe("DELETE");
257
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1");
258
+ });
259
+
260
+ test("a 404 is idempotent (already gone), not an error", async () => {
261
+ const { http } = recorder(() => ({ status: 404, text: "gone" }));
262
+ await expect(spriteDestroy({ id: "task-1", endpoint: "http://x" }, undefined, http)).resolves.toEqual({});
263
+ });
264
+ });
265
+
266
+ // ── Bearer header on the default client ───────────────────────────────────────
267
+
268
+ describe("defaultSpritesHttp bearer header", () => {
269
+ test("sends Authorization: Bearer <token> when a token is set", async () => {
270
+ const seen: Array<Record<string, string> | undefined> = [];
271
+ const fakeFetch = (async (_url: string, init: { headers?: Record<string, string> }) => {
272
+ seen.push(init.headers);
273
+ return { status: 200, text: async () => "{}" } as unknown as Response;
274
+ }) as unknown as typeof fetch;
275
+ const http = defaultSpritesHttp("secret-token", fakeFetch);
276
+ await http("GET", "http://x/v1/sprites/task-1");
277
+ expect(seen[0]?.authorization).toBe("Bearer secret-token");
278
+ });
279
+
280
+ test("no Authorization header when no token is set", async () => {
281
+ const seen: Array<Record<string, string> | undefined> = [];
282
+ const fakeFetch = (async (_url: string, init: { headers?: Record<string, string> }) => {
283
+ seen.push(init?.headers);
284
+ return { status: 200, text: async () => "{}" } as unknown as Response;
285
+ }) as unknown as typeof fetch;
286
+ const prev = process.env.SPRITES_API_TOKEN;
287
+ delete process.env.SPRITES_API_TOKEN;
288
+ try {
289
+ const http = defaultSpritesHttp(undefined, fakeFetch);
290
+ await http("GET", "http://x/v1/sprites/task-1");
291
+ expect(seen[0]?.authorization).toBeUndefined();
292
+ } finally {
293
+ if (prev !== undefined) process.env.SPRITES_API_TOKEN = prev;
294
+ }
295
+ });
296
+ });