thunderjar 0.1.3-preview-eee7160 → 0.1.3-preview-e09ffa1

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 (81) hide show
  1. package/dist/cli.js +1934 -1721
  2. package/package.json +14 -8
  3. package/src/api/checkoutCommit.ts +35 -0
  4. package/src/api/claudeCode.ts +74 -0
  5. package/src/api/declarations.ts +176 -133
  6. package/src/api/index.ts +44 -0
  7. package/src/api/parameterNames.ts +19 -0
  8. package/src/AGENTS.md +0 -156
  9. package/src/adapters/docker/docker.ts +0 -117
  10. package/src/api/fragments.ts +0 -30
  11. package/src/api/parameter-names.d.ts +0 -16
  12. package/src/cli/commands/build/build.docker.test.ts +0 -173
  13. package/src/cli/commands/build/build.test.ts +0 -234
  14. package/src/cli/commands/build/build.ts +0 -115
  15. package/src/cli/commands/declarations.typecheck.test.ts +0 -173
  16. package/src/cli/commands/executions/executions.test.ts +0 -84
  17. package/src/cli/commands/executions/executions.ts +0 -31
  18. package/src/cli/commands/format.ts +0 -33
  19. package/src/cli/commands/generate/generate.test.ts +0 -101
  20. package/src/cli/commands/generate/generate.ts +0 -18
  21. package/src/cli/commands/generate/parameter-names.ts +0 -131
  22. package/src/cli/commands/init/init.test.ts +0 -123
  23. package/src/cli/commands/init/init.ts +0 -189
  24. package/src/cli/commands/options.ts +0 -17
  25. package/src/cli/commands/plan/plan.test.ts +0 -168
  26. package/src/cli/commands/plan/plan.ts +0 -72
  27. package/src/cli/commands/remeasure/remeasure.test.ts +0 -536
  28. package/src/cli/commands/remeasure/remeasure.ts +0 -194
  29. package/src/cli/commands/run/run.test.ts +0 -622
  30. package/src/cli/commands/run/run.ts +0 -203
  31. package/src/cli/commands/show/show.test.ts +0 -288
  32. package/src/cli/commands/show/show.ts +0 -252
  33. package/src/cli/commands/validation.test.ts +0 -200
  34. package/src/cli/index.test.ts +0 -47
  35. package/src/cli/index.ts +0 -9
  36. package/src/cli/program.ts +0 -29
  37. package/src/images/prerun-builds.ts +0 -108
  38. package/src/images/prerun.test.ts +0 -243
  39. package/src/images/prerun.ts +0 -272
  40. package/src/images/repository.ts +0 -72
  41. package/src/index.ts +0 -24
  42. package/src/instruments/instruments.test.ts +0 -240
  43. package/src/instruments/instruments.ts +0 -205
  44. package/src/matrix/execution.ts +0 -122
  45. package/src/matrix/hashing.test.ts +0 -298
  46. package/src/matrix/hashing.ts +0 -131
  47. package/src/matrix/permutations.test.ts +0 -58
  48. package/src/matrix/permutations.ts +0 -50
  49. package/src/matrix/resolve.ts +0 -63
  50. package/src/matrix/tags.test.ts +0 -15
  51. package/src/matrix/tags.ts +0 -22
  52. package/src/matrix/ulid.test.ts +0 -28
  53. package/src/matrix/ulid.ts +0 -22
  54. package/src/measuring/aggregate.test.ts +0 -188
  55. package/src/measuring/aggregate.ts +0 -154
  56. package/src/measuring/measure.test.ts +0 -105
  57. package/src/measuring/measure.ts +0 -62
  58. package/src/measuring/remeasure-templates/v1/isPrime.test.template.txt +0 -4
  59. package/src/measuring/remeasure-templates/v2/isPrime.test.template.txt +0 -5
  60. package/src/measuring/remeasure.test.ts +0 -352
  61. package/src/measuring/remeasure.ts +0 -113
  62. package/src/platform/fs.test.ts +0 -89
  63. package/src/platform/fs.ts +0 -123
  64. package/src/platform/path.test.ts +0 -38
  65. package/src/platform/path.ts +0 -72
  66. package/src/platform/signals.test.ts +0 -25
  67. package/src/platform/signals.ts +0 -68
  68. package/src/project/config.test.ts +0 -68
  69. package/src/project/config.ts +0 -297
  70. package/src/running/runner.test.ts +0 -73
  71. package/src/running/runner.ts +0 -252
  72. package/src/store/run-data-store.test.ts +0 -232
  73. package/src/store/run-data-store.ts +0 -254
  74. package/src/testing/cli.ts +0 -65
  75. package/src/testing/fake-docker.test.ts +0 -197
  76. package/src/testing/fake-docker.ts +0 -104
  77. package/src/testing/fixture-project.ts +0 -177
  78. package/src/testing/measurement-context.ts +0 -5
  79. package/src/testing/seed-repo.test.ts +0 -10
  80. package/src/testing/seed-repo.ts +0 -83
  81. package/src/testing/test-setup.ts +0 -10
@@ -0,0 +1,19 @@
1
+ // The names a project declares under `.thunderjar/test-case-parameters/`. `thunderjar generate`
2
+ // writes them to `.thunderjar/_generated/parameterNames.d.ts` as global types, e.g.
3
+ // `type TaskName = "is-prime"`, and fills in `ThunderjarParameterNames` with them. Until it
4
+ // has run, every name is any string.
5
+
6
+ declare global {
7
+ interface ThunderjarParameterNames {}
8
+ }
9
+
10
+ type Generated<K extends string, Fallback> = ThunderjarParameterNames extends { [P in K]: infer T } ? T : Fallback;
11
+
12
+ export type BaseImageNames = Generated<"baseImage", string>;
13
+ export type CodeStateNames = Generated<"codeState", string>;
14
+ export type PromptSetNames = Generated<"promptSet", string>;
15
+ export type TaskNames = Generated<"task", string>;
16
+ /** The initial prompts of each task, by task name. */
17
+ export type InitialPromptNamesByTask = Generated<"initialPrompt", Record<string, string>>;
18
+ /** `<harness>@<version>/<model>`. */
19
+ export type HarnessModelNames = Generated<"harnessModel", string>;
package/src/AGENTS.md DELETED
@@ -1,156 +0,0 @@
1
- # src/
2
-
3
- Thunderjar's source, in layers. Each layer imports only from the layers below it, and the lint enforces it: `scripts/layout.ts` lists the layers in order, and `bun run lint` fails an import that points upward or a file in a folder the layout doesn't know.
4
-
5
- ## Layers
6
-
7
- ```mermaid
8
- flowchart TD
9
- cli --> commands["cli/commands"]
10
- commands --> images & running & measuring & store
11
- images --> matrix
12
- running --> matrix
13
- measuring --> running & store & matrix
14
- store --> matrix
15
- matrix --> project & instruments
16
- project --> instruments
17
- instruments --> api
18
- ```
19
-
20
- The diagram shows the imports that exist today. What the lint allows is any layer lower in this order, lowest first:
21
-
22
- ```
23
- api → instruments → project → matrix → store → running → images → measuring → cli/commands → cli
24
- ```
25
-
26
- Every layer may also import `platform`, `adapters/docker` and `testing`.
27
-
28
- ## Folders
29
-
30
- ```
31
- src/
32
- index.ts the `thunderjar` package: re-exports api/
33
- api/
34
- instruments/
35
- project/
36
- matrix/
37
- store/
38
- running/
39
- images/
40
- measuring/
41
- cli/
42
- index.ts
43
- program.ts
44
- commands/
45
- format.ts
46
- <command>/
47
- platform/
48
- adapters/docker/
49
- testing/
50
- ```
51
-
52
- - `index.ts`
53
- - The package entry users import `thunderjar` from. It only re-exports `api/`.
54
- - `api/`
55
- - What users' configuration files import.
56
- - `declarations.ts`: the `declareX()` functions. At runtime each returns its argument; the types give `config` and `ctx` their shape.
57
- - `fragments.ts`: ready-made Dockerfile fragments for `applyParameter`, such as `checkoutCommit`.
58
- - `parameter-names.d.ts`: fallback types for parameter names. A real project sees its `_generated/` file instead.
59
- - `instruments/`
60
- - The built-in measuring instruments (`fileCreated`, `grep`, `templateTest`), and the aggregate methods that fit each one's values.
61
- - `project/`
62
- - `config.ts`: finds `.thunderjar/`, and loads and checks a test case and its measurements against the files on disk. Configuration errors come from here.
63
- - `matrix/`
64
- - Working out what to run. Nothing here runs anything.
65
- - `permutations.ts`: a test case's cross product, in a stable order.
66
- - `hashing.ts`: parameter and permutation hashes, from content, never names.
67
- - `resolve.ts`: every permutation of a test case, with its hash.
68
- - `execution.ts`: one execution laid out, with each container run's tags and harness command.
69
- - `tags.ts`: prerun and postrun image tags. `ulid.ts`: execution IDs.
70
- - `store/`
71
- - `run-data-store.ts`: where executions, container runs and aggregation results are kept. It's an interface; for now the only store is a JSON file.
72
- - `running/`
73
- - `runner.ts`: runs one container run in a container from its prerun image, committing it into its postrun image, and opens the measurement context that instruments read a run through. A host runner stands in for Docker in tests of `run`.
74
- - `images/`
75
- - `prerun.ts`: renders a permutation's prerun Dockerfile from its parameters' fragments, checks the commits it reads, and writes its build context.
76
- - `prerun-builds.ts`: builds a test case's missing prerun images, for both `build` and `run`, after checking everything the builds need.
77
- - `repository.ts`: read-only access to the user's git repository.
78
- - `measuring/`
79
- - `measure.ts`: applies a task's measurements to one container run.
80
- - `aggregate.ts`: combines a permutation's iterations into its aggregation result.
81
- - `remeasure.ts`: for a backfill, whether a stored measure is missing, stale or current, which measurements each container run needs, and each execution's measurement statuses and deleted names.
82
- - `remeasure-templates/`: two versions of a bundled template, static test data for `remeasure.test.ts`.
83
- - `cli/commands/`
84
- - One folder per command, holding the command and its tests, e.g.
85
- ```
86
- cli/commands/build/
87
- build.ts
88
- build.test.ts
89
- build.docker.test.ts
90
- ```
91
- - `<command>.ts` exports the command's function, e.g. `build()`, and `createBuildCommand()`, which declares its arguments and options as a Commander `Command` and calls that function.
92
- - A command's tests drive the CLI against a checked-in `fixtureTests/` scenario, unchanged, and assert only on what the command produced. Every expected result lives in the test. See "Fixture tests" in the root `AGENTS.md`.
93
- - `format.ts`: how commands print what they share, such as counts and durations.
94
- - Tests that cut across commands sit directly in `cli/commands/`: configuration errors (`validation.test.ts`) and typed parameter names (`declarations.typecheck.test.ts`).
95
- - `cli/`
96
- - `program.ts`: `createProgram()`, the Commander program, which adds every command's `createXCommand()`.
97
- - `index.ts`: the `thunderjar` entry point. It parses the arguments with `createProgram()`.
98
- - `platform/`
99
- - Stands in for `node:path`, `node:fs` and `node:os`, which this repository doesn't use (see "Use Bun" in the root `AGENTS.md`). It imports nothing else from `src/`.
100
- - `path.ts`: POSIX path functions, such as `join`, `resolve` and `relative`.
101
- - `fs.ts`: file helpers on Bun's APIs and shell, such as `makeTempDir`, `remove` and `listEntries`. To read or write a file, use `Bun.file` and `Bun.write` directly.
102
- - `adapters/docker/`
103
- - The real Docker, behind the seam tests swap out. It imports nothing else from `src/` but `platform/`.
104
- - `testing/`
105
- - Test helpers, shared by tests in every layer.
106
- - `cli.ts`: runs the CLI in a `fixtureTests/` scenario, the way `bun run fixture` does. `scenario()` names a scenario's project; `copyOfFixtures()` copies it for `init` only, the one command that writes into the project; a real `run` passes `--data-dir` instead.
107
- - `fake-docker.ts`: the recording fake Docker, so a test can say which images exist, and which postrun images' saved files `docker create` and `docker cp` read. It never runs a container.
108
- - `measurement-context.ts`: the host measurement context, for instrument tests.
109
- - `test-setup.ts`, which every `bun test` preloads.
110
- - NOT ENDORSED: `fixture-project.ts` and `seed-repo.ts` generate throwaway projects. Older tests still use them while they move to scenarios; new tests don't.
111
-
112
- ## Rules
113
-
114
- ### Never import upward
115
-
116
- ✅ **Do** import only from a layer below, or from `platform`, `adapters/docker` or `testing`.
117
-
118
- ❌ **Don't** import from a layer above, even for one function. Move the function down instead.
119
-
120
- **Why:** the layers are what keep each part testable on its own, and readable without the parts above it. One upward import makes two layers depend on each other.
121
-
122
- e.g. `config.ts` checks a measurement's aggregate methods. It used to import them from `measuring/aggregate.ts`, above it, so the list moved down into `instruments/`, which both use.
123
-
124
- ### Put a new module in the lowest layer that can hold it
125
-
126
- ✅ **Do** pick the layer by what the module needs to import: the lowest layer whose imports suffice.
127
-
128
- ❌ **Don't** put it in `cli/commands/` because a command is its first user.
129
-
130
- **Why:** a module placed too high can't be used by the layers that will later need it, and the next person moves it.
131
-
132
- ### Keep tests beside their code
133
-
134
- ✅ **Do** put `<module>.test.ts` in the same folder as `<module>.ts`, and real-Docker tests in `<module>.docker.test.ts`.
135
-
136
- ❌ **Don't** gather tests in a separate tree.
137
-
138
- **Why:** a module and its tests change together, and the folder shows at a glance what is tested.
139
-
140
- A test of a lower layer that needs something from a higher one gets it through `testing/`, e.g. `instruments.test.ts` gets the host measurement context from `testing/measurement-context.ts`. That keeps `testing/` the one place where tests reach across layers.
141
-
142
- ### Share between commands through format.ts
143
-
144
- ✅ **Do** put anything two commands print alike in `cli/commands/format.ts`, or lower if it isn't about printing.
145
-
146
- ❌ **Don't** import one command from another in a command's own code.
147
-
148
- **Why:** commands are siblings. If one imports another, changing how `run` prints breaks `show`.
149
-
150
- The lint can't tell a command's code from its tests, so it allows command folders to import each other. A test shouldn't need to: one that needs, say, a recorded execution to `show` starts from a scenario with a checked-in run data store instead of running `run`.
151
-
152
- ### Adding a layer or a command
153
-
154
- - A command: add a folder under `cli/commands/` whose `<command>.ts` exports `createXCommand()`, add its name to `COMMANDS` in `scripts/layout.ts`, and add the command to `createProgram()` in `cli/program.ts`.
155
- - A layer: add it to `LAYERS` in `scripts/layout.ts`, in order, then update the diagram and this file.
156
- - Then run `bun run lint`, which regenerates the rules in `.oxlintrc.json`.
@@ -1,117 +0,0 @@
1
- // The Docker seam — spec/120-test-boundaries.md#principles. Everything Thunderjar asks of
2
- // Docker goes through a `Docker`; the real one runs the `docker` CLI. Tests swap the binary,
3
- // not the code path: `THUNDERJAR_DOCKER` names the executable to run in place of `docker`, such
4
- // as fixtureTests/fake-docker.ts, which records the commands it is given
5
- // (spec/120-test-boundaries.md#2-cli-with-recording-adapters).
6
- //
7
- // Thunderjar builds prerun images, runs a container run in a container from one and commits it
8
- // into a postrun image, and copies files out of a postrun image to measure it.
9
-
10
- export interface Docker {
11
- /** Why the Docker daemon can't be reached, or undefined when it can. */
12
- unreachable(): Promise<string | undefined>;
13
- imageExists(ref: string): Promise<boolean>;
14
- /**
15
- * Builds the Dockerfile in `contextDir` and tags the image `ref`. Docker's own output goes to
16
- * stderr. Throws if the build fails.
17
- */
18
- build(contextDir: string, ref: string): Promise<void>;
19
- /**
20
- * Runs `script` with `sh -c` in a new container named `name` from the image `ref`, and
21
- * leaves the container once it stops. Returns its stdout; its stderr goes to stderr.
22
- */
23
- run(ref: string, name: string, script: string): Promise<{ exitCode: number; stdout: string }>;
24
- /**
25
- * Commits the stopped container `name` into the image `ref`, with the entrypoint and command
26
- * of the image `like` (the one it ran from), so starting it doesn't run `run`'s script again.
27
- */
28
- commit(name: string, ref: string, like: string): Promise<void>;
29
- /** Creates, without starting it, a container named `name` from the image `ref`. */
30
- create(ref: string, name: string): Promise<void>;
31
- /** Copies `path` out of the container `name` to `dest` on the host, as `docker cp` does. */
32
- copyFrom(name: string, path: string, dest: string): Promise<void>;
33
- /** Removes the container `name`, stopped or not. */
34
- removeContainer(name: string): Promise<void>;
35
- }
36
-
37
- export class DockerBuildError extends Error {
38
- constructor(
39
- readonly ref: string,
40
- readonly exitCode: number,
41
- ) {
42
- super(`docker build of ${ref} exited ${exitCode}`);
43
- }
44
- }
45
-
46
- /** A docker command that exited non-zero, with what it wrote to stderr. */
47
- export class DockerCommandError extends Error {
48
- constructor(
49
- readonly args: string[],
50
- readonly exitCode: number,
51
- readonly stderr: string,
52
- ) {
53
- super(`docker ${args[0]} exited ${exitCode}${stderr.trim() ? `: ${stderr.trim().split("\n").at(-1)}` : ""}`);
54
- }
55
- }
56
-
57
- /**
58
- * Runs `binary` with `args`. "collect" discards stdout and collects stderr; "stderr" passes all
59
- * of its output to our stderr, keeping stdout for Thunderjar's own; "stdout" collects stdout and
60
- * passes stderr to ours.
61
- */
62
- async function docker(binary: string, args: string[], output: "collect" | "stderr" | "stdout") {
63
- const stdio: Record<typeof output, { stdout: "ignore" | "pipe" | number; stderr: "pipe" | number }> = {
64
- collect: { stdout: "ignore", stderr: "pipe" },
65
- stderr: { stdout: 2, stderr: 2 },
66
- stdout: { stdout: "pipe", stderr: 2 },
67
- };
68
- const child = Bun.spawn([binary, ...args], { stdin: "ignore", ...stdio[output] });
69
- const text = (stream: unknown) => (stream instanceof ReadableStream ? new Response(stream).text() : Promise.resolve(""));
70
- const [stdout, stderr] = await Promise.all([text(child.stdout), text(child.stderr), child.exited]);
71
- return { exitCode: child.exitCode ?? 1, stdout, stderr };
72
- }
73
-
74
- /** A Docker that runs the `docker` CLI, or `binary` in its place. */
75
- export function dockerCli(binary: string = process.env.THUNDERJAR_DOCKER || "docker"): Docker {
76
- /** Runs a command whose output isn't needed, throwing if it fails. */
77
- const must = async (args: string[]) => {
78
- const { exitCode, stderr } = await docker(binary, args, "collect");
79
- if (exitCode !== 0) throw new DockerCommandError(args, exitCode, stderr);
80
- };
81
- return {
82
- async unreachable() {
83
- try {
84
- const { exitCode, stderr } = await docker(binary, ["version", "--format", "{{.Server.Version}}"], "collect");
85
- if (exitCode === 0) return undefined;
86
- return stderr.trim().split("\n").at(-1) || `\`${binary} version\` exited ${exitCode}`;
87
- } catch (error) {
88
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return `\`${binary}\` was not found`;
89
- throw error;
90
- }
91
- },
92
- async imageExists(ref) {
93
- return (await docker(binary, ["image", "inspect", "--format", "{{.Id}}", ref], "collect")).exitCode === 0;
94
- },
95
- async build(contextDir, ref) {
96
- const { exitCode } = await docker(binary, ["build", "--tag", ref, contextDir], "stderr");
97
- if (exitCode !== 0) throw new DockerBuildError(ref, exitCode);
98
- },
99
- async run(ref, name, script) {
100
- // The image's own entrypoint isn't run: the script is all that runs.
101
- const { exitCode, stdout } = await docker(binary, ["run", "--name", name, "--entrypoint", "sh", ref, "-c", script], "stdout");
102
- return { exitCode, stdout };
103
- },
104
- async commit(name, ref, like) {
105
- const format = "{{json .Config.Entrypoint}}\t{{json .Config.Cmd}}";
106
- const inspected = await docker(binary, ["image", "inspect", "--format", format, like], "stdout");
107
- if (inspected.exitCode !== 0) throw new DockerCommandError(["image", "inspect", like], inspected.exitCode, "");
108
- const [entrypoint, cmd] = inspected.stdout.trim().split("\t");
109
- // `ENTRYPOINT []` is ignored; `[""]` clears it.
110
- const changes = [`ENTRYPOINT ${entrypoint === "null" ? '[""]' : entrypoint}`, `CMD ${cmd === "null" ? '["sh"]' : cmd}`];
111
- await must(["commit", ...changes.flatMap((change) => ["--change", change]), name, ref]);
112
- },
113
- create: (ref, name) => must(["create", "--name", name, "--entrypoint", "sh", ref]),
114
- copyFrom: (name, path, dest) => must(["cp", `${name}:${path}`, dest]),
115
- removeContainer: (name) => must(["rm", "--force", name]),
116
- };
117
- }
@@ -1,30 +0,0 @@
1
- // Dockerfile fragments a parameter's `applyParameter` can use, exported from `thunderjar`.
2
-
3
- import type { GitBuildContext } from "./declarations.ts";
4
-
5
- /**
6
- * Checks `commit` out into /workspace as a real repository — spec/062-code-state.md#how-the-code-gets-into-the-image:
7
- * cloned from a bundle of the commit and its ancestors only (so `git log --all` shows the agent
8
- * nothing after it), detached, with no remote, and a git identity so the agent can commit.
9
- *
10
- * ```ts
11
- * export default declareCodeState({
12
- * config: { commit: "a1b2c3" },
13
- * applyParameter: (config, ctx) => `${checkoutCommit(ctx, config.commit)}\nRUN npm ci`,
14
- * });
15
- * ```
16
- */
17
- export function checkoutCommit(ctx: { git: GitBuildContext }, commit: string): string {
18
- return [
19
- `COPY ${ctx.git.bundle(commit)} /tmp/code.bundle`,
20
- [
21
- "RUN git clone --quiet /tmp/code.bundle /workspace",
22
- `git -C /workspace checkout --quiet --detach ${commit}`,
23
- "git -C /workspace branch --quiet --delete --force code-state",
24
- "git -C /workspace remote remove origin",
25
- `git -C /workspace config user.name "Thunderjar agent"`,
26
- "git -C /workspace config user.email agent@thunderjar.invalid",
27
- "rm /tmp/code.bundle",
28
- ].join(" \\\n && "),
29
- ].join("\n");
30
- }
@@ -1,16 +0,0 @@
1
- // Fallback ambient types for the parameter names a test case selects.
2
- //
3
- // `thunderjar generate` writes a project-specific `_generated/parameter-names.d.ts`
4
- // declaring these same global names as literal unions (see
5
- // spec/055-declaring-test-cases.md#typed-names). A real project's own TypeScript
6
- // program sees only that generated file, never this one — this file exists solely so
7
- // that this package's own declarations (which reference these names directly, with no
8
- // import) type-check on their own, and so a program that hasn't run `generate` yet
9
- // still compiles, permissively.
10
-
11
- type BaseImageName = string;
12
- type CodeStateName = string;
13
- type PromptSetName = string;
14
- type TaskName = string;
15
- type InitialPromptNames = Record<string, string>;
16
- type HarnessModelName = string;
@@ -1,173 +0,0 @@
1
- // Docker image tests for `build` — spec/120-test-boundaries.md#31-generating-the-pre-run-image.
2
- // Docker and the images are real; the agent is the fake one. Skipped by `bun test`; run with
3
- // `bun run test:prepare && bun run test:docker`. Each run passes a throwaway `--image-prefix`
4
- // and removes every image under it afterwards.
5
- //
6
- // Each runs the CLI in place in a checked-in scenario, as by hand:
7
- //
8
- // bun run fixture scenario_4 build two-commits
9
-
10
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
- import { makeTempDir, readTextSync } from "../../../platform/fs.ts";
12
- import { dirname, join } from "../../../platform/path.ts";
13
- import { dockerCli } from "../../../adapters/docker/docker.ts";
14
- import { scenario, thunderjar } from "../../../testing/cli.ts";
15
-
16
- const BASE_IMAGE = `node:20-bookworm`;
17
- const docker = dockerCli("docker");
18
- const prefix = `thunderjar-test-${process.pid}-${Date.now()}`;
19
-
20
- /**
21
- * Runs `thunderjar <command> <testCase> --image-prefix <prefix>` in `project` against real Docker,
22
- * with build contexts under a fresh $TMPDIR, and durations replaced by a placeholder.
23
- */
24
- async function cli(project: string, command: string, testCase: string, env: Record<string, string> = {}) {
25
- const tmpDir = await makeTempDir("thunderjar-build-docker-");
26
- const result = await thunderjar(project, [command, testCase, "--image-prefix", prefix], {
27
- THUNDERJAR_DOCKER: "docker",
28
- TMPDIR: tmpDir,
29
- ...env,
30
- });
31
- const normalise = (text: string) =>
32
- text.replaceAll(tmpDir, "$TMPDIR").replaceAll(prefix, "<prefix>").replace(/ built \(\d+\.\d\ds\)$/gm, " built (<t>s)");
33
- return { ...result, stdout: normalise(result.stdout), stderr: normalise(result.stderr) };
34
- }
35
-
36
- /** Runs `script` with sh in a throwaway container from `ref`. */
37
- function inImage(ref: string, script: string) {
38
- const result = Bun.spawnSync(["docker", "run", "--rm", ref, "sh", "-c", script], { stdout: "pipe", stderr: "pipe" });
39
- expect(result.stderr.toString()).toBe("");
40
- return result.stdout.toString().trim();
41
- }
42
-
43
- const prefixed = (tag: string) => `${prefix}:${tag}`;
44
-
45
- describe.skipIf(!process.env.THUNDERJAR_TEST_DOCKER)("build, against real Docker", () => {
46
- beforeAll(async () => {
47
- const unreachable = await docker.unreachable();
48
- if (unreachable) throw new Error(`Docker can't be reached (${unreachable}).`);
49
- if (!(await docker.imageExists(BASE_IMAGE))) {
50
- throw new Error(`The fixture image ${BASE_IMAGE} is missing. Run \`bun run test:prepare\` first.`);
51
- }
52
- });
53
-
54
- afterAll(() => {
55
- const images = Bun.spawnSync(["docker", "image", "ls", "--format", "{{.Repository}}:{{.Tag}}", prefix], { stdout: "pipe" })
56
- .stdout.toString()
57
- .split("\n")
58
- .filter(Boolean);
59
- if (images.length > 0) Bun.spawnSync(["docker", "image", "rm", "--force", ...images]);
60
- });
61
-
62
- describe("scenario_1_minimal_initial_setup", () => {
63
- const fixtures = scenario("scenario_1_minimal_initial_setup");
64
- const REF = prefixed("prerun-h8ffdda6e");
65
-
66
- test("build example: /workspace holds the code state's and prompt set's files, and node; a second build reuses it", async () => {
67
- // Outside any git repository, too: its parameters read no commits.
68
- const built = await cli(fixtures, "build", "example", { GIT_CEILING_DIRECTORIES: dirname(fixtures) });
69
-
70
- expect(built.exitCode).toBe(0);
71
- expect(built.stdout).toBe(`Building 1 prerun image for example (1 permutation)
72
- Build contexts in $TMPDIR/thunderjar/builds
73
-
74
- 1 node20 | baseline | snerk | fake-harness-alpha@1.0.0/fake-model-small | plain
75
- <prefix>:prerun-h8ffdda6e built (<t>s)
76
-
77
- 1 prerun image: 1 built, 0 reused
78
- `);
79
- expect(inImage(REF, "pwd && ls -A")).toBe("/workspace\nFAKE_INSTRUCTIONS.md\npackage-lock.json\npackage.json");
80
- expect(inImage(REF, "head -1 FAKE_INSTRUCTIONS.md")).toBe("# Snerk");
81
- expect(inImage(REF, "node --version")).toStartWith("v20.");
82
-
83
- const again = await cli(fixtures, "build", "example");
84
- expect(again.stdout).toBe(`Every prerun image for example exists (1 permutation)
85
-
86
- 1 node20 | baseline | snerk | fake-harness-alpha@1.0.0/fake-model-small | plain
87
- <prefix>:prerun-h8ffdda6e reused
88
-
89
- 1 prerun image: 0 built, 1 reused
90
- `);
91
- }, 300_000);
92
- });
93
-
94
- describe("scenario_2_typical_project", () => {
95
- const fixtures = scenario("scenario_2_typical_project");
96
- const ALPHA = prefixed("prerun-hefe85dda");
97
-
98
- test("build two-pairs: builds each missing prerun image; with one removed, builds only that one, and plan shows both reused", async () => {
99
- const built = await cli(fixtures, "build", "two-pairs");
100
-
101
- expect(built.exitCode).toBe(0);
102
- // The tags are those `thunderjar plan two-pairs` shows.
103
- expect(built.stdout).toBe(`Building 2 prerun images for two-pairs (2 permutations)
104
- Build contexts in $TMPDIR/thunderjar/builds
105
-
106
- 1 node20 | baseline | snerk | fake-harness-alpha@1.0.0/fake-model-small | terse
107
- <prefix>:prerun-hefe85dda built (<t>s)
108
-
109
- 2 node20 | baseline | snerk | fake-harness-beta@1.0.0/fake-model-large | terse
110
- <prefix>:prerun-h56ea4852 built (<t>s)
111
-
112
- 2 prerun images: 2 built, 0 reused
113
- `);
114
-
115
- Bun.spawnSync(["docker", "image", "rm", "--force", ALPHA]);
116
- const again = await cli(fixtures, "build", "two-pairs");
117
- expect(again.stdout).toContain(" <prefix>:prerun-hefe85dda built (<t>s)\n");
118
- expect(again.stdout).toContain(" <prefix>:prerun-h56ea4852 reused\n");
119
- expect(again.stdout).toEndWith("2 prerun images: 1 built, 1 reused\n");
120
-
121
- const plan = await cli(fixtures, "plan", "two-pairs");
122
- expect(plan.stdout).toContain(" prerun-hefe85dda (exists, reused)\n");
123
- expect(plan.stdout).toContain(" prerun-h56ea4852 (exists, reused)\n");
124
- }, 300_000);
125
- });
126
-
127
- // Scenario 4's parameters read real commits of this repository, so its images show what only
128
- // the copied files can: a prompt file read at its commit, and a bundle with nothing after its commit.
129
- describe("scenario_4_git_project", () => {
130
- const fixtures = scenario("scenario_4_git_project");
131
- const FIRST = "523984c7fd72f047265ab89ce5901605af6acb2b";
132
- const SECOND = "952687db16b373d58f79f46690f9c5b0aa1aa141";
133
- const PROJECT = "fixtureTests/scenario_4_git_project/fixtures/project";
134
- const refs = new Map<string, string>();
135
-
136
- beforeAll(async () => {
137
- const { stdout, exitCode } = await cli(fixtures, "build", "two-commits");
138
- expect(exitCode).toBe(0);
139
- expect(stdout).toEndWith("4 prerun images: 4 built, 0 reused\n");
140
- // Each permutation's row, by code state and prompt set, and the image it built.
141
- for (const [, codeState, promptSet, tag] of stdout.matchAll(/\| (\w+) +\| ([\w-]+) +\|.*\n {4}<prefix>:(\S+) {2}built/g)) {
142
- refs.set(`${codeState}/${promptSet}`, prefixed(tag!));
143
- }
144
- expect(refs.size).toBe(4);
145
- }, 300_000);
146
-
147
- test("the first code state: this repository at the first commit, detached, with nothing after it", () => {
148
- const ref = refs.get("first/house-style-second")!;
149
- expect(inImage(ref, "pwd && git rev-parse HEAD && git rev-parse --abbrev-ref HEAD")).toBe(`/workspace\n${FIRST}\nHEAD`);
150
- expect(inImage(ref, "git remote && git for-each-ref refs/heads")).toBe("");
151
- expect(inImage(ref, `git log --all --format=%H | grep -c ${SECOND} || true`)).toBe("0");
152
- expect(inImage(ref, `ls ${PROJECT}`)).toBe("greet.js\npackage-lock.json\npackage.json\nprompts");
153
- // A git identity for the agent.
154
- expect(inImage(ref, "git config user.email")).toBe("agent@thunderjar.invalid");
155
- });
156
-
157
- test("the second code state: the second commit, with the file it added", () => {
158
- const ref = refs.get("second/house-style-first")!;
159
- expect(inImage(ref, "git rev-parse HEAD")).toBe(SECOND);
160
- expect(inImage(ref, `ls ${PROJECT}/farewell.js && cat ${PROJECT}/greet.js | tail -1`)).toBe(
161
- `${PROJECT}/farewell.js\nexport const greet = (name) => \`Hello, \${name}!\`;`,
162
- );
163
- });
164
-
165
- test("a prompt file is read at its prompt set's commit, never from the worktree", () => {
166
- // The prompt set's commit, whichever the code state's is.
167
- expect(inImage(refs.get("first/house-style-second")!, "head -1 HOUSE_STYLE.md")).toBe("# House style (second commit)");
168
- expect(inImage(refs.get("second/house-style-first")!, "head -1 HOUSE_STYLE.md")).toBe("# House style (first commit)");
169
- // The worktree's copy has moved on from both.
170
- expect(readTextSync(join(fixtures, "project", "prompts", "house-style.md"))).toStartWith("# House style (worktree");
171
- });
172
- });
173
- });