Fix release version labels across deployment views
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
import { campaignOutcome, type CampaignSummary } from '../../src/lib/ui/campaigns.js';
|
||||
import {
|
||||
campaignOutcome,
|
||||
campaignTitle,
|
||||
type CampaignSummary
|
||||
} from '../../src/lib/ui/campaigns.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
beforeEach(async () => {
|
||||
@@ -61,6 +65,48 @@ it('searches script names, campaign IDs and friendly server names across all ret
|
||||
expect((await get('q=not-found')).total).toBe(0);
|
||||
});
|
||||
|
||||
it('exposes release versions for campaign titles and both campaign detail endpoints', async () => {
|
||||
const { handle, get, add } = await setup();
|
||||
add('aggregate-script', ['done']);
|
||||
add('legacy-script', ['done']);
|
||||
add('config', ['done']);
|
||||
handle.exec(`
|
||||
INSERT INTO scripts(id,name,target_rel_path,kind)
|
||||
VALUES(1,'Double Tap','double.gsc','script'),(2,'Server settings','settings.cfg','config');
|
||||
INSERT INTO script_versions(id,script_id,version,release_version,kind,sha256,size)
|
||||
VALUES(1,1,2,'1.3.0','script','old',0),(2,1,11,'2.4.1','script','new',0),
|
||||
(3,2,4,NULL,'config','settings',0);
|
||||
INSERT INTO deployment_scripts(deployment_id,script_id,script_version_id,prior_version_id,kind)
|
||||
VALUES('aggregate-script',1,2,1,'script'),('config',2,3,NULL,'config');
|
||||
UPDATE deployments SET script_id=1,script_version_id=2,prior_version_id=1 WHERE id='legacy-script';
|
||||
`);
|
||||
const { deployments } = await get();
|
||||
const versions = {
|
||||
script_kind: 'script',
|
||||
script_version: 11,
|
||||
script_release_version: '2.4.1',
|
||||
prior_version: 2,
|
||||
prior_release_version: '1.3.0'
|
||||
};
|
||||
const aggregate = deployments.find((row) => row.id === 'aggregate-script')!;
|
||||
const legacy = deployments.find((row) => row.id === 'legacy-script')!;
|
||||
expect(aggregate.scripts).toEqual([expect.objectContaining(versions)]);
|
||||
expect(legacy).toMatchObject(versions);
|
||||
expect(campaignTitle(aggregate)).toBe('Promote · Double Tap 2.4.1 (upload 11)');
|
||||
expect(campaignTitle(legacy)).toBe('Promote · Double Tap 2.4.1 (upload 11)');
|
||||
expect(campaignTitle(deployments.find((row) => row.id === 'config')!)).toBe(
|
||||
'Promote · Server settings v4'
|
||||
);
|
||||
|
||||
const { GET: campaignDetail } = await import('../../src/routes/api/campaigns/[id]/+server.js');
|
||||
const { GET: deploymentDetail } =
|
||||
await import('../../src/routes/api/deployments/[id]/+server.js');
|
||||
for (const detail of [campaignDetail, deploymentDetail]) {
|
||||
const response = await (await detail({ params: { id: 'aggregate-script' } })).json();
|
||||
expect(response.scripts).toEqual([expect.objectContaining(versions)]);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps API status filters and active/history partitions consistent with displayed outcomes', async () => {
|
||||
const { handle, get, add } = await setup();
|
||||
add('complete', ['done']);
|
||||
|
||||
@@ -15,6 +15,95 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe('persisted launch recipe summaries', () => {
|
||||
it.each([false, true])(
|
||||
'restores exact historical release labels with cached summary=%s without rewriting the recipe',
|
||||
async (cached) => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { createScript, uploadNewVersion } =
|
||||
await import('../../src/lib/server/fleet/scripts.js');
|
||||
const {
|
||||
captureRecipe,
|
||||
summarizeRecipe,
|
||||
recipeSummaryForDeployment,
|
||||
recipeForDeployment
|
||||
} = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const saved = createScript({
|
||||
name: 'Historical script',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
targetRelPath: 'history.gsc',
|
||||
content: Buffer.from('historical bytes'),
|
||||
releaseVersion: '2.3.4'
|
||||
});
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
.run(saved.version.id, saved.script.id);
|
||||
const recipe = captureRecipe('t5', 'theater');
|
||||
const original = recipe.files[0];
|
||||
delete original.releaseVersion;
|
||||
const unversioned = createScript({
|
||||
name: 'Legacy script',
|
||||
targetRelPath: 'legacy.gsc',
|
||||
content: Buffer.from('historical bytes'),
|
||||
releaseVersion: '1.0.0'
|
||||
});
|
||||
db()
|
||||
.prepare('UPDATE script_versions SET release_version = NULL WHERE id = ?')
|
||||
.run(unversioned.version.id);
|
||||
recipe.files.push(
|
||||
{ ...original, path: 'recorded.gsc', releaseVersion: '1.8.0' },
|
||||
{ ...original, path: 'legacy.gsc', versionId: unversioned.version.id },
|
||||
{ ...original, path: 'missing.gsc', versionId: 9999 },
|
||||
{ ...original, path: 'mismatched.gsc', sha256: 'different' }
|
||||
);
|
||||
const latest = uploadNewVersion(
|
||||
saved.script.id,
|
||||
Buffer.from('newer bytes'),
|
||||
undefined,
|
||||
'9.0.0'
|
||||
);
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
.run(latest.id, saved.script.id);
|
||||
const encoded = JSON.stringify(recipe);
|
||||
const summary = summarizeRecipe(recipe);
|
||||
db()
|
||||
.exec(`INSERT INTO server_deployments(id,server,action,state,stage,started_at,triggered_by)
|
||||
VALUES('historical','retired-server','create','done','Complete','2025-01-01','fixture')`);
|
||||
db()
|
||||
.prepare(
|
||||
'INSERT INTO server_launch_recipes(deployment_id,recipe,recipe_summary) VALUES(?,?,?)'
|
||||
)
|
||||
.run('historical', encoded, cached ? JSON.stringify(summary) : null);
|
||||
|
||||
const restored = recipeSummaryForDeployment('historical')!.recipe;
|
||||
expect(restored.fingerprint).toBe(summary.fingerprint);
|
||||
expect(restored.files.find((file) => file.path === 'history.gsc')).toMatchObject({
|
||||
version: 1,
|
||||
releaseVersion: '2.3.4'
|
||||
});
|
||||
expect(
|
||||
restored.files.find((file) => file.path === 'recorded.gsc')!.releaseVersion
|
||||
).toBe('1.8.0');
|
||||
expect(
|
||||
restored.files.find((file) => file.path === 'legacy.gsc')!.releaseVersion
|
||||
).toBeNull();
|
||||
for (const path of ['missing.gsc', 'mismatched.gsc'])
|
||||
expect(restored.files.find((file) => file.path === path)).not.toHaveProperty(
|
||||
'releaseVersion'
|
||||
);
|
||||
expect(recipeSummaryForDeployment('historical')!.recipe).toEqual(restored);
|
||||
expect(recipeForDeployment('historical')!.recipe).toEqual(recipe);
|
||||
const stored = db()
|
||||
.prepare(
|
||||
'SELECT recipe,recipe_summary FROM server_launch_recipes WHERE deployment_id = ?'
|
||||
)
|
||||
.get('historical') as { recipe: string; recipe_summary: string };
|
||||
expect(stored.recipe).toBe(encoded);
|
||||
expect(JSON.parse(stored.recipe_summary)).toEqual(restored);
|
||||
}
|
||||
);
|
||||
|
||||
it('lazily upgrades legacy snapshots once without changing fingerprints or retained bytes', async () => {
|
||||
const recipe: LaunchRecipe = {
|
||||
format: 1,
|
||||
|
||||
@@ -82,12 +82,62 @@ describe('launch recipes and previews', () => {
|
||||
expect(second.name).toBe(first.name);
|
||||
expect(second.port).toBe(first.port);
|
||||
expect(first.recipe.files).toHaveLength(3);
|
||||
expect(first.recipe.files[0]).toMatchObject({ version: 1, releaseVersion: '1.0.0' });
|
||||
expect(first.recipe.files[0]).not.toHaveProperty('content');
|
||||
expect(db().prepare('SELECT * FROM server_keys').all()).toEqual([]);
|
||||
expect(db().prepare('SELECT * FROM server_creation_jobs').all()).toEqual([]);
|
||||
expect(JSON.stringify(first)).not.toContain('private-');
|
||||
});
|
||||
|
||||
it('retains the selected script and plugin release in both planned and captured files', async () => {
|
||||
const { script } = await seedArtifacts();
|
||||
const { createScript, uploadNewVersion } =
|
||||
await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { captureRecipe, captureRecipePlan } =
|
||||
await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const selected = uploadNewVersion(
|
||||
script.script.id,
|
||||
Buffer.from('updated perks'),
|
||||
undefined,
|
||||
'2.3.4'
|
||||
);
|
||||
const plugin = createScript({
|
||||
name: 'Native perks',
|
||||
kind: 'plugin',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
targetRelPath: 'perks.dll',
|
||||
content: Buffer.from('plugin'),
|
||||
releaseVersion: 'v4.5.6'
|
||||
});
|
||||
for (const [id, versionId] of [
|
||||
[script.script.id, selected.id],
|
||||
[plugin.script.id, plugin.version.id]
|
||||
])
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id = ? WHERE id = ?')
|
||||
.run(versionId, id);
|
||||
for (const capture of [captureRecipePlan, captureRecipe]) {
|
||||
const recipe = capture('t5', 'theater');
|
||||
expect(recipe.files).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: 'perks.gsc',
|
||||
version: 2,
|
||||
releaseVersion: '2.3.4'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: 'perks.dll',
|
||||
version: 1,
|
||||
releaseVersion: 'v4.5.6'
|
||||
}),
|
||||
expect.objectContaining({ kind: 'config', version: null, releaseVersion: null })
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the same operation for repeated submission, even after expiry and restart', async () => {
|
||||
const { previewCreation, queueCreateServer } =
|
||||
await import('../../src/lib/server/fleet/create.js');
|
||||
|
||||
@@ -174,7 +174,7 @@ async function moveTarget() {
|
||||
}
|
||||
|
||||
describe('rebalance instance migrations', () => {
|
||||
it('preserves deployed configuration, patched file versions, identity and credentials on an active source', async () => {
|
||||
it.each(['current', 'legacy'])('preserves deployed files (%s)', async (recipeFormat) => {
|
||||
const { config } = await import('../../src/lib/server/config.js');
|
||||
Object.assign(config.rcon, { password: '' });
|
||||
const { createScript, uploadNewVersion } =
|
||||
@@ -196,7 +196,16 @@ describe('rebalance instance migrations', () => {
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('original deployed script')
|
||||
});
|
||||
for (const artifact of [baseline, script])
|
||||
const plugin = createScript({
|
||||
releaseVersion: 'v2.4.0',
|
||||
name: 'Deployed plugin',
|
||||
kind: 'plugin',
|
||||
targetRelPath: 'preserved.dll',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('deployed plugin')
|
||||
});
|
||||
for (const artifact of [baseline, script, plugin])
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(artifact.version.id, artifact.script.id);
|
||||
@@ -243,10 +252,36 @@ describe('rebalance instance migrations', () => {
|
||||
const original = nativeInstance(job.name)!;
|
||||
const recipe = captureMoveRecipe(job.name);
|
||||
expect(recipe.files.every((file) => file.content === undefined)).toBe(true);
|
||||
expect(recipe.files.find((file) => file.path === 'preserved.gsc')).toMatchObject({
|
||||
versionId: patched.id,
|
||||
version: patched.version,
|
||||
releaseVersion: '1.0.1'
|
||||
});
|
||||
expect(recipe.files.find((file) => file.path === 'preserved.dll')).toMatchObject({
|
||||
versionId: plugin.version.id,
|
||||
releaseVersion: 'v2.4.0'
|
||||
});
|
||||
const id = requestInstanceMove(job.name, target.id, {
|
||||
triggeredBy: 'rebalance:test',
|
||||
recipe
|
||||
});
|
||||
if (recipeFormat === 'legacy') {
|
||||
const stored = db()
|
||||
.prepare(
|
||||
'SELECT recipe, recipe_summary FROM server_launch_recipes WHERE deployment_id=?'
|
||||
)
|
||||
.get(id) as { recipe: string; recipe_summary: string };
|
||||
const legacy = (encoded: string) => {
|
||||
const value = JSON.parse(encoded);
|
||||
for (const file of value.files) delete file.releaseVersion;
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
db()
|
||||
.prepare(
|
||||
'UPDATE server_launch_recipes SET recipe=?,recipe_summary=? WHERE deployment_id=?'
|
||||
)
|
||||
.run(legacy(stored.recipe), legacy(stored.recipe_summary), id);
|
||||
}
|
||||
await advance();
|
||||
const removal = hostMigration(id)!.remove_operation_id!;
|
||||
expect(removal).toBeTruthy();
|
||||
@@ -304,7 +339,14 @@ describe('rebalance instance migrations', () => {
|
||||
generation: 2
|
||||
});
|
||||
expect(getHost(hostId)?.mode).toBe('active');
|
||||
expect(JSON.stringify(getDeployment(id)?.recipe)).not.toContain(launch.rconPassword);
|
||||
const displayedRecipe = getDeployment(id)?.recipe;
|
||||
expect(displayedRecipe?.files.find((file) => file.path === 'preserved.gsc')).toMatchObject({
|
||||
releaseVersion: '1.0.1'
|
||||
});
|
||||
expect(displayedRecipe?.files.find((file) => file.path === 'preserved.dll')).toMatchObject({
|
||||
releaseVersion: 'v2.4.0'
|
||||
});
|
||||
expect(JSON.stringify(displayedRecipe)).not.toContain(launch.rconPassword);
|
||||
});
|
||||
|
||||
it('rechecks occupancy, target capacity and run admission before removing the source', async () => {
|
||||
@@ -1773,17 +1815,21 @@ describe('native instance provisioning', () => {
|
||||
expect(getHost(hostId)?.mode).toBe('active');
|
||||
});
|
||||
it('builds campaign replacements with requested plugins, configuration, additions and removals', async () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { createScript, uploadNewVersion } =
|
||||
await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
let replacementPluginId = 0;
|
||||
for (const path of ['replace.dll', 'remove.dll']) {
|
||||
const file = createScript({
|
||||
name: path,
|
||||
kind: 'plugin',
|
||||
releaseVersion: 'v2.0.0',
|
||||
targetRelPath: path,
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('promoted plugin')
|
||||
});
|
||||
if (path === 'replace.dll') replacementPluginId = file.script.id;
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(file.version.id, file.script.id);
|
||||
@@ -1791,6 +1837,21 @@ describe('native instance provisioning', () => {
|
||||
const job = await queue();
|
||||
await advance();
|
||||
await acknowledge(job.deploymentId);
|
||||
const replacement = uploadNewVersion(
|
||||
replacementPluginId,
|
||||
Buffer.from('requested plugin'),
|
||||
undefined,
|
||||
'v2.1.0'
|
||||
);
|
||||
const addition = createScript({
|
||||
name: 'New script',
|
||||
kind: 'script',
|
||||
releaseVersion: '3.2.1',
|
||||
targetRelPath: 'new.gsc',
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: Buffer.from('new script')
|
||||
});
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at) VALUES('files-rebuild','promote',?)"
|
||||
@@ -1803,9 +1864,19 @@ describe('native instance provisioning', () => {
|
||||
.run(job.name);
|
||||
const { queueFileChange } = await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const changes = [
|
||||
{ kind: 'plugin', relPath: 'replace.dll', content: Buffer.from('requested plugin') },
|
||||
{
|
||||
kind: 'plugin',
|
||||
relPath: 'replace.dll',
|
||||
content: Buffer.from('requested plugin'),
|
||||
scriptVersionId: replacement.id
|
||||
},
|
||||
{ kind: 'plugin', relPath: 'remove.dll', content: null },
|
||||
{ kind: 'script', relPath: 'new.gsc', content: Buffer.from('new script') },
|
||||
{
|
||||
kind: 'script',
|
||||
relPath: 'new.gsc',
|
||||
content: Buffer.from('new script'),
|
||||
scriptVersionId: addition.version.id
|
||||
},
|
||||
{ kind: 'config', relPath: 'custom.cfg', content: Buffer.from('set custom 1') }
|
||||
] as const;
|
||||
for (const change of changes)
|
||||
@@ -1839,6 +1910,21 @@ describe('native instance provisioning', () => {
|
||||
if (change.content === null) expect(actual).toBeUndefined();
|
||||
else expect(Buffer.from(actual!.content!, 'base64')).toEqual(change.content);
|
||||
}
|
||||
expect(recipe.files.find((file) => file.path === 'replace.dll')).toMatchObject({
|
||||
versionId: replacement.id,
|
||||
version: replacement.version,
|
||||
releaseVersion: 'v2.1.0'
|
||||
});
|
||||
expect(recipe.files.find((file) => file.path === 'new.gsc')).toMatchObject({
|
||||
versionId: addition.version.id,
|
||||
version: addition.version.version,
|
||||
releaseVersion: '3.2.1'
|
||||
});
|
||||
expect(recipe.files.find((file) => file.path === 'custom.cfg')).toMatchObject({
|
||||
versionId: null,
|
||||
version: null,
|
||||
releaseVersion: null
|
||||
});
|
||||
const files = getHostOperation(staged.stage_operation_id)!.payload.files as Array<{
|
||||
id: number;
|
||||
path: string;
|
||||
|
||||
@@ -15,13 +15,25 @@ it('names campaigns with their script or beta and gives maintenance a readable t
|
||||
rolled_back_by: null,
|
||||
script_name: null,
|
||||
script_version: null,
|
||||
script_release_version: null,
|
||||
script_kind: null,
|
||||
scripts: [],
|
||||
servers: []
|
||||
};
|
||||
expect(campaignTitle(campaign)).toBe('Promote · Beta #74');
|
||||
expect(
|
||||
campaignTitle({ ...campaign, scripts: [{ script_name: 'Double Tap', script_version: 2 }] })
|
||||
).toBe('Promote · Double Tap v2');
|
||||
campaignTitle({
|
||||
...campaign,
|
||||
scripts: [
|
||||
{
|
||||
script_name: 'Double Tap',
|
||||
script_version: 2,
|
||||
script_release_version: '1.3.0',
|
||||
script_kind: 'script'
|
||||
}
|
||||
]
|
||||
})
|
||||
).toBe('Promote · Double Tap 1.3.0 (upload 2)');
|
||||
expect(
|
||||
campaignTitle({
|
||||
...campaign,
|
||||
@@ -32,6 +44,38 @@ it('names campaigns with their script or beta and gives maintenance a readable t
|
||||
expect(campaignTitle({ ...campaign, action: 'drift' })).toBe('Maintenance repair');
|
||||
});
|
||||
|
||||
it('uses release labels for legacy campaigns and preserves config version labels', () => {
|
||||
const campaign = {
|
||||
action: 'promote',
|
||||
beta_id: null,
|
||||
rolled_back_by: null,
|
||||
script_name: 'Double Tap',
|
||||
script_version: 11,
|
||||
script_release_version: '2.4.1',
|
||||
script_kind: 'script',
|
||||
scripts: [],
|
||||
servers: []
|
||||
};
|
||||
expect(campaignTitle(campaign)).toBe('Promote · Double Tap 2.4.1 (upload 11)');
|
||||
expect(campaignTitle({ ...campaign, script_release_version: null })).toBe(
|
||||
'Promote · Double Tap Unversioned (upload 11)'
|
||||
);
|
||||
expect(campaignTitle({ ...campaign, script_version: null })).toBe('Promote · Double Tap');
|
||||
expect(
|
||||
campaignTitle({
|
||||
...campaign,
|
||||
scripts: [
|
||||
{
|
||||
script_name: 'Server settings',
|
||||
script_version: 4,
|
||||
script_release_version: null,
|
||||
script_kind: 'config'
|
||||
}
|
||||
]
|
||||
})
|
||||
).toBe('Promote · Server settings v4');
|
||||
});
|
||||
|
||||
it('summarizes progress without letting deleted servers create false alarms or false completion', () => {
|
||||
const campaign = {
|
||||
rolled_back_by: null,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
import LaunchRecipe from '../../src/lib/ui/components/LaunchRecipe.svelte';
|
||||
import type { LaunchFileSummary, LaunchRecipeSummary } from '../../src/lib/deployment-plan.js';
|
||||
|
||||
const file = (overrides: Partial<LaunchFileSummary>): LaunchFileSummary => ({
|
||||
name: 'Script',
|
||||
kind: 'script',
|
||||
path: 'script.gsc',
|
||||
versionId: 20,
|
||||
version: 11,
|
||||
sha256: 'a'.repeat(64),
|
||||
size: 1200,
|
||||
...overrides
|
||||
});
|
||||
const recipe: LaunchRecipeSummary = {
|
||||
game: 't5',
|
||||
mapSlug: 'theater',
|
||||
mapName: 'Kino der Toten',
|
||||
expectedMap: 'zombie_theater',
|
||||
capturedAt: '2026-10-17T12:00:00Z',
|
||||
image: 'runtime:latest',
|
||||
configFile: 'dedicated_sp.cfg',
|
||||
configSource: 'Production',
|
||||
fingerprint: 'b'.repeat(64),
|
||||
files: [
|
||||
file({ releaseVersion: '1.2.3' }),
|
||||
file({ kind: 'plugin', releaseVersion: 'v3.1.0' }),
|
||||
file({ version: 2 }),
|
||||
file({ kind: 'config', version: 3 }),
|
||||
file({ kind: 'config', versionId: null, version: null })
|
||||
]
|
||||
};
|
||||
|
||||
it.each([false, true])(
|
||||
'shows release versions and preserves legacy/generated labels in historical=%s launch files',
|
||||
(historical) => {
|
||||
const body = render(LaunchRecipe, { props: { recipe, historical } }).body.replace(
|
||||
/\s+/g,
|
||||
' '
|
||||
);
|
||||
expect(body).toContain('1.2.3 (upload 11) · script');
|
||||
expect(body).toContain('v3.1.0 (upload 11) · plugin');
|
||||
expect(body).toContain('Unversioned (upload 2) · script');
|
||||
expect(body).toContain('v3 · config');
|
||||
expect(body).toContain('Generated · config');
|
||||
expect(body).not.toContain('v11 ·');
|
||||
expect(body).not.toContain('v2 ·');
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user