| author | |
| committer | |
| log | 194f838e7636f97739c440c7855aed195dbd7f52 |
| tree | 636e1e0ee32d20178fb127362450ef5e0e2a9f0a |
| parent | ec44bc56f598be16ffbc22ed0e4e095447043844 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>21 files changed, 258 insertions(+), 231 deletions(-)
apps/desktop/src/preview-pip-preload.ts-1| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | // @effect-diagnostics globalDate:off - This isolated Electron preload does not run inside an Effect runtime. | |
| 2 | 1 | import type { DesktopPreviewRecordingFrame } from "@t3tools/contracts"; |
| 3 | 2 | import { contextBridge, ipcRenderer } from "electron"; |
| 4 | 3 |
apps/mobile/metro.config.js+1-2| ... | ... | @@ -36,8 +36,7 @@ config.resolver = { |
| 36 | 36 | new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`), |
| 37 | 37 | ], |
| 38 | 38 | extraNodeModules: { |
| 39 | // oxlint-disable-next-line unicorn/no-useless-fallback-in-spread | |
| 40 | ...(config.resolver?.extraNodeModules ?? {}), | |
| 39 | ...config.resolver?.extraNodeModules, | |
| 41 | 40 | shiki: mobileShikiRoot, |
| 42 | 41 | "@shikijs/core": resolveShikiDependencyRoot("@shikijs/core"), |
| 43 | 42 | "@shikijs/engine-javascript": resolveShikiDependencyRoot("@shikijs/engine-javascript"), |
apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx+10-8| ... | ... | @@ -9,10 +9,10 @@ const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([ |
| 9 | 9 | StyleSheet.create({}), |
| 10 | 10 | ]); |
| 11 | 11 | |
| 12 | const textDefaults: TextProps = { | |
| 12 | const textDefaults = { | |
| 13 | 13 | allowFontScaling: true, |
| 14 | 14 | selectable: true, |
| 15 | }; | |
| 15 | } satisfies TextProps; | |
| 16 | 16 | |
| 17 | 17 | const useTextAncestorContext = () => React.useContext(TextAncestorContext); |
| 18 | 18 | |
| ... | ... | @@ -28,7 +28,11 @@ export type ContextMenuActionEvent = { |
| 28 | 28 | nativeEvent: { target: number; actionIdentifier: string }; |
| 29 | 29 | }; |
| 30 | 30 | |
| 31 | export type MarkdownTextPrimitiveProps = TextProps & { | |
| 31 | /** | |
| 32 | * `onTextLayout` is not offered: the native view reports plain line strings | |
| 33 | * while the React Native Text fallback reports measured `TextLayoutLine`s. | |
| 34 | */ | |
| 35 | export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & { | |
| 32 | 36 | uiTextView?: boolean; |
| 33 | 37 | contextMenuConfig?: string; |
| 34 | 38 | onContextMenuAction?: (event: ContextMenuActionEvent) => void; |
| ... | ... | @@ -75,16 +79,14 @@ function MarkdownTextPrimitiveChild({ style, children, ...rest }: MarkdownTextPr |
| 75 | 79 | }); |
| 76 | 80 | |
| 77 | 81 | if (!isAncestor) { |
| 82 | // Press handlers are delivered by the text runs; the container never sees them. | |
| 83 | const { onPress: _onPress, onLongPress: _onLongPress, ...containerProps } = rest; | |
| 78 | 84 | return ( |
| 79 | 85 | <TextAncestorContext.Provider value={contextValue}> |
| 80 | 86 | <T3MarkdownTextNativeComponent |
| 81 | 87 | {...textDefaults} |
| 82 | {...rest} | |
| 83 | // ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'} | |
| 88 | {...containerProps} | |
| 84 | 89 | style={[flattenedStyle]} |
| 85 | // @ts-expect-error Weirdness | |
| 86 | onPress={undefined} | |
| 87 | onLongPress={undefined} | |
| 88 | 90 | > |
| 89 | 91 | {nativeChildren} |
| 90 | 92 | </T3MarkdownTextNativeComponent> |
apps/server/scripts/migrate-dev-db.ts-1| ... | ... | @@ -24,7 +24,6 @@ |
| 24 | 24 | * cursors never rewind. |
| 25 | 25 | */ |
| 26 | 26 | |
| 27 | // @effect-diagnostics nodeBuiltinImport:off - node:os resolves the shared T3 home guard. | |
| 28 | 27 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 29 | 28 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 30 | 29 | import * as NodeOS from "node:os"; |
apps/server/scripts/t3-sqlite-state.ts-1| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | #!/usr/bin/env node |
| 2 | 2 | |
| 3 | // @effect-diagnostics nodeBuiltinImport:off - node:os resolves the shared T3 home guard. | |
| 4 | 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 5 | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 6 | 5 | import * as NodeOS from "node:os"; |
apps/server/src/assets/AttachmentUpload.ts-1| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | |
| 2 | 1 | import * as NodeCrypto from "node:crypto"; |
| 3 | 2 | |
| 4 | 3 | import { |
apps/server/src/http.ts+1-2| ... | ... | @@ -73,8 +73,7 @@ export function downloadContentDisposition(fileName?: string): string { |
| 73 | 73 | return "attachment"; |
| 74 | 74 | } |
| 75 | 75 | // toWellFormed: encodeURIComponent throws URIError on unpaired surrogates. |
| 76 | // eslint-disable-next-line no-control-regex -- Header filenames must strip ASCII controls. | |
| 77 | const sanitized = fileName.toWellFormed().replace(/[\u0000-\u001f"\\]/g, "_"); | |
| 76 | const sanitized = fileName.toWellFormed().replace(/[\p{Cc}"\\]/gu, "_"); | |
| 78 | 77 | const asciiFallback = sanitized.replace(/[^\u0020-\u007e]/g, "_"); |
| 79 | 78 | const needsExtended = asciiFallback !== sanitized; |
| 80 | 79 | const extendedName = encodeURIComponent(sanitized).replace( |
apps/server/src/relay/AgentAwarenessRelay.test.ts+37-49| ... | ... | @@ -358,58 +358,45 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 358 | 358 | ).toEqual([activeThreadId]); |
| 359 | 359 | }); |
| 360 | 360 | |
| 361 | it("signs the activity publish JWT and rejects tampering", async () => { | |
| 362 | const keyPair = NodeCrypto.generateKeyPairSync("ed25519", { | |
| 363 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, | |
| 364 | publicKeyEncoding: { format: "pem", type: "spki" }, | |
| 365 | }); | |
| 366 | const payload = { | |
| 367 | iss: "t3-env:env", | |
| 368 | aud: "https://relay.example.test", | |
| 369 | sub: "env", | |
| 370 | jti: "nonce-1", | |
| 371 | iat: 100, | |
| 372 | exp: 200, | |
| 373 | environmentId: state.environmentId, | |
| 374 | threadId: state.threadId, | |
| 375 | state, | |
| 376 | } satisfies RelayAgentActivityPublishProofPayload; | |
| 377 | const proof = await Effect.runPromise( | |
| 378 | AgentAwarenessRelay.signRelayAgentActivityPublishProof({ | |
| 361 | it.effect("signs the activity publish JWT and rejects tampering", () => | |
| 362 | Effect.gen(function* () { | |
| 363 | const keyPair = NodeCrypto.generateKeyPairSync("ed25519", { | |
| 364 | privateKeyEncoding: { format: "pem", type: "pkcs8" }, | |
| 365 | publicKeyEncoding: { format: "pem", type: "spki" }, | |
| 366 | }); | |
| 367 | const payload = { | |
| 368 | iss: "t3-env:env", | |
| 369 | aud: "https://relay.example.test", | |
| 370 | sub: "env", | |
| 371 | jti: "nonce-1", | |
| 372 | iat: 100, | |
| 373 | exp: 200, | |
| 374 | environmentId: state.environmentId, | |
| 375 | threadId: state.threadId, | |
| 376 | state, | |
| 377 | } satisfies RelayAgentActivityPublishProofPayload; | |
| 378 | const proof = yield* AgentAwarenessRelay.signRelayAgentActivityPublishProof({ | |
| 379 | 379 | privateKey: keyPair.privateKey, |
| 380 | 380 | payload, |
| 381 | }), | |
| 382 | ); | |
| 383 | ||
| 384 | await expect( | |
| 385 | Effect.runPromise( | |
| 386 | verifyRelayJwt({ | |
| 387 | publicKey: keyPair.publicKey, | |
| 388 | token: proof, | |
| 389 | typ: RELAY_ACTIVITY_PUBLISH_TYP, | |
| 390 | issuer: "t3-env:env", | |
| 391 | audience: "https://relay.example.test", | |
| 392 | nowEpochSeconds: 150, | |
| 393 | }), | |
| 394 | ), | |
| 395 | ).resolves.toMatchObject({ jti: "nonce-1", state }); | |
| 396 | await expect( | |
| 397 | Effect.runPromise( | |
| 381 | }); | |
| 382 | const verify = (token: string) => | |
| 398 | 383 | verifyRelayJwt({ |
| 399 | 384 | publicKey: keyPair.publicKey, |
| 400 | token: (() => { | |
| 401 | const [header, body, signature = ""] = proof.split("."); | |
| 402 | const corruptedSignature = `${signature.startsWith("a") ? "b" : "a"}${signature.slice(1)}`; | |
| 403 | return `${header}.${body}.${corruptedSignature}`; | |
| 404 | })(), | |
| 385 | token, | |
| 405 | 386 | typ: RELAY_ACTIVITY_PUBLISH_TYP, |
| 406 | 387 | issuer: "t3-env:env", |
| 407 | 388 | audience: "https://relay.example.test", |
| 408 | 389 | nowEpochSeconds: 150, |
| 409 | }), | |
| 410 | ), | |
| 411 | ).rejects.toBeDefined(); | |
| 412 | }); | |
| 390 | }); | |
| 391 | ||
| 392 | expect(yield* verify(proof)).toMatchObject({ jti: "nonce-1", state }); | |
| 393 | ||
| 394 | const [header, body, signature = ""] = proof.split("."); | |
| 395 | const corruptedSignature = `${signature.startsWith("a") ? "b" : "a"}${signature.slice(1)}`; | |
| 396 | const rejection = yield* Effect.flip(verify(`${header}.${body}.${corruptedSignature}`)); | |
| 397 | expect(rejection).toBeDefined(); | |
| 398 | }), | |
| 399 | ); | |
| 413 | 400 | |
| 414 | 401 | it.effect("keeps the orchestration listener armed until relay config is installed", () => |
| 415 | 402 | Effect.scoped( |
| ... | ... | @@ -557,10 +544,11 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 557 | 544 | Effect.scoped( |
| 558 | 545 | Effect.gen(function* () { |
| 559 | 546 | const originalFetch = globalThis.fetch; |
| 560 | const context = yield* Effect.context<never>(); | |
| 561 | const runFork = Effect.runForkWith(context); | |
| 562 | 547 | const events = yield* Queue.unbounded<OrchestrationEvent>(); |
| 563 | const fetchSeen = yield* Deferred.make<URL>(); | |
| 548 | let resolveFetchSeen: (url: URL) => void = () => {}; | |
| 549 | const fetchSeen = new Promise<URL>((resolve) => { | |
| 550 | resolveFetchSeen = resolve; | |
| 551 | }); | |
| 564 | 552 | const userSpans: Array<string> = []; |
| 565 | 553 | const productSpans: Array<string> = []; |
| 566 | 554 | const collectingTracer = (spans: Array<string>) => |
| ... | ... | @@ -648,7 +636,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 648 | 636 | ? input |
| 649 | 637 | : (input as unknown as { readonly url: string }).url, |
| 650 | 638 | ); |
| 651 | runFork(Deferred.succeed(fetchSeen, url)); | |
| 639 | resolveFetchSeen(url); | |
| 652 | 640 | return Promise.resolve(Response.json({ ok: true, deliveries: [] })); |
| 653 | 641 | }) as unknown as typeof fetch; |
| 654 | 642 | yield* Effect.addFinalizer(() => |
| ... | ... | @@ -707,7 +695,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 707 | 695 | occurredAt: now, |
| 708 | 696 | } as unknown as OrchestrationEvent); |
| 709 | 697 | |
| 710 | const url = yield* Deferred.await(fetchSeen).pipe(Effect.timeout("2 seconds")); | |
| 698 | const url = yield* Effect.promise(() => fetchSeen).pipe(Effect.timeout("2 seconds")); | |
| 711 | 699 | expect(url.origin).toBe("https://transport.example.test"); |
| 712 | 700 | expect(productSpans).toContain("makePublishProof"); |
| 713 | 701 | expect(userSpans).not.toContain("makePublishProof"); |
apps/server/src/server.test.ts+8-8| ... | ... | @@ -56,7 +56,6 @@ import * as Effect from "effect/Effect"; |
| 56 | 56 | import * as FileSystem from "effect/FileSystem"; |
| 57 | 57 | import * as Fiber from "effect/Fiber"; |
| 58 | 58 | import * as Layer from "effect/Layer"; |
| 59 | import * as ManagedRuntime from "effect/ManagedRuntime"; | |
| 60 | 59 | import * as Option from "effect/Option"; |
| 61 | 60 | import * as Path from "effect/Path"; |
| 62 | 61 | import * as PubSub from "effect/PubSub"; |
| ... | ... | @@ -398,7 +397,9 @@ const makeBrowserOtlpPayload = (spanName: string) => |
| 398 | 397 | ({ close }) => Effect.promise(close), |
| 399 | 398 | ); |
| 400 | 399 | |
| 401 | const runtime = ManagedRuntime.make( | |
| 400 | // The exporter's batch fiber is forked while the layer builds and ticks on | |
| 401 | // a wall-clock interval, so the whole tracer runs on the live clock. | |
| 402 | yield* Layer.build( | |
| 402 | 403 | OtlpTracer.layer({ |
| 403 | 404 | url: collector.url, |
| 404 | 405 | exportInterval: "10 millis", |
| ... | ... | @@ -411,14 +412,13 @@ const makeBrowserOtlpPayload = (spanName: string) => |
| 411 | 412 | }, |
| 412 | 413 | }, |
| 413 | 414 | }).pipe(Layer.provide(browserOtlpTracingLayer)), |
| 415 | ).pipe( | |
| 416 | Effect.flatMap((tracing) => | |
| 417 | Effect.void.pipe(Effect.withSpan(spanName), Effect.provideContext(tracing)), | |
| 418 | ), | |
| 419 | TestClock.withLive, | |
| 414 | 420 | ); |
| 415 | 421 | |
| 416 | try { | |
| 417 | yield* Effect.promise(() => runtime.runPromise(Effect.void.pipe(Effect.withSpan(spanName)))); | |
| 418 | } finally { | |
| 419 | yield* Effect.promise(() => runtime.dispose()); | |
| 420 | } | |
| 421 | ||
| 422 | 422 | const request = yield* Effect.raceFirst( |
| 423 | 423 | Effect.promise(() => collector.firstRequest).pipe(Effect.orDie), |
| 424 | 424 | Effect.sleep(Duration.seconds(1)).pipe( |
apps/server/src/serviceLauncher.ts-1| ... | ... | @@ -1,5 +1,4 @@ |
| 1 | 1 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | // @effect-diagnostics globalDate:off | |
| 3 | 2 | // @effect-diagnostics globalTimers:off |
| 4 | 3 | // This file is shipped as a standalone bundle and copied to a stable path by |
| 5 | 4 | // `t3 service update`. Keep runtime imports limited to Node built-ins. |
apps/web/src/components/settings/ThemeSearchSection.tsx+6-7| ... | ... | @@ -204,13 +204,12 @@ export function ThemeSearchSection({ |
| 204 | 204 | return; |
| 205 | 205 | } |
| 206 | 206 | void runSearch(debouncedQuery); |
| 207 | // `installingId` and `sortBy` are deliberately not dependencies: the | |
| 208 | // guards above read the current values from the fresh render closure. An | |
| 209 | // install finishing reruns the search only when the query or sort changed | |
| 210 | // while it was in flight (checked via lastSearchKeyRef, recorded only | |
| 211 | // once a search succeeds), so the install error the user needs to see is | |
| 212 | // preserved across that rerun. | |
| 213 | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 207 | // `sortBy` is deliberately not a direct dependency: the guards above read | |
| 208 | // the current value from the fresh render closure. An install finishing | |
| 209 | // reruns the search only when the query or sort changed while it was in | |
| 210 | // flight (checked via lastSearchKeyRef, recorded only once a search | |
| 211 | // succeeds), so the install error the user needs to see is preserved | |
| 212 | // across that rerun. | |
| 214 | 213 | }, [open, query, debouncedQuery, installingId, runSearch]); |
| 215 | 214 | |
| 216 | 215 | const handleSortChange = useCallback((value: OpenVsxThemeSort | null) => { |
apps/web/src/hooks/useTheme.ts+1-2| ... | ... | @@ -356,8 +356,7 @@ function applyTheme(theme: Theme, { suppressTransitions = false, preservePreview |
| 356 | 356 | syncDesktopTheme(theme, followSystem, appearanceMode); |
| 357 | 357 | if (suppressTransitions) { |
| 358 | 358 | // Force a reflow so the no-transitions class takes effect before removal |
| 359 | // oxlint-disable-next-line no-unused-expressions | |
| 360 | document.documentElement.offsetHeight; | |
| 359 | void document.documentElement.offsetHeight; | |
| 361 | 360 | requestAnimationFrame(() => { |
| 362 | 361 | document.documentElement.classList.remove("no-transitions"); |
| 363 | 362 | }); |
oxlint-plugin-t3code/rules/no-global-process-runtime.ts+1-20| ... | ... | @@ -4,23 +4,8 @@ import * as Option from "effect/Option"; |
| 4 | 4 | import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts"; |
| 5 | 5 | |
| 6 | 6 | const RUNTIME_PROPERTIES = new Set(["platform", "arch"]); |
| 7 | const HOST_PROCESS_REFERENCE_FILE = "packages/shared/src/hostProcess.ts"; | |
| 8 | 7 | const NODE_OS_MODULES = new Set(["node:os", "os"]); |
| 9 | 8 | |
| 10 | const normalizePath = (path: string) => path.replaceAll("\\", "/"); | |
| 11 | ||
| 12 | const toRepoPath = (filename: string, cwd: string) => { | |
| 13 | const normalizedFilename = normalizePath(filename); | |
| 14 | const normalizedCwd = normalizePath(cwd).replace(/\/+$/u, ""); | |
| 15 | const prefix = `${normalizedCwd}/`; | |
| 16 | return normalizedFilename.startsWith(prefix) | |
| 17 | ? normalizedFilename.slice(prefix.length) | |
| 18 | : normalizedFilename; | |
| 19 | }; | |
| 20 | ||
| 21 | const isHostProcessReferenceFile = (filename: string, cwd: string) => | |
| 22 | toRepoPath(filename, cwd) === HOST_PROCESS_REFERENCE_FILE; | |
| 23 | ||
| 24 | 9 | const isGlobalProcessObject = (node: unknown): boolean => { |
| 25 | 10 | const expression = unwrapExpression(node); |
| 26 | 11 | if (isIdentifier(expression, "process")) return true; |
| ... | ... | @@ -48,7 +33,7 @@ export default defineRule({ |
| 48 | 33 | type: "problem", |
| 49 | 34 | docs: { |
| 50 | 35 | description: |
| 51 | "Disallow direct host runtime platform/architecture reads outside the shared host process references.", | |
| 36 | "Disallow direct host runtime platform/architecture reads; the shared host process references are exempted in the lint config.", | |
| 52 | 37 | }, |
| 53 | 38 | }, |
| 54 | 39 | createOnce(context) { |
| ... | ... | @@ -117,8 +102,6 @@ export default defineRule({ |
| 117 | 102 | before: resetBindings, |
| 118 | 103 | ImportDeclaration: trackImportDeclaration, |
| 119 | 104 | MemberExpression(node) { |
| 120 | if (isHostProcessReferenceFile(context.filename, context.cwd)) return; | |
| 121 | ||
| 122 | 105 | const property = getPropertyName(node.property); |
| 123 | 106 | if (Option.isNone(property) || !RUNTIME_PROPERTIES.has(property.value)) return; |
| 124 | 107 | if (!isGlobalProcessObject(node.object)) return; |
| ... | ... | @@ -129,8 +112,6 @@ export default defineRule({ |
| 129 | 112 | }); |
| 130 | 113 | }, |
| 131 | 114 | CallExpression(node) { |
| 132 | if (isHostProcessReferenceFile(context.filename, context.cwd)) return; | |
| 133 | ||
| 134 | 115 | const property = getNodeOsRuntimeCall(node.callee); |
| 135 | 116 | if (Option.isNone(property)) return; |
| 136 | 117 |
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.test.ts+31| ... | ... | @@ -71,3 +71,34 @@ productionRule.valid( |
| 71 | 71 | export const main = () => Effect.runPromise(Effect.void); |
| 72 | 72 | `, |
| 73 | 73 | ); |
| 74 | ||
| 75 | const legacyRule = createOxlintRuleHarness("t3code/no-manual-effect-runtime-in-tests", { | |
| 76 | filename: "legacy.test.ts", | |
| 77 | ruleOptions: [{ maxOccurrences: 2 }], | |
| 78 | }); | |
| 79 | ||
| 80 | describe("t3code/no-manual-effect-runtime-in-tests with maxOccurrences", () => { | |
| 81 | legacyRule.valid( | |
| 82 | "allows occurrences up to the ceiling", | |
| 83 | ` | |
| 84 | import * as Effect from "effect/Effect"; | |
| 85 | ||
| 86 | test("first", () => Effect.runSync(Effect.void)); | |
| 87 | test("second", () => Effect.runSync(Effect.void)); | |
| 88 | `, | |
| 89 | ); | |
| 90 | ||
| 91 | legacyRule.invalid( | |
| 92 | "reports occurrences beyond the ceiling", | |
| 93 | ` | |
| 94 | import * as Effect from "effect/Effect"; | |
| 95 | ||
| 96 | test("first", () => Effect.runSync(Effect.void)); | |
| 97 | test("second", () => Effect.runSync(Effect.void)); | |
| 98 | test("third", () => Effect.runSync(Effect.void)); | |
| 99 | `, | |
| 100 | (output) => { | |
| 101 | assert.equal(output.match(/no-manual-effect-runtime-in-tests/g)?.length, 1); | |
| 102 | }, | |
| 103 | ); | |
| 104 | }); |
oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.ts+27-39| ... | ... | @@ -19,44 +19,17 @@ const EFFECT_RUNTIME_METHODS = new Set([ |
| 19 | 19 | "runSyncWith", |
| 20 | 20 | ]); |
| 21 | 21 | |
| 22 | // Existing manual runners are tracked as debt. The rule permits no net-new | |
| 23 | // occurrences in these files, while unlisted test files must have zero. | |
| 24 | const LEGACY_BASELINE = new Map<string, number>([ | |
| 25 | ["apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts", 1], | |
| 26 | ["apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts", 2], | |
| 27 | ["apps/mobile/src/state/use-remote-environment-registry.test.ts", 2], | |
| 28 | ["apps/server/src/orchestration/commandInvariants.test.ts", 6], | |
| 29 | ["apps/server/src/orchestration/Layers/CheckpointReactor.test.ts", 42], | |
| 30 | ["apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts", 5], | |
| 31 | ["apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts", 4], | |
| 32 | ["apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts", 70], | |
| 33 | ["apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts", 31], | |
| 34 | ["apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts", 2], | |
| 35 | ["apps/server/src/orchestration/projector.test.ts", 20], | |
| 36 | ["apps/server/src/project/Layers/ProjectSetupScriptRunner.test.ts", 4], | |
| 37 | ["apps/server/src/provider/acp/CursorAcpSupport.test.ts", 1], | |
| 38 | ["apps/server/src/provider/Layers/ClaudeAdapter.test.ts", 2], | |
| 39 | ["apps/server/src/provider/Layers/CodexAdapter.test.ts", 1], | |
| 40 | ["apps/server/src/provider/Layers/CodexSessionRuntime.test.ts", 5], | |
| 41 | ["apps/server/src/provider/Layers/CursorAdapter.test.ts", 1], | |
| 42 | ["apps/server/src/provider/Layers/CursorProvider.test.ts", 4], | |
| 43 | ["apps/server/src/provider/Layers/ProviderService.test.ts", 2], | |
| 44 | ["apps/server/src/provider/Layers/ProviderSessionReaper.test.ts", 21], | |
| 45 | ["apps/server/src/relay/AgentAwarenessRelay.test.ts", 4], | |
| 46 | ["apps/server/src/server.test.ts", 1], | |
| 47 | ["apps/web/src/cloud/dpop.test.ts", 2], | |
| 48 | ["apps/web/src/environments/runtime/service.addSavedEnvironment.test.ts", 1], | |
| 49 | ["oxlint-plugin-t3code/rules/no-manual-effect-runtime-in-tests.test.ts", 7], | |
| 50 | ["packages/client-runtime/src/relay/managedRelayState.test.ts", 1], | |
| 51 | ["packages/client-runtime/src/wsTransport.test.ts", 2], | |
| 52 | ]); | |
| 53 | ||
| 54 | const baselineFor = (filename: string): number => { | |
| 55 | const normalized = filename.replaceAll("\\", "/"); | |
| 56 | for (const [suffix, count] of LEGACY_BASELINE) { | |
| 57 | if (normalized.endsWith(suffix)) return count; | |
| 58 | } | |
| 59 | return 0; | |
| 22 | // Existing manual runners are tracked as debt through the `maxOccurrences` | |
| 23 | // option, set per file in the lint config. The rule permits no net-new | |
| 24 | // occurrences in those files, while every other test file must have zero. | |
| 25 | const readMaxOccurrences = (options: ReadonlyArray<unknown>): number => { | |
| 26 | const [first] = options; | |
| 27 | return typeof first === "object" && | |
| 28 | first !== null && | |
| 29 | "maxOccurrences" in first && | |
| 30 | typeof first.maxOccurrences === "number" | |
| 31 | ? first.maxOccurrences | |
| 32 | : 0; | |
| 60 | 33 | }; |
| 61 | 34 | |
| 62 | 35 | const manualRunnerName = (callee: unknown): Option.Option<string> => { |
| ... | ... | @@ -87,11 +60,26 @@ export default defineRule({ |
| 87 | 60 | description: |
| 88 | 61 | "Disallow manually creating or running Effect runtimes in tests; use @effect/vitest.", |
| 89 | 62 | }, |
| 63 | schema: [ | |
| 64 | { | |
| 65 | type: "object", | |
| 66 | properties: { | |
| 67 | maxOccurrences: { | |
| 68 | type: "integer", | |
| 69 | minimum: 0, | |
| 70 | description: | |
| 71 | "Legacy debt ceiling for this file: occurrences beyond this count are reported.", | |
| 72 | }, | |
| 73 | }, | |
| 74 | additionalProperties: false, | |
| 75 | }, | |
| 76 | ], | |
| 77 | defaultOptions: [{ maxOccurrences: 0 }], | |
| 90 | 78 | }, |
| 91 | 79 | create(context) { |
| 92 | 80 | if (!TEST_FILE_PATTERN.test(context.filename)) return {}; |
| 93 | 81 | |
| 94 | const allowedCount = baselineFor(context.filename); | |
| 82 | const allowedCount = readMaxOccurrences(context.options); | |
| 95 | 83 | let occurrenceCount = 0; |
| 96 | 84 | |
| 97 | 85 | return { |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.test.ts+7-20| ... | ... | @@ -7,15 +7,11 @@ const guardedMobileFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-them |
| 7 | 7 | }); |
| 8 | 8 | const reviewedInteropFile = createOxlintRuleHarness( |
| 9 | 9 | "t3code/no-mobile-uniwind-theme-escape-hatches", |
| 10 | { filename: "apps/mobile/src/features/home/HomeHeader.tsx" }, | |
| 10 | { | |
| 11 | filename: "apps/mobile/src/features/home/HomeHeader.tsx", | |
| 12 | ruleOptions: [{ allowUniwindTheme: true }], | |
| 13 | }, | |
| 11 | 14 | ); |
| 12 | const gitOverlayInteropFile = createOxlintRuleHarness( | |
| 13 | "t3code/no-mobile-uniwind-theme-escape-hatches", | |
| 14 | { filename: "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx" }, | |
| 15 | ); | |
| 16 | const webFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-theme-escape-hatches", { | |
| 17 | filename: "apps/web/src/ThemeSurface.tsx", | |
| 18 | }); | |
| 19 | 15 | |
| 20 | 16 | describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { |
| 21 | 17 | guardedMobileFile.valid( |
| ... | ... | @@ -93,18 +89,9 @@ describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { |
| 93 | 89 | `, |
| 94 | 90 | ); |
| 95 | 91 | |
| 96 | gitOverlayInteropFile.valid( | |
| 97 | "allows the native liquid-glass theme boundary", | |
| 98 | ` | |
| 99 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 100 | ||
| 101 | export const tint = useUniwindTheme()["--color-glass-surface"]; | |
| 102 | `, | |
| 103 | ); | |
| 104 | ||
| 105 | webFile.valid( | |
| 106 | "does not impose the mobile custom-theme policy on web code", | |
| 107 | `const surface = <div className="bg-white dark:bg-black" />;`, | |
| 92 | reviewedInteropFile.invalid( | |
| 93 | "still reports appearance variants in reviewed interop boundaries", | |
| 94 | `const surface = <View className="bg-white dark:bg-black" />;`, | |
| 108 | 95 | ); |
| 109 | 96 | |
| 110 | 97 | guardedMobileFile.invalid( |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.ts+29-42| ... | ... | @@ -3,44 +3,9 @@ import * as Option from "effect/Option"; |
| 3 | 3 | |
| 4 | 4 | import { getPropertyName, unwrapExpression } from "../utils.ts"; |
| 5 | 5 | |
| 6 | const MOBILE_SOURCE_MARKER = "/apps/mobile/src/"; | |
| 7 | 6 | const APPEARANCE_VARIANT_PATTERN = /\b(?:dark|light):(?=\S)/u; |
| 8 | 7 | const APPEARANCE_VARIANT_MESSAGE = |
| 9 | 8 | "dark:/light: utilities do not follow registered custom themes; use an adaptive semantic token."; |
| 10 | const THEME_INTEROP_ALLOWLIST = new Set([ | |
| 11 | "features/archive/ArchivedThreadsScreen.tsx", | |
| 12 | "features/connection/ConnectionsNewRouteScreen.tsx", | |
| 13 | "features/files/FileMarkdownPreview.tsx", | |
| 14 | "features/files/SourceFileSurface.tsx", | |
| 15 | "features/files/ThreadFilesRouteScreen.tsx", | |
| 16 | "features/files/thread-file-navigator-pane.tsx", | |
| 17 | "features/home/HomeHeader.tsx", | |
| 18 | "features/review/ReviewSheet.tsx", | |
| 19 | "features/review/useNativeReviewDiffBridge.ts", | |
| 20 | "features/settings/SettingsEnvironmentsRouteScreen.tsx", | |
| 21 | "features/settings/appearance/components/AppearancePreviews.tsx", | |
| 22 | "features/settings/appearance/components/FontSizeSliderRow.tsx", | |
| 23 | "features/threads/GitActionProgressOverlay.tsx", | |
| 24 | "features/threads/NewTaskContextPickerScreens.tsx", | |
| 25 | "features/threads/NewTaskDraftScreen.tsx", | |
| 26 | "features/threads/ThreadComposer.tsx", | |
| 27 | "features/threads/ThreadFeed.tsx", | |
| 28 | "features/threads/ThreadSettingsSheet.tsx", | |
| 29 | "features/threads/git/GitOverviewSheet.tsx", | |
| 30 | "features/threads/thread-list-items.tsx", | |
| 31 | "features/threads/thread-list-v2-items.tsx", | |
| 32 | "lib/useMobileNavigationTheme.ts", | |
| 33 | "native/T3ComposerEditor.ios.tsx", | |
| 34 | "native/T3ComposerEditor.native.tsx", | |
| 35 | ]); | |
| 36 | ||
| 37 | const mobileSourcePath = (filename: string): string | undefined => { | |
| 38 | const normalized = `/${filename.replaceAll("\\", "/")}`; | |
| 39 | const markerIndex = normalized.lastIndexOf(MOBILE_SOURCE_MARKER); | |
| 40 | return markerIndex === -1 | |
| 41 | ? undefined | |
| 42 | : normalized.slice(markerIndex + MOBILE_SOURCE_MARKER.length); | |
| 43 | }; | |
| 44 | 9 | |
| 45 | 10 | const literalStringValue = (node: unknown): Option.Option<string> => { |
| 46 | 11 | if (typeof node !== "object" || node === null) return Option.none(); |
| ... | ... | @@ -54,18 +19,40 @@ const reportsAppearanceVariant = (value: string) => APPEARANCE_VARIANT_PATTERN.t |
| 54 | 19 | const importsModule = (source: string, modulePath: string): boolean => |
| 55 | 20 | source.replace(/\.[cm]?[jt]sx?$/u, "").endsWith(modulePath); |
| 56 | 21 | |
| 22 | const readAllowUniwindTheme = (options: ReadonlyArray<unknown>): boolean => { | |
| 23 | const [first] = options; | |
| 24 | return ( | |
| 25 | typeof first === "object" && | |
| 26 | first !== null && | |
| 27 | "allowUniwindTheme" in first && | |
| 28 | first.allowUniwindTheme === true | |
| 29 | ); | |
| 30 | }; | |
| 31 | ||
| 57 | 32 | export default defineRule({ |
| 58 | 33 | meta: { |
| 59 | 34 | type: "problem", |
| 60 | 35 | docs: { |
| 61 | 36 | description: |
| 62 | "Keep mobile theme styling on semantic Uniwind classes and reviewed native interop boundaries.", | |
| 37 | "Keep mobile theme styling on semantic Uniwind classes. Scope the rule to mobile sources and exempt reviewed native interop boundaries in the lint config.", | |
| 63 | 38 | }, |
| 39 | schema: [ | |
| 40 | { | |
| 41 | type: "object", | |
| 42 | properties: { | |
| 43 | allowUniwindTheme: { | |
| 44 | type: "boolean", | |
| 45 | description: | |
| 46 | "Permit useUniwindTheme in a reviewed native/third-party interop boundary that cannot consume a className.", | |
| 47 | }, | |
| 48 | }, | |
| 49 | additionalProperties: false, | |
| 50 | }, | |
| 51 | ], | |
| 52 | defaultOptions: [{ allowUniwindTheme: false }], | |
| 64 | 53 | }, |
| 65 | 54 | create(context) { |
| 66 | const sourcePath = mobileSourcePath(context.filename); | |
| 67 | if (sourcePath === undefined) return {}; | |
| 68 | ||
| 55 | const allowUniwindTheme = readAllowUniwindTheme(context.options); | |
| 69 | 56 | const uniwindNamespaces = new Set<Variable>(); |
| 70 | 57 | |
| 71 | 58 | const resolveVariable = (node: unknown): Variable | undefined => { |
| ... | ... | @@ -131,13 +118,13 @@ export default defineRule({ |
| 131 | 118 | |
| 132 | 119 | if ( |
| 133 | 120 | !isTypeOnly && |
| 134 | importsModule(source.value, "/useUniwindTheme") && | |
| 135 | !THEME_INTEROP_ALLOWLIST.has(sourcePath) | |
| 121 | !allowUniwindTheme && | |
| 122 | importsModule(source.value, "/useUniwindTheme") | |
| 136 | 123 | ) { |
| 137 | 124 | context.report({ |
| 138 | 125 | node: specifier, |
| 139 | 126 | message: |
| 140 | "Use className for theme styling, or review and add this native/third-party interop boundary to the lint allowlist.", | |
| 127 | "Use className for theme styling, or review this native/third-party interop boundary and enable allowUniwindTheme for it in the lint config.", | |
| 141 | 128 | }); |
| 142 | 129 | } |
| 143 | 130 | } |
oxlint-plugin-t3code/test/utils.ts+3-1| ... | ... | @@ -61,6 +61,8 @@ interface RuleHarness { |
| 61 | 61 | |
| 62 | 62 | interface RuleHarnessOptions { |
| 63 | 63 | readonly filename?: string; |
| 64 | /** Rule options, as they would appear after the severity in the lint config. */ | |
| 65 | readonly ruleOptions?: ReadonlyArray<unknown>; | |
| 64 | 66 | } |
| 65 | 67 | |
| 66 | 68 | const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> => |
| ... | ... | @@ -111,7 +113,7 @@ export const createOxlintRuleHarness = ( |
| 111 | 113 | configPath, |
| 112 | 114 | yield* encodeOxlintConfig({ |
| 113 | 115 | jsPlugins: [{ name: "t3code", specifier: pluginPath }], |
| 114 | rules: { [ruleName]: "error" }, | |
| 116 | rules: { [ruleName]: ["error", ...(options.ruleOptions ?? [])] }, | |
| 115 | 117 | }), |
| 116 | 118 | ); |
| 117 | 119 | yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true }); |
packages/client-runtime/src/relay/managedRelayState.test.ts+30-22| ... | ... | @@ -243,30 +243,38 @@ describe("createManagedRelayQueryManager", () => { |
| 243 | 243 | }), |
| 244 | 244 | ); |
| 245 | 245 | |
| 246 | it("emits credential changes only when the managed relay account changes", async () => { | |
| 247 | setManagedRelaySession(registry, { | |
| 248 | accountId: "account-1", | |
| 249 | readClerkToken: () => Promise.resolve("first-token"), | |
| 250 | }); | |
| 251 | const changes = Effect.runPromise( | |
| 252 | managedRelayAccountChanges(registry).pipe(Stream.take(2), Stream.runCollect), | |
| 253 | ); | |
| 254 | await vi.waitFor(() => { | |
| 255 | expect(registry.getNodes().get(managedRelaySessionAtom)?.listeners.size).toBeGreaterThan(0); | |
| 256 | }); | |
| 246 | it.effect("emits credential changes only when the managed relay account changes", () => | |
| 247 | Effect.gen(function* () { | |
| 248 | setManagedRelaySession(registry, { | |
| 249 | accountId: "account-1", | |
| 250 | readClerkToken: () => Promise.resolve("first-token"), | |
| 251 | }); | |
| 252 | const changes = yield* managedRelayAccountChanges(registry).pipe( | |
| 253 | Stream.take(2), | |
| 254 | Stream.runCollect, | |
| 255 | Effect.forkChild, | |
| 256 | ); | |
| 257 | yield* Effect.promise(() => | |
| 258 | vi.waitFor(() => { | |
| 259 | expect(registry.getNodes().get(managedRelaySessionAtom)?.listeners.size).toBeGreaterThan( | |
| 260 | 0, | |
| 261 | ); | |
| 262 | }), | |
| 263 | ); | |
| 257 | 264 | |
| 258 | setManagedRelaySession(registry, { | |
| 259 | accountId: "account-1", | |
| 260 | readClerkToken: () => Promise.resolve("refreshed-token"), | |
| 261 | }); | |
| 262 | setManagedRelaySession(registry, { | |
| 263 | accountId: "account-2", | |
| 264 | readClerkToken: () => Promise.resolve("second-token"), | |
| 265 | }); | |
| 266 | setManagedRelaySession(registry, null); | |
| 265 | setManagedRelaySession(registry, { | |
| 266 | accountId: "account-1", | |
| 267 | readClerkToken: () => Promise.resolve("refreshed-token"), | |
| 268 | }); | |
| 269 | setManagedRelaySession(registry, { | |
| 270 | accountId: "account-2", | |
| 271 | readClerkToken: () => Promise.resolve("second-token"), | |
| 272 | }); | |
| 273 | setManagedRelaySession(registry, null); | |
| 267 | 274 | |
| 268 | expect(Array.from(await changes)).toEqual(["account-2", null]); | |
| 269 | }); | |
| 275 | expect(Array.from(yield* Fiber.join(changes))).toEqual(["account-2", null]); | |
| 276 | }), | |
| 277 | ); | |
| 270 | 278 | |
| 271 | 279 | it("shares one Clerk token read across concurrent relay list and status queries", async () => { |
| 272 | 280 | const secondEnvironment = { |
scripts/build-desktop-artifact.test.ts-1| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off - packaged-archive fixtures compute the sidecar digest with the same Node primitive as the builder. | |
| 2 | 1 | import * as NodeCrypto from "node:crypto"; |
| 3 | 2 | |
| 4 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
vite.config.ts+66-3| ... | ... | @@ -26,7 +26,6 @@ export default defineConfig({ |
| 26 | 26 | }, |
| 27 | 27 | fmt: { |
| 28 | 28 | ignorePatterns: [ |
| 29 | ".reference", | |
| 30 | 29 | ".repos/**", |
| 31 | 30 | ".alchemy", |
| 32 | 31 | "dist", |
| ... | ... | @@ -37,7 +36,6 @@ export default defineConfig({ |
| 37 | 36 | "**/routeTree.gen.ts", |
| 38 | 37 | "apps/mobile/android/**", |
| 39 | 38 | "apps/mobile/ios/**", |
| 40 | "apps/web/src/lib/vendor/qrcodegen.ts", | |
| 41 | 39 | "apps/mobile/uniwind-types.d.ts", |
| 42 | 40 | "*.icon/**", |
| 43 | 41 | ], |
| ... | ... | @@ -119,11 +117,76 @@ export default defineConfig({ |
| 119 | 117 | "t3code/no-global-process-runtime": "error", |
| 120 | 118 | "t3code/no-inline-schema-compile": "warn", |
| 121 | 119 | "t3code/no-manual-effect-runtime-in-tests": "error", |
| 122 | "t3code/no-mobile-uniwind-theme-escape-hatches": "error", | |
| 123 | 120 | "t3code/no-native-title-tooltip": "error", |
| 124 | 121 | "t3code/namespace-node-imports": "error", |
| 125 | 122 | }, |
| 123 | overrides: [ | |
| 124 | { | |
| 125 | // The one place that reads the host platform to seed the injected references. | |
| 126 | files: ["packages/shared/src/hostProcess.ts"], | |
| 127 | rules: { "t3code/no-global-process-runtime": "off" }, | |
| 128 | }, | |
| 129 | { | |
| 130 | files: ["apps/mobile/src/**"], | |
| 131 | rules: { "t3code/no-mobile-uniwind-theme-escape-hatches": "error" }, | |
| 132 | }, | |
| 133 | { | |
| 134 | // Reviewed native and third-party interop boundaries that cannot consume a className. | |
| 135 | files: [ | |
| 136 | "apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx", | |
| 137 | "apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx", | |
| 138 | "apps/mobile/src/features/files/FileMarkdownPreview.tsx", | |
| 139 | "apps/mobile/src/features/files/SourceFileSurface.tsx", | |
| 140 | "apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx", | |
| 141 | "apps/mobile/src/features/files/thread-file-navigator-pane.tsx", | |
| 142 | "apps/mobile/src/features/home/HomeHeader.tsx", | |
| 143 | "apps/mobile/src/features/review/ReviewSheet.tsx", | |
| 144 | "apps/mobile/src/features/review/useNativeReviewDiffBridge.ts", | |
| 145 | "apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx", | |
| 146 | "apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx", | |
| 147 | "apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx", | |
| 148 | "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx", | |
| 149 | "apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx", | |
| 150 | "apps/mobile/src/features/threads/NewTaskDraftScreen.tsx", | |
| 151 | "apps/mobile/src/features/threads/ThreadComposer.tsx", | |
| 152 | "apps/mobile/src/features/threads/ThreadFeed.tsx", | |
| 153 | "apps/mobile/src/features/threads/ThreadSettingsSheet.tsx", | |
| 154 | "apps/mobile/src/features/threads/git/GitOverviewSheet.tsx", | |
| 155 | "apps/mobile/src/features/threads/thread-list-items.tsx", | |
| 156 | "apps/mobile/src/features/threads/thread-list-v2-items.tsx", | |
| 157 | "apps/mobile/src/lib/useMobileNavigationTheme.ts", | |
| 158 | "apps/mobile/src/native/T3ComposerEditor.ios.tsx", | |
| 159 | "apps/mobile/src/native/T3ComposerEditor.native.tsx", | |
| 160 | ], | |
| 161 | rules: { | |
| 162 | "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }], | |
| 163 | }, | |
| 164 | }, | |
| 165 | // Legacy manual Effect runners tracked as debt: no net-new occurrences. | |
| 166 | // Lower a ceiling when you migrate a file, and delete its entry at zero. | |
| 167 | ...Object.entries({ | |
| 168 | "apps/server/src/orchestration/Layers/CheckpointReactor.test.ts": 42, | |
| 169 | "apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts": 5, | |
| 170 | "apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts": 4, | |
| 171 | "apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts": 66, | |
| 172 | "apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts": 29, | |
| 173 | "apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts": 2, | |
| 174 | "apps/server/src/orchestration/commandInvariants.test.ts": 5, | |
| 175 | "apps/server/src/orchestration/projector.test.ts": 20, | |
| 176 | "apps/server/src/provider/Layers/CodexAdapter.test.ts": 1, | |
| 177 | "apps/server/src/provider/Layers/CodexSessionRuntime.test.ts": 5, | |
| 178 | "apps/server/src/provider/Layers/CursorAdapter.test.ts": 1, | |
| 179 | "apps/server/src/provider/Layers/CursorProvider.test.ts": 1, | |
| 180 | "apps/server/src/provider/Layers/ProviderService.test.ts": 2, | |
| 181 | "apps/server/src/provider/Layers/ProviderSessionReaper.test.ts": 12, | |
| 182 | "apps/server/src/provider/acp/CursorAcpSupport.test.ts": 1, | |
| 183 | }).map(([file, maxOccurrences]) => { | |
| 184 | const rule: ["error", { maxOccurrences: number }] = ["error", { maxOccurrences }]; | |
| 185 | return { files: [file], rules: { "t3code/no-manual-effect-runtime-in-tests": rule } }; | |
| 186 | }), | |
| 187 | ], | |
| 126 | 188 | options: { |
| 189 | reportUnusedDisableDirectives: "error", | |
| 127 | 190 | // Revisit once Oxlint's tsgolint path can integrate with @effect/tsgo diagnostics. |
| 128 | 191 | typeAware: false, |
| 129 | 192 | typeCheck: false, |