@givolt/i3-composer-agent 2.3.1 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude/commands/i3-prototype.md +5 -7
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-prototype.toml +5 -7
- package/README.md +16 -7
- package/bin/agent-targets.json +4 -2
- package/bin/i3-composer-agent.cjs +74 -7
- package/bin/install.cjs +16 -1
- package/bin/native-layout.cjs +1 -1
- package/bin/native-migration.cjs +27 -3
- package/bin/tool-selector.cjs +24 -7
- package/bin/wizard.cjs +89 -15
- package/commands/i3-prototype.toml +5 -7
- package/docs/codebuddy-setup.md +91 -3
- package/docs/codex-setup.md +6 -0
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
- package/docs/ui-prototype-native/USAGE.md +93 -0
- package/docs/workbuddy-setup.md +6 -0
- package/evals/cases/i3-frontend-ui-engineering.json +16 -0
- package/evals/cases/i3-ui-prototype.json +109 -0
- package/evals/cases/i3-using-agent-skills.json +28 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
- package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
- package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
- package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
- package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
- package/hooks/codebuddy-upstream.cjs +93 -0
- package/hooks/i3-prototype.cjs +12 -0
- package/hooks/manage.cjs +162 -0
- package/package.json +61 -1
- package/plugin.json +1 -1
- package/scripts/design-provider-test.js +7 -0
- package/scripts/enterprise-template-test.js +27 -1
- package/scripts/lib/prototype-fixture.cjs +17 -0
- package/scripts/prototype-cache-test.js +55 -0
- package/scripts/prototype-generation-entry-test.js +6 -0
- package/scripts/prototype-generation-evidence-test.js +10 -0
- package/scripts/prototype-generation-native-test.js +78 -0
- package/scripts/prototype-generation-test.js +99 -0
- package/scripts/prototype-inventory-test.js +43 -0
- package/scripts/prototype-locations-test.js +79 -0
- package/scripts/prototype-project-test.js +183 -0
- package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
- package/scripts/ui-prototype-content-test.js +123 -0
- package/scripts/ui-prototype-dependencies-test.js +213 -0
- package/scripts/ui-prototype-devtools-test.js +53 -0
- package/scripts/ui-prototype-execution-evidence-test.js +62 -0
- package/scripts/ui-prototype-execution-gates-test.js +66 -0
- package/scripts/ui-prototype-execution-policy-test.js +42 -0
- package/scripts/ui-prototype-execution-runtime-test.js +27 -0
- package/scripts/ui-prototype-ide-source-test.js +73 -0
- package/scripts/ui-prototype-native-test.js +504 -0
- package/scripts/ui-prototype-registration-test.js +53 -0
- package/scripts/ui-prototype-template-test.js +261 -0
- package/scripts/ui-prototype-test.js +10 -0
- package/scripts/ui-prototype-transcript-auto-test.js +67 -0
- package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
- package/skills/i3-incremental-implementation/SKILL.md +1 -1
- package/skills/i3-ui-prototype/SKILL.md +158 -19
- package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
- package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
- package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
- package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
- package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
- package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
- package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
- package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
- package/skills/i3-ui-prototype/references/content-template.md +81 -0
- package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
- package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
- package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
- package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
- package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
- package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
- package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
- package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
- package/skills/i3-ui-prototype/references/project-layout.md +92 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
- package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
- package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
- package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
- package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
- package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
- package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
- package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
- package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
- package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
- package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
- package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
- package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
- package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
- package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
- package/skills/i3-using-agent-skills/SKILL.md +2 -5
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const legacyFixture = require('./lib/prototype-fixture.cjs');
|
|
9
|
+
const installationFile = path.join(__dirname, 'lib/installation.js');
|
|
10
|
+
const skillsDirectory = fs.existsSync(installationFile) ? require(installationFile).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
|
|
11
|
+
const lib = require(path.join(skillsDirectory, 'i3-ui-prototype/scripts/native-pilot-lib.cjs'));
|
|
12
|
+
function project(t) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'native-prototype-')); t.after(() => fs.rmSync(dir, { recursive:true, force:true })); return dir; }
|
|
13
|
+
test('analysis is read-only even if no prototype exists', t => {
|
|
14
|
+
const dir=project(t); const before=fs.readdirSync(dir);
|
|
15
|
+
assert.equal(lib.analyze({project:dir}).operation,'analyze');
|
|
16
|
+
assert.deepEqual(fs.readdirSync(dir),before);
|
|
17
|
+
});
|
|
18
|
+
test('theme requires a sourced optional capability and fixes its generated configuration and acceptance', t => {
|
|
19
|
+
const dir=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir}),contract=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs'));
|
|
20
|
+
const plain=lib.createPrototype({project:dir,id:'plain',templateRoot:template.directory,expertMode:'template'}).directory;
|
|
21
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(plain,'app/public/prototype-config.json'))).theme,undefined);
|
|
22
|
+
const plainBefore=lib.manifest(plain),plainAcceptance=JSON.parse(fs.readFileSync(path.join(plain,'inputs/acceptance.json')));
|
|
23
|
+
assert.equal(plainAcceptance.requiredScenarios.length+plainAcceptance.frameworkScenarios.length,20);
|
|
24
|
+
const file=path.join(template.directory,'index.html');fs.writeFileSync(file,fs.readFileSync(file,'utf8').replace(/<!-- OPTIONAL_FOUR_THEMES:[\s\S]*?(<aside[\s\S]*?<\/aside>)\s*-->/,'$1'));contract.syncTemplate(template.directory);
|
|
25
|
+
assert.throws(()=>lib.createPrototype({project:dir,id:'unsourced-theme',templateRoot:template.directory,expertMode:'template'}),/theme.*source|主题.*来源/i);
|
|
26
|
+
const scope={schemaVersion:1,requirements:[{id:'THEME-1',text:'四种命名主题,默认明亮',source:{type:'user',reference:'theme-request',statement:'明亮、暗黑、清新、科技'},capabilities:[{pageId:'devices',regionId:'device-list',capabilityId:'theme'}]}],exemptions:[]};
|
|
27
|
+
write(dir,'scope.json',scope);
|
|
28
|
+
const themed=lib.createPrototype({project:dir,id:'themed',templateRoot:template.directory,expertMode:'template',scope:path.join(dir,'scope.json')}).directory;
|
|
29
|
+
const config=JSON.parse(fs.readFileSync(path.join(themed,'app/public/prototype-config.json')));
|
|
30
|
+
assert.deepEqual(config.theme,{defaultTheme:'light',presets:['light','dark','fresh','tech']});
|
|
31
|
+
const browser=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-browser.cjs'));
|
|
32
|
+
assert.deepEqual(browser.themePlan(themed),{source:'inputs/capabilities.json',bindingId:'devices/device-list/theme',defaultTheme:'light',presets:['light','dark','fresh','tech']});
|
|
33
|
+
assert.equal(browser.themePlan(plain),null);
|
|
34
|
+
assert.equal(lib.inspect(themed).status,'passed');
|
|
35
|
+
const themedAcceptance=JSON.parse(fs.readFileSync(path.join(themed,'inputs/acceptance.json')));
|
|
36
|
+
assert.equal(themedAcceptance.requiredScenarios.filter(id=>id.includes('/theme/')).length,3);
|
|
37
|
+
assert.equal(themedAcceptance.requiredScenarios.length+themedAcceptance.frameworkScenarios.length,23);
|
|
38
|
+
assert.deepEqual(themedAcceptance.requiredScenarios.filter(id=>!id.includes('/theme/')),plainAcceptance.requiredScenarios,'The four-theme opt-in cannot remove baseline scenarios');
|
|
39
|
+
assert.deepEqual(themedAcceptance.browserReview.observations,['dom','screenshot','console','network','accessibility','performance']);
|
|
40
|
+
assert.equal(lib.manifest(plain).digest,plainBefore.digest,'New theme inputs must not mutate an existing unbound instance');
|
|
41
|
+
assert.equal(lib.inspect(plain).status,'passed');
|
|
42
|
+
config.theme.defaultTheme='dark';write(themed,'app/public/prototype-config.json',config);assert.equal(lib.inspect(themed).status,'failed','runtime configuration cannot override the fixed binding');
|
|
43
|
+
});
|
|
44
|
+
test('analysis leaves host execution unverified unless explicitly declared and rejects invalid modes without writes', t => {
|
|
45
|
+
const dir=project(t),before=fs.readdirSync(dir);
|
|
46
|
+
const unknown=lib.analyze({project:dir});
|
|
47
|
+
assert.equal(unknown.execution,null,'the script cannot infer independent host invocation');
|
|
48
|
+
assert.equal(unknown.executionEvidence,'unverified');
|
|
49
|
+
for(const execution of ['inline','delegated']) {
|
|
50
|
+
const result=lib.analyze({project:dir,execution});
|
|
51
|
+
assert.equal(result.execution,execution);assert.equal(result.executionEvidence,'caller-declared');
|
|
52
|
+
assert.equal(result.readOnly,true);assert.deepEqual(result.sideEffects,[]);
|
|
53
|
+
}
|
|
54
|
+
for(const execution of ['agent',true,null,''])assert.throws(()=>lib.analyze({project:dir,execution}),/execution|inline|delegated/);
|
|
55
|
+
assert.deepEqual(fs.readdirSync(dir),before);
|
|
56
|
+
});
|
|
57
|
+
test('new fixed content projection acceptance cannot be dropped while legacy inputs retain their original list', t => {
|
|
58
|
+
const dir=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir});
|
|
59
|
+
const root=lib.createPrototype({project:dir,id:'projection-guard',templateRoot:template.directory,expertMode:'template'}).directory;
|
|
60
|
+
const acceptance=JSON.parse(fs.readFileSync(path.join(root,'inputs/acceptance.json'))),framework=JSON.parse(fs.readFileSync(path.join(root,'inputs/framework.json')));
|
|
61
|
+
acceptance.frameworkScenarios=acceptance.frameworkScenarios.filter(id=>id!=='template-content-projection');write(root,'inputs/acceptance.json',acceptance);
|
|
62
|
+
let current=lib.manifest(root);
|
|
63
|
+
assert.throws(()=>lib.revise(root,{revision:'r002',expectedRevision:'r001',expectedDigest:current.digest,acceptInputs:true}),/template-content-projection/);
|
|
64
|
+
framework.acceptance.scenarios=framework.acceptance.scenarios.filter(scenario=>scenario.id!=='template-content-projection');write(root,'inputs/framework.json',framework);
|
|
65
|
+
current=lib.manifest(root);
|
|
66
|
+
lib.revise(root,{revision:'r002',expectedRevision:'r001',expectedDigest:current.digest,acceptInputs:true});
|
|
67
|
+
assert.equal(lib.inspect(root).status,'passed','An older fixed framework snapshot is not implicitly given a new scenario');
|
|
68
|
+
});
|
|
69
|
+
test('browser pagination expectations come from fixed instance bindings and retain legacy fixed semantics', t => {
|
|
70
|
+
const dir=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir});
|
|
71
|
+
const browser=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-browser.cjs'));
|
|
72
|
+
const fixed=lib.createPrototype({project:dir,id:'fixed-devices',templateRoot:template.directory,expertMode:'template'}).directory;
|
|
73
|
+
assert.deepEqual(browser.paginationPlan(fixed),{source:'inputs/capabilities.json',bindingId:'devices/device-list/pagination',mode:'fixed',defaultPageSize:10,pageSizeOptions:[10]});
|
|
74
|
+
const html=path.join(template.directory,'index.html');
|
|
75
|
+
fs.writeFileSync(html,fs.readFileSync(html,'utf8').replace('"fixedPageSize":10,"minimumTestPages":3','"mode":"adjustable","defaultPageSize":10,"pageSizeOptions":[10,20,50],"minimumTestPages":3'));
|
|
76
|
+
require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')).syncTemplate(template.directory);
|
|
77
|
+
const adjustable=lib.createPrototype({project:dir,id:'adjustable-devices',templateRoot:template.directory,expertMode:'template'}).directory;
|
|
78
|
+
assert.equal(lib.inspect(adjustable).status,'passed');
|
|
79
|
+
const config=path.join(adjustable,'app/public/prototype-config.json'),data=JSON.parse(fs.readFileSync(config));
|
|
80
|
+
data.pagination={mode:'fixed',fixedPageSize:99};fs.writeFileSync(config,JSON.stringify(data));
|
|
81
|
+
assert.deepEqual(browser.paginationPlan(adjustable),{source:'inputs/capabilities.json',bindingId:'devices/device-list/pagination',mode:'adjustable',defaultPageSize:10,pageSizeOptions:[10,20,50]},'page runtime self-report cannot change the acceptance contract');
|
|
82
|
+
});
|
|
83
|
+
test('pagination focus regression is enabled only by the fixed framework snapshot', t => {
|
|
84
|
+
const dir=project(t),browser=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-browser.cjs'));
|
|
85
|
+
write(dir,'inputs/framework.json',{acceptance:{paginationFocusGuard:true}});assert.equal(browser.paginationFocusPlan(dir),true);
|
|
86
|
+
write(dir,'inputs/framework.json',{acceptance:{}});assert.equal(browser.paginationFocusPlan(dir),false,'Legacy snapshots do not gain a new guard');
|
|
87
|
+
for(const value of [false,'true',1]){write(dir,'inputs/framework.json',{acceptance:{paginationFocusGuard:value}});assert.throws(()=>browser.paginationFocusPlan(dir),/focus guard/);}
|
|
88
|
+
});
|
|
89
|
+
test('native candidate fixes inputs and excludes runtime progress from its identity', t => {
|
|
90
|
+
const dir=project(t); const template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir});
|
|
91
|
+
const created=lib.createPrototype({project:dir,id:'devices',templateRoot:template.directory,expertMode:'template'});
|
|
92
|
+
assert.equal(created.status,'created'); const candidate=lib.manifest(created.directory);
|
|
93
|
+
assert.equal(lib.inspect(created.directory).status,'passed');
|
|
94
|
+
fs.writeFileSync(path.join(created.directory,'execution.json'),JSON.stringify({owner:'other-progress',status:'running'}));
|
|
95
|
+
fs.mkdirSync(path.join(created.directory,'qa'),{recursive:true}); fs.writeFileSync(path.join(created.directory,'qa','progress.log'),'testing');
|
|
96
|
+
assert.equal(lib.manifest(created.directory).digest,candidate.digest);
|
|
97
|
+
fs.appendFileSync(path.join(template.directory,'index.html'),'\n<!-- future template -->');
|
|
98
|
+
assert.equal(lib.manifest(created.directory).digest,candidate.digest);
|
|
99
|
+
fs.appendFileSync(path.join(created.directory,'app','src','App.vue'),'\n<!-- user change -->');
|
|
100
|
+
assert.notEqual(lib.manifest(created.directory).digest,candidate.digest);
|
|
101
|
+
});
|
|
102
|
+
test('a stale revision cannot overwrite a candidate and revise preserves actual source edits', t => {
|
|
103
|
+
const dir=project(t); const template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir});
|
|
104
|
+
const created=lib.createPrototype({project:dir,id:'devices',templateRoot:template.directory,expertMode:'template'});
|
|
105
|
+
const initial=lib.manifest(created.directory);
|
|
106
|
+
fs.appendFileSync(path.join(created.directory,'app','src','App.vue'),'\n<!-- keep me -->');
|
|
107
|
+
assert.throws(()=>lib.revise(created.directory,{revision:'r002',expectedRevision:'r001',expectedDigest:initial.digest}),/changed|stale|冲突|变化/);
|
|
108
|
+
const current=lib.manifest(created.directory);
|
|
109
|
+
lib.revise(created.directory,{revision:'r002',expectedRevision:'r001',expectedDigest:current.digest});
|
|
110
|
+
assert.match(fs.readFileSync(path.join(created.directory,'app','src','App.vue'),'utf8'),/keep me/);
|
|
111
|
+
assert.equal(lib.manifest(created.directory).revision,'r002');
|
|
112
|
+
});
|
|
113
|
+
test('required capability deletion and input mutation cannot reuse the old contract', t => {
|
|
114
|
+
const dir=project(t); const template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir});
|
|
115
|
+
const created=lib.createPrototype({project:dir,id:'devices',templateRoot:template.directory,expertMode:'template'});
|
|
116
|
+
const initial=lib.manifest(created.directory);
|
|
117
|
+
const file=path.join(created.directory,'inputs','capabilities.json'); const data=JSON.parse(fs.readFileSync(file));
|
|
118
|
+
data.bindings=data.bindings.filter(binding=>binding.capabilityId!=='pagination'); fs.writeFileSync(file,JSON.stringify(data));
|
|
119
|
+
assert.notEqual(lib.manifest(created.directory).digest,initial.digest);
|
|
120
|
+
assert.equal(lib.inspect(created.directory).status,'failed');
|
|
121
|
+
});
|
|
122
|
+
test('build manifests verify bytes, and a build for an old candidate is rejected', t => {
|
|
123
|
+
const dir=project(t); const template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:dir});
|
|
124
|
+
const created=lib.createPrototype({project:dir,id:'devices',templateRoot:template.directory,expertMode:'template'});
|
|
125
|
+
assert.equal(lib.inspect(created.directory,{acceptance:true}).status,'failed');
|
|
126
|
+
assert.throws(()=>lib.verifyBuild(created.directory,'missing'),/build|构建|ENOENT/);
|
|
127
|
+
assert.throws(()=>lib.confirmBaseline(created.directory,{revision:'r001',digest:lib.manifest(created.directory).digest}),/confirmation|确认|reviewer/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// These are synthetic guard fixtures, never runtime or user-acceptance evidence.
|
|
131
|
+
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
132
|
+
function write(directory, relative, value) {
|
|
133
|
+
const file=path.join(directory,relative); fs.mkdirSync(path.dirname(file),{recursive:true});
|
|
134
|
+
fs.writeFileSync(file,Buffer.isBuffer(value)||typeof value==='string'?value:JSON.stringify(value));
|
|
135
|
+
}
|
|
136
|
+
function candidate(t) {
|
|
137
|
+
const root=project(t); const template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root});
|
|
138
|
+
return lib.createPrototype({project:root,id:'devices',templateRoot:template.directory,expertMode:'template'}).directory;
|
|
139
|
+
}
|
|
140
|
+
function syntheticBuild(directory,{embedded=true}={}) {
|
|
141
|
+
const current=lib.manifest(directory);
|
|
142
|
+
const bytes={
|
|
143
|
+
'index.html':Buffer.from('<!doctype html><script type="module" src="/assets/app.js"></script><link rel="stylesheet" href="/assets/app.css">'),
|
|
144
|
+
'assets/app.js':Buffer.from(`window.__PROTOTYPE_META__={candidateId:${JSON.stringify(embedded?current.digest:'previous-candidate')}};`),
|
|
145
|
+
'assets/app.css':Buffer.from('body{color:#123}'),
|
|
146
|
+
'prototype-config.json':fs.readFileSync(path.join(directory,'app/public/prototype-config.json')),
|
|
147
|
+
};
|
|
148
|
+
const files=Object.fromEntries(Object.entries(bytes).sort(([a],[b])=>a.localeCompare(b)).map(([name,value])=>[name,hash(value)]));
|
|
149
|
+
const buildDigest=hash(JSON.stringify(files)),buildId=`b-${buildDigest.slice(0,20)}`;
|
|
150
|
+
for(const [name,value]of Object.entries(bytes))write(directory,`builds/${buildId}/site/${name}`,value);
|
|
151
|
+
write(directory,`builds/${buildId}/manifest.json`,{schemaVersion:1,buildId,buildDigest,candidateDigest:current.digest,inputDigest:current.inputDigest,revision:current.revision,files,source:'synthetic-unit-fixture',checks:{typecheck:'synthetic contract fixture; not executed',test:'synthetic contract fixture; not executed',build:'synthetic contract fixture; not executed'}});
|
|
152
|
+
return {current,buildId,buildDigest,files};
|
|
153
|
+
}
|
|
154
|
+
function syntheticReport(directory,build=syntheticBuild(directory)) {
|
|
155
|
+
const png=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=','base64');
|
|
156
|
+
write(directory,'qa/fixtures/a.png',png);write(directory,'qa/fixtures/b.png',png);
|
|
157
|
+
write(directory,'qa/fixtures/trace.zip',Buffer.concat([Buffer.from([0x50,0x4b,0x05,0x06]),Buffer.alloc(18)]));
|
|
158
|
+
const acceptance=JSON.parse(fs.readFileSync(path.join(directory,'inputs/acceptance.json')));
|
|
159
|
+
const previewBaseURL='http://127.0.0.1:4173/';
|
|
160
|
+
write(directory,'last-build.json',{buildId:build.buildId,buildDigest:build.buildDigest,candidateDigest:build.current.digest});
|
|
161
|
+
if(acceptance.browserReview){
|
|
162
|
+
const devtools=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-devtools.cjs')),observations={};
|
|
163
|
+
for(const kind of devtools.KINDS){const evidence=`qa/fixtures/devtools-${kind}.${kind==='screenshot'?'png':kind==='performance'?'json':'txt'}`;write(directory,evidence,kind==='screenshot'?png:kind==='performance'?{traceEvents:[{name:'synthetic-contract-fixture',ts:1}]}:`Synthetic ${kind} fixture; not executed`);observations[kind]={status:'passed',summary:'Synthetic guard fixture only; not actual browser acceptance',evidence};}
|
|
164
|
+
devtools.record(directory,{schemaVersion:1,status:'passed',revision:build.current.revision,candidateDigest:build.current.digest,buildId:build.buildId,buildDigest:build.buildDigest,executor:'synthetic-unit-fixture',startedAt:'2026-10-17T00:00:00.000Z',completedAt:'2026-10-17T00:00:01.000Z',previewBaseURL,skill:{name:acceptance.browserReview.skill,toolCallId:'synthetic-skill',evidence:'Synthetic fixture; no host invocation'},mcp:{name:'chrome-devtools-mcp',version:'1.11.0',evidence:'Synthetic fixture; no host invocation',calls:['evaluate_script','take_screenshot','list_console_messages','list_network_requests','take_snapshot','performance_start_trace','performance_stop_trace'].map((method,index)=>({tool:`mcp__chrome_devtools__${method}`,toolCallId:`synthetic-${index}`}))},observations,openItems:[]},lib);
|
|
165
|
+
}
|
|
166
|
+
return {id:'synthetic-guard-fixture',status:'passed',candidateDigest:build.current.digest,revision:build.current.revision,buildId:build.buildId,buildDigest:build.buildDigest,executor:'unit-fixture',tool:'unit-fixture',source:'synthetic-unit-fixture',startedAt:'2026-10-17T00:00:00.000Z',completedAt:'2026-10-17T00:00:01.000Z',consoleErrors:[],journeys:[...acceptance.requiredScenarios,...acceptance.frameworkScenarios].map(id=>({id,status:'passed'})),screenshots:['qa/fixtures/a.png','qa/fixtures/b.png'],trace:'qa/fixtures/trace.zip',viewports:[1440,768],previewBaseURL,observedResources:Object.entries(build.files).map(([name,sha256])=>({path:name,url:new URL(name==='index.html'?'':name,previewBaseURL).href,status:200,sha256,resourceType:name==='index.html'?'document':name.endsWith('.js')?'script':name.endsWith('.css')?'stylesheet':'fetch'}))};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
test('new policy blocks finalization and human baseline when DevTools review is missing or stale',t=>{
|
|
170
|
+
const directory=candidate(t),report=syntheticReport(directory);lib.recordRun(directory,report);
|
|
171
|
+
const current=lib.manifest(directory),file=path.join(directory,'qa/devtools-review.json'),valid=fs.readFileSync(file);
|
|
172
|
+
fs.rmSync(file);assert.equal(lib.state(directory).nextAction,'devtools-review');assert.throws(()=>lib.finalize(directory),/DevTools review required/);
|
|
173
|
+
assert.throws(()=>lib.confirmBaseline(directory,{revision:current.revision,digest:current.digest,reviewer:'synthetic-fixture',statement:'Synthetic approval fixture only',source:'unit-test'}),/DevTools review required/);
|
|
174
|
+
assert.equal(fs.existsSync(path.join(directory,'qa/delivery.json')),false);fs.writeFileSync(file,valid);
|
|
175
|
+
const delivery=lib.finalize(directory);assert.equal(delivery.checks.devtoolsReview,'current');assert.equal(lib.state(directory).delivery.status,'current');
|
|
176
|
+
fs.appendFileSync(path.join(directory,'qa/fixtures/devtools-dom.txt'),'changed artifact');assert.equal(lib.state(directory).delivery.status,'stale');assert.throws(()=>lib.finalize(directory),/DevTools review required/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('old fixed inputs without browserReview retain their delivery contract',t=>{
|
|
180
|
+
const directory=candidate(t),acceptance=JSON.parse(fs.readFileSync(path.join(directory,'inputs/acceptance.json')));delete acceptance.browserReview;write(directory,'inputs/acceptance.json',acceptance);
|
|
181
|
+
const framework=JSON.parse(fs.readFileSync(path.join(directory,'inputs/framework.json')));delete framework.acceptance.browserReview;write(directory,'inputs/framework.json',framework);
|
|
182
|
+
const edited=lib.manifest(directory);lib.revise(directory,{revision:'r002',expectedRevision:edited.revision,expectedDigest:edited.digest,acceptInputs:true});
|
|
183
|
+
const report=syntheticReport(directory);lib.recordRun(directory,report);
|
|
184
|
+
assert.equal(lib.state(directory).devtoolsReview.status,'not-required');const result=lib.finalize(directory);assert.equal(result.status,'complete');assert.equal(result.devtoolsReview,undefined);
|
|
185
|
+
});
|
|
186
|
+
test('required DevTools policy cannot disappear from only the derived acceptance input',t=>{
|
|
187
|
+
const directory=candidate(t),acceptance=JSON.parse(fs.readFileSync(path.join(directory,'inputs/acceptance.json')));delete acceptance.browserReview;write(directory,'inputs/acceptance.json',acceptance);
|
|
188
|
+
const current=lib.manifest(directory);assert.throws(()=>lib.revise(directory,{revision:'r002',expectedRevision:current.revision,expectedDigest:current.digest,acceptInputs:true}),/DevTools|browser review/i);assert.equal(lib.manifest(directory).revision,'r001');
|
|
189
|
+
});
|
|
190
|
+
test('a finalized receipt becomes stale if its valid browser report bytes change',t=>{
|
|
191
|
+
const directory=candidate(t),report=syntheticReport(directory);lib.recordRun(directory,report);lib.finalize(directory);
|
|
192
|
+
const file=path.join(directory,'qa/runs',report.id,'report.json'),changed=JSON.parse(fs.readFileSync(file));changed.executor='different-synthetic-executor';write(directory,path.relative(directory,file),changed);
|
|
193
|
+
assert.equal(lib.state(directory).browser.status,'current','the edited report remains structurally valid');
|
|
194
|
+
assert.equal(lib.state(directory).delivery.status,'stale','the saved receipt binds the exact report bytes');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('professional review requires recorded decisions and verifies adopted guidance; zero adoption remains valid', t => {
|
|
198
|
+
const directory=candidate(t),meta=JSON.parse(fs.readFileSync(path.join(directory,'prototype.json')));
|
|
199
|
+
meta.expertMode='professional';write(directory,'prototype.json',meta);
|
|
200
|
+
const query={query:'error summary validation',source:'synthetic-unit-provider',resultDigest:hash('fixture-result')};
|
|
201
|
+
const advice={provider:{availability:'available',digest:hash('provider')},queries:[query],records:[],status:'reviewed'};
|
|
202
|
+
write(directory,'qa/design-advice.json',advice);write(directory,'design-system/decisions.json',{reviewStatus:'reviewed',records:[]});
|
|
203
|
+
assert.equal(lib.inspect(directory,{acceptance:true,report:syntheticReport(directory)}).status,'failed','empty decisions must not pass as reviewed');
|
|
204
|
+
const rejected={query:query.query,resultIdentity:'fixture result',decision:'rejected',rationale:'Already covered by the native form contract',verification:'No additional implementation required'};
|
|
205
|
+
advice.records=[rejected];write(directory,'qa/design-advice.json',advice);
|
|
206
|
+
assert.equal(lib.inspect(directory,{acceptance:true,report:syntheticReport(directory)}).status,'failed','QA alone cannot change design decisions');
|
|
207
|
+
write(directory,'design-system/decisions.json',{reviewStatus:'reviewed',records:[rejected]});
|
|
208
|
+
assert.equal(lib.inspect(directory,{acceptance:true,report:syntheticReport(directory)}).status,'passed');
|
|
209
|
+
lib.recordRun(directory,syntheticReport(directory));
|
|
210
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(directory,'prototype.json'))).expert.status,'reviewed','zero adoption must remain reviewed after real report validation');
|
|
211
|
+
const adopted={...rejected,decision:'adopted',implementation:'app/src/App.vue'};
|
|
212
|
+
advice.records=[adopted];write(directory,'qa/design-advice.json',advice);write(directory,'design-system/decisions.json',{reviewStatus:'reviewed',records:[adopted]});
|
|
213
|
+
assert.equal(lib.inspect(directory,{acceptance:true,report:syntheticReport(directory)}).status,'failed','adoption needs a real verified scenario');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('failed staging releases the project lock so a repaired directory can be retried', t => {
|
|
217
|
+
const root=project(t);legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root});
|
|
218
|
+
fs.writeFileSync(path.join(root,'prototypes'),'blocking file');
|
|
219
|
+
assert.throws(()=>lib.createPrototype({project:root,id:'devices',templateRoot:path.join(root,'ui-templates/admin-list'),expertMode:'template'}),/EEXIST/);
|
|
220
|
+
assert.equal(fs.existsSync(path.join(root,'.i3-ui-prototypes.lock')),false);
|
|
221
|
+
fs.rmSync(path.join(root,'prototypes'));
|
|
222
|
+
assert.equal(lib.createPrototype({project:root,id:'devices',templateRoot:path.join(root,'ui-templates/admin-list'),expertMode:'template'}).status,'created');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('all source resources participate while only explicit root caches are excluded', t => {
|
|
226
|
+
const directory=candidate(t);
|
|
227
|
+
for(const relative of ['app/src/copy.log','app/src/dist/label.txt','app/public/coverage/text.txt'])write(directory,relative,'first');
|
|
228
|
+
const before=lib.manifest(directory);
|
|
229
|
+
for(const relative of ['app/src/copy.log','app/src/dist/label.txt','app/public/coverage/text.txt'])assert.ok(Object.hasOwn(before.files,relative),relative);
|
|
230
|
+
write(directory,'app/src/copy.log','changed');assert.notEqual(lib.manifest(directory).digest,before.digest);
|
|
231
|
+
const source=lib.manifest(directory).digest;
|
|
232
|
+
write(directory,'app/node_modules/fake.js','dependency cache');write(directory,'app/dist/output.js','build output');
|
|
233
|
+
assert.equal(lib.manifest(directory).digest,source);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('requirements map references actual pages and rejects unknown page identities', t => {
|
|
237
|
+
const directory=candidate(t),file=path.join(directory,'requirements-map.json');
|
|
238
|
+
const map=JSON.parse(fs.readFileSync(file));
|
|
239
|
+
assert.deepEqual(map.requirements[0].pages,['devices']);
|
|
240
|
+
map.requirements[0].pages=['missing-page'];fs.writeFileSync(file,JSON.stringify(map));
|
|
241
|
+
assert.equal(lib.inspect(directory).status,'failed');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('a build claiming current metadata must actually embed its candidate identity in JavaScript', t => {
|
|
245
|
+
const directory=candidate(t),build=syntheticBuild(directory,{embedded:false});
|
|
246
|
+
assert.throws(()=>lib.verifyBuild(directory,build.buildId),/embed|candidate|identity|标识/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('browser guard rejects missing, stale or mismatched resource observations before publishing', t => {
|
|
250
|
+
const directory=candidate(t),report=syntheticReport(directory);
|
|
251
|
+
assert.equal(lib.inspect(directory,{acceptance:true,report}).status,'passed');
|
|
252
|
+
lib.recordRun(directory,report);
|
|
253
|
+
const latest=fs.readFileSync(path.join(directory,'qa/latest.json'),'utf8');
|
|
254
|
+
const badReports=[
|
|
255
|
+
{...report,id:'missing-observations',observedResources:[]},
|
|
256
|
+
{...report,id:'missing-stylesheet',observedResources:report.observedResources.filter(entry=>!entry.path.endsWith('.css'))},
|
|
257
|
+
{...report,id:'wrong-bytes',observedResources:report.observedResources.map((entry,index)=>index?entry:{...entry,sha256:'0'.repeat(64)})},
|
|
258
|
+
{...report,id:'other-server',observedResources:report.observedResources.map(entry=>({...entry,url:entry.url.replace(':4173',':9999')}))},
|
|
259
|
+
{...report,id:'bad-status',observedResources:report.observedResources.map(entry=>({...entry,status:304}))},
|
|
260
|
+
{...report,id:'wrong-request-type',observedResources:report.observedResources.map(entry=>entry.path.endsWith('.js')?{...entry,resourceType:'fetch'}:entry)},
|
|
261
|
+
{...report,id:'bad-time',startedAt:'invented'},
|
|
262
|
+
];
|
|
263
|
+
for(const bad of badReports){assert.throws(()=>lib.recordRun(directory,bad));assert.equal(fs.readFileSync(path.join(directory,'qa/latest.json'),'utf8'),latest);assert.equal(fs.existsSync(path.join(directory,`qa/runs/${bad.id}/report.json`)),false);}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('nonempty text is refused as PNG or trace evidence', t => {
|
|
267
|
+
const directory=candidate(t),report=syntheticReport(directory);
|
|
268
|
+
fs.writeFileSync(path.join(directory,report.screenshots[0]),'not a PNG');
|
|
269
|
+
assert.throws(()=>lib.recordRun(directory,report),/PNG|screenshot/);
|
|
270
|
+
const valid=syntheticReport(directory);
|
|
271
|
+
fs.writeFileSync(path.join(directory,valid.trace),'not a ZIP');
|
|
272
|
+
assert.throws(()=>lib.recordRun(directory,valid),/ZIP|trace/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('failed baseline staging leaves no partial revision and can be retried', t => {
|
|
276
|
+
const directory=candidate(t),report=syntheticReport(directory);lib.recordRun(directory,report);
|
|
277
|
+
const current=lib.manifest(directory),confirmation={revision:current.revision,digest:current.digest,reviewer:'synthetic-fixture',statement:'Synthetic unit test only; not actual human acceptance',source:'unit-test'};
|
|
278
|
+
const original=fs.copyFileSync;
|
|
279
|
+
try{fs.copyFileSync=()=>{throw new Error('simulated copy failure');};assert.throws(()=>lib.confirmBaseline(directory,confirmation),/simulated copy failure/);}finally{fs.copyFileSync=original;}
|
|
280
|
+
assert.equal(fs.existsSync(path.join(directory,'baselines/r001')),false);
|
|
281
|
+
assert.equal(lib.confirmBaseline(directory,confirmation).digest,current.digest);
|
|
282
|
+
assert.equal(lib.compareBaseline(directory).status,'unchanged');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('editing and syncing HTML projects supported content into new prototypes without mutating old ones', t => {
|
|
286
|
+
const root=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root});
|
|
287
|
+
const first=lib.createPrototype({project:root,id:'first',templateRoot:template.directory,expertMode:'template'});
|
|
288
|
+
const configFile=path.join(first.directory,'app/public/prototype-config.json');
|
|
289
|
+
const original=fs.readFileSync(configFile,'utf8');
|
|
290
|
+
assert.equal(JSON.parse(original).content.fields.name.label,'设备名称');
|
|
291
|
+
const htmlFile=path.join(template.directory,'index.html');
|
|
292
|
+
fs.writeFileSync(htmlFile,fs.readFileSync(htmlFile,'utf8')
|
|
293
|
+
.replace('<h1>设备管理</h1>','<h1>资产台账</h1>')
|
|
294
|
+
.replace('data-field-id="device-name">设备名称','data-field-id="device-name">资产名称')
|
|
295
|
+
.replace('data-action-id="query" type="button">查询','data-action-id="query" type="button">检索'));
|
|
296
|
+
require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')).syncTemplate(template.directory);
|
|
297
|
+
const second=lib.createPrototype({project:root,id:'second',templateRoot:template.directory,expertMode:'template'});
|
|
298
|
+
const updated=JSON.parse(fs.readFileSync(path.join(second.directory,'app/public/prototype-config.json')));
|
|
299
|
+
assert.equal(updated.title,'资产台账');
|
|
300
|
+
assert.equal(updated.content.fields.name.label,'资产名称');
|
|
301
|
+
assert.equal(updated.content.actions.query,'检索');
|
|
302
|
+
assert.equal(fs.readFileSync(configFile,'utf8'),original);
|
|
303
|
+
assert.equal(lib.inspect(first.directory).status,'passed');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('unsupported template fields require explicit framework adaptation instead of silently disappearing', t => {
|
|
307
|
+
const root=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root}),htmlFile=path.join(template.directory,'index.html');
|
|
308
|
+
fs.writeFileSync(htmlFile,fs.readFileSync(htmlFile,'utf8').replace('data-field-id="device-status">状态</th>','data-field-id="device-status">状态</th><th data-field-id="device-owner">负责人</th>'));
|
|
309
|
+
require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')).syncTemplate(template.directory);
|
|
310
|
+
assert.throws(()=>lib.createPrototype({project:root,id:'unsupported',templateRoot:template.directory,expertMode:'template'}),/unsupported|adaptation|适配/i);
|
|
311
|
+
assert.equal(fs.existsSync(path.join(root,'prototypes/unsupported')),false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('HTML cannot silently remove the required name field validation in the pilot projection', t => {
|
|
315
|
+
const root=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root}),htmlFile=path.join(template.directory,'index.html');
|
|
316
|
+
fs.writeFileSync(htmlFile,fs.readFileSync(htmlFile,'utf8').replace('value="一号循环泵" required','value="一号循环泵"'));
|
|
317
|
+
require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')).syncTemplate(template.directory);
|
|
318
|
+
assert.throws(()=>lib.createPrototype({project:root,id:'no-validation',templateRoot:template.directory,expertMode:'template'}),/required|必填|adaptation/i);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('a changed fixed input requires explicit acceptance and failed acceptance does not advance revision', t => {
|
|
322
|
+
const directory=candidate(t),scopeFile=path.join(directory,'inputs/scope.json');
|
|
323
|
+
const scope=JSON.parse(fs.readFileSync(scopeFile));scope.openQuestions=['Explicitly added review question'];fs.writeFileSync(scopeFile,JSON.stringify(scope));
|
|
324
|
+
const current=lib.manifest(directory),options={revision:'r002',expectedRevision:'r001',expectedDigest:current.digest};
|
|
325
|
+
assert.throws(()=>lib.revise(directory,options),/input|contract|输入/i);
|
|
326
|
+
assert.equal(lib.manifest(directory).revision,'r001');
|
|
327
|
+
const updated=lib.revise(directory,{...options,acceptInputs:true});
|
|
328
|
+
assert.equal(updated.revision,'r002');assert.equal(lib.inspect(directory).status,'passed');
|
|
329
|
+
const file=path.join(directory,'inputs/capabilities.json'),capabilities=JSON.parse(fs.readFileSync(file));
|
|
330
|
+
capabilities.bindings=capabilities.bindings.filter(binding=>binding.capabilityId!=='pagination');fs.writeFileSync(file,JSON.stringify(capabilities));
|
|
331
|
+
const invalid=lib.manifest(directory);
|
|
332
|
+
assert.throws(()=>lib.revise(directory,{revision:'r003',expectedRevision:'r002',expectedDigest:invalid.digest,acceptInputs:true}),/binding|capability|pagination/i);
|
|
333
|
+
assert.equal(lib.manifest(directory).revision,'r002');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('CLI check rejects a stale generated index without updating it', async t => {
|
|
337
|
+
const root=project(t),template=legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root}),file=path.join(template.directory,'capabilities.generated.json');
|
|
338
|
+
const before=fs.readFileSync(file,'utf8');fs.appendFileSync(path.join(template.directory,'index.html'),'\n<!-- edit -->');
|
|
339
|
+
const cli=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-pilot.cjs'));
|
|
340
|
+
await assert.rejects(cli.main(['sync-template',template.directory,'--check']),/stale|不同步/);
|
|
341
|
+
assert.equal(fs.readFileSync(file,'utf8'),before);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('an explicit title is projected to the visible page and unsupported frameworks write nothing', t => {
|
|
345
|
+
const root=project(t);legacyFixture.prepareTemplate(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/template-contract.cjs')), {project:root});
|
|
346
|
+
assert.throws(()=>lib.createPrototype({project:root,id:'wrong',framework:'react-material-ui',expertMode:'template'}),/framework|框架|unsupported/i);
|
|
347
|
+
assert.equal(fs.existsSync(path.join(root,'prototypes')),false);assert.equal(fs.existsSync(path.join(root,'.i3-ui-prototypes.json')),false);
|
|
348
|
+
const {directory}=lib.createPrototype({project:root,id:'titled',templateRoot:path.join(root,'ui-templates/admin-list'),title:'明确指定的新标题',expertMode:'template'});
|
|
349
|
+
const config=JSON.parse(fs.readFileSync(path.join(directory,'app/public/prototype-config.json')));
|
|
350
|
+
assert.equal(config.title,'明确指定的新标题');assert.equal(config.content.pageTitle,config.title);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('ordinary revise can start repair of broken user code without accepting changed contracts', t => {
|
|
354
|
+
const directory=candidate(t);fs.rmSync(path.join(directory,'app/src/App.vue'));
|
|
355
|
+
const current=lib.manifest(directory);
|
|
356
|
+
assert.equal(lib.revise(directory,{revision:'r002',expectedRevision:current.revision,expectedDigest:current.digest}).revision,'r002');
|
|
357
|
+
assert.equal(lib.inspect(directory).status,'failed');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('a failing real npm test prevents the build command and fixed build publication', t => {
|
|
361
|
+
const directory=candidate(t),app=path.join(directory,'app'),file=path.join(app,'package.json');
|
|
362
|
+
const pkg=JSON.parse(fs.readFileSync(file));
|
|
363
|
+
pkg.scripts={typecheck:'node -e "process.exit(0)"',test:'node -e "process.exit(3)"',build:'node build-test.cjs'};
|
|
364
|
+
fs.writeFileSync(file,JSON.stringify(pkg));
|
|
365
|
+
fs.writeFileSync(path.join(app,'build-test.cjs'),'require("node:fs").writeFileSync("build-ran.txt","unexpected build execution");\n');
|
|
366
|
+
fs.mkdirSync(path.join(app,'node_modules/vite'),{recursive:true});
|
|
367
|
+
assert.throws(()=>lib.build(directory),/npm test failed/);
|
|
368
|
+
assert.equal(fs.existsSync(path.join(app,'build-ran.txt')),false);
|
|
369
|
+
assert.equal(fs.existsSync(path.join(directory,'last-build.json')),false);
|
|
370
|
+
assert.equal(fs.existsSync(path.join(directory,'builds')),false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('a build manifest missing its required test step cannot be reused for acceptance', t => {
|
|
374
|
+
const directory=candidate(t),build=syntheticBuild(directory),file=path.join(directory,`builds/${build.buildId}/manifest.json`);
|
|
375
|
+
const data=JSON.parse(fs.readFileSync(file));delete data.checks.test;fs.writeFileSync(file,JSON.stringify(data));
|
|
376
|
+
assert.throws(()=>lib.verifyBuild(directory,build.buildId),/test|check|步骤/);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('state is a read-only compact handoff with exact current commands', async t => {
|
|
380
|
+
const directory=candidate(t),before=lib.manifest(directory);
|
|
381
|
+
const tree=()=>Object.fromEntries(require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-files.cjs')).walk(directory).map(file=>[file,hash(fs.readFileSync(path.join(directory,file)))]));
|
|
382
|
+
const snapshot=tree();
|
|
383
|
+
const result=lib.state(directory);
|
|
384
|
+
assert.equal(result.candidateDigest,before.digest);
|
|
385
|
+
assert.equal(result.inputDigest,before.inputDigest);
|
|
386
|
+
assert.equal(result.revision,'r001');
|
|
387
|
+
assert.equal(result.design.status,'skipped');
|
|
388
|
+
assert.equal(result.build.status,'not-run');
|
|
389
|
+
assert.equal(result.browser.status,'not-run');
|
|
390
|
+
assert.equal(result.nextAction,'build');
|
|
391
|
+
assert.ok(result.scopes.requirements.length);
|
|
392
|
+
assert.ok(result.commands.revise.argv.includes(before.digest));
|
|
393
|
+
assert.ok(result.commands.browser.argv.includes('browser'));
|
|
394
|
+
assert.deepEqual(tree(),snapshot,'state must not write progress, directories or host memory');
|
|
395
|
+
const cli=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-pilot.cjs'));
|
|
396
|
+
assert.equal((await cli.main(['state',directory])).candidateDigest,before.digest);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('state identifies pending professional review without inventing decisions', t => {
|
|
400
|
+
const directory=candidate(t),meta=JSON.parse(fs.readFileSync(path.join(directory,'prototype.json')));
|
|
401
|
+
meta.expertMode='professional';write(directory,'prototype.json',meta);
|
|
402
|
+
write(directory,'qa/design-advice.json',{provider:{availability:'available',digest:hash('fixture-provider')},queries:[],records:[],status:'pending'});
|
|
403
|
+
write(directory,'design-system/decisions.json',{reviewStatus:'pending',records:[]});
|
|
404
|
+
assert.equal(lib.state(directory).design.status,'pending');
|
|
405
|
+
assert.equal(lib.state(directory).nextAction,'review-design');
|
|
406
|
+
assert.throws(()=>lib.finalize(directory),/acceptance|验收/i);
|
|
407
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(directory,'qa/design-advice.json'))).status,'pending');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('finalize refuses unverified candidates and preserves user handoff on failure', t => {
|
|
411
|
+
const directory=candidate(t),file=path.join(directory,'HANDOFF.md'),before=fs.readFileSync(file,'utf8');
|
|
412
|
+
assert.throws(()=>lib.finalize(directory),/acceptance|verified|验收/i);
|
|
413
|
+
assert.equal(fs.readFileSync(file,'utf8'),before);
|
|
414
|
+
assert.equal(fs.existsSync(path.join(directory,'.prototype-write.lock')),false);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('finalize summarizes verified guard fixtures without changing identity or human approval', t => {
|
|
418
|
+
const directory=candidate(t),report=syntheticReport(directory);lib.recordRun(directory,report);
|
|
419
|
+
write(directory,'last-build.json',{buildId:report.buildId,buildDigest:report.buildDigest,candidateDigest:report.candidateDigest});
|
|
420
|
+
const before=lib.manifest(directory),file=path.join(directory,'HANDOFF.md');
|
|
421
|
+
fs.appendFileSync(file,'\nUSER_HANDOFF_NOTE: preserve this paragraph\n');
|
|
422
|
+
const result=lib.finalize(directory,{expectedRevision:before.revision,expectedDigest:before.digest});
|
|
423
|
+
assert.equal(result.status,'complete');assert.equal(result.candidateDigest,before.digest);
|
|
424
|
+
assert.equal(result.review,'pending-review');assert.equal(result.verification,'passed');
|
|
425
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(directory,'qa/delivery.json'))).candidateDigest,before.digest);
|
|
426
|
+
assert.equal(lib.state(directory).delivery.status,'current');
|
|
427
|
+
assert.equal(lib.state(directory).nextAction,'return-result');
|
|
428
|
+
assert.match(fs.readFileSync(file,'utf8'),/USER_HANDOFF_NOTE/);
|
|
429
|
+
assert.equal(lib.manifest(directory).digest,before.digest);
|
|
430
|
+
const text=fs.readFileSync(file,'utf8');lib.finalize(directory);
|
|
431
|
+
assert.equal(fs.readFileSync(file,'utf8'),text,'repeat finalize must not duplicate generated handoff');
|
|
432
|
+
assert.equal(fs.existsSync(path.join(directory,'baseline.json')),false);
|
|
433
|
+
fs.appendFileSync(path.join(directory,'app/src/App.vue'),'\n<!-- new user edit -->');
|
|
434
|
+
const changed=lib.state(directory);
|
|
435
|
+
assert.equal(changed.delivery.status,'stale');assert.equal(changed.nextAction,'build');
|
|
436
|
+
assert.throws(()=>lib.finalize(directory),/acceptance|stale|验收/i);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('browser and final handoff cannot write while another candidate owner is active', async t => {
|
|
440
|
+
const directory=candidate(t),report=syntheticReport(directory);lib.recordRun(directory,report);
|
|
441
|
+
const current=lib.manifest(directory);lib.claim(directory,{owner:'active-owner',expectedRevision:current.revision,expectedDigest:current.digest});
|
|
442
|
+
try {
|
|
443
|
+
assert.throws(()=>lib.finalize(directory),/EEXIST|owner|writing|锁/i);
|
|
444
|
+
const browser=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-browser.cjs'));
|
|
445
|
+
await assert.rejects(browser.main(directory,{playwrightModule:'definitely-unavailable'}),/EEXIST|owner|writing|锁/i);
|
|
446
|
+
} finally { lib.release(directory,'active-owner'); }
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('installed browser resolves only an existing project or explicit Playwright provider', t => {
|
|
450
|
+
const directory=candidate(t),browser=require(path.join(skillsDirectory,'i3-ui-prototype/scripts/native-browser.cjs'));
|
|
451
|
+
assert.throws(()=>browser.resolvePlaywright(directory,{playwrightModule:path.join(directory,'absent-provider')}),/Playwright|provider|模块/i);
|
|
452
|
+
const provider=path.join(directory,'app/node_modules/playwright');
|
|
453
|
+
write(provider,'package.json',{name:'playwright',version:'0.0.0-resolution-unit-fixture',main:'index.js'});
|
|
454
|
+
write(provider,'index.js','throw new Error("Resolution must not execute this unit fixture");');
|
|
455
|
+
assert.equal(browser.resolvePlaywright(directory).packageRoot,fs.realpathSync(provider));
|
|
456
|
+
assert.equal(browser.resolvePlaywright(directory,{playwrightModule:provider}).version,'0.0.0-resolution-unit-fixture');
|
|
457
|
+
const projectRoot=path.resolve(directory,'../..'),tools=path.join(projectRoot,'.prototype-tools/node_modules/playwright');
|
|
458
|
+
write(tools,'package.json',{name:'playwright',version:'0.0.0-tools-unit-fixture',main:'index.js'});
|
|
459
|
+
write(tools,'index.js','throw new Error("Resolution only; never execute the fixture");');
|
|
460
|
+
assert.equal(browser.resolvePlaywright(directory).packageRoot,fs.realpathSync(tools));
|
|
461
|
+
const currentTools=path.join(projectRoot,'.codebuddy/tools/i3-prototype/playwright/node_modules/playwright');
|
|
462
|
+
write(currentTools,'package.json',{name:'playwright',version:'0.0.0-current-tools-unit-fixture',main:'index.js'});
|
|
463
|
+
write(currentTools,'index.js','throw new Error("Resolution only; never execute the fixture");');
|
|
464
|
+
assert.equal(browser.resolvePlaywright(directory,{host:'codebuddy'}).packageRoot,fs.realpathSync(currentTools));
|
|
465
|
+
fs.rmSync(path.join(projectRoot,'.codebuddy/tools'),{recursive:true});
|
|
466
|
+
assert.equal(lib.state(directory).commands.browser.argv.includes(fs.realpathSync(tools)),true,'state prints the discovered project tool provider');
|
|
467
|
+
assert.equal(browser.resolvePlaywright(directory,{playwrightModule:provider}).packageRoot,fs.realpathSync(provider),'explicit provider wins');
|
|
468
|
+
fs.rmSync(tools,{recursive:true});fs.rmSync(provider,{recursive:true});
|
|
469
|
+
const projectProvider=path.join(projectRoot,'node_modules/playwright');
|
|
470
|
+
write(projectProvider,'package.json',{name:'playwright',version:'0.0.0-project-unit-fixture',main:'index.js'});
|
|
471
|
+
write(projectProvider,'index.js','throw new Error("Resolution only; never execute the fixture");');
|
|
472
|
+
assert.equal(browser.resolvePlaywright(directory).packageRoot,fs.realpathSync(projectProvider));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('approval applies only to its confirmed revision and digest after direct user edits', t => {
|
|
476
|
+
const directory=candidate(t),first=syntheticReport(directory);lib.recordRun(directory,first);
|
|
477
|
+
const approved=lib.manifest(directory);
|
|
478
|
+
lib.confirmBaseline(directory,{revision:approved.revision,digest:approved.digest,reviewer:'synthetic-unit-reviewer',statement:'Synthetic approval fixture only, never actual user approval',source:'unit-fixture'});
|
|
479
|
+
const baselineFile=path.join(directory,'baseline.json'),baseline=fs.readFileSync(baselineFile,'utf8');
|
|
480
|
+
assert.equal(lib.inspect(directory).review,'approved');
|
|
481
|
+
write(directory,'last-build.json',{buildId:first.buildId,buildDigest:first.buildDigest,candidateDigest:first.candidateDigest});
|
|
482
|
+
assert.equal(lib.finalize(directory).humanConfirmationRequired,false,'a matching existing approval remains valid');
|
|
483
|
+
fs.appendFileSync(path.join(directory,'app/src/App.vue'),'\n<!-- direct user edit, same revision -->');
|
|
484
|
+
const edited=lib.manifest(directory);assert.equal(edited.revision,approved.revision);assert.notEqual(edited.digest,approved.digest);
|
|
485
|
+
const state=lib.state(directory);
|
|
486
|
+
assert.equal(state.review.status,'pending-review','the previous human decision cannot approve new bytes');
|
|
487
|
+
assert.equal(lib.inspect(directory).review,'pending-review');
|
|
488
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(directory,'prototype.json'))).review.status,'approved','read-only state must preserve the historical stored approval');
|
|
489
|
+
const second=syntheticReport(directory);second.id='synthetic-after-user-edit';lib.recordRun(directory,second);
|
|
490
|
+
write(directory,'last-build.json',{buildId:second.buildId,buildDigest:second.buildDigest,candidateDigest:second.candidateDigest});
|
|
491
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(directory,'prototype.json'))).review.status,'pending-review','recordRun must not carry approval into the new candidate');
|
|
492
|
+
// Compatibility with an artifact already written by the previous runtime:
|
|
493
|
+
// fresh QA exists but its stored approval still refers to the old candidate.
|
|
494
|
+
const legacy=JSON.parse(fs.readFileSync(path.join(directory,'prototype.json')));
|
|
495
|
+
legacy.review={status:'approved',revision:approved.revision,digest:approved.digest};write(directory,'prototype.json',legacy);
|
|
496
|
+
const result=lib.finalize(directory);
|
|
497
|
+
assert.equal(result.review,'pending-review');assert.equal(result.humanConfirmationRequired,true);
|
|
498
|
+
assert.equal(result.candidateDigest,edited.digest);
|
|
499
|
+
const incorrectReceipt={...result,review:'approved',humanConfirmationRequired:false};write(directory,'qa/delivery.json',incorrectReceipt);
|
|
500
|
+
assert.equal(lib.state(directory).delivery.status,'stale','a receipt made by the old bug is not current');
|
|
501
|
+
assert.equal(lib.finalize(directory).humanConfirmationRequired,true);
|
|
502
|
+
assert.equal(fs.readFileSync(baselineFile,'utf8'),baseline,'do not move or rewrite the old human baseline');
|
|
503
|
+
assert.equal(lib.compareBaseline(directory).status,'changed');
|
|
504
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const R = require(prototypeScripts+'/codebuddy-registration.cjs');
|
|
6
|
+
const options = { action: 'enable', scope: 'project', product: 'ide', base: '/work/project/.codebuddy',
|
|
7
|
+
project: '/work/project', nodePath: '/runtime with spaces/node', scriptPath: '/work/project/.codebuddy/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs', scriptDigest: 'a'.repeat(64) };
|
|
8
|
+
test('registration planning preserves unrelated hooks and never grants permissions', () => {
|
|
9
|
+
const before = { theme: 'dark', hooks: { PreToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'original-check' }] }] } };
|
|
10
|
+
const result = R.plan({ ...options, currentText: JSON.stringify(before) });
|
|
11
|
+
const after = JSON.parse(result.afterText);
|
|
12
|
+
assert.equal(after.theme, 'dark'); assert.deepEqual(after.hooks.PreToolUse[0], before.hooks.PreToolUse[0]);
|
|
13
|
+
assert.ok(result.summary.events.includes('PostToolUse'));
|
|
14
|
+
assert.match(after.hooks.PreToolUse[1].hooks[0].command, /'\/runtime with spaces\/node'/);
|
|
15
|
+
assert.equal(result.recordAfter.active, undefined);
|
|
16
|
+
assert.equal(result.summary.status, 'registered');
|
|
17
|
+
});
|
|
18
|
+
test('repeated enable is idempotent, disable removes only owned groups', () => {
|
|
19
|
+
const first = R.plan({ ...options, currentText: '{"userOption":42}' });
|
|
20
|
+
const again = R.plan({ ...options, currentText: first.afterText, record: first.recordAfter });
|
|
21
|
+
assert.equal(again.afterText, first.afterText);
|
|
22
|
+
const removed = R.plan({ ...options, action: 'disable', currentText: again.afterText, record: again.recordAfter });
|
|
23
|
+
assert.equal(JSON.parse(removed.afterText).userOption, 42);
|
|
24
|
+
assert.equal(Object.values(JSON.parse(removed.afterText).hooks).flat().length, 0);
|
|
25
|
+
});
|
|
26
|
+
test('missing ownership and modified owned hooks cannot be force adopted', () => {
|
|
27
|
+
const first = R.plan({ ...options, currentText: '{}' });
|
|
28
|
+
assert.throws(() => R.plan({ ...options, currentText: first.afterText }), /ownership|Ownership/);
|
|
29
|
+
const edited = JSON.parse(first.afterText); edited.hooks.Stop[0].hooks[0].timeout = 9;
|
|
30
|
+
assert.throws(() => R.plan({ ...options, currentText: JSON.stringify(edited), record: first.recordAfter }), /modified|changed/);
|
|
31
|
+
assert.throws(() => R.plan({ ...options, currentText: '{broken' }), /JSON/);
|
|
32
|
+
});
|
|
33
|
+
test('global CLI registration and defaults remain separate from actual activation', () => {
|
|
34
|
+
const keep = R.plan({ ...options, action: 'keep', currentText: null });
|
|
35
|
+
assert.equal(keep.afterText, null); assert.equal(keep.summary.status, 'not-enabled');
|
|
36
|
+
const global = R.plan({ ...options, scope: 'global', base: '/user/.codebuddy', product: 'cli', currentText: '{}' });
|
|
37
|
+
assert.equal(global.summary.status, 'pending-host-review');
|
|
38
|
+
assert.match(global.recordAfter.groups.Stop.hooks[0].command, /--scope' 'global/);
|
|
39
|
+
assert.throws(() => R.plan({ ...options, product: 'workbuddy' }), /product/);
|
|
40
|
+
});
|
|
41
|
+
test('configuration transaction refuses stale previews and restores only its own failed write in memory', () => {
|
|
42
|
+
const change = R.plan({ ...options, currentText: '{}' });
|
|
43
|
+
const files = new Map([[change.settingsPath, '{}']]); let fail = true;
|
|
44
|
+
const io = { read: file => files.get(file) ?? null, write(file, text) {
|
|
45
|
+
if (file === change.recordPath && fail) { fail = false; throw new Error('record write failed'); }
|
|
46
|
+
if (text === null) files.delete(file); else files.set(file, text);
|
|
47
|
+
} };
|
|
48
|
+
assert.throws(() => R.apply(change, io), /record write failed/);
|
|
49
|
+
assert.equal(files.get(change.settingsPath), '{}');
|
|
50
|
+
files.set(change.settingsPath, '{"userEdit":true}');
|
|
51
|
+
assert.throws(() => R.apply(change, io), /changed after preview/);
|
|
52
|
+
assert.equal(files.get(change.settingsPath), '{"userEdit":true}');
|
|
53
|
+
});
|