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
140 changes: 1 addition & 139 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
quality:
name: Format, Lint, Typecheck, Test, Browser Test, Build
name: Format, Lint, Typecheck, Test, Build
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
Expand All @@ -25,24 +25,6 @@ jobs:
with:
node-version-file: package.json

- name: Cache Bun and Turbo
uses: actions/cache@v5
with:
path: |
~/.bun/install/cache
.turbo
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}-${{ hashFiles('turbo.json') }}
restore-keys: |
${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}-

- name: Cache Playwright browsers
uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-

- name: Install dependencies
run: bun install --frozen-lockfile

Expand All @@ -65,130 +47,10 @@ jobs:
- name: Test
run: bun run test

# Run the workspace-local playwright binary directly: bunx stalled after
# the chromium download on hosted runners. The step timeout stops any
# remaining apt/download stalls from eating the whole job budget.
- name: Install browser test runtime
timeout-minutes: 15
env:
DEBIAN_FRONTEND: noninteractive
run: |
cd apps/web
./node_modules/.bin/playwright install --with-deps chromium

- name: Browser test (stable)
timeout-minutes: 20
run: bun run --cwd apps/web test:browser:stable

# Pixel/font/layout comparisons are tracked explicitly in
# apps/web/BROWSER_TEST_QUARANTINE.md while their Linux rendering is fixed.
# All untagged browser tests run in the blocking stable step above.
- name: Browser test (Linux geometry quarantine)
continue-on-error: true
timeout-minutes: 10
run: bun run --cwd apps/web test:browser:geometry

- name: Build desktop pipeline
run: bun run build:desktop

- name: Verify preload bundle output
run: |
test -f apps/desktop/dist-electron/preload.js
grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js

windows_process:
name: Windows Process Regression
runs-on: windows-2022
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Verify Windows terminal PTY native dependency under Bun
run: bun scripts/node-pty-smoke.mjs

- name: Test Windows process planning
run: bun run --cwd packages/shared test src/windowsProcess.test.ts

- name: Test Effect Windows process spawn
run: bun run --cwd apps/server test src/windowsProcessEffect.test.ts

- name: Test Windows desktop backend shutdown
run: bun run --cwd apps/desktop test src/backendShutdown.windows.integration.test.ts

# The 0.6.0 wedge hit Windows hardest, and the repair path is built out of
# exactly what Windows treats differently: unlinking a marker, reclaiming
# stranded partials, and renaming a database file that may still be held
# open. Linux-only coverage cannot speak for any of that.
- name: Test migration recovery marker
run: bun run --cwd packages/shared test src/migrationRecovery.test.ts

- name: Test desktop migration recovery
run: bun run --cwd apps/desktop test src/desktopMigrationRecovery.test.ts

- name: Test migration backup and restore
run: bun run --cwd apps/server test src/persistence/MigrationBackup.test.ts

- name: Test migration replay
run: bun run --cwd apps/server test src/persistence/Migrations/MigrationReplay.test.ts

# Released (id, name) migration pairs are recorded in every user's
# effect_sql_migrations table, so renumbering or renaming one corrupts existing
# installs. This job needs the release tags, hence the unshallow checkout; the
# guard itself is dependency-free and skips with a warning if tags are missing.
migration_lineage:
name: Migration Lineage
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json

- name: Verify released migration lineage
run: node scripts/check-migration-lineage.ts

release_smoke:
name: Release Smoke
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: package.json

- name: Install dependencies
run: bun install --frozen-lockfile --ignore-scripts

- name: Verify Synara identity
run: bun run brand:check

- name: Exercise release-only workflow steps
run: node scripts/release-smoke.ts
1 change: 1 addition & 0 deletions .github/workflows/device-helper-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ jobs:
timeout-minutes: 15
env:
DEVELOPER_DIR: ${{ steps.toolchain.outputs.developer_dir }}
DEVICE_HELPER_REQUIRED_FRAMES: 4
run: bun run test:device

# Uploaded even when the probe failed: that JSON names the capability and
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ jobs:
bun-version-file: package.json

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Verify Synara identity
run: bun run brand:check
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Enough time for exactly one coffee and one suspiciously calm compile.
### Why does that long-running timer mention 42 minutes?

It is only a metaphor for persistence.

### Can Forkara help with non-code forks?

Yes. If the fork has a clear owner and a clear next step, we can still model it in our glossary.
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/ForkDenialStage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ describe("ForkDenialStage", () => {
it("renders every stage label in provided order", () => {
const stages = getForkDenialStages({ includeFinalForkState: true });
const markup = renderToStaticMarkup(<ForkDenialStageList stages={stages} />);
const indexes = stages.map((stage) => markup.indexOf(stage.label));
const normalizedMarkup = markup
.replace(/&#39;|&#x27;|&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&amp;/g, "&");
const indexes = stages.map((stage) => normalizedMarkup.indexOf(stage.label));

for (const index of indexes) {
expect(index).toBeGreaterThanOrEqual(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,20 @@ describe("ComposerPendingApprovalPanel", () => {
approval: makeApproval({
requestKind: "permissions",
detail: "Request to adjust LICENSE terms",
permissionProfile: { license: { file: "LICENSE", readable: true }, copyrightOwner: "forkara-team" },
permissionProfile: {
license: { file: "LICENSE", readable: true },
copyrightOwner: "forkara-team",
},
}),
});

try {
await expect.element(
page.getByText(
"License Changer check: confirm legal ownership before proceeding",
),
).toBeInTheDocument();
await expect.element(page.getByRole("link", { name: /Read current LICENSE/u })).toBeInTheDocument();
await expect
.element(page.getByText("License Changer check: confirm legal ownership before proceeding"))
.toBeInTheDocument();
await expect
.element(page.getByRole("link", { name: /Read current LICENSE/u }))
.toBeInTheDocument();
await expect
.element(
page.getByText(
Expand Down
23 changes: 18 additions & 5 deletions apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,18 @@ const KIND_PROMPT: Record<PendingApproval["requestKind"], string> = {

const LICENSE_REFERENCE_URL = "https://github.com/redxzeta/forkara/blob/built-from-scratch/LICENSE";

const LICENSE_RELATED_KEYWORDS = ["license", "copyright", "copyright owner", "license_file", "license file"];
const LICENSE_RELATED_KEYWORDS = [
"license",
"copyright",
"copyright owner",
"license_file",
"license file",
];

function hasLicenseSignals(input: { detail?: string; permissionProfile?: Record<string, unknown> }): boolean {
function hasLicenseSignals(input: {
detail?: string;
permissionProfile?: Record<string, unknown>;
}): boolean {
const detail = input.detail?.toLowerCase() ?? "";
const profile = input.permissionProfile;
if (!detail && !profile) {
Expand All @@ -101,7 +110,11 @@ export const ComposerPendingApprovalPanel = function ComposerPendingApprovalPane
}: ComposerPendingApprovalPanelProps) {
const parsed = parseApprovalDetail(approval.detail);
const licenseProfileHint =
approval.requestKind === "permissions" && hasLicenseSignals({ detail: approval.detail, permissionProfile: approval.permissionProfile });
approval.requestKind === "permissions" &&
hasLicenseSignals({
...(approval.detail ? { detail: approval.detail } : {}),
...(approval.permissionProfile ? { permissionProfile: approval.permissionProfile } : {}),
});
const requestId = approval.requestId;
const actions =
approval.sessionApprovalAvailable === false
Expand Down Expand Up @@ -193,8 +206,8 @@ function ApprovalDetail({
return (
<div className="mt-2">
<p className="mb-1.5 text-[11.5px] leading-snug text-muted-foreground/70">
Changing license terms is not a cosmetic toggle. Verify ownership and repository policy before allowing
this, and keep the existing LICENSE as the source of truth.
Changing license terms is not a cosmetic toggle. Verify ownership and repository policy
before allowing this, and keep the existing LICENSE as the source of truth.
</p>
<a
href={LICENSE_REFERENCE_URL}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";

import { getRepositoryLabel, UPSTREAM_AMNESIA_HINT, UPSTREAM_AMNESIA_LABEL } from "./EnvironmentPanel";
import {
getRepositoryLabel,
UPSTREAM_AMNESIA_HINT,
UPSTREAM_AMNESIA_LABEL,
} from "./EnvironmentPanel";
import { shouldShowStudioFolderRow } from "./EnvironmentPanel.logic";

describe("shouldShowStudioFolderRow", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export function getRepositoryLabel(input: {
}): ReactNode {
if (!input.hideUpstreamRepositoryInfo) {
return (
<span className="truncate">{input.githubRepository?.nameWithOwner ?? "Unknown repository"}</span>
<span className="truncate">
{input.githubRepository?.nameWithOwner ?? "Unknown repository"}
</span>
);
}

Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/lib/forkDenial.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";

import { FORK_DENIAL_FINAL_STAGE, FORK_DENIAL_STAGES, getForkDenialStages, isFinalForkDenialStage } from "./forkDenial";
import {
FORK_DENIAL_FINAL_STAGE,
FORK_DENIAL_STAGES,
getForkDenialStages,
isFinalForkDenialStage,
} from "./forkDenial";

describe("forkDenial", () => {
it("defines the escalation order for fork-denial copy", () => {
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/lib/forkDenial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ export const FORK_DENIAL_STAGES = [
{ id: "fork-isnt-that-bad", label: "The fork isn't that bad" },
] as const;

export const FORK_DENIAL_FINAL_STAGE = { id: "okay-its-a-fork", label: "Okay, it's a fork" } as const;
export const FORK_DENIAL_FINAL_STAGE = {
id: "okay-its-a-fork",
label: "Okay, it's a fork",
} as const;

export type ForkDenialStage = (typeof FORK_DENIAL_STAGES)[number] | typeof FORK_DENIAL_FINAL_STAGE;

export function getForkDenialStages(input: { includeFinalForkState?: boolean } = {}): readonly ForkDenialStage[] {
export function getForkDenialStages(
input: { includeFinalForkState?: boolean } = {},
): readonly ForkDenialStage[] {
return input.includeFinalForkState
? [...FORK_DENIAL_STAGES, FORK_DENIAL_FINAL_STAGE]
: [...FORK_DENIAL_STAGES];
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/lib/toolCallLabel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ describe("deriveSynaraMcpToolTitle", () => {
"Forkara is reading thread activity",
"Forkara read thread activity",
],
["synara_read_thread_events", "Forkara is reading thread events", "Forkara read thread events"],
[
"synara_read_thread_events",
"Forkara is reading thread events",
"Forkara read thread events",
],
[
"synara_read_thread_runtime_events",
"Forkara is reading thread runtime events",
Expand All @@ -111,7 +115,11 @@ describe("deriveSynaraMcpToolTitle", () => {
"Forkara finished waiting for threads",
],
["synara_send_message", "Forkara is sending a message", "Forkara sent a message"],
["synara_interrupt_thread", "Forkara is interrupting a thread", "Forkara interrupted a thread"],
[
"synara_interrupt_thread",
"Forkara is interrupting a thread",
"Forkara interrupted a thread",
],
["synara_set_thread_title", "Forkara is renaming a thread", "Forkara renamed a thread"],
["synara_set_thread_archived", "Forkara is updating a thread", "Forkara updated a thread"],
[
Expand Down
Loading
Loading