@rigkit/engine 0.2.3 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigkit/engine",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/authoring.ts CHANGED
@@ -25,7 +25,7 @@ import type {
25
25
  WorkflowWorkspaceDefinition,
26
26
  } from "./types.ts";
27
27
 
28
- const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers"]);
28
+ const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers", "step"]);
29
29
  const reservedHostOperationIds = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
30
30
 
31
31
  const readEnv = (name: string, fallback?: string): string => {
@@ -51,9 +51,17 @@ export function workflow<const Name extends string, const Providers extends Work
51
51
  providers: options.providers,
52
52
  sequence: <InputContext extends JsonObject = {}>(sequenceName: string) =>
53
53
  createSequence(app as unknown as WorkflowDefinition<string, Providers>, sequenceName, []),
54
- task: (taskName: string, optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, any>, maybeHandler?: WorkflowTaskHandler<Providers, {}, any>) =>
54
+ task: (
55
+ taskName: string,
56
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
57
+ maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
58
+ ) =>
55
59
  createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
56
- step: (taskName: string, optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, any>, maybeHandler?: WorkflowTaskHandler<Providers, {}, any>) =>
60
+ step: (
61
+ taskName: string,
62
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
63
+ maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
64
+ ) =>
57
65
  createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
58
66
  };
59
67
 
@@ -127,6 +135,7 @@ function createSequence<
127
135
  WorkspaceData extends object = JsonObject,
128
136
  OperationIds extends string = never,
129
137
  WorkspaceOperationIds extends string = never,
138
+ PreviousTaskIds extends string = never,
130
139
  >(
131
140
  app: WorkflowDefinition<string, Providers>,
132
141
  name: string,
@@ -140,7 +149,8 @@ function createSequence<
140
149
  OutputContext,
141
150
  WorkspaceData,
142
151
  OperationIds,
143
- WorkspaceOperationIds
152
+ WorkspaceOperationIds,
153
+ PreviousTaskIds
144
154
  > {
145
155
  const node = {
146
156
  kind: "rigkit.workflow-node" as const,
@@ -153,16 +163,16 @@ function createSequence<
153
163
  workspaceOperations,
154
164
  task: (
155
165
  taskName: string,
156
- optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
157
- maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
166
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
167
+ maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
158
168
  ) => {
159
169
  const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
160
170
  return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
161
171
  },
162
172
  step: (
163
173
  taskName: string,
164
- optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
165
- maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
174
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
175
+ maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
166
176
  ) => {
167
177
  const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
168
178
  return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
@@ -209,7 +219,8 @@ function createSequence<
209
219
  OutputContext,
210
220
  WorkspaceData,
211
221
  OperationIds,
212
- WorkspaceOperationIds
222
+ WorkspaceOperationIds,
223
+ PreviousTaskIds
213
224
  >;
214
225
  }
215
226
 
@@ -319,15 +330,19 @@ function createOperationInputBuilder<Input extends object>(
319
330
  } as WorkflowOperationInputBuilder<Input>;
320
331
  }
321
332
 
322
- function createTask<Providers extends WorkflowProviderMap, InputContext extends JsonObject>(
333
+ function createTask<
334
+ Providers extends WorkflowProviderMap,
335
+ InputContext extends JsonObject,
336
+ PreviousTaskIds extends string = string,
337
+ >(
323
338
  app: WorkflowDefinition<string, Providers>,
324
339
  name: string,
325
- optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, any>,
326
- maybeHandler?: WorkflowTaskHandler<Providers, InputContext, any>,
340
+ optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
341
+ maybeHandler?: WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
327
342
  ): WorkflowTaskNode<Providers, InputContext, any> {
328
343
  const options = typeof optionsOrHandler === "function" ? undefined : optionsOrHandler;
329
344
  const handler = (typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler) as
330
- | WorkflowTaskHandler<Providers, InputContext, any>
345
+ | WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>
331
346
  | undefined;
332
347
  if (!handler) throw new Error(`Task ${name} is missing a handler`);
333
348
 
@@ -10,7 +10,7 @@ sequence("normal-operation-ids")
10
10
  });
11
11
 
12
12
  sequence("workspace-operation-ids")
13
- .step("prepare", async () => ({ snapshotId: "snap-1" }))
13
+ .step("prepare", async () => ({ ctx: { snapshotId: "snap-1" } }))
14
14
  .workspace({
15
15
  create: async ({ workflow, workspace }) => {
16
16
  const snapshotId: string = workflow.ctx.snapshotId;
@@ -44,6 +44,26 @@ sequence("workspace-operation-ids")
44
44
  run: async () => null,
45
45
  });
46
46
 
47
+ sequence("typed-step-invalidation")
48
+ .step("github-auth", async () => ({ ctx: { token: "ok" } }))
49
+ .step("check-auth", async ({ step }) => {
50
+ const token: string = step.ctx.token;
51
+ void token;
52
+ return step.invalidate("github-auth");
53
+ });
54
+
55
+ sequence("typed-step-invalidation-targets")
56
+ .step("github-auth", async () => ({ ctx: { token: "ok" } }))
57
+ .step("check-auth", async ({ step }) => {
58
+ // @ts-expect-error invalidation target must be a previous task id
59
+ return step.invalidate("missing-auth");
60
+ });
61
+
62
+ sequence("duplicate-step-id")
63
+ .step("prepare" as const, async () => ({ ctx: { snapshotId: "snap-1" } }))
64
+ // @ts-expect-error duplicate task ids are rejected for literal ids
65
+ .step("prepare" as const, async ({ step }) => ({ ctx: step.ctx }));
66
+
47
67
  sequence("reserved-operation-id")
48
68
  // @ts-expect-error reserved operation ids are rejected for literal ids
49
69
  .operation("create" as const, {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { Database } from "bun:sqlite";
3
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { createDevMachineEngine, type InteractionPresentationRequest } from "./engine.ts";
@@ -27,34 +27,34 @@ describe("DevMachineEngine workflow runtime", () => {
27
27
  },
28
28
  });
29
29
 
30
- const base = app.sequence("base").task("first", async ({ runtime, test }) => {
31
- runtime.log("preparing base\\n", { label: "setup" });
30
+ const base = app.sequence("base").task("first", async ({ step, test }) => {
31
+ step.log("preparing base\\n", { label: "setup" });
32
32
  const vm = await test.createVm();
33
33
  await vm.exec("touch /tmp/first", { name: "touch first" });
34
34
  if (!(await vm.exists("/tmp/first"))) throw new Error("first was not created");
35
- return { first: true, vm: await vm.snapshotRef() };
35
+ return { ctx: { first: true, vm: await vm.snapshotRef() } };
36
36
  });
37
37
 
38
- const left = app.sequence("left").task("second", async ({ ctx, test }) => {
39
- if (!ctx.first) throw new Error("missing first context");
40
- const vm = await test.fromSnapshot(ctx.vm);
38
+ const left = app.sequence("left").task("second", async ({ step, test }) => {
39
+ if (!step.ctx.first) throw new Error("missing first context");
40
+ const vm = await test.fromSnapshot(step.ctx.vm);
41
41
  await vm.exec("touch /tmp/second", { name: "touch second" });
42
- return { second: true, vm: await vm.snapshotRef() };
42
+ return { ctx: { second: true, vm: await vm.snapshotRef() } };
43
43
  });
44
44
 
45
- const right = app.sequence("right").task("data", async ({ ctx }) => {
46
- if (!ctx.first) throw new Error("missing first context");
47
- return { data: "right-ready" };
45
+ const right = app.sequence("right").task("data", async ({ step }) => {
46
+ if (!step.ctx.first) throw new Error("missing first context");
47
+ return { ctx: { data: "right-ready" } };
48
48
  });
49
49
 
50
50
  export default app
51
51
  .sequence("root")
52
52
  .add(base)
53
53
  .parallel({ left, right })
54
- .task("join", async ({ ctx }) => {
55
- if (!ctx.left.second) throw new Error("missing left context");
56
- if (ctx.right.data !== "right-ready") throw new Error("missing right context");
57
- return { vm: ctx.left.vm, summary: ctx.right.data };
54
+ .task("join", async ({ step }) => {
55
+ if (!step.ctx.left.second) throw new Error("missing left context");
56
+ if (step.ctx.right.data !== "right-ready") throw new Error("missing right context");
57
+ return { ctx: { vm: step.ctx.left.vm, summary: step.ctx.right.data } };
58
58
  })
59
59
  .workspace({
60
60
  create: async ({ providers, workflow, workspace, local }) => {
@@ -189,8 +189,10 @@ describe("DevMachineEngine workflow runtime", () => {
189
189
  const vm = await providers.test.createVm();
190
190
  await vm.exec("touch /tmp/template", { name: "prepare template" });
191
191
  return {
192
- vm: await vm.snapshotRef(),
193
- repoPath: "/workspace/repo",
192
+ ctx: {
193
+ vm: await vm.snapshotRef(),
194
+ repoPath: "/workspace/repo",
195
+ },
194
196
  };
195
197
  })
196
198
  .workspace({
@@ -272,6 +274,37 @@ describe("DevMachineEngine workflow runtime", () => {
272
274
  expect(status).toEqual({ workspace: "created", vmId: "vm-2" });
273
275
  });
274
276
 
277
+ test("rejects workspace names that are not shell-safe", async () => {
278
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
279
+ writeFileSync(
280
+ join(projectDir, "rig.config.ts"),
281
+ `
282
+ import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
283
+
284
+ const root = sequence("workspace-names")
285
+ .step("ready", async () => ({ ctx: { ready: true } }))
286
+ .workspace({
287
+ create: async ({ workspace }) => ({ name: workspace.name }),
288
+ remove: async () => {},
289
+ });
290
+
291
+ export default defineConfig({
292
+ providers: {},
293
+ workflows: { root },
294
+ });
295
+ `,
296
+ );
297
+
298
+ const engine = await createDevMachineEngine({ projectDir });
299
+ await engine.load();
300
+
301
+ await expect(engine.fork({ name: "" })).rejects.toThrow("create requires a workspace name");
302
+ for (const name of ["some workspace", "some/workspace", "-workspace"]) {
303
+ await expect(engine.fork({ name })).rejects.toThrow("Workspace name");
304
+ }
305
+ expect(engine.listWorkspaces()).toEqual([]);
306
+ });
307
+
275
308
  test("loads multiple workflows from defineConfig", async () => {
276
309
  const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
277
310
  writeFileSync(
@@ -279,8 +312,8 @@ describe("DevMachineEngine workflow runtime", () => {
279
312
  `
280
313
  import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
281
314
 
282
- const api = sequence("api").step("ready", async () => ({ api: true }));
283
- const web = sequence("web").step("ready", async () => ({ web: true }));
315
+ const api = sequence("api").step("ready", async () => ({ ctx: { api: true } }));
316
+ const web = sequence("web").step("ready", async () => ({ ctx: { web: true } }));
284
317
 
285
318
  export default defineConfig({
286
319
  providers: {},
@@ -310,7 +343,7 @@ describe("DevMachineEngine workflow runtime", () => {
310
343
  `
311
344
  import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
312
345
 
313
- const root = sequence("factory-test").step("ready", async () => ({ ready: true }));
346
+ const root = sequence("factory-test").step("ready", async () => ({ ctx: { ready: true } }));
314
347
 
315
348
  export default defineConfig({
316
349
  providers: {},
@@ -432,7 +465,7 @@ describe("DevMachineEngine workflow runtime", () => {
432
465
  const app = workflow("handler-cache", { providers: {} });
433
466
 
434
467
  export default app.sequence("root").task("value", async () => {
435
- return { value: "${value}" };
468
+ return { ctx: { value: "${value}" } };
436
469
  });
437
470
  `,
438
471
  );
@@ -490,7 +523,7 @@ describe("DevMachineEngine workflow runtime", () => {
490
523
  },
491
524
  });
492
525
 
493
- export default app.sequence("root").task("ready", async () => ({ ready: true }));
526
+ export default app.sequence("root").task("ready", async () => ({ ctx: { ready: true } }));
494
527
  `,
495
528
  );
496
529
 
@@ -533,6 +566,73 @@ describe("DevMachineEngine workflow runtime", () => {
533
566
  expect(metadata["config.path"]).toBe(join(projectDir, "rig.config.ts"));
534
567
  });
535
568
 
569
+ test("stores provider host JSON state outside project state", async () => {
570
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
571
+ const hostStorageDir = join(projectDir, ".host-storage");
572
+ const opened: string[] = [];
573
+ const plugin: BaseProviderPlugin = {
574
+ providerId: "test",
575
+ async createProvider({ storage, hostStorage, local }) {
576
+ storage.set("project", { value: "state" });
577
+ hostStorage.set("token", { value: "secret" });
578
+ await local.open("rigkit://provider-auth");
579
+ return new FakeWorkflowProvider();
580
+ },
581
+ };
582
+
583
+ writeFileSync(
584
+ join(projectDir, "rig.config.ts"),
585
+ `
586
+ import { defineProvider, workflow } from "${import.meta.dir}/index.ts";
587
+
588
+ const app = workflow("provider-host-storage", {
589
+ providers: {
590
+ test: defineProvider("test", {}),
591
+ },
592
+ });
593
+
594
+ export default app.sequence("root").task("ready", async () => ({ ctx: { ready: true } }));
595
+ `,
596
+ );
597
+
598
+ const engine = await createDevMachineEngine({
599
+ projectDir,
600
+ hostStorageDir,
601
+ providers: [plugin],
602
+ local: {
603
+ open: async (target) => {
604
+ opened.push(target);
605
+ },
606
+ },
607
+ });
608
+
609
+ await engine.load();
610
+ await engine.plan();
611
+
612
+ expect(opened).toEqual(["rigkit://provider-auth"]);
613
+
614
+ const files = readdirSync(hostStorageDir);
615
+ expect(files).toHaveLength(1);
616
+ const hostState = JSON.parse(readFileSync(join(hostStorageDir, files[0]!), "utf8"));
617
+ expect(hostState.records.token.value.value).toBe("secret");
618
+
619
+ const main = new Database(engine.getProjectInfo().statePath);
620
+ const projectRow = main
621
+ .query<{ value_json: string }, []>(
622
+ "select value_json from provider_state where provider_id = 'test' and key = 'project'",
623
+ )
624
+ .get();
625
+ const leakedHostRow = main
626
+ .query<{ value_json: string }, []>(
627
+ "select value_json from provider_state where provider_id = 'test' and key = 'token'",
628
+ )
629
+ .get();
630
+ main.close();
631
+
632
+ expect(projectRow ? JSON.parse(projectRow.value_json).value : undefined).toBe("state");
633
+ expect(leakedHostRow).toBeNull();
634
+ });
635
+
536
636
  test("rejects task outputs that are not JSON serializable", async () => {
537
637
  const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
538
638
  writeFileSync(
@@ -547,7 +647,7 @@ describe("DevMachineEngine workflow runtime", () => {
547
647
  });
548
648
 
549
649
  export default app.sequence("bad").task("returns-function", async () => {
550
- return { fn: () => "nope" };
650
+ return { ctx: { fn: () => "nope" } };
551
651
  });
552
652
  `,
553
653
  );
@@ -576,7 +676,7 @@ describe("DevMachineEngine workflow runtime", () => {
576
676
 
577
677
  export default app.sequence("auth").task("login", async ({ test }) => {
578
678
  const result = await test.openTerminal("GitHub auth", "gh auth login");
579
- return { finished: result.finished };
679
+ return { ctx: { finished: result.finished } };
580
680
  });
581
681
  `,
582
682
  );
@@ -622,7 +722,7 @@ describe("DevMachineEngine workflow runtime", () => {
622
722
 
623
723
  export default app.sequence("auth").task("login", async ({ test }) => {
624
724
  const result = await test.openTerminal("GitHub auth", "gh auth login");
625
- return { finished: result.finished };
725
+ return { ctx: { finished: result.finished } };
626
726
  });
627
727
  `,
628
728
  );
@@ -676,7 +776,7 @@ describe("DevMachineEngine workflow runtime", () => {
676
776
  export default app.sequence("setup").task("touch", async ({ test }) => {
677
777
  const vm = await test.createVm();
678
778
  await vm.exec("touch /tmp/setup", { name: "touch setup" });
679
- return { vm: await vm.snapshotRef() };
779
+ return { ctx: { vm: await vm.snapshotRef() } };
680
780
  });
681
781
  `,
682
782
  );
@@ -734,8 +834,8 @@ describe("DevMachineEngine workflow runtime", () => {
734
834
 
735
835
  export default app.sequence("schema").task("value", { output: schema }, async () => {
736
836
  return process.env.RIGKIT_SCHEMA_MODE === "next"
737
- ? { value: "ok", next: true }
738
- : { value: "ok" };
837
+ ? { ctx: { value: "ok", next: true } }
838
+ : { ctx: { value: "ok" } };
739
839
  });
740
840
  `,
741
841
  );
@@ -767,6 +867,94 @@ describe("DevMachineEngine workflow runtime", () => {
767
867
  }
768
868
  }
769
869
  });
870
+
871
+ test("expires task cache when cacheTTL has elapsed", async () => {
872
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cache-ttl-"));
873
+ writeFileSync(
874
+ join(projectDir, "rig.config.ts"),
875
+ `
876
+ import { sequence } from "${import.meta.dir}/index.ts";
877
+
878
+ export default sequence("ttl").task("daily-check", { cacheTTL: "1d" }, async () => {
879
+ return { ctx: { checked: true } };
880
+ });
881
+ `,
882
+ );
883
+
884
+ const engine = await createDevMachineEngine({ projectDir });
885
+ await engine.load();
886
+ await engine.apply();
887
+ expect((await engine.plan()).cachedNodeCount).toBe(1);
888
+
889
+ const db = new Database(engine.getProjectInfo().statePath);
890
+ db.run("update workflow_node_runs set created_at = ?", [
891
+ new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
892
+ ]);
893
+ db.close();
894
+
895
+ const expired = await engine.plan();
896
+ expect(expired.cachedNodeCount).toBe(0);
897
+ expect(expired.nodes[0]?.status).toBe("pending");
898
+ });
899
+
900
+ test("step.invalidate invalidates a previous task and replays from that point", async () => {
901
+ const projectDir = mkdtempSync(join(tmpdir(), "rigkit-invalidate-"));
902
+ const previous = {
903
+ authCount: process.env.RIGKIT_AUTH_COUNT,
904
+ checkCount: process.env.RIGKIT_CHECK_COUNT,
905
+ forceReauth: process.env.RIGKIT_FORCE_REAUTH,
906
+ };
907
+ process.env.RIGKIT_AUTH_COUNT = "0";
908
+ process.env.RIGKIT_CHECK_COUNT = "0";
909
+ process.env.RIGKIT_FORCE_REAUTH = "0";
910
+
911
+ writeFileSync(
912
+ join(projectDir, "rig.config.ts"),
913
+ `
914
+ import { sequence } from "${import.meta.dir}/index.ts";
915
+
916
+ export default sequence("reauth")
917
+ .task("prepare", async () => ({ ctx: { prepared: true } }))
918
+ .task("github-auth", async () => {
919
+ const count = Number(process.env.RIGKIT_AUTH_COUNT ?? "0") + 1;
920
+ process.env.RIGKIT_AUTH_COUNT = String(count);
921
+ return { ctx: { token: "token-" + count } };
922
+ })
923
+ .task("check-auth", { cacheTTL: 0 }, async ({ step }) => {
924
+ const count = Number(process.env.RIGKIT_CHECK_COUNT ?? "0") + 1;
925
+ process.env.RIGKIT_CHECK_COUNT = String(count);
926
+ if (process.env.RIGKIT_FORCE_REAUTH === "1") {
927
+ process.env.RIGKIT_FORCE_REAUTH = "0";
928
+ return step.invalidate("github-auth");
929
+ }
930
+ return { ctx: step.ctx };
931
+ });
932
+ `,
933
+ );
934
+
935
+ try {
936
+ const engine = await createDevMachineEngine({ projectDir });
937
+ await engine.load();
938
+
939
+ const first = await engine.apply();
940
+ expect(first.context.token).toBe("token-1");
941
+ expect(process.env.RIGKIT_AUTH_COUNT).toBe("1");
942
+ expect(process.env.RIGKIT_CHECK_COUNT).toBe("1");
943
+
944
+ process.env.RIGKIT_FORCE_REAUTH = "1";
945
+ const second = await engine.apply();
946
+ expect(second.context.token).toBe("token-2");
947
+ expect(process.env.RIGKIT_AUTH_COUNT).toBe("2");
948
+ expect(process.env.RIGKIT_CHECK_COUNT).toBe("3");
949
+
950
+ const validRuns = engine.listNodeRuns().filter((run) => !run.invalidated);
951
+ expect(validRuns.map((run) => run.nodePath).sort()).toEqual(["check-auth", "github-auth", "prepare"]);
952
+ } finally {
953
+ restoreEnv("RIGKIT_AUTH_COUNT", previous.authCount);
954
+ restoreEnv("RIGKIT_CHECK_COUNT", previous.checkCount);
955
+ restoreEnv("RIGKIT_FORCE_REAUTH", previous.forceReauth);
956
+ }
957
+ });
770
958
  });
771
959
 
772
960
  type FakeSnapshotRef = {
@@ -879,6 +1067,14 @@ function result(ok: boolean): ExecResult {
879
1067
  return { stdout: "", stderr: "", exitCode: ok ? 0 : 1, ok };
880
1068
  }
881
1069
 
1070
+ function restoreEnv(name: string, value: string | undefined): void {
1071
+ if (value === undefined) {
1072
+ delete process.env[name];
1073
+ } else {
1074
+ process.env[name] = value;
1075
+ }
1076
+ }
1077
+
882
1078
  function isFakeSnapshotRef(value: unknown): value is FakeSnapshotRef {
883
1079
  return Boolean(
884
1080
  value &&