@intentius/chant-lexicon-e2b 0.0.0-stage → 0.124.32
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/README.md +147 -2
- package/dist/catalog.d.ts +19 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +58 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.d.ts +15 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/docs.js +123 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-cli.js +4 -0
- package/dist/codegen/generate.d.ts +11 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/generate.js +20 -0
- package/dist/codegen/package.d.ts +10 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/codegen/package.js +34 -0
- package/dist/config.d.ts +113 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +82 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/integrity.json +15 -0
- package/dist/lint/audit-catalog.d.ts +11 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/audit-catalog.js +15 -0
- package/dist/lint/post-synth/e2b011-template-name.d.ts +12 -0
- package/dist/lint/post-synth/e2b011-template-name.d.ts.map +1 -0
- package/dist/lint/post-synth/e2b011-template-name.js +48 -0
- package/dist/lint/post-synth/e2b012-steward-tools.d.ts +20 -0
- package/dist/lint/post-synth/e2b012-steward-tools.d.ts.map +1 -0
- package/dist/lint/post-synth/e2b012-steward-tools.js +53 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/index.js +6 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/pinned-image.d.ts +14 -0
- package/dist/lint/rules/pinned-image.d.ts.map +1 -0
- package/dist/lint/rules/pinned-image.js +36 -0
- package/dist/lint/rules/template-literal.d.ts +11 -0
- package/dist/lint/rules/template-literal.d.ts.map +1 -0
- package/dist/lint/rules/template-literal.js +39 -0
- package/dist/lint/rules/template-size.d.ts +10 -0
- package/dist/lint/rules/template-size.d.ts.map +1 -0
- package/dist/lint/rules/template-size.js +31 -0
- package/dist/lsp/completions.d.ts +7 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/completions.js +10 -0
- package/dist/lsp/hover.d.ts +7 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/lsp/hover.js +30 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +45 -0
- package/dist/okf/index.md +14 -0
- package/dist/okf/rules/E2B001.md +15 -0
- package/dist/okf/rules/E2B002.md +15 -0
- package/dist/okf/rules/E2B011.md +15 -0
- package/dist/okf/rules/E2B012.md +15 -0
- package/dist/okf/types/Template.md +16 -0
- package/dist/op/runtime.d.ts +119 -0
- package/dist/op/runtime.d.ts.map +1 -0
- package/dist/op/runtime.js +522 -0
- package/dist/op/sandbox.d.ts +67 -0
- package/dist/op/sandbox.d.ts.map +1 -0
- package/dist/op/sandbox.js +87 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/package-cli.js +14 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +66 -0
- package/dist/resources.d.ts +38 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +21 -0
- package/dist/rules/e2b011-template-name.ts +51 -0
- package/dist/rules/e2b012-steward-tools.ts +58 -0
- package/dist/rules/pinned-image.ts +41 -0
- package/dist/rules/template-literal.ts +42 -0
- package/dist/rules/template-size.ts +36 -0
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +70 -0
- package/dist/skills/chant-e2b-ops.md +69 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +67 -4
- package/src/catalog.ts +72 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +130 -0
- package/src/codegen/generate-cli.ts +5 -0
- package/src/codegen/generate.ts +22 -0
- package/src/codegen/package.ts +39 -0
- package/src/config.ts +102 -0
- package/src/index.ts +6 -0
- package/src/lint/audit-catalog.ts +29 -0
- package/src/lint/post-synth/e2b011-template-name.ts +51 -0
- package/src/lint/post-synth/e2b012-steward-tools.ts +58 -0
- package/src/lint/post-synth/index.ts +9 -0
- package/src/lint/post-synth/post-synth.test.ts +59 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/pinned-image.ts +41 -0
- package/src/lint/rules/rules.test.ts +46 -0
- package/src/lint/rules/template-literal.ts +42 -0
- package/src/lint/rules/template-size.ts +36 -0
- package/src/lsp/completions.test.ts +23 -0
- package/src/lsp/completions.ts +13 -0
- package/src/lsp/hover.test.ts +29 -0
- package/src/lsp/hover.ts +35 -0
- package/src/op/ledger-sync.test.ts +136 -0
- package/src/op/runtime.test.ts +394 -0
- package/src/op/runtime.ts +573 -0
- package/src/op/sandbox.ts +138 -0
- package/src/package-cli.ts +16 -0
- package/src/plugin.test.ts +39 -0
- package/src/plugin.ts +79 -0
- package/src/resources.ts +50 -0
- package/src/serializer.test.ts +88 -0
- package/src/serializer.ts +73 -0
- package/src/skills/chant-e2b-ops.md +69 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The e2b opRuntime provider.
|
|
3
|
+
*
|
|
4
|
+
* Every test drives the real provider through its two seams: a fake
|
|
5
|
+
* `StewardSandboxes` that records what it was asked and answers each script by
|
|
6
|
+
* what kind it is (prepare, run, finish, ledger read, stop), and a fake `git`
|
|
7
|
+
* for the local checkout. No network, no sandbox, no repository.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import type { ChantConfig } from "@intentius/chant/config";
|
|
12
|
+
import type { OpConfig, StepRecord } from "@intentius/chant/op";
|
|
13
|
+
import {
|
|
14
|
+
createE2bOpRuntime,
|
|
15
|
+
httpsRemote,
|
|
16
|
+
ledgerScript,
|
|
17
|
+
parseLedger,
|
|
18
|
+
prepareScript,
|
|
19
|
+
repoName,
|
|
20
|
+
runScript,
|
|
21
|
+
shellQuote,
|
|
22
|
+
stopScript,
|
|
23
|
+
} from "./runtime";
|
|
24
|
+
import {
|
|
25
|
+
RUN_ID_ENV,
|
|
26
|
+
RUN_OP_ENV,
|
|
27
|
+
RUN_STARTED_ENV,
|
|
28
|
+
type CommandResult,
|
|
29
|
+
type RunCommandOptions,
|
|
30
|
+
type SandboxProcess,
|
|
31
|
+
type StewardSandbox,
|
|
32
|
+
type StewardSandboxes,
|
|
33
|
+
} from "./sandbox";
|
|
34
|
+
|
|
35
|
+
// ── fixtures ──────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const OP = { name: "deploy", overview: "Deploy", phases: [] } as unknown as OpConfig;
|
|
38
|
+
|
|
39
|
+
function record(over: Record<string, unknown> = {}) {
|
|
40
|
+
return {
|
|
41
|
+
version: 1,
|
|
42
|
+
id: "local-1",
|
|
43
|
+
op: "deploy",
|
|
44
|
+
env: "local",
|
|
45
|
+
started: "2026-10-15T10:00:00.000Z",
|
|
46
|
+
ended: "2026-10-15T10:01:00.000Z",
|
|
47
|
+
status: "ok",
|
|
48
|
+
labels: {},
|
|
49
|
+
outcomes: {},
|
|
50
|
+
phases: [],
|
|
51
|
+
...over,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const STEP: StepRecord = { phase: "Apply", fn: "shellCmd", status: "ok", durationMs: 5 };
|
|
56
|
+
|
|
57
|
+
type ScriptKind = "prepare" | "run" | "finish" | "ledger" | "stop" | "other";
|
|
58
|
+
|
|
59
|
+
function kindOf(script: string): ScriptKind {
|
|
60
|
+
if (script.includes("git clone")) return "prepare";
|
|
61
|
+
if (script.includes("exec node_modules/.bin/chant run")) return "run";
|
|
62
|
+
if (script.includes("git ls-tree")) return "ledger";
|
|
63
|
+
if (script.startsWith("kill -TERM")) return "stop";
|
|
64
|
+
if (script.includes("ledger_sync")) return "finish";
|
|
65
|
+
return "other";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface Call {
|
|
69
|
+
kind: ScriptKind;
|
|
70
|
+
script: string;
|
|
71
|
+
envs: Record<string, string>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface FakeOptions {
|
|
75
|
+
/** The steward's sandbox already exists, in this state. */
|
|
76
|
+
existing?: string;
|
|
77
|
+
processes?: SandboxProcess[];
|
|
78
|
+
answers?: Partial<Record<ScriptKind, Partial<CommandResult> & { chunks?: string[] }>>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fakeSandboxes(opts: FakeOptions = {}) {
|
|
82
|
+
const calls: Call[] = [];
|
|
83
|
+
const events: string[] = [];
|
|
84
|
+
const sandbox: StewardSandbox = {
|
|
85
|
+
id: "sb-1",
|
|
86
|
+
async run(script: string, runOpts: RunCommandOptions = {}) {
|
|
87
|
+
const kind = kindOf(script);
|
|
88
|
+
calls.push({ kind, script, envs: runOpts.envs ?? {} });
|
|
89
|
+
runOpts.onStart?.(42);
|
|
90
|
+
const answer = opts.answers?.[kind] ?? {};
|
|
91
|
+
for (const chunk of answer.chunks ?? []) runOpts.onStdout?.(chunk);
|
|
92
|
+
if (answer.stderr) runOpts.onStderr?.(answer.stderr);
|
|
93
|
+
return { exitCode: answer.exitCode ?? 0, stdout: answer.stdout ?? "", stderr: answer.stderr ?? "" };
|
|
94
|
+
},
|
|
95
|
+
async processes() {
|
|
96
|
+
return opts.processes ?? [];
|
|
97
|
+
},
|
|
98
|
+
async kill(pid) {
|
|
99
|
+
events.push(`kill ${pid}`);
|
|
100
|
+
},
|
|
101
|
+
async keepAwake(ms) {
|
|
102
|
+
events.push(`awake ${ms}`);
|
|
103
|
+
},
|
|
104
|
+
async pause() {
|
|
105
|
+
events.push("pause");
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const sandboxes: StewardSandboxes = {
|
|
109
|
+
async find(steward) {
|
|
110
|
+
events.push(`find ${steward}`);
|
|
111
|
+
return opts.existing ? { id: "sb-1", state: opts.existing } : undefined;
|
|
112
|
+
},
|
|
113
|
+
async create(steward, createOpts) {
|
|
114
|
+
events.push(`create ${steward} ${createOpts.template}`);
|
|
115
|
+
return sandbox;
|
|
116
|
+
},
|
|
117
|
+
async connect(id) {
|
|
118
|
+
events.push(`connect ${id}`);
|
|
119
|
+
return sandbox;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
return { sandboxes, calls, events };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fakeGit(over: Record<string, string> = {}) {
|
|
126
|
+
const answers: Record<string, string> = {
|
|
127
|
+
"remote get-url origin": "git@github.com:acme/infra.git",
|
|
128
|
+
"rev-parse HEAD": "0123456789abcdef0123456789abcdef01234567",
|
|
129
|
+
"status --porcelain --untracked-files=no": "",
|
|
130
|
+
...over,
|
|
131
|
+
};
|
|
132
|
+
return async (args: string[]) => {
|
|
133
|
+
const key = args.join(" ");
|
|
134
|
+
if (!(key in answers)) throw new Error(`unexpected git ${key}`);
|
|
135
|
+
return answers[key];
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runtime(fake: ReturnType<typeof fakeSandboxes>, extra: { config?: ChantConfig; env?: NodeJS.ProcessEnv; git?: Record<string, string> } = {}) {
|
|
140
|
+
const stderr: string[] = [];
|
|
141
|
+
const provider = createE2bOpRuntime({
|
|
142
|
+
config: extra.config ?? ({ lexicons: ["e2b"] } as ChantConfig),
|
|
143
|
+
sandboxes: fake.sandboxes,
|
|
144
|
+
git: fakeGit(extra.git),
|
|
145
|
+
env: extra.env ?? {},
|
|
146
|
+
stderr: (chunk) => void stderr.push(chunk),
|
|
147
|
+
now: () => new Date("2026-10-15T10:00:00.000Z"),
|
|
148
|
+
});
|
|
149
|
+
return { provider, stderr };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── start ─────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("start", () => {
|
|
155
|
+
it("makes the steward, runs the op in its clone, streams the steps and pauses it", async () => {
|
|
156
|
+
const fake = fakeSandboxes({
|
|
157
|
+
answers: { run: { chunks: [`${JSON.stringify(STEP)}\n${JSON.stringify(record()).slice(0, 20)}`, `${JSON.stringify(record()).slice(20)}\n`] } },
|
|
158
|
+
});
|
|
159
|
+
const { provider } = runtime(fake);
|
|
160
|
+
const steps: StepRecord[] = [];
|
|
161
|
+
const handle = await provider.start(OP, { env: "prod", params: { region: "us east" }, progress: (s) => void steps.push(s) });
|
|
162
|
+
const status = await handle.result();
|
|
163
|
+
|
|
164
|
+
expect(status).toMatchObject({ op: "deploy", runId: "local-1", state: "completed" });
|
|
165
|
+
expect(steps).toEqual([STEP]);
|
|
166
|
+
expect(fake.events).toEqual(["find infra", "create infra base", "pause"]);
|
|
167
|
+
expect(fake.calls.map((c) => c.kind)).toEqual(["prepare", "run", "finish"]);
|
|
168
|
+
|
|
169
|
+
const [prepare, run] = fake.calls;
|
|
170
|
+
expect(prepare.script).toContain("git clone --quiet --no-checkout https://github.com/acme/infra.git");
|
|
171
|
+
expect(prepare.script).toContain("git checkout --quiet --force --detach 0123456789abcdef0123456789abcdef01234567");
|
|
172
|
+
expect(run.script).toContain("chant run deploy --env prod --param 'region=us east' --on local --json --progress-json");
|
|
173
|
+
expect(run.envs[RUN_OP_ENV]).toBe("deploy");
|
|
174
|
+
expect(run.envs[RUN_ID_ENV]).toBe(handle.runId);
|
|
175
|
+
expect(run.envs[RUN_STARTED_ENV]).toBe("2026-10-15T10:00:00.000Z");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("wakes an existing steward rather than making one", async () => {
|
|
179
|
+
const fake = fakeSandboxes({ existing: "paused", answers: { run: { chunks: [`${JSON.stringify(record())}\n`] } } });
|
|
180
|
+
const { provider } = runtime(fake);
|
|
181
|
+
await (await provider.start(OP, {})).result();
|
|
182
|
+
expect(fake.events.slice(0, 2)).toEqual(["find infra", "connect sb-1"]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("refuses a steward that is already running an op", async () => {
|
|
186
|
+
const fake = fakeSandboxes({ existing: "running", processes: [{ pid: 7, envs: { [RUN_OP_ENV]: "migrate", [RUN_ID_ENV]: "r-9" } }] });
|
|
187
|
+
const { provider } = runtime(fake);
|
|
188
|
+
await expect(provider.start(OP, {})).rejects.toThrow(/steward "infra" \(sb-1\) is running op "migrate" \(run r-9\)/);
|
|
189
|
+
expect(fake.calls).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("reports a gated run with its gate", async () => {
|
|
193
|
+
const gate = { name: "approve", since: "2026-10-15T10:00:30.000Z" };
|
|
194
|
+
const fake = fakeSandboxes({ answers: { run: { exitCode: 3, chunks: [`${JSON.stringify(record({ status: "gated", gate }))}\n`] } } });
|
|
195
|
+
const { provider } = runtime(fake);
|
|
196
|
+
const status = await (await provider.start(OP, {})).result();
|
|
197
|
+
expect(status).toMatchObject({ state: "gated", gate });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("fails without running the op when the clone cannot be prepared, and still pauses", async () => {
|
|
201
|
+
const fake = fakeSandboxes({ answers: { prepare: { exitCode: 2, stderr: "e2b runtime: commit 0123 is not on https://x; push it first" } } });
|
|
202
|
+
const { provider } = runtime(fake);
|
|
203
|
+
const status = await (await provider.start(OP, {})).result();
|
|
204
|
+
expect(status.state).toBe("failed");
|
|
205
|
+
expect(status.error).toContain("push it first");
|
|
206
|
+
expect(fake.calls.map((c) => c.kind)).toEqual(["prepare"]);
|
|
207
|
+
expect(fake.events.at(-1)).toBe("pause");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("reports a run killed before it wrote a record as cancelled", async () => {
|
|
211
|
+
const fake = fakeSandboxes({ answers: { run: { exitCode: -1 } } });
|
|
212
|
+
const { provider } = runtime(fake);
|
|
213
|
+
expect((await (await provider.start(OP, {})).result()).state).toBe("cancelled");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("reports a run that exited without a record as failed, with its output", async () => {
|
|
217
|
+
const fake = fakeSandboxes({ answers: { run: { exitCode: 1, stderr: "Op \"deploy\" not found" } } });
|
|
218
|
+
const { provider } = runtime(fake);
|
|
219
|
+
const status = await (await provider.start(OP, {})).result();
|
|
220
|
+
expect(status.state).toBe("failed");
|
|
221
|
+
expect(status.error).toMatch(/chant exited 1 in steward "infra" without a run record:\nOp "deploy" not found/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("stops the run in the sandbox when the caller aborts", async () => {
|
|
225
|
+
const fake = fakeSandboxes({ answers: { run: { exitCode: -1 } } });
|
|
226
|
+
const { provider } = runtime(fake);
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
controller.abort();
|
|
229
|
+
const status = await (await provider.start(OP, { signal: controller.signal })).result();
|
|
230
|
+
expect(status.state).toBe("cancelled");
|
|
231
|
+
expect(fake.calls.map((c) => c.kind)).toEqual(["prepare"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("hands the git token to the sandbox's commands only", async () => {
|
|
235
|
+
const fake = fakeSandboxes({ answers: { run: { chunks: [`${JSON.stringify(record())}\n`] } } });
|
|
236
|
+
const { provider } = runtime(fake, { env: { CHANT_GIT_TOKEN: "s3cret" } });
|
|
237
|
+
await (await provider.start(OP, {})).result();
|
|
238
|
+
for (const call of fake.calls) {
|
|
239
|
+
expect(call.envs.CHANT_GIT_TOKEN).toBe("s3cret");
|
|
240
|
+
expect(call.script).not.toContain("s3cret");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("warns that uncommitted changes don't travel", async () => {
|
|
245
|
+
const fake = fakeSandboxes({ answers: { run: { chunks: [`${JSON.stringify(record())}\n`] } } });
|
|
246
|
+
const { provider, stderr } = runtime(fake, { git: { "status --porcelain --untracked-files=no": " M src/app.ts" } });
|
|
247
|
+
await (await provider.start(OP, {})).result();
|
|
248
|
+
expect(stderr.join("")).toContain("uncommitted changes; the steward runs commit 0123456789ab without them");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── profiles ──────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
describe("profiles", () => {
|
|
255
|
+
const config = {
|
|
256
|
+
lexicons: ["e2b"],
|
|
257
|
+
e2b: {
|
|
258
|
+
profiles: {
|
|
259
|
+
prod: { steward: "prod-steward", template: "chant", repo: "https://git.example.com/acme/infra.git", gitToken: { env: "PROD_GIT" } },
|
|
260
|
+
},
|
|
261
|
+
defaultProfile: "prod",
|
|
262
|
+
},
|
|
263
|
+
} as unknown as ChantConfig;
|
|
264
|
+
|
|
265
|
+
it("takes the steward, template and repository from the default profile", async () => {
|
|
266
|
+
const fake = fakeSandboxes({ answers: { run: { chunks: [`${JSON.stringify(record())}\n`] } } });
|
|
267
|
+
const { provider } = runtime(fake, { config, env: { PROD_GIT: "t" } });
|
|
268
|
+
await (await provider.start(OP, {})).result();
|
|
269
|
+
expect(fake.events.slice(0, 2)).toEqual(["find prod-steward", "create prod-steward chant"]);
|
|
270
|
+
expect(fake.calls[0].script).toContain("git clone --quiet --no-checkout https://git.example.com/acme/infra.git");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("refuses a profile name that is not declared", async () => {
|
|
274
|
+
const { provider } = runtime(fakeSandboxes(), { config });
|
|
275
|
+
await expect(provider.start(OP, { profile: "staging" })).rejects.toThrow(/no profile "staging" under e2b.profiles/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("refuses a declared git token variable that is not set", async () => {
|
|
279
|
+
const { provider } = runtime(fakeSandboxes(), { config });
|
|
280
|
+
await expect(provider.start(OP, {})).rejects.toThrow(/git token variable PROD_GIT is not set/);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("asks for an API key when no seam is injected", async () => {
|
|
284
|
+
const provider = createE2bOpRuntime({ config: { lexicons: ["e2b"] } as ChantConfig, git: fakeGit(), env: {} });
|
|
285
|
+
await expect(provider.start(OP, {})).rejects.toThrow(/E2B_API_KEY is not set/);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ── status, log, list, cancel ─────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
describe("reads", () => {
|
|
292
|
+
const ledger = [record({ id: "a", started: "2026-10-15T09:00:00.000Z" }), record({ id: "b", started: "2026-10-15T11:00:00.000Z", status: "fail" })]
|
|
293
|
+
.map((r) => JSON.stringify(r))
|
|
294
|
+
.join("\n");
|
|
295
|
+
|
|
296
|
+
it("has nothing to say when the steward has never been made", async () => {
|
|
297
|
+
const fake = fakeSandboxes();
|
|
298
|
+
const { provider } = runtime(fake);
|
|
299
|
+
expect(await provider.status("deploy")).toBeUndefined();
|
|
300
|
+
expect(await provider.log("deploy")).toEqual([]);
|
|
301
|
+
expect(fake.events).not.toContain("create infra base");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("reports the run in progress from the sandbox's processes", async () => {
|
|
305
|
+
const fake = fakeSandboxes({
|
|
306
|
+
existing: "running",
|
|
307
|
+
processes: [{ pid: 7, envs: { [RUN_OP_ENV]: "deploy", [RUN_ID_ENV]: "r-1", [RUN_STARTED_ENV]: "2026-10-15T10:00:00.000Z" } }],
|
|
308
|
+
});
|
|
309
|
+
const { provider } = runtime(fake);
|
|
310
|
+
expect(await provider.status("deploy")).toEqual({ op: "deploy", runId: "r-1", state: "running", startedAt: "2026-10-15T10:00:00.000Z" });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("reads the latest settled run from the steward's ledger", async () => {
|
|
314
|
+
const fake = fakeSandboxes({ existing: "paused", answers: { ledger: { stdout: ledger } } });
|
|
315
|
+
const { provider } = runtime(fake);
|
|
316
|
+
expect(await provider.status("deploy")).toMatchObject({ runId: "b", state: "failed" });
|
|
317
|
+
expect((await provider.log("deploy", { limit: 1 })).map((r) => r.id)).toEqual(["b"]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("lists the latest state per op, the live run over its record", async () => {
|
|
321
|
+
const fake = fakeSandboxes({
|
|
322
|
+
existing: "running",
|
|
323
|
+
processes: [{ pid: 7, envs: { [RUN_OP_ENV]: "migrate", [RUN_ID_ENV]: "m-2" } }],
|
|
324
|
+
answers: { ledger: { stdout: `${ledger}\n${JSON.stringify(record({ id: "m-1", op: "migrate" }))}` } },
|
|
325
|
+
});
|
|
326
|
+
const { provider } = runtime(fake);
|
|
327
|
+
const states = await provider.list([OP, { ...OP, name: "migrate" }, { ...OP, name: "never" }] as OpConfig[]);
|
|
328
|
+
expect(states.get("deploy")).toMatchObject({ runId: "b" });
|
|
329
|
+
expect(states.get("migrate")).toMatchObject({ runId: "m-2", state: "running" });
|
|
330
|
+
expect(states.get("never")).toBeUndefined();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("cancels only the op that is running", async () => {
|
|
334
|
+
const fake = fakeSandboxes({ existing: "running", processes: [{ pid: 7, envs: { [RUN_OP_ENV]: "deploy" } }] });
|
|
335
|
+
const { provider } = runtime(fake);
|
|
336
|
+
await expect(provider.cancel("migrate", { force: true })).rejects.toThrow(/is not running op "migrate"/);
|
|
337
|
+
await provider.cancel("deploy", { force: true });
|
|
338
|
+
expect(fake.calls.at(-1)).toMatchObject({ kind: "stop", script: expect.stringContaining("kill -TERM 7") });
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("kills a run that ignores SIGTERM", async () => {
|
|
342
|
+
const fake = fakeSandboxes({ existing: "running", processes: [{ pid: 7, envs: { [RUN_OP_ENV]: "deploy" } }], answers: { stop: { exitCode: 1 } } });
|
|
343
|
+
const { provider } = runtime(fake);
|
|
344
|
+
await provider.cancel("deploy", { force: true });
|
|
345
|
+
expect(fake.events).toContain("kill 7");
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ── pure helpers ──────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
describe("helpers", () => {
|
|
352
|
+
it("rewrites an ssh remote to https", () => {
|
|
353
|
+
expect(httpsRemote("git@github.com:acme/infra.git")).toBe("https://github.com/acme/infra.git");
|
|
354
|
+
expect(httpsRemote("ssh://git@git.example.com:2222/acme/infra")).toBe("https://git.example.com/acme/infra");
|
|
355
|
+
expect(httpsRemote("https://github.com/acme/infra.git")).toBe("https://github.com/acme/infra.git");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("names a steward after its repository", () => {
|
|
359
|
+
expect(repoName("https://github.com/acme/infra.git")).toBe("infra");
|
|
360
|
+
expect(repoName("git@github.com:acme/infra")).toBe("infra");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("quotes only what needs it", () => {
|
|
364
|
+
expect(shellQuote("region=us-east-1")).toBe("region=us-east-1");
|
|
365
|
+
expect(shellQuote("it's")).toBe(`'it'\\''s'`);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("spells the run's command line", () => {
|
|
369
|
+
expect(runScript("deploy", { work: { item: "w-1", holder: "ci" } })).toBe(
|
|
370
|
+
`cd "$HOME/chant-steward" && exec node_modules/.bin/chant run deploy --work w-1 --holder ci --on local --json --progress-json`,
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("refuses to fold a stray value into the stop script", () => {
|
|
375
|
+
expect(stopScript(12.7)).toMatch(/^kill -TERM 12 /);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("reads ledger lines newest first, skipping what isn't a record", () => {
|
|
379
|
+
const text = [JSON.stringify(record({ id: "old", started: "2026-01-01T00:00:00Z" })), "not json", "{}", JSON.stringify(record({ id: "new" }))].join("\n");
|
|
380
|
+
expect(parseLedger(text).map((r) => r.id)).toEqual(["new", "old"]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("reads only the asked ops' ledgers", () => {
|
|
384
|
+
expect(ledgerScript(["deploy", "migrate"])).toContain("for p in runs__deploy.jsonl runs__migrate.jsonl;");
|
|
385
|
+
expect(ledgerScript([])).toContain("for p in runs__*.jsonl;");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("prepares from the commit, keeping ignored files", () => {
|
|
389
|
+
const script = prepareScript("https://github.com/acme/infra.git", "abc");
|
|
390
|
+
expect(script).toContain("git clean -fdq");
|
|
391
|
+
expect(script).not.toContain("clean -fdx");
|
|
392
|
+
expect(script.indexOf("ledger_sync\n")).toBeLessThan(script.indexOf("git checkout"));
|
|
393
|
+
});
|
|
394
|
+
});
|