test: expand Drupal corpus proofs and native package regression

This commit is contained in:
Forge
2026-09-22 13:15:58 +00:00
parent 7e674aa0fa
commit e72c064546
5 changed files with 324 additions and 1 deletions
+2
View File
@@ -7,6 +7,8 @@ on:
branches: [feature/extensions-author-kit-20260922]
paths:
- 'src/**'
- 'extensions/drupal/**'
- 'scripts/build-extensions.mjs'
- '__tests__/**'
- 'scripts/validation/**'
- 'package*.json'
+255
View File
@@ -0,0 +1,255 @@
{
"pathauto": {
"flows": [
[
"/admin/config/search/path/settings",
"pathauto.routing.yml",
"buildForm",
"src/Form/PathautoSettingsForm.php",
"Drupal route handler",
"/admin/config/search/path/settings PathautoSettingsForm.buildForm"
],
[
"/admin/config/search/path/update_bulk",
"pathauto.routing.yml",
"buildForm",
"src/Form/PathautoBulkUpdateForm.php",
"Drupal route handler",
"/admin/config/search/path/update_bulk PathautoBulkUpdateForm.buildForm"
],
[
"getPunctuationCharacters",
"src/AliasCleaner.php",
"pathautoPunctuationCharsAlter",
"tests/modules/pathauto_custom_punctuation_test/src/Hook/PathautoCustomPunctuationTestHooks.php",
"Drupal hook pathauto_punctuation_chars_alter",
"AliasCleaner.getPunctuationCharacters PathautoCustomPunctuationTestHooks.pathautoPunctuationCharsAlter"
]
],
"extra": [
[
"PathautoGenerator",
"src/PathautoGenerator.php",
"AliasCleaner",
"src/AliasCleaner.php",
"Drupal service injection"
],
[
"pathauto_update_8100",
"pathauto.install",
"AliasTypeManager",
"src/AliasTypeManager.php",
"Drupal service lookup"
],
[
"service:pathauto.alias_cleaner",
"pathauto.services.yml",
"AliasCleaner",
"src/AliasCleaner.php",
"Drupal service implementation"
],
[
"PathautoGenerator",
"src/PathautoGenerator.php",
"AliasUniquifier",
"src/AliasUniquifier.php",
"Drupal service injection"
],
[
"Action:pathauto_update_alias",
"src/Plugin/Action/UpdateAction.php",
"UpdateAction",
"src/Plugin/Action/UpdateAction.php",
"Drupal plugin implementation"
],
[
"getPunctuationCharacters",
"src/AliasCleaner.php",
"pathauto_custom_punctuation_test_pathauto_punctuation_chars_alter",
"tests/modules/pathauto_custom_punctuation_test/pathauto_custom_punctuation_test.module",
"Drupal hook pathauto_punctuation_chars_alter"
]
],
"negative": [
[
"/admin/config/search/path/patterns/add",
"pathauto.routing.yml",
"Drupal route handler",
"_entity_form"
],
[
"buildForm",
"src/Form/PathautoBulkUpdateForm.php",
"Drupal plugin construction",
"createInstance($id)"
]
]
},
"commerce": {
"flows": [
[
"/cart",
"modules/cart/commerce_cart.routing.yml",
"cartPage",
"modules/cart/src/Controller/CartController.php",
"Drupal route handler",
"/cart CartController.cartPage"
],
[
"/checkout/{commerce_order}/{step}",
"modules/checkout/commerce_checkout.routing.yml",
"formPage",
"modules/checkout/src/Controller/CheckoutController.php",
"Drupal route handler",
"/checkout/{commerce_order}/{step} CheckoutController.formPage"
],
[
"/admin/commerce/orders/add",
"modules/order/commerce_order.routing.yml",
"buildForm",
"modules/order/src/Form/OrderAddForm.php",
"Drupal route handler",
"/admin/commerce/orders/add OrderAddForm.buildForm"
]
],
"extra": [
[
"CurrentCountry",
"src/CurrentCountry.php",
"ChainCountryResolver",
"src/Resolver/ChainCountryResolver.php",
"Drupal service injection"
],
[
"Block:commerce_cart",
"modules/cart/src/Plugin/Block/CartBlock.php",
"CartBlock",
"modules/cart/src/Plugin/Block/CartBlock.php",
"Drupal plugin implementation"
],
[
"commerce_cart_cron",
"modules/cart/commerce_cart.module",
"Cron",
"modules/cart/src/Cron.php",
"Drupal service lookup"
],
[
"CurrentLocale",
"src/CurrentLocale.php",
"ChainLocaleResolver",
"src/Resolver/ChainLocaleResolver.php",
"Drupal service injection"
],
[
"QueueWorker:commerce_cart_expiration",
"modules/cart/src/Plugin/QueueWorker/CartExpiration.php",
"CartExpiration",
"modules/cart/src/Plugin/QueueWorker/CartExpiration.php",
"Drupal plugin implementation"
],
[
"addOrderItem",
"modules/cart/src/CartManager.php",
"displayAddToCartMessage",
"modules/cart/src/EventSubscriber/CartEventSubscriber.php",
"Drupal event commerce_cart.entity.add"
]
],
"negative": [
[
"/admin/commerce/config/products",
"modules/product/commerce_product.routing.yml",
"Drupal route handler",
"SystemController::systemAdminMenuBlockPage"
]
]
},
"drupal": {
"flows": [
[
"setAccount",
"core/lib/Drupal/Core/Session/AccountProxy.php",
"setDefaultTimeZone",
"core/modules/system/src/TimeZoneResolver.php",
"Drupal event account.set",
"AccountProxy.setAccount TimeZoneResolver.setDefaultTimeZone"
],
[
"filterResponse",
"core/modules/big_pipe/src/Render/BigPipe.php",
"onResponse",
"core/lib/Drupal/Core/EventSubscriber/ActiveLinkResponseFilter.php",
"Drupal event Symfony\\Component\\HttpKernel\\KernelEvents::RESPONSE",
"BigPipe.filterResponse ActiveLinkResponseFilter.onResponse"
],
[
"finish",
"core/lib/Drupal/Core/Config/ConfigImporter.php",
"onConfigImporterImport",
"core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php",
"Drupal event config.importer.import",
"ConfigImporter.finish ConfigSnapshotSubscriber.onConfigImporterImport"
]
],
"extra": [
[
"/admin/modules",
"core/modules/system/system.routing.yml",
"buildForm",
"core/modules/system/src/Form/ModulesListForm.php",
"Drupal route handler"
],
[
"service:renderer",
"core/core.services.yml",
"Renderer",
"core/lib/Drupal/Core/Render/Renderer.php",
"Drupal service implementation"
],
[
"ConfigActionManager",
"core/lib/Drupal/Core/Config/Action/ConfigActionManager.php",
"ConfigManager",
"core/lib/Drupal/Core/Config/ConfigManager.php",
"Drupal service injection"
],
[
"_batch_page",
"core/includes/batch.inc",
"BatchStorage",
"core/lib/Drupal/Core/Batch/BatchStorage.php",
"Drupal service lookup"
],
[
"_filter_url",
"core/modules/filter/filter.module",
"FilterUrl",
"core/modules/filter/src/Plugin/Filter/FilterUrl.php",
"Drupal plugin construction"
],
[
"Filter:filter_url",
"core/modules/filter/src/Plugin/Filter/FilterUrl.php",
"FilterUrl",
"core/modules/filter/src/Plugin/Filter/FilterUrl.php",
"Drupal plugin implementation"
],
[
"drupal_flush_all_caches",
"core/includes/common.inc",
"cacheFlush",
"core/modules/locale/src/Hook/LocaleHooks.php",
"Drupal hook cache_flush"
]
],
"negative": [
[
"invokeAllDeprecated",
"core/lib/Drupal/Core/Extension/ModuleHandler.php",
"Drupal hook ",
"invokeAll($hook"
]
]
}
}
@@ -0,0 +1,36 @@
// Three separately receipted phases so a task interruption cannot erase progress.
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),crypto=require('node:crypto');
const {DatabaseSync}=require('node:sqlite'); const {CodeGraph}=require('../../dist');
const [project,phase,output]=process.argv.slice(2);fs.mkdirSync(path.dirname(output),{recursive:true});
const files=['review_convergence.php','review_convergence.routing.yml'];
function snapshot(){const db=new DatabaseSync(path.join(project,'.codegraph/codegraph.db'),{readOnly:true});try{
const nodes=db.prepare('SELECT * FROM nodes ORDER BY id').all().map(({updated_at,...n})=>n);
const edges=db.prepare('SELECT source,target,kind,metadata,line,col,provenance FROM edges ORDER BY source,target,kind,line,col,metadata').all();
return {hash:crypto.createHash('sha256').update(JSON.stringify({nodes,edges})).digest('hex'),nodes,edges};
}finally{db.close();}}
(async()=>{const record=fs.existsSync(output)?JSON.parse(fs.readFileSync(output)):{project,phases:[]};let g;
try{
if(phase==='add'){
assert.equal(record.phases.length,0);record.original=snapshot().hash;
for(const file of files)assert.ok(!fs.existsSync(path.join(project,file)));
fs.writeFileSync(path.join(project,files[0]),`<?php
namespace Drupal\\review;
class ReviewSubscriber { public static function getSubscribedEvents() { return ['review.convergence' => 'onReview']; } public function onReview($event) {} }
class ReviewDispatch { public function send($dispatcher,$event) { $dispatcher->dispatch('review.convergence', $event); } }
`);
fs.writeFileSync(path.join(project,files[1]),"review.route:\n path: '/__review_unknown'\n defaults:\n _controller: '\\Missing\\Unknown::missing'\n");
}else assert.ok(['rebuild','remove'].includes(phase)&&record.incremental);
if(phase==='remove'){for(const file of files)fs.unlinkSync(path.join(project,file));}
const start=performance.now();g=await CodeGraph.open(project);
const result=phase==='rebuild'?await g.refreshPluginIndex():await g.sync({paths:files});
assert.ok(result?.success!==false&&!result?.filesErrored);g.close();g=null;
const state=snapshot();
if(phase==='add'){
const route=state.nodes.find(n=>n.name==='/__review_unknown');assert.ok(route);assert.ok(!state.edges.some(e=>e.source===route.id));
const send=state.nodes.find(n=>n.name==='send'&&n.file_path===files[0]),receive=state.nodes.find(n=>n.name==='onReview'&&n.file_path===files[0]);assert.ok(send&&receive);
assert.equal(state.edges.filter(e=>e.source===send.id&&e.target===receive.id&&JSON.parse(e.metadata||'{}').label==='Drupal event review.convergence').length,1);
record.incremental=state.hash;
}else assert.equal(state.hash,phase==='rebuild'?record.incremental:record.original,'Incremental/full convergence');
record.phases.push({phase,hash:state.hash,wallMs:performance.now()-start});record.success=record.phases.length===3;
}catch(e){record.failure=String(e.stack);throw e;}finally{g?.close();fs.writeFileSync(output,JSON.stringify(record,null,2));}
})().catch(e=>{console.error(e);process.exitCode=1;});
@@ -0,0 +1,28 @@
// Read-only source-grounded probes over an already indexed disposable corpus.
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),crypto=require('node:crypto');
const {DatabaseSync}=require('node:sqlite');
const {CodeGraph}=require('../../dist');const {ToolHandler}=require('../../dist/mcp/tools');
const [name,project,output]=process.argv.slice(2),cases=require('./drupal-review-cases.json')[name];
const db=new DatabaseSync(path.join(project,'.codegraph/codegraph.db'),{readOnly:true});
const hash=b=>crypto.createHash('sha256').update(b).digest('hex');
const record={name,project,positive:[],negative:[],probes:[],success:false};
function evidence(file,line){const bytes=fs.readFileSync(path.join(project,file));const lines=bytes.toString().split('\n');assert.ok(line>=1&&line<=lines.length);return {file,line,sha256:hash(bytes),excerpt:lines.slice(Math.max(0,line-3),line+3).join('\n')};}
(async()=>{try{
const edges=db.prepare("SELECT s.name source,s.file_path sourceFile,s.start_line sourceLine,t.name target,t.file_path targetFile,t.start_line targetLine,e.source sourceId,e.target targetId,e.kind,e.line,e.metadata FROM edges e JOIN nodes s ON e.source=s.id JOIN nodes t ON e.target=t.id WHERE json_extract(e.metadata,'$.synthesizedBy')='drupal'").all();
const seen=new Set();for(const e of edges){const key=[e.sourceId,e.targetId,e.kind,e.line].join('|');assert.ok(!seen.has(key),'Duplicate extension edge '+key);seen.add(key);}
for(const [source,sf,target,tf,label]of [...cases.flows,...cases.extra]){
const matches=edges.filter(e=>e.source===source&&e.sourceFile===sf&&e.target===target&&e.targetFile===tf&&JSON.parse(e.metadata).label===label);assert.equal(matches.length,1,'Expected unique grounded edge '+JSON.stringify([source,sf,target,tf,label]));
const e=matches[0],m=JSON.parse(e.metadata),at=/^(.*):(\d+)$/.exec(m.registeredAt);assert.ok(at);
record.positive.push({source,target,label,kind:e.kind,from:evidence(sf,e.sourceLine),to:evidence(tf,e.targetLine),wiring:evidence(at[1],Number(at[2]))});
}
for(const [source,file,labelPrefix,text]of cases.negative){
assert.ok(fs.readFileSync(path.join(project,file),'utf8').includes(text),'Negative has source evidence');
assert.ok(db.prepare('SELECT 1 FROM nodes WHERE name=? AND file_path=?').get(source,file),'Negative source node exists');
assert.equal(edges.filter(e=>e.source===source&&e.sourceFile===file&&JSON.parse(e.metadata).label.startsWith(labelPrefix)).length,0,'Unsupported/dynamic target must stay unresolved');
record.negative.push({source,file,labelPrefix,text,sha256:hash(fs.readFileSync(path.join(project,file)))});
}
const graph=await CodeGraph.open(project);try{
for(const c of cases.flows){const response=await new ToolHandler(graph).execute('codegraph_explore',{query:c[5]});const text=response.content.map(v=>v.text||'').join('\n');assert.ok(!response.isError&&text.includes(c[4]),'Expected labelled flow');record.probes.push({query:c[5],text});}
}finally{graph.close();}
record.edgeCount=edges.length;record.noDuplicateKeys=true;record.success=true;
}finally{db.close();fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,JSON.stringify(record,null,2));}})().catch(e=>{console.error(e);process.exitCode=1;});
+3 -1
View File
@@ -21,7 +21,7 @@ const playwright = process.env.PLAYWRIGHT_MODULE || path.join(root, '.qa/browser
env.PLAYWRIGHT_MODULE = playwright;
env.CHROME_PATH = process.env.CHROME_PATH || require(playwright).chromium.executablePath();
async function run(name, command, args, timeout = 360000, overrides = {}) {
if (metadata.scope === 'marketplace' && !['build','marketplace-build','focused','registry-storage','external-author','compatibility','browser'].includes(name)) {
if (metadata.scope === 'marketplace' && !['build','marketplace-build','drupal-package','drupal-accuracy','focused','registry-storage','external-author','compatibility','browser'].includes(name)) {
metadata.notRun.push({ name, reason: 'Unchanged core lifecycle; retain prior native evidence instead of rerunning' }); save(); return true;
}
const step = { name, command: [command, ...args], cwd: root, revision: version, overrides, started: new Date().toISOString(), status: 'running' };
@@ -45,6 +45,8 @@ async function run(name, command, args, timeout = 360000, overrides = {}) {
const built = await run('build', ...npmCommand(['run', 'build']));
if (built) {
await run('marketplace-build', ...npmCommand(['run', 'build', '--prefix', 'marketplace']));
await run('drupal-package', process.execPath, ['scripts/build-extensions.mjs']);
await run('drupal-accuracy', process.execPath, ['--test', 'scripts/validation/drupal-accuracy.test.cjs', 'scripts/validation/drupal-negative.test.cjs']);
await run('focused', process.execPath, ['node_modules/vitest/vitest.mjs', 'run',
'__tests__/marketplace-storage.test.ts', '__tests__/foundation.test.ts', '__tests__/extension-trust.test.ts', '__tests__/extension-releases.test.ts', '__tests__/extension-marketplace.test.ts', '__tests__/extension-author.test.ts', '__tests__/plugins.test.ts', '__tests__/extension-explore.test.ts',
'__tests__/db-reopen-on-replace.test.ts', '__tests__/status-json.test.ts', '__tests__/sync.test.ts', '__tests__/concurrent-locking.test.ts',