Skip to content

test(web-shared): make web-shared independent of apps/web - #7058

Closed
chrarnoldus wants to merge 6 commits into
web-shared-specifierfrom
web-shared-standalone-tests
Closed

chrarnoldus wants to merge 6 commits into
web-shared-specifierfrom
web-shared-standalone-tests

Conversation

@chrarnoldus

Copy link
Copy Markdown
Contributor

Summary

Second preparation PR for #7051, stacked on #7056. It makes packages/web-shared independent of apps/web, tests included, while the @/ fallback for consumers still exists. These are the web-shared test-isolation commits from #7051, replayed before the consumer-wide specifier rewrite instead of after it.

  • Shared test helpers and constants move into web-shared.
    • The apps/web test helpers that web-shared tests use move with git mv to packages/web-shared/src/tests/helpers: user, organization, microdollar-usage, approval, payment-method, openrouter-models and fake-r2.
    • So do the self-contained constant modules they need, to the same paths: lib/auth/constants, lib/organizations/constants and lib/ai-gateway/latest-model-aliases.
    • So does lib/balanceCache, which only depends on web-shared modules.
    • Every importer now uses @kilocode/web-shared/<path>, whether it used an @/ alias or a relative path before.
  • Test cases that exercise apps/web code move into apps/web. Every test case is kept; only its file changes.
    • kilo-pass-org/consumption.test.ts and model-stats/model-stats-cache.test.ts move whole.
    • The removeUserFromOrganization, next-auth-options, passkey, platform-admin-grant, getProfileRedirectPath, subscription-accounting and control-plane-budget cases move next to the apps/web code they drive.
  • Boundary. packages/web-shared/tsconfig.json and tsconfig.lib.json no longer map @/. pnpm --filter @kilocode/web-shared typecheck now checks tests as well, with tsconfig.json. src/tests stays out of the runtime tsconfig.lib.json check and the Storybook program. An oxlint rule rejects @/ and relative apps/web imports anywhere in the package.
  • Docs. packages/web-shared/AGENTS.md says that nothing in web-shared imports apps/web, and where shared test helpers and apps/web-dependent test cases live.

Script

The moves use the script below. It resolves every @/ and relative module specifier with ts.resolveModuleName against the tsconfig that owns the importing file, before the move. It rewrites specifiers that resolve to a moved file, then runs git mv. Afterwards, the moved files' own @/ imports of web-shared modules were converted with the --only packages/web-shared codemod from #7056, then oxfmt ran.

Commit Files Specifiers
Helpers and constants (10 files moved) 335 405 retargeted in 326 files, plus 8 @/ self-imports in 4 moved files
balanceCache (1 file moved) 9 8 retargeted, plus 2 @/ self-imports

The moved files and the test-case moves match the corresponding commits on #7051. The one exception is a comment in fake-r2.helper.ts, which the docs commit updates.

Verification

  • cd apps/web && pnpm exec tsgo --noEmit, pnpm --filter @kilocode/web-shared typecheck (now tsconfig.lib.json and tsconfig.json), and the apps/ai-gateway and apps/storybook typechecks: all pass
  • pnpm --filter @kilocode/trpc build: dist/index.d.ts and dist/mobile.d.ts are byte-identical to the main build. apps/mobile typecheck passes
  • oxlint on apps/web/src, packages/web-shared/src and apps/ai-gateway/src: no findings, including under the new web-shared rule. oxfmt --list-different and git diff --check are clean
  • Ran the 13 test files the test-case moves touch, in both apps, against the local test database: 376 passed, 2 skipped (unchanged skips). Ran services/cloud-agent-next report-consumer.glanceable.test.ts, which loads apps/web code with vi.mock: 14 passed. CI runs the full suites
  • cd apps/web && NODE_ENV=test pnpm exec jest --listTests: lists all 166 remaining packages/web-shared tests (168 before, minus the 2 files that moved to apps/web)

Visual Changes

N/A

Reviewer Notes

Move script source (move-to-web-shared.mjs, run as node move-to-web-shared.mjs . <apps/web/src/...>…)
// Moves files from apps/web/src to the same path under packages/web-shared/src
// with `git mv`, and points every module specifier that resolved to a moved
// file at `@kilocode/web-shared/<path>`. Only string literals in
// module-specifier positions change.
//
// Usage: node move-to-web-shared.mjs <repo-root> <apps/web/src/...> [...]
//
// Specifiers are resolved before the move with ts.resolveModuleName against
// the tsconfig that owns the importing file, so `@/` aliases and relative
// specifiers are both covered.

import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';

const repoRoot = path.resolve(process.argv[2]);
const moved = process.argv.slice(3);
const require = createRequire(path.join(repoRoot, 'package.json'));
const ts = require('typescript');

const WEB_SRC = 'apps/web/src/';
const SHARED_SRC = 'packages/web-shared/src/';
const NEW_PREFIX = '@kilocode/web-shared/';

const destinations = new Map();
for (const file of moved) {
  if (!file.startsWith(WEB_SRC)) throw new Error(`${file} is not under ${WEB_SRC}`);
  const target = SHARED_SRC + file.slice(WEB_SRC.length);
  const specifier = NEW_PREFIX + target.slice(SHARED_SRC.length).replace(/(\/index)?\.tsx?$/, '');
  destinations.set(path.join(repoRoot, file), specifier);
}

// First match wins.
const PROJECTS = [
  ['apps/ai-gateway/', 'apps/ai-gateway/tsconfig.json'],
  ['apps/storybook/', 'apps/storybook/tsconfig.json'],
  ['packages/trpc/', 'packages/trpc/tsconfig.json'],
  ['packages/web-shared/', 'packages/web-shared/tsconfig.json'],
  ['', 'apps/web/tsconfig.json'],
];
const SCAN_DIRS = ['apps/web', 'apps/ai-gateway', 'apps/storybook', 'packages', 'services'];
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);

const options = new Map();
function compilerOptionsFor(relFile) {
  const tsconfig = PROJECTS.find(([prefix]) => relFile.startsWith(prefix))[1];
  if (!options.has(tsconfig)) {
    const parsed = ts.getParsedCommandLineOfConfigFile(path.join(repoRoot, tsconfig), {}, {
      ...ts.sys,
      onUnRecoverableConfigFileDiagnostic: diagnostic => {
        throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
      },
    });
    options.set(tsconfig, parsed.options);
  }
  return options.get(tsconfig);
}

function isStringLike(node) {
  return node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node));
}

function collectSpecifierNodes(sourceFile) {
  const found = [];
  const visit = node => {
    if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) {
      if (isStringLike(node.moduleSpecifier)) found.push(node.moduleSpecifier);
    } else if (
      ts.isImportEqualsDeclaration(node) &&
      ts.isExternalModuleReference(node.moduleReference) &&
      isStringLike(node.moduleReference.expression)
    ) {
      found.push(node.moduleReference.expression);
    } else if (ts.isImportTypeNode(node)) {
      const literal = ts.isLiteralTypeNode(node.argument) ? node.argument.literal : undefined;
      if (isStringLike(literal)) found.push(literal);
    } else if (ts.isCallExpression(node) && isStringLike(node.arguments[0])) {
      const callee = node.expression;
      const isModuleCall =
        callee.kind === ts.SyntaxKind.ImportKeyword ||
        (ts.isIdentifier(callee) && callee.text === 'require') ||
        (ts.isPropertyAccessExpression(callee) &&
          ts.isIdentifier(callee.expression) &&
          ['jest', 'vi', 'require'].includes(callee.expression.text));
      if (isModuleCall) found.push(node.arguments[0]);
    }
    ts.forEachChild(node, visit);
  };
  visit(sourceFile);
  return found;
}

function scriptKindFor(file) {
  const extension = path.extname(file);
  if (extension === '.tsx') return ts.ScriptKind.TSX;
  if (extension === '.jsx') return ts.ScriptKind.JSX;
  if (['.js', '.mjs', '.cjs'].includes(extension)) return ts.ScriptKind.JS;
  return ts.ScriptKind.TS;
}

const files = execFileSync('git', ['ls-files', '-z', '--', ...SCAN_DIRS], {
  cwd: repoRoot,
  encoding: 'utf8',
  maxBuffer: 64 * 1024 * 1024,
})
  .split('\0')
  .filter(file => file && SOURCE_EXTENSIONS.has(path.extname(file)));

let changedFiles = 0;
let changedSpecifiers = 0;
for (const relFile of files) {
  const absFile = path.join(repoRoot, relFile);
  const text = readFileSync(absFile, 'utf8');
  const sourceFile = ts.createSourceFile(absFile, text, ts.ScriptTarget.Latest, true, scriptKindFor(relFile));
  const edits = [];
  for (const node of collectSpecifierNodes(sourceFile)) {
    const specifier = node.text;
    if (!specifier.startsWith('@/') && !specifier.startsWith('.')) continue;
    const resolved = ts.resolveModuleName(specifier, absFile, compilerOptionsFor(relFile), ts.sys)
      .resolvedModule?.resolvedFileName;
    const replacement = resolved && destinations.get(resolved);
    if (!replacement) continue;
    const start = node.getStart(sourceFile) + 1;
    edits.push({ start, end: start + specifier.length, replacement });
  }
  if (edits.length === 0) continue;
  let next = text;
  for (const edit of edits.sort((a, b) => b.start - a.start)) {
    next = next.slice(0, edit.start) + edit.replacement + next.slice(edit.end);
  }
  writeFileSync(absFile, next);
  changedFiles += 1;
  changedSpecifiers += edits.length;
}

for (const file of moved) {
  const target = SHARED_SRC + file.slice(WEB_SRC.length);
  execFileSync('mkdir', ['-p', path.dirname(path.join(repoRoot, target))]);
  execFileSync('git', ['mv', file, target], { cwd: repoRoot });
}

console.log(JSON.stringify({ moved: moved.length, changedFiles, changedSpecifiers }, null, 2));

Test helpers are about to live in packages/web-shared/src/tests; exclude
them from tsconfig.lib.json and Storybook like apps/web/src/tests.
git mv the apps/web test helpers that web-shared tests use (user,
organization, microdollar-usage, approval, payment-method,
openrouter-models, fake-r2) to packages/web-shared/src/tests/helpers, and
the self-contained constant modules they or web-shared tests need
(lib/auth/constants, lib/organizations/constants,
lib/ai-gateway/latest-model-aliases) to the same paths in web-shared.

Script output only, followed by oxfmt: every specifier that resolved to a
moved file now uses @kilocode/web-shared/<path> (405 specifiers in 326
files), and the moved files' own @/ imports of web-shared modules use the
package specifier (8 specifiers in 4 files).
git mv apps/web/src/lib/balanceCache.ts, which only depends on web-shared
modules and is used by web-shared tests, and point its importers at
@kilocode/web-shared/lib/balanceCache (script output, followed by oxfmt).
…ps/web

Every test case is kept; only its file changes.

- Whole files: kilo-pass-org/consumption.test.ts (its fixtures use the
  apps/web Kilo Pass org service) and model-stats/model-stats-cache.test.ts
  (drives the apps/web /api/models/stats routes) move to the same paths in
  apps/web.
- removeUserFromOrganization cases, and the organizations and usage cases
  that use it, move to organization-member-removal(-usage).test.ts.
- next-auth-options, passkey and platform-admin-grant cases from
  user/server.test.ts move to user/next-auth-options.test.ts, and the
  getProfileRedirectPath cases to user/profile-redirect-path.test.ts.
- The subscription-accounting cases from kilo-pass/issuance.test.ts move
  to kilo-pass/subscription-accounting.test.ts; their shared
  createTestSubscription fixture becomes tests/helpers/kilo-pass.helper.ts.
- The upstream-budget assertion from trpc/control-plane-budget.test.ts
  moves to bounded-service-fetch.test.ts.
packages/web-shared/tsconfig.json and tsconfig.lib.json no longer map @/,
so neither runtime code nor tests can import apps/web through it, and
`pnpm --filter @kilocode/web-shared typecheck` now also typechecks the
tests with tsconfig.json. An oxlint rule rejects @/ and relative apps/web
imports anywhere in the package.
@kilo-code-bot

kilo-code-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (357 files)
  • .oxlintrc.json - new packages/web-shared import-boundary rule; pattern grammar is valid and leaves @kilocode/* untouched
  • packages/web-shared/tsconfig.json, tsconfig.lib.json, package.json, AGENTS.md - removes the @/* mapping, adds the tests program to typecheck
  • apps/storybook/tsconfig.json - excludes the moved packages/web-shared/src/tests/** from the Storybook program
  • packages/web-shared/src/lib/balanceCache.ts, lib/auth/constants.ts, lib/organizations/constants.ts, lib/ai-gateway/latest-model-aliases.ts - moved modules, import-specifier changes only
  • packages/web-shared/src/tests/helpers/* - moved test helpers plus new kilo-pass.helper.ts (used by issuance.test.ts and subscription-accounting.test.ts)
  • apps/web/src/lib/kilo-pass-org/consumption.test.ts, apps/web/src/lib/model-stats/model-stats-cache.test.ts - moved whole; imports retargeted correctly
  • New apps/web test cases: organization-member-removal*.test.ts, subscription-accounting.test.ts, next-auth-options.test.ts, profile-redirect-path.test.ts
  • ~330 files under apps/web/src and packages/web-shared/src - mechanical @/ and relative specifier rewrites; no logic changes found

Verified: no duplicate paths across apps/web/src and packages/web-shared/src; no remaining @/ module specifiers in packages/web-shared; all rewritten @kilocode/web-shared/... targets resolve; moved-module diffs are import-only; no stale imports of moved modules outside the rewritten trees.


Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch web-shared-specifier

@chrarnoldus chrarnoldus closed this Oct 2, 2026
@chrarnoldus
chrarnoldus deleted the web-shared-standalone-tests branch October 2, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant