@intentius/chant-lexicon-fly 0.89.0 → 0.90.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/manifest.json +1 -1
- package/dist/op/activities/emulator-images.d.ts +13 -0
- package/dist/op/activities/emulator-images.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +4 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/sprite-services.d.ts +158 -0
- package/dist/op/activities/sprite-services.d.ts.map +1 -0
- package/dist/op/activities/sprites-contract.d.ts +17 -1
- package/dist/op/activities/sprites-contract.d.ts.map +1 -1
- package/dist/op/activities/sprites-emulator.d.ts +12 -0
- package/dist/op/activities/sprites-emulator.d.ts.map +1 -1
- package/dist/op/activities/sprites-fake.d.ts +4 -0
- package/dist/op/activities/sprites-fake.d.ts.map +1 -1
- package/dist/op/activities/sprites.d.ts +53 -0
- package/dist/op/activities/sprites.d.ts.map +1 -1
- package/dist/op/builders.d.ts +20 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/skills/chant-fly-sprites.md +4 -2
- package/package.json +2 -2
- package/src/index.ts +9 -0
- package/src/op/activities/emulator-images.ts +14 -0
- package/src/op/activities/index.ts +34 -0
- package/src/op/activities/sprite-config.test.ts +19 -0
- package/src/op/activities/sprite-services.docker.integration.test.ts +87 -0
- package/src/op/activities/sprite-services.test.ts +263 -0
- package/src/op/activities/sprite-services.ts +311 -0
- package/src/op/activities/sprites-contract.test.ts +15 -4
- package/src/op/activities/sprites-contract.ts +24 -6
- package/src/op/activities/sprites-emulator.ts +52 -1
- package/src/op/activities/sprites-fake.ts +57 -8
- package/src/op/activities/sprites.test.ts +102 -0
- package/src/op/activities/sprites.ts +108 -7
- package/src/op/builders.ts +28 -1
- package/src/skills/chant-fly-sprites.md +4 -2
|
@@ -61,7 +61,10 @@ export {
|
|
|
61
61
|
spriteRestore,
|
|
62
62
|
listCheckpoints,
|
|
63
63
|
spriteDestroy,
|
|
64
|
+
spriteDelete,
|
|
65
|
+
spriteUrl,
|
|
64
66
|
resolveSpritesEndpoint,
|
|
67
|
+
resolveSpritesToken,
|
|
65
68
|
defaultSpritesHttp,
|
|
66
69
|
spriteCreateBody,
|
|
67
70
|
parseCreateResponse,
|
|
@@ -84,8 +87,39 @@ export type {
|
|
|
84
87
|
ListCheckpointsArgs,
|
|
85
88
|
Checkpoint,
|
|
86
89
|
SpriteDestroyArgs,
|
|
90
|
+
SpriteDeleteArgs,
|
|
91
|
+
SpriteUrlArgs,
|
|
92
|
+
SpriteUrlResult,
|
|
87
93
|
} from "./sprites";
|
|
88
94
|
|
|
95
|
+
// Sprite Services activities (#2711) — imperative create/get/list/start/stop/
|
|
96
|
+
// delete/logs for one background service at a time, the single-service
|
|
97
|
+
// primitives underneath `spriteApplyServices`'s batch reconcile.
|
|
98
|
+
// `loadActivities(["fly"])` binds these; the step builders live in core.
|
|
99
|
+
export {
|
|
100
|
+
spriteServiceCreate,
|
|
101
|
+
spriteServiceGet,
|
|
102
|
+
spriteServiceList,
|
|
103
|
+
spriteServiceStart,
|
|
104
|
+
spriteServiceStop,
|
|
105
|
+
spriteServiceDelete,
|
|
106
|
+
spriteServiceLogs,
|
|
107
|
+
spriteServiceCreateBody,
|
|
108
|
+
parseServiceLogNdjson,
|
|
109
|
+
} from "./sprite-services";
|
|
110
|
+
export type {
|
|
111
|
+
SpriteService,
|
|
112
|
+
SpriteServiceState,
|
|
113
|
+
SpriteServiceCreateArgs,
|
|
114
|
+
SpriteServiceGetArgs,
|
|
115
|
+
SpriteServiceListArgs,
|
|
116
|
+
SpriteServiceStartArgs,
|
|
117
|
+
SpriteServiceStopArgs,
|
|
118
|
+
SpriteServiceDeleteArgs,
|
|
119
|
+
SpriteServiceLogsArgs,
|
|
120
|
+
SpriteServiceLogsResult,
|
|
121
|
+
} from "./sprite-services";
|
|
122
|
+
|
|
89
123
|
// Sprite filesystem activities (#848) — imperative file I/O over the fs API.
|
|
90
124
|
// `loadActivities(["fly"])` binds these; the step builders live in core.
|
|
91
125
|
export {
|
|
@@ -107,6 +107,25 @@ describe("spriteApplyNetworkPolicy reconcile", () => {
|
|
|
107
107
|
spriteApplyNetworkPolicy({ id: "np-1", rules: [{ domain: "", action: "allow" }], endpoint: fake.url }),
|
|
108
108
|
).rejects.toThrow(/missing domain/);
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
// #2719: the real Sprites API and every official SDK (Go, JS, Python,
|
|
112
|
+
// Elixir) answer the policy update with 204 and no body; a 200 with the
|
|
113
|
+
// rules echoed back fails an SDK-backed client. Pin the fake's raw wire
|
|
114
|
+
// reply directly (spriteApplyNetworkPolicy itself tolerates any <300, so it
|
|
115
|
+
// wouldn't catch a regression here). GET is unchanged: 200 with the rules.
|
|
116
|
+
test("POST /policy/network answers 204 with no body; GET stays 200 with the rules", async () => {
|
|
117
|
+
await createImpl({ name: "np-wire", endpoint: fake.url });
|
|
118
|
+
const url = `${fake.url}/v1/sprites/np-wire/policy/network`;
|
|
119
|
+
const rules = [{ domain: "github.com", action: "allow" }];
|
|
120
|
+
|
|
121
|
+
const post = await fetch(url, { method: "POST", body: JSON.stringify({ rules }) });
|
|
122
|
+
expect(post.status).toBe(204);
|
|
123
|
+
expect(await post.text()).toBe("");
|
|
124
|
+
|
|
125
|
+
const get = await fetch(url);
|
|
126
|
+
expect(get.status).toBe(200);
|
|
127
|
+
expect(await get.json()).toEqual({ rules });
|
|
128
|
+
});
|
|
110
129
|
});
|
|
111
130
|
|
|
112
131
|
describe("spriteApplyServices reconcile", () => {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spriteCreate, spriteDelete, spriteUrl } from "./sprites";
|
|
3
|
+
import { spriteServiceCreate, spriteServiceStop, spriteServiceStart } from "./sprite-services";
|
|
4
|
+
import { spritesContainerUp, spritesContainerDown } from "./sprites-emulator";
|
|
5
|
+
|
|
6
|
+
// Acceptance (#2711) against the REAL spritzer image in container exec mode
|
|
7
|
+
// (SPRITZER_EXEC=container, SPRITZER_RUNTIME=docker — INTENTIUS/spritzer#22):
|
|
8
|
+
// create a sprite, create a service running `python3 -m http.server 8080`,
|
|
9
|
+
// wait for its URL to answer, stop it (the URL stops answering), start it
|
|
10
|
+
// again (the URL answers again), delete the sprite. `sprite-services.test.ts`
|
|
11
|
+
// runs the same scenario against the in-process fake, no Docker. Docker
|
|
12
|
+
// required; deterministically skipped in CI (GitHub runners have Docker, so
|
|
13
|
+
// relying on absence would pull the image on every run) — run locally, or
|
|
14
|
+
// opt in with SPRITES_DOCKER=1, same convention as the other docker tests
|
|
15
|
+
// here.
|
|
16
|
+
//
|
|
17
|
+
// Cleanup: the sprite container spritzer makes (`spritzer-<name>`) is removed
|
|
18
|
+
// by `spriteDelete` before spritzer itself comes down; `docker ps -a` after
|
|
19
|
+
// this suite should show neither.
|
|
20
|
+
|
|
21
|
+
const CONTAINER = "chant-spritzer-container-it";
|
|
22
|
+
const PORT = 4294;
|
|
23
|
+
const SPRITE = "chant-2711-web";
|
|
24
|
+
|
|
25
|
+
let available = false;
|
|
26
|
+
let endpoint = "";
|
|
27
|
+
let prevBaseUrl: string | undefined;
|
|
28
|
+
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
if (process.env.CI && !process.env.SPRITES_DOCKER) {
|
|
31
|
+
available = false;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const up = await spritesContainerUp({ name: CONTAINER, port: PORT, timeoutMs: 60_000 });
|
|
36
|
+
endpoint = up.endpoint;
|
|
37
|
+
prevBaseUrl = process.env.SPRITES_BASE_URL;
|
|
38
|
+
process.env.SPRITES_BASE_URL = endpoint;
|
|
39
|
+
available = true;
|
|
40
|
+
} catch {
|
|
41
|
+
available = false;
|
|
42
|
+
}
|
|
43
|
+
}, 90_000);
|
|
44
|
+
|
|
45
|
+
afterAll(async () => {
|
|
46
|
+
if (prevBaseUrl === undefined) delete process.env.SPRITES_BASE_URL;
|
|
47
|
+
else process.env.SPRITES_BASE_URL = prevBaseUrl;
|
|
48
|
+
if (available) {
|
|
49
|
+
// Idempotent: already deleted by the test on the happy path. A failed
|
|
50
|
+
// test still leaves no sprite container behind.
|
|
51
|
+
await spriteDelete({ id: SPRITE }).catch(() => {});
|
|
52
|
+
await spritesContainerDown({ name: CONTAINER });
|
|
53
|
+
}
|
|
54
|
+
}, 30_000);
|
|
55
|
+
|
|
56
|
+
describe("sprite services against real spritzer 0.6.0, container mode (#2711)", () => {
|
|
57
|
+
test("create → url waits and answers → stop → url stops answering → start → answers again → delete", async (ctx) => {
|
|
58
|
+
if (!available) ctx.skip();
|
|
59
|
+
|
|
60
|
+
await spriteCreate({ name: SPRITE });
|
|
61
|
+
|
|
62
|
+
await spriteServiceCreate({
|
|
63
|
+
id: SPRITE,
|
|
64
|
+
name: "web",
|
|
65
|
+
cmd: "python3",
|
|
66
|
+
args: ["-m", "http.server", "8080"],
|
|
67
|
+
http_port: 8080,
|
|
68
|
+
durationMs: 1000,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { url } = await spriteUrl({ id: SPRITE, path: "/", timeoutMs: 20_000, intervalMs: 500 });
|
|
72
|
+
expect(url).toContain(SPRITE);
|
|
73
|
+
|
|
74
|
+
await spriteServiceStop({ id: SPRITE, name: "web" });
|
|
75
|
+
// The process is gone; nothing answers on the sprite URL any more.
|
|
76
|
+
const afterStop = await fetch(`${url}/`);
|
|
77
|
+
expect(afterStop.status).toBe(503);
|
|
78
|
+
|
|
79
|
+
await spriteServiceStart({ id: SPRITE, name: "web" });
|
|
80
|
+
const { url: url2 } = await spriteUrl({ id: SPRITE, path: "/", timeoutMs: 20_000, intervalMs: 500 });
|
|
81
|
+
expect(url2).toBe(url);
|
|
82
|
+
|
|
83
|
+
await spriteDelete({ id: SPRITE });
|
|
84
|
+
const afterDelete = await fetch(`${endpoint}/v1/sprites/${SPRITE}`);
|
|
85
|
+
expect(afterDelete.status).toBe(404);
|
|
86
|
+
}, 120_000);
|
|
87
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import type { SpritesHttp } from "./sprites";
|
|
3
|
+
import { spriteCreate as createImpl } from "./sprites";
|
|
4
|
+
import { createSpritesFake } from "./sprites-fake";
|
|
5
|
+
import {
|
|
6
|
+
spriteServiceCreate,
|
|
7
|
+
spriteServiceGet,
|
|
8
|
+
spriteServiceList,
|
|
9
|
+
spriteServiceStart,
|
|
10
|
+
spriteServiceStop,
|
|
11
|
+
spriteServiceDelete,
|
|
12
|
+
spriteServiceLogs,
|
|
13
|
+
spriteServiceCreateBody,
|
|
14
|
+
parseServiceLogNdjson,
|
|
15
|
+
} from "./sprite-services";
|
|
16
|
+
|
|
17
|
+
// A recording HTTP stub: captures every call and answers from a callback.
|
|
18
|
+
function recorder(answer: (method: string, url: string, body?: unknown) => { status: number; text: string }) {
|
|
19
|
+
const calls: Array<{ method: string; url: string; body?: unknown }> = [];
|
|
20
|
+
const http: SpritesHttp = async (method, url, body) => {
|
|
21
|
+
calls.push({ method, url, body });
|
|
22
|
+
return answer(method, url, body);
|
|
23
|
+
};
|
|
24
|
+
return { http, calls };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const running = (name: string, extra: Record<string, unknown> = {}) =>
|
|
28
|
+
JSON.stringify({ name, cmd: "run", state: { name, status: "running", pid: 4321 }, ...extra });
|
|
29
|
+
|
|
30
|
+
// ── Pure helpers ────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
describe("spriteServiceCreateBody", () => {
|
|
33
|
+
test("omits undefined optional fields", () => {
|
|
34
|
+
expect(spriteServiceCreateBody({ id: "s", name: "web", cmd: "run-web" })).toEqual({ cmd: "run-web" });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("includes every field that is set", () => {
|
|
38
|
+
expect(
|
|
39
|
+
spriteServiceCreateBody({
|
|
40
|
+
id: "s",
|
|
41
|
+
name: "web",
|
|
42
|
+
cmd: "python3",
|
|
43
|
+
args: ["-m", "http.server", "8080"],
|
|
44
|
+
env: { PORT: "8080" },
|
|
45
|
+
dir: "/app",
|
|
46
|
+
needs: ["db"],
|
|
47
|
+
http_port: 8080,
|
|
48
|
+
}),
|
|
49
|
+
).toEqual({
|
|
50
|
+
cmd: "python3",
|
|
51
|
+
args: ["-m", "http.server", "8080"],
|
|
52
|
+
env: { PORT: "8080" },
|
|
53
|
+
dir: "/app",
|
|
54
|
+
needs: ["db"],
|
|
55
|
+
http_port: 8080,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("parseServiceLogNdjson", () => {
|
|
61
|
+
test("keeps stdout/stderr data in order, drops the complete marker", () => {
|
|
62
|
+
const text = [
|
|
63
|
+
JSON.stringify({ type: "started", data: "web started (pid 1)" }),
|
|
64
|
+
JSON.stringify({ type: "stdout", data: "Serving on 0.0.0.0:8080" }),
|
|
65
|
+
JSON.stringify({ type: "complete", data: "ignored" }),
|
|
66
|
+
].join("\n");
|
|
67
|
+
expect(parseServiceLogNdjson(text)).toEqual(["web started (pid 1)", "Serving on 0.0.0.0:8080"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("skips blank and unparseable lines", () => {
|
|
71
|
+
expect(parseServiceLogNdjson('\n{"type":"stdout","data":"a"}\n\nnot json\n')).toEqual(["a"]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── Activity request shapes (injected SpritesHttp) ─────────────────────────
|
|
76
|
+
|
|
77
|
+
describe("spriteServiceCreate", () => {
|
|
78
|
+
test("PUTs .../services/{name} with ?duration, then GETs the definitive state", async () => {
|
|
79
|
+
const { http, calls } = recorder((method) => ({
|
|
80
|
+
status: method === "PUT" ? 200 : 200,
|
|
81
|
+
text: method === "PUT" ? '{"type":"complete"}' : running("web", { http_port: 8080 }),
|
|
82
|
+
}));
|
|
83
|
+
const res = await spriteServiceCreate(
|
|
84
|
+
{ id: "task-1", name: "web", cmd: "python3", args: ["-m", "http.server", "8080"], http_port: 8080, endpoint: "http://x" },
|
|
85
|
+
undefined,
|
|
86
|
+
http,
|
|
87
|
+
);
|
|
88
|
+
expect(res.state.status).toBe("running");
|
|
89
|
+
expect(calls[0].method).toBe("PUT");
|
|
90
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/task-1/services/web?duration=500ms");
|
|
91
|
+
expect(calls[0].body).toEqual({ cmd: "python3", args: ["-m", "http.server", "8080"], http_port: 8080 });
|
|
92
|
+
expect(calls[1].method).toBe("GET");
|
|
93
|
+
expect(calls[1].url).toBe("http://x/v1/sprites/task-1/services/web");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a custom durationMs is passed through as ms", async () => {
|
|
97
|
+
const { http, calls } = recorder(() => ({ status: 200, text: running("web") }));
|
|
98
|
+
await spriteServiceCreate({ id: "t", name: "web", cmd: "x", durationMs: 2000, endpoint: "http://x" }, undefined, http);
|
|
99
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web?duration=2000ms");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("throws when the resulting state is failed", async () => {
|
|
103
|
+
const { http } = recorder((method) => ({
|
|
104
|
+
status: 200,
|
|
105
|
+
text: method === "PUT"
|
|
106
|
+
? '{"type":"error","data":"boom"}'
|
|
107
|
+
: JSON.stringify({ name: "web", cmd: "x", state: { name: "web", status: "failed", error: "exec: not found" } }),
|
|
108
|
+
}));
|
|
109
|
+
await expect(
|
|
110
|
+
spriteServiceCreate({ id: "t", name: "web", cmd: "nope", endpoint: "http://x" }, undefined, http),
|
|
111
|
+
).rejects.toThrow(/failed to start.*exec: not found/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("throws on a non-2xx PUT", async () => {
|
|
115
|
+
const { http } = recorder(() => ({ status: 400, text: "bad request" }));
|
|
116
|
+
await expect(
|
|
117
|
+
spriteServiceCreate({ id: "t", name: "web", cmd: "x", endpoint: "http://x" }, undefined, http),
|
|
118
|
+
).rejects.toThrow(/create failed \(400\)/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("spriteServiceGet", () => {
|
|
123
|
+
test("GETs .../services/{name}", async () => {
|
|
124
|
+
const { http, calls } = recorder(() => ({ status: 200, text: running("web") }));
|
|
125
|
+
const res = await spriteServiceGet({ id: "t", name: "web", endpoint: "http://x" }, undefined, http);
|
|
126
|
+
expect(res.state.status).toBe("running");
|
|
127
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("spriteServiceList", () => {
|
|
132
|
+
test("GETs .../services and returns the array", async () => {
|
|
133
|
+
const list = [JSON.parse(running("web")), JSON.parse(running("db"))];
|
|
134
|
+
const { http, calls } = recorder(() => ({ status: 200, text: JSON.stringify(list) }));
|
|
135
|
+
const res = await spriteServiceList({ id: "t", endpoint: "http://x" }, undefined, http);
|
|
136
|
+
expect(res).toEqual(list);
|
|
137
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("spriteServiceStart / spriteServiceStop", () => {
|
|
142
|
+
test("start POSTs .../start?duration and re-GETs", async () => {
|
|
143
|
+
const { http, calls } = recorder((method) => ({
|
|
144
|
+
status: 200,
|
|
145
|
+
text: method === "POST" ? '{"type":"complete"}' : running("web"),
|
|
146
|
+
}));
|
|
147
|
+
const res = await spriteServiceStart({ id: "t", name: "web", endpoint: "http://x" }, undefined, http);
|
|
148
|
+
expect(res.state.status).toBe("running");
|
|
149
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web/start?duration=500ms");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("stop POSTs .../stop and re-GETs", async () => {
|
|
153
|
+
const { http, calls } = recorder((method) => ({
|
|
154
|
+
status: 200,
|
|
155
|
+
text: method === "POST"
|
|
156
|
+
? '{"type":"complete"}'
|
|
157
|
+
: JSON.stringify({ name: "web", cmd: "x", state: { name: "web", status: "stopped" } }),
|
|
158
|
+
}));
|
|
159
|
+
const res = await spriteServiceStop({ id: "t", name: "web", endpoint: "http://x" }, undefined, http);
|
|
160
|
+
expect(res.state.status).toBe("stopped");
|
|
161
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web/stop");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("stop passes ?timeout when timeoutMs is set", async () => {
|
|
165
|
+
const { http, calls } = recorder((method) => ({
|
|
166
|
+
status: 200,
|
|
167
|
+
text: method === "POST" ? "{}" : running("web"),
|
|
168
|
+
}));
|
|
169
|
+
await spriteServiceStop({ id: "t", name: "web", timeoutMs: 3000, endpoint: "http://x" }, undefined, http);
|
|
170
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web/stop?timeout=3000ms");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("spriteServiceDelete", () => {
|
|
175
|
+
test("DELETEs .../services/{name}", async () => {
|
|
176
|
+
const { http, calls } = recorder(() => ({ status: 200, text: "{}" }));
|
|
177
|
+
await spriteServiceDelete({ id: "t", name: "web", endpoint: "http://x" }, undefined, http);
|
|
178
|
+
expect(calls[0].method).toBe("DELETE");
|
|
179
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a 404 is idempotent, not an error", async () => {
|
|
183
|
+
const { http } = recorder(() => ({ status: 404, text: "gone" }));
|
|
184
|
+
await expect(spriteServiceDelete({ id: "t", name: "web", endpoint: "http://x" }, undefined, http)).resolves.toEqual({});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("spriteServiceLogs", () => {
|
|
189
|
+
test("GETs .../logs and returns the parsed tail", async () => {
|
|
190
|
+
const { http, calls } = recorder(() => ({
|
|
191
|
+
status: 200,
|
|
192
|
+
text: [JSON.stringify({ type: "stdout", data: "line 1" }), JSON.stringify({ type: "complete" })].join("\n"),
|
|
193
|
+
}));
|
|
194
|
+
const res = await spriteServiceLogs({ id: "t", name: "web", endpoint: "http://x" }, undefined, http);
|
|
195
|
+
expect(res).toEqual({ lines: ["line 1"] });
|
|
196
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web/logs");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("passes ?lines when set", async () => {
|
|
200
|
+
const { http, calls } = recorder(() => ({ status: 200, text: "" }));
|
|
201
|
+
await spriteServiceLogs({ id: "t", name: "web", lines: 5, endpoint: "http://x" }, undefined, http);
|
|
202
|
+
expect(calls[0].url).toBe("http://x/v1/sprites/t/services/web/logs?lines=5");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── End to end against the in-process fake (S7, no Docker) ────────────────
|
|
207
|
+
//
|
|
208
|
+
// The acceptance scenario from #2711, offline: create a sprite, create a
|
|
209
|
+
// service with http_port, list/get it, stop and start it, delete it. The
|
|
210
|
+
// docker-gated `sprite-services.docker.integration.test.ts` runs the same
|
|
211
|
+
// scenario against real spritzer 0.6.0 in container mode.
|
|
212
|
+
|
|
213
|
+
let fake: { url: string; close(): Promise<void> };
|
|
214
|
+
let prevBaseUrl: string | undefined;
|
|
215
|
+
|
|
216
|
+
beforeAll(async () => {
|
|
217
|
+
fake = await createSpritesFake();
|
|
218
|
+
prevBaseUrl = process.env.SPRITES_BASE_URL;
|
|
219
|
+
process.env.SPRITES_BASE_URL = fake.url;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
afterAll(async () => {
|
|
223
|
+
if (prevBaseUrl === undefined) delete process.env.SPRITES_BASE_URL;
|
|
224
|
+
else process.env.SPRITES_BASE_URL = prevBaseUrl;
|
|
225
|
+
await fake?.close();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("sprite services against the in-process fake (#2711)", () => {
|
|
229
|
+
test("create, get, list, stop, start, delete", async () => {
|
|
230
|
+
await createImpl({ name: "svc-fake-1" });
|
|
231
|
+
|
|
232
|
+
const created = await spriteServiceCreate({
|
|
233
|
+
id: "svc-fake-1",
|
|
234
|
+
name: "web",
|
|
235
|
+
cmd: "python3",
|
|
236
|
+
args: ["-m", "http.server", "8080"],
|
|
237
|
+
http_port: 8080,
|
|
238
|
+
});
|
|
239
|
+
expect(created.state.status).toBe("running");
|
|
240
|
+
|
|
241
|
+
const got = await spriteServiceGet({ id: "svc-fake-1", name: "web" });
|
|
242
|
+
expect(got.name).toBe("web");
|
|
243
|
+
expect(got.http_port).toBe(8080);
|
|
244
|
+
|
|
245
|
+
const list = await spriteServiceList({ id: "svc-fake-1" });
|
|
246
|
+
expect(list.map((s) => s.name)).toEqual(["web"]);
|
|
247
|
+
|
|
248
|
+
const stopped = await spriteServiceStop({ id: "svc-fake-1", name: "web" });
|
|
249
|
+
expect(stopped.state.status).toBe("stopped");
|
|
250
|
+
|
|
251
|
+
const restarted = await spriteServiceStart({ id: "svc-fake-1", name: "web" });
|
|
252
|
+
expect(restarted.state.status).toBe("running");
|
|
253
|
+
|
|
254
|
+
const logs = await spriteServiceLogs({ id: "svc-fake-1", name: "web" });
|
|
255
|
+
expect(logs.lines.length).toBeGreaterThan(0);
|
|
256
|
+
|
|
257
|
+
await spriteServiceDelete({ id: "svc-fake-1", name: "web" });
|
|
258
|
+
await expect(spriteServiceGet({ id: "svc-fake-1", name: "web" })).rejects.toThrow(/get failed \(404\)/);
|
|
259
|
+
|
|
260
|
+
// A second delete is idempotent.
|
|
261
|
+
await expect(spriteServiceDelete({ id: "svc-fake-1", name: "web" })).resolves.toEqual({});
|
|
262
|
+
});
|
|
263
|
+
});
|