import assert from 'node:assert/strict';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
utimesSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
import {
getDshProfileRebuildPlan,
syncOhosDshProfile,
} from './prepare-dsh-profile.mjs';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
function createPackageTree() {
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-dsh-profile-'));
const packageDir = path.join(root, 'packages', 'dsh-acp');
const outDir = path.join(packageDir, 'dist-profile');
const prepareScriptPath = path.join(root, 'scripts', 'prepare-dsh-profile.mjs');
mkdirSync(path.join(packageDir, 'src'), { recursive: true });
mkdirSync(path.join(packageDir, 'presets'), { recursive: true });
mkdirSync(path.join(packageDir, 'scripts'), { recursive: true });
mkdirSync(path.join(root, 'scripts'), { recursive: true });
mkdirSync(outDir, { recursive: true });
writeFileSync(path.join(packageDir, 'src', 'app.ts'), 'export {}\n');
writeFileSync(path.join(packageDir, 'presets', 'preset.yml'), 'name: test\n');
writeFileSync(path.join(packageDir, 'cordis.yml'), 'name: test\n');
writeFileSync(path.join(packageDir, 'package.json'), '{}\n');
writeFileSync(path.join(packageDir, 'package-lock.json'), '{}\n');
writeFileSync(path.join(packageDir, 'tsconfig.build.json'), '{}\n');
writeFileSync(path.join(packageDir, 'scripts', 'build-profile.mjs'), 'export {}\n');
writeFileSync(prepareScriptPath, 'export {}\n');
return { root, packageDir, outDir, prepareScriptPath };
}
function writeStamp(outDir, stamp = { profile: 'bitfun-acp', content: 'abc' }) {
const stampPath = path.join(outDir, '.bitfun-bridge.json');
writeFileSync(stampPath, `${JSON.stringify(stamp)}\n`);
return stampPath;
}
function setMtime(filePath, mtimeMs) {
const atime = new Date(mtimeMs);
const mtime = new Date(mtimeMs);
utimesSync(filePath, atime, mtime);
}
test('rebuilds when the profile stamp is missing', () => {
const { root, packageDir, outDir, prepareScriptPath } = createPackageTree();
try {
const plan = getDshProfileRebuildPlan({ packageDir, outDir, prepareScriptPath });
assert.equal(plan.shouldBuild, true);
assert.match(plan.reason, /stamp is missing/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('rebuilds when force is requested', () => {
const { root, packageDir, outDir, prepareScriptPath } = createPackageTree();
try {
const stampPath = writeStamp(outDir);
setMtime(stampPath, Date.now() + 60_000);
const plan = getDshProfileRebuildPlan({
packageDir,
outDir,
prepareScriptPath,
force: true,
});
assert.equal(plan.shouldBuild, true);
assert.match(plan.reason, /Force rebuild/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('rebuilds when the stamp is incomplete', () => {
const { root, packageDir, outDir, prepareScriptPath } = createPackageTree();
try {
writeStamp(outDir, { profile: 'bitfun-acp' });
const plan = getDshProfileRebuildPlan({ packageDir, outDir, prepareScriptPath });
assert.equal(plan.shouldBuild, true);
assert.match(plan.reason, /incomplete/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('rebuilds when an input is newer than the stamp', () => {
const { root, packageDir, outDir, prepareScriptPath } = createPackageTree();
try {
const stampPath = writeStamp(outDir);
const older = Date.now() - 60_000;
const newer = Date.now();
for (const filePath of [
path.join(packageDir, 'src', 'app.ts'),
path.join(packageDir, 'presets', 'preset.yml'),
path.join(packageDir, 'cordis.yml'),
path.join(packageDir, 'package.json'),
path.join(packageDir, 'package-lock.json'),
path.join(packageDir, 'tsconfig.build.json'),
path.join(packageDir, 'scripts', 'build-profile.mjs'),
prepareScriptPath,
stampPath,
]) {
setMtime(filePath, older);
}
setMtime(path.join(packageDir, 'src', 'app.ts'), newer);
const plan = getDshProfileRebuildPlan({ packageDir, outDir, prepareScriptPath });
assert.equal(plan.shouldBuild, true);
assert.match(plan.reason, /inputs changed/);
assert.match(plan.reason, /app\.ts/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('skips when a complete stamp is newer than every input', () => {
const { root, packageDir, outDir, prepareScriptPath } = createPackageTree();
try {
const older = Date.now() - 60_000;
const newer = Date.now();
for (const filePath of [
path.join(packageDir, 'src', 'app.ts'),
path.join(packageDir, 'presets', 'preset.yml'),
path.join(packageDir, 'cordis.yml'),
path.join(packageDir, 'package.json'),
path.join(packageDir, 'package-lock.json'),
path.join(packageDir, 'tsconfig.build.json'),
path.join(packageDir, 'scripts', 'build-profile.mjs'),
prepareScriptPath,
]) {
setMtime(filePath, older);
}
const stampPath = writeStamp(outDir);
setMtime(stampPath, newer);
const plan = getDshProfileRebuildPlan({ packageDir, outDir, prepareScriptPath });
assert.equal(plan.shouldBuild, false);
assert.match(plan.reason, /up to date/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('stages a clean HarmonyOS profile with resfile-compatible names', () => {
const root = mkdtempSync(path.join(tmpdir(), 'bitfun-dsh-ohos-profile-'));
const sourceDir = path.join(root, 'dist-profile');
const destinationDir = path.join(root, 'resfile', 'dsh-profile');
try {
mkdirSync(path.join(sourceDir, 'node_modules', '@example', 'bridge'), { recursive: true });
writeFileSync(path.join(sourceDir, '.bitfun-bridge.json'), '{"profile":"bitfun-acp"}\n');
writeFileSync(path.join(sourceDir, 'node_modules', '@example', 'bridge', 'index.js'), '');
mkdirSync(destinationDir, { recursive: true });
writeFileSync(path.join(destinationDir, 'stale.js'), 'stale');
syncOhosDshProfile({ sourceDir, destinationDir });
assert.equal(existsSync(path.join(destinationDir, 'stale.js')), false);
assert.equal(existsSync(path.join(destinationDir, '.bitfun-bridge.json')), false);
assert.equal(existsSync(path.join(destinationDir, 'node_modules')), false);
assert.equal(existsSync(path.join(destinationDir, 'bitfun-bridge.json')), true);
assert.equal(
existsSync(
path.join(destinationDir, 'vendor-node-modules', '@example', 'bridge', 'index.js'),
),
true,
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('desktop:dev does not compile the DeepSeek profile', () => {
const devScript = readFileSync(path.join(repoRoot, 'scripts', 'dev.cjs'), 'utf8');
assert.doesNotMatch(devScript, /runCommandPrefixed\('dsh-profile'/);
assert.doesNotMatch(devScript, /\['run', 'prepare:dsh-profile'\]/);
assert.match(
devScript,
/Prepare resources \(parallel: monaco, version, mobile-web, flashgrep\)/,
);
});
test('desktop clippy does not compile the DeepSeek profile', () => {
const packageJson = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
assert.doesNotMatch(packageJson.scripts['lint:rs:desktop'], /prepare:dsh-profile/);
});
test('official frontend packaging still compiles the DeepSeek profile', () => {
const frontendBuildAll = readFileSync(
path.join(repoRoot, 'scripts', 'frontend-build-all.mjs'),
'utf8',
);
assert.match(frontendBuildAll, /prepare:dsh-profile/);
});