# Files changed · PR \#22562 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22562/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[test: ban wall-clock readings in tests](https://git.cafe/pingdotgg/t3code/pull/22562.md)

Version: 23

Head commit: 595c60457c6bdfc4569d7212072c70f8fe9e4fe9

Base commit: 85eb3371170ef9e423a1d38d1de99f35a6d6a0ac

Comparison base: 85eb3371170ef9e423a1d38d1de99f35a6d6a0ac

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/features/home/useThreadListActions.test.ts

Status: modified; +2; −1

```diff
@@ -391,7 +391,8 @@
       await useThreadListActions().unpinThread(thread);
       expect(readThreadUndoNotice()?.action).toBe("Unpinned");
       await readThreadUndoNotice()!.undo();
+      // 2099 is the unexpired case; 2000 has long passed.
+      const restoresSnooze = snoozedUntil === "2099-01-01T09:00:00.000Z";
-      const restoresSnooze = snoozedUntil != null && Date.parse(snoozedUntil) > Date.now();
       expect(state.requests.map(({ action }) => action)).toEqual(
         restoresSnooze ? ["unpin", "pin", "snooze"] : ["unpin", "pin"],
       );
```

## apps/server/src/serverRuntimeState.test.ts

Status: modified; +2; −2

```diff
@@ -206,8 +206,8 @@
       const state = yield* makeState;
       yield* ServerRuntimeState.persistServerRuntimeState({
         path: statePath,
-        // @effect-diagnostics-next-line globalDateInEffect:off - Legacy identity uses the real process start time, not TestClock.
-        state: { ...state, startedAt: new Date().toISOString() },
+        // Any record time after this process started proves the PID was not reused.
+        state: { ...state, startedAt: "2100-01-01T00:00:00.000Z" },
       });
       const refused = yield* Effect.scoped(ServerOwnership.acquireServerOwnership(statePath)).pipe(
         Effect.flip,
```

## apps/web/src/components/onboarding/WelcomeWizard.import.browser.test.tsx

Status: modified; +9; −3

```diff
@@ -163,6 +163,9 @@
     AsyncResult.success(session(allowed ? [AuthOrchestrationOperateScope] : [])),
   );
 }
+
+// Default selection only picks recently active projects, so pin "now".
+const NOW = Date.parse("2026-01-01T00:00:00.000Z");
 
 function candidate(path: string): AgentSessionProjectCandidate {
   return {
@@ -170,7 +173,7 @@
     title: path.split("/").at(-1)!,
     sources: ["codex"],
     threadCount: 10,
-    lastActiveAt: new Date(Date.now() - 60_000).toISOString(),
+    lastActiveAt: new Date(NOW - 60_000).toISOString(),
     alreadyImported: false,
   };
 }
@@ -227,7 +230,9 @@
 }
 
 // The mocked modules read `state`, so every test starts by replacing all of it.
+// Returns the clock spy so each test can `using` it; the spy would otherwise outlive the test.
 function setup() {
+  const now = vi.spyOn(Date, "now").mockReturnValue(NOW);
   state.sessions.clear();
   for (const id of [primaryId, remoteId]) {
     state.sessions.set(
@@ -253,10 +258,11 @@
   state.onDone.mockReset();
   state.scan.mockReset();
   state.refreshScan.mockReset();
+  return now;
 }
 
 it("rechecks after project creation and can retry its history without recreating the project", async () => {
-  setup();
+  using _now = setup();
   const creation = deferred<void>();
   state.createProject.mockImplementationOnce(async (input: CreateProjectInput) => {
     await creation.promise;
@@ -283,7 +289,7 @@
 });
 
 it("completes accepted imports when permission is revoked after the final server write", async () => {
+  using _now = setup();
-  setup();
   state.candidates = state.candidates.slice(0, 1);
   const imported = deferred<{ importedCount: number; skippedCount: number }>();
   state.importThreads.mockImplementationOnce(async () =>
```

## apps/web/src/hooks/useThreadActions.undo.test.ts

Status: modified; +1; −1

```diff
@@ -265,7 +265,7 @@
   it("wakes the thread from the snooze notice", async () => {
     const add = vi.spyOn(toastManager, "add").mockReturnValue("toast");
     const actions = useThreadActions();
-    await actions.snoozeThread(target, new Date(Date.now() + 60_000).toISOString());
+    await actions.snoozeThread(target, "2100-01-01T00:00:00.000Z");
     expect(useThreadUndoNotice.getState().notice).toMatchObject({ action: "Snoozed", count: 1 });
     expect(add).not.toHaveBeenCalled();
     await currentUndo()();
```

## oxlint-plugin-t3code/index.ts

Status: modified; +2; −0

```diff
@@ -20,6 +20,7 @@
 import noTestTimeoutOrUnreasonedSkip from "./rules/no-test-timeout-or-unreasoned-skip.ts";
 import noRpcPermissionBypass from "./rules/no-rpc-permission-bypass.ts";
 import noUnscopedHas from "./rules/no-unscoped-has.ts";
+import noWallClockInTests from "./rules/no-wall-clock-in-tests.ts";
 import preferCatchTags from "./rules/prefer-catch-tags.ts";
 import preferEffectFn from "./rules/prefer-effect-fn.ts";
 import requireCenteredScrollGutter from "./rules/require-centered-scroll-gutter.ts";
@@ -51,6 +52,7 @@
     "no-test-timeout-or-unreasoned-skip": noTestTimeoutOrUnreasonedSkip,
     "no-rpc-permission-bypass": noRpcPermissionBypass,
     "no-unscoped-has": noUnscopedHas,
+    "no-wall-clock-in-tests": noWallClockInTests,
     "prefer-catch-tags": preferCatchTags,
     "prefer-effect-fn": preferEffectFn,
     "require-centered-scroll-gutter": requireCenteredScrollGutter,
```

## oxlint-plugin-t3code/rules/no-wall-clock-in-tests.test.ts

Status: added; +94; −0

```diff
@@ -1,0 +1,94 @@
+import { assert, describe } from "@effect/vitest";
+
+import { createOxlintRuleHarness } from "../test/utils.ts";
+
+const rule = createOxlintRuleHarness("t3code/no-wall-clock-in-tests", {
+  filename: "fixture.test.ts",
+});
+
+describe("t3code/no-wall-clock-in-tests", () => {
+  rule.valid(
+    "ignores a computed member with a variable key",
+    `
+      import { it } from "vitest";
+      it("parses", () => {
+        const now = "parse";
+        const parsed = Date[now]("2026-01-01");
+        const literal = Date["parse"]("2026-01-01");
+      });
+    `,
+  );
+
+  rule.valid(
+    "allows fixed dates and readings that seed mocks",
+    `
+      import { vi } from "vitest";
+
+      const epoch = new Date("2026-01-01T00:00:00.000Z");
+      export const later = new Date(epoch.getTime() + 1_000);
+      vi.setSystemTime(new Date());
+      vi.spyOn(Date, "now").mockReturnValue(Date.now() + 5_000);
+      const clock = vi.fn(() => performance.now());
+      clock.mockImplementation(() => Number(process.hrtime.bigint()));
+      vi.mock("./clock", () => ({ now: () => Date.now() }));
+    `,
+  );
+
+  rule.invalid(
+    "reports elapsed-time measurement",
+    `
+      const startedAt = performance.now();
+      const elapsed = performance.now() - startedAt;
+      if (elapsed > 10_000) throw new Error("slow");
+    `,
+    (output) => {
+      assert.match(output, /Wall-clock reading in a test/);
+      assert.equal(output.match(/no-wall-clock-in-tests/g)?.length, 2);
+    },
+  );
+
+  rule.invalid(
+    "reports Date.now fixtures",
+    `
+      export const session = { expiresAt: Date.now() + 60_000 };
+    `,
+  );
+
+  rule.invalid(
+    "reports new Date() with no arguments",
+    `
+      export const createdAt = new Date().toISOString();
+    `,
+  );
+
+  rule.invalid(
+    "reports process.hrtime and process.hrtime.bigint",
+    `
+      export const a = process.hrtime();
+      export const b = process.hrtime.bigint();
+    `,
+    (output) => {
+      assert.equal(output.match(/no-wall-clock-in-tests/g)?.length, 2);
+    },
+  );
+
+  rule.invalid(
+    "reports a reading outside the mock's arguments",
+    `
+      import { vi } from "vitest";
+
+      const mock = vi.fn();
+      mock.mockReturnValue(1);
+      export const now = Date.now();
+    `,
+  );
+});
+
+const productionRule = createOxlintRuleHarness("t3code/no-wall-clock-in-tests");
+
+productionRule.valid(
+  "ignores non-test files",
+  `
+    export const now = Date.now();
+  `,
+);
```

## oxlint-plugin-t3code/rules/no-wall-clock-in-tests.ts

Status: added; +107; −0

```diff
@@ -1,0 +1,107 @@
+import { defineRule, type ESTree } from "@oxlint/plugins";
+import * as Option from "effect/Option";
+
+import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts";
+
+const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
+const WALL_CLOCK_CALLS = new Set([
+  "Date.now",
+  "performance.now",
+  "process.hrtime",
+  "process.hrtime.bigint",
+]);
+const VI_MOCKING_FUNCTIONS = new Set(["fn", "mock", "setSystemTime", "spyOn"]);
+const MOCK_METHODS = new Set([
+  "mockImplementation",
+  "mockImplementationOnce",
+  "mockResolvedValue",
+  "mockResolvedValueOnce",
+  "mockReturnValue",
+  "mockReturnValueOnce",
+]);
+
+const MESSAGE =
+  "Wall-clock reading in a test; elapsed-time assertions pass or fail with runner load. Use TestClock/`Clock` under `it.effect`, `vi.setSystemTime`, or a fixed epoch. Benchmarks live behind an env gate.";
+
+const isStringLiteral = (node: unknown) =>
+  Option.exists(
+    unwrapExpression(node),
+    (expression) => expression.type === "Literal" && typeof expression.value === "string",
+  );
+
+const memberPath = (node: unknown): Option.Option<ReadonlyArray<string>> => {
+  const expression = unwrapExpression(node);
+  if (Option.isNone(expression)) return Option.none();
+  if (expression.value.type === "Identifier") return Option.some([expression.value.name]);
+  if (expression.value.type !== "MemberExpression") return Option.none();
+  // `Date[now]` with a variable key is unknowable; only a literal key names a member.
+  if (expression.value.computed && !isStringLiteral(expression.value.property))
+    return Option.none();
+  const property = getPropertyName(expression.value.property);
+  if (Option.isNone(property)) return Option.none();
+  return Option.map(memberPath(expression.value.object), (path) => [...path, property.value]);
+};
+
+const isWallClockCall = (callee: unknown): boolean =>
+  Option.match(memberPath(callee), {
+    onNone: () => false,
+    onSome: (path) => WALL_CLOCK_CALLS.has(path.join(".")),
+  });
+
+const isMockingCall = (callee: unknown): boolean => {
+  const expression = unwrapExpression(callee);
+  if (Option.isNone(expression) || expression.value.type !== "MemberExpression") return false;
+  const property = getPropertyName(expression.value.property);
+  if (Option.isNone(property)) return false;
+  if (MOCK_METHODS.has(property.value)) return true;
+  return (
+    VI_MOCKING_FUNCTIONS.has(property.value) &&
+    isIdentifier(unwrapExpression(expression.value.object), "vi")
+  );
+};
+
+// Readings that feed a mock (or live in a vi.mock factory) seed fake time
+// rather than measure the runner, so they stay allowed.
+const isMockArgument = (node: ESTree.Node): boolean => {
+  let child: ESTree.Node = node;
+  let current = node.parent;
+  while (current) {
+    if (
+      current.type === "CallExpression" &&
+      current.callee !== child &&
+      isMockingCall(current.callee)
+    ) {
+      return true;
+    }
+    child = current;
+    current = current.parent;
+  }
+  return false;
+};
+
+export default defineRule({
+  meta: {
+    type: "problem",
+    docs: {
+      description:
+        "Disallow wall-clock readings (Date.now, new Date(), performance.now, process.hrtime) in tests.",
+    },
+  },
+  create(context) {
+    if (!TEST_FILE_PATTERN.test(context.filename)) return {};
+
+    return {
+      CallExpression(node) {
+        if (!isWallClockCall(node.callee)) return;
+        if (isMockArgument(node)) return;
+        context.report({ node, message: MESSAGE });
+      },
+      NewExpression(node) {
+        if (node.arguments.length > 0) return;
+        if (!isIdentifier(unwrapExpression(node.callee), "Date")) return;
+        if (isMockArgument(node)) return;
+        context.report({ node, message: MESSAGE });
+      },
+    };
+  },
+});
```

## vite.config.ts

Status: modified; +1; −0

```diff
@@ -192,6 +192,7 @@
       "t3code/no-test-timeout-or-unreasoned-skip": "error",
       "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }],
       "t3code/no-unscoped-has": "error",
+      "t3code/no-wall-clock-in-tests": "error",
       "t3code/namespace-node-imports": "error",
       "t3code/prefer-catch-tags": "error",
       "t3code/prefer-effect-fn": "error",
```
