Skip to content
Open
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
14 changes: 14 additions & 0 deletions src/code-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("://")
);
}
Expand Down
71 changes: 69 additions & 2 deletions test/code-mode.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
});
6 changes: 6 additions & 0 deletions test/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down