From 6faabddc303ea488b39b7f28b76c23114e2a9c40 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 14 Jul 2026 01:09:52 +0530 Subject: [PATCH 1/2] fix: allow leading '@' in model IDs for Cloudflare Workers AI The isValidModelId regex required the first character to be alphanumeric, rejecting Cloudflare Workers AI model IDs like @cf/meta/llama-3-8b-instruct. Add '@' to the allowed first character set. Fixes #315 --- src/constants.ts | 2 +- test/constants.test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 1c1eb2bb..74b26bc3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -375,7 +375,7 @@ export function isValidModelId(value: string): boolean { return ( modelId.length > 0 && modelId.length <= 120 && - /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/u.test(modelId) && + /^[@A-Za-z0-9][A-Za-z0-9._:/+-]*$/u.test(modelId) && !modelId.includes("://") ); } diff --git a/test/constants.test.ts b/test/constants.test.ts index bbcae399..2fdf5698 100644 --- a/test/constants.test.ts +++ b/test/constants.test.ts @@ -24,6 +24,12 @@ describe("isValidModelId", () => { expect(isValidModelId("nvidia/nemotron-3-super-120b-a12b")).toBe(true); }); + test("accepts Cloudflare Workers AI model ids with leading '@'", () => { + expect(isValidModelId("@cf/meta/llama-3-8b-instruct")).toBe(true); + expect(isValidModelId("@cf/moonshotai/kimi-k2.7-code")).toBe(true); + expect(isValidModelId("@cf/qwen/qwen1.5-14b-chat-awq")).toBe(true); + }); + test("rejects empty, whitespace-only, and over-long ids", () => { expect(isValidModelId("")).toBe(false); expect(isValidModelId(" ")).toBe(false); From c624da73900e6108e5168dd2d5035012161247e4 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 14 Jul 2026 01:16:00 +0530 Subject: [PATCH 2/2] fix: preserve customized workflow file instead of unconditionally overwriting The workflow write was unconditional, destroying user customizations (cron schedule, fork guards, env vars, etc.) on every run. The generated wiki then documented the overwritten defaults rather than the user's actual config. Now writeCodeModeWorkflow() checks for an existing file first and only creates the workflow when it doesn't exist, matching the existing preserve-by-default pattern used for agent snippet files. Adds tests verifying: - Workflow is created when absent - Existing customized workflow is preserved as-is - Idempotent across repeated runs Fixes #316 --- src/code-mode.ts | 14 +++++++++ test/code-mode.test.ts | 71 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/code-mode.ts b/src/code-mode.ts index f3447c5e..0bd217dc 100644 --- a/src/code-mode.ts +++ b/src/code-mode.ts @@ -29,6 +29,20 @@ async function writeCodeModeWorkflow( "openwiki-update.yml", ); await mkdir(path.dirname(workflowPath), { recursive: true }); + + // Only create the workflow if it doesn't already exist — respect any + // customizations the user has made (cron schedule, env vars, fork guards, etc.). + try { + await readFile(workflowPath, "utf8"); + // File exists: leave it untouched so user edits survive. + return; + } catch (error) { + if (!isFileNotFoundError(error)) { + throw error; + } + // File not found: fall through to create it. + } + await writeFile(workflowPath, createCodeModeWorkflow(cronExpression), "utf8"); } diff --git a/test/code-mode.test.ts b/test/code-mode.test.ts index 11df0650..771b0c46 100644 --- a/test/code-mode.test.ts +++ b/test/code-mode.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, test } from "vitest"; @@ -100,7 +100,7 @@ Trailing notes that must survive. }); describe("ensureCodeModeRepoSetup workflow", () => { - test("generated PR includes agent files and the workflow in add-paths", async () => { + test("creates workflow when it does not exist", async () => { const repo = await createTempRepo(); await ensureCodeModeRepoSetup(repo); @@ -119,4 +119,71 @@ describe("ensureCodeModeRepoSetup workflow", () => { expect(workflow).toContain(managedPath); } }); + + test("preserves existing customized workflow and does not overwrite", async () => { + const repo = await createTempRepo(); + const customWorkflow = `name: OpenWiki Update + +on: + workflow_dispatch: + schedule: + - cron: "0 2 * * 1" + +permissions: + contents: write + pull-requests: write + +jobs: + update: + if: github.repository == 'my-org/my-repo' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + persist-credentials: true + + - name: Run OpenWiki + run: openwiki code --update --print + env: + OPENWIKI_PROVIDER: anthropic + ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }} + OPENWIKI_MODEL_ID: claude-sonnet-4-20250514 +`; + const workflowDir = path.join(repo, ".github", "workflows"); + await mkdir(workflowDir, { recursive: true }); + await writeFile( + path.join(workflowDir, "openwiki-update.yml"), + customWorkflow, + "utf8", + ); + + await ensureCodeModeRepoSetup(repo); + + const workflow = await readIfPresent( + path.join(workflowDir, "openwiki-update.yml"), + ); + // Must preserve the user's customizations, not overwrite with defaults. + expect(workflow).toBe(customWorkflow); + expect(workflow).toContain('cron: "0 2 * * 1"'); + expect(workflow).toContain("if: github.repository == 'my-org/my-repo'"); + expect(workflow).toContain("OPENWIKI_PROVIDER: anthropic"); + expect(workflow).toContain("claude-sonnet-4-20250514"); + expect(workflow).toContain("persist-credentials: true"); + }); + + test("is idempotent across repeated runs for workflow", async () => { + const repo = await createTempRepo(); + + await ensureCodeModeRepoSetup(repo); + const first = await readIfPresent( + path.join(repo, ".github", "workflows", "openwiki-update.yml"), + ); + await ensureCodeModeRepoSetup(repo); + const second = await readIfPresent( + path.join(repo, ".github", "workflows", "openwiki-update.yml"), + ); + + expect(second).toEqual(first); + }); });