From c07b19c4c5102f2f4205d5ef851b920dc0098391 Mon Sep 17 00:00:00 2001 From: k-atana-agent Date: Mon, 26 Oct 2026 02:06:41 +0800 Subject: [PATCH 1/4] ci: run tsc and bun test (and check goldens are in sync) on every PR The only workflow was Rust. tsc, bun test and the TS reference behind the Rust golden fixtures were never run in CI. Docker integration tests are skipped in CI via FSC_SKIP_DOCKER_TESTS=1 (shared helper test/helpers/docker.ts replaces six copies of the same availability check). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016FSVfwwP6TVxGLUWatWM53 --- .github/workflows/typescript.yml | 31 +++++++++++++++++++ .../core/test/copyFilesToContainer.test.ts | 7 +---- packages/core/test/dockerInstance.test.ts | 7 +---- .../core/test/dockerInstanceFileRead.test.ts | 7 +---- packages/core/test/helpers/docker.ts | 14 +++++++++ packages/core/test/runCommandAsync.test.ts | 7 +---- .../core/test/taskSolverClaudeCode.test.ts | 7 +---- packages/core/test/taskSolverGemini.test.ts | 7 +---- 8 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/typescript.yml create mode 100644 packages/core/test/helpers/docker.ts diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml new file mode 100644 index 0000000..9e5f9b2 --- /dev/null +++ b/.github/workflows/typescript.yml @@ -0,0 +1,31 @@ +name: typescript +on: + push: + branches: [main] + pull_request: +jobs: + check: + runs-on: ubuntu-latest + env: + FSC_SKIP_DOCKER_TESTS: "1" + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: git identity (tests create commits) + run: | + git config --global user.name ci + git config --global user.email ci@example.com + git config --global init.defaultBranch main + - run: bun install --frozen-lockfile + - name: typecheck + run: bun run typecheck + - name: tests + working-directory: packages/core + run: bun run test + - name: golden fixtures are in sync with the TypeScript reference + run: | + bun script/gen-golden.ts + bun script/gen-merge-golden.ts + git diff --exit-code -- crates/*/tests/golden diff --git a/packages/core/test/copyFilesToContainer.test.ts b/packages/core/test/copyFilesToContainer.test.ts index 6fe8386..deeffb9 100644 --- a/packages/core/test/copyFilesToContainer.test.ts +++ b/packages/core/test/copyFilesToContainer.test.ts @@ -3,12 +3,7 @@ import { spawnSync } from "bun"; import { DockerInstance } from "../src/dockerInstance"; import { RunStatus } from "../src/taskExecutor"; -const dockerAvailable = (() => { - try { - const r = spawnSync({ cmd: ["docker", "info"], timeout: 5000 }); - return r.exitCode === 0; - } catch { return false; } -})(); +import { dockerAvailable } from "./helpers/docker"; const testDocker = test.skipIf(!dockerAvailable); diff --git a/packages/core/test/dockerInstance.test.ts b/packages/core/test/dockerInstance.test.ts index a3ebcee..87288b8 100644 --- a/packages/core/test/dockerInstance.test.ts +++ b/packages/core/test/dockerInstance.test.ts @@ -2,12 +2,7 @@ import { expect, test, describe } from "bun:test"; import { spawnSync } from "bun"; import { DockerInstance, DockerRunStatus } from "../src/dockerInstance"; -const dockerAvailable = (() => { - try { - const r = spawnSync({ cmd: ["docker", "info"], timeout: 5000 }); - return r.exitCode === 0; - } catch { return false; } -})(); +import { dockerAvailable } from "./helpers/docker"; const testDocker = test.skipIf(!dockerAvailable); diff --git a/packages/core/test/dockerInstanceFileRead.test.ts b/packages/core/test/dockerInstanceFileRead.test.ts index cebe925..6017f2a 100644 --- a/packages/core/test/dockerInstanceFileRead.test.ts +++ b/packages/core/test/dockerInstanceFileRead.test.ts @@ -2,12 +2,7 @@ import { expect, test, describe } from "bun:test"; import { spawnSync } from "bun"; import { DockerInstance } from "../src/dockerInstance"; -const dockerAvailable = (() => { - try { - const r = spawnSync({ cmd: ["docker", "info"], timeout: 5000 }); - return r.exitCode === 0; - } catch { return false; } -})(); +import { dockerAvailable } from "./helpers/docker"; const testDocker = test.skipIf(!dockerAvailable); diff --git a/packages/core/test/helpers/docker.ts b/packages/core/test/helpers/docker.ts new file mode 100644 index 0000000..f02216a --- /dev/null +++ b/packages/core/test/helpers/docker.ts @@ -0,0 +1,14 @@ +import { spawnSync } from "bun"; + +/** + * True when Docker integration tests can run. Set FSC_SKIP_DOCKER_TESTS=1 to skip them even if + * Docker is installed (CI does this: the native pipeline does not need Docker). + */ +export const dockerAvailable: boolean = (() => { + if (process.env.FSC_SKIP_DOCKER_TESTS === "1") return false; + try { + return spawnSync({ cmd: ["docker", "info"], timeout: 5000 }).exitCode === 0; + } catch { + return false; + } +})(); diff --git a/packages/core/test/runCommandAsync.test.ts b/packages/core/test/runCommandAsync.test.ts index 8dd153f..17dc9d7 100644 --- a/packages/core/test/runCommandAsync.test.ts +++ b/packages/core/test/runCommandAsync.test.ts @@ -2,12 +2,7 @@ import { expect, test, describe, beforeAll, afterAll, jest } from 'bun:test'; import { spawnSync } from "bun"; import { DockerInstance, DockerRunStatus } from "../src/dockerInstance"; -const dockerAvailable = (() => { - try { - const r = spawnSync({ cmd: ["docker", "info"], timeout: 5000 }); - return r.exitCode === 0; - } catch { return false; } -})(); +import { dockerAvailable } from "./helpers/docker"; const describeDocker = dockerAvailable ? describe : describe.skip; diff --git a/packages/core/test/taskSolverClaudeCode.test.ts b/packages/core/test/taskSolverClaudeCode.test.ts index 78a6d27..3d002cf 100644 --- a/packages/core/test/taskSolverClaudeCode.test.ts +++ b/packages/core/test/taskSolverClaudeCode.test.ts @@ -8,12 +8,7 @@ import { WorkStyle } from "../src/workStyle"; import {CLAUDE_CODE_API_KEY, CLAUDE_CODE_API_URL} from "./apiKeySetup"; import { spawnSync } from "bun"; -const dockerAvailable = (() => { - try { - const r = spawnSync({ cmd: ["docker", "info"], timeout: 5000 }); - return r.exitCode === 0; - } catch { return false; } -})(); +import { dockerAvailable } from "./helpers/docker"; const testDocker = test.skipIf(!dockerAvailable || !process.env.ANTHROPIC_API_KEY); diff --git a/packages/core/test/taskSolverGemini.test.ts b/packages/core/test/taskSolverGemini.test.ts index b4ec37f..c66aa97 100644 --- a/packages/core/test/taskSolverGemini.test.ts +++ b/packages/core/test/taskSolverGemini.test.ts @@ -8,12 +8,7 @@ import { WorkStyle } from "../src/workStyle"; import {GEMINI_API_KEY} from "./apiKeySetup"; import { spawnSync } from "bun"; -const dockerAvailable = (() => { - try { - const r = spawnSync({ cmd: ["docker", "info"], timeout: 5000 }); - return r.exitCode === 0; - } catch { return false; } -})(); +import { dockerAvailable } from "./helpers/docker"; const testDocker = test.skipIf(!dockerAvailable || !process.env.GEMINI_API_KEY); From 12431ce2db95f73fc47114efa50d0e872f64a361 Mon Sep 17 00:00:00 2001 From: k-atana-agent Date: Mon, 26 Oct 2026 02:09:21 +0800 Subject: [PATCH 2/4] ci: bun install --ignore-scripts (tree-sitter native build fails on current Node) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016FSVfwwP6TVxGLUWatWM53 --- .github/workflows/typescript.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 9e5f9b2..a502189 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -18,7 +18,9 @@ jobs: git config --global user.name ci git config --global user.email ci@example.com git config --global init.defaultBranch main - - run: bun install --frozen-lockfile + # The tree-sitter native build (node-gyp) fails on current Node, locally and on the runner, + # and nothing under src/ or test/ imports it. + - run: bun install --frozen-lockfile --ignore-scripts - name: typecheck run: bun run typecheck - name: tests From c35b487809e22f89c7e299b90da3734c0e8ade3c Mon Sep 17 00:00:00 2001 From: k-atana-agent Date: Mon, 26 Oct 2026 02:11:57 +0800 Subject: [PATCH 3/4] test: stop the child_process mock leaking across test files; full-history checkout in CI The visualization test replaced child_process for the whole process and never restored it, so CodeCommitter tests that ran after it saw execSync() === 'abc123def456' (found by the first CI run; file order differs from local). Coupling tests read the repo's own history, which a depth-1 checkout does not have. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016FSVfwwP6TVxGLUWatWM53 --- .github/workflows/typescript.yml | 2 ++ .../core/test/codeCommitterReportVisualization.test.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index a502189..5ffffa7 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -10,6 +10,8 @@ jobs: FSC_SKIP_DOCKER_TESTS: "1" steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # the co-change coupling tests read the repository's own git history - uses: oven-sh/setup-bun@v2 with: bun-version: latest diff --git a/packages/core/test/codeCommitterReportVisualization.test.ts b/packages/core/test/codeCommitterReportVisualization.test.ts index cec7220..eaec92b 100644 --- a/packages/core/test/codeCommitterReportVisualization.test.ts +++ b/packages/core/test/codeCommitterReportVisualization.test.ts @@ -1,8 +1,16 @@ -import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test"; +import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test"; +import * as realChildProcess from "child_process"; import { CodeCommitter } from "../src/codeCommitter"; import type { TaskResult } from "../src/task"; import { TaskStatus } from "../src/task"; +// mock.module() replaces the module for the whole process and is not undone by mock.restore(). +// Without this, every test file that runs after this one sees execSync() === 'abc123def456'. +const originalChildProcess = { ...realChildProcess }; +afterAll(() => { + mock.module('child_process', () => originalChildProcess); +}); + describe('CodeCommitter Beautiful Report Visualization Demo', () => { let mockTaskResults: TaskResult[]; From 38457e5cfb96c6f2a2c980736b07dcfc01795e47 Mon Sep 17 00:00:00 2001 From: k-atana-agent Date: Mon, 26 Oct 2026 02:15:02 +0800 Subject: [PATCH 4/4] test: coupling/replay tests use the repo they live in, not a hard-coded D: path Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016FSVfwwP6TVxGLUWatWM53 --- packages/core/test/salacia/coupling.test.ts | 5 ++++- packages/core/test/salacia/replay-real-commits.test.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/test/salacia/coupling.test.ts b/packages/core/test/salacia/coupling.test.ts index 34909c5..e180580 100644 --- a/packages/core/test/salacia/coupling.test.ts +++ b/packages/core/test/salacia/coupling.test.ts @@ -1,7 +1,10 @@ import { describe, test, expect } from "bun:test"; import { getCoChangedFiles, expandWithCoupling } from "../../src/salacia/coupling"; -const REPO = "D:/projects/full-self-coding"; +import { resolve } from "path"; + +// The repository this test file lives in (test/salacia -> packages/core -> packages -> repo root). +const REPO = resolve(import.meta.dir, "..", "..", "..", ".."); describe("getCoChangedFiles", () => { test("returns co-changed files for scheduler.ts", () => { diff --git a/packages/core/test/salacia/replay-real-commits.test.ts b/packages/core/test/salacia/replay-real-commits.test.ts index 5e1f3c7..f40c909 100644 --- a/packages/core/test/salacia/replay-real-commits.test.ts +++ b/packages/core/test/salacia/replay-real-commits.test.ts @@ -1,10 +1,12 @@ import { describe, test, expect } from "bun:test"; +import { resolve } from "path"; import { gitExec } from "../../src/execution/gitExec"; import { generateContract } from "../../src/salacia/contract"; import { detectDrift, isFatalDrift, exceedsThreshold } from "../../src/salacia/drift"; import type { Task } from "../../src/task"; -const REPO = "D:/projects/full-self-coding"; +// The repository this test file lives in (test/salacia -> packages/core -> packages -> repo root). +const REPO = resolve(import.meta.dir, "..", "..", "..", ".."); function getRecentCommits(n: number): Array<{ hash: string; message: string; files: string[] }> { const log = gitExec(["log", "--oneline", `-${n}`], REPO);