From e8596af25837e2e0863eb13dfb140dfa5ae7c1b3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 14 Sep 2026 21:29:52 -0400 Subject: [PATCH] test(mobile): pin Project host routing to the declared method, not the identifier prefix The guard matched `githubProject*.request(` sites, so an operation renamed off that prefix left the prefix match empty and the host could go with the rename: renaming `githubProjectFieldUpdate` to `projectFieldUpdate` at its definition and its one call site and deleting `host: activeGitHubProjectHost` from the `github.project.updateItemField` request kept all three tests and `tsc` green, and `host` is optional in the params type so nothing else caught it. Derive the list from the board module by the method each operation declares instead, and scan every product file under `mobile/src` rather than a hand-listed eight, so a site that moves stays covered. Coverage goes from 13 matched sites to 17 across all 16 declared operations, because the old regex also missed the `op\n .request(` form four of them use. An operation that stops being requested at all now fails too. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- ...github-project-host-routing-source.test.ts | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/mobile/src/tasks/github-project-host-routing-source.test.ts b/mobile/src/tasks/github-project-host-routing-source.test.ts index 12a13959f4e..5d05f93bd69 100644 --- a/mobile/src/tasks/github-project-host-routing-source.test.ts +++ b/mobile/src/tasks/github-project-host-routing-source.test.ts @@ -1,7 +1,9 @@ -import { readFileSync } from 'node:fs' +import { readFileSync, readdirSync } from 'node:fs' +import { join, relative, resolve } from 'node:path' import { describe, expect, it } from 'vitest' const readSource = (path: string): string => readFileSync(new URL(path, import.meta.url), 'utf8') +const productRoot = resolve(import.meta.dirname, '..') const source = [ readSource('./use-mobile-tasks-project-loading-actions.tsx'), readSource('./use-mobile-tasks-project-workspace-comment-actions.tsx'), @@ -25,18 +27,52 @@ function sendsMethod(operations: string, operation: string, method: string): boo return offset !== -1 && operations.slice(offset, offset + 400).includes(`method: '${method}'`) } +/** Every product file that could send a board request. Recorder fixtures are not call sites. */ +function productSources(directory: string): string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = join(directory, entry.name) + if (entry.isDirectory()) { + return entry.name === 'test-support' ? [] : productSources(path) + } + return /\.tsx?$/.test(entry.name) && !entry.name.includes('.test.') ? [path] : [] + }) +} + +/** + * The board's operations by the method each declares, never by the `githubProject` identifier + * prefix: renaming an operation off that prefix takes it out of a prefix match, so the rename can + * delete the host with this test still green. The method it sends is what routing follows. + */ +function projectOperations(): string[] { + const declarations = [...boardOperations.matchAll(/export const (\w+) =/g)] + return declarations + .filter((declaration, index) => + boardOperations + .slice(declaration.index, declarations[index + 1]?.index ?? boardOperations.length) + .includes("method: 'github.project.") + ) + .map((declaration) => declaration[1]!) +} + describe('mobile GitHub Project host routing boundary', () => { it('host-qualifies every Project RPC request', () => { - const calls = [...source.matchAll(/\b(githubProject[A-Za-z]+)\.request\(/g)] - expect(calls.length).toBeGreaterThan(10) - for (const call of calls) { - const request = source.slice(call.index, call.index + 700) - expect(request, `${call[1]} must carry a host`).toMatch(/\bhost\s*:/) - expect( - boardOperations.includes(`export const ${call[1]} =`), - `${call[1]} must be a declared Project operation` - ).toBe(true) + const operations = projectOperations() + expect(operations.length).toBeGreaterThan(10) + const unrouted: string[] = [] + const wired = new Set() + for (const path of productSources(productRoot)) { + const contents = readFileSync(path, 'utf8') + for (const operation of operations) { + for (const call of contents.matchAll(new RegExp(`\\b${operation}\\s*\\.request\\(`, 'g'))) { + wired.add(operation) + if (!/\bhost\s*:/.test(contents.slice(call.index, call.index + 700))) { + unrouted.push(`${relative(productRoot, path)} sends ${operation} with no host`) + } + } + } } + expect(unrouted).toEqual([]) + expect(operations.filter((operation) => !wired.has(operation))).toEqual([]) }) it('pins Project-row PR actions to the row repository identity', () => {