Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ jobs:
# their shared test/unit/support/mcp-cli-harness.ts import nothing but node:* builtins + vitest,
# and mcp-discovery.test.ts spawns packages/gittensory-mcp/bin/gittensory-mcp.js as a real
# subprocess via StdioClientTransport -- none of the 6 ever load root src/ in-process. This mirrors
# the `mcp` filter's own established trust boundary above (a self-contained package build).
# the `mcp` filter's own established trust boundary above (a self-contained package build). They are
# NOT self-contained w.r.t. packages/gittensory-engine/**, though: gittensory-mcp now has a real
# dependency on @jsonbored/gittensory-engine (isTestFile/isCodeFile), which the bin subprocess and
# local-branch.js load at runtime -- hence that path below, alongside the mcp package's own.
# test/unit/mcp-output-schemas.test.ts is DELIBERATELY NOT in this filter: unlike the other 6, it
# imports src/mcp/server.ts in-process, and server.ts alone directly imports ~40 other src/ modules
# (src/github/app.ts, src/signals/slop.ts, src/settings/autonomy.ts, src/orb/analytics.ts, and
Expand All @@ -121,6 +124,7 @@ jobs:
- 'migrations/**'
- '.github/workflows/**'
- 'packages/gittensory-mcp/**'
- 'packages/gittensory-engine/**'
- 'test/helpers/**'
- 'test/unit/mcp-cli-*.test.ts'
- 'test/unit/mcp-discovery.test.ts'
Expand Down Expand Up @@ -396,11 +400,14 @@ jobs:
- name: Worker runtime tests
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
run: npm run test:workers
# gittensory-mcp now depends on @jsonbored/gittensory-engine for real (isTestFile/isCodeFile), so a
# PR that only touches packages/gittensory-engine/** must also rebuild + pack-check the mcp package,
# not just the mcp filter's own (deliberately narrower) path list.
- name: Build MCP
if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }}
if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }}
run: npm run build:mcp
- name: MCP package check
if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }}
if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.engine == 'true' }}
run: npm run test:mcp-pack
- name: Build miner CLI
if: ${{ github.event_name == 'push' || needs.changes.outputs.miner == 'true' }}
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 3 additions & 30 deletions packages/gittensory-mcp/lib/local-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { execFileSync } from "node:child_process";
import { realpathSync } from "node:fs";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { isCodeFile, isTestPath as isTestFile } from "@jsonbored/gittensory-engine/signals/test-evidence";

export { isCodeFile, isTestFile };

const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");

Expand Down Expand Up @@ -587,36 +590,6 @@ function firstCommitTitle(messages) {
return messages.find((message) => message.trim().length > 0)?.split("\n")[0]?.trim();
}

// Must mirror the canonical server-side matcher in src/signals/test-evidence.ts (isTestPath); the
// server local-branch analysis delegates to it, so this client copy classifies the same diff and must
// agree. The last two branches (Cypress/e2e `*.cy.*`/`*.e2e.*` and `__snapshots__/`) were missing here,
// so a changed `Button.cy.ts` / snapshot file was misclassified as source (isCodeFile) and dropped from
// testFiles, inflating source-line/token counts and under-reporting test evidence in the local packet.
export function isTestFile(file) {
return (
/(^|\/)(test|tests|spec|__tests__)\//i.test(file) ||
/(^|\/)src\/test\//i.test(file) ||
/(^|\/)[^/]+_test\.(go|py|rb|dart)$/i.test(file) || // Dart/Flutter `foo_test.dart` co-located with source
/(^|\/)test_[^/]*\.py$/i.test(file) || // pytest's default `test_*.py` prefix convention (the suffix rule above only catches `*_test.py`)
/(^|\/)[^/]+_spec\.rb$/i.test(file) ||
/\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs)$/i.test(file) ||
/(^|\/)[^/]+\.(cy|e2e)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i.test(file) ||
// JVM / C# / Swift / PHP `SomethingTest(s)`/`SomethingSpec` class-suffix convention (JUnit, Kotlin/ScalaTest,
// Spock, xUnit/NUnit, XCTest, PHPUnit/PHPSpec). Case-sensitive on the PascalCase suffix so it can't false-positive on words
// that merely end in "test"/"spec" (Latest.java, Contest.cs, manifest.scala, Latest.php).
/(^|\/)\w*(Tests?|Spec)\.(java|kt|kts|scala|cs|swift|groovy|php)$/.test(file) ||
/(^|\/)__snapshots__\//i.test(file)
);
}

function isGeneratedCodeFile(file) {
return /\.(g|freezed|gr)\.dart$/i.test(file);
}

export function isCodeFile(file) {
return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file);
}

function numberValue(value) {
const number = Number(value);
return Number.isFinite(number) ? number : undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/gittensory-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"build": "node --check bin/gittensory-mcp.js && node --check lib/local-branch.js && node --check scripts/gittensor-score-preview.mjs"
},
"dependencies": {
"@jsonbored/gittensory-engine": "^0.1.0",
"@modelcontextprotocol/sdk": "1.29.0",
"zod": "^4.4.3"
},
Expand Down
25 changes: 1 addition & 24 deletions packages/gittensory-mcp/scripts/gittensor-score-preview.mjs
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
#!/usr/bin/env node
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

function isTestFile(file) {
return (
/(^|\/)(test|tests|spec|__tests__)\//i.test(file) ||
/(^|\/)src\/test\//i.test(file) ||
/(^|\/)[^/]+_test\.(go|py|rb|dart)$/i.test(file) || // Dart/Flutter `foo_test.dart` co-located with source
/(^|\/)test_[^/]*\.py$/i.test(file) || // pytest's default `test_*.py` prefix (the suffix rule above only catches `*_test.py`)
/(^|\/)[^/]+_spec\.rb$/i.test(file) ||
/\.(test|spec)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs)$/i.test(file) ||
/(^|\/)[^/]+\.(cy|e2e)\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i.test(file) ||
// JVM/.NET/Swift/PHP PascalCase test-class suffix (case-sensitive, matching the
// signal classifiers) so C#/Swift/Groovy/PHP tests aren't counted as source.
/(^|\/)\w*(Tests?|Spec)\.(java|kt|kts|scala|cs|swift|groovy|php)$/.test(file) ||
/(^|\/)__snapshots__\//i.test(file)
);
}

function isGeneratedCodeFile(file) {
return /\.(g|freezed|gr)\.dart$/i.test(file);
}

function isCodeFile(file) {
return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|py|rb|rs|kt|scala|java|go|sql|cs|swift|groovy|php|cpp|cc|c|h|hpp|m|vue|svelte|astro|dart)$/i.test(file) && !isTestFile(file) && !isGeneratedCodeFile(file);
}
import { isCodeFile, isTestPath as isTestFile } from "@jsonbored/gittensory-engine/signals/test-evidence";

function lineCount(file) {
const additions = Number(file.additions ?? 0);
Expand Down
Loading