2026-05-30 01:22:32 +00:00
import { spawnSync } from "node:child_process" ;
import { chmod , mkdir , mkdtemp , readFile , writeFile } from "node:fs/promises" ;
import { tmpdir } from "node:os" ;
import path from "node:path" ;
import { describe , expect , it } from "vitest" ;
const repoRoot = process . cwd ( ) ;
describe ( "development scripts" , ( ) = > {
it ( "propagates CI check failures" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
await writeFile ( path . join ( cwd , "fail.mjs" ) , "process.exit(7);\n" ) ;
await writeJson ( path . join ( cwd , "package.json" ) , {
scripts : {
"typecheck:ci" : "node fail.mjs" ,
"test:ci" : 'node -e "process.exit(0)"' ,
2026-06-16 03:58:55 +00:00
"lint:ci" : 'node -e "process.exit(0)"' ,
2026-05-30 01:22:32 +00:00
"format:check:ci" : 'node -e "process.exit(0)"' ,
2026-05-30 02:00:08 +00:00
build : 'node -e "process.exit(0)"' ,
2026-05-30 01:22:32 +00:00
} ,
} ) ;
const result = runNodeScript ( "scripts/check.mjs" , [ "--ci" ] , cwd ) ;
expect ( result . status ) . toBe ( 7 ) ;
} ) ;
2026-05-31 11:26:11 +00:00
it ( "fails style builds when CSS files are missing from the style order file" , async ( ) = > {
2026-05-30 02:00:08 +00:00
const cwd = await tempWorkspace ( ) ;
await mkdir ( path . join ( cwd , "src" , "styles" ) , { recursive : true } ) ;
2026-05-31 11:26:11 +00:00
await writeJson ( path . join ( cwd , "src" , "styles" , "order.json" ) , [ "00-tokens.css" ] ) ;
2026-05-30 02:00:08 +00:00
await writeFile ( path . join ( cwd , "src" , "styles" , "00-tokens.css" ) , ".codex-panel { color: var(--text-normal); }\n" ) ;
await writeFile ( path . join ( cwd , "src" , "styles" , "10-unlisted.css" ) , ".codex-panel__extra { display: block; }\n" ) ;
const result = runNodeScript ( "scripts/build-styles.mjs" , [ "--check" ] , cwd ) ;
expect ( result . status ) . toBe ( 1 ) ;
2026-05-31 11:26:11 +00:00
expect ( result . stderr ) . toContain ( "CSS files missing from src/styles/order.json: 10-unlisted.css" ) ;
2026-05-30 02:00:08 +00:00
} ) ;
2026-05-30 01:22:32 +00:00
it ( "passes the expected codex generate-ts arguments" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
const binDir = path . join ( cwd , "bin" ) ;
await mkdir ( binDir , { recursive : true } ) ;
await mkdir ( path . join ( cwd , "src" , "generated" , "app-server" ) , { recursive : true } ) ;
const codexBin = path . join ( binDir , "codex" ) ;
await writeFile (
codexBin ,
[
"#!/usr/bin/env node" ,
"import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';" ,
"appendFileSync('codex-args.txt', `${process.argv.slice(2).join('\\n')}\\n`);" ,
"mkdirSync('src/generated/app-server/v2', { recursive: true });" ,
"writeFileSync('src/generated/app-server/v2/Example.ts', '// GENERATED CODE! DO NOT MODIFY BY HAND!\\nexport type Example = string | null | null;\\n');" ,
] . join ( "\n" ) ,
) ;
await chmod ( codexBin , 0 o755 ) ;
const result = runNodeScript ( "scripts/generate-app-server-types.mjs" , [ ] , cwd , {
PATH : ` ${ binDir } ${ path . delimiter } ${ process . env [ "PATH" ] ? ? "" } ` ,
} ) ;
expect ( result . status ) . toBe ( 0 ) ;
await expect ( readFile ( path . join ( cwd , "codex-args.txt" ) , "utf8" ) ) . resolves . toBe (
"app-server\ngenerate-ts\n--experimental\n--out\nsrc/generated/app-server\n" ,
) ;
await expect ( readFile ( path . join ( cwd , "src" , "generated" , "app-server" , "v2" , "Example.ts" ) , "utf8" ) ) . resolves . toContain (
"export type Example = string | null;" ,
) ;
} ) ;
2026-06-12 09:21:17 +00:00
it ( "reads app-server initialize flags from the connection client in API baseline checks" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
const binDir = path . join ( cwd , "bin" ) ;
await mkdir ( binDir , { recursive : true } ) ;
await mkdir ( path . join ( cwd , "scripts" ) , { recursive : true } ) ;
await mkdir ( path . join ( cwd , "src" , "app-server" , "connection" ) , { recursive : true } ) ;
await writeJson ( path . join ( cwd , "package.json" ) , {
version : "1.0.0" ,
devDependencies : {
obsidian : "~1.12.3" ,
} ,
} ) ;
await writeJson ( path . join ( cwd , "package-lock.json" ) , {
packages : {
"node_modules/obsidian" : {
version : "1.12.3" ,
} ,
} ,
} ) ;
await writeJson ( path . join ( cwd , "manifest.json" ) , {
minAppVersion : "1.12.0" ,
} ) ;
await writeJson ( path . join ( cwd , "versions.json" ) , {
"1.0.0" : "1.12.0" ,
} ) ;
await writeFile (
path . join ( cwd , "README.md" ) ,
[
"## Compatibility" ,
"" ,
"| Key | Version | Notes |" ,
"| --- | --- | --- |" ,
"| `codex.testedCliVersion` | `0.139.0` | Tested CLI. |" ,
"| `obsidian.minAppVersion` | `1.12.0` | Minimum app version. |" ,
] . join ( "\n" ) ,
) ;
await writeFile (
path . join ( cwd , "scripts" , "generate-app-server-types.mjs" ) ,
'run("codex", ["app-server", "generate-ts", "--experimental"]);\n' ,
) ;
await writeFile (
path . join ( cwd , "src" , "app-server" , "connection" , "client.ts" ) ,
[
'const init = await this.request("initialize", {' ,
" capabilities: {" ,
" experimentalApi: true," ,
" requestAttestation: false," ,
" }," ,
"});" ,
] . join ( "\n" ) ,
) ;
const codexBin = path . join ( binDir , "codex" ) ;
await writeFile ( codexBin , "#!/usr/bin/env node\nconsole.log('codex 0.139.0');\n" ) ;
await chmod ( codexBin , 0 o755 ) ;
const result = runNodeScript ( "scripts/api-baseline.mjs" , [ "--json" ] , cwd , {
PATH : ` ${ binDir } ${ path . delimiter } ${ process . env [ "PATH" ] ? ? "" } ` ,
} ) ;
expect ( result . status ) . toBe ( 0 ) ;
const report = JSON . parse ( result . stdout ) ;
expect ( report . codex . initializeExperimentalApi ) . toBe ( true ) ;
expect ( report . codex . initializeRequestAttestationDisabled ) . toBe ( true ) ;
expect ( report . failures ) . toEqual ( [ ] ) ;
} ) ;
2026-05-30 01:22:32 +00:00
it ( "keeps files unchanged when format check finds unformatted content" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
await mkdir ( path . join ( cwd , "src" ) , { recursive : true } ) ;
await mkdir ( path . join ( cwd , "tests" ) , { recursive : true } ) ;
await mkdir ( path . join ( cwd , "scripts" ) , { recursive : true } ) ;
const sourcePath = path . join ( cwd , "src" , "unformatted.ts" ) ;
const source = "export const value=1\n" ;
await writeFile ( sourcePath , source ) ;
const result = runNodeScript ( "scripts/format.mjs" , [ "--check" ] , cwd ) ;
expect ( result . status ) . toBe ( 1 ) ;
expect ( result . stderr ) . toContain ( "Unformatted files:" ) ;
expect ( result . stderr ) . toContain ( "src/unformatted.ts" ) ;
await expect ( readFile ( sourcePath , "utf8" ) ) . resolves . toBe ( source ) ;
} ) ;
2026-06-13 12:50:28 +00:00
it ( "detects runtime import cycles" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
await writeImportCycleFixture ( cwd , {
"src/a.ts" : 'import { b } from "./b";\nexport const a = b;\n' ,
"src/b.ts" : 'import { a } from "./a";\nexport const b = a;\n' ,
} ) ;
2026-06-16 03:58:55 +00:00
const result = runNodeScript ( "scripts/lint/check-import-cycles.mjs" , [ ] , cwd ) ;
2026-06-13 12:50:28 +00:00
expect ( result . status ) . toBe ( 1 ) ;
expect ( result . stderr ) . toContain ( "found 1 cycle" ) ;
expect ( result . stderr ) . toContain ( "src/a.ts -> src/b.ts -> src/a.ts" ) ;
} ) ;
2026-06-15 06:50:49 +00:00
it ( "ignores type-only import cycles" , async ( ) = > {
2026-06-13 12:50:28 +00:00
const cwd = await tempWorkspace ( ) ;
await writeImportCycleFixture ( cwd , {
"src/a.ts" : 'import type { B } from "./b";\nexport interface A { b?: B }\n' ,
"src/b.ts" : 'import type { A } from "./a";\nexport interface B { a?: A }\n' ,
} ) ;
2026-06-16 03:58:55 +00:00
const result = runNodeScript ( "scripts/lint/check-import-cycles.mjs" , [ ] , cwd ) ;
2026-06-13 12:50:28 +00:00
2026-06-15 06:50:49 +00:00
expect ( result . status ) . toBe ( 0 ) ;
expect ( result . stdout ) . toContain ( "No import cycles found." ) ;
} ) ;
it ( "detects import cycles when mixed imports include runtime values" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
await writeImportCycleFixture ( cwd , {
"src/a.ts" : 'import { type B, b } from "./b";\nexport interface A { b?: B }\nexport const a = b;\n' ,
"src/b.ts" : 'import { a, type A } from "./a";\nexport interface B { a?: A }\nexport const b = a;\n' ,
} ) ;
2026-06-16 03:58:55 +00:00
const result = runNodeScript ( "scripts/lint/check-import-cycles.mjs" , [ ] , cwd ) ;
2026-06-15 06:50:49 +00:00
2026-06-13 12:50:28 +00:00
expect ( result . status ) . toBe ( 1 ) ;
expect ( result . stderr ) . toContain ( "found 1 cycle" ) ;
expect ( result . stderr ) . toContain ( "src/a.ts -> src/b.ts -> src/a.ts" ) ;
} ) ;
2026-05-30 01:22:32 +00:00
it ( "fails release prepare before changing version files when release notes already exist" , async ( ) = > {
const cwd = await tempWorkspace ( ) ;
await mkdir ( path . join ( cwd , ".github" , "release-notes" ) , { recursive : true } ) ;
const packageJson = { version : "2.3.2" } ;
const packageLockJson = { version : "2.3.2" , packages : { "" : { version : "2.3.2" } } } ;
const manifestJson = { version : "2.3.2" , minAppVersion : "1.12.0" } ;
const versionsJson = { "2.3.2" : "1.12.0" } ;
await writeJson ( path . join ( cwd , "package.json" ) , packageJson ) ;
await writeJson ( path . join ( cwd , "package-lock.json" ) , packageLockJson ) ;
await writeJson ( path . join ( cwd , "manifest.json" ) , manifestJson ) ;
await writeJson ( path . join ( cwd , "versions.json" ) , versionsJson ) ;
await writeFile ( path . join ( cwd , ".github" , "release-notes" , "2.3.3.md" ) , "## Changes\n\n- Existing\n" ) ;
const result = runNodeScript ( "scripts/release/prepare.mjs" , [ "2.3.3" ] , cwd ) ;
expect ( result . status ) . toBe ( 1 ) ;
expect ( result . stderr ) . toContain ( ".github/release-notes/2.3.3.md already exists" ) ;
await expect ( readJson ( path . join ( cwd , "package.json" ) ) ) . resolves . toEqual ( packageJson ) ;
await expect ( readJson ( path . join ( cwd , "package-lock.json" ) ) ) . resolves . toEqual ( packageLockJson ) ;
await expect ( readJson ( path . join ( cwd , "manifest.json" ) ) ) . resolves . toEqual ( manifestJson ) ;
await expect ( readJson ( path . join ( cwd , "versions.json" ) ) ) . resolves . toEqual ( versionsJson ) ;
} ) ;
} ) ;
async function tempWorkspace ( ) : Promise < string > {
return mkdtemp ( path . join ( tmpdir ( ) , "codex-panel-scripts-" ) ) ;
}
2026-06-13 12:50:28 +00:00
async function writeImportCycleFixture ( cwd : string , files : Record < string , string > ) : Promise < void > {
await writeJson ( path . join ( cwd , "tsconfig.json" ) , {
compilerOptions : {
target : "ES2022" ,
module : "ESNext" ,
moduleResolution : "Bundler" ,
verbatimModuleSyntax : true ,
strict : true ,
} ,
include : [ "src/**/*.ts" ] ,
} ) ;
for ( const [ file , source ] of Object . entries ( files ) ) {
await mkdir ( path . dirname ( path . join ( cwd , file ) ) , { recursive : true } ) ;
await writeFile ( path . join ( cwd , file ) , source ) ;
}
}
2026-05-30 01:22:32 +00:00
function runNodeScript ( script : string , args : string [ ] = [ ] , cwd = repoRoot , env : NodeJS.ProcessEnv = { } ) {
return spawnSync ( process . execPath , [ path . join ( repoRoot , script ) , . . . args ] , {
cwd ,
encoding : "utf8" ,
env : { . . . process . env , . . . env } ,
shell : false ,
} ) ;
}
async function writeJson ( file : string , value : unknown ) : Promise < void > {
await writeFile ( file , ` ${ JSON . stringify ( value , null , 2 ) } \ n ` ) ;
}
async function readJson ( file : string ) : Promise < unknown > {
return JSON . parse ( await readFile ( file , "utf8" ) ) ;
}