@specific.dev/spectest 0.116.1 → 0.116.2

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.
@@ -127,18 +127,23 @@ export async function collectStickyCache(config, retention, run = exec, usage =
127
127
  }, kubernetes) {
128
128
  validateStickyConfig(config);
129
129
  const buildctl = ["--addr", "tcp://127.0.0.1:1234", "prune"];
130
- await run("buildctl", [...buildctl, "--keep-duration", "168h"], 120_000);
131
- // Docker's until filter and Created field refer to the publisher's build
132
- // date, not our use. Never prune a just-used image merely because it is old.
130
+ // Docker 29's containerd until filter uses local image-record creation;
131
+ // inspect's Created uses the publisher's build date. Neither tracks use.
132
+ // Never prune a just-used image merely because either timestamp is old.
133
133
  const protectedImage = (id) => retention.protectedImages.has(id) || kubernetes?.protectedImages.has(id);
134
134
  const images = [...retention.images].sort((a, b) => a.lastUsed - b.lastUsed || a.id.localeCompare(b.id));
135
135
  const expired = new Set();
136
136
  for (const image of images) {
137
- if (protectedImage(image.id) || image.lastUsed >= retention.observedAt - 7 * 24 * 60 * 60 * 1000)
137
+ if (protectedImage(image.id) || image.lastUsed >= retention.observedAt - 6 * 60 * 60 * 1000)
138
138
  continue;
139
139
  await run("docker", ["image", "rm", "-f", image.id], 120_000);
140
140
  expired.add(image.id);
141
141
  }
142
+ // Drop expired image references first so they no longer pin obsolete build
143
+ // layers. Cache mounts need a longer window: a cached RUN skips mounting
144
+ // its package cache, even though a later dependency change will need it.
145
+ await run("buildctl", [...buildctl, "--keep-duration", "6h", "--filter", "type!=exec.cachemount"], 120_000);
146
+ await run("buildctl", [...buildctl, "--keep-duration", "168h", "--filter", "type==exec.cachemount"], 120_000);
142
147
  const withinTarget = (fs) => fs.freeBytes >= config.headroomBytes;
143
148
  let fs = await usage();
144
149
  if (withinTarget(fs))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.116.1",
3
+ "version": "0.116.2",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -88,18 +88,32 @@ test("healthy cache keeps recently used images and a cache that cannot shrink is
88
88
  .rejects.toThrow("cannot reclaim required write headroom");
89
89
  });
90
90
 
91
- test("seven-day expiry uses our last observation and respects image protection", async () => {
91
+ test("six-hour image expiry uses last observation and respects the boundary and image protection", async () => {
92
92
  const removed: string[] = [];
93
93
  const run = async (command: string, args: string[]) => {
94
94
  if (command === "docker" && args[1] === "rm") removed.push(args.at(-1)!);
95
95
  return "";
96
96
  };
97
97
  await collectStickyCache(config, retention([
98
- { id: "unused-for-eight-days", lastUsed: now - 8 * 86400_000 },
98
+ { id: "unused-for-seven-hours", lastUsed: now - 7 * 3600_000 },
99
+ { id: "exactly-six-hours", lastUsed: now - 6 * 3600_000 },
100
+ { id: "used-five-hours-ago", lastUsed: now - 5 * 3600_000 },
99
101
  { id: "used-today", lastUsed: now },
100
102
  { id: "protected", lastUsed: 0 },
101
103
  ], ["protected"]), run, async () => ({ totalBytes: 256 * GiB, freeBytes: 220 * GiB }));
102
- expect(removed).toEqual(["unused-for-eight-days"]);
104
+ expect(removed).toEqual(["unused-for-seven-hours"]);
105
+ });
106
+
107
+ test("publication removes expired images before pruning build records and gives cache mounts seven days", async () => {
108
+ const calls: string[][] = [];
109
+ const run = async (command: string, args: string[]) => { calls.push([command, ...args]); return ""; };
110
+ await collectStickyCache(config, retention([{ id: "superseded", lastUsed: now - 7 * 3600_000 }]),
111
+ run, async () => ({ totalBytes: 251 * GiB, freeBytes: 170 * GiB }));
112
+ expect(calls).toEqual([
113
+ ["docker", "image", "rm", "-f", "superseded"],
114
+ ["buildctl", "--addr", "tcp://127.0.0.1:1234", "prune", "--keep-duration", "6h", "--filter", "type!=exec.cachemount"],
115
+ ["buildctl", "--addr", "tcp://127.0.0.1:1234", "prune", "--keep-duration", "168h", "--filter", "type==exec.cachemount"],
116
+ ]);
103
117
  });
104
118
 
105
119
  test("pressure cannot evict the images used by the environment being published", async () => {
@@ -137,17 +137,22 @@ export async function collectStickyCache(
137
137
  ): Promise<void> {
138
138
  validateStickyConfig(config);
139
139
  const buildctl = ["--addr", "tcp://127.0.0.1:1234", "prune"];
140
- await run("buildctl", [...buildctl, "--keep-duration", "168h"], 120_000);
141
- // Docker's until filter and Created field refer to the publisher's build
142
- // date, not our use. Never prune a just-used image merely because it is old.
140
+ // Docker 29's containerd until filter uses local image-record creation;
141
+ // inspect's Created uses the publisher's build date. Neither tracks use.
142
+ // Never prune a just-used image merely because either timestamp is old.
143
143
  const protectedImage = (id: string) => retention.protectedImages.has(id) || kubernetes?.protectedImages.has(id);
144
144
  const images = [...retention.images].sort((a, b) => a.lastUsed - b.lastUsed || a.id.localeCompare(b.id));
145
145
  const expired = new Set<string>();
146
146
  for (const image of images) {
147
- if (protectedImage(image.id) || image.lastUsed >= retention.observedAt - 7 * 24 * 60 * 60 * 1000) continue;
147
+ if (protectedImage(image.id) || image.lastUsed >= retention.observedAt - 6 * 60 * 60 * 1000) continue;
148
148
  await run("docker", ["image", "rm", "-f", image.id], 120_000);
149
149
  expired.add(image.id);
150
150
  }
151
+ // Drop expired image references first so they no longer pin obsolete build
152
+ // layers. Cache mounts need a longer window: a cached RUN skips mounting
153
+ // its package cache, even though a later dependency change will need it.
154
+ await run("buildctl", [...buildctl, "--keep-duration", "6h", "--filter", "type!=exec.cachemount"], 120_000);
155
+ await run("buildctl", [...buildctl, "--keep-duration", "168h", "--filter", "type==exec.cachemount"], 120_000);
151
156
  const withinTarget = (fs: { freeBytes: number }) => fs.freeBytes >= config.headroomBytes;
152
157
  let fs = await usage();
153
158
  if (withinTarget(fs)) return;