dsh-agent-team-ui-testing 0.1.1 → 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -36
  3. package/annotation-capture.js +111 -0
  4. package/browser-safety.js +45 -1
  5. package/client.js +3721 -132
  6. package/context-menu.js +134 -0
  7. package/core/clock.js +88 -0
  8. package/core/config-store.js +566 -0
  9. package/core/config.js +892 -0
  10. package/core/contracts.js +240 -0
  11. package/core/errors.js +156 -0
  12. package/core/evidence.js +141 -0
  13. package/core/geometry.js +217 -0
  14. package/core/identity.js +180 -0
  15. package/core/index.js +24 -0
  16. package/core/p0.js +1101 -0
  17. package/core/readiness.js +175 -0
  18. package/core/registry.js +239 -0
  19. package/core/tab-access.js +1350 -0
  20. package/docs/adapters.md +160 -0
  21. package/docs/architecture.md +44 -9
  22. package/docs/capability-tiers.md +20 -4
  23. package/docs/capture-p0.md +39 -2
  24. package/docs/diagnose-reason-codes.md +157 -0
  25. package/docs/element-picker.md +24 -6
  26. package/docs/getting-started.md +47 -1
  27. package/docs/images/tutorial-01-overview.png +0 -0
  28. package/docs/images/tutorial-02-history-dropdown.png +0 -0
  29. package/docs/images/tutorial-03-pick-element.png +0 -0
  30. package/docs/images/tutorial-04-annotate.png +0 -0
  31. package/docs/index.md +11 -7
  32. package/docs/multi-tab.md +55 -20
  33. package/docs/network-p0.md +2 -2
  34. package/docs/panel-context-menu.md +41 -0
  35. package/docs/t2-structured-gui.md +2 -2
  36. package/docs/t3-vision.md +6 -1
  37. package/docs/t4-dynamic-behavior.md +1 -0
  38. package/docs/tools.md +65 -7
  39. package/docs/tutorial.md +125 -0
  40. package/docs/verification.md +26 -2
  41. package/docs/viewport-control.md +21 -8
  42. package/index.js +3263 -251
  43. package/integration/cross-environment.js +1535 -0
  44. package/integration/diagnose.js +945 -0
  45. package/integration/tab-principal.js +493 -0
  46. package/locale/en.json +1 -1
  47. package/locale/zh.json +1 -1
  48. package/network-p0.js +24 -7
  49. package/package.json +4 -3
  50. package/select-control.js +298 -0
  51. package/skills/ui-testing-common/SKILL.md +54 -0
  52. package/skills/ui-testing-fake/SKILL.md +43 -0
  53. package/skills/{ui-testing → ui-testing-web}/SKILL.md +64 -24
  54. package/skills/{ui-testing → ui-testing-web}/manifests/declared-capabilities.json +9 -2
  55. package/skills/{ui-testing → ui-testing-web}/references/contracts/capability.md +3 -3
  56. package/skills/{ui-testing → ui-testing-web}/references/contracts/evidence.md +1 -1
  57. package/skills/{ui-testing → ui-testing-web}/references/contracts/operation.md +1 -1
  58. package/skills/{ui-testing → ui-testing-web}/references/core/fallback.md +1 -1
  59. package/skills/{ui-testing → ui-testing-web}/references/core/page-identity.md +3 -2
  60. package/skills/{ui-testing → ui-testing-web}/references/core/selection.md +1 -1
  61. package/skills/{ui-testing → ui-testing-web}/references/core/tool-routing-and-limits.md +6 -3
  62. package/skills/{ui-testing → ui-testing-web}/references/platforms/index.md +1 -1
  63. package/skills/{ui-testing → ui-testing-web}/references/platforms/web.md +2 -2
  64. package/skills/ui-testing-web/references/scenarios/annotations.md +104 -0
  65. package/skills/{ui-testing → ui-testing-web}/references/scenarios/functional.md +1 -1
  66. package/skills/{ui-testing → ui-testing-web}/references/scenarios/visual-motion.md +1 -1
  67. package/skills/{ui-testing → ui-testing-web}/references/tiers/t1-programmatic.md +2 -2
  68. package/skills/{ui-testing → ui-testing-web}/references/tiers/t3-static-visual.md +2 -2
  69. package/skills/{ui-testing → ui-testing-web}/references/tiers/t4-dynamic-visual.md +2 -1
  70. package/targets/fake/adapter.js +1163 -0
  71. package/targets/fake/fixtures.js +177 -0
  72. package/targets/web/adapter.js +1231 -0
  73. package/targets/web/provider.js +415 -0
  74. package/skills/ui-testing/references/scenarios/annotations.md +0 -87
  75. /package/skills/{ui-testing → ui-testing-web}/references/core/dynamic-visual.md +0 -0
  76. /package/skills/{ui-testing → ui-testing-web}/references/core/network-and-state.md +0 -0
  77. /package/skills/{ui-testing → ui-testing-web}/references/core/report-template.md +0 -0
  78. /package/skills/{ui-testing → ui-testing-web}/references/core/reporting.md +0 -0
  79. /package/skills/{ui-testing → ui-testing-web}/references/core/safety.md +0 -0
  80. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-examples.md +0 -0
  81. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-gate.md +0 -0
  82. /package/skills/{ui-testing → ui-testing-web}/references/core/verification.md +0 -0
  83. /package/skills/{ui-testing → ui-testing-web}/references/core/workflow.md +0 -0
  84. /package/skills/{ui-testing → ui-testing-web}/references/tiers/t2-structured.md +0 -0
  85. /package/skills/{ui-testing → ui-testing-web}/templates/capability-note.md +0 -0
  86. /package/skills/{ui-testing → ui-testing-web}/templates/platform.md +0 -0
  87. /package/skills/{ui-testing → ui-testing-web}/templates/regression-case.md +0 -0
  88. /package/skills/{ui-testing → ui-testing-web}/templates/scenario.md +0 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Coordinate-space transforms (P0-C, M0 §3 family C).
3
+ *
4
+ * Pure affine 2D transforms between explicit coordinate spaces. Spaces are never assumed to be
5
+ * interchangeable: a mapping carries `from`, `to` and `geometryVersion`, and comparisons refuse
6
+ * mismatched spaces or geometry versions. No platform/geometry discovery happens here.
7
+ */
8
+ import { ERROR_CODES } from './contracts.js';
9
+ import { AdapterError } from './errors.js';
10
+
11
+ /** Every coordinate space the neutral model may talk about. */
12
+ export const COORDINATE_SPACES = Object.freeze([
13
+ 'document-css-px',
14
+ 'image-px',
15
+ 'viewport-css-px',
16
+ 'screen-logical-px',
17
+ 'screen-physical-px',
18
+ ]);
19
+
20
+ const SPACE_SET = new Set(COORDINATE_SPACES);
21
+
22
+ function isFiniteNumber(value) {
23
+ return typeof value === 'number' && Number.isFinite(value);
24
+ }
25
+
26
+ function assertSpace(space, field) {
27
+ if (typeof space !== 'string' || !SPACE_SET.has(space)) {
28
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `unknown coordinate space for ${field}`, {
29
+ details: { field, value: typeof space === 'string' ? space : null },
30
+ });
31
+ }
32
+ return space;
33
+ }
34
+
35
+ function assertNumber(value, field) {
36
+ if (!isFiniteNumber(value)) {
37
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `${field} must be a finite number`, {
38
+ details: { field, value: Number.isNaN(value) ? 'NaN' : typeof value },
39
+ });
40
+ }
41
+ return value;
42
+ }
43
+
44
+ function assertVersion(value) {
45
+ if (!Number.isInteger(value) || value < 0) {
46
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'geometryVersion must be a non-negative integer', {
47
+ details: { field: 'geometryVersion', value: Number.isFinite(value) ? value : null },
48
+ });
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function assertPoint(point, field = 'point') {
54
+ if (point === null || typeof point !== 'object') {
55
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `${field} must be an object with x/y`, {
56
+ details: { field },
57
+ });
58
+ }
59
+ return { x: assertNumber(point.x, `${field}.x`), y: assertNumber(point.y, `${field}.y`) };
60
+ }
61
+
62
+ function assertRect(rect, field = 'rect') {
63
+ if (rect === null || typeof rect !== 'object') {
64
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `${field} must be an object with x/y/width/height`, {
65
+ details: { field },
66
+ });
67
+ }
68
+ return {
69
+ x: assertNumber(rect.x, `${field}.x`),
70
+ y: assertNumber(rect.y, `${field}.y`),
71
+ width: assertNumber(rect.width, `${field}.width`),
72
+ height: assertNumber(rect.height, `${field}.height`),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * @param {{from:string, to:string, scaleX?:number, scaleY?:number, offsetX?:number,
78
+ * offsetY?:number, geometryVersion?:number}} input
79
+ * @returns {{from:string, to:string, scaleX:number, scaleY:number, offsetX:number,
80
+ * offsetY:number, geometryVersion:number}}
81
+ */
82
+ export function createTransform({
83
+ from,
84
+ to,
85
+ scaleX = 1,
86
+ scaleY = 1,
87
+ offsetX = 0,
88
+ offsetY = 0,
89
+ geometryVersion = 1,
90
+ } = {}) {
91
+ return {
92
+ from: assertSpace(from, 'from'),
93
+ to: assertSpace(to, 'to'),
94
+ scaleX: assertNumber(scaleX, 'scaleX'),
95
+ scaleY: assertNumber(scaleY, 'scaleY'),
96
+ offsetX: assertNumber(offsetX, 'offsetX'),
97
+ offsetY: assertNumber(offsetY, 'offsetY'),
98
+ geometryVersion: assertVersion(geometryVersion),
99
+ };
100
+ }
101
+
102
+ /** Identity mapping within one space. */
103
+ export function identityTransform(space, geometryVersion = 1) {
104
+ return createTransform({ from: space, to: space, geometryVersion });
105
+ }
106
+
107
+ function assertTransform(transform, field = 'transform') {
108
+ if (transform === null || typeof transform !== 'object') {
109
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `${field} must be a transform object`, {
110
+ details: { field },
111
+ });
112
+ }
113
+ return createTransform(transform);
114
+ }
115
+
116
+ /**
117
+ * Compose two transforms: `b` is applied to the output of `a`.
118
+ *
119
+ * Requires a continuous space chain (`a.to === b.from`) and the same geometry version; otherwise
120
+ * the result would silently mix incompatible observations.
121
+ */
122
+ export function composeTransforms(a, b) {
123
+ const first = assertTransform(a, 'a');
124
+ const second = assertTransform(b, 'b');
125
+ if (first.to !== second.from) {
126
+ throw new AdapterError(ERROR_CODES.CAPABILITY_UNSUPPORTED, 'cannot compose transforms across a space gap', {
127
+ details: { firstTo: first.to, secondFrom: second.from },
128
+ });
129
+ }
130
+ if (first.geometryVersion !== second.geometryVersion) {
131
+ throw new AdapterError(ERROR_CODES.CAPABILITY_UNSUPPORTED, 'cannot compose transforms from different geometry versions', {
132
+ details: { first: first.geometryVersion, second: second.geometryVersion },
133
+ });
134
+ }
135
+ return {
136
+ from: first.from,
137
+ to: second.to,
138
+ scaleX: first.scaleX * second.scaleX,
139
+ scaleY: first.scaleY * second.scaleY,
140
+ offsetX: first.offsetX * second.scaleX + second.offsetX,
141
+ offsetY: first.offsetY * second.scaleY + second.offsetY,
142
+ geometryVersion: first.geometryVersion,
143
+ };
144
+ }
145
+
146
+ /** Invert a transform. Zero scale is not invertible and reports CAPABILITY_UNSUPPORTED. */
147
+ export function invertTransform(transform) {
148
+ const source = assertTransform(transform);
149
+ if (source.scaleX === 0 || source.scaleY === 0) {
150
+ throw new AdapterError(ERROR_CODES.CAPABILITY_UNSUPPORTED, 'transform has zero scale and cannot be inverted', {
151
+ details: { scaleX: source.scaleX, scaleY: source.scaleY },
152
+ });
153
+ }
154
+ return {
155
+ from: source.to,
156
+ to: source.from,
157
+ scaleX: 1 / source.scaleX,
158
+ scaleY: 1 / source.scaleY,
159
+ offsetX: -source.offsetX / source.scaleX,
160
+ offsetY: -source.offsetY / source.scaleY,
161
+ geometryVersion: source.geometryVersion,
162
+ };
163
+ }
164
+
165
+ /** Map one point through a transform. */
166
+ export function mapPoint(transform, point) {
167
+ const source = assertTransform(transform);
168
+ const { x, y } = assertPoint(point);
169
+ return {
170
+ x: x * source.scaleX + source.offsetX,
171
+ y: y * source.scaleY + source.offsetY,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Map a rect through a transform. The result keeps non-negative width/height, so a flipped
177
+ * (negative-scale) mapping is still a usable rect.
178
+ */
179
+ export function mapRect(transform, rect) {
180
+ const source = assertTransform(transform);
181
+ const { x, y, width, height } = assertRect(rect);
182
+ const x1 = x * source.scaleX + source.offsetX;
183
+ const x2 = (x + width) * source.scaleX + source.offsetX;
184
+ const y1 = y * source.scaleY + source.offsetY;
185
+ const y2 = (y + height) * source.scaleY + source.offsetY;
186
+ return {
187
+ x: Math.min(x1, x2),
188
+ y: Math.min(y1, y2),
189
+ width: Math.abs(x2 - x1),
190
+ height: Math.abs(y2 - y1),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Assert two mappings describe the same observation geometry, so results may be compared.
196
+ *
197
+ * - Different `from`/`to` spaces -> CAPABILITY_UNSUPPORTED (cannot compare across spaces).
198
+ * - Different `geometryVersion` -> GENERATION_MISMATCH (geometry changed under the same spaces).
199
+ */
200
+ export function assertCompatibleMapping(a, b) {
201
+ const first = assertTransform(a, 'a');
202
+ const second = assertTransform(b, 'b');
203
+ if (first.from !== second.from || first.to !== second.to) {
204
+ throw new AdapterError(ERROR_CODES.CAPABILITY_UNSUPPORTED, 'mappings target different coordinate spaces', {
205
+ details: {
206
+ first: { from: first.from, to: first.to },
207
+ second: { from: second.from, to: second.to },
208
+ },
209
+ });
210
+ }
211
+ if (first.geometryVersion !== second.geometryVersion) {
212
+ throw new AdapterError(ERROR_CODES.GENERATION_MISMATCH, 'mappings belong to different geometry versions', {
213
+ details: { first: first.geometryVersion, second: second.geometryVersion },
214
+ });
215
+ }
216
+ return true;
217
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Neutral identity factories: scope, operation, target, session, generation.
3
+ *
4
+ * Rules (M0 §2.1 / §3):
5
+ * - Missing fields are `null`; identities never invent workspace ids, target ids or generations.
6
+ * - Operation summaries record shape only (types, lengths, counts), never argument plaintext.
7
+ * - Generation checks are explicit; a mismatch is a state error, not a silent retry.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { realpathSync } from 'node:fs';
11
+ import { resolve as resolvePath } from 'node:path';
12
+ import { ERROR_CODES } from './contracts.js';
13
+ import { AdapterError } from './errors.js';
14
+
15
+ function str(value) {
16
+ return typeof value === 'string' && value.length > 0 ? value : null;
17
+ }
18
+
19
+ function finiteOrNull(value) {
20
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
21
+ }
22
+
23
+ function integerOrNull(value) {
24
+ return Number.isInteger(value) ? value : null;
25
+ }
26
+
27
+ /** Summarize an argument bag without retaining any value plaintext. */
28
+ export function summarizeArgs(args) {
29
+ if (args === undefined) return { type: 'undefined' };
30
+ if (args === null) return { type: 'null' };
31
+ if (typeof args === 'string') return { type: 'string', length: args.length };
32
+ if (typeof args === 'number') return { type: 'number' };
33
+ if (typeof args === 'boolean') return { type: 'boolean' };
34
+ if (typeof args === 'bigint') return { type: 'bigint' };
35
+ if (typeof args === 'function') return { type: 'function' };
36
+ if (Array.isArray(args)) return { type: 'array', length: args.length };
37
+ if (typeof args === 'object') {
38
+ const keys = Object.keys(args);
39
+ return { type: 'object', keys: keys.length };
40
+ }
41
+ return { type: typeof args };
42
+ }
43
+
44
+ let operationCounter = 0;
45
+
46
+ function nextOperationId() {
47
+ operationCounter += 1;
48
+ return `op-${Date.now().toString(36)}-${operationCounter.toString(36)}`;
49
+ }
50
+
51
+ /**
52
+ * Stable workspace key shared by the config buckets and runtime entries.
53
+ *
54
+ * - Explicit workspace id wins: `workspace:<workspaceId>`.
55
+ * - Otherwise the normalized (realpath when possible) cwd is hashed: `path:<sha256 hex>`.
56
+ * - Missing identity returns null; nothing is invented.
57
+ *
58
+ * @param {{agent?:object, cwd?:string, workspaceId?:string}} [input]
59
+ * @returns {string|null}
60
+ */
61
+ export function workspaceKeyOf({ agent = null, cwd = null, workspaceId = null } = {}) {
62
+ const explicitId = typeof workspaceId === 'string' && workspaceId.length > 0
63
+ ? workspaceId
64
+ : (typeof agent?.workspaceId === 'string' && agent.workspaceId.length > 0 ? agent.workspaceId : null);
65
+ if (explicitId !== null) return `workspace:${explicitId}`;
66
+
67
+ const rawCwd = typeof cwd === 'string' && cwd.length > 0
68
+ ? cwd
69
+ : (typeof agent?.session?.header?.cwd === 'string' && agent.session.header.cwd.length > 0
70
+ ? agent.session.header.cwd
71
+ : null);
72
+ if (rawCwd === null) return null;
73
+
74
+ let normalized;
75
+ try {
76
+ normalized = realpathSync(rawCwd);
77
+ } catch {
78
+ normalized = resolvePath(rawCwd);
79
+ }
80
+ const digest = createHash('sha256').update(normalized).digest('hex');
81
+ return `path:${digest}`;
82
+ }
83
+
84
+ /**
85
+ * @param {{id?:string, kind?:string, workspaceId?:string, cwd?:string, sessionId?:string}} [scope]
86
+ * @returns {{id:string|null, kind:string|null, workspaceId:string|null, cwd:string|null,
87
+ * sessionId:string|null}}
88
+ */
89
+ export function newScopeIdentity(scope = {}) {
90
+ const source = scope && typeof scope === 'object' ? scope : {};
91
+ return {
92
+ id: str(source.id),
93
+ kind: str(source.kind),
94
+ workspaceId: str(source.workspaceId),
95
+ cwd: str(source.cwd),
96
+ sessionId: str(source.sessionId),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * @param {{tool?:string, args?:unknown, startedAt?:number|null, operationId?:string}} [input]
102
+ * @returns {{operationId:string, tool:string|null, argsSummary:object,
103
+ * startedAt:number|null, finishedAt:null}}
104
+ */
105
+ export function newOperation({ tool, args, startedAt, operationId } = {}) {
106
+ return {
107
+ operationId: str(operationId) ?? nextOperationId(),
108
+ tool: str(tool),
109
+ argsSummary: summarizeArgs(args),
110
+ startedAt: finiteOrNull(startedAt),
111
+ finishedAt: null,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * @param {{targetId?:string, environment?:string, kind?:string, generation?:number,
117
+ * label?:string, lifecycle?:string}} [input]
118
+ */
119
+ export function newTargetIdentity({
120
+ targetId,
121
+ environment,
122
+ kind,
123
+ generation,
124
+ label,
125
+ lifecycle,
126
+ } = {}) {
127
+ return {
128
+ targetId: str(targetId),
129
+ environment: str(environment),
130
+ kind: str(kind),
131
+ generation: integerOrNull(generation),
132
+ label: str(label),
133
+ lifecycle: str(lifecycle),
134
+ };
135
+ }
136
+
137
+ /**
138
+ * @param {{sessionId?:string, environment?:string, generation?:number,
139
+ * configRevision?:number, targetId?:string}} [input]
140
+ */
141
+ export function newSessionIdentity({
142
+ sessionId,
143
+ environment,
144
+ generation,
145
+ configRevision,
146
+ targetId,
147
+ } = {}) {
148
+ return {
149
+ sessionId: str(sessionId),
150
+ environment: str(environment),
151
+ generation: integerOrNull(generation),
152
+ configRevision: integerOrNull(configRevision),
153
+ targetId: str(targetId),
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Assert that a target/session is at the generation the caller observed.
159
+ *
160
+ * @param {{generation?:number}|number|null|undefined} target
161
+ * @param {number|null|undefined} expected
162
+ * @param {{code?:string}} [options]
163
+ * @returns {typeof target} the checked value, for chaining
164
+ */
165
+ export function assertGeneration(target, expected, { code = ERROR_CODES.GENERATION_MISMATCH } = {}) {
166
+ const actual = target !== null && typeof target === 'object'
167
+ ? integerOrNull(target.generation)
168
+ : integerOrNull(target);
169
+ const wanted = integerOrNull(expected);
170
+ if (actual !== wanted) {
171
+ throw new AdapterError(code, `generation mismatch: expected ${String(wanted)}, got ${String(actual)}`, {
172
+ details: {
173
+ expected: wanted,
174
+ actual,
175
+ targetId: target !== null && typeof target === 'object' ? str(target.targetId) : null,
176
+ },
177
+ });
178
+ }
179
+ return target;
180
+ }
package/core/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Public surface of the cross-environment core.
3
+ *
4
+ * Integration code imports from here; `core/` never imports index.js, targets/, browser-safety.js
5
+ * or any platform implementation. Every symbol below is a pure function, frozen data, or a class
6
+ * whose side effects are explicit (ConfigStore file I/O, AdapterRegistry.dispose).
7
+ */
8
+ export * from './contracts.js';
9
+ export * from './errors.js';
10
+ export * from './identity.js';
11
+ export * from './geometry.js';
12
+ export * from './clock.js';
13
+ export * from './evidence.js';
14
+ export * from './config.js';
15
+ export * from './config-store.js';
16
+ export * from './registry.js';
17
+ export * from './readiness.js';
18
+ // P0 neutral models live in core/p0.js (separate write scope); the barrel exposes them so
19
+ // adapters/providers have one import surface. No symbol names collide with the modules above.
20
+ export * from './p0.js';
21
+ // Team/page access model (M1): Principal, page-group keys, operation effects, control state
22
+ // machine. Integration code imports it from this barrel; the module itself imports nothing but
23
+ // `contracts.js`/`errors.js`, so authorisation stays testable without a browser or a Host.
24
+ export * from './tab-access.js';