117 lines
5.9 KiB
TypeScript
117 lines
5.9 KiB
TypeScript
import { strict as assert } from 'node:assert'
|
|
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { test } from 'node:test'
|
|
import ts from 'typescript'
|
|
|
|
const source = ts.createSourceFile('index.ts', readFileSync(new URL('./index.ts', import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true)
|
|
function loadFunction(name: string, dependencies: Record<string, unknown>) {
|
|
const declaration = source.statements.find((item) => ts.isFunctionDeclaration(item) && item.name?.text === name)
|
|
assert.ok(declaration)
|
|
const code = ts.transpile(declaration.getText(source), { target: ts.ScriptTarget.ES2022 })
|
|
return new Function(...Object.keys(dependencies), `${code}; return ${name};`)(...Object.values(dependencies))
|
|
}
|
|
|
|
function command(executable: string, args: string[], cwd: string) {
|
|
const result = spawnSync(executable, args, {
|
|
cwd, encoding: 'utf8', timeout: 15000,
|
|
env: { ...process.env, GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@example.com', GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@example.com' },
|
|
})
|
|
assert.equal(result.status, 0, result.stderr || String(result.error))
|
|
return result.stdout.trim()
|
|
}
|
|
|
|
function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'lms-preserve-'))
|
|
const template = join(root, 'template')
|
|
mkdirSync(template)
|
|
command('git', ['init', '-b', 'main'], template)
|
|
writeFileSync(join(template, 'node.py'), 'template source\n')
|
|
writeFileSync(join(template, '.gitignore'), 'build/\ninstall/\nlog/\n')
|
|
command('git', ['add', '.'], template)
|
|
command('git', ['commit', '-m', 'template'], template)
|
|
const install = loadFunction('installRemoteWorkspaceModule', {
|
|
normalizeRemoteTarget: (target: unknown) => target,
|
|
remoteRepositoryUrl: (url: string) => url,
|
|
withGitCredential: (url: string) => url,
|
|
remotePathJoin: join,
|
|
safeArtifactFileName: () => 'unused.tar',
|
|
quoteShellSingle: (value: string) => "'" + value.replaceAll("'", "'\\''") + "'",
|
|
encodeBase64: (value: string) => Buffer.from(value).toString('base64'),
|
|
expandRemotePathVar: () => [],
|
|
runSsh: async (_target: unknown, script: string) => command('bash', ['-c', script], root),
|
|
parseModuleFolderName: () => ({ name: 'Test', category: 'Control' }),
|
|
})
|
|
const target = { workspaceRoot: join(root, 'workspace') }
|
|
return { root, template, target, install, modulePath: join(target.workspaceRoot, 'module') }
|
|
}
|
|
|
|
test('reinstall preserves saved source, local commits, untracked files and build outputs despite changed repository URLs', async () => {
|
|
const f = fixture()
|
|
try {
|
|
await f.install(f.target, 'Test', 'module', f.template)
|
|
writeFileSync(join(f.modulePath, 'node.py'), 'student commit\n')
|
|
command('git', ['add', 'node.py'], f.modulePath)
|
|
command('git', ['commit', '-m', 'student work'], f.modulePath)
|
|
const head = command('git', ['rev-parse', 'HEAD'], f.modulePath)
|
|
writeFileSync(join(f.modulePath, 'node.py'), 'saved but not committed\n')
|
|
writeFileSync(join(f.modulePath, 'new.py'), 'new student file\n')
|
|
for (const dir of ['build', 'install', 'log']) {
|
|
mkdirSync(join(f.modulePath, dir))
|
|
writeFileSync(join(f.modulePath, dir, 'keep'), dir)
|
|
}
|
|
const status = command('git', ['status', '--porcelain'], f.modulePath)
|
|
await f.install(f.target, 'Test', 'module', '/missing/new-template', '/missing/new-student', {
|
|
prebuiltArtifacts: [{ url: 'https://invalid.example/overwrite.tar', required: true }],
|
|
})
|
|
assert.equal(command('git', ['rev-parse', 'HEAD'], f.modulePath), head)
|
|
assert.equal(command('git', ['status', '--porcelain'], f.modulePath), status)
|
|
assert.equal(readFileSync(join(f.modulePath, 'node.py'), 'utf8'), 'saved but not committed\n')
|
|
assert.equal(readFileSync(join(f.modulePath, 'new.py'), 'utf8'), 'new student file\n')
|
|
for (const dir of ['build', 'install', 'log']) assert.equal(readFileSync(join(f.modulePath, dir, 'keep'), 'utf8'), dir)
|
|
} finally { rmSync(f.root, { recursive: true, force: true }) }
|
|
})
|
|
|
|
test('first install restores the student repository instead of replacing it with template source', async () => {
|
|
const f = fixture()
|
|
try {
|
|
const student = join(f.root, 'student')
|
|
command('git', ['clone', f.template, student], f.root)
|
|
writeFileSync(join(student, 'node.py'), 'submitted student work\n')
|
|
command('git', ['add', '.'], student)
|
|
command('git', ['commit', '-m', 'submission'], student)
|
|
await f.install(f.target, 'Test', 'module', f.template, student)
|
|
assert.equal(readFileSync(join(f.modulePath, 'node.py'), 'utf8'), 'submitted student work\n')
|
|
assert.equal(command('git', ['branch', '--show-current'], f.modulePath), 'student-work')
|
|
} finally { rmSync(f.root, { recursive: true, force: true }) }
|
|
})
|
|
|
|
test('first install initializes an empty student repository', async () => {
|
|
const f = fixture()
|
|
try {
|
|
const student = join(f.root, 'student.git')
|
|
command('git', ['init', '--bare', student], f.root)
|
|
await f.install(f.target, 'Test', 'module', f.template, student)
|
|
assert.equal(command('git', ['--git-dir', student, 'show', 'main:node.py'], f.root), 'template source')
|
|
} finally { rmSync(f.root, { recursive: true, force: true }) }
|
|
})
|
|
|
|
test('legacy session reset only stops modules and returns stop failures', async () => {
|
|
const target = { workspaceRoot: '/student/workspace' }
|
|
const modules = [{ moduleName: 'Test', folderName: 'module', templateRepositoryUrl: '/template' }]
|
|
const result = { ok: false, message: 'stop failed' }
|
|
let calls = 0
|
|
const reset = loadFunction('resetRemoteWorkspaceSession', {
|
|
stopRemoteWorkspaceModules: async (actualTarget: unknown, actualModules: unknown) => {
|
|
calls++
|
|
assert.equal(actualTarget, target)
|
|
assert.equal(actualModules, modules)
|
|
return result
|
|
},
|
|
})
|
|
assert.equal(await reset(target, modules), result)
|
|
assert.equal(calls, 1)
|
|
})
|